You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by wy...@apache.org on 2026/10/14 01:19:08 UTC

(maka) 08/14: feat(agent): restore persistent session checklists

This is an automated email from the ASF dual-hosted git repository.

M4n5ter pushed a commit to branch feat/runtime-host-rust
in repository https://gitbox.apache.org/repos/asf/maka.git

commit 8e5f8fcee2403cb7c63b43a9b5fa394ad46fe4b6
Author: Wang <m4...@gmail.com>
AuthorDate: Tue Oct 13 21:43:06 2026 +0800

    feat(agent): restore persistent session checklists
    
    Connect todo_read and todo_write to the existing session repository, original tool journal, ordinary Agent contract, and default product. Show bounded live progress and a read-only checklist panel in terminal and desktop presentations.
    
    Capture selected model context limits and compaction policy with accepted work, preserving full supported checklist inputs across Direct and Code Mode. Verify full-size receipts, isolation, cancellation, revision conflicts, and actual product restart.
---
 packages/plugin-sdk/src/configurations.ts          |  10 +
 packages/plugin-sdk/src/contracts.ts               | 196 +++++++++++++++
 packages/plugin-sdk/src/generated/runtime.ts       | 252 +++++++++++++++++++
 plugins/agent/agent-screen/actions.ts              |  27 +-
 plugins/agent/agent-screen/checklist-view.ts       | 111 ++++++++
 plugins/agent/agent-screen/events.ts               |   8 +-
 plugins/agent/agent-screen/frame.ts                |  43 +++-
 plugins/agent/agent-screen/index.ts                |   1 +
 plugins/agent/agent-screen/raw-events.ts           |  10 +
 plugins/agent/agent-screen/state.ts                |  10 +-
 plugins/agent/api/src/contract.rs                  |  14 ++
 plugins/agent/api/src/contract_todo.rs             |  46 ++++
 plugins/agent/assistant/src/todo.rs                |   4 +-
 plugins/agent/native/README.md                     |   4 +
 plugins/agent/native/README.zh-CN.md               |   4 +
 plugins/agent/native/src/model.rs                  |  31 ++-
 plugins/agent/native/src/owner.rs                  |  14 +-
 plugins/agent/native/src/owner/queue.rs            |   1 +
 plugins/agent/native/src/owner/tests.rs            |   4 +
 plugins/agent/native/src/owner/tests/selection.rs  |  19 +-
 plugins/agent/native/src/owner/tests/todo.rs       | 279 +++++++++++++++++++++
 plugins/agent/native/src/owner/todo.rs             | 255 +++++++++++++++++++
 plugins/agent/native/src/service.rs                |  19 ++
 plugins/shared/locale.ts                           |  21 ++
 products/maka/bundle/src/installation.ts           |   1 +
 products/maka/bundle/tests/task-workbench.test.mjs |  77 ++++++
 products/maka/cli/notices.json                     |  23 +-
 .../integration/terminal/bundled_product/chat.rs   |   1 +
 .../terminal/bundled_product/chat/todo.rs          | 142 +++++++++++
 tools/schema/src/lib.rs                            |   2 +
 30 files changed, 1600 insertions(+), 29 deletions(-)

diff --git a/packages/plugin-sdk/src/configurations.ts b/packages/plugin-sdk/src/configurations.ts
index 5805e80843..9afd392ad0 100644
--- a/packages/plugin-sdk/src/configurations.ts
+++ b/packages/plugin-sdk/src/configurations.ts
@@ -175,6 +175,11 @@ export const configurations = {
         type: 'boolean',
         default: false,
       },
+      todo: {
+        description: 'Enable the session-scoped, model-maintained checklist.',
+        type: 'boolean',
+        default: false,
+      },
       recall: {
         description: 'Explicit access to this Agent’s earlier conversations for model tools.',
         type: 'boolean',
@@ -654,6 +659,11 @@ export const configurations = {
             description: 'Enable model proposals and approved scheduled work for this Agent.',
             default: false,
           },
+          todo: {
+            type: 'boolean',
+            description: 'Enable the session-scoped, model-maintained checklist.',
+            default: false,
+          },
           recall: {
             type: 'boolean',
             description: 'Explicit access to this Agent’s earlier conversations for model tools.',
diff --git a/packages/plugin-sdk/src/contracts.ts b/packages/plugin-sdk/src/contracts.ts
index 4b35f63341..92e6c87073 100644
--- a/packages/plugin-sdk/src/contracts.ts
+++ b/packages/plugin-sdk/src/contracts.ts
@@ -7222,6 +7222,104 @@ export const contracts = {
           },
           resources: {},
         },
+        checklist: {
+          mode: 'query',
+          input: {
+            $schema: 'https://json-schema.org/draft/2020-12/schema',
+            title: 'AgentChecklistRead',
+            type: 'object',
+            properties: {
+              sessionId: {
+                type: 'string',
+              },
+              offset: {
+                type: 'integer',
+                format: 'uint',
+                minimum: 0,
+                maximum: 200,
+                default: 0,
+              },
+            },
+            additionalProperties: false,
+            required: ['sessionId'],
+          },
+          output: {
+            $schema: 'https://json-schema.org/draft/2020-12/schema',
+            title: 'AgentChecklist',
+            type: 'object',
+            properties: {
+              enabled: {
+                type: 'boolean',
+              },
+              revision: {
+                type: ['integer', 'null'],
+                format: 'uint64',
+                minimum: 0,
+              },
+              total: {
+                type: 'integer',
+                format: 'uint',
+                minimum: 0,
+              },
+              completed: {
+                type: 'integer',
+                format: 'uint',
+                minimum: 0,
+              },
+              current: {
+                anyOf: [
+                  {
+                    $ref: '#/$defs/AgentTodoItem',
+                  },
+                  {
+                    type: 'null',
+                  },
+                ],
+              },
+              offset: {
+                type: 'integer',
+                format: 'uint',
+                minimum: 0,
+              },
+              items: {
+                type: 'array',
+                items: {
+                  $ref: '#/$defs/AgentTodoItem',
+                },
+                maxItems: 20,
+              },
+              nextOffset: {
+                type: ['integer', 'null'],
+                format: 'uint',
+                minimum: 0,
+              },
+            },
+            additionalProperties: false,
+            required: ['enabled', 'total', 'completed', 'offset', 'items'],
+            $defs: {
+              AgentTodoItem: {
+                type: 'object',
+                properties: {
+                  content: {
+                    type: 'string',
+                    minLength: 1,
+                    maxLength: 200,
+                  },
+                  status: {
+                    $ref: '#/$defs/AgentTodoStatus',
+                  },
+                },
+                additionalProperties: false,
+                required: ['content', 'status'],
+              },
+              AgentTodoStatus: {
+                type: 'string',
+                enum: ['pending', 'in_progress', 'completed'],
+              },
+            },
+          },
+          resources: {},
+        },
         create: {
           mode: 'mutation',
           input: {
@@ -18610,6 +18708,104 @@ export const contracts = {
           },
           resources: {},
         },
+        watchChecklist: {
+          mode: 'subscription',
+          input: {
+            $schema: 'https://json-schema.org/draft/2020-12/schema',
+            title: 'AgentChecklistRead',
+            type: 'object',
+            properties: {
+              sessionId: {
+                type: 'string',
+              },
+              offset: {
+                type: 'integer',
+                format: 'uint',
+                minimum: 0,
+                maximum: 200,
+                default: 0,
+              },
+            },
+            additionalProperties: false,
+            required: ['sessionId'],
+          },
+          output: {
+            $schema: 'https://json-schema.org/draft/2020-12/schema',
+            title: 'AgentChecklist',
+            type: 'object',
+            properties: {
+              enabled: {
+                type: 'boolean',
+              },
+              revision: {
+                type: ['integer', 'null'],
+                format: 'uint64',
+                minimum: 0,
+              },
+              total: {
+                type: 'integer',
+                format: 'uint',
+                minimum: 0,
+              },
+              completed: {
+                type: 'integer',
+                format: 'uint',
+                minimum: 0,
+              },
+              current: {
+                anyOf: [
+                  {
+                    $ref: '#/$defs/AgentTodoItem',
+                  },
+                  {
+                    type: 'null',
+                  },
+                ],
+              },
+              offset: {
+                type: 'integer',
+                format: 'uint',
+                minimum: 0,
+              },
+              items: {
+                type: 'array',
+                items: {
+                  $ref: '#/$defs/AgentTodoItem',
+                },
+                maxItems: 20,
+              },
+              nextOffset: {
+                type: ['integer', 'null'],
+                format: 'uint',
+                minimum: 0,
+              },
+            },
+            additionalProperties: false,
+            required: ['enabled', 'total', 'completed', 'offset', 'items'],
+            $defs: {
+              AgentTodoItem: {
+                type: 'object',
+                properties: {
+                  content: {
+                    type: 'string',
+                    minLength: 1,
+                    maxLength: 200,
+                  },
+                  status: {
+                    $ref: '#/$defs/AgentTodoStatus',
+                  },
+                },
+                additionalProperties: false,
+                required: ['content', 'status'],
+              },
+              AgentTodoStatus: {
+                type: 'string',
+                enum: ['pending', 'in_progress', 'completed'],
+              },
+            },
+          },
+          resources: {},
+        },
         watchSchedules: {
           mode: 'subscription',
           input: {
diff --git a/packages/plugin-sdk/src/generated/runtime.ts b/packages/plugin-sdk/src/generated/runtime.ts
index fe8661310b..d853c6486d 100644
--- a/packages/plugin-sdk/src/generated/runtime.ts
+++ b/packages/plugin-sdk/src/generated/runtime.ts
@@ -205,6 +205,8 @@ export type RuntimeProtocol =
   | SkillPreparationRecord
   | AgentList
   | AgentCreate
+  | AgentChecklistRead
+  | AgentChecklist
   | AgentScheduleRead
   | AgentSchedules
   | AgentScheduleChange
@@ -2940,6 +2942,7 @@ export type AgentWorkspaceSelection =
       sessionId: string;
       kind: 'session';
     };
+export type AgentTodoStatus = 'pending' | 'in_progress' | 'completed';
 export type AgentScheduleRead =
   | {
       cursor?: string | null;
@@ -6490,6 +6493,10 @@ export interface AgentConfiguration {
    * Enable model proposals and approved scheduled work for this Agent.
    */
   scheduledTasks?: boolean;
+  /**
+   * Enable the session-scoped, model-maintained checklist.
+   */
+  todo?: boolean;
   /**
    * Explicit access to this Agent’s earlier conversations for model tools.
    */
@@ -8404,6 +8411,251 @@ export interface AgentProjectRef {
   id: string;
   revision: string;
 }
+export interface AgentChecklistRead {
+  sessionId: string;
+  offset?: number;
+}
+export interface AgentChecklist {
+  enabled: boolean;
+  revision?: number | null;
+  total: number;
+  completed: number;
+  current?: AgentTodoItem | null;
+  offset: number;
+  /**
+   * @maxItems 20
+   */
+  items:
+    | []
+    | [AgentTodoItem]
+    | [AgentTodoItem, AgentTodoItem]
+    | [AgentTodoItem, AgentTodoItem, AgentTodoItem]
+    | [AgentTodoItem, AgentTodoItem, AgentTodoItem, AgentTodoItem]
+    | [AgentTodoItem, AgentTodoItem, AgentTodoItem, AgentTodoItem, AgentTodoItem]
+    | [AgentTodoItem, AgentTodoItem, AgentTodoItem, AgentTodoItem, AgentTodoItem, AgentTodoItem]
+    | [
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+      ]
+    | [
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+      ]
+    | [
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+      ]
+    | [
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+      ]
+    | [
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+      ]
+    | [
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+      ]
+    | [
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+      ]
+    | [
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+      ]
+    | [
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+      ]
+    | [
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+      ]
+    | [
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+      ]
+    | [
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+      ]
+    | [
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+      ]
+    | [
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+        AgentTodoItem,
+      ];
+  nextOffset?: number | null;
+}
+export interface AgentTodoItem {
+  content: string;
+  status: AgentTodoStatus;
+}
 export interface AgentScheduledSummary {
   id: string;
   revision: number;
diff --git a/plugins/agent/agent-screen/actions.ts b/plugins/agent/agent-screen/actions.ts
index 561eb82616..7b85b18f12 100644
--- a/plugins/agent/agent-screen/actions.ts
+++ b/plugins/agent/agent-screen/actions.ts
@@ -299,6 +299,8 @@ export function select(state: State, actions: Action[], session: AgentSession |
   state.recallAnchor = null;
   state.selected = session;
   state.epoch++;
+  state.checklist = { watch: null, page: null, failed: false };
+  if (session) watchChecklist(state);
   state.window = visit?.window ?? null;
   if (visit?.window) visit.following = false;
   state.source = visit?.source ?? null;
@@ -326,6 +328,18 @@ export function select(state: State, actions: Action[], session: AgentSession |
   if (session) observe(state, actions);
 }
 
+export function watchChecklist(state: State, offset = 0) {
+  if (!state.selected) return;
+  state.checklist = {
+    watch: {
+      key: `checklist-${++state.serial}`,
+      input: { sessionId: state.selected.id, offset },
+    },
+    page: null,
+    failed: false,
+  };
+}
+
 export function readHistorySource(state: State, actions: Action[], position: SourcePosition) {
   const source = state.source;
   if (!source || !state.selected) return;
@@ -668,6 +682,13 @@ export function rootCommands(
           },
         ]
       : []),
+    {
+      kind: 'checklist-open',
+      id: 'checklist-open',
+      label: text.checklistTitle,
+      hint: '',
+      disabled: !route || !state.selected || state.checklist.page?.enabled === false,
+    },
     {
       kind: 'details-open',
       id: 'details-open',
@@ -863,7 +884,11 @@ export function rootCommand(
     if (target === 'tui') state.inspector = state.inspector === 'terminal' ? null : 'terminal';
     else state.dialog = 'terminal';
   } else if (kind === 'skills-open') state.dialog = 'skills';
-  else if (kind === 'details-open') {
+  else if (kind === 'checklist-open') {
+    if (!state.checklist.watch) watchChecklist(state);
+    if (target === 'tui') state.inspector = state.inspector === 'checklist' ? null : 'checklist';
+    else state.dialog = 'checklist';
+  } else if (kind === 'details-open') {
     if (target === 'tui') state.inspector = state.inspector === 'details' ? null : 'details';
     else state.dialog = 'details';
   } else if (kind === 'usage-open' && state.selected) openUsage(state, state.selected);
diff --git a/plugins/agent/agent-screen/checklist-view.ts b/plugins/agent/agent-screen/checklist-view.ts
new file mode 100644
index 0000000000..6f7e0bb6fc
--- /dev/null
+++ b/plugins/agent/agent-screen/checklist-view.ts
@@ -0,0 +1,111 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import type { Theme } from '../../shared/contracts.js';
+import { copy } from '../../shared/locale.js';
+import { fill, type Node, type Presentation } from '../../shared/view.js';
+import type { State } from './state.js';
+
+export function checklistView<N extends Node>(
+  presentation: Presentation<N>,
+  state: State,
+  theme: Theme,
+  locale: string,
+): N {
+  const ui = presentation.paint(theme),
+    text = copy(locale),
+    page = state.checklist.page;
+  const open =
+    presentation.target === 'tui'
+      ? state.inspector === 'checklist' && state.dialog === null
+      : state.dialog === 'checklist';
+  return ui.box(
+    'checklist-panel',
+    [
+      ui.box(
+        'checklist-header',
+        [
+          ui.text('checklist-title', text.checklistTitle, 17, theme.text, 550),
+          ui.button('checklist-close', text.closeModels),
+        ],
+        { row: true, gap: 1 },
+      ),
+      ui.text('checklist-reported', text.checklistReported, 12, theme.muted),
+      ...(state.checklist.failed
+        ? [
+            ui.text('checklist-error', text.checklistFailed),
+            ui.button('checklist-retry', text.checklistRetry),
+          ]
+        : !page
+          ? [ui.text('checklist-loading', text.checklistLoading)]
+          : [
+              ui.text('checklist-count', `${page.completed}/${page.total}`, 13, theme.muted),
+              presentation.reading(
+                'checklist-reading',
+                {
+                  command: null,
+                  content: ui.box(
+                    'checklist-items',
+                    page.items.length
+                      ? page.items.map((item, index) =>
+                          ui.text(
+                            `checklist-item-${page.offset + index}`,
+                            `${item.status === 'completed' ? '[x]' : item.status === 'in_progress' ? '[>]' : '[ ]'} ${item.content}`,
+                            13,
+                            item.status === 'completed' ? theme.muted : theme.text,
+                            item.status === 'in_progress' ? 550 : 400,
+                          ),
+                        )
+                      : [ui.text('checklist-empty', text.checklistEmpty, 13, theme.muted)],
+                    {
+                      gap: 1,
+                      scroll: 'manual',
+                      terminal: { grow: 1 },
+                      style: { width: fill, height: fill },
+                    },
+                  ),
+                },
+                {},
+              ),
+              ...(page.total > 20
+                ? [
+                    ui.box(
+                      'checklist-pages',
+                      [
+                        ui.button('checklist-previous', text.previousModels, page.offset === 0),
+                        ui.text(
+                          'checklist-range',
+                          `${page.offset + 1}–${page.offset + page.items.length}/${page.total}`,
+                        ),
+                        ui.button('checklist-next', text.nextModelPage, page.nextOffset === null),
+                      ],
+                      { row: true, gap: 1 },
+                    ),
+                  ]
+                : []),
+            ]),
+    ],
+    {
+      visible: open,
+      gap: 1,
+      terminal: { grow: 1 },
+      style: { width: fill, height: fill },
+    },
+  );
+}
diff --git a/plugins/agent/agent-screen/events.ts b/plugins/agent/agent-screen/events.ts
index 98737d6c4a..870a740b2e 100644
--- a/plugins/agent/agent-screen/events.ts
+++ b/plugins/agent/agent-screen/events.ts
@@ -23,6 +23,7 @@ import type {
   AgentDeliveryPage,
   AgentScheduleAlerts,
   AgentSessionPage,
+  AgentChecklist,
   ComponentInput,
   ComponentValues,
   Emission,
@@ -178,7 +179,12 @@ export function handleEvent<N extends Node>(
     return frame(presentation, configuredLocale, state, values, actions);
   if (input.kind === 'action') {
     const outcome = input.event as ActionOutcome;
-    if (state.scheduleWatch === input.key) {
+    if (state.checklist.watch?.key === input.key) {
+      if (outcome.status === 'updated') state.checklist.page = outcome.value as AgentChecklist;
+      else if (outcome.status === 'failed' || outcome.status === 'not_accepted') {
+        state.checklist = { watch: null, page: null, failed: true };
+      }
+    } else if (state.scheduleWatch === input.key) {
       if (outcome.status === 'updated') state.scheduleAlerts = outcome.value as AgentScheduleAlerts;
       else if (outcome.status === 'failed' || outcome.status === 'not_accepted')
         state.scheduleWatch = null;
diff --git a/plugins/agent/agent-screen/frame.ts b/plugins/agent/agent-screen/frame.ts
index a15070f7da..327477f449 100644
--- a/plugins/agent/agent-screen/frame.ts
+++ b/plugins/agent/agent-screen/frame.ts
@@ -48,6 +48,7 @@ import {
   type Presentation,
 } from '../../shared/view.js';
 
+import { checklistView } from './checklist-view.js';
 import { attachmentView } from '../../shared/attachments.js';
 
 import { nextDraftStep, retainSelectedDraft } from '../../shared/drafts.js';
@@ -235,6 +236,7 @@ export function frame<N extends Node>(
   const inspectorOpen =
     conversationRoute(state) && !!state.selected && (state.inspector !== null || inlineInteraction);
   const inspectorSlots = [
+    checklistView(presentation, state, theme, locale),
     outputsView(presentation, state, theme, locale),
     presentation.slot(
       'terminal-panel',
@@ -376,6 +378,20 @@ export function frame<N extends Node>(
             ),
           ]
         : []),
+      ...(conversationRoute(state) &&
+      state.selected &&
+      state.checklist.page?.enabled &&
+      state.checklist.page.total > 0
+        ? [
+            ui.button(
+              'checklist-open',
+              `${text.checklistTitle} ${state.checklist.page.completed}/${state.checklist.page.total}${state.checklist.page.current ? ': ' + compactLabel(state.checklist.page.current.content, 80) : ''}`,
+              !!state.dialog,
+              false,
+              { align: 'start', width: fill },
+            ),
+          ]
+        : []),
       conversation,
       ...(target === 'tui' && (!state.embedding.fixed || state.embedding.session)
         ? [
@@ -472,13 +488,15 @@ export function frame<N extends Node>(
                     : text.approvalTitle
                 : state.dialog === 'directories'
                   ? text.directoryTitle
-                  : state.dialog === 'details'
-                    ? text.detailsTitle
-                    : state.dialog === 'queue'
-                      ? text.queueEdit
-                      : state.dialog === 'help'
-                        ? text.helpTitle
-                        : text.paletteTitle,
+                  : state.dialog === 'checklist'
+                    ? text.checklistTitle
+                    : state.dialog === 'details'
+                      ? text.detailsTitle
+                      : state.dialog === 'queue'
+                        ? text.queueEdit
+                        : state.dialog === 'help'
+                          ? text.helpTitle
+                          : text.paletteTitle,
       background,
       ui.box(
         'dialog-content',
@@ -630,6 +648,17 @@ export function frame<N extends Node>(
   out.actions = actions;
   out.events = events;
   out.subscriptions = [
+    ...(state.checklist.watch
+      ? [
+          {
+            ...state.checklist.watch,
+            binding: 'agent',
+            member: 0,
+            method: 'watchChecklist',
+            resources: {},
+          },
+        ]
+      : []),
     ...(state.scheduleWatch && !state.embedding.fixed
       ? [
           {
diff --git a/plugins/agent/agent-screen/index.ts b/plugins/agent/agent-screen/index.ts
index 4d47f1c391..acfce196f2 100644
--- a/plugins/agent/agent-screen/index.ts
+++ b/plugins/agent/agent-screen/index.ts
@@ -75,6 +75,7 @@ export function agentScreen<N extends Node>(
         scheduledTasks,
         web,
         jev,
+        checklist: { watch: null, page: null, failed: false },
         scheduleAlerts: { pending: 0, reminders: [] },
         scheduleWatch: scheduledTasks ? 'schedule-alerts-initial' : null,
         scheduleRequested: null,
diff --git a/plugins/agent/agent-screen/raw-events.ts b/plugins/agent/agent-screen/raw-events.ts
index 84405577f7..183fdfede4 100644
--- a/plugins/agent/agent-screen/raw-events.ts
+++ b/plugins/agent/agent-screen/raw-events.ts
@@ -38,6 +38,7 @@ import {
 import { draftReady } from '../../shared/drafts.js';
 
 import {
+  watchChecklist,
   catalog,
   catalogPending,
   checkInteraction,
@@ -251,6 +252,15 @@ export function handleRaw<N extends Node>(
     history(state, actions, { kind: 'tail' }, state.latestKnown, true);
   } else if (input.key === 'catalog-refresh') {
     if (!mutationPending(state) && !catalogPending(state)) catalog(state);
+  } else if (input.key === 'checklist-close') {
+    if (state.dialog === 'checklist') closeDialog(state);
+    if (state.inspector === 'checklist') state.inspector = null;
+  } else if (input.key === 'checklist-retry') {
+    watchChecklist(state);
+  } else if (input.key === 'checklist-next' && state.checklist.page?.nextOffset != null) {
+    watchChecklist(state, state.checklist.page.nextOffset);
+  } else if (input.key === 'checklist-previous' && state.checklist.page) {
+    watchChecklist(state, Math.max(0, state.checklist.page.offset - 20));
   } else if (input.key === 'inspector-close' && !state.dialog) {
     state.inspector = null;
   } else if (input.key === 'palette-open' && state.dialog === null) {
diff --git a/plugins/agent/agent-screen/state.ts b/plugins/agent/agent-screen/state.ts
index 026add76ea..87176bf92d 100644
--- a/plugins/agent/agent-screen/state.ts
+++ b/plugins/agent/agent-screen/state.ts
@@ -40,6 +40,7 @@ import type {
   AgentSessionPage,
   AgentList,
   AgentScheduleAlerts,
+  AgentChecklist,
   FileCapture,
   RecoveryRef,
   EditorCommand,
@@ -245,13 +246,18 @@ export interface State {
   scheduledTasks: boolean;
   web: boolean;
   jev: boolean;
+  checklist: {
+    watch: { key: string; input: { sessionId: string; offset: number } } | null;
+    page: AgentChecklist | null;
+    failed: boolean;
+  };
   scheduleAlerts: AgentScheduleAlerts;
   scheduleWatch: string | null;
   scheduleRequested: { sequence: number; taskId: string } | null;
   settingsCategory: SettingsCategory;
   insightsScope: InsightsProps['scope'];
   sidebar: { open: boolean } | { command: TerminalPaneCommand | null };
-  inspector: 'details' | 'terminal' | 'outputs' | null;
+  inspector: 'details' | 'terminal' | 'outputs' | 'checklist' | null;
   terminals: boolean;
   skills: boolean;
   place: string;
@@ -261,6 +267,7 @@ export interface State {
     | 'skills'
     | 'palette'
     | 'directories'
+    | 'checklist'
     | 'details'
     | 'help'
     | 'queue'
@@ -354,6 +361,7 @@ export type RootCommand =
   | 'model-setup'
   | 'terminal-open'
   | 'skills-open'
+  | 'checklist-open'
   | 'details-open'
   | 'usage-open'
   | 'create-workspace'
diff --git a/plugins/agent/api/src/contract.rs b/plugins/agent/api/src/contract.rs
index f197179fcc..caf90f3bec 100644
--- a/plugins/agent/api/src/contract.rs
+++ b/plugins/agent/api/src/contract.rs
@@ -29,6 +29,9 @@ use std::collections::BTreeMap;
 
 #[path = "contract_imports.rs"]
 mod imports;
+#[path = "contract_todo.rs"]
+mod todo;
+pub use todo::*;
 #[path = "contract_scheduler.rs"]
 mod scheduler;
 pub use scheduler::*;
@@ -67,6 +70,9 @@ pub struct Configuration {
     /// Enable model proposals and approved scheduled work for this Agent.
     #[serde(default)]
     pub scheduled_tasks: bool,
+    /// Enable the session-scoped, model-maintained checklist.
+    #[serde(default)]
+    pub todo: bool,
     /// Explicit access to this Agent’s earlier conversations for model tools.
     #[serde(default)]
     pub recall: bool,
@@ -1061,6 +1067,14 @@ pub fn contract() -> Contract {
                         resources: BTreeMap::new(),
                     },
                 ),
+                (
+                    "checklist".into(),
+                    method::<ChecklistRead, Checklist>(MethodMode::Query),
+                ),
+                (
+                    "watchChecklist".into(),
+                    method::<ChecklistRead, Checklist>(MethodMode::Subscription),
+                ),
                 (
                     "schedules".into(),
                     method::<ScheduleRead, Schedules>(MethodMode::Query),
diff --git a/plugins/agent/api/src/contract_todo.rs b/plugins/agent/api/src/contract_todo.rs
new file mode 100644
index 0000000000..82ac01dc59
--- /dev/null
+++ b/plugins/agent/api/src/contract_todo.rs
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+//! Bounded read-only presentation of the session's model-maintained checklist.
+use super::*;
+pub use maka_assistant::todo::{Item as TodoItem, Status as TodoStatus};
+
+#[derive(Clone, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "camelCase", deny_unknown_fields)]
+#[schemars(rename = "AgentChecklistRead")]
+pub struct ChecklistRead {
+    pub session_id: String,
+    #[serde(default)]
+    #[schemars(range(max = 200))]
+    pub offset: usize,
+}
+#[derive(Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "camelCase", deny_unknown_fields)]
+#[schemars(rename = "AgentChecklist")]
+pub struct Checklist {
+    pub enabled: bool,
+    pub revision: Option<u64>,
+    pub total: usize,
+    pub completed: usize,
+    pub current: Option<TodoItem>,
+    pub offset: usize,
+    #[schemars(length(max = 20))]
+    pub items: Vec<TodoItem>,
+    pub next_offset: Option<usize>,
+}
diff --git a/plugins/agent/assistant/src/todo.rs b/plugins/agent/assistant/src/todo.rs
index 4dafeaec9a..b3b299fa23 100644
--- a/plugins/agent/assistant/src/todo.rs
+++ b/plugins/agent/assistant/src/todo.rs
@@ -27,11 +27,12 @@ use std::sync::Arc;
 use tokio::sync::watch;
 
 pub const ID: &str = "maka.todo";
-const MAX_ITEMS: usize = 200;
+pub const MAX_ITEMS: usize = 200;
 const MAX_CONTENT_CHARS: usize = 200;
 
 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
 #[serde(deny_unknown_fields)]
+#[schemars(rename = "AgentTodoItem")]
 pub struct Item {
     #[schemars(length(min = 1, max = MAX_CONTENT_CHARS))]
     pub content: String,
@@ -39,6 +40,7 @@ pub struct Item {
 }
 #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
 #[serde(rename_all = "snake_case")]
+#[schemars(rename = "AgentTodoStatus")]
 pub enum Status {
     Pending,
     InProgress,
diff --git a/plugins/agent/native/README.md b/plugins/agent/native/README.md
index 55ec4bf37b..caff0d5ae0 100644
--- a/plugins/agent/native/README.md
+++ b/plugins/agent/native/README.md
@@ -31,4 +31,8 @@ The default Maka composition enables `toolDiscovery`: optional tool schemas load
 
 The independent `applyPatch` model override selects either patch editing or structured Write/Edit tools. Like Code Mode, it defaults on for GPT and DeepSeek models and the DeepSeek endpoint, and is frozen for queued work and continuations. Selection follows the bound Files contract and method even when a composition renames its tools. Read tools remain available, and all file operations retain their live permission checks. The connection settings model-overrides JSON accepts `codeMode` a [...]
 
+The default Maka composition enables `todo`: `todo_read` and `todo_write` maintain a persistent checklist for the current session, including in Code Mode and read-only workspaces. Writes replace the whole list with revision checks. The UI shows reported progress and a read-only Checklist panel, with bounded pages covering up to 200 items. Checklist completion is model-reported progress, not proof that an operation succeeded.
+
+Model context capacity, any independent input ceiling and the compaction threshold are captured with the accepted model selection and retained across queueing and restart. Without an explicit threshold, known input capacity sets the proactive threshold at 85%; output reserve remains a separate engine calculation.
+
 [Source](src/lib.rs) · [Cargo manifest](Cargo.toml)
diff --git a/plugins/agent/native/README.zh-CN.md b/plugins/agent/native/README.zh-CN.md
index b5018abbbb..6676233bab 100644
--- a/plugins/agent/native/README.zh-CN.md
+++ b/plugins/agent/native/README.zh-CN.md
@@ -31,4 +31,8 @@ Maka 默认组合启用 `toolDiscovery`:可选工具通过 `tool_search` 加
 
 独立的模型覆盖项 `applyPatch` 选择补丁编辑或 Write/Edit 结构化编辑。与 Code Mode 一样,GPT、DeepSeek 模型及 DeepSeek 端点默认启用;排队消息和恢复运行保留接收时的选择。筛选依据是实际绑定的 Files 契约和方法,工具改名不影响策略。读取工具保留,文件操作仍检查实时权限。连接设置的模型覆盖 JSON 可分别设置 `codeMode` 与 `applyPatch`。
 
+Maka 默认组合启用 `todo`:`todo_read` 和 `todo_write` 维护当前会话的持久清单,也可用于 Code Mode 和只读工作区。写入会校验版本并替换整个列表。UI 显示已报告的进度和只读「清单」面板,以有界分页浏览最多 200 项。清单完成状态是模型报告的进度,不代表操作已成功的执行证据。
+
+模型上下文总容量、独立输入上限与压缩阈值随模型选择一起保存,排队和重启后继续使用接收时的值。未显式设置压缩阈值时,使用已知输入容量的 85%;输出预留仍由引擎单独计算。
+
 [源码](src/lib.rs) · [Cargo 配置](Cargo.toml)
diff --git a/plugins/agent/native/src/model.rs b/plugins/agent/native/src/model.rs
index 36bbf97f03..67e77460da 100644
--- a/plugins/agent/native/src/model.rs
+++ b/plugins/agent/native/src/model.rs
@@ -25,6 +25,7 @@ use maka_agent_facts::composition::SourceRevision;
 use maka_agent_facts::configuration::ModelCapabilities;
 use maka_agent_facts::configuration::ModelInfo;
 use maka_agent_facts::configuration::ModelSelection;
+use maka_agent_facts::context::ModelRequestContext;
 use maka_agent_facts::execution::EditingTools;
 use maka_agent_facts::execution::ModelBinding;
 use maka_agent_facts::execution::ThinkingLevel;
@@ -78,6 +79,7 @@ pub(crate) struct AcceptedModel {
     /// None preserves the unfiltered tool selection of older accepted messages.
     #[serde(default, skip_serializing_if = "Option::is_none")]
     pub editing_tools: Option<EditingTools>,
+    context: ModelRequestContext,
     provider: maka_agent_facts::provider::Identity,
     provider_configuration: Value,
     basis: ExecutionBasis,
@@ -118,6 +120,7 @@ impl Source {
             configuration,
             credentials,
             accepted: AcceptedModel {
+                context: request_context(row, &model.info)?,
                 editing_tools: Some(EditingTools::for_model(
                     &selection.model_id,
                     &model.base_url,
@@ -240,13 +243,39 @@ impl Source {
                     .map(|value| value.as_object().expect("validated overlay").clone()),
             },
             options: model.provider_options,
-            context: None,
+            context: Some(self.accepted.context.clone()),
             main_output_limit: model.main_output_limit,
             supports_vision: capabilities.vision.unwrap_or(false),
             revision: self.revision.clone(),
         })
     }
 }
+fn request_context(
+    row: &maka_agent_facts::configuration::ConnectionCatalogEntry,
+    model: &ModelInfo,
+) -> Result<ModelRequestContext, RunError> {
+    if matches!((model.context_window, model.input_limit), (Some(capacity), Some(input)) if input > capacity)
+    {
+        return Err(unavailable());
+    }
+    let input = match (model.context_window, model.input_limit) {
+        (Some(capacity), Some(input)) => Some(capacity.min(input)),
+        (capacity, input) => capacity.or(input),
+    };
+    let context = ModelRequestContext {
+        provider_id: row.provider.name.clone(),
+        context_window: input,
+        model_context_window: model.context_window,
+        declared_window: row
+            .model_overrides
+            .as_ref()
+            .and_then(|models| models.get(&model.id))
+            .and_then(|profile| profile.compaction_threshold)
+            .or_else(|| input.map(|limit| ((u128::from(limit) * 85 / 100) as u64).max(1))),
+    };
+    context.validate().map_err(|_| unavailable())?;
+    Ok(context)
+}
 impl ModelSource for Source {
     fn capture(&self) -> BoxFuture<'_, Result<PreparedModel, RunError>> {
         Box::pin(async move {
diff --git a/plugins/agent/native/src/owner.rs b/plugins/agent/native/src/owner.rs
index f52c9ae125..d548303497 100644
--- a/plugins/agent/native/src/owner.rs
+++ b/plugins/agent/native/src/owner.rs
@@ -36,6 +36,7 @@ mod queue;
 mod recall;
 mod scheduler;
 mod skills;
+mod todo;
 mod work;
 mod worktrees;
 use maka_agent_facts::composition::SourceKind;
@@ -113,6 +114,7 @@ pub(crate) struct Owner {
     pub store: Arc<EventLog>,
     pub recall: Arc<recall::Recall>,
     scheduler: Arc<scheduler::Scheduler>,
+    todo: Arc<todo::Todo>,
     pub settings: Configuration,
     pub prefix: String,
     pub data_source: DataSource,
@@ -250,6 +252,10 @@ impl Owner {
         if settings.scheduled_tasks {
             additional.extend(scheduler.tools());
         }
+        let todo = Arc::new(todo::Todo::new(persistence.store().clone(), prefix.clone()));
+        if settings.todo {
+            additional.extend(todo.tools());
+        }
         let internal_tools = additional
             .iter()
             .map(|tool| tool.definition.clone())
@@ -302,7 +308,8 @@ impl Owner {
             cells,
             Arc::new(crate::model::Prices(configuration.clone())),
         );
-        let owner = Arc::new(Self {
+        Ok(Arc::new(Self {
+            todo,
             scheduler,
             recall,
             store: persistence.store().clone(),
@@ -333,8 +340,7 @@ impl Owner {
             producers: TaskTracker::new(),
             producer_gate: Arc::default(),
             stop,
-        });
-        Ok(owner)
+        }))
     }
     pub fn operation_id(&self, operation: &maka_plugin_core::operation::RecoveryRef) -> String {
         // Use the actual complete original reference. UUIDs alone do not name
@@ -960,12 +966,14 @@ impl Owner {
             owner.workers.wait().await;
             owner.engine.drain().await;
             let scheduler = owner.scheduler.finish().await;
+            let todo = owner.todo.finish().await;
             let configuration = owner.configuration.shutdown().await;
             let runs = owner.runs.lock().await;
             if !runs.quarantined.is_empty() {
                 return Err("agent_cleanup_unconfirmed".into());
             }
             scheduler?;
+            todo?;
             configuration.map_err(|_| "agent_configuration_cleanup_unconfirmed".into())
         })
     }
diff --git a/plugins/agent/native/src/owner/queue.rs b/plugins/agent/native/src/owner/queue.rs
index 067239de41..d512a9e65e 100644
--- a/plugins/agent/native/src/owner/queue.rs
+++ b/plugins/agent/native/src/owner/queue.rs
@@ -40,6 +40,7 @@ pub(super) enum PreparedRun {
 impl Owner {
     pub fn activated(self: &Arc<Self>) {
         self.scheduler.bind(self);
+        self.todo.bind(self);
         self.start_schedules();
         let owner = self.clone();
         self.workers.spawn(async move { owner.schedule().await });
diff --git a/plugins/agent/native/src/owner/tests.rs b/plugins/agent/native/src/owner/tests.rs
index 317b9cc6ea..68421f334c 100644
--- a/plugins/agent/native/src/owner/tests.rs
+++ b/plugins/agent/native/src/owner/tests.rs
@@ -17,6 +17,9 @@
  * under the License.
  */
 
+#[path = "tests/todo.rs"]
+mod todo_tests;
+
 #[path = "tests/computer.rs"]
 mod computer;
 
@@ -515,6 +518,7 @@ impl Fixture {
             isolated_workspaces: false,
             assistant: None,
             scheduled_tasks: false,
+            todo: false,
             recall: false,
             import_roots: vec![maka_host_runtime::DirectoryRootSpec {
                 label: "Import sources".into(),
diff --git a/plugins/agent/native/src/owner/tests/selection.rs b/plugins/agent/native/src/owner/tests/selection.rs
index 94b3c65086..10afe4b9f7 100644
--- a/plugins/agent/native/src/owner/tests/selection.rs
+++ b/plugins/agent/native/src/owner/tests/selection.rs
@@ -719,12 +719,15 @@ async fn accepted_code_mode_survives_settings_changes_and_legacy_messages_remain
             ModelOverride {
                 code_mode: Some(true),
                 apply_patch: Some(false),
+                context_window: Some(1_000_000),
+                input_limit: Some(900_000),
+                compaction_threshold: Some(700_000),
                 ..Default::default()
             },
         )]));
     })
     .await;
-    let (source, _) = crate::model::Source::select(
+    let (source, prepared) = crate::model::Source::select(
         fixture.config.clone(),
         fixture.owner.credentials.clone(),
         fixture.selection().await,
@@ -739,6 +742,10 @@ async fn accepted_code_mode_survives_settings_changes_and_legacy_messages_remain
         source.accepted.editing_tools,
         Some(maka_agent_facts::execution::EditingTools::Structured)
     );
+    let context = prepared.context.unwrap();
+    assert_eq!(context.model_context_window, Some(1_000_000));
+    assert_eq!(context.context_window, Some(900_000));
+    assert_eq!(context.declared_window, Some(700_000));
     let stored = serde_json::to_value(&source.accepted).unwrap();
     edit(&fixture, |changes| {
         changes.model_overrides = Patch::Set(BTreeMap::from([(
@@ -746,6 +753,8 @@ async fn accepted_code_mode_survives_settings_changes_and_legacy_messages_remain
             ModelOverride {
                 code_mode: Some(false),
                 apply_patch: Some(true),
+                context_window: Some(128_000),
+                input_limit: Some(96_000),
                 ..Default::default()
             },
         )]));
@@ -759,13 +768,13 @@ async fn accepted_code_mode_survives_settings_changes_and_legacy_messages_remain
         fixture.owner.revision.clone(),
     )
     .unwrap();
-    restored.capture().await.unwrap();
+    assert_eq!(restored.capture().await.unwrap().context, Some(context));
     assert_eq!(restored.accepted.tool_mode, ToolMode::CodeMode);
     assert_eq!(
         restored.accepted.editing_tools,
         Some(maka_agent_facts::execution::EditingTools::Structured)
     );
-    let (next, _) = crate::model::Source::select(
+    let (next, prepared) = crate::model::Source::select(
         fixture.config.clone(),
         fixture.owner.credentials.clone(),
         fixture.selection().await,
@@ -775,6 +784,10 @@ async fn accepted_code_mode_survives_settings_changes_and_legacy_messages_remain
     )
     .await
     .unwrap();
+    let context = prepared.context.unwrap();
+    assert_eq!(context.model_context_window, Some(128_000));
+    assert_eq!(context.context_window, Some(96_000));
+    assert_eq!(context.declared_window, Some(81_600));
     assert_eq!(next.accepted.tool_mode, ToolMode::Direct);
     assert_eq!(
         next.accepted.editing_tools,
diff --git a/plugins/agent/native/src/owner/tests/todo.rs b/plugins/agent/native/src/owner/tests/todo.rs
new file mode 100644
index 0000000000..e4dcce37d2
--- /dev/null
+++ b/plugins/agent/native/src/owner/tests/todo.rs
@@ -0,0 +1,279 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+use super::*;
+use maka_agent_facts::{
+    event::{Fact, ToolOutcome},
+    tool_call::{ToolCallIdentity, ToolRejection},
+    tools::{ToolCallContext, ToolJournal, ToolPreparer},
+};
+
+async fn fixture(base: &str) -> Fixture {
+    Fixture::configured(base, 1, None, None, None, |settings| {
+        settings.todo = true;
+        settings.max_steps = 3;
+        settings.permissions.access = WorkspaceAccess::ReadOnly;
+    })
+    .await
+}
+fn last_tool(sent: &Value) -> &str {
+    let messages = sent["messages"].as_array().unwrap();
+    let tool = messages
+        .iter()
+        .rev()
+        .find(|message| message["role"] == "tool")
+        .unwrap_or_else(|| {
+            panic!(
+                "Expected a tool result; received: {:?}",
+                messages
+                    .iter()
+                    .map(|message| (
+                        message["role"].clone(),
+                        message["content"]
+                            .to_string()
+                            .chars()
+                            .take(1000)
+                            .collect::<String>()
+                    ))
+                    .collect::<Vec<_>>()
+            )
+        });
+    tool["content"].as_str().unwrap()
+}
+async fn page(f: &Fixture, session: &str, offset: usize) -> Value {
+    f.query("checklist", json!({"sessionId":session,"offset":offset}))
+        .await
+        .unwrap()
+}
+async fn reopen(f: &mut Fixture) {
+    let deployment = f.app.source().unwrap().deployment;
+    f.app.shutdown().await.unwrap();
+    let mut native = NativeRegistry::default();
+    maka_settings_native::provide(&mut native).unwrap();
+    provide_all(&mut native).unwrap();
+    f.app = Application::start_from_store(
+        f.store.clone(),
+        deployment,
+        native,
+        f.issuer.clone(),
+        Arc::new(Grant(f.issuer.clone(), None)),
+        Some(maka_host_js::factory(Default::default())),
+    )
+    .await
+    .unwrap();
+    f.app.ready().await.unwrap();
+}
+#[tokio::test]
+async fn todo_real_model_direct_and_code_mode_keep_full_list_isolation_and_restart() {
+    tokio::time::timeout(Duration::from_secs(90), async {
+        for code in [false, true] {
+            let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
+            let mut f = fixture(&format!("http://{}/v1", listener.local_addr().unwrap())).await;
+            selection::edit(&f, |changes| {
+                changes.model_overrides = Patch::Set(BTreeMap::from([("fixture-model".into(), ModelOverride { code_mode: Some(code), context_window: Some(1_000_000), input_limit: Some(900_000), ..Default::default() })]));
+            }).await;
+            let session = f.create("Checklist source").await;
+            let other = f.create("Separate checklist").await;
+            let items: Vec<Value> = (0..200).map(|index| json!({"content":format!("{index:03} {}", "😀".repeat(196)),"status":if index == 0 { "completed" } else if index == 1 { "in_progress" } else { "pending" }})).collect();
+            f.send(&session).await.unwrap();
+            let (mut socket, _) = listener.accept().await.unwrap();
+            let sent = request(&mut socket).await;
+            let tool = if code { "exec" } else { "todo_write" };
+            assert!(sent["tools"].as_array().unwrap().iter().any(|t| t["function"]["name"] == tool));
+            let input = if code { json!({"code":"const todos = Array.from({length:200}, (_, index) => ({content:String(index).padStart(3, '0') + ' ' + '😀'.repeat(196), status:index === 0 ? 'completed' : index === 1 ? 'in_progress' : 'pending'})); await tools.todo_write({todos}); const read = await tools.todo_read({}); if (JSON.stringify(read.items) !== JSON.stringify(todos)) throw Error('read differs from complete write'); text(read.items.at(-1));"}) } else { json!({"todos":items}) };
+            service_tools::named_tool_response(&mut socket, tool, input).await;
+            let (mut socket, _) = listener.accept().await.unwrap();
+            let written = request(&mut socket).await;
+            if code {
+                assert!(last_tool(&written).contains("199"), "Code Mode must read the complete list");
+            } else {
+                // Ordinary large tool results use the existing bounded archive projection.
+                let archived: Value = serde_json::from_str(last_tool(&written)).unwrap();
+                assert_eq!(archived["kind"], "maka.archived_tool_result");
+                assert_eq!(archived["toolName"], "todo_write");
+                assert!(archived["page"]["next"].is_object());
+            }
+            if !code {
+                service_tools::named_tool_response(&mut socket,"todo_read",json!({})).await;
+                let (mut next, _) = listener.accept().await.unwrap();
+                let read = request(&mut next).await;
+                let archived: Value = serde_json::from_str(last_tool(&read)).unwrap();
+                assert_eq!(archived["kind"], "maka.archived_tool_result");
+                assert_eq!(archived["toolName"], "todo_read");
+                response(&mut next).await;
+            } else { response(&mut socket).await; }
+            f.drained().await;
+            let first = page(&f,&session,0).await;
+            assert_eq!(first["total"],200);
+            assert_eq!(first["completed"],1);
+            assert_eq!(first["current"],items[1]);
+            assert_eq!(first["items"],json!(items[..20]));
+            assert!(serde_json::to_vec(&first).unwrap().len() < 48 * 1024);
+            let mut all = vec![];
+            for offset in (0..200).step_by(20) {
+                let current = page(&f,&session,offset).await;
+                all.extend(current["items"].as_array().unwrap().iter().cloned());
+                assert_eq!(current["revision"],first["revision"]);
+            }
+            assert_eq!(all,items);
+            assert_eq!(page(&f,&other,0).await["total"],0);
+            assert!(f.query("checklist",json!({"sessionId":"foreign-session","offset":0})).await.is_err());
+            let facts = service_tools::facts(&f,&session).await;
+            let dispatched: Vec<_> = facts.iter().filter_map(|fact| match fact { Fact::ToolDispatched { name, operation_id, .. } if name == "todo_write" || name == "todo_read" => Some(operation_id), _ => None }).collect();
+            assert_eq!(dispatched.len(),2);
+            let original = f.store.scoped_prefix(maka_agent_facts::event::LogScope::Session { id: session.clone() }, 256, 4 * 1024 * 1024).await.unwrap();
+            for id in dispatched {
+                let event = original.events.iter().find(|event| matches!(&event.event.fact, Fact::ToolSettled { operation_id, .. } if operation_id == id)).unwrap();
+                let output = f.store.resolve_tool_result(&session, &event.event.id).await.unwrap();
+                let maka_agent_facts::tool_output::ToolOutput::Json(document) = output else { panic!("original checklist JSON is retained") };
+                assert_eq!(document["items"],json!(items), "every item survives in each original write/read receipt");
+                assert_eq!(facts.iter().filter(|fact| matches!(fact, Fact::ToolSettled { operation_id, outcome: ToolOutcome::Succeeded { .. } } if operation_id == id)).count(),1);
+            }
+            reopen(&mut f).await;
+            assert_eq!(page(&f,&session,0).await,first);
+            assert_eq!(page(&f,&other,0).await["total"],0);
+            f.app.shutdown().await.unwrap();
+            f.config.shutdown().await.unwrap();
+        }
+    }).await.unwrap();
+}
+
+#[tokio::test]
+async fn todo_prepared_replacements_conflict_and_cancelled_or_closed_invocations_cannot_write() {
+    tokio::time::timeout(Duration::from_secs(45), async {
+        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
+        let f = fixture(&format!("http://{}/v1", listener.local_addr().unwrap())).await;
+        let session = f.create("Concurrent checklist").await;
+        f.send(&session).await.unwrap();
+        let (mut socket, _) = listener.accept().await.unwrap();
+        request(&mut socket).await;
+        let invocation = f
+            .owner
+            .runs
+            .lock()
+            .await
+            .controls
+            .get(&session)
+            .unwrap()
+            .invocation
+            .clone();
+        let journal = ToolJournal::new(f.store.clone(), invocation.clone());
+        let context = |id: &str| ToolCallContext {
+            invocation: invocation.clone(),
+            operation_id: id.into(),
+        };
+        let input = |text: &str| json!({"todos":[{"content":text,"status":"in_progress"}]});
+        let first = f
+            .owner
+            .todo
+            .prepare(
+                "todo_write".into(),
+                input("First"),
+                context("first"),
+                CancellationToken::new(),
+            )
+            .await
+            .unwrap();
+        let second = f
+            .owner
+            .todo
+            .prepare(
+                "todo_write".into(),
+                input("Second"),
+                context("second"),
+                CancellationToken::new(),
+            )
+            .await
+            .unwrap();
+        for (id, text, prepared, success) in [
+            ("first", "First", first, true),
+            ("second", "Second", second, false),
+        ] {
+            let outcome = journal
+                .invoke_prepared_call(
+                    id.into(),
+                    ToolCallIdentity::standalone(id.into()),
+                    "todo_write".into(),
+                    input(text),
+                    CancellationToken::new(),
+                    prepared,
+                )
+                .await;
+            assert_eq!(outcome.is_ok(), success, "{outcome:?}");
+        }
+        let before = page(&f, &session, 0).await;
+        assert_eq!(before["items"][0]["content"], "First");
+        let token = CancellationToken::new();
+        token.cancel();
+        assert!(matches!(
+            f.owner
+                .todo
+                .prepare(
+                    "todo_write".into(),
+                    input("Cancelled"),
+                    context("cancelled"),
+                    token
+                )
+                .await,
+            Err(ToolRejection::Cancelled)
+        ));
+        let pending = f
+            .owner
+            .todo
+            .prepare(
+                "todo_write".into(),
+                input("Late"),
+                context("late"),
+                CancellationToken::new(),
+            )
+            .await
+            .unwrap();
+        response(&mut socket).await;
+        f.drained().await;
+        assert!(
+            f.owner
+                .todo
+                .prepare(
+                    "todo_write".into(),
+                    input("Closed"),
+                    context("closed"),
+                    CancellationToken::new()
+                )
+                .await
+                .is_err()
+        );
+        assert!(
+            journal
+                .invoke_prepared_call(
+                    "late".into(),
+                    ToolCallIdentity::standalone("late".into()),
+                    "todo_write".into(),
+                    input("Late"),
+                    CancellationToken::new(),
+                    pending
+                )
+                .await
+                .is_err()
+        );
+        assert_eq!(page(&f, &session, 0).await, before);
+        f.app.shutdown().await.unwrap();
+    })
+    .await
+    .unwrap();
+}
diff --git a/plugins/agent/native/src/owner/todo.rs b/plugins/agent/native/src/owner/todo.rs
new file mode 100644
index 0000000000..2b9948ad75
--- /dev/null
+++ b/plugins/agent/native/src/owner/todo.rs
@@ -0,0 +1,255 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+use super::*;
+use maka_agent_facts::{
+    tool_call::ToolRejection,
+    tool_output::{DurableToolProjection, ToolOutput, ToolSuccess},
+    tools::*,
+};
+use maka_assistant::todo::{Document, Item, MAX_ITEMS, Repository, Status};
+use maka_capabilities::storage::{Namespace, StoreError};
+use std::sync::{OnceLock, Weak};
+
+pub(super) struct Todo {
+    storage: Arc<maka_host_runtime::storage::Storage>,
+    repository: Repository,
+    owner: OnceLock<Weak<Owner>>,
+}
+impl Todo {
+    pub fn new(store: Arc<EventLog>, prefix: String) -> Self {
+        let storage = Arc::new(maka_host_runtime::storage::Storage::new(
+            store,
+            prefix,
+            Namespace::new(
+                maka_assistant::todo::ID,
+                maka_plugin_core::scope::Scope::Profile,
+            )
+            .expect("todo namespace"),
+        ));
+        Self {
+            repository: Repository {
+                store: storage.clone(),
+                changed: tokio::sync::watch::channel(()).0,
+            },
+            storage,
+            owner: OnceLock::new(),
+        }
+    }
+    pub fn bind(&self, owner: &Arc<Owner>) {
+        self.owner
+            .set(Arc::downgrade(owner))
+            .expect("todo bound once");
+    }
+    pub async fn finish(&self) -> Result<(), String> {
+        self.storage.finish().await
+    }
+    pub fn tools(self: &Arc<Self>) -> Vec<ToolRegistration> {
+        [
+            ("todo_read", "Read the complete current session checklist. Completed items are model-reported progress, not verified execution evidence.", serde_json::to_value(schemars::schema_for!(Read)).expect("todo schema")),
+            ("todo_write", "Atomically replace the complete current session checklist; include every item to keep. At most 200 items, each 1–200 characters. Completed is model-reported progress, not verified execution evidence.", serde_json::to_value(schemars::schema_for!(Write)).expect("todo schema")),
+        ].into_iter().map(|(name, description, input_schema)| ToolRegistration {
+            definition: ToolDefinition { provider: None, name: name.into(), description: description.into(), input_schema, output_schema: None, freeform: None },
+            nesting: ToolNesting::Nestable, semantics: ToolSemantics::Parallel, handler: ToolHandler::Prepared(self.clone()),
+        }).collect()
+    }
+}
+#[derive(Deserialize, schemars::JsonSchema)]
+#[serde(deny_unknown_fields)]
+struct Read {}
+#[derive(Deserialize, schemars::JsonSchema)]
+#[serde(deny_unknown_fields)]
+struct Write {
+    #[schemars(length(max = MAX_ITEMS))]
+    todos: Vec<Item>,
+}
+impl ToolPreparer for Todo {
+    fn names(&self) -> Vec<String> {
+        vec!["todo_read".into(), "todo_write".into()]
+    }
+    fn prepare(
+        &self,
+        name: String,
+        input: Value,
+        context: ToolCallContext,
+        cancellation: CancellationToken,
+    ) -> PreparationFuture {
+        let owner = self.owner.get().and_then(Weak::upgrade);
+        Box::pin(async move {
+            if cancellation.is_cancelled() {
+                return Err(ToolRejection::Cancelled);
+            }
+            let owner = owner.ok_or(ToolRejection::Unavailable)?;
+            let replacement = match name.as_str() {
+                "todo_read" => {
+                    serde_json::from_value::<Read>(input).map_err(invalid)?;
+                    None
+                }
+                "todo_write" => Some(
+                    Document::normalize(
+                        serde_json::from_value::<Write>(input)
+                            .map_err(invalid)?
+                            .todos,
+                    )
+                    .map_err(invalid)?,
+                ),
+                _ => return Err(ToolRejection::Unavailable),
+            };
+            owner
+                .check_todo_invocation(&context)
+                .await
+                .map_err(|message| ToolRejection::PreparationFailed { message })?;
+            // Freeze the observed revision before T1; two concurrent replacements cannot silently overwrite each other.
+            let expected = owner
+                .todo
+                .repository
+                .read(&context.invocation.session_id)
+                .await
+                .map_err(|error| ToolRejection::PreparationFailed {
+                    message: error.to_string(),
+                })?
+                .revision;
+            Ok(PreparedEffect::new(move |cancellation| {
+                Box::pin(async move {
+                    if cancellation.is_cancelled() {
+                        return Err(failed("Checklist call cancelled"));
+                    }
+                    let runs = owner.runs.lock().await;
+                    if runs.closing {
+                        return Err(failed("Checklist unavailable"));
+                    }
+                    let captured = owner.clone();
+                    let job = owner.workers.spawn(async move {
+                        captured
+                            .check_todo_invocation(&context)
+                            .await
+                            .map_err(failed)?;
+                        let session = &context.invocation.session_id;
+                        let document = match replacement {
+                            Some(document) => {
+                                captured
+                                    .todo
+                                    .repository
+                                    .replace(session, expected, &document)
+                                    .await
+                                    .map_err(storage_error)?;
+                                document
+                            }
+                            None => {
+                                captured
+                                    .todo
+                                    .repository
+                                    .read(session)
+                                    .await
+                                    .map_err(storage_error)?
+                                    .document
+                            }
+                        };
+                        let text = document.render();
+                        Ok(ToolSuccess::projected(
+                            ToolOutput::Json(serde_json::to_value(document).map_err(failed)?),
+                            DurableToolProjection::Text { text },
+                        ))
+                    });
+                    drop(runs);
+                    job.await
+                        .map_err(|error| ToolError::OutcomeUnknown(error.to_string()))?
+                })
+            }))
+        })
+    }
+}
+impl Owner {
+    async fn check_todo_invocation(&self, context: &ToolCallContext) -> Result<(), String> {
+        if !self.settings.todo || self.session(&context.invocation.session_id).await?.archived {
+            return Err("checklist_unavailable".into());
+        }
+        let boundary = self
+            .store
+            .invocation_boundary(
+                &context.invocation.session_id,
+                &context.invocation.invocation_id,
+            )
+            .await
+            .map_err(|e| e.to_string())?
+            .ok_or("checklist_invocation_unavailable")?
+            .0;
+        if boundary.invocation != context.invocation
+            || matches!(
+                boundary.state,
+                maka_event_log::turns::InvocationState::Ended { .. }
+            )
+        {
+            return Err("checklist_invocation_closed".into());
+        }
+        Ok(())
+    }
+    pub fn checklist_changes(&self) -> tokio::sync::watch::Receiver<()> {
+        self.todo.repository.changed.subscribe()
+    }
+    pub async fn checklist(&self, input: &ChecklistRead) -> Result<Checklist, String> {
+        self.session(&input.session_id).await?;
+        if input.offset > MAX_ITEMS {
+            return Err("checklist_offset_invalid".into());
+        }
+        let snapshot = self
+            .todo
+            .repository
+            .read(&input.session_id)
+            .await
+            .map_err(|e| e.to_string())?;
+        let items = if self.settings.todo {
+            snapshot.document.items
+        } else {
+            vec![]
+        };
+        let total = items.len();
+        let offset = input.offset.min(total.saturating_sub(1) / 20 * 20);
+        Ok(Checklist {
+            enabled: self.settings.todo,
+            revision: snapshot.revision,
+            total,
+            completed: items
+                .iter()
+                .filter(|item| item.status == Status::Completed)
+                .count(),
+            current: items
+                .iter()
+                .find(|item| item.status == Status::InProgress)
+                .cloned(),
+            offset,
+            items: items.into_iter().skip(offset).take(20).collect(),
+            next_offset: (offset + 20 < total).then_some(offset + 20),
+        })
+    }
+}
+fn invalid(error: impl std::fmt::Display) -> ToolRejection {
+    ToolRejection::InvalidInput {
+        message: error.to_string(),
+    }
+}
+fn failed(error: impl std::fmt::Display) -> ToolError {
+    ToolError::Failed(error.to_string())
+}
+fn storage_error(error: StoreError) -> ToolError {
+    match error {
+        StoreError::OutcomeUnknown(message) => ToolError::OutcomeUnknown(message),
+        other => failed(other),
+    }
+}
diff --git a/plugins/agent/native/src/service.rs b/plugins/agent/native/src/service.rs
index d966132049..462679a9fa 100644
--- a/plugins/agent/native/src/service.rs
+++ b/plugins/agent/native/src/service.rs
@@ -82,6 +82,25 @@ impl Handler for Agent {
                     }
                 }
                 "tools" => encode(owner.tool_names()),
+                "checklist" => encode(owner.checklist(&decode(input)?).await?),
+                "watchChecklist" => {
+                    let input: ChecklistRead = decode(input)?;
+                    let mut changes = owner.checklist_changes();
+                    let mut revision = None;
+                    loop {
+                        changes.borrow_and_update();
+                        let page = owner.checklist(&input).await?;
+                        if revision != Some(page.revision) {
+                            revision = Some(page.revision);
+                            context.emit(encode(page)?).await?;
+                        }
+                        tokio::select! {
+                            biased;
+                            _ = context.scope().cancellation.cancelled() => return Err("checklist_observation_cancelled".into()),
+                            result = changes.changed() => result.map_err(|_| "checklist_observation_closed")?,
+                        }
+                    }
+                }
                 "schedules" => encode(owner.schedules(decode(input)?).await?),
                 "watchSchedules" | "scheduleAlerts" => {
                     let mut changes = owner.schedule_changes();
diff --git a/plugins/shared/locale.ts b/plugins/shared/locale.ts
index f8552782e3..9440a838e1 100644
--- a/plugins/shared/locale.ts
+++ b/plugins/shared/locale.ts
@@ -24,6 +24,13 @@ export function resolveLocale(configured: string, native: unknown): string {
 }
 
 export const en = {
+  checklistTitle: 'Checklist',
+  checklistLoading: 'Loading checklist…',
+  checklistRetry: 'Retry',
+  checklistEmpty: 'No checklist yet. The agent keeps one here as it works.',
+  checklistReported: 'Progress reported by the agent.',
+  checklistFailed: 'Unable to load the checklist.',
+
   scheduleTitle: 'Scheduled tasks',
   scheduleNew: 'New task',
   scheduleEmpty: 'No scheduled tasks.',
@@ -959,6 +966,13 @@ export const en = {
 } as const;
 export type Copy = { [K in keyof typeof en]: string };
 const zhCN: Copy = {
+  checklistTitle: '清单',
+  checklistLoading: '正在加载清单…',
+  checklistRetry: '重试',
+  checklistEmpty: '还没有清单。智能体工作时会在这里维护。',
+  checklistReported: '进度由智能体报告。',
+  checklistFailed: '无法加载清单。',
+
   scheduleTitle: '定时任务',
   scheduleNew: '新建任务',
   scheduleEmpty: '暂无定时任务。',
@@ -1844,6 +1858,13 @@ const zhCN: Copy = {
   connecting: '正在连接…',
 };
 const zhTW: Copy = {
+  checklistTitle: '清單',
+  checklistLoading: '正在載入清單…',
+  checklistRetry: '重試',
+  checklistEmpty: '還沒有清單。智慧體工作時會在這裡維護。',
+  checklistReported: '進度由智慧體回報。',
+  checklistFailed: '無法載入清單。',
+
   scheduleTitle: '定時任務',
   scheduleNew: '新增任務',
   scheduleEmpty: '尚無定時任務。',
diff --git a/products/maka/bundle/src/installation.ts b/products/maka/bundle/src/installation.ts
index 5d5a1c467a..bbaae75327 100644
--- a/products/maka/bundle/src/installation.ts
+++ b/products/maka/bundle/src/installation.ts
@@ -116,6 +116,7 @@ export function installation(
     maxSteps: 16,
     recall: true,
     scheduledTasks: true,
+    todo: true,
     tools,
     skillLibrary: 'skills',
   };
diff --git a/products/maka/bundle/tests/task-workbench.test.mjs b/products/maka/bundle/tests/task-workbench.test.mjs
index 51ad1ec13c..e6196d7005 100644
--- a/products/maka/bundle/tests/task-workbench.test.mjs
+++ b/products/maka/bundle/tests/task-workbench.test.mjs
@@ -604,3 +604,80 @@ test('a settled stop observation clears the stopping notice and shows the canoni
   assert.equal(c.state.notice, null);
   assert.equal(c.find('task-state').text, 'Stopped');
 });
+
+test('the checklist follows its session, pages live progress, and rejects stale observations', () => {
+  const c = client()
+    .observe(1)
+    .history(page(1, [chunk(1, 'Working')]));
+  const firstWatch = c.out.subscriptions.find((watch) => watch.method === 'watchChecklist');
+  assert.equal(firstWatch.input.sessionId, 'Task A');
+  const items = Array.from({ length: 20 }, (_, index) => ({
+    content: `Step ${index + 1} 中文🙂`,
+    status: index === 0 ? 'completed' : index === 1 ? 'in_progress' : 'pending',
+  }));
+  const update = (watch, value) =>
+    c.event({
+      kind: 'action',
+      key: watch.key,
+      event: { status: 'updated', value },
+    });
+  const first = {
+    enabled: true,
+    revision: 1,
+    total: 25,
+    completed: 1,
+    current: items[1],
+    offset: 0,
+    items,
+    nextOffset: 20,
+  };
+  update(firstWatch, first);
+  assert.match(JSON.stringify(c.find('checklist-open')), /1\/25.*Step 2 中文🙂/u);
+  c.raw('checklist-open');
+  assert.equal(c.state.inspector, 'checklist');
+  assert.match(JSON.stringify(c.find('checklist-items')), /\[x\] Step 1 中文🙂/u);
+  assert.match(JSON.stringify(c.find('checklist-items')), /\[>\] Step 2 中文🙂/u);
+  c.raw('checklist-next');
+  const nextWatch = c.out.subscriptions.find((watch) => watch.method === 'watchChecklist');
+  assert.equal(nextWatch.input.offset, 20);
+  assert.notEqual(nextWatch.key, firstWatch.key);
+  update(firstWatch, first);
+  assert.equal(c.state.checklist.page, null);
+  update(nextWatch, {
+    ...first,
+    offset: 20,
+    items: [{ content: 'Final step 中文🙂', status: 'pending' }],
+    nextOffset: null,
+  });
+  assert.match(JSON.stringify(c.find('checklist-items')), /Final step 中文🙂/u);
+  c.select('Task B').observe(0).history(page(0, []));
+  const otherWatch = c.out.subscriptions.find((watch) => watch.method === 'watchChecklist');
+  assert.equal(otherWatch.input.sessionId, 'Task B');
+  update(nextWatch, first);
+  assert.equal(c.state.checklist.page, null);
+  update(otherWatch, {
+    enabled: true,
+    revision: null,
+    total: 0,
+    completed: 0,
+    current: null,
+    offset: 0,
+    items: [],
+    nextOffset: null,
+  });
+  assert.equal(c.find('checklist-open'), undefined);
+  c.raw('checklist-open');
+  assert.match(JSON.stringify(c.find('checklist-empty')), /No checklist yet/u);
+  c.event({
+    kind: 'action',
+    key: otherWatch.key,
+    event: { status: 'failed', message: 'disconnected' },
+  });
+  assert.match(JSON.stringify(c.find('checklist-error')), /Unable to load/u);
+  c.raw('checklist-retry');
+  assert.equal(
+    c.out.subscriptions.find((watch) => watch.method === 'watchChecklist').input.sessionId,
+    'Task B',
+  );
+  assert.equal(c.state.checklist.failed, false);
+});
diff --git a/products/maka/cli/notices.json b/products/maka/cli/notices.json
index 5f2da1917f..b1de050e6b 100644
--- a/products/maka/cli/notices.json
+++ b/products/maka/cli/notices.json
@@ -4,9 +4,9 @@
     "Cargo.toml": "003213e40354396554e46f94cb2e74f09cee28d0483461052f36f1202a308e5b",
     "package-lock.json": "a5af15aca21fabe0e1bdd48784167df4dc81cee17777d54861dcbd641168a4eb",
     "package.json": "38baf3beeb5876849ca16e31868d59bfaee769f260c7555b4a8a1847b92e28a1",
-    "packages/plugin-sdk/src/configurations.ts": "e34800933cb9f21ec818c673b89204fb9325098b6a744f2fcf51efc517db37f7",
-    "packages/plugin-sdk/src/contracts.ts": "7174bfdddb2a5374faa6c802cdf467d4ee8c08d850ce155892bee8891683eb29",
-    "packages/plugin-sdk/src/generated/runtime.ts": "b2da16c5556f76bdda22f572def61368fea50d4d79d30700bce6985ffa8ab6cb",
+    "packages/plugin-sdk/src/configurations.ts": "1aa59b78422414e59ee006c887b261c7ec44c2a6f081196dc85da2f9dd3c4ca2",
+    "packages/plugin-sdk/src/contracts.ts": "39a3e3c3939e9d077f8ec7f885797eed5283d592e36c7574b8fbc083c4fe6f78",
+    "packages/plugin-sdk/src/generated/runtime.ts": "8c0d99f1b84724b6d6215d63caa202b071c43d11e3525bf46bc0c6115685fe6f",
     "packages/plugin-sdk/src/index.ts": "27b68be5a29d1857f7fe361c8c678e77ba21413c79ec3870c1ce30d0faf9323f",
     "packages/plugin-sdk/src/native.ts": "5423d017d47817b1d262e28512f10638ac4351d9e1873743dc2f30ed30064e70",
     "packages/plugin-sdk/src/ui.ts": "d6c64e9406c41ad6bbda73435b559aad24c31adb3c5bbb4ab6f49fff8a8330ca",
@@ -43,22 +43,23 @@
     "plugins/agent/acp-executor/engine/tests/support/tui-auth.mjs": "b66436ace65b5874b6cd3ab66ece94984552c4a0772a0ae2d9b05db6b9f47d0f",
     "plugins/agent/acp-executor/native/Cargo.toml": "6a896cb424d361ef3a98efa56a92ffc8680156473822bd1d38374b60f3f1da66",
     "plugins/agent/acp-executor/package.ts": "9c055b7299b8d7eaf8c91bea7e684dd15164212ac2a6ef341322464bc88edfc5",
-    "plugins/agent/agent-screen/actions.ts": "f48ac028432f8d0689774dfeba4486a4188dac6cd27d5624daa1eb33129034c6",
+    "plugins/agent/agent-screen/actions.ts": "c8cea23c8e54370398d3a58519ab12eb0089d4832208581d2f16d49261879599",
+    "plugins/agent/agent-screen/checklist-view.ts": "a2787c2f37debb0349dd1ca298275164071f559244856f0a8be94b76014aabf0",
     "plugins/agent/agent-screen/component-events.ts": "c36419956786e9a08edd1b2c083e95c068858655804938a9cbdcf56c38eba2fe",
     "plugins/agent/agent-screen/conversation-view.ts": "9f315a832d2b6193cf01cc2f5cce2089c4aa92c92cc6f0635c84ba04430b3180",
     "plugins/agent/agent-screen/desktop.ts": "d64f976e965c124ab736edf11adc5d998d22d021c8e85753f88a963d16f2834f",
-    "plugins/agent/agent-screen/events.ts": "5b3a966990d8434729b2d01270aef5c57e7bdf42d08ab43c82e723c2002e3e10",
-    "plugins/agent/agent-screen/frame.ts": "aa1c21c3457c933266be55769f32cab99f87af1ba8305d647d7a5d4a32fabe06",
+    "plugins/agent/agent-screen/events.ts": "f99c2334f1b8ce80229c6261513923f196b80b92a2d41fecd3b05323db6b0e11",
+    "plugins/agent/agent-screen/frame.ts": "ae499e57343c43daa487303a0651fbf353c5b92a34a7db081c051fac6750178a",
     "plugins/agent/agent-screen/header-view.ts": "86c2aa17d0603eed5bf8df9742248fbd844407aac522f0c000e824f0fae0ef9a",
-    "plugins/agent/agent-screen/index.ts": "ef4ec3e1722887782d1d5f30b536ebb098abd026aa72e15067357ca3c23f98aa",
+    "plugins/agent/agent-screen/index.ts": "0408dbf16d67b298b8711632e5d0ba750cb3313fa92cfa04357ac8b0dbb20060",
     "plugins/agent/agent-screen/messages.ts": "e4bda48f40c1c6bfa9a216e4bca5d2d2a420d44cef0b59e83db5fab29f86f52f",
     "plugins/agent/agent-screen/navigation-view.ts": "5674c945ba8307387e295f8f398da61184dfcc662b1d10d9058b67c755a1096b",
     "plugins/agent/agent-screen/outputs-view.ts": "85458c8556b9a5da0c36e8a29b0c1208b4b9a0a7948732c990e8830d52490395",
     "plugins/agent/agent-screen/package.ts": "bc925efa98b53ff956ea7ec43b6d1b09e2a740b3b9a0525e4bef8e0877c44018",
     "plugins/agent/agent-screen/pages-view.ts": "2102dea3cc5c739fed04f6e4f0d17d1d22ac01c25fae924a5e46baad972e95b3",
-    "plugins/agent/agent-screen/raw-events.ts": "48918ee5c86e9b70c8ed2ff9f69226fbab2971c7a7bf1f5acb0e7c48a46eec99",
+    "plugins/agent/agent-screen/raw-events.ts": "3ab7b2a17f7b84cfab324582677ab5c951402bbc7722ab386274a27079e5755b",
     "plugins/agent/agent-screen/results.ts": "264d34893dbfe4f68091fef06f8d74c97366212603d0475ca36d073d5d685cb0",
-    "plugins/agent/agent-screen/state.ts": "605a9ab488da78be5d4cc37ea1ec7d5c527cbff03a784a2f074c5a99e583b64d",
+    "plugins/agent/agent-screen/state.ts": "55bcb20f67d5128680c53c84ba67819a3807bfa685f836b101479dbbe42dfcaf",
     "plugins/agent/agent-screen/task-overview.ts": "4ceca90536fcc91619571d8d46caab33146ba6298f88bd85a626cc995c4acb27",
     "plugins/agent/agent-screen/tui.ts": "c56f6d0f7e2382f67aef31a486b4524073a401d0ddc4dae4b92c7aaf9a7933db",
     "plugins/agent/agent/package.ts": "b0124f84e1701973067f3d6d38b40f71cc5f6cd7b80a5c5d93744e957603abd9",
@@ -280,7 +281,7 @@
     "plugins/shared/deployment-import.ts": "a5f00ccb328b628c43147e73a70bc8483b94eba19010ff2575025ac23b70feb9",
     "plugins/shared/drafts.ts": "0f0defa0085af58539a7deaabe09450e3f3effdb9894e0e745f2aab79cb8bf5d",
     "plugins/shared/instance-settings.ts": "36efaddf311497b81edb84d071617e257153b0b560a16922c9a56c2201fb6535",
-    "plugins/shared/locale.ts": "a3c2090826fc6cdb11fcd2e5f492d7c1a92b18431c25371a400c40cb8a58f307",
+    "plugins/shared/locale.ts": "e9073d191ab6c4d9053c2a09eecdbbc6e901c22d887330cbe2a77d64e71b1327",
     "plugins/shared/message-copy.ts": "baad601787ef3b8db42aee852a80b74f71b38b80f2f61ac77512df82e42aa20c",
     "plugins/shared/message-view.ts": "955b89979758e69c80569e77fdd0dc24f11de7f40851266bef394731f4026a49",
     "plugins/shared/motion.ts": "b0c328cea67850426de62714b806a94f52056eab4438c2c3a48c8a8dd6ac41c2",
@@ -367,7 +368,7 @@
     "products/maka/bundle/package.json": "dd17e0a82ab68c03cdfd8adb3e243d4c5e881530db66ff311cb642b3bdc1f7c0",
     "products/maka/bundle/scripts/build.mjs": "db11d8cd6f29ef7ead087261848568da1ad3889a870a89f23fa969449d769d47",
     "products/maka/bundle/src/composition.ts": "f711ef5a92090bacaae0e5d49f425611e9ecd4ca64d01ff5411cdbb16633c2de",
-    "products/maka/bundle/src/installation.ts": "32feebe56ee1593f5481e4a82354ff53168e3681f37110ac47be7a36d0a72f8e",
+    "products/maka/bundle/src/installation.ts": "0f241f2f2d4f1a153d9ee70558106456fe4883df3dafcc25d4b4efc79b26c9dc",
     "products/maka/cli/Cargo.toml": "dff55e4f17eecb5107b75c179e0077f122409503993d19bd24c70e5c9380ffba",
     "products/maka/cli/build.rs": "98452274bb0f20fa55fda5a20313f81740e759652aa4227fba0b0de5e3f4cc29",
     "products/maka/cli/build/product.rs": "02555a9bd0181080a061fe35b1b1caabec6c2dc338a35923b49f59f010892a6c",
diff --git a/products/maka/cli/tests/integration/terminal/bundled_product/chat.rs b/products/maka/cli/tests/integration/terminal/bundled_product/chat.rs
index 66cb33f537..fc06639685 100644
--- a/products/maka/cli/tests/integration/terminal/bundled_product/chat.rs
+++ b/products/maka/cli/tests/integration/terminal/bundled_product/chat.rs
@@ -28,6 +28,7 @@ use uuid::Uuid;
 
 mod code_mode;
 mod computer;
+mod todo;
 
 async fn seed_connection(
     host: &Path,
diff --git a/products/maka/cli/tests/integration/terminal/bundled_product/chat/todo.rs b/products/maka/cli/tests/integration/terminal/bundled_product/chat/todo.rs
new file mode 100644
index 0000000000..b5619997a4
--- /dev/null
+++ b/products/maka/cli/tests/integration/terminal/bundled_product/chat/todo.rs
@@ -0,0 +1,142 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+use super::*;
+
+#[test]
+#[ignore = "real default product checklist model tools, paging and process restart"]
+fn default_product_checklist_tracks_model_progress_and_survives_restart() {
+    let listener = TcpListener::bind("127.0.0.1:0").unwrap();
+    let base = format!("http://{}/v1", listener.local_addr().unwrap());
+    let (requests, received) = mpsc::channel();
+    let server = std::thread::spawn(move || {
+        for _ in 0..4 {
+            let (mut socket, _) = listener.accept().unwrap();
+            let value = request(&mut socket);
+            socket.write_all(b"HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nTransfer-Encoding: chunked\r\nConnection: close\r\n\r\n").unwrap();
+            socket.flush().unwrap();
+            if requests.send((value, socket)).is_err() {
+                break;
+            }
+        }
+    });
+    let directory = tempfile::tempdir().unwrap();
+    let workspace = directory.path().join("workspace");
+    std::fs::create_dir(&workspace).unwrap();
+    let ui = directory.path().join("ui");
+    let host = ui.join("host");
+    let namespaces = RootNamespaces::for_current_account().unwrap();
+    drop(RootOwner::create(&ui, &namespaces).unwrap());
+    let runtime = tokio::runtime::Runtime::new().unwrap();
+    runtime.block_on(seed_connection(
+        &host,
+        &namespaces,
+        &base,
+        json!({"fixture-model":{"codeMode":true}}),
+    ));
+    let mut tui = Pty::spawn(&[
+        "application",
+        "launch",
+        "--root",
+        ui.to_str().unwrap(),
+        "--workspace",
+        workspace.to_str().unwrap(),
+        "--trust-workspace",
+        "--locale",
+        "en",
+    ]);
+    tui.wait_until_with_timeout(|s| s.contains("Settings"), Duration::from_secs(60));
+    tui.send(b"\x0b");
+    tui.wait_for("Commands");
+    tui.send(b"New conversation");
+    tui.wait_for("› New conversation");
+    tui.send(b"\r");
+    tui.wait_for("Message…");
+    click(&mut tui, "Message…");
+    paste(&mut tui, "Keep a checklist for these steps.");
+    tui.wait_for("Keep a checklist for these steps.");
+    tui.send(b"\r");
+    let (sent, mut socket) = next_request(&mut tui, &received);
+    assert!(
+        sent["tools"]
+            .as_array()
+            .unwrap()
+            .iter()
+            .any(|tool| tool["function"]["name"] == "exec")
+    );
+    let items: Vec<_> = (1..=25).map(|index| json!({"content":format!("Step {index:02} 中文🙂"),"status":if index == 1 { "completed" } else if index == 2 { "in_progress" } else { "pending" }})).collect();
+    code_mode::tool_response(
+        &mut socket,
+        "exec",
+        json!({"code":format!("await tools.todo_write({}); text(await tools.todo_read({{}}));",json!({"todos":items}))}),
+    );
+    let (result, mut socket) = next_request(&mut tui, &received);
+    assert!(result.to_string().contains("Step 25 中文🙂"), "{result}");
+    delta(&mut socket, "Checklist is ready.");
+    complete(&mut socket);
+    tui.wait_for("Checklist is ready.");
+    tui.wait_for("Checklist 1/25");
+    click(&mut tui, "Checklist 1/25");
+    tui.wait_for("[x] Step 01 中文🙂");
+    tui.wait_for("[>] Step 02 中文🙂");
+    click(&mut tui, "Next");
+    tui.wait_for("[ ] Step 25 中文🙂");
+    evidence(&tui, "checklist-page-two");
+    click(&mut tui, "Close");
+    tui.wait_for("Message…");
+    tui.send(b"\x11");
+    tui.finish();
+    let mut restored = Pty::spawn(&["application", "launch", "--root", ui.to_str().unwrap()]);
+    restored.wait_until_with_timeout(|s| s.contains("Checklist 1/25"), Duration::from_secs(60));
+    click(&mut restored, "Checklist 1/25");
+    restored.wait_for("[>] Step 02 中文🙂");
+    click(&mut restored, "Next");
+    restored.wait_for("[ ] Step 25 中文🙂");
+    evidence(&restored, "checklist-restarted");
+    click(&mut restored, "Close");
+    restored.send(b"\x0b");
+    restored.wait_for("Commands");
+    restored.send(b"New conversation");
+    restored.wait_for("› New conversation");
+    restored.send(b"\r");
+    restored.wait_until(|s| {
+        s.contains("Message…") && !s.contains("Checklist 1/25") && !s.contains("Restoring draft…")
+    });
+    click(&mut restored, "Message…");
+    paste(&mut restored, "Read this conversation checklist.");
+    restored.wait_for("Read this conversation checklist.");
+    restored.send(b"\r");
+    let (_, mut socket) = next_request(&mut restored, &received);
+    code_mode::tool_response(
+        &mut socket,
+        "exec",
+        json!({"code":"const list = await tools.todo_read({}); if (list.items.length !== 0) throw Error('another session leaked'); text('separate checklist is empty');"}),
+    );
+    let (result, mut socket) = next_request(&mut restored, &received);
+    assert!(
+        result.to_string().contains("separate checklist is empty"),
+        "{result}"
+    );
+    delta(&mut socket, "This conversation has no checklist.");
+    complete(&mut socket);
+    restored.wait_for("This conversation has no checklist.");
+    restored.send(b"\x11");
+    restored.finish();
+    server.join().unwrap();
+}
diff --git a/tools/schema/src/lib.rs b/tools/schema/src/lib.rs
index 25ea4c7f48..f56ee92185 100644
--- a/tools/schema/src/lib.rs
+++ b/tools/schema/src/lib.rs
@@ -226,6 +226,8 @@ pub fn catalog() -> Value {
         root::<maka_skills_api::contract::PreparationRecord>(&mut g),
         root::<maka_agent_api::contract::List>(&mut g),
         root::<maka_agent_api::contract::Create>(&mut g),
+        root::<maka_agent_api::contract::ChecklistRead>(&mut g),
+        root::<maka_agent_api::contract::Checklist>(&mut g),
         root::<maka_agent_api::contract::ScheduleRead>(&mut g),
         root::<maka_agent_api::contract::Schedules>(&mut g),
         root::<maka_agent_api::contract::ScheduleChange>(&mut g),


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org