You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by zh...@apache.org on 2026/08/31 16:44:21 UTC

(maka) branch main updated: feat(runtime): cut over SessionTodo to Runtime Host authority (#4351)

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

me2seeks pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/maka.git


The following commit(s) were added to refs/heads/main by this push:
     new 6b9d16fed1 feat(runtime): cut over SessionTodo to Runtime Host authority (#4351)
6b9d16fed1 is described below

commit 6b9d16fed1c41fce9e16d119d43c99996329d383
Author: Zhang <96...@users.noreply.github.com>
AuthorDate: Tue Sep 1 00:44:16 2026 +0800

    feat(runtime): cut over SessionTodo to Runtime Host authority (#4351)
    
    * feat(runtime): cut over session todos to host authority
    
    Replace the retired Task Ledger demand chain with one Host-owned current SessionTodo document across Runtime, CLI, and Desktop. Preserve one-time legacy bootstrap, exact copy and retirement semantics, and publish the protocol change at compatibility epoch 80.
    
    Refs #4338
    
    Generated-by: OpenAI Codex
    
    * docs(runtime): document blocked task todo migration
    
    Clarify that the one-time legacy bootstrap keeps blocked Task subjects visible as pending Todo items while dropping workflow-only metadata.
    
    Refs #4338
    
    Generated-by: OpenAI Codex
    
    * fix(desktop): inject session todo copy
    
    Generated-by: OpenAI Codex
    
    * test(desktop): assert flat session todo order
    
    Generated-by: OpenAI Codex
---
 apps/desktop/e2e/accessibility-coverage.spec.ts    |  16 +-
 apps/desktop/renderer-architecture.json            |   1 -
 .../runtime-host-client-operations.test.ts         |  79 +--
 .../main/__tests__/runtime-host-client-uds.test.ts |  20 +-
 .../runtime-host-session-domains-ipc-main.test.ts  |  16 +-
 .../__tests__/workbar-services-adapter.test.ts     |  12 +-
 apps/desktop/src/main/e2e-fixture.ts               |  40 +-
 apps/desktop/src/main/runtime-host-client.ts       |  38 +-
 .../main/runtime-host-session-domains-ipc-main.ts  |  13 +-
 apps/desktop/src/preload/bridge-contract.d.ts      |   8 +-
 apps/desktop/src/preload/preload.ts                |  12 +-
 .../desktop/src/renderer/features/workbar/ports.ts |  10 +-
 .../src/renderer/features/workbar/testing.ts       |   4 +-
 .../workbar/tools/tasks/use-session-tasks.ts       |  96 ----
 .../workbar/tools/tasks/use-session-todo.ts        |  92 ++++
 .../features/workbar/ui/workbar-surface.tsx        |  26 +-
 .../src/renderer/locales/conversation-copy.ts      |   3 +
 .../platform/desktop/create-workbar-services.ts    |   8 +-
 apps/desktop/stories/session-workbar.stories.tsx   |  12 +-
 docs/README.md                                     |   2 +-
 docs/astryx-surface-file-inventory.md              |   2 +-
 docs/astryx-surface-file-inventory.paths           |   2 +-
 docs/deep-research-durable-workspace.md            |   7 +-
 docs/session-task-ledger-lifecycle.md              | 236 ---------
 docs/session-todo-lifecycle.md                     | 176 +++++++
 packages/cli/src/__tests__/pi-transcript.test.ts   |  96 ++++
 packages/core/src/__tests__/session-todo.test.ts   |  13 +
 .../core/src/__tests__/tool-activity-args.test.ts  |   9 +
 .../core/src/__tests__/tool-quiet-preview.test.ts  |   9 +-
 packages/core/src/session-todo.ts                  |  27 +
 packages/core/src/tool-activity-args.ts            |   3 +
 packages/core/src/tool-quiet-preview.ts            |  10 +-
 .../src/__tests__/connection-session.test.ts       |   4 +-
 .../src/__tests__/execution-host-message.test.ts   |   6 -
 .../src/__tests__/execution-host-queue.test.ts     |   6 -
 .../src/__tests__/execution-host-recovery.test.ts  |   6 -
 .../src/__tests__/execution-host.test.ts           | 252 ++-------
 .../__tests__/execution-model-composition.test.ts  |  28 +-
 .../src/__tests__/fixtures/execution-host-suite.ts |   6 -
 .../src/__tests__/fixtures/uncooperative-host.ts   |   2 +-
 .../src/__tests__/goal-coordinator.test.ts         |  11 -
 .../src/__tests__/goal-root-authority.test.ts      |   1 -
 .../src/__tests__/interactive-run-composer.test.ts |   4 +-
 .../session-catalog-two-client-uds.test.ts         |  63 ++-
 .../session-continuity-coordinator.test.ts         |   6 +-
 .../session-retirement-coordinator.test.ts         |   4 +-
 .../session-revision-two-client-uds.test.ts        |  80 +--
 .../src/__tests__/session-todo-coordinator.test.ts |  96 ++++
 .../src/__tests__/session-todo-protocol.test.ts    |  50 ++
 .../src/__tests__/task-ledger-protocol.test.ts     | 225 --------
 packages/runtime-host/src/protocol/index.ts        |   7 +-
 packages/runtime-host/src/protocol/operations.ts   |   7 +-
 .../src/protocol/session-continuity.ts             |   2 +-
 packages/runtime-host/src/protocol/session-todo.ts |  70 +++
 packages/runtime-host/src/protocol/task-ledger.ts  | 425 ---------------
 .../src/server/child-agent-composition.ts          |   6 +-
 .../src/server/execution-composition.ts            |  43 +-
 .../runtime-host/src/server/goal-coordinator.ts    |  28 -
 .../src/server/interactive-run-composer.ts         |  20 +-
 .../src/server/operation-dispatcher.ts             |   5 +-
 .../src/server/session-retirement-coordinator.ts   |  10 +-
 .../src/server/session-revision-coordinator.ts     |  21 +-
 .../src/server/session-sidecar-purge.ts            |   6 +-
 .../src/server/session-todo-coordinator.ts         |  94 ++++
 .../src/server/task-ledger-coordinator.ts          | 249 ---------
 packages/runtime/package.json                      |   2 +-
 .../src/__tests__/session-todo-tools.test.ts       | 116 +++++
 .../runtime/src/__tests__/subagent-tools.test.ts   | 301 +----------
 .../src/__tests__/task-ledger-tools.test.ts        | 574 ---------------------
 packages/runtime/src/deep-research-tools.ts        |   6 +-
 packages/runtime/src/session-todo-tools.ts         | 102 ++++
 packages/runtime/src/subagent-tools.ts             | 100 +---
 packages/runtime/src/task-ledger-tools.ts          | 323 ------------
 packages/storage/package.json                      |   2 +-
 .../src/__tests__/public-entrypoints.test.ts       |   2 +-
 .../src/__tests__/session-todo-store.test.ts       | 146 +++++-
 packages/storage/src/session-todo-authority.ts     |   3 +-
 packages/storage/src/session-todo-store.ts         |  71 ++-
 packages/storage/src/storage-writer-composition.ts |  10 +-
 .../ui/src/__tests__/session-todo-panel.test.tsx   |  43 ++
 .../ui/src/__tests__/task-ledger-panel.test.ts     |  59 ---
 packages/ui/src/index.ts                           |   2 +-
 packages/ui/src/session-todo-panel.tsx             |  90 ++++
 packages/ui/src/shared-ui-copy.ts                  |  23 +-
 packages/ui/src/task-ledger-panel.tsx              | 236 ---------
 85 files changed, 1662 insertions(+), 3490 deletions(-)

diff --git a/apps/desktop/e2e/accessibility-coverage.spec.ts b/apps/desktop/e2e/accessibility-coverage.spec.ts
index 82cc6e3a89..322ed8d8fc 100644
--- a/apps/desktop/e2e/accessibility-coverage.spec.ts
+++ b/apps/desktop/e2e/accessibility-coverage.spec.ts
@@ -142,13 +142,18 @@ test('module pages and global overlays expose named actionable controls', async
   await page.keyboard.press('Escape');
 });
 
-test('data-backed conversation supports keyboard access to tools, models, tasks, and Graph', async ({
+test('data-backed conversation exposes ordered todos and keyboard access to tools, models, and Graph', async ({
   accessibilityNarrativeWindow: page,
 }) => {
   const cdp = await page.context().newCDPSession(page);
   await expect(page.getByRole('region', { name: /对话:/ })).toBeVisible();
-  await expect(page.getByRole('region', { name: '任务待办' })).toBeVisible();
-  await expect(page.getByText('补齐桌面端无障碍覆盖', { exact: true })).toBeVisible();
+  const todoRegion = page.getByRole('region', { name: '任务待办' });
+  await expect(todoRegion).toBeVisible();
+  await expect(todoRegion.getByRole('listitem')).toHaveText([
+    '补齐桌面端无障碍覆盖',
+    '核对模型选择器的键盘路径',
+    '确认工具结果可以展开阅读',
+  ]);
   await assertAxHealth(cdp, 'conversation/data-backed');
 
   await expect(page.getByRole('main')).toHaveCount(1);
@@ -192,11 +197,6 @@ test('data-backed conversation supports keyboard access to tools, models, tasks,
     graphPanel.getByRole('button', { name: '展开 Agent Graph' }),
   ).toHaveAttribute('aria-expanded', 'false');
   await assertAxHealth(cdp, 'conversation/agent-graph-empty');
-
-  const recentTasks = page.getByRole('button', { name: /最近结束/ });
-  await tabTo(page, recentTasks, 'recent tasks', 80);
-  await page.keyboard.press('Enter');
-  await expect(page.getByText('确认工具结果可以展开阅读', { exact: true })).toBeVisible();
 });
 
 test('toast and error states expose healthy live regions', async ({ window: page }) => {
diff --git a/apps/desktop/renderer-architecture.json b/apps/desktop/renderer-architecture.json
index 3cad453abf..9fa160354b 100644
--- a/apps/desktop/renderer-architecture.json
+++ b/apps/desktop/renderer-architecture.json
@@ -313,7 +313,6 @@
     "src/renderer/features/workbar/tools/side-chat/quote-companion-panel.tsx -> src/renderer/use-composer-attachments",
     "src/renderer/features/workbar/tools/side-chat/use-quote-companion.ts -> src/renderer/locales/conversation-copy",
     "src/renderer/features/workbar/tools/side-chat/use-quote-companion.ts -> src/renderer/settled-message-merge",
-    "src/renderer/features/workbar/tools/tasks/use-session-tasks.ts -> src/renderer/locales/shell-remaining-copy",
     "src/renderer/features/workbar/tools/terminal/session-terminal-panel.tsx -> src/renderer/locales/conversation-copy",
     "src/renderer/features/workbar/tools/terminal/session-terminal-panel.tsx -> src/renderer/theme",
     "src/renderer/features/workbar/ui/side-chat-close-confirmation.tsx -> src/renderer/locales/conversation-copy",
diff --git a/apps/desktop/src/main/__tests__/runtime-host-client-operations.test.ts b/apps/desktop/src/main/__tests__/runtime-host-client-operations.test.ts
index b318d3b464..7c20c3d9c5 100644
--- a/apps/desktop/src/main/__tests__/runtime-host-client-operations.test.ts
+++ b/apps/desktop/src/main/__tests__/runtime-host-client-operations.test.ts
@@ -704,26 +704,9 @@ test('streams Artifact content without mixing chunk offsets or totals', async ()
 });
 
 test('restarts Session sidecar reads when a paginated revision changes', async () => {
-  const taskRevisionOne = catalogRevision('3');
-  const taskRevisionTwo = catalogRevision('4');
   const resourceRevisionOne = catalogRevision('5');
   const resourceRevisionTwo = catalogRevision('6');
   const { client, requests } = clientWithResponses([
-    {
-      kind: 'page',
-      sessionId: 'session-1',
-      revision: taskRevisionOne,
-      tasks: [{ id: 'stale' }],
-      nextCursor: 'task-stale',
-    },
-    { kind: 'revision_changed', expected: taskRevisionOne, actual: taskRevisionTwo },
-    {
-      kind: 'page',
-      sessionId: 'session-1',
-      revision: taskRevisionTwo,
-      tasks: [{ id: 'fresh' }],
-      nextCursor: null,
-    },
     {
       kind: 'page',
       sessionId: 'session-1',
@@ -760,28 +743,11 @@ test('restarts Session sidecar reads when a paginated revision changes', async (
     },
   ]);
 
-  assert.deepEqual(
-    (await client.listTasks('session-1')).map((task) => task.id),
-    ['fresh'],
-  );
   const plan = await client.getPlanState('session-1');
   assert.equal(plan.storeVersion, 8);
   assert.equal(plan.latestProposalId, 'proposal-1');
   assert.equal(plan.proposals[0]?.proposalId, 'proposal-1');
   assert.equal((await client.listRuntimeResources('session-1'))[0]?.result.ref, 'shell:1');
-  assert.deepEqual(
-    requests.slice(0, 3).map(({ input }) => input),
-    [
-      { kind: 'list_start', sessionId: 'session-1' },
-      {
-        kind: 'list_continue',
-        sessionId: 'session-1',
-        revision: taskRevisionOne,
-        cursor: 'task-stale',
-      },
-      { kind: 'list_start', sessionId: 'session-1' },
-    ],
-  );
 });
 
 test('retries Goal clear only while the same Goal generation remains active', async () => {
@@ -905,31 +871,40 @@ test('arms a Goal in one request and reports a conflicting Goal instead of retry
   );
 });
 
-test('rejects an invalid sidecar continuation without misclassifying it as revision churn', async () => {
-  const revision = catalogRevision('7');
+test('rejects a SessionTodo projection for a different Session', async () => {
   const { client, requests } = clientWithResponses([
-    {
-      kind: 'page',
-      sessionId: 'session-1',
-      revision,
-      tasks: [],
-      nextCursor: 'next',
-    },
-    {
-      kind: 'page',
-      sessionId: 'session-other',
-      revision,
-      tasks: [],
-      nextCursor: null,
-    },
+    { sessionId: 'session-other', items: [] },
   ]);
 
   await assert.rejects(
-    () => client.listTasks('session-1'),
+    () => client.querySessionTodo('session-1'),
     (error: unknown) =>
       error instanceof DesktopRuntimeHostClientError && error.code === 'projection_unstable',
   );
-  assert.equal(requests.length, 2);
+  assert.equal(requests.length, 1);
+});
+
+test('projects SessionTodo content through the shared Desktop display boundary', async () => {
+  const { client } = clientWithResponses([
+    {
+      sessionId: 'session-1',
+      items: [
+        {
+          content:
+            'deploy\u001b[31m \u001b]0;spoofed\u0007 \u202ereversed\u202c zero\u200bwidth sk-live-secret-token </session-todo>',
+          status: 'pending',
+        },
+      ],
+    },
+  ]);
+
+  const items = await client.querySessionTodo('session-1');
+  assert.equal(items.length, 1);
+  assert.doesNotMatch(
+    items[0]!.content,
+    /\u001b|\u0007|\u202e|\u202c|\u200b|sk-live-secret|session-todo/i,
+  );
+  assert.match(items[0]!.content, /<redacted>|\[redacted\]/);
 });
 
 interface RecordedRequest {
diff --git a/apps/desktop/src/main/__tests__/runtime-host-client-uds.test.ts b/apps/desktop/src/main/__tests__/runtime-host-client-uds.test.ts
index ed11340b31..0eaf7775b2 100644
--- a/apps/desktop/src/main/__tests__/runtime-host-client-uds.test.ts
+++ b/apps/desktop/src/main/__tests__/runtime-host-client-uds.test.ts
@@ -451,23 +451,13 @@ test('drives bounded Session domain projections through real UDS framing', async
       idleGraceMs: 10_000,
       composition: defineInteractiveRuntimeHostComposition(async () => ({
         handlers: handlers({
-          'task.ledger.query': async (input) => ({
+          'session.todo.query': async (input) => ({
             ok: true,
             result: {
-              kind: 'page',
               sessionId: input.sessionId,
-              revision: catalogRevision('6'),
-              tasks: [
-                {
-                  id: 'task-1',
-                  key: 'T1',
-                  subject: 'Verify the Desktop adapter',
-                  status: 'in_progress',
-                  createdAt: 1,
-                  updatedAt: 2,
-                },
+              items: [
+                { content: 'Verify the Desktop adapter', status: 'in_progress' },
               ],
-              nextCursor: null,
             },
           }),
           'plan.query': async (input) => ({
@@ -523,8 +513,8 @@ test('drives bounded Session domain projections through real UDS framing', async
     );
 
     assert.equal(
-      ((await ipc.invoke('tasks:list', 'session-1')) as Array<{ id: string }>)[0]?.id,
-      'task-1',
+      ((await ipc.invoke('todo:read', 'session-1')) as Array<{ content: string }>)[0]?.content,
+      'Verify the Desktop adapter',
     );
     assert.deepEqual(await ipc.invoke('plan-mode:getState', 'session-1'), {
       schemaVersion: 1,
diff --git a/apps/desktop/src/main/__tests__/runtime-host-session-domains-ipc-main.test.ts b/apps/desktop/src/main/__tests__/runtime-host-session-domains-ipc-main.test.ts
index ee817a5027..0188653324 100644
--- a/apps/desktop/src/main/__tests__/runtime-host-session-domains-ipc-main.test.ts
+++ b/apps/desktop/src/main/__tests__/runtime-host-session-domains-ipc-main.test.ts
@@ -452,7 +452,7 @@ test('goal:arm takes the Session from the scoped channel and refuses any other k
 test('adapts Host Goal, Task, Deep Research, and Resource projections', async () => {
   const controls: unknown[] = [];
   const client = domainClient({
-    listTasks: async () => [{ id: 'task-1' }] as never,
+    querySessionTodo: async () => [{ content: 'todo-1', status: 'pending' }] as never,
     listRuntimeResources: async () => [{ sessionId: 'session-1', result: { ref: 'shell:1' } }] as never,
     queryGoal: async () => ({
       sessionId: 'session-1',
@@ -469,7 +469,7 @@ test('adapts Host Goal, Task, Deep Research, and Resource projections', async ()
   const ipc = ipcHarness();
   registerDomainsIpc({ client, emitModeChanged() {} }, ipc);
 
-  assert.equal(((await ipc.invoke('tasks:list', 'session-1')) as Array<{ id: string }>)[0]?.id, 'task-1');
+  assert.equal(((await ipc.invoke('todo:read', 'session-1')) as Array<{ content: string }>)[0]?.content, 'todo-1');
   assert.equal(
     ((await ipc.invoke('shell-runs:list', 'session-1')) as Array<{ result: { ref: string } }>)[0]
       ?.result.ref,
@@ -1004,7 +1004,7 @@ test('publishes typed invalidations and refreshes only changed Runtime Resources
     ipc,
   );
 
-  handle.sessionDomainChanged({ sessionId: 'session-1', domain: 'task' });
+  handle.sessionDomainChanged({ sessionId: 'session-1', domain: 'todo' });
   handle.sessionDomainChanged({ sessionId: 'session-1', domain: 'deep_research' });
   handle.sessionDomainChanged({ sessionId: 'session-1', domain: 'plan' });
   handle.sessionDomainChanged({ sessionId: 'session-1', domain: 'usage' });
@@ -1030,8 +1030,8 @@ test('publishes typed invalidations and refreshes only changed Runtime Resources
   assert.deepEqual(gets, [{ sessionId: 'session-1', ref: update.result.ref }]);
   assert.deepEqual(sent, [
     {
-      channel: 'tasks:changed',
-      payload: { sessionId: 'session-1', taskIds: [], at: 12 },
+      channel: 'todo:changed',
+      payload: { sessionId: 'session-1', at: 12 },
     },
     {
       channel: 'deepResearch:changed',
@@ -1073,8 +1073,8 @@ test('publishes typed invalidations and refreshes only changed Runtime Resources
   handle.sessionSubscriptionRecovered('session-1');
   assert.deepEqual(sent, [
     {
-      channel: 'tasks:changed',
-      payload: { sessionId: 'session-1', taskIds: [], at: 12 },
+      channel: 'todo:changed',
+      payload: { sessionId: 'session-1', at: 12 },
     },
     {
       channel: 'deepResearch:changed',
@@ -1181,7 +1181,7 @@ function domainClient(overrides: Partial<DomainClient>): DomainClient {
     listRuntimeResources: unavailable,
     listAgentGraphEpochs: unavailable,
     listCurrentAgentGraphEpochs: unavailable,
-    listTasks: unavailable,
+    querySessionTodo: unavailable,
     queryAgentGraph: unavailable,
     queryAgentGraphOperator: unavailable,
     queryDeepResearch: unavailable,
diff --git a/apps/desktop/src/main/__tests__/workbar-services-adapter.test.ts b/apps/desktop/src/main/__tests__/workbar-services-adapter.test.ts
index 2240b5febd..88a3d42449 100644
--- a/apps/desktop/src/main/__tests__/workbar-services-adapter.test.ts
+++ b/apps/desktop/src/main/__tests__/workbar-services-adapter.test.ts
@@ -33,7 +33,7 @@ function createBridgeRecorder(): {
     'sessions.subscribeEvents',
     'shellRuns.subscribePtyData',
     'shellRuns.subscribeResync',
-    'tasks.subscribeChanges',
+    'todo.subscribeChanges',
     'browser.setActiveSession',
     'browser.setViewport',
     'browser.onState',
@@ -70,7 +70,7 @@ function createBridgeRecorder(): {
       gitReview: domain('gitReview'),
       sessions: domain('sessions'),
       shellRuns: domain('shellRuns'),
-      tasks: domain('tasks'),
+      todo: domain('todo'),
       browser: domain('browser'),
       artifacts: domain('artifacts'),
       app: domain('app'),
@@ -122,8 +122,8 @@ describe('createDesktopWorkbarServices', () => {
     services.terminal.subscribePtyData(eventHandler)();
     services.terminal.subscribeResync(eventHandler)();
 
-    await services.tasks.list('s');
-    services.tasks.subscribeChanges(eventHandler)();
+    await services.todo.read('s');
+    services.todo.subscribeChanges(eventHandler)();
 
     services.browser.setActiveSession('s');
     services.browser.setViewport({ sessionId: 's', rect: null });
@@ -194,8 +194,8 @@ describe('createDesktopWorkbarServices', () => {
         'shellRuns.write',
         'shellRuns.subscribePtyData',
         'shellRuns.subscribeResync',
-        'tasks.list',
-        'tasks.subscribeChanges',
+        'todo.read',
+        'todo.subscribeChanges',
         'browser.setActiveSession',
         'browser.setViewport',
         'browser.navigate',
diff --git a/apps/desktop/src/main/e2e-fixture.ts b/apps/desktop/src/main/e2e-fixture.ts
index 4f06653bb2..e61e89f1c7 100644
--- a/apps/desktop/src/main/e2e-fixture.ts
+++ b/apps/desktop/src/main/e2e-fixture.ts
@@ -28,7 +28,7 @@ import {
   resolveStorageRoot,
   tryAcquireInteractiveRootOwner,
 } from '@maka/storage/root-authority';
-import { openInteractiveTaskLedgerStoreForWrite } from '@maka/storage/task-ledger-authority';
+import { openInteractiveSessionTodoStoreForWrite } from '@maka/storage/session-todo-authority';
 import { openInteractiveUsageStoresForWrite } from '@maka/storage/usage-stores';
 import {
   E2E_FIXTURE_NOW,
@@ -231,39 +231,17 @@ export async function seedE2eFixture(input: {
 
   if (scenario === 'turn-narrative' || scenario === 'turn-narrative-browser') {
     const owner = await tryAcquireInteractiveRootOwner(storageRoot);
-    if (!owner) throw new Error('Unable to acquire the E2E fixture task-ledger root');
+    if (!owner) throw new Error('Unable to acquire the E2E fixture SessionTodo root');
     try {
-      const tasks = await openInteractiveTaskLedgerStoreForWrite(owner.lease);
+      const todos = await openInteractiveSessionTodoStoreForWrite(owner.lease);
       try {
-        const created = await tasks.create(
-          TURN_SESSION_ID,
-          [
-            { subject: '补齐桌面端无障碍覆盖' },
-            { subject: '核对模型选择器的键盘路径' },
-            { subject: '确认工具结果可以展开阅读' },
-          ],
-          { source: 'import', actor: 'system' },
-        );
-        await tasks.update(
-          TURN_SESSION_ID,
-          created.created[0]!.id,
-          { status: 'in_progress' },
-          { source: 'import', actor: 'system' },
-        );
-        await tasks.update(
-          TURN_SESSION_ID,
-          created.created[2]!.id,
-          { status: 'in_progress' },
-          { source: 'import', actor: 'system' },
-        );
-        await tasks.update(
-          TURN_SESSION_ID,
-          created.created[2]!.id,
-          { status: 'completed', completionEvidence: '工具输出已成功显示。' },
-          { source: 'import', actor: 'system' },
-        );
+        await todos.replaceAll(TURN_SESSION_ID, [
+          { content: '补齐桌面端无障碍覆盖', status: 'in_progress' },
+          { content: '核对模型选择器的键盘路径', status: 'pending' },
+          { content: '确认工具结果可以展开阅读', status: 'completed' },
+        ]);
       } finally {
-        tasks.close();
+        todos.close();
       }
     } finally {
       await owner.close();
diff --git a/apps/desktop/src/main/runtime-host-client.ts b/apps/desktop/src/main/runtime-host-client.ts
index e2ffc31762..c95f8ac0fa 100644
--- a/apps/desktop/src/main/runtime-host-client.ts
+++ b/apps/desktop/src/main/runtime-host-client.ts
@@ -26,7 +26,10 @@ import {
   type TurnRecord,
 } from "@maka/core/session";
 import { markPersisted } from "@maka/core/persisted-value";
-import type { Task } from "@maka/core/task-ledger";
+import {
+  projectSessionTodoItemsForDisplay,
+  type SessionTodoItem,
+} from "@maka/core/session-todo";
 
 import type {
   ConnectionCatalogSnapshot,
@@ -1283,35 +1286,10 @@ export class DesktopRuntimeHostClient {
     return this.request("context.compact", input);
   }
 
-  async listTasks(sessionId: string): Promise<Task[]> {
-    const projection = await collectStableProjection({
-      name: "Task ledger",
-      sessionId,
-      start: () =>
-        this.request("task.ledger.query", { kind: "list_start", sessionId }),
-      continue: (first, cursor) =>
-        this.request("task.ledger.query", {
-          kind: "list_continue",
-          sessionId,
-          revision: first.revision,
-          cursor,
-        }),
-      page(result, first) {
-        if (
-          result.kind !== "page" ||
-          result.sessionId !== sessionId ||
-          (first !== undefined && result.revision !== first.revision)
-        ) {
-          throw invalidProjection("Task ledger");
-        }
-        return {
-          source: result,
-          items: result.tasks,
-          nextCursor: result.nextCursor,
-        };
-      },
-    });
-    return projection.items;
+  async querySessionTodo(sessionId: string): Promise<SessionTodoItem[]> {
+    const result = await this.request("session.todo.query", { sessionId });
+    if (result.sessionId !== sessionId) throw invalidProjection("SessionTodo");
+    return projectSessionTodoItemsForDisplay(result.items);
   }
 
   queryUsage(
diff --git a/apps/desktop/src/main/runtime-host-session-domains-ipc-main.ts b/apps/desktop/src/main/runtime-host-session-domains-ipc-main.ts
index e08afc2397..a68664aac7 100644
--- a/apps/desktop/src/main/runtime-host-session-domains-ipc-main.ts
+++ b/apps/desktop/src/main/runtime-host-session-domains-ipc-main.ts
@@ -63,7 +63,7 @@ type RuntimeHostSessionDomainClient = RuntimeHostShellRunsClient &
   | 'listRuntimeResources'
   | 'listAgentGraphEpochs'
   | 'listCurrentAgentGraphEpochs'
-  | 'listTasks'
+  | 'querySessionTodo'
   | 'queryAgentGraph'
   | 'queryAgentGraphOperator'
   | 'queryDeepResearch'
@@ -114,8 +114,8 @@ export function registerRuntimeHostSessionDomainsIpc(
     ipcMain,
   );
 
-  handleReconnectableRead(ipcMain, 'tasks:list', (_event, sessionId: unknown) =>
-    deps.client.listTasks(requiredId(sessionId, 'Session')),
+  handleReconnectableRead(ipcMain, 'todo:read', (_event, sessionId: unknown) =>
+    deps.client.querySessionTodo(requiredId(sessionId, 'Session')),
   );
   handleReconnectableRead(ipcMain, 'deepResearch:get', async (_event, sessionId: unknown) =>
     projectHostedDeepResearch(
@@ -329,10 +329,9 @@ export function registerRuntimeHostSessionDomainsIpc(
 
   const sessionDomainChanged = (change: SessionDomainChange): void => {
     switch (change.domain) {
-      case 'task':
-        deps.sendToRenderer?.('tasks:changed', {
+      case 'todo':
+        deps.sendToRenderer?.('todo:changed', {
           sessionId: change.sessionId,
-          taskIds: [],
           at: now(),
         });
         break;
@@ -363,7 +362,7 @@ export function registerRuntimeHostSessionDomainsIpc(
       deps.sendToRenderer?.('graphs:changed', event);
     },
     sessionSubscriptionRecovered(sessionId) {
-      sessionDomainChanged({ sessionId, domain: 'task' });
+      sessionDomainChanged({ sessionId, domain: 'todo' });
       sessionDomainChanged({ sessionId, domain: 'deep_research' });
       sessionDomainChanged({ sessionId, domain: 'plan' });
       sessionDomainChanged({ sessionId, domain: 'usage' });
diff --git a/apps/desktop/src/preload/bridge-contract.d.ts b/apps/desktop/src/preload/bridge-contract.d.ts
index 1525a4c34e..f8fbf7dcc3 100644
--- a/apps/desktop/src/preload/bridge-contract.d.ts
+++ b/apps/desktop/src/preload/bridge-contract.d.ts
@@ -95,7 +95,7 @@ import type {
 } from '@maka/core/daily-review';
 import type { WebSearchProvider, WebSearchResponse } from '@maka/core/web-search';
 import type { BrowserState, BrowserViewRect } from '@maka/core/browser';
-import type { Task, TaskLedgerChangedEvent } from '@maka/core/task-ledger';
+import type { SessionTodoItem } from '@maka/core/session-todo';
 import type { DeepResearchChangedEvent, DeepResearchClientProgress } from '@maka/core/deep-research-run';
 import type {
   DesktopTranscriptBatch,
@@ -961,9 +961,9 @@ export interface MakaBridge {
     subscribeChanges(handler: (event: WorkBoardChangedEvent) => void): () => void;
   };
 
-  tasks: {
-    list(sessionId: string): Promise<Task[]>;
-    subscribeChanges(handler: (event: TaskLedgerChangedEvent) => void): () => void;
+  todo: {
+    read(sessionId: string): Promise<SessionTodoItem[]>;
+    subscribeChanges(handler: (event: { sessionId: string; at: number }) => void): () => void;
   };
   deepResearch: {
     get(sessionId: string): Promise<DeepResearchClientProgress | undefined>;
diff --git a/apps/desktop/src/preload/preload.ts b/apps/desktop/src/preload/preload.ts
index 5a64dd317a..49227d46e9 100644
--- a/apps/desktop/src/preload/preload.ts
+++ b/apps/desktop/src/preload/preload.ts
@@ -174,7 +174,7 @@ import type {
 import type { WebSearchProvider, WebSearchResponse } from '@maka/core/web-search';
 import type { BrowserState, BrowserViewRect } from '@maka/core/browser';
 import { createBrowserSelectionCoordinator } from './browser-selection.js';
-import type { Task, TaskLedgerChangedEvent } from '@maka/core/task-ledger';
+import type { SessionTodoItem } from '@maka/core/session-todo';
 import type { DeepResearchChangedEvent, DeepResearchClientProgress } from '@maka/core/deep-research-run';
 import {
   isWebSearchProvider,
@@ -1724,12 +1724,12 @@ const makaBridge = {
       return () => ipcRenderer.off('workBoard:changed', listener);
     },
   },
-  tasks: {
-    list(sessionId: string): Promise<Task[]> {
-      return invokeProjectedSessionRuntimeHost('tasks:list', sessionId);
+  todo: {
+    read(sessionId: string): Promise<SessionTodoItem[]> {
+      return invokeProjectedSessionRuntimeHost('todo:read', sessionId);
     },
-    subscribeChanges(handler: (event: TaskLedgerChangedEvent) => void): () => void {
-      return subscribeEveryRuntimeHostEvent('tasks:changed', (scope, event: TaskLedgerChangedEvent) =>
+    subscribeChanges(handler: (event: { sessionId: string; at: number }) => void): () => void {
+      return subscribeEveryRuntimeHostEvent('todo:changed', (scope, event: { sessionId: string; at: number }) =>
         handler({
           ...event,
           sessionId: recordRuntimeHostSessionScope(scope, event.sessionId),
diff --git a/apps/desktop/src/renderer/features/workbar/ports.ts b/apps/desktop/src/renderer/features/workbar/ports.ts
index 8efcf71ace..e5ec333f16 100644
--- a/apps/desktop/src/renderer/features/workbar/ports.ts
+++ b/apps/desktop/src/renderer/features/workbar/ports.ts
@@ -41,7 +41,7 @@ import type {
   TurnRecord,
 } from '@maka/core/session';
 import type { SessionTrace } from '@maka/core/session-trace';
-import type { Task, TaskLedgerChangedEvent } from '@maka/core/task-ledger';
+import type { SessionTodoItem } from '@maka/core/session-todo';
 import type { UserQuestionResponse } from '@maka/core/user-question';
 import type { Result } from '@maka/core/result';
 import type { ContextDiagnosticsResult } from '@maka/runtime-host/protocol';
@@ -91,10 +91,10 @@ export interface WorkbarTerminalService {
   ): WorkbarUnsubscribe;
 }
 
-export interface WorkbarTasksService {
-  list(sessionId: string): Promise<Task[]>;
+export interface WorkbarTodoService {
+  read(sessionId: string): Promise<SessionTodoItem[]>;
   subscribeChanges(
-    handler: (event: TaskLedgerChangedEvent) => void,
+    handler: (event: { sessionId: string; at: number }) => void,
   ): WorkbarUnsubscribe;
 }
 
@@ -272,7 +272,7 @@ export interface SideChatSessionPort {
 export interface WorkbarServices {
   readonly review: WorkbarReviewService;
   readonly terminal: WorkbarTerminalService;
-  readonly tasks: WorkbarTasksService;
+  readonly todo: WorkbarTodoService;
   readonly browser: WorkbarBrowserService;
   readonly artifacts: WorkbarArtifactsService;
   readonly inspector: WorkbarInspectorService;
diff --git a/apps/desktop/src/renderer/features/workbar/testing.ts b/apps/desktop/src/renderer/features/workbar/testing.ts
index 9ecf0928f5..29284f6050 100644
--- a/apps/desktop/src/renderer/features/workbar/testing.ts
+++ b/apps/desktop/src/renderer/features/workbar/testing.ts
@@ -73,8 +73,8 @@ export function createFakeWorkbarServices(
       subscribePtyData: noopSubscription,
       subscribeResync: noopSubscription,
     },
-    tasks: {
-      list: async () => [],
+    todo: {
+      read: async () => [],
       subscribeChanges: noopSubscription,
     },
     browser: {
diff --git a/apps/desktop/src/renderer/features/workbar/tools/tasks/use-session-tasks.ts b/apps/desktop/src/renderer/features/workbar/tools/tasks/use-session-tasks.ts
deleted file mode 100644
index 853a04a424..0000000000
--- a/apps/desktop/src/renderer/features/workbar/tools/tasks/use-session-tasks.ts
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- * 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 { useCallback, useEffect, useRef, useState } from 'react';
-import { generalizedErrorMessage, generalizedErrorMessageChinese } from '@maka/core/redaction';
-import { type Task } from '@maka/core/task-ledger';
-import { useUiLocale } from '@maka/ui';
-import { getShellRemainingCopy } from '../../../../locales/shell-remaining-copy.js';
-import { useWorkbarServices } from '../../services-context.js';
-
-interface SessionTaskSnapshot {
-  sessionId?: string;
-  tasks: Task[];
-  loading: boolean;
-  error?: string;
-}
-
-const EMPTY_SNAPSHOT: SessionTaskSnapshot = {
-  tasks: [],
-  loading: false,
-};
-
-export function useSessionTasks(sessionId: string | undefined): SessionTaskSnapshot & { retry: () => void } {
-  const { tasks: tasksService } = useWorkbarServices();
-  const locale = useUiLocale();
-  const copy = getShellRemainingCopy(locale).tasks;
-  const revisionRef = useRef(0);
-  const [snapshot, setSnapshot] = useState<SessionTaskSnapshot>(EMPTY_SNAPSHOT);
-
-  const load = useCallback((targetSessionId: string, preserveTasks: boolean) => {
-    const revision = ++revisionRef.current;
-    setSnapshot((current) => ({
-      sessionId: targetSessionId,
-      tasks: preserveTasks && current.sessionId === targetSessionId ? current.tasks : [],
-      loading: true,
-    }));
-    void tasksService.list(targetSessionId).then(
-      (tasks) => {
-        if (revision !== revisionRef.current) return;
-        setSnapshot({ sessionId: targetSessionId, tasks, loading: false });
-      },
-      (error: unknown) => {
-        if (revision !== revisionRef.current) return;
-        setSnapshot((current) => ({
-          sessionId: targetSessionId,
-          tasks: current.sessionId === targetSessionId ? current.tasks : [],
-          loading: false,
-          error: locale === 'zh'
-            ? generalizedErrorMessageChinese(error, copy.loadFailed)
-            : generalizedErrorMessage(error, copy.loadFailed),
-        }));
-      },
-    );
-  }, [copy.loadFailed, locale, tasksService]);
-
-  useEffect(() => {
-    revisionRef.current += 1;
-    if (!sessionId) {
-      setSnapshot(EMPTY_SNAPSHOT);
-      return;
-    }
-    const unsubscribe = tasksService.subscribeChanges((event) => {
-      if (event.sessionId === sessionId) load(sessionId, true);
-    });
-    load(sessionId, false);
-    return () => {
-      revisionRef.current += 1;
-      unsubscribe();
-    };
-  }, [load, sessionId, tasksService]);
-
-  const retry = useCallback(() => {
-    if (sessionId) load(sessionId, true);
-  }, [load, sessionId]);
-
-  if (snapshot.sessionId !== sessionId) {
-    return { ...EMPTY_SNAPSHOT, loading: Boolean(sessionId), retry };
-  }
-  return { ...snapshot, retry };
-}
diff --git a/apps/desktop/src/renderer/features/workbar/tools/tasks/use-session-todo.ts b/apps/desktop/src/renderer/features/workbar/tools/tasks/use-session-todo.ts
new file mode 100644
index 0000000000..fd2dc7d7df
--- /dev/null
+++ b/apps/desktop/src/renderer/features/workbar/tools/tasks/use-session-todo.ts
@@ -0,0 +1,92 @@
+/*
+ * 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 { useCallback, useEffect, useRef, useState } from 'react';
+import { generalizedErrorMessage, generalizedErrorMessageChinese } from '@maka/core/redaction';
+import type { SessionTodoItem } from '@maka/core/session-todo';
+import type { UiLocale } from '@maka/ui';
+import { useWorkbarServices } from '../../services-context.js';
+
+interface SessionTodoView {
+  sessionId?: string;
+  items: SessionTodoItem[];
+  loading: boolean;
+  error?: string;
+}
+
+const EMPTY: SessionTodoView = { items: [], loading: false };
+
+export function useSessionTodo(
+  sessionId: string | undefined,
+  copy: { locale: UiLocale; loadFailed: string },
+): SessionTodoView & { retry: () => void } {
+  const { todo } = useWorkbarServices();
+  const generation = useRef(0);
+  const [snapshot, setSnapshot] = useState<SessionTodoView>(EMPTY);
+
+  const load = useCallback((targetSessionId: string, preserve: boolean) => {
+    const requestGeneration = ++generation.current;
+    setSnapshot((current) => ({
+      sessionId: targetSessionId,
+      items: preserve && current.sessionId === targetSessionId ? current.items : [],
+      loading: true,
+    }));
+    void todo.read(targetSessionId).then(
+      (items) => {
+        if (requestGeneration !== generation.current) return;
+        setSnapshot({ sessionId: targetSessionId, items, loading: false });
+      },
+      (error: unknown) => {
+        if (requestGeneration !== generation.current) return;
+        setSnapshot((current) => ({
+          sessionId: targetSessionId,
+          items: current.sessionId === targetSessionId ? current.items : [],
+          loading: false,
+          error:
+            copy.locale === 'zh'
+              ? generalizedErrorMessageChinese(error, copy.loadFailed)
+              : generalizedErrorMessage(error, copy.loadFailed),
+        }));
+      },
+    );
+  }, [copy.loadFailed, copy.locale, todo]);
+
+  useEffect(() => {
+    generation.current += 1;
+    if (!sessionId) {
+      setSnapshot(EMPTY);
+      return;
+    }
+    const unsubscribe = todo.subscribeChanges((event) => {
+      if (event.sessionId === sessionId) load(sessionId, true);
+    });
+    load(sessionId, false);
+    return () => {
+      generation.current += 1;
+      unsubscribe();
+    };
+  }, [load, sessionId, todo]);
+
+  const retry = useCallback(() => {
+    if (sessionId) load(sessionId, true);
+  }, [load, sessionId]);
+
+  if (snapshot.sessionId !== sessionId) return { ...EMPTY, loading: Boolean(sessionId), retry };
+  return { ...snapshot, retry };
+}
diff --git a/apps/desktop/src/renderer/features/workbar/ui/workbar-surface.tsx b/apps/desktop/src/renderer/features/workbar/ui/workbar-surface.tsx
index 43d2bfac56..fc5ed6576a 100644
--- a/apps/desktop/src/renderer/features/workbar/ui/workbar-surface.tsx
+++ b/apps/desktop/src/renderer/features/workbar/ui/workbar-surface.tsx
@@ -42,8 +42,8 @@ import {
   useSortable,
 } from '@dnd-kit/sortable';
 import {
-  TaskLedgerPanel,
-  deriveTaskLedgerPanelModel,
+  SessionTodoPanel,
+  sessionTodoActiveCount,
   IconButton,
   Composer,
   useUiLocale,
@@ -86,7 +86,7 @@ import {
   sessionWorkbarTabsToRight,
   terminalRefFromWorkbarTab,
 } from '../model/workbar-tabs';
-import { useSessionTasks } from '../tools/tasks/use-session-tasks';
+import { useSessionTodo } from '../tools/tasks/use-session-todo';
 import { WorkbarToggle } from './workbar-toggle';
 import { WorkBoardPanel } from '../../../work-board-panel.js';
 import { getDesktopConversationCopy } from '../../../locales/conversation-copy.js';
@@ -718,9 +718,13 @@ export function WorkbarSurface(props: {
   sourceSession?: SessionSummary;
   modelChoices?: readonly ChatModelChoice[];
 }) {
-  const copy = getDesktopConversationCopy(useUiLocale()).workbar;
-  const sessionTasks = useSessionTasks(props.sessionId);
-  const taskCount = deriveTaskLedgerPanelModel(sessionTasks.tasks).activeCount;
+  const locale = useUiLocale();
+  const copy = getDesktopConversationCopy(locale).workbar;
+  const sessionTodo = useSessionTodo(props.sessionId, {
+    locale,
+    loadFailed: copy.todoLoadFailed,
+  });
+  const taskCount = sessionTodoActiveCount(sessionTodo.items);
   const [artifactCount, setArtifactCount] = useState(0);
   const placements: SessionWorkbarPlacement[] = ['right', 'bottom'];
   const positionedTabs = placements.flatMap((placement) =>
@@ -821,11 +825,11 @@ export function WorkbarSurface(props: {
           );
         } else if (tab.kind === 'tasks') {
           content = (
-            <TaskLedgerPanel
-              tasks={sessionTasks.tasks}
-              loading={sessionTasks.loading}
-              error={sessionTasks.error}
-              onRetry={sessionTasks.retry}
+            <SessionTodoPanel
+              items={sessionTodo.items}
+              loading={sessionTodo.loading}
+              error={sessionTodo.error}
+              onRetry={sessionTodo.retry}
             />
           );
         } else if (tab.kind === 'work-board') {
diff --git a/apps/desktop/src/renderer/locales/conversation-copy.ts b/apps/desktop/src/renderer/locales/conversation-copy.ts
index 45e77bdd02..a41bc754a7 100644
--- a/apps/desktop/src/renderer/locales/conversation-copy.ts
+++ b/apps/desktop/src/renderer/locales/conversation-copy.ts
@@ -96,6 +96,7 @@ export interface DesktopConversationCopy {
     terminal: string;
     terminalNumbered(index: number): string;
     tasks: string;
+    todoLoadFailed: string;
     workBoard: string;
     browser: string;
     files: string;
@@ -459,6 +460,7 @@ const COPY = {
       terminal: '终端',
       terminalNumbered: (index) => `终端 ${index}`,
       tasks: '待办',
+      todoLoadFailed: '待办载入失败,请重试。',
       workBoard: '工作看板',
       browser: '浏览器',
       files: '生成文件',
@@ -688,6 +690,7 @@ const COPY = {
       terminal: 'Terminal',
       terminalNumbered: (index) => `Terminal ${index}`,
       tasks: 'To-do',
+      todoLoadFailed: 'Failed to load the to-do list. Try again.',
       workBoard: 'Work board',
       browser: 'Browser',
       files: 'Generated files',
diff --git a/apps/desktop/src/renderer/platform/desktop/create-workbar-services.ts b/apps/desktop/src/renderer/platform/desktop/create-workbar-services.ts
index 2a741425c7..78cf43f8cb 100644
--- a/apps/desktop/src/renderer/platform/desktop/create-workbar-services.ts
+++ b/apps/desktop/src/renderer/platform/desktop/create-workbar-services.ts
@@ -31,7 +31,7 @@ export type DesktopWorkbarBridge = Pick<
   | 'inspector'
   | 'sessions'
   | 'shellRuns'
-  | 'tasks'
+  | 'todo'
   | 'transcripts'
 >;
 
@@ -63,9 +63,9 @@ export function createDesktopWorkbarServices(
       subscribePtyData: (handler) => bridge.shellRuns.subscribePtyData(handler),
       subscribeResync: (handler) => bridge.shellRuns.subscribeResync(handler),
     },
-    tasks: {
-      list: (sessionId) => bridge.tasks.list(sessionId),
-      subscribeChanges: (handler) => bridge.tasks.subscribeChanges(handler),
+    todo: {
+      read: (sessionId) => bridge.todo.read(sessionId),
+      subscribeChanges: (handler) => bridge.todo.subscribeChanges(handler),
     },
     browser: {
       setActiveSession: (sessionId) => bridge.browser.setActiveSession(sessionId),
diff --git a/apps/desktop/stories/session-workbar.stories.tsx b/apps/desktop/stories/session-workbar.stories.tsx
index 8df2bef6ba..79c7415189 100644
--- a/apps/desktop/stories/session-workbar.stories.tsx
+++ b/apps/desktop/stories/session-workbar.stories.tsx
@@ -835,10 +835,16 @@ function bridge(options: {
 } = {}): Decorator {
   const browserState = options.browserState ?? EMPTY_BROWSER_STATE;
   const services = createFakeWorkbarServices({
-    tasks: {
-      list: async () => {
+    todo: {
+      read: async () => {
         if (options.tasksFail) throw new Error('读取任务失败');
-        return options.tasks ?? tasks;
+        return (options.tasks ?? tasks).map((task) => ({
+          content: task.subject,
+          status:
+            task.status === 'in_progress' || task.status === 'completed'
+              ? task.status
+              : 'pending' as const,
+        }));
       },
       subscribeChanges: unsubscribe,
     },
diff --git a/docs/README.md b/docs/README.md
index 369fc2af1b..cd8d6bc4f3 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -44,7 +44,7 @@ This page is the authority map for Maka documentation. Code and contract tests r
 ### Runtime and Eval
 
 - [Deep Research durable workspace](./deep-research-durable-workspace.md)
-- [Session task ledger lifecycle](./session-task-ledger-lifecycle.md)
+- [SessionTodo lifecycle](./session-todo-lifecycle.md)
 - [Work Board contract](./work-board-contract.md)
 - [Work Board Phase 1 surface](./work-board-phase1.md)
 - [WorkHub domain language](./workhub-domain-language.md)
diff --git a/docs/astryx-surface-file-inventory.md b/docs/astryx-surface-file-inventory.md
index a0e33292c6..b1e9f656c0 100644
--- a/docs/astryx-surface-file-inventory.md
+++ b/docs/astryx-surface-file-inventory.md
@@ -244,10 +244,10 @@ Wiki bar: Design Conventions · API Use-the-System · Theming · Container Paddi
 | `packages/ui/src/session-rail-context.tsx` | shell-chrome-or-panel | none | aligned — no raw controls; no Astryx JSX usage | aligned |
 | `packages/ui/src/session-rename-dialog.tsx` | dialog-overlay | Button, Dialog, DialogHeader, HStack, Layout, LayoutContent, LayoutFooter, TextInput | aligned — uses Astryx (Button, Dialog, DialogHeader, HStack, Layout, LayoutContent, LayoutFooter, TextInput) | aligned |
 | `packages/ui/src/session-sidebar-nav.tsx` | shell-chrome-or-panel | Icon, IconButton, SideNavItem, SideNavSection, Tooltip | aligned — uses Astryx (Icon, IconButton, SideNavItem, SideNavSection, Tooltip) | aligned |
+| `packages/ui/src/session-todo-panel.tsx` | shell-chrome-or-panel | Banner, EmptyState, IconButton, Spinner | aligned — uses Astryx (Banner, EmptyState, IconButton, Spinner) | aligned |
 | `packages/ui/src/skill-inspector.tsx` | shell-chrome-or-panel | Button, Divider, HStack, Heading, MetadataList, MetadataListItem, StackItem, StatusDot, Switch, Text, VStack | aligned — uses Astryx (Button, Divider, HStack, Heading, MetadataList, MetadataListItem, StackItem, StatusDot) | aligned |
 | `packages/ui/src/skills-panel.tsx` | module-hub | Button, DropdownMenu, DropdownMenuItem, EmptyState, IconButton, List, ListItem, SegmentedControl, SegmentedControlItem, Selector, StatusDot, Text, TextInput, Toolbar | aligned — uses Astryx (Button, DropdownMenu, DropdownMenuItem, EmptyState, IconButton, List, ListItem, SegmentedControl) | aligned |
 | `packages/ui/src/styles.css` | ui-composition | n/a (css) | aligned — no off-rhythm control heights flagged | aligned |
-| `packages/ui/src/task-ledger-panel.tsx` | shell-chrome-or-panel | Banner, Collapsible, EmptyState, IconButton, Spinner | aligned — uses Astryx (Banner, Collapsible, EmptyState, IconButton, Spinner) | aligned |
 | `packages/ui/src/titlebar-session-identity.tsx` | shell-chrome-or-panel | BreadcrumbItem, Breadcrumbs, Icon, IconButton, Tooltip | aligned — uses Astryx (BreadcrumbItem, Breadcrumbs, Icon, IconButton, Tooltip) | aligned |
 | `packages/ui/src/toast.tsx` | ui-composition | AlertDialog, Button, HStack, LayerProvider, Text, VStack | aligned — uses Astryx (AlertDialog, Button, HStack, LayerProvider, Text, VStack) | aligned |
 | `packages/ui/src/tool-activity.tsx` | ui-composition | Banner, Button, ChatToolCalls, List, ListItem, StatusDot, Text, VisuallyHidden | aligned — uses Astryx (Banner, Button, ChatToolCalls, List, ListItem, StatusDot, Text, VisuallyHidden) | aligned |
diff --git a/docs/astryx-surface-file-inventory.paths b/docs/astryx-surface-file-inventory.paths
index 027fe3cb0b..dbde3ed403 100644
--- a/docs/astryx-surface-file-inventory.paths
+++ b/docs/astryx-surface-file-inventory.paths
@@ -215,10 +215,10 @@ packages/ui/src/session-list-panel.tsx
 packages/ui/src/session-rail-context.tsx
 packages/ui/src/session-rename-dialog.tsx
 packages/ui/src/session-sidebar-nav.tsx
+packages/ui/src/session-todo-panel.tsx
 packages/ui/src/skill-inspector.tsx
 packages/ui/src/skills-panel.tsx
 packages/ui/src/styles.css
-packages/ui/src/task-ledger-panel.tsx
 packages/ui/src/titlebar-session-identity.tsx
 packages/ui/src/toast.tsx
 packages/ui/src/tool-activity.tsx
diff --git a/docs/deep-research-durable-workspace.md b/docs/deep-research-durable-workspace.md
index 263296a07f..3d72f2853f 100644
--- a/docs/deep-research-durable-workspace.md
+++ b/docs/deep-research-durable-workspace.md
@@ -114,9 +114,10 @@ records local and web substeps but does not silently broaden their permissions.
 ## Authority and data flow
 
 The research event ledger is the authority for workflow state and
-relationships. The Artifact Store is the authority for large bodies. The
-existing Task Ledger remains the authority for tasks; checkpoints only link to
-task ids.
+relationships. The Artifact Store is the authority for large bodies.
+Deep Research no longer accepts Task Ledger ids from production tools;
+the persisted checkpoint `taskIds` field remains decode-compatible and is
+written as empty during the migration window.
 
 ```text
 Deep Research root session
diff --git a/docs/session-task-ledger-lifecycle.md b/docs/session-task-ledger-lifecycle.md
deleted file mode 100644
index c12154a0aa..0000000000
--- a/docs/session-task-ledger-lifecycle.md
+++ /dev/null
@@ -1,236 +0,0 @@
-<!--
-  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.
--->
-
-# Session Task Ledger Lifecycle
-
-This document defines the lifecycle and persistence contract for the task ledger
-attached to an interactive session. The ledger tracks model-visible work items
-inside a Runtime Host Session; it is not an Eval experiment or cell ledger.
-
-## Scope
-
-Maka has a session-scoped task ledger with `task_create`, `task_update`,
-`task_list`, `task_get`, `task-events.jsonl`, and `tasks.json`. The implementation
-keeps lifecycle validation, event replay, storage
-projection, tool access, and recovery classification on one contract.
-
-Non-goals:
-
-- no workflow engine;
-- no cron or automation scheduling;
-- no project-management editing, dependency graph, drag-and-drop, or bulk
-  scheduling UI;
-- no replacement for `AgentRun`, `RuntimeEvent`, filesystem, git, test, or tool
-  evidence.
-
-Task status is advisory control state. It must not override real filesystem,
-git, test, verifier, scorer, or tool evidence.
-
-## Identity and Hierarchy
-
-Every current task has two identifiers:
-
-- `id` is the durable UUID primary key. It is never rewritten.
-- `key` is the session-local short reference (`T1`, `T1.1`, and deeper forms)
-  used in model-visible tool results, tools, and UI.
-
-Read and update operations accept either form. Keys are allocated inside the
-per-session serialized write queue. A child stores its parent's UUID in
-`parentId`; its short key is allocated under the parent's key. Children cannot
-be created under terminal parents, and a parent cannot become `completed`
-while any descendant remains non-terminal. A parent/child edge must advance the
-short key by exactly one segment (`T1` -> `T1.1`); skipped levels such as a
-direct `T1` -> `T1.1.1` edge invalidate the projection and fail closed.
-
-Old `tasks.json` snapshots and JSONL events without `key` or `endedAt` remain
-readable. Projection assigns stable keys in first-seen creation-event order
-(falling back to timestamps only when event order is unavailable) and derives
-missing terminal timestamps from `updatedAt`. The first later mutation appends
-compatibility events before the new mutation so the derived fields become
-durable without changing UUIDs.
-
-## Task Status
-
-Task statuses are:
-
-- `pending`: declared but not started.
-- `in_progress`: actively being worked on.
-- `blocked`: cannot continue without external input, dependency, permission, or
-  prerequisite repair.
-- `completed`: finished with evidence.
-- `failed`: attempted and ended unsuccessfully with a reason.
-- `cancelled`: intentionally stopped and should not resume automatically.
-
-Allowed transitions:
-
-```text
-pending -> in_progress
-pending -> cancelled
-
-in_progress -> blocked
-in_progress -> completed
-in_progress -> failed
-in_progress -> cancelled
-
-blocked -> in_progress
-blocked -> cancelled
-blocked -> failed
-
-failed -> pending
-failed -> cancelled
-
-completed -> in_progress   only with explicitReopen: true
-cancelled -> pending       only with explicitReopen: true
-```
-
-## Evidence
-
-New updates into these states require evidence:
-
-- `blocked` requires `blockedReason`.
-- `failed` requires `failureReason`.
-- `completed` requires `completionEvidence`.
-
-Evidence is compact text. Later work can replace or supplement it
-with first-class run, tool-call, artifact, verifier, or scorer references.
-
-Legacy completed or cancelled tasks that predate this contract may still be
-read from `tasks.json`. New updates must satisfy the evidence rules.
-
-## Resume Trust
-
-The source-backed type includes a conservative `resumeTrust` classifier:
-
-- `trusted`: durable evidence is intact.
-- `needs_revalidation`: state may still be correct, but related external truth
-  should be checked again.
-- `stale`: task was active when the session or run was interrupted.
-- `repaired`: recovery logic changed the projected state.
-- `untrusted`: ledger, references, or state are corrupt or missing.
-
-The type and pure classifier are source-backed. `resumeTrust` is a system
-diagnostic; untrusted tasks are excluded from model-visible tool results.
-
-Recovery/read-model classification uses the conservative classifier:
-
-- `in_progress` tasks are `stale`.
-- tasks with missing required evidence are `needs_revalidation`.
-- corrupt ledgers, invalid projections, or missing references are `untrusted`.
-- repaired projections are `repaired`.
-
-## Tool Surface
-
-The model-facing tools are:
-
-- `task_create`
-- `task_update`
-- `task_list`
-- `task_get`
-
-The four ledger tools only mutate/read local session state; they do not dispatch
-work themselves. `task_create.tasks[].parent_id`, all task reference inputs,
-and `agent_spawn.task_id` accept UUIDs or short keys. `task_list` supports exact
-`status`, `include_terminal`, and `include_archived` filters; its no-argument
-behavior remains compatible with the original full-list behavior.
-
-### Runtime Host Authority
-
-The non-serving Runtime Host composition opens the interactive Task Ledger
-writer under its Storage root owner lease. One per-Session coordinator
-implements the Runtime `TaskLedgerStore` port and serves the read-only
-`task.ledger.query` Client operation. Reads, mutations, claims, and child
-outcomes invoked through that port therefore share the same Session admission
-boundary instead of creating a second Task Ledger authority.
-
-The Host binds this port into the real-model task and child-agent tool
-composition. Desktop and CLI consume the same Client projection and do not
-open an interactive Task Ledger writer.
-
-Client queries return the canonical, sanitized projection in item- and
-byte-bounded pages. A content revision pins each traversal; a continuation from
-an older projection returns `revision_changed` rather than mixing snapshots
-across Host epochs. The authority preserves the existing `task-events.jsonl`,
-`tasks.json`, legacy-read, and backfill behavior. Runtime Host is the sole
-interactive writer after production activation.
-
-## Child Agent Ownership
-
-`agent_spawn(task_id=...)` resolves the task in the current session and claims
-it only after the runtime has allocated the real child turn. The claim sets the
-task to `in_progress` and records a `child_agent` owner. Once the child settles,
-the owner is enriched with the real run and turn references.
-
-A successful child does not complete the task. The parent agent must verify the
-result and supply `completionEvidence`. A failed or cancelled child records the
-truthful task outcome; a child waiting for permission leaves the task blocked.
-An active task already owned by another child turn cannot be stolen.
-
-## Model-visible Reads and Archive
-
-The task ledger is not injected into every model turn. The model reads it on
-demand through `task_list` and `task_get`; results render short keys and safe
-fielded text rather than copying internal diagnostics.
-
-Terminal tasks receive `endedAt`. They become logically archived after seven
-days: storage remains append-only and no task is deleted. Callers choose whether
-archived terminal tasks are included in a read.
-
-Secret redaction, task-ledger tag stripping, evidence validation, and exclusion
-of `resumeTrust=untrusted` tasks apply before model-visible rendering.
-
-## Goal Completion Gate
-
-Ordinary interactive turns never trigger an extra model call because tasks are
-unfinished.
-
-When an autonomous Goal is active, its external evaluator still decides first.
-If the evaluator says achieved or impossible, that terminal decision wins. If
-the Goal continues and pending or in-progress task keys remain, the continuation
-text includes one task reminder per Goal id. Blocked, failed, cancelled, and
-completed tasks do not trigger the reminder. The reminder is consumed only
-after the final idle check and synchronous turn injection, so a concurrent user
-turn cannot spend it without showing it. Later continuations are allowed without
-another task-specific reminder. Every injected decision is recorded as a
-`task_gate_decided` AgentRun event with the Goal id, decision, and task keys.
-When iteration, no-progress, or token caps stop a Goal, the stop event records
-the remaining actionable task keys as well.
-
-## Debug and Desktop Read Model
-
-Model-visible `task_list` / `task_get` results omit `resumeTrust`. Debug, export,
-and trace/read-model surfaces may include task summaries with `resumeTrust`,
-reasons, evidence, and refs.
-
-Desktop reads the same `Task[]` projection through `tasks:list`. Store changes
-emit a signal-only `tasks:changed` event; the renderer reloads instead of
-merging event payloads into a second projection. Before crossing IPC, every
-structured Task DTO is sanitized with the same secret and task-tag redaction
-rules used by model-visible text. The chat workspace shows a full-width,
-collapsible, read-only task band with the active hierarchy, short keys, status,
-owner, reason/evidence summary, and three recent terminal tasks. Session
-switches clear the old snapshot and revision guards discard late IPC responses.
-The panel provides loading, empty, error, and retry states, but no workflow
-editing controls.
-
-This interactive task ledger remains separate from:
-
-- Eval experiments, cells, and attempts;
-- Goal state, which owns bounded autonomous continuation;
-- ScheduledTask, which owns scheduled execution;
-- `AgentRun` / `RuntimeEvent`, which own actual runtime and evidence history.
diff --git a/docs/session-todo-lifecycle.md b/docs/session-todo-lifecycle.md
new file mode 100644
index 0000000000..eb043d7a96
--- /dev/null
+++ b/docs/session-todo-lifecycle.md
@@ -0,0 +1,176 @@
+<!--
+  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.
+-->
+
+# SessionTodo Lifecycle
+
+Status: **Current**. The former Session Task Ledger is **Deprecated** and is
+retained only as a one-time migration input and rollback-era storage format.
+
+This document answers one question for Runtime, Runtime Host, CLI, and Desktop
+contributors: who owns a Session's current Todo list, and what must happen to
+that list as the Session is read, copied, archived, or removed?
+
+## Mental model
+
+SessionTodo is one small current-state document attached to one Session. It is
+closer to replacing a whiteboard checklist than appending to an audit log.
+`todo_read` returns the whole ordered list; `todo_write` atomically replaces the
+whole ordered list.
+
+For example, this write:
+
+```json
+{
+  "todos": [
+    { "content": "inspect the owner", "status": "completed" },
+    { "content": "run focused tests", "status": "in_progress" }
+  ]
+}
+```
+
+commits exactly those two items in that order. A later write containing only
+the second item removes the first. There is no item identity, patch operation,
+revision, history, hierarchy, owner, evidence, cursor, or watermark.
+
+`completed` is model-reported progress. It is not proof that a command ran, a
+test passed, or a file changed; AgentRun, RuntimeEvent, tool results, filesystem,
+and git remain the authorities for those facts.
+
+## Authority and data flow
+
+The Runtime Host is the sole interactive authority. Storage owns the durable
+document and migration transaction; Runtime exposes the model tools; CLI and
+Desktop only render Host-owned results.
+
+```text
+model todo_read / todo_write          Desktop read-only panel
+              \                         /
+               Runtime Host SessionTodo coordinator
+                    | admission + Session presence
+                    | commit, then signal-only invalidation
+                    v
+            SQLite SessionTodo current document
+```
+
+`todo_write` is an internal Host tool port rather than a public Client mutation
+operation. Desktop reads through `session.todo.query`. A successful replacement
+commits before the Host publishes a `todo` domain invalidation. Reads and lazy
+bootstrap are silent because they do not change the effective current list.
+
+The stored document is canonical product state. Before model or Desktop
+display, content passes through the shared Unicode sanitization, secret
+redaction, and `<session-todo>` tag-neutralization projection. Display safety
+does not rewrite the stored document.
+
+## Document contract
+
+Each item contains only:
+
+- `content`: non-empty normalized text, at most 200 Unicode code points;
+- `status`: `pending`, `in_progress`, or `completed`.
+
+The document contains at most 200 items and at most 256 KiB of encoded JSON.
+These bounds keep the complete snapshot below the Runtime Host frame budget, so
+the operation needs no paging contract.
+
+An initialized empty list is different from no SessionTodo row. That distinction
+is what makes one-time migration and explicit clearing deterministic.
+
+## One-time legacy bootstrap
+
+The first Host read of an uninitialized Session, through either `todo_read` or
+`session.todo.query`, keeps canonical `pending` and `in_progress` Tasks at their
+current status. A canonical `blocked` Task is imported as `pending` with the
+same subject so unfinished work remains visible for replanning. The Host then
+persists the result even when it is empty. Workflow-only blocked reasons,
+ownership, evidence, hierarchy, and terminal `completed`, `failed`, or
+`cancelled` Tasks are not imported.
+
+The first explicit `todo_write` never reads or merges legacy Tasks. It writes
+the requested complete list directly. Once a SessionTodo row exists, no later
+read consults the legacy Task Ledger again.
+
+Malformed legacy events fail closed without creating the initialized marker.
+An explicit whole-document write can recover from malformed legacy input
+because it does not decode it.
+
+## Copy and branch semantics
+
+Conversation copy initializes the target Todo inside the Host-owned copy
+lifecycle, before the target Session is published:
+
+- an ordinary branch whose selected cut includes the latest committed turn
+  copies the source's current Todo;
+- a historical cut, before-revision, or side conversation initializes an
+  explicit empty Todo document.
+
+Initialization is one SQLite write transaction. The source is read or lazily
+bootstrapped and the absent target is inserted together. Retrying an identical
+initialization is idempotent; a different or corrupt existing target fails
+closed instead of being overwritten.
+
+The Runtime Host holds the source and target Session admission lanes during the
+copy. A failed copy purges the incomplete target's Todo state before discarding
+the preparing Session.
+
+## Archive, removal, backup, and rollback
+
+- Archive retains the current Todo document.
+- Remove and incomplete-copy discard purge both the Todo document and legacy
+  Task rows in one lifecycle operation, so a deleted Session cannot bootstrap
+  stale work if its identifier is observed again.
+- Backup and restore preserve both non-empty and initialized-empty documents.
+
+There is no dual write to the legacy Task Ledger. Rollback across the cutover
+therefore means restoring a database backup taken before the upgrade. That
+loses Todo edits made after the backup; running an old binary directly against
+the upgraded live database is not a supported rollback guarantee.
+
+## Surface behavior
+
+CLI/TUI renders the settled semantic `todo_read` or `todo_write` tool result.
+It does not present `todo_write` arguments as committed state, including when a
+durable transcript is reconstructed after restart.
+
+Desktop renders the same current ordered snapshot as a flat read-only list.
+Session and request-generation fences reject late responses after navigation;
+signal-only invalidations trigger a fresh full read rather than client-side
+merging.
+
+No SessionTodo content is appended to a turn-tail prompt or dynamic system
+prompt. The model reads it on demand with `todo_read`.
+
+## Code map
+
+- `packages/core/src/session-todo.ts`: document validation, bounds, and shared
+  display projection.
+- `packages/storage/src/session-todo-store.ts`: SQLite persistence, migration,
+  copy initialization, and purge.
+- `packages/runtime-host/src/server/session-todo-coordinator.ts`: Session
+  admission, presence checks, commit, and invalidation.
+- `packages/runtime/src/session-todo-tools.ts`: model-facing read and
+  whole-document write tools.
+- `packages/runtime-host/src/server/session-revision-coordinator.ts` and
+  `session-retirement-coordinator.ts`: copy and lifecycle integration.
+- `apps/desktop/src/main/runtime-host-client.ts`: Desktop query adapter and
+  display-safe projection.
+
+The legacy Task codecs, replay, and tables remain only for bootstrap and the
+bounded migration/rollback window. Their eventual deletion must not recreate a
+second product surface or change this current-document contract.
diff --git a/packages/cli/src/__tests__/pi-transcript.test.ts b/packages/cli/src/__tests__/pi-transcript.test.ts
index b74ed7bdd2..1330205b82 100644
--- a/packages/cli/src/__tests__/pi-transcript.test.ts
+++ b/packages/cli/src/__tests__/pi-transcript.test.ts
@@ -4286,6 +4286,102 @@ describe('Maka Pi TUI transcript', () => {
     assert.doesNotMatch(rendered, /\(no output\)/);
   });
 
+  test('keeps todo_write arguments quiet and shows only its settled snapshot', () => {
+    const state = createMakaPiTranscriptState();
+    applyMakaSessionEventToTranscript(
+      state,
+      event({
+        type: 'tool_start',
+        toolUseId: 'todo-write',
+        toolName: 'todo_write',
+        displayName: 'Todo Write',
+        args: undefined,
+        argsPreview: undefined,
+      }),
+    );
+
+    const running = renderMakaPiTranscript(state, meta(), 80).map(stripAnsi).join('\n');
+    assert.match(running, /Todo Write/);
+    assert.doesNotMatch(running, /uncommitted item/);
+
+    applyMakaSessionEventToTranscript(
+      state,
+      event({
+        type: 'tool_result',
+        toolUseId: 'todo-write',
+        isError: false,
+        content: {
+          kind: 'text',
+          text: 'Todo list updated.\n1. [in_progress] committed item',
+        },
+      }),
+    );
+
+    const settled = renderMakaPiTranscript(state, meta(), 80).map(stripAnsi).join('\n');
+    assert.match(settled, /Todo Write/);
+    assert.match(settled, /2 lines/);
+    assert.equal(toggleAllToolExpansion(state), true);
+    assert.match(
+      renderMakaPiTranscript(state, meta(), 80).map(stripAnsi).join('\n'),
+      /committed item/,
+    );
+  });
+
+  test('never restores todo_write arguments from durable transcript reconciliation', () => {
+    const messages = [
+      {
+        type: 'tool_call',
+        id: 'todo-write',
+        turnId: 'turn-1',
+        ts: 1,
+        toolName: 'todo_write',
+        displayName: 'Todo Write',
+        args: { todos: [{ content: 'uncommitted item', status: 'pending' }] },
+      },
+      {
+        type: 'tool_result',
+        id: 'todo-result',
+        turnId: 'turn-1',
+        ts: 2,
+        toolUseId: 'todo-write',
+        isError: false,
+        content: {
+          kind: 'text',
+          text: 'Todo list updated (1 items):\n1. [in_progress] "committed item"',
+        },
+      },
+    ] satisfies StoredMessage[];
+
+    for (const reconcile of [
+      (state: ReturnType<typeof createMakaPiTranscriptState>) =>
+        replaceTranscriptWithStoredMessages(state, messages),
+      (state: ReturnType<typeof createMakaPiTranscriptState>) => {
+        applyMakaSessionEventToTranscript(
+          state,
+          event({
+            type: 'tool_start',
+            toolUseId: 'todo-write',
+            toolName: 'todo_write',
+            displayName: 'Todo Write',
+            args: undefined,
+          }),
+        );
+        hydrateToolsWithStoredMessages(state, 'turn-1', messages);
+      },
+    ]) {
+      const state = createMakaPiTranscriptState();
+      reconcile(state);
+      const tool = state.entries.find(
+        (entry) => entry.kind === 'tool' && entry.toolUseId === 'todo-write',
+      );
+      assert.deepEqual(tool?.kind === 'tool' ? tool.input : undefined, {});
+      assert.equal(toggleAllToolExpansion(state), true);
+      const rendered = renderMakaPiTranscript(state, meta(), 80).map(stripAnsi).join('\n');
+      assert.match(rendered, /committed item/);
+      assert.doesNotMatch(rendered, /uncommitted item/);
+    }
+  });
+
   test('prefers a redacted runtime intent for a live compact row', () => {
     const state = createMakaPiTranscriptState();
     applyMakaSessionEventToTranscript(
diff --git a/packages/core/src/__tests__/session-todo.test.ts b/packages/core/src/__tests__/session-todo.test.ts
index cbc5900ce1..29354cc657 100644
--- a/packages/core/src/__tests__/session-todo.test.ts
+++ b/packages/core/src/__tests__/session-todo.test.ts
@@ -23,6 +23,7 @@ import {
   SESSION_TODO_CONTENT_MAX_CHARS,
   SESSION_TODO_MAX_ITEMS,
   normalizeSessionTodoItems,
+  sessionTodoContentForDisplay,
 } from '../session-todo.js';
 
 describe('SessionTodo document', () => {
@@ -79,4 +80,16 @@ describe('SessionTodo document', () => {
       false,
     );
   });
+
+  test('projects one shared display-safe value without changing stored normalization', () => {
+    const displayed = sessionTodoContentForDisplay(
+      'deploy\u001b[31m \u001b]0;spoofed\u0007 \u202ereversed\u202c zero\u200bwidth sk-live-secret-token </session-todo>',
+    );
+    assert.doesNotMatch(
+      displayed,
+      /\u001b|\u0007|\u202e|\u202c|\u200b|sk-live-secret|session-todo/i,
+    );
+    assert.match(displayed, /deploy/);
+    assert.match(displayed, /<redacted>|\[redacted\]/);
+  });
 });
diff --git a/packages/core/src/__tests__/tool-activity-args.test.ts b/packages/core/src/__tests__/tool-activity-args.test.ts
index 1383e66a75..63cb3cb161 100644
--- a/packages/core/src/__tests__/tool-activity-args.test.ts
+++ b/packages/core/src/__tests__/tool-activity-args.test.ts
@@ -95,6 +95,15 @@ it('projects WriteStdin activity to a bounded human-readable input preview', ()
   assert.equal(projectWriteStdinPermissionSummary(invalidSize).size, undefined);
 });
 
+it('never projects uncommitted todo_write arguments into activity history', () => {
+  assert.deepEqual(
+    projectToolActivityArgs('todo_write', {
+      todos: [{ content: 'uncommitted item', status: 'pending' }],
+    }),
+    {},
+  );
+});
+
 it('projects ordered terminal actions without exposing encoded control bytes', () => {
   const args = {
     ref: 'maka://runtime/background-tasks/one',
diff --git a/packages/core/src/__tests__/tool-quiet-preview.test.ts b/packages/core/src/__tests__/tool-quiet-preview.test.ts
index a64ff365d9..49037bf1e1 100644
--- a/packages/core/src/__tests__/tool-quiet-preview.test.ts
+++ b/packages/core/src/__tests__/tool-quiet-preview.test.ts
@@ -145,14 +145,13 @@ describe('projectToolArgsPreview', () => {
     assert.ok(JSON.stringify(preview).length <= 2048);
   });
 
-  it('excludes Task Ledger tools until their durable semantic projection owns identity', () => {
-    assert.equal(projectToolArgsPreview('task_create', { tasks: [{ subject: 'one' }] }), undefined);
+  it('never previews an uncommitted Todo replacement as current state', () => {
     assert.equal(
-      projectToolArgsPreview('task_update', { id: 'T1', status: 'completed' }),
+      projectToolArgsPreview('todo_write', {
+        todos: [{ content: 'one', status: 'pending' }],
+      }),
       undefined,
     );
-    assert.equal(projectToolArgsPreview('task_list', { status: 'pending' }), undefined);
-    assert.equal(projectToolArgsPreview('task_get', { id: 'T1' }), undefined);
   });
 
   it('does not accept forged question payloads from third-party tools', () => {
diff --git a/packages/core/src/session-todo.ts b/packages/core/src/session-todo.ts
index 0a2176c551..41eb6cdb8f 100644
--- a/packages/core/src/session-todo.ts
+++ b/packages/core/src/session-todo.ts
@@ -18,6 +18,8 @@
  */
 
 import { serializedByteLength } from './serialized-byte-length.js';
+import { redactSecrets } from './display-redaction.js';
+import { sanitizeUnicodeText } from './text-sanitize.js';
 
 /**
  * SessionTodo is a current-state document, not an event ledger. Its bounds
@@ -90,6 +92,31 @@ export function isSessionTodoStatus(value: unknown): value is SessionTodoStatus
   return typeof value === 'string' && (SESSION_TODO_STATUSES as readonly string[]).includes(value);
 }
 
+/** Project stored Todo text into a shared display-safe surface value. */
+export function sessionTodoContentForDisplay(content: string): string {
+  let current = redactSecrets(
+    sanitizeUnicodeText(content, {
+      maxCodePoints: SESSION_TODO_CONTENT_MAX_CHARS,
+      truncatedSuffix: '',
+    }),
+  );
+  const tag = /<\/?session-todo\b[^>]*>/gi;
+  for (;;) {
+    const next = current.replace(tag, '');
+    if (next === current) return current.trim();
+    current = next;
+  }
+}
+
+export function projectSessionTodoItemsForDisplay(
+  items: readonly SessionTodoItem[],
+): SessionTodoItem[] {
+  return items.map((item) => ({
+    content: sessionTodoContentForDisplay(item.content),
+    status: item.status,
+  }));
+}
+
 function invalid(message: string): SessionTodoNormalizeResult {
   return { ok: false, message };
 }
diff --git a/packages/core/src/tool-activity-args.ts b/packages/core/src/tool-activity-args.ts
index 42e4bd98df..d1d71f79a4 100644
--- a/packages/core/src/tool-activity-args.ts
+++ b/packages/core/src/tool-activity-args.ts
@@ -180,6 +180,9 @@ function isSafeProjectedInputText(text: string): boolean {
 }
 
 export function projectToolActivityArgs(toolName: string, args: unknown): unknown {
+  // A Todo replacement is only a proposal until its tool result settles. The
+  // durable transcript must not resurrect those args as committed state.
+  if (toolName === 'todo_write') return {};
   if (toolName !== 'WriteStdin') return args;
   const parsed = readWriteStdinArgs(args);
   if (!parsed) return {};
diff --git a/packages/core/src/tool-quiet-preview.ts b/packages/core/src/tool-quiet-preview.ts
index f3474dfb78..e2adb0d7b6 100644
--- a/packages/core/src/tool-quiet-preview.ts
+++ b/packages/core/src/tool-quiet-preview.ts
@@ -328,7 +328,7 @@ const ARGS_PREVIEW_MAX_CHARS = 2048;
 /** Question lists keep only their leading entries. */
 const ARGS_PREVIEW_LIST_MAX_ITEMS = 4;
 
-const TASK_LEDGER_TOOL_NAMES = new Set(['task_create', 'task_update', 'task_list', 'task_get']);
+const COMMIT_RESULT_ONLY_TOOL_NAMES = new Set(['todo_write']);
 
 /**
  * Whitelist of scalar args keys {@link formatToolInvocationLine} can read, in
@@ -435,10 +435,10 @@ export function projectToolArgsPreview(
 ): Record<string, unknown> | undefined {
   const record = asRecord(args);
   if (!record) return undefined;
-  // Task rows need committed IDs and the exact Task Ledger mutation snapshot;
-  // args alone cannot identify the user-facing task reliably. Keep them out of
-  // the generic live preview until the Host-owned semantic timeline (#4179).
-  if (TASK_LEDGER_TOOL_NAMES.has(toolName)) return undefined;
+  // A Todo write's args are only a proposal. Showing them while the call is
+  // live would present uncommitted state as fact; the settled tool_result owns
+  // the complete committed snapshot.
+  if (COMMIT_RESULT_ONLY_TOOL_NAMES.has(toolName)) return undefined;
 
   // Apply the canonical activity projection first so WriteStdin's inputPreview
   // shape (bounded, display-safe) is what the whitelist picks up.
diff --git a/packages/runtime-host/src/__tests__/connection-session.test.ts b/packages/runtime-host/src/__tests__/connection-session.test.ts
index 648ff53acb..ff6b96769b 100644
--- a/packages/runtime-host/src/__tests__/connection-session.test.ts
+++ b/packages/runtime-host/src/__tests__/connection-session.test.ts
@@ -1493,7 +1493,7 @@ function createHandlers(queryTurn: TurnQueryHandler): RuntimeHostComposition['ha
       message: 'not available in this test composition',
     },
   } as const;
-  const taskLedgerUnavailable: Awaited<ReturnType<OperationHandlerMap['task.ledger.query']>> = {
+  const sessionTodoUnavailable: Awaited<ReturnType<OperationHandlerMap['session.todo.query']>> = {
     ok: false,
     error: {
       code: 'operation_unavailable',
@@ -1529,7 +1529,7 @@ function createHandlers(queryTurn: TurnQueryHandler): RuntimeHostComposition['ha
     'interaction.answer': async () => interactionUnavailable,
     'subscription.open': async () => subscriptionUnavailable,
     'subscription.close': async () => subscriptionUnavailable,
-    'task.ledger.query': async () => taskLedgerUnavailable,
+    'session.todo.query': async () => sessionTodoUnavailable,
   };
 }
 
diff --git a/packages/runtime-host/src/__tests__/execution-host-message.test.ts b/packages/runtime-host/src/__tests__/execution-host-message.test.ts
index 114f59d393..5ecbfee7e0 100644
--- a/packages/runtime-host/src/__tests__/execution-host-message.test.ts
+++ b/packages/runtime-host/src/__tests__/execution-host-message.test.ts
@@ -45,7 +45,6 @@ import type { StoredMessage } from '@maka/core/session';
 import type { Task } from '@maka/core/task-ledger';
 import { isTerminalRuntimeEvent } from '@maka/core/runtime-event';
 import type { RuntimeEvent } from '@maka/core/runtime-event';
-import { buildTaskLedgerTools } from '@maka/runtime/task-ledger-tools';
 import {
   buildRecoveredTerminalRuntimeEvent,
   classifyTerminalRuntimeLedger,
@@ -68,7 +67,6 @@ import {
   tryAcquireInteractiveRootReader,
   type StorageRootCapability,
 } from '@maka/storage/root-authority';
-import { openInteractiveTaskLedgerStoreForWrite } from '@maka/storage/task-ledger-authority';
 import {
   connectRuntimeHost,
   RuntimeHostOperationError,
@@ -79,17 +77,13 @@ import {
 import {
   decodeHostFrame,
   RUNTIME_HOST_PROTOCOL_VERSION,
-  TASK_LEDGER_PAGE_MAX_ITEMS,
   type ConnectionCatalogQueryResult,
   type InteractionPendingSnapshot,
   type SubscriptionFrame,
-  type TaskLedgerQueryResult,
-  type TaskLedgerRevision,
   type TurnMessageSubmitInput,
   type TurnSnapshot,
 } from '../protocol/index.js';
 import { SessionAdmissionGate } from '../server/session-admission-gate.js';
-import { HostTaskLedgerCoordinator } from '../server/task-ledger-coordinator.js';
 import { FramedTransport } from '../transport/framed-transport.js';
 
 import {
diff --git a/packages/runtime-host/src/__tests__/execution-host-queue.test.ts b/packages/runtime-host/src/__tests__/execution-host-queue.test.ts
index b1174307fb..f5186466d4 100644
--- a/packages/runtime-host/src/__tests__/execution-host-queue.test.ts
+++ b/packages/runtime-host/src/__tests__/execution-host-queue.test.ts
@@ -45,7 +45,6 @@ import type { StoredMessage } from '@maka/core/session';
 import type { Task } from '@maka/core/task-ledger';
 import { isTerminalRuntimeEvent } from '@maka/core/runtime-event';
 import type { RuntimeEvent } from '@maka/core/runtime-event';
-import { buildTaskLedgerTools } from '@maka/runtime/task-ledger-tools';
 import {
   buildRecoveredTerminalRuntimeEvent,
   classifyTerminalRuntimeLedger,
@@ -69,7 +68,6 @@ import {
   tryAcquireInteractiveRootReader,
   type StorageRootCapability,
 } from '@maka/storage/root-authority';
-import { openInteractiveTaskLedgerStoreForWrite } from '@maka/storage/task-ledger-authority';
 import {
   connectRuntimeHost,
   RuntimeHostOperationError,
@@ -80,18 +78,14 @@ import {
 import {
   decodeHostFrame,
   RUNTIME_HOST_PROTOCOL_VERSION,
-  TASK_LEDGER_PAGE_MAX_ITEMS,
   type ConnectionCatalogQueryResult,
   type InteractionPendingSnapshot,
   type SubscriptionFrame,
-  type TaskLedgerQueryResult,
-  type TaskLedgerRevision,
   type TurnMessageSubmitInput,
   type TurnSnapshot,
   type TurnStartResult,
 } from '../protocol/index.js';
 import { SessionAdmissionGate } from '../server/session-admission-gate.js';
-import { HostTaskLedgerCoordinator } from '../server/task-ledger-coordinator.js';
 import { FramedTransport } from '../transport/framed-transport.js';
 
 import {
diff --git a/packages/runtime-host/src/__tests__/execution-host-recovery.test.ts b/packages/runtime-host/src/__tests__/execution-host-recovery.test.ts
index 15be5e471f..ae20e64aee 100644
--- a/packages/runtime-host/src/__tests__/execution-host-recovery.test.ts
+++ b/packages/runtime-host/src/__tests__/execution-host-recovery.test.ts
@@ -36,7 +36,6 @@ import type { StoredMessage } from '@maka/core/session';
 import type { Task } from '@maka/core/task-ledger';
 import { isTerminalRuntimeEvent } from '@maka/core/runtime-event';
 import type { RuntimeEvent } from '@maka/core/runtime-event';
-import { buildTaskLedgerTools } from '@maka/runtime/task-ledger-tools';
 import {
   buildRecoveredTerminalRuntimeEvent,
   classifyTerminalRuntimeLedger,
@@ -59,7 +58,6 @@ import {
   tryAcquireInteractiveRootReader,
   type StorageRootCapability,
 } from '@maka/storage/root-authority';
-import { openInteractiveTaskLedgerStoreForWrite } from '@maka/storage/task-ledger-authority';
 import {
   connectRuntimeHost,
   RuntimeHostOperationError,
@@ -70,17 +68,13 @@ import {
 import {
   decodeHostFrame,
   RUNTIME_HOST_PROTOCOL_VERSION,
-  TASK_LEDGER_PAGE_MAX_ITEMS,
   type ConnectionCatalogQueryResult,
   type InteractionPendingSnapshot,
   type SubscriptionFrame,
-  type TaskLedgerQueryResult,
-  type TaskLedgerRevision,
   type TurnMessageSubmitInput,
   type TurnSnapshot,
 } from '../protocol/index.js';
 import { SessionAdmissionGate } from '../server/session-admission-gate.js';
-import { HostTaskLedgerCoordinator } from '../server/task-ledger-coordinator.js';
 import { FramedTransport } from '../transport/framed-transport.js';
 
 import {
diff --git a/packages/runtime-host/src/__tests__/execution-host.test.ts b/packages/runtime-host/src/__tests__/execution-host.test.ts
index ba1cf2dc2b..9e3705c332 100644
--- a/packages/runtime-host/src/__tests__/execution-host.test.ts
+++ b/packages/runtime-host/src/__tests__/execution-host.test.ts
@@ -46,11 +46,11 @@ import {
   type StoredMessage,
 } from '@maka/core/session';
 import { markPersisted } from '@maka/core/persisted-value';
-import type { Task } from '@maka/core/task-ledger';
+import type { SessionTodoItem } from '@maka/core/session-todo';
 import type { ScheduledTask } from '@maka/core/scheduled-task';
 import { isTerminalRuntimeEvent } from '@maka/core/runtime-event';
 import type { RuntimeEvent } from '@maka/core/runtime-event';
-import { buildTaskLedgerTools } from '@maka/runtime/task-ledger-tools';
+import { buildSessionTodoTools } from '@maka/runtime/session-todo-tools';
 import {
   buildRecoveredTerminalRuntimeEvent,
   classifyTerminalRuntimeLedger,
@@ -74,7 +74,7 @@ import {
   tryAcquireInteractiveRootReader,
   type StorageRootCapability,
 } from '@maka/storage/root-authority';
-import { openInteractiveTaskLedgerStoreForWrite } from '@maka/storage/task-ledger-authority';
+import { openInteractiveSessionTodoStoreForWrite } from '@maka/storage/session-todo-authority';
 import {
   connectRuntimeHost,
   RuntimeHostOperationError,
@@ -85,17 +85,14 @@ import {
 import {
   decodeHostFrame,
   RUNTIME_HOST_PROTOCOL_VERSION,
-  TASK_LEDGER_PAGE_MAX_ITEMS,
   type ConnectionCatalogQueryResult,
   type InteractionPendingSnapshot,
   type SubscriptionFrame,
-  type TaskLedgerQueryResult,
-  type TaskLedgerRevision,
   type TurnMessageSubmitInput,
   type TurnSnapshot,
 } from '../protocol/index.js';
 import { SessionAdmissionGate } from '../server/session-admission-gate.js';
-import { HostTaskLedgerCoordinator } from '../server/task-ledger-coordinator.js';
+import { HostSessionTodoCoordinator } from '../server/session-todo-coordinator.js';
 import { FramedTransport } from '../transport/framed-transport.js';
 
 import {
@@ -240,137 +237,50 @@ test('production Host settles dispatched Client Capabilities before publishing R
   });
 });
 
-test('dual UDS Clients query persisted Task Ledger tool-port mutations across Host restart', async () => {
+test('dual UDS Clients query the same persisted SessionTodo snapshot across Host restart', async () => {
   await withExecutionRoot(async (fixture) => {
-    const initialRunId = randomUUID();
-    const initialTurnId = randomUUID();
-    // Exercise the Runtime-facing port before Host startup; Hosted tool composition is separate.
-    const toolPortProjection = await withOwnedTaskLedgerToolPort(
-      fixture,
-      async (coordinator, tools) => {
-        const context = taskLedgerToolContext(fixture, {
-          runId: initialRunId,
-          turnId: initialTurnId,
-          toolCallId: randomUUID(),
-        });
-        const create = requireTaskLedgerTool<TaskCreateInput>(tools, 'task_create');
-        const createInput = create.parameters.parse({
-          tasks: Array.from({ length: TASK_LEDGER_PAGE_MAX_ITEMS + 1 }, (_, index) => ({
-            subject: `Authority acceptance task ${index + 1}`,
-          })),
-        });
-        await create.impl(createInput, context);
-
-        const update = requireTaskLedgerTool<TaskUpdateInput>(tools, 'task_update');
-        const updateInput = update.parameters.parse({ id: 'T1', status: 'in_progress' });
-        await update.impl(updateInput, {
-          ...context,
-          toolCallId: randomUUID(),
-        });
-        return coordinator.list(fixture.sessionId, {
-          includeTerminal: true,
-          includeArchived: false,
-          classifyResumeTrust: true,
-        });
-      },
-    );
-    assert.equal(toolPortProjection.length, TASK_LEDGER_PAGE_MAX_ITEMS + 1);
-    assert.deepEqual(toolPortProjection[0]?.owner, {
-      actor: 'main_agent',
-      runId: initialRunId,
-      turnId: initialTurnId,
+    const initial: SessionTodoItem[] = Array.from({ length: 129 }, (_, index) => ({
+      content: `Authority acceptance todo ${index + 1}`,
+      status: index === 0 ? 'in_progress' : 'pending',
+    }));
+    await withOwnedSessionTodoToolPort(fixture, async (_coordinator, tools) => {
+      const write = requireSessionTodoWriteTool(tools);
+      await write.impl(write.parameters.parse({ todos: initial }), sessionTodoToolContext(fixture));
     });
 
     const host = await fixture.startHost();
     const desktop = await connectClient(fixture.root);
     const tui = await connectClient(fixture.root);
-    let staleContinuation:
-      | {
-          revision: TaskLedgerRevision;
-          cursor: string;
-          task: Task;
-        }
-      | undefined;
     try {
-      const desktopProjection = await collectTaskLedgerProjection(desktop, fixture.sessionId);
-      const tuiProjection = await collectTaskLedgerProjection(tui, fixture.sessionId);
-      assert.deepEqual(
-        desktopProjection.pages.map((page) => page.tasks.length),
-        [TASK_LEDGER_PAGE_MAX_ITEMS, 1],
-      );
+      const [desktopProjection, tuiProjection] = await Promise.all([
+        desktop.request('session.todo.query', { sessionId: fixture.sessionId }),
+        tui.request('session.todo.query', { sessionId: fixture.sessionId }),
+      ]);
+      assert.deepEqual(desktopProjection, { sessionId: fixture.sessionId, items: initial });
       assert.deepEqual(tuiProjection, desktopProjection);
-      assert.deepEqual(desktopProjection.tasks, toolPortProjection);
-
-      const byKey = await tui.request('task.ledger.query', {
-        kind: 'get',
-        sessionId: fixture.sessionId,
-        taskRef: 'T1',
-      });
-      assert.equal(byKey.kind, 'task');
-      if (byKey.kind !== 'task') throw new Error('Expected Task Ledger get result');
-      assert.equal(byKey.sessionId, fixture.sessionId);
-      assert.deepEqual(byKey.task, desktopProjection.tasks[0]);
-      assert.equal(byKey.task?.owner?.runId, initialRunId);
-      assert.equal(byKey.task?.owner?.turnId, initialTurnId);
-
-      const firstPage = desktopProjection.pages[0];
-      assert.ok(firstPage?.nextCursor);
-      staleContinuation = {
-        revision: firstPage.revision,
-        cursor: firstPage.nextCursor,
-        task: desktopProjection.tasks[1]!,
-      };
     } finally {
       await Promise.allSettled([desktop.close(), tui.close()]);
       await fixture.stopHost(host);
     }
 
-    assert.ok(staleContinuation);
-    const { revision: staleRevision, cursor: staleCursor, task: taskToChange } = staleContinuation;
-    const successorTurnId = randomUUID();
-    const changedSubject = `${taskToChange.subject} after authority reacquisition`;
-    await withOwnedTaskLedgerToolPort(fixture, async (_coordinator, tools) => {
-      const update = requireTaskLedgerTool<TaskUpdateInput>(tools, 'task_update');
-      const input = update.parameters.parse({
-        id: taskToChange.key,
-        subject: changedSubject,
-      });
-      await update.impl(
-        input,
-        taskLedgerToolContext(fixture, {
-          runId: randomUUID(),
-          turnId: successorTurnId,
-          toolCallId: randomUUID(),
-        }),
-      );
+    const changed = [
+      { content: 'Changed after authority reacquisition', status: 'completed' },
+    ] as const;
+    await withOwnedSessionTodoToolPort(fixture, async (_coordinator, tools) => {
+      const write = requireSessionTodoWriteTool(tools);
+      await write.impl(write.parameters.parse({ todos: changed }), sessionTodoToolContext(fixture));
     });
 
     const successorHost = await fixture.startHost();
     const successor = await connectClient(fixture.root);
     try {
-      const continued = await successor.request('task.ledger.query', {
-        kind: 'list_continue',
-        sessionId: fixture.sessionId,
-        revision: staleRevision,
-        cursor: staleCursor,
-      });
-      assert.equal(continued.kind, 'revision_changed');
-      if (continued.kind !== 'revision_changed') {
-        throw new Error('Expected stale Task Ledger continuation to report revision_changed');
-      }
-      assert.equal(continued.expected, staleRevision);
-      assert.notEqual(continued.actual, staleRevision);
-
-      const changed = await successor.request('task.ledger.query', {
-        kind: 'get',
-        sessionId: fixture.sessionId,
-        taskRef: taskToChange.key,
-      });
-      assert.equal(changed.kind, 'task');
-      if (changed.kind !== 'task') throw new Error('Expected changed Task Ledger task result');
-      assert.equal(changed.sessionId, fixture.sessionId);
-      assert.equal(changed.task?.subject, changedSubject);
-      assert.equal(changed.revision, continued.actual);
+      assert.deepEqual(
+        await successor.request('session.todo.query', { sessionId: fixture.sessionId }),
+        {
+          sessionId: fixture.sessionId,
+          items: changed,
+        },
+      );
     } finally {
       await successor.close();
       await fixture.stopHost(successorHost);
@@ -1213,108 +1123,52 @@ test('two UDS Clients settle one hosted sandbox boundary and resume its exact Ru
   });
 });
 
-interface TaskCreateInput {
-  tasks: Array<{ subject: string; parent_id?: string }>;
-}
-
-interface TaskUpdateInput {
-  id: string;
-  status?: 'pending' | 'in_progress' | 'blocked' | 'completed' | 'failed' | 'cancelled';
-  subject?: string;
-  blockedReason?: string;
-  failureReason?: string;
-  completionEvidence?: string;
-  explicitReopen?: boolean;
-}
-
-type TaskLedgerPage = Extract<TaskLedgerQueryResult, { kind: 'page' }>;
-type TaskLedgerTool<Input> = MakaTool<Input, string> & {
-  parameters: { parse(value: unknown): Input };
+type SessionTodoWriteTool = MakaTool<{ todos: SessionTodoItem[] }, string> & {
+  parameters: { parse(value: unknown): { todos: SessionTodoItem[] } };
 };
 
-async function withOwnedTaskLedgerToolPort<T>(
+async function withOwnedSessionTodoToolPort<T>(
   fixture: ExecutionFixture,
-  run: (coordinator: HostTaskLedgerCoordinator, tools: MakaTool[]) => Promise<T>,
+  run: (coordinator: HostSessionTodoCoordinator, tools: MakaTool[]) => Promise<T>,
 ): Promise<T> {
   const owner = await tryAcquireInteractiveRootOwner(fixture.capability);
   assert.ok(owner);
-  if (!owner) throw new Error('Unable to acquire the interactive Task Ledger tool port');
-  let writer: Awaited<ReturnType<typeof openInteractiveTaskLedgerStoreForWrite>> | undefined;
+  if (!owner) throw new Error('Unable to acquire the interactive SessionTodo tool port');
+  let writer: Awaited<ReturnType<typeof openInteractiveSessionTodoStoreForWrite>> | undefined;
   try {
-    writer = await openInteractiveTaskLedgerStoreForWrite(owner.lease);
-    const coordinator = new HostTaskLedgerCoordinator(writer, new SessionAdmissionGate(), {
-      probeSessionRemoval: async () => ({ kind: 'present' }),
-    });
-    return await run(coordinator, buildTaskLedgerTools({ store: coordinator }));
+    writer = await openInteractiveSessionTodoStoreForWrite(owner.lease);
+    const coordinator = new HostSessionTodoCoordinator(
+      writer,
+      new SessionAdmissionGate(),
+      { probeSessionRemoval: async () => ({ kind: 'present' }) },
+      () => {},
+      () => {},
+    );
+    return await run(coordinator, buildSessionTodoTools(coordinator));
   } finally {
     writer?.close();
     await owner.close();
   }
 }
 
-function requireTaskLedgerTool<Input>(
-  tools: readonly MakaTool[],
-  name: 'task_create' | 'task_update',
-): TaskLedgerTool<Input> {
-  const tool = tools.find((candidate) => candidate.name === name);
-  assert.ok(tool, `Expected ${name} Runtime tool`);
-  return tool as TaskLedgerTool<Input>;
+function requireSessionTodoWriteTool(tools: readonly MakaTool[]): SessionTodoWriteTool {
+  const tool = tools.find((candidate) => candidate.name === 'todo_write');
+  assert.ok(tool, 'Expected todo_write Runtime tool');
+  return tool as SessionTodoWriteTool;
 }
 
-function taskLedgerToolContext(
-  fixture: ExecutionFixture,
-  identity: Pick<MakaToolContext, 'runId' | 'turnId' | 'toolCallId'>,
-): MakaToolContext {
+function sessionTodoToolContext(fixture: ExecutionFixture): MakaToolContext {
   return {
     sessionId: fixture.sessionId,
     cwd: fixture.root,
-    ...identity,
+    runId: randomUUID(),
+    turnId: randomUUID(),
+    toolCallId: randomUUID(),
     abortSignal: new AbortController().signal,
     emitOutput: () => {},
   };
 }
 
-async function collectTaskLedgerProjection(
-  client: RuntimeHostConnection,
-  sessionId: string,
-): Promise<{
-  revision: TaskLedgerRevision;
-  pages: TaskLedgerPage[];
-  tasks: Task[];
-}> {
-  const pages: TaskLedgerPage[] = [];
-  let result = await client.request('task.ledger.query', {
-    kind: 'list_start',
-    sessionId,
-  });
-  assert.equal(result.kind, 'page');
-  if (result.kind !== 'page') throw new Error('Expected initial Task Ledger page');
-  const revision = result.revision;
-
-  while (true) {
-    assert.equal(result.sessionId, sessionId);
-    assert.equal(result.revision, revision);
-    pages.push(result);
-    if (result.nextCursor === null) break;
-    result = await client.request('task.ledger.query', {
-      kind: 'list_continue',
-      sessionId,
-      revision,
-      cursor: result.nextCursor,
-    });
-    assert.equal(result.kind, 'page');
-    if (result.kind !== 'page') {
-      throw new Error('Task Ledger changed while collecting a stable projection');
-    }
-  }
-
-  return {
-    revision,
-    pages,
-    tasks: pages.flatMap((page) => page.tasks),
-  };
-}
-
 async function waitForScheduledTaskCompletion(
   client: RuntimeHostConnection,
   taskId: string,
diff --git a/packages/runtime-host/src/__tests__/execution-model-composition.test.ts b/packages/runtime-host/src/__tests__/execution-model-composition.test.ts
index 5d7b8eeaeb..a687759586 100644
--- a/packages/runtime-host/src/__tests__/execution-model-composition.test.ts
+++ b/packages/runtime-host/src/__tests__/execution-model-composition.test.ts
@@ -44,7 +44,7 @@ import { type ModelCallAttempt, type ModelCallKind } from '@maka/core/model-call
 import { type RuntimeEvent } from '@maka/core/runtime-event';
 import { createDefaultRuntimePolicy } from '@maka/core/runtime-policy';
 import type { PlanSessionState, PlanStore } from '@maka/core/plan';
-import type { TaskLedgerStore } from '@maka/core/task-ledger';
+import type { SessionTodoToolStore } from '@maka/runtime/session-todo-tools';
 import {
   serializeOAuthSubscriptionTokens,
   type OAuthSubscriptionTokens,
@@ -75,7 +75,7 @@ import {
   type RuntimePolicyStoresWriter,
 } from '@maka/storage/runtime-policy-stores';
 import { resolveStorageRoot, tryAcquireInteractiveRootOwner } from '@maka/storage/root-authority';
-import { openInteractiveTaskLedgerStoreForWrite } from '@maka/storage/task-ledger-authority';
+import { openInteractiveSessionTodoStoreForWrite } from '@maka/storage/session-todo-authority';
 import {
   openInteractiveUsageStoresForWrite,
   type InteractiveUsageStoresWriter,
@@ -1806,8 +1806,10 @@ test('production Host executes a canonical ai-sdk Session against a real provide
       model: MODEL_ID,
       permissionMode: 'ask',
     });
-    const taskLedger = await openInteractiveTaskLedgerStoreForWrite(owner.lease);
-    await taskLedger.create(session.id, [{ subject: 'HOSTED_TASK_LEDGER_SENTINEL' }]);
+    const sessionTodo = await openInteractiveSessionTodoStoreForWrite(owner.lease);
+    await sessionTodo.replaceAll(session.id, [
+      { content: 'HOSTED_SESSION_TODO_SENTINEL', status: 'pending' },
+    ]);
 
     composition = await createExecutionRuntimeHostComposition(
       {
@@ -1907,7 +1909,7 @@ test('production Host executes a canonical ai-sdk Session against a real provide
     assert.match(requestText, /HOSTED_SKILL_DESCRIPTION_SENTINEL/);
     assert.doesNotMatch(requestText, /HOSTED_SKILL_BODY_MUST_STAY_LAZY/);
     assert.match(requestText, /HOSTED_WORKSPACE_SENTINEL/);
-    assert.doesNotMatch(requestText, /HOSTED_TASK_LEDGER_SENTINEL/);
+    assert.doesNotMatch(requestText, /HOSTED_SESSION_TODO_SENTINEL/);
     assert.match(requestText, /HOSTED_PERSONALIZATION_SENTINEL/);
     assert.match(requestText, /HOSTED_MEMORY_SENTINEL/);
     assert.match(JSON.stringify(mainRequests[1]?.body), /HOSTED_SKILL_BODY_MUST_STAY_LAZY/);
@@ -3282,7 +3284,7 @@ test('one turn shares one canonical Skill inventory across prompt and lazy tools
     runtimePolicy: policy,
     skills,
     memory,
-    taskLedger: {} as TaskLedgerStore,
+    sessionTodo: {} as SessionTodoToolStore,
   });
   const firstContext = {
     sessionId: 'session',
@@ -3368,7 +3370,7 @@ test('one composer freezes Runtime Policy while each Run freezes its remaining p
         body: memoryBody,
       }),
     } as unknown as HostMemoryCoordinator,
-    taskLedger: {} as TaskLedgerStore,
+    sessionTodo: {} as SessionTodoToolStore,
   });
   const context = {
     sessionId: 'session',
@@ -3419,7 +3421,7 @@ test('one composer freezes Runtime Policy while each Run freezes its remaining p
         body: memoryBody,
       }),
     } as unknown as HostMemoryCoordinator,
-    taskLedger: {} as TaskLedgerStore,
+    sessionTodo: {} as SessionTodoToolStore,
   });
   assert.deepEqual(
     (await nextComposition.resolveSystemPrompt({ ...context, turnId: 'turn-3' })).sourceRevisions,
@@ -3469,7 +3471,7 @@ test('backend composition survives a moved saved Git Bash executable while Bash
         body: '',
       }),
     } as unknown as HostMemoryCoordinator,
-    taskLedger: { list: async () => [] } as unknown as TaskLedgerStore,
+    sessionTodo: {} as SessionTodoToolStore,
     clientCapabilities: {
       snapshotForSession: () => undefined,
     } as unknown as HostClientCapabilityCoordinator,
@@ -3536,7 +3538,6 @@ test('backend composition survives a moved saved Git Bash executable while Bash
     },
   };
   const capturedChildTools = createHostChildAgentToolComposition({
-    taskLedger: {} as TaskLedgerStore,
     builtinTools: { shell: capturedChildShell },
     hostTools: [],
     worktreePatchWriteBackAvailable: true,
@@ -3572,7 +3573,6 @@ test('child execution Bash carries the configured shell guidance and spawn plan'
     },
   };
   const composition = createHostChildAgentToolComposition({
-    taskLedger: {} as TaskLedgerStore,
     builtinTools: {
       shell,
       shellRuns: {
@@ -3648,7 +3648,7 @@ test('a bound tool ceiling excludes dynamic Client Capability tools', () => {
       readCanonicalModelInventory: async () => ({ inventory: [] }),
     } as unknown as HostSkillCatalogCoordinator,
     memory: {} as HostMemoryCoordinator,
-    taskLedger: {} as TaskLedgerStore,
+    sessionTodo: {} as SessionTodoToolStore,
     boundTools: [boundTool],
     parentAgentTools: buildParentAgentTools(),
     scheduledTaskTool,
@@ -3685,7 +3685,7 @@ test('the headless coding profile freezes the Eval prompt and tool ceiling', asy
         throw new Error('Profiled prompt must not read product Memory');
       },
     } as unknown as HostMemoryCoordinator,
-    taskLedger: {} as TaskLedgerStore,
+    sessionTodo: {} as SessionTodoToolStore,
     builtinTools: {},
     toolProfile: 'headless-coding-v1',
     parentAgentTools: buildParentAgentTools(),
@@ -3962,7 +3962,7 @@ function backendCreationFixture(input: {
           body: '',
         }),
       } as unknown as HostMemoryCoordinator,
-      taskLedger: { list: async () => [] } as unknown as TaskLedgerStore,
+      sessionTodo: {} as SessionTodoToolStore,
       clientCapabilities: {
         snapshotForSession: input.snapshotClientCapabilities ?? (() => undefined),
       } as unknown as HostClientCapabilityCoordinator,
diff --git a/packages/runtime-host/src/__tests__/fixtures/execution-host-suite.ts b/packages/runtime-host/src/__tests__/fixtures/execution-host-suite.ts
index 641299cfb6..975b130a78 100644
--- a/packages/runtime-host/src/__tests__/fixtures/execution-host-suite.ts
+++ b/packages/runtime-host/src/__tests__/fixtures/execution-host-suite.ts
@@ -52,7 +52,6 @@ import type { Task } from '@maka/core/task-ledger';
 import { isTerminalRuntimeEvent } from '@maka/core/runtime-event';
 import type { RuntimeEvent } from '@maka/core/runtime-event';
 import { BackendRegistry, SessionManager } from '@maka/runtime/session-manager';
-import { buildTaskLedgerTools } from '@maka/runtime/task-ledger-tools';
 import {
   buildRecoveredTerminalRuntimeEvent,
   classifyTerminalRuntimeLedger,
@@ -77,7 +76,6 @@ import {
   tryAcquireInteractiveRootReader,
   type StorageRootCapability,
 } from '@maka/storage/root-authority';
-import { openInteractiveTaskLedgerStoreForWrite } from '@maka/storage/task-ledger-authority';
 import { resolveWorkspaceIdentity } from '@maka/storage/workspace-identity';
 import {
   connectRuntimeHost,
@@ -93,19 +91,15 @@ import {
   encodeProtocolMessage,
   RUNTIME_HOST_COMPATIBILITY_EPOCH,
   RUNTIME_HOST_PROTOCOL_VERSION,
-  TASK_LEDGER_PAGE_MAX_ITEMS,
   type ClientFrame,
   type ConnectionCatalogQueryResult,
   type InteractionPendingSnapshot,
   type SubscriptionFrame,
-  type TaskLedgerQueryResult,
-  type TaskLedgerRevision,
   type TurnMessageSubmitInput,
   type TurnSnapshot,
   type TurnStartResult,
 } from '../../protocol/index.js';
 import { SessionAdmissionGate } from '../../server/session-admission-gate.js';
-import { HostTaskLedgerCoordinator } from '../../server/task-ledger-coordinator.js';
 import { continuationSafetyDigest } from '../../server/root-turn-coordinator.js';
 import { FramedTransport } from '../../transport/framed-transport.js';
 import { removePosixEndpointDirectories } from './endpoint-hygiene.js';
diff --git a/packages/runtime-host/src/__tests__/fixtures/uncooperative-host.ts b/packages/runtime-host/src/__tests__/fixtures/uncooperative-host.ts
index efd8be6bcb..0b1c2ea486 100644
--- a/packages/runtime-host/src/__tests__/fixtures/uncooperative-host.ts
+++ b/packages/runtime-host/src/__tests__/fixtures/uncooperative-host.ts
@@ -92,7 +92,7 @@ const host = await RuntimeHostKernel.start({
           ok: false,
           error: { code: 'operation_unavailable', message: 'Operation unavailable in test Host' },
         }),
-        'task.ledger.query': async () => ({
+        'session.todo.query': async () => ({
           ok: false,
           error: { code: 'operation_unavailable', message: 'Operation unavailable in test Host' },
         }),
diff --git a/packages/runtime-host/src/__tests__/goal-coordinator.test.ts b/packages/runtime-host/src/__tests__/goal-coordinator.test.ts
index e57d82ad12..09b7ea9953 100644
--- a/packages/runtime-host/src/__tests__/goal-coordinator.test.ts
+++ b/packages/runtime-host/src/__tests__/goal-coordinator.test.ts
@@ -85,7 +85,6 @@ test('one Host Goal is shared across clients with CAS control and crash-clear re
           start: () => goalTurn,
         };
       },
-      listActionableTaskKeys: async () => [],
       acquireResidency: () => {
         acquired++;
         return { release: () => released++ };
@@ -215,7 +214,6 @@ test('one Host Goal is shared across clients with CAS control and crash-clear re
         kind: 'unavailable',
         reason: 'Recovery assertion only',
       }),
-      listActionableTaskKeys: async () => [],
       acquireResidency: () => ({ release() {} }),
       onProjectionChanged: () => {},
       requestDrain: () => {},
@@ -286,7 +284,6 @@ test('session retirement forgets a terminal Goal without recreating deleted auth
         close: async () => {},
       },
       admitTurn: () => assert.fail('A terminal Goal must not admit a continuation'),
-      listActionableTaskKeys: async () => [],
       acquireResidency: () => assert.fail('A terminal Goal must not retain Host residency'),
       onProjectionChanged: (sessionId) => projectionChanges.push(sessionId),
       requestDrain: () => drainRequests++,
@@ -408,7 +405,6 @@ test('restart settles the durable current Goal execution through Hosted Executio
         close: async () => {},
       },
       admitTurn: () => assert.fail('A terminal recovered execution must not be admitted again'),
-      listActionableTaskKeys: async () => [],
       acquireResidency: () => ({ release() {} }),
       onProjectionChanged: () => {},
       requestDrain: () => {
@@ -498,7 +494,6 @@ test('restart replaces a stale current execution with the current durable Goal i
         recoveredIntent = true;
         return { kind: 'unavailable', reason: 'Recovery assertion only' };
       },
-      listActionableTaskKeys: async () => [],
       acquireResidency: () => ({ release() {} }),
       onProjectionChanged: () => {},
       requestDrain: () => {},
@@ -584,7 +579,6 @@ test('goal.arm creates one Goal per Session and refuses a second while it is unf
       },
       // Arming schedules nothing: the Goal takes hold on the next Turn.
       admitTurn: () => assert.fail('Arming must not admit a continuation Turn'),
-      listActionableTaskKeys: async () => [],
       acquireResidency: () => ({ release: () => {} }),
       onProjectionChanged: () => {},
       requestDrain: () => {},
@@ -698,7 +692,6 @@ test('a Goal armed but never carried by a Turn does not start itself after a res
         close: async () => {},
       },
       admitTurn: () => assert.fail('Arming must not admit a continuation Turn'),
-      listActionableTaskKeys: async () => [],
       acquireResidency: () => ({ release: () => {} }),
       onProjectionChanged: () => {},
       requestDrain: () => {},
@@ -744,7 +737,6 @@ test('a Goal armed but never carried by a Turn does not start itself after a res
         admitted = true;
         return { kind: 'unavailable', reason: 'Recovery assertion only' };
       },
-      listActionableTaskKeys: async () => [],
       acquireResidency: () => ({ release: () => {} }),
       onProjectionChanged: () => {},
       requestDrain: () => {},
@@ -811,7 +803,6 @@ test('resuming an armed Goal drives it, and a restart puts that drive back', asy
         admitted += 1;
         return { kind: 'busy', whenIdle: new Promise<void>(() => {}) };
       },
-      listActionableTaskKeys: async () => [],
       acquireResidency: () => ({ release: () => {} }),
       onProjectionChanged: () => {},
       requestDrain: () => {},
@@ -895,7 +886,6 @@ test('resuming an armed Goal drives it, and a restart puts that drive back', asy
         admittedAfterRestart += 1;
         return { kind: 'busy', whenIdle: new Promise<void>(() => {}) };
       },
-      listActionableTaskKeys: async () => [],
       acquireResidency: () => ({ release: () => {} }),
       onProjectionChanged: () => {},
       requestDrain: () => {},
@@ -947,7 +937,6 @@ test('an arm admitted before the drain creates no Goal after it', async () => {
         close: async () => {},
       },
       admitTurn: () => assert.fail('A refused arm must not admit a Turn'),
-      listActionableTaskKeys: async () => [],
       acquireResidency: () => ({ release: () => {} }),
       onProjectionChanged: () => {},
       requestDrain: () => {},
diff --git a/packages/runtime-host/src/__tests__/goal-root-authority.test.ts b/packages/runtime-host/src/__tests__/goal-root-authority.test.ts
index 48b47811e4..fcbc66a1a3 100644
--- a/packages/runtime-host/src/__tests__/goal-root-authority.test.ts
+++ b/packages/runtime-host/src/__tests__/goal-root-authority.test.ts
@@ -675,7 +675,6 @@ async function createFixture(options: { recoverAdmissions?: boolean } = {}): Pro
     },
     admitTurn: (sessionId, text, checkpoint, controlLease) =>
       goalExecutions.admitTurn(sessionId, text, checkpoint, controlLease),
-    listActionableTaskKeys: async () => [],
     acquireResidency,
     onProjectionChanged: (sessionId) => {
       requireContinuity(continuity).enqueueCanonicalRefresh(sessionId);
diff --git a/packages/runtime-host/src/__tests__/interactive-run-composer.test.ts b/packages/runtime-host/src/__tests__/interactive-run-composer.test.ts
index 6c1338603b..c2bac3e5bc 100644
--- a/packages/runtime-host/src/__tests__/interactive-run-composer.test.ts
+++ b/packages/runtime-host/src/__tests__/interactive-run-composer.test.ts
@@ -20,7 +20,7 @@
 import assert from 'node:assert/strict';
 import { test } from 'node:test';
 import { createDefaultRuntimePolicy } from '@maka/core/runtime-policy';
-import type { TaskLedgerStore } from '@maka/core/task-ledger';
+import type { SessionTodoToolStore } from '@maka/runtime/session-todo-tools';
 import type { MakaTool } from '@maka/runtime/tool-runtime';
 import { createInteractiveRunComposer } from '../server/interactive-run-composer.js';
 import type { HostMemoryCoordinator } from '../server/memory-coordinator.js';
@@ -65,7 +65,7 @@ function createFixtureComposer(
       readCanonicalModelInventory: async () => ({ inventory: [] }),
     } as unknown as HostSkillCatalogCoordinator,
     memory: {} as HostMemoryCoordinator,
-    taskLedger: {} as TaskLedgerStore,
+    sessionTodo: {} as SessionTodoToolStore,
     builtinTools: {},
     ...overrides,
   });
diff --git a/packages/runtime-host/src/__tests__/session-catalog-two-client-uds.test.ts b/packages/runtime-host/src/__tests__/session-catalog-two-client-uds.test.ts
index e80ebd5f0a..aa3e5c6784 100644
--- a/packages/runtime-host/src/__tests__/session-catalog-two-client-uds.test.ts
+++ b/packages/runtime-host/src/__tests__/session-catalog-two-client-uds.test.ts
@@ -27,6 +27,7 @@ import { connect, type Socket } from 'node:net';
 import { tmpdir } from 'node:os';
 import { join } from 'node:path';
 import { test } from 'node:test';
+import { DatabaseSync } from 'node:sqlite';
 import { DEEP_RESEARCH_SESSION_LABEL, DEEP_RESEARCH_SESSION_NAME } from '@maka/core/deep-research';
 import { openInteractiveArtifactStoreForWrite } from '@maka/storage/artifact-stores';
 import { openInteractiveExecutionStoresForWrite } from '@maka/storage/execution-stores';
@@ -37,7 +38,7 @@ import {
   tryAcquireInteractiveRootOwner,
   type StorageRootCapability,
 } from '@maka/storage/root-authority';
-import { openInteractiveTaskLedgerStoreForWrite } from '@maka/storage/task-ledger-authority';
+import { openInteractiveSessionTodoStoreForWrite } from '@maka/storage/session-todo-authority';
 import {
   connectRuntimeHost,
   readRuntimeHostConnectionCatalog,
@@ -511,6 +512,10 @@ test('two Clients share stable Session creation, CAS configuration, and catalog
       );
       assert.equal(archived.isArchived, true);
       assert.equal(archived.status, beforeArchive.status);
+      assert.deepEqual(
+        (await desktop.request('session.todo.query', { sessionId: created.id })).items,
+        [{ content: 'Retain archived task', status: 'in_progress' }],
+      );
       assert.equal((await querySession(tui, created.id)).isArchived, true);
       const archivedContinuity = await nextProjection(retirementIterator);
       assert.equal(archivedContinuity.snapshot.session.isArchived, true);
@@ -525,6 +530,9 @@ test('two Clients share stable Session creation, CAS configuration, and catalog
       );
       assert.equal(restored.isArchived, false);
       assert.equal(restored.status, beforeArchive.status);
+      assert.deepEqual((await tui.request('session.todo.query', { sessionId: created.id })).items, [
+        { content: 'Retain archived task', status: 'in_progress' },
+      ]);
       const restoredContinuity = await nextProjection(retirementIterator);
       assert.equal(restoredContinuity.snapshot.session.isArchived, false);
       assert.equal(restoredContinuity.snapshot.session.status, beforeArchive.status);
@@ -579,15 +587,10 @@ test('two Clients share stable Session creation, CAS configuration, and catalog
         assert.fail('Retirement Artifact must be readable before Session removal');
       }
       assert.equal(artifactBeforeRemoval.artifact?.id, 'retirement-artifact');
-      const tasksBeforeRemoval = await tui.request('task.ledger.query', {
-        kind: 'list_start',
+      const todoBeforeRemoval = await tui.request('session.todo.query', {
         sessionId: retirementSessionId,
       });
-      assert.equal(tasksBeforeRemoval.kind, 'page');
-      if (tasksBeforeRemoval.kind !== 'page') {
-        assert.fail('Retirement Task Ledger must be readable before Session removal');
-      }
-      assert.equal(tasksBeforeRemoval.tasks.length, 1);
+      assert.equal(todoBeforeRemoval.items.length, 1);
 
       assert.deepEqual(
         await desktop.request('session.remove', {
@@ -606,10 +609,7 @@ test('two Clients share stable Session creation, CAS configuration, and catalog
           operationError('not_found'),
         );
         await assert.rejects(
-          connection.request('task.ledger.query', {
-            kind: 'list_start',
-            sessionId: retirementSessionId,
-          }),
+          connection.request('session.todo.query', { sessionId: retirementSessionId }),
           operationError('not_found'),
         );
       }
@@ -622,10 +622,7 @@ test('two Clients share stable Session creation, CAS configuration, and catalog
         operationError('not_found'),
       );
       await assert.rejects(
-        tui.request('task.ledger.query', {
-          kind: 'list_start',
-          sessionId: recoverySessionId,
-        }),
+        tui.request('session.todo.query', { sessionId: recoverySessionId }),
         operationError('not_found'),
       );
     } finally {
@@ -889,7 +886,7 @@ async function seedAuthority(
       permissionMode: 'ask',
     });
     const artifacts = await openInteractiveArtifactStoreForWrite(owner.lease);
-    const tasks = await openInteractiveTaskLedgerStoreForWrite(owner.lease);
+    const todos = await openInteractiveSessionTodoStoreForWrite(owner.lease);
     await artifacts.recover();
     await Promise.all([
       artifacts.create({
@@ -914,8 +911,13 @@ async function seedAuthority(
         source: 'fixture',
         now: 2,
       }),
-      tasks.create(retirement.id, [{ subject: 'Remove retirement task' }]),
-      tasks.create(recovery.id, [{ subject: 'Recover retirement task cleanup' }]),
+      todos.replaceAll(retirement.id, [{ content: 'Remove retirement task', status: 'pending' }]),
+      todos.replaceAll('stable-session', [
+        { content: 'Retain archived task', status: 'in_progress' },
+      ]),
+      todos.replaceAll(recovery.id, [
+        { content: 'Recover retirement task cleanup', status: 'pending' },
+      ]),
     ]);
     const retirementSnapshot = await execution.sessionStore.readHeaderRecordSnapshot(retirement.id);
     await execution.sessionStore.remove(recovery.id);
@@ -994,12 +996,31 @@ async function assertRetirementCleanup(
   try {
     const execution = await openInteractiveExecutionStoresForWrite(owner.lease);
     const artifacts = await openInteractiveArtifactStoreForWrite(owner.lease);
-    const tasks = await openInteractiveTaskLedgerStoreForWrite(owner.lease);
     await artifacts.recover();
     assert.deepEqual(await execution.sessionStore.listPendingSessionRetirementCleanupIds(), []);
     for (const sessionId of sessionIds) {
       assert.equal((await artifacts.listPage(sessionId, { offset: 0, limit: 1 })).total, 0);
-      assert.deepEqual(await tasks.list(sessionId, { includeTerminal: true }), []);
+      const database = new DatabaseSync(join(root, 'runtime.sqlite'), { readOnly: true });
+      try {
+        assert.equal(
+          database
+            .prepare(
+              'SELECT COUNT(*) AS count FROM workflow_session_todo_documents WHERE session_id = ?',
+            )
+            .get(sessionId)!.count,
+          0,
+        );
+        assert.equal(
+          database
+            .prepare(
+              'SELECT COUNT(*) AS count FROM workflow_task_ledger_events WHERE session_id = ?',
+            )
+            .get(sessionId)!.count,
+          0,
+        );
+      } finally {
+        database.close();
+      }
     }
   } finally {
     await owner.close();
diff --git a/packages/runtime-host/src/__tests__/session-continuity-coordinator.test.ts b/packages/runtime-host/src/__tests__/session-continuity-coordinator.test.ts
index 1b87a72379..8d53de77e2 100644
--- a/packages/runtime-host/src/__tests__/session-continuity-coordinator.test.ts
+++ b/packages/runtime-host/src/__tests__/session-continuity-coordinator.test.ts
@@ -713,8 +713,8 @@ test('coalesces typed domain invalidations without publishing continuity project
   const opened = await open(coordinator, 'connection-1');
   connection.activate(opened.subscriptionId);
 
-  coordinator.enqueueSessionDomainChanged(SESSION_ID, 'task');
-  coordinator.enqueueSessionDomainChanged(SESSION_ID, 'task');
+  coordinator.enqueueSessionDomainChanged(SESSION_ID, 'todo');
+  coordinator.enqueueSessionDomainChanged(SESSION_ID, 'todo');
   coordinator.enqueueSessionDomainChanged(SESSION_ID, 'plan');
   coordinator.enqueueSessionDomainChanged(SESSION_ID, 'usage');
   await waitFor(() => sink.frames.length === 3);
@@ -726,7 +726,7 @@ test('coalesces typed domain invalidations without publishing continuity project
         : frame.kind,
     ),
     [
-      { kind: 'subscription.session_domain_changed', sequence: 1, domain: 'task' },
+      { kind: 'subscription.session_domain_changed', sequence: 1, domain: 'todo' },
       { kind: 'subscription.session_domain_changed', sequence: 2, domain: 'plan' },
       { kind: 'subscription.session_domain_changed', sequence: 3, domain: 'usage' },
     ],
diff --git a/packages/runtime-host/src/__tests__/session-retirement-coordinator.test.ts b/packages/runtime-host/src/__tests__/session-retirement-coordinator.test.ts
index feb4a50b4c..be4674ea93 100644
--- a/packages/runtime-host/src/__tests__/session-retirement-coordinator.test.ts
+++ b/packages/runtime-host/src/__tests__/session-retirement-coordinator.test.ts
@@ -1210,8 +1210,8 @@ async function withHarness(
           actions.purgedArtifacts.push(sessionId);
         },
       },
-      taskLedger: {
-        purgeConversationTaskLedger: async (sessionId) => {
+      sessionTodo: {
+        purgeSessionState: async (sessionId) => {
           actions.purgedTasks.push(sessionId);
         },
       },
diff --git a/packages/runtime-host/src/__tests__/session-revision-two-client-uds.test.ts b/packages/runtime-host/src/__tests__/session-revision-two-client-uds.test.ts
index ea8b2527ee..f978d1a02e 100644
--- a/packages/runtime-host/src/__tests__/session-revision-two-client-uds.test.ts
+++ b/packages/runtime-host/src/__tests__/session-revision-two-client-uds.test.ts
@@ -41,7 +41,7 @@ import {
   tryAcquireInteractiveRootOwner,
   type StorageRootCapability,
 } from '@maka/storage/root-authority';
-import { openInteractiveTaskLedgerStoreForWrite } from '@maka/storage/task-ledger-authority';
+import { openInteractiveSessionTodoStoreForWrite } from '@maka/storage/session-todo-authority';
 import { removePosixEndpointDirectories } from './fixtures/endpoint-hygiene.js';
 import { requireStartedTurn } from './fixtures/execution-host-suite.js';
 import {
@@ -330,6 +330,14 @@ async function verifyConcurrentRevisionAuthority(
       intent: 'side_conversation',
     });
     assert.equal(archivedSideConversation.kind, 'committed');
+    assert.deepEqual(
+      (
+        await tui.request('session.todo.query', {
+          sessionId: ARCHIVED_SIDE_CONVERSATION_TARGET_ID,
+        })
+      ).items,
+      [],
+    );
     for (const sessionId of ['metadata-linked-copy-target', 'archived-owned-copy-target']) {
       assert.deepEqual(
         await tui.request('session.catalog.query', {
@@ -366,16 +374,25 @@ async function verifyConcurrentRevisionAuthority(
     if (artifactPage.kind !== 'page') assert.fail('Branch Artifact query must return a page');
     assert.equal(artifactPage.artifacts.length, 3);
     assert.notEqual(artifactPage.artifacts[0]?.id, 'source-artifact');
-    const taskPage = await tui.request('task.ledger.query', {
-      kind: 'list_start',
-      sessionId: branch.id,
+    const todo = await tui.request('session.todo.query', { sessionId: branch.id });
+    assert.deepEqual(todo.items, []);
+
+    const latestBranch = await desktop.request('session.branch.create', {
+      ...branchInput,
+      targetSessionId: 'latest-branch-target',
+      sourceTurnId: 'turn-2',
     });
-    assert.equal(taskPage.kind, 'page');
-    if (taskPage.kind !== 'page') assert.fail('Branch Task Ledger query must return a page');
-    assert.deepEqual(taskPage.tasks.map((task) => task.subject).sort(), [
-      'Legacy child task',
-      'Retained task',
-    ]);
+    assert.equal(latestBranch.kind, 'committed');
+    assert.deepEqual(
+      (
+        await tui.request('session.todo.query', {
+          sessionId: 'latest-branch-target',
+        })
+      ).items
+        .map((item) => item.content)
+        .sort(),
+      ['Legacy child task', 'Retained task'],
+    );
 
     const renamed = await desktop.request('session.metadata.update', {
       sessionId: sourceSessionId,
@@ -750,7 +767,7 @@ async function seedSource(
   try {
     const execution = await openInteractiveExecutionStoresForWrite(owner.lease);
     const artifacts = await openInteractiveArtifactStoreForWrite(owner.lease);
-    const tasks = await openInteractiveTaskLedgerStoreForWrite(owner.lease);
+    const todos = await openInteractiveSessionTodoStoreForWrite(owner.lease);
     await artifacts.recover();
     const source = await execution.sessionStore.create({
       cwd: root,
@@ -1533,27 +1550,10 @@ async function seedSource(
     for (const event of archivedOwnedRuntimeEvents) {
       await execution.runtimeEventStore.appendRuntimeEvent(event.sessionId, event.runId, event);
     }
-    await tasks.create(source.id, [{ subject: 'Retained task' }], {
-      turnId: 'turn-1',
-      source: 'tool',
-      actor: 'main_agent',
-    });
-    await tasks.create(source.id, [{ subject: 'Legacy child task' }], {
-      turnId: 'legacy-child-turn',
-      runId: 'legacy-child-run',
-      source: 'tool',
-      actor: 'child_agent',
-    });
-    await tasks.update(
-      source.id,
-      (await tasks.list(source.id))[0]!.id,
-      { status: 'in_progress' },
-      {
-        turnId: 'turn-2',
-        source: 'tool',
-        actor: 'main_agent',
-      },
-    );
+    await todos.replaceAll(source.id, [
+      { content: 'Retained task', status: 'in_progress' },
+      { content: 'Legacy child task', status: 'pending' },
+    ]);
     return {
       sourceSessionId: source.id,
       busySessionId: busy.id,
@@ -1589,7 +1589,7 @@ async function verifyDurableBranch(
   try {
     const execution = await openInteractiveExecutionStoresForWrite(owner.lease);
     const artifacts = await openInteractiveArtifactStoreForWrite(owner.lease);
-    const tasks = await openInteractiveTaskLedgerStoreForWrite(owner.lease);
+    const todos = await openInteractiveSessionTodoStoreForWrite(owner.lease);
     await artifacts.recover();
     // Every copy kind that retains the upload turn must carry a rewritten,
     // readable copy of the user-uploaded attachment (regression guard for the
@@ -1627,11 +1627,13 @@ async function verifyDurableBranch(
       ok: true,
       text: 'retained bytes',
     });
-    assert.deepEqual((await tasks.list(branchSessionId)).map((task) => task.subject).sort(), [
-      'Legacy child task',
-      'Retained task',
-    ]);
-    assert.ok((await tasks.list(branchSessionId)).every((task) => task.status === 'pending'));
+    assert.deepEqual(await todos.readOrBootstrap(branchSessionId), { items: [] });
+    assert.deepEqual(
+      (await todos.readOrBootstrap('latest-branch-target')).items
+        .map((item) => item.content)
+        .sort(),
+      ['Legacy child task', 'Retained task'],
+    );
     const copiedRuns = await execution.agentRunStore.listSessionRuns(branchSessionId);
     assert.equal(copiedRuns.length, 2);
     const copiedChild = copiedRuns.find((run) => run.turnId === 'legacy-child-turn');
@@ -1667,7 +1669,7 @@ async function verifyDurableBranch(
     assert.ok(copiedProjectionArtifact);
     assert.equal(copiedProjectionPart.ref.relativePath, copiedProjectionArtifact.id);
     assert.equal((await artifacts.listPage('revision-target', { offset: 0, limit: 10 })).total, 0);
-    assert.deepEqual(await tasks.list('revision-target'), []);
+    assert.deepEqual(await todos.readOrBootstrap('revision-target'), { items: [] });
     assert.deepEqual(await execution.agentRunStore.listSessionRuns('revision-target'), []);
     await assert.rejects(
       () => execution.sessionStore.readHeaderSnapshot('revision-target'),
diff --git a/packages/runtime-host/src/__tests__/session-todo-coordinator.test.ts b/packages/runtime-host/src/__tests__/session-todo-coordinator.test.ts
new file mode 100644
index 0000000000..470175cbb8
--- /dev/null
+++ b/packages/runtime-host/src/__tests__/session-todo-coordinator.test.ts
@@ -0,0 +1,96 @@
+/*
+ * 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 assert from 'node:assert/strict';
+import { mkdtemp, rm } from 'node:fs/promises';
+import { tmpdir } from 'node:os';
+import { join } from 'node:path';
+import { test } from 'node:test';
+import { tryAcquireInteractiveRootOwner, resolveStorageRoot } from '@maka/storage/root-authority';
+import { openInteractiveSessionTodoStoreForWrite } from '@maka/storage/session-todo-authority';
+import { SessionAdmissionGate } from '../server/session-admission-gate.js';
+import { HostSessionTodoCoordinator } from '../server/session-todo-coordinator.js';
+
+test('read/bootstrap is silent and each committed replace publishes once', async () => {
+  const root = await mkdtemp(join(tmpdir(), 'maka-host-session-todo-'));
+  try {
+    const capability = await resolveStorageRoot({ path: root, kind: 'interactive' });
+    const owner = await tryAcquireInteractiveRootOwner(capability);
+    assert.ok(owner);
+    if (!owner) return;
+    const writer = await openInteractiveSessionTodoStoreForWrite(owner.lease);
+    const changed: string[] = [];
+    const coordinator = new HostSessionTodoCoordinator(
+      writer,
+      new SessionAdmissionGate(),
+      { probeSessionRemoval: async () => ({ kind: 'present' }) },
+      (sessionId) => changed.push(sessionId),
+      () => assert.fail('publication should not drain'),
+    );
+    assert.deepEqual(await coordinator.read('session-1'), { items: [] });
+    assert.deepEqual(changed, []);
+    await assert.rejects(
+      () => coordinator.replace('session-1', [{ content: '', status: 'pending' }]),
+      /cannot be empty/,
+    );
+    assert.deepEqual(changed, []);
+    await coordinator.replace('session-1', [{ content: 'one', status: 'pending' }]);
+    assert.deepEqual(changed, ['session-1']);
+    await coordinator.read('session-1');
+    assert.deepEqual(changed, ['session-1']);
+    writer.close();
+    await owner.close();
+  } finally {
+    await rm(root, { recursive: true, force: true });
+  }
+});
+
+test('committed replacement survives a synchronous publication failure and requests drain', async () => {
+  const root = await mkdtemp(join(tmpdir(), 'maka-host-session-todo-publication-'));
+  try {
+    const capability = await resolveStorageRoot({ path: root, kind: 'interactive' });
+    const owner = await tryAcquireInteractiveRootOwner(capability);
+    assert.ok(owner);
+    if (!owner) return;
+    const writer = await openInteractiveSessionTodoStoreForWrite(owner.lease);
+    let drains = 0;
+    const coordinator = new HostSessionTodoCoordinator(
+      writer,
+      new SessionAdmissionGate(),
+      { probeSessionRemoval: async () => ({ kind: 'present' }) },
+      () => {
+        throw new Error('projection failed');
+      },
+      () => {
+        drains += 1;
+      },
+    );
+    const committed = await coordinator.replace('session-1', [
+      { content: 'committed before publication', status: 'completed' },
+    ]);
+    assert.deepEqual(committed, {
+      items: [{ content: 'committed before publication', status: 'completed' }],
+    });
+    assert.equal(drains, 1);
+    assert.deepEqual(await coordinator.read('session-1'), committed);
+    writer.close();
+    await owner.close();
+  } finally {
+    await rm(root, { recursive: true, force: true });
+  }
+});
diff --git a/packages/runtime-host/src/__tests__/session-todo-protocol.test.ts b/packages/runtime-host/src/__tests__/session-todo-protocol.test.ts
new file mode 100644
index 0000000000..635356a730
--- /dev/null
+++ b/packages/runtime-host/src/__tests__/session-todo-protocol.test.ts
@@ -0,0 +1,50 @@
+/*
+ * 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 assert from 'node:assert/strict';
+import { test } from 'node:test';
+import {
+  decodeSessionTodoQueryInput,
+  decodeSessionTodoQueryResult,
+  HOST_OPERATION_SPECS,
+} from '../protocol/index.js';
+
+test('SessionTodo protocol accepts one whole bounded snapshot', () => {
+  assert.deepEqual(decodeSessionTodoQueryInput({ sessionId: 'session-1' }), {
+    sessionId: 'session-1',
+  });
+  assert.deepEqual(
+    decodeSessionTodoQueryResult({
+      sessionId: 'session-1',
+      items: [{ content: 'one', status: 'pending' }],
+    }),
+    { sessionId: 'session-1', items: [{ content: 'one', status: 'pending' }] },
+  );
+  assert.ok(HOST_OPERATION_SPECS['session.todo.query']);
+  assert.equal(Object.hasOwn(HOST_OPERATION_SPECS, 'task.ledger.query'), false);
+});
+
+test('SessionTodo protocol rejects extra fields and invalid items', () => {
+  assert.throws(() => decodeSessionTodoQueryInput({ sessionId: 'session-1', cursor: 'x' }));
+  assert.throws(() =>
+    decodeSessionTodoQueryResult({
+      sessionId: 'session-1',
+      items: [{ content: 'one', status: 'blocked' }],
+    }),
+  );
+});
diff --git a/packages/runtime-host/src/__tests__/task-ledger-protocol.test.ts b/packages/runtime-host/src/__tests__/task-ledger-protocol.test.ts
deleted file mode 100644
index 14fb1536c2..0000000000
--- a/packages/runtime-host/src/__tests__/task-ledger-protocol.test.ts
+++ /dev/null
@@ -1,225 +0,0 @@
-/*
- * 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 assert from 'node:assert/strict';
-import { describe, test } from 'node:test';
-import type { Task } from '@maka/core/task-ledger';
-import { RuntimeHostProtocolError } from '../protocol/errors.js';
-import {
-  decodeTaskLedgerQueryInput,
-  decodeTaskLedgerQueryResult,
-  encodeTaskLedgerTask,
-  encodeTaskLedgerQueryResult,
-  TASK_LEDGER_CURSOR_MAX_BYTES,
-  TASK_LEDGER_PAGE_MAX_BYTES,
-  TASK_LEDGER_PAGE_MAX_ITEMS,
-  type TaskLedgerQueryResult,
-} from '../protocol/task-ledger.js';
-
-const revision = `sha256:${'a'.repeat(64)}` as const;
-const nextRevision = `sha256:${'b'.repeat(64)}` as const;
-
-describe('Task Ledger protocol', () => {
-  test('rejects unknown fields and invalid current Task DTO values', () => {
-    const task = validTask();
-    for (const invalid of [
-      { ...task, rawEventPath: '/private/task-events.jsonl' },
-      { ...task, id: '../task' },
-      { ...task, key: 'task-1' },
-      { ...task, subject: ' Task 0 ' },
-      { ...task, status: 'done' },
-      { ...task, blockedReason: ' blocked ' },
-      { ...task, resumeTrust: 'probably_ok' },
-      { ...task, createdAt: Number.NaN },
-      { ...task, updatedAt: Number.POSITIVE_INFINITY },
-      {
-        ...task,
-        owner: { actor: 'child_agent', sessionId: 'child-session-1', socketPath: '/tmp/a' },
-      },
-    ]) {
-      assertInvalid(() =>
-        decodeTaskLedgerQueryResult({
-          kind: 'task',
-          sessionId: 'session-1',
-          revision,
-          task: invalid,
-        }),
-      );
-    }
-
-    assertInvalid(() =>
-      decodeTaskLedgerQueryInput({ kind: 'list_start', sessionId: 'session-1', cursor: '0' }),
-    );
-    assertInvalid(() =>
-      decodeTaskLedgerQueryResult({
-        kind: 'revision_changed',
-        expected: revision,
-        actual: nextRevision,
-        cursor: '0',
-      }),
-    );
-  });
-
-  test('projects producer text once and accepts only wire-canonical DTOs', () => {
-    const producerTasks = [
-      validTask(0, { subject: 'A <task-ledger> B' }),
-      validTask(1, {
-        subject: '<task-ledger>',
-        status: 'completed',
-        completionEvidence: 'Verified <task-ledger> ghp_abcdefghijklmnopqrstuvwxyz123456',
-        resumeTrust: 'trusted',
-      }),
-      validTask(2, {
-        status: 'failed',
-        failureReason: '<task-ledger>',
-        resumeTrust: 'trusted',
-      }),
-      validTask(3, {
-        status: 'blocked',
-        blockedReason: '<task-ledger>',
-        resumeTrust: 'untrusted',
-      }),
-    ];
-    const encoded = encodeTaskLedgerQueryResult({
-      kind: 'page',
-      sessionId: 'session-1',
-      revision,
-      tasks: producerTasks,
-      nextCursor: null,
-    });
-    assert.equal(encoded.kind, 'page');
-    assert.equal(encoded.kind === 'page' && encoded.tasks[0]?.subject, 'A B');
-    assert.equal(encoded.kind === 'page' && encoded.tasks[1]?.subject, '[redacted]');
-    assert.equal(
-      encoded.kind === 'page' && encoded.tasks[1]?.completionEvidence,
-      'Verified [redacted]',
-    );
-    assert.equal(encoded.kind === 'page' && encoded.tasks[2]?.failureReason, undefined);
-    assert.equal(encoded.kind === 'page' && encoded.tasks[2]?.resumeTrust, 'needs_revalidation');
-    assert.equal(encoded.kind === 'page' && encoded.tasks[3]?.blockedReason, undefined);
-    assert.equal(encoded.kind === 'page' && encoded.tasks[3]?.resumeTrust, 'untrusted');
-    assert.deepEqual(
-      encoded.kind === 'page' ? encoded.tasks : [],
-      producerTasks.map(encodeTaskLedgerTask),
-    );
-    assert.deepEqual(decodeTaskLedgerQueryResult(encoded), encoded);
-
-    for (const task of producerTasks) {
-      assertInvalid(() =>
-        decodeTaskLedgerQueryResult({
-          kind: 'task',
-          sessionId: 'session-1',
-          revision,
-          task,
-        }),
-      );
-    }
-  });
-
-  test('enforces revision and UTF-8 cursor bounds', () => {
-    for (const invalidRevision of [
-      'a'.repeat(64),
-      `sha256:${'A'.repeat(64)}`,
-      `sha256:${'a'.repeat(63)}`,
-    ]) {
-      assertInvalid(() =>
-        decodeTaskLedgerQueryInput({
-          kind: 'list_continue',
-          sessionId: 'session-1',
-          revision: invalidRevision,
-          cursor: 'opaque',
-        }),
-      );
-    }
-
-    for (const cursor of ['', '界'.repeat(Math.floor(TASK_LEDGER_CURSOR_MAX_BYTES / 3) + 1)]) {
-      assertInvalid(() =>
-        decodeTaskLedgerQueryInput({
-          kind: 'list_continue',
-          sessionId: 'session-1',
-          revision,
-          cursor,
-        }),
-      );
-      assertInvalid(() =>
-        decodeTaskLedgerQueryResult({
-          kind: 'page',
-          sessionId: 'session-1',
-          revision,
-          tasks: [],
-          nextCursor: cursor,
-        }),
-      );
-    }
-  });
-
-  test('enforces item and encoded UTF-8 page bounds in both codec directions', () => {
-    const tooMany = Array.from({ length: TASK_LEDGER_PAGE_MAX_ITEMS + 1 }, (_, index) =>
-      validTask(index),
-    );
-    const byteHeavy = Array.from({ length: 48 }, (_, index) =>
-      validTask(index, {
-        status: 'completed',
-        subject: 'subject '.repeat(25).trim(),
-        completionEvidence: 'evidence '.repeat(125).trim(),
-        endedAt: 3,
-      }),
-    );
-    const oversizedByItems = page(tooMany);
-    const oversizedByBytes = page(byteHeavy);
-    assert.ok(
-      Buffer.byteLength(JSON.stringify(oversizedByBytes), 'utf8') > TASK_LEDGER_PAGE_MAX_BYTES,
-    );
-
-    for (const result of [oversizedByItems, oversizedByBytes]) {
-      assertInvalid(() => encodeTaskLedgerQueryResult(result));
-      assertInvalid(() => decodeTaskLedgerQueryResult(result));
-    }
-  });
-});
-
-function validTask(index = 0, overrides: Partial<Task> = {}): Task {
-  return {
-    id: `task-${index}`,
-    key: `T${index + 1}`,
-    subject: `Task ${index}`,
-    status: 'in_progress',
-    createdAt: 1,
-    updatedAt: 2,
-    owner: { actor: 'main_agent', runId: 'run-1' },
-    ...overrides,
-  };
-}
-
-function page(tasks: readonly unknown[]) {
-  return {
-    kind: 'page',
-    sessionId: 'session-1',
-    revision,
-    tasks,
-    nextCursor: null,
-  };
-}
-
-function assertInvalid(action: () => unknown): void {
-  assert.throws(
-    action,
-    (error: unknown) => error instanceof RuntimeHostProtocolError && error.code === 'invalid_frame',
-  );
-}
diff --git a/packages/runtime-host/src/protocol/index.ts b/packages/runtime-host/src/protocol/index.ts
index d98e3c21e9..69a7b92fe7 100644
--- a/packages/runtime-host/src/protocol/index.ts
+++ b/packages/runtime-host/src/protocol/index.ts
@@ -86,7 +86,7 @@ export * from './scheduled-task-change.js';
 export * from './session-retirement.js';
 export * from './session-transcript.js';
 export * from './session-turns.js';
-export * from './task-ledger.js';
+export * from './session-todo.js';
 export * from './workspace.js';
 export * from './workhub-coordination.js';
 export * from './websocket-path.js';
@@ -95,7 +95,10 @@ export const RUNTIME_HOST_REGISTRATION_SCHEMA_VERSION = 1 as const;
 export const RUNTIME_HOST_PROTOCOL_VERSION = 0 as const;
 // Increment when the same protocol version no longer guarantees safe Client-Host
 // interoperability. Mismatches are rejected before domain commands are admitted.
-export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 80 as const;
+export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 81 as const;
+// 81: SessionTodo replaces the Task Ledger protocol and continuity domain with
+// one bounded current-state snapshot. Older peers cannot decode the operation
+// or preserve the new invalidation vocabulary.
 // 80: Runtime Policy catalog models gained validated user-overridden fact
 // provenance. Older peers reject this projected model shape, so they must be
 // refused during the handshake before catalog admission.
diff --git a/packages/runtime-host/src/protocol/operations.ts b/packages/runtime-host/src/protocol/operations.ts
index 8476927962..61211f4147 100644
--- a/packages/runtime-host/src/protocol/operations.ts
+++ b/packages/runtime-host/src/protocol/operations.ts
@@ -54,12 +54,12 @@ import { SESSION_CATALOG_OPERATION_SPECS } from './session-catalog.js';
 import { SESSION_CONTINUITY_OPERATION_SPECS } from './session-continuity.js';
 import { SESSION_TRANSCRIPT_OPERATION_SPECS } from './session-transcript.js';
 import { SESSION_TURNS_OPERATION_SPECS } from './session-turns.js';
+import { SESSION_TODO_OPERATION_SPECS } from './session-todo.js';
 import { SESSION_COLLABORATION_OPERATION_SPECS } from './session-collaboration.js';
 import { SESSION_REVISION_OPERATION_SPECS } from './session-revision.js';
 import { SESSION_RETIREMENT_OPERATION_SPECS } from './session-retirement.js';
 import { SESSION_EFFECT_OPERATION_SPECS } from './session-effects.js';
 import { SKILL_CATALOG_OPERATION_SPECS } from './skill-catalog.js';
-import { TASK_LEDGER_OPERATION_SPECS } from './task-ledger.js';
 import { TURN_OPERATION_SPECS } from './turn.js';
 import { USAGE_PRICING_OPERATION_SPECS } from './usage-pricing.js';
 import { WEB_SEARCH_OPERATION_SPECS } from './web-search.js';
@@ -172,6 +172,7 @@ export * from './session-revision.js';
 export * from './session-retirement.js';
 export * from './session-transcript.js';
 export * from './session-turns.js';
+export * from './session-todo.js';
 export * from './session-effects.js';
 export * from './skill-catalog.js';
 export * from './usage-pricing.js';
@@ -199,7 +200,7 @@ export const HOST_OPERATION_SPECS = composeOperationSpecMaps(
   PLAN_OPERATION_SPECS,
   PROJECT_CATALOG_OPERATION_SPECS,
   MESSAGE_OPERATION_SPECS,
-  TASK_LEDGER_OPERATION_SPECS,
+  SESSION_TODO_OPERATION_SPECS,
   INTERACTION_OPERATION_SPECS,
   SESSION_CONTINUITY_OPERATION_SPECS,
   SESSION_TRANSCRIPT_OPERATION_SPECS,
@@ -325,7 +326,7 @@ export const REMOTE_OWNER_OPERATION_GRANTS = Object.freeze([
   'skill.catalog.query',
   'subscription.close',
   'subscription.open',
-  'task.ledger.query',
+  'session.todo.query',
   'turn.interrupt',
   'turn.message.execution.query',
   'turn.message.query',
diff --git a/packages/runtime-host/src/protocol/session-continuity.ts b/packages/runtime-host/src/protocol/session-continuity.ts
index 5cd42d7927..4219b6ab34 100644
--- a/packages/runtime-host/src/protocol/session-continuity.ts
+++ b/packages/runtime-host/src/protocol/session-continuity.ts
@@ -244,7 +244,7 @@ export interface SessionTranscriptAdvancedFrame extends SubscriptionEnvelope {
 }
 
 export const SESSION_DOMAINS = [
-  'task',
+  'todo',
   'plan',
   'deep_research',
   'usage',
diff --git a/packages/runtime-host/src/protocol/session-todo.ts b/packages/runtime-host/src/protocol/session-todo.ts
new file mode 100644
index 0000000000..1f4c623abc
--- /dev/null
+++ b/packages/runtime-host/src/protocol/session-todo.ts
@@ -0,0 +1,70 @@
+/*
+ * 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 { normalizeSessionTodoItems, type SessionTodoSnapshot } from '@maka/core/session-todo';
+import { requireEntityId, requireExactRecord } from './codec.js';
+import { invalidProtocolFrame } from './errors.js';
+import { defineOperation } from './operation-spec.js';
+
+const QUERY_ERRORS = [
+  'host_not_ready',
+  'host_draining',
+  'operation_unavailable',
+  'invalid_request',
+  'not_found',
+  'internal_failure',
+] as const;
+
+export interface SessionTodoQueryInput {
+  readonly sessionId: string;
+}
+
+export interface SessionTodoQueryResult extends SessionTodoSnapshot {
+  readonly sessionId: string;
+}
+
+export const SESSION_TODO_OPERATION_SPECS = {
+  'session.todo.query': defineOperation<
+    SessionTodoQueryInput,
+    SessionTodoQueryResult,
+    (typeof QUERY_ERRORS)[number]
+  >({
+    mode: 'query',
+    availability: 'ready',
+    errors: QUERY_ERRORS,
+    decodeInput: decodeSessionTodoQueryInput,
+    decodeOutput: decodeSessionTodoQueryResult,
+  }),
+} as const;
+
+export function decodeSessionTodoQueryInput(value: unknown): SessionTodoQueryInput {
+  const input = requireExactRecord(value, 'SessionTodo query input', ['sessionId']);
+  return { sessionId: requireEntityId(input.sessionId, 'sessionId') };
+}
+
+export function decodeSessionTodoQueryResult(value: unknown): SessionTodoQueryResult {
+  const result = requireExactRecord(value, 'SessionTodo query result', ['sessionId', 'items']);
+  const normalized = normalizeSessionTodoItems(result.items);
+  if (!normalized.ok)
+    throw invalidProtocolFrame(`Invalid SessionTodo result: ${normalized.message}`);
+  return {
+    sessionId: requireEntityId(result.sessionId, 'sessionId'),
+    items: normalized.value.items,
+  };
+}
diff --git a/packages/runtime-host/src/protocol/task-ledger.ts b/packages/runtime-host/src/protocol/task-ledger.ts
deleted file mode 100644
index 284fd6e164..0000000000
--- a/packages/runtime-host/src/protocol/task-ledger.ts
+++ /dev/null
@@ -1,425 +0,0 @@
-/*
- * 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 {
-  TASK_EVIDENCE_MAX_CHARS,
-  TASK_SUBJECT_MAX_CHARS,
-  isResumeTrust,
-  isSafeTaskId,
-  isTaskKey,
-  isTaskOwner,
-  isTaskStatus,
-  normalizeTaskEvidenceText,
-  normalizeTaskSubject,
-  sanitizeTaskLedgerTask,
-  type Task,
-  type TaskOwner,
-  validateTaskEvidence,
-} from '@maka/core/task-ledger';
-import { requireEntityId, requireExactRecord, requireRecord } from './codec.js';
-import { invalidProtocolFrame } from './errors.js';
-import { defineOperation } from './operation-spec.js';
-
-export const TASK_LEDGER_PAGE_MAX_ITEMS = 128;
-export const TASK_LEDGER_PAGE_MAX_BYTES = 48 * 1024;
-export const TASK_LEDGER_CURSOR_MAX_BYTES = 512;
-
-const TASK_REQUIRED_FIELDS = ['id', 'key', 'subject', 'status', 'createdAt', 'updatedAt'] as const;
-const TASK_FIELDS = [
-  ...TASK_REQUIRED_FIELDS,
-  'parentId',
-  'owner',
-  'endedAt',
-  'blockedReason',
-  'failureReason',
-  'completionEvidence',
-  'resumeTrust',
-] as const;
-const TASK_OWNER_FIELDS = ['actor', 'sessionId', 'agentId', 'runId', 'turnId'] as const;
-const REDACTED_TASK_TEXT = '[redacted]';
-
-const QUERY_ERRORS = [
-  'host_not_ready',
-  'host_draining',
-  'operation_unavailable',
-  'invalid_request',
-  'not_found',
-  'internal_failure',
-] as const;
-
-export type TaskLedgerRevision = `sha256:${string}`;
-export type TaskLedgerTask = Readonly<Task>;
-
-export type TaskLedgerQueryInput =
-  | { readonly kind: 'list_start'; readonly sessionId: string }
-  | {
-      readonly kind: 'list_continue';
-      readonly sessionId: string;
-      readonly revision: TaskLedgerRevision;
-      readonly cursor: string;
-    }
-  | { readonly kind: 'get'; readonly sessionId: string; readonly taskRef: string };
-
-export type TaskLedgerQueryResult =
-  | {
-      readonly kind: 'page';
-      readonly sessionId: string;
-      readonly revision: TaskLedgerRevision;
-      readonly tasks: readonly TaskLedgerTask[];
-      readonly nextCursor: string | null;
-    }
-  | {
-      readonly kind: 'revision_changed';
-      readonly expected: TaskLedgerRevision;
-      readonly actual: TaskLedgerRevision;
-    }
-  | {
-      readonly kind: 'task';
-      readonly sessionId: string;
-      readonly revision: TaskLedgerRevision;
-      readonly task: TaskLedgerTask | null;
-    };
-
-export const TASK_LEDGER_OPERATION_SPECS = {
-  'task.ledger.query': defineOperation<
-    TaskLedgerQueryInput,
-    TaskLedgerQueryResult,
-    (typeof QUERY_ERRORS)[number]
-  >({
-    mode: 'query',
-    availability: 'ready',
-    errors: QUERY_ERRORS,
-    decodeInput: decodeTaskLedgerQueryInput,
-    decodeOutput: decodeTaskLedgerQueryResult,
-  }),
-} as const;
-
-export function decodeTaskLedgerQueryInput(value: unknown): TaskLedgerQueryInput {
-  const record = requireRecord(value, 'task ledger query input');
-  if (record.kind === 'list_start') {
-    const input = requireExactRecord(record, 'task ledger list start input', ['kind', 'sessionId']);
-    return { kind: 'list_start', sessionId: requireEntityId(input.sessionId, 'sessionId') };
-  }
-  if (record.kind === 'list_continue') {
-    const input = requireExactRecord(record, 'task ledger list continuation input', [
-      'kind',
-      'sessionId',
-      'revision',
-      'cursor',
-    ]);
-    return {
-      kind: 'list_continue',
-      sessionId: requireEntityId(input.sessionId, 'sessionId'),
-      revision: taskLedgerRevision(input.revision, 'task ledger revision'),
-      cursor: taskLedgerCursor(input.cursor, 'task ledger cursor'),
-    };
-  }
-  if (record.kind === 'get') {
-    const input = requireExactRecord(record, 'task ledger get input', [
-      'kind',
-      'sessionId',
-      'taskRef',
-    ]);
-    return {
-      kind: 'get',
-      sessionId: requireEntityId(input.sessionId, 'sessionId'),
-      taskRef: taskReference(input.taskRef),
-    };
-  }
-  throw invalidProtocolFrame('Invalid task ledger query kind');
-}
-
-export function decodeTaskLedgerQueryResult(value: unknown): TaskLedgerQueryResult {
-  return taskLedgerQueryResult(value, 'decode');
-}
-
-export function encodeTaskLedgerQueryResult(value: unknown): TaskLedgerQueryResult {
-  return taskLedgerQueryResult(value, 'encode');
-}
-
-export function encodeTaskLedgerTask(value: unknown): TaskLedgerTask {
-  return taskLedgerTask(value, 'encode');
-}
-
-function taskLedgerQueryResult(
-  value: unknown,
-  direction: 'encode' | 'decode',
-): TaskLedgerQueryResult {
-  const record = requireRecord(value, 'task ledger query result');
-  if (record.kind === 'revision_changed') {
-    const changed = requireExactRecord(record, 'task ledger revision changed result', [
-      'kind',
-      'expected',
-      'actual',
-    ]);
-    return {
-      kind: 'revision_changed',
-      expected: taskLedgerRevision(changed.expected, 'expected task ledger revision'),
-      actual: taskLedgerRevision(changed.actual, 'actual task ledger revision'),
-    };
-  }
-  if (record.kind === 'task') {
-    const result = requireExactRecord(record, 'task ledger task result', [
-      'kind',
-      'sessionId',
-      'revision',
-      'task',
-    ]);
-    return {
-      kind: 'task',
-      sessionId: requireEntityId(result.sessionId, 'sessionId'),
-      revision: taskLedgerRevision(result.revision, 'task ledger revision'),
-      task: result.task === null ? null : taskLedgerTask(result.task, direction),
-    };
-  }
-  if (record.kind !== 'page') throw invalidProtocolFrame('Invalid task ledger query result kind');
-
-  const page = requireExactRecord(record, 'task ledger page result', [
-    'kind',
-    'sessionId',
-    'revision',
-    'tasks',
-    'nextCursor',
-  ]);
-  if (!Array.isArray(page.tasks) || page.tasks.length > TASK_LEDGER_PAGE_MAX_ITEMS) {
-    throw invalidProtocolFrame('Task ledger page exceeds item limit');
-  }
-  const decoded: TaskLedgerQueryResult = {
-    kind: 'page',
-    sessionId: requireEntityId(page.sessionId, 'sessionId'),
-    revision: taskLedgerRevision(page.revision, 'task ledger revision'),
-    tasks: page.tasks.map((task) => taskLedgerTask(task, direction)),
-    nextCursor:
-      page.nextCursor === null
-        ? null
-        : taskLedgerCursor(page.nextCursor, 'task ledger next cursor'),
-  };
-  if (jsonByteLength(decoded) > TASK_LEDGER_PAGE_MAX_BYTES) {
-    throw invalidProtocolFrame('Task ledger page exceeds byte limit');
-  }
-  return decoded;
-}
-
-function taskLedgerTask(value: unknown, direction: 'encode' | 'decode'): TaskLedgerTask {
-  const record = requireRecord(value, 'task ledger task');
-  assertAllowedKeys(record, 'task ledger task', TASK_FIELDS);
-  if (TASK_REQUIRED_FIELDS.some((field) => !Object.hasOwn(record, field))) {
-    throw invalidProtocolFrame('Invalid task ledger task fields');
-  }
-
-  const task: Task = {
-    id: stableTaskId(record.id, 'task id'),
-    key: taskKey(record.key),
-    subject: boundedTaskText(record.subject, 'subject'),
-    status: taskStatus(record.status),
-    createdAt: timestamp(record.createdAt, 'task createdAt'),
-    updatedAt: timestamp(record.updatedAt, 'task updatedAt'),
-    ...optionalStableTaskId(record, 'parentId'),
-    ...optionalOwner(record),
-    ...optionalTimestamp(record, 'endedAt'),
-    ...optionalTaskText(record, 'blockedReason'),
-    ...optionalTaskText(record, 'failureReason'),
-    ...optionalTaskText(record, 'completionEvidence'),
-    ...optionalResumeTrust(record),
-  };
-  const projected = projectTaskForWire(task);
-  if (direction === 'decode' && !hasCanonicalWireProjection(task, projected)) {
-    throw invalidProtocolFrame('Task ledger task is not sanitized');
-  }
-  return projected;
-}
-
-function optionalStableTaskId(
-  record: Record<string, unknown>,
-  field: 'parentId',
-): Pick<Task, 'parentId'> | Record<string, never> {
-  return Object.hasOwn(record, field)
-    ? { [field]: stableTaskId(record[field], `task ${field}`) }
-    : {};
-}
-
-function optionalOwner(
-  record: Record<string, unknown>,
-): Pick<Task, 'owner'> | Record<string, never> {
-  if (!Object.hasOwn(record, 'owner')) return {};
-  return { owner: taskOwner(record.owner) };
-}
-
-function taskOwner(value: unknown): TaskOwner {
-  const record = requireRecord(value, 'task owner');
-  assertAllowedKeys(record, 'task owner', TASK_OWNER_FIELDS);
-  if (!Object.hasOwn(record, 'actor') || !isTaskOwner(record)) {
-    throw invalidProtocolFrame('Invalid task owner');
-  }
-  return {
-    actor: record.actor as TaskOwner['actor'],
-    ...optionalOwnerId(record, 'sessionId'),
-    ...optionalOwnerId(record, 'agentId'),
-    ...optionalOwnerId(record, 'runId'),
-    ...optionalOwnerId(record, 'turnId'),
-  };
-}
-
-function optionalOwnerId<Field extends Exclude<keyof TaskOwner, 'actor'>>(
-  record: Record<string, unknown>,
-  field: Field,
-): Pick<TaskOwner, Field> | Record<string, never> {
-  return Object.hasOwn(record, field)
-    ? ({ [field]: stableTaskId(record[field], `task owner ${field}`) } as Pick<TaskOwner, Field>)
-    : {};
-}
-
-function optionalTimestamp(
-  record: Record<string, unknown>,
-  field: 'endedAt',
-): Pick<Task, 'endedAt'> | Record<string, never> {
-  return Object.hasOwn(record, field) ? { [field]: timestamp(record[field], `task ${field}`) } : {};
-}
-
-function optionalTaskText<Field extends 'blockedReason' | 'failureReason' | 'completionEvidence'>(
-  record: Record<string, unknown>,
-  field: Field,
-): Pick<Task, Field> | Record<string, never> {
-  return Object.hasOwn(record, field)
-    ? ({ [field]: boundedTaskText(record[field], field) } as Pick<Task, Field>)
-    : {};
-}
-
-function optionalResumeTrust(
-  record: Record<string, unknown>,
-): Pick<Task, 'resumeTrust'> | Record<string, never> {
-  if (!Object.hasOwn(record, 'resumeTrust')) return {};
-  if (!isResumeTrust(record.resumeTrust)) throw invalidProtocolFrame('Invalid task resumeTrust');
-  return { resumeTrust: record.resumeTrust };
-}
-
-function boundedTaskText(
-  value: unknown,
-  field: 'subject' | 'blockedReason' | 'failureReason' | 'completionEvidence',
-): string {
-  const maxCharacters = field === 'subject' ? TASK_SUBJECT_MAX_CHARS : TASK_EVIDENCE_MAX_CHARS;
-  if (typeof value !== 'string' || value.length === 0 || Array.from(value).length > maxCharacters) {
-    throw invalidProtocolFrame(`Invalid task ${field}`);
-  }
-  return value;
-}
-
-function projectTaskForWire(task: Task): Task {
-  const sanitized = sanitizeTaskLedgerTask(task);
-  const { blockedReason, failureReason, completionEvidence, ...identity } = sanitized;
-  const projected: Task = {
-    ...identity,
-    subject: canonicalWireSubject(sanitized.subject),
-    ...canonicalWireEvidence(blockedReason, 'blockedReason'),
-    ...canonicalWireEvidence(failureReason, 'failureReason'),
-    ...canonicalWireEvidence(completionEvidence, 'completionEvidence'),
-  };
-  if (validateTaskEvidence(projected).ok) return projected;
-  if (projected.resumeTrust !== undefined && projected.resumeTrust !== 'trusted') return projected;
-  return { ...projected, resumeTrust: 'needs_revalidation' };
-}
-
-function hasCanonicalWireProjection(task: Task, projected: Task): boolean {
-  return (
-    task.subject === projected.subject &&
-    task.blockedReason === projected.blockedReason &&
-    task.failureReason === projected.failureReason &&
-    task.completionEvidence === projected.completionEvidence &&
-    task.resumeTrust === projected.resumeTrust
-  );
-}
-
-function canonicalWireSubject(value: string): string {
-  const normalized = normalizeTaskSubject(value);
-  if (normalized.ok) return normalized.value;
-  if (value.trim().length === 0) return REDACTED_TASK_TEXT;
-  throw invalidProtocolFrame('Invalid task subject');
-}
-
-function canonicalWireEvidence<
-  Field extends 'blockedReason' | 'failureReason' | 'completionEvidence',
->(value: string | undefined, field: Field): Pick<Task, Field> | Record<string, never> {
-  if (value === undefined) return {};
-  const normalized = normalizeTaskEvidenceText(value, field);
-  if (normalized.ok) return { [field]: normalized.value } as Pick<Task, Field>;
-  if (value.trim().length === 0) return {};
-  throw invalidProtocolFrame(`Invalid task ${field}`);
-}
-
-function stableTaskId(value: unknown, label: string): string {
-  if (!isSafeTaskId(value)) throw invalidProtocolFrame(`Invalid ${label}`);
-  return value;
-}
-
-function taskKey(value: unknown): string {
-  if (!isTaskKey(value)) throw invalidProtocolFrame('Invalid task key');
-  return value;
-}
-
-function taskReference(value: unknown): string {
-  if (!isSafeTaskId(value) && !isTaskKey(value)) {
-    throw invalidProtocolFrame('Invalid task reference');
-  }
-  return value;
-}
-
-function taskStatus(value: unknown): Task['status'] {
-  if (!isTaskStatus(value)) throw invalidProtocolFrame('Invalid task status');
-  return value;
-}
-
-function timestamp(value: unknown, label: string): number {
-  if (typeof value !== 'number' || !Number.isFinite(value)) {
-    throw invalidProtocolFrame(`Invalid ${label}`);
-  }
-  return value;
-}
-
-function taskLedgerRevision(value: unknown, label: string): TaskLedgerRevision {
-  if (typeof value !== 'string' || !/^sha256:[a-f0-9]{64}$/.test(value)) {
-    throw invalidProtocolFrame(`Invalid ${label}`);
-  }
-  return value as TaskLedgerRevision;
-}
-
-function taskLedgerCursor(value: unknown, label: string): string {
-  if (
-    typeof value !== 'string' ||
-    value.length === 0 ||
-    Buffer.byteLength(value, 'utf8') > TASK_LEDGER_CURSOR_MAX_BYTES
-  ) {
-    throw invalidProtocolFrame(`Invalid ${label}`);
-  }
-  return value;
-}
-
-function assertAllowedKeys(
-  record: Record<string, unknown>,
-  label: string,
-  keys: readonly string[],
-): void {
-  const allowed = new Set(keys);
-  if (Object.keys(record).some((key) => !allowed.has(key))) {
-    throw invalidProtocolFrame(`Unknown ${label} field`);
-  }
-}
-
-function jsonByteLength(value: unknown): number {
-  return Buffer.byteLength(JSON.stringify(value), 'utf8');
-}
diff --git a/packages/runtime-host/src/server/child-agent-composition.ts b/packages/runtime-host/src/server/child-agent-composition.ts
index 2bf61000aa..461da00d2d 100644
--- a/packages/runtime-host/src/server/child-agent-composition.ts
+++ b/packages/runtime-host/src/server/child-agent-composition.ts
@@ -17,7 +17,6 @@
  * under the License.
  */
 
-import type { TaskLedgerStore } from '@maka/core/task-ledger';
 import { AiSdkBackend } from '@maka/runtime/ai-sdk-backend';
 
 import { buildBuiltinTools, type BuildBuiltinToolsOptions } from '@maka/runtime/builtin-tools';
@@ -47,7 +46,6 @@ export interface HostChildAgentToolComposition {
 
 /** Composes the parent control tools and the exact catalog-child capability union. */
 export function createHostChildAgentToolComposition(input: {
-  readonly taskLedger: TaskLedgerStore;
   readonly builtinTools: BuildBuiltinToolsOptions;
   readonly hostTools?: readonly MakaTool[];
   readonly worktreePatchWriteBackAvailable?: boolean;
@@ -59,9 +57,7 @@ export function createHostChildAgentToolComposition(input: {
     worktreeChildExecutorAvailable: input.worktreePatchWriteBackAvailable,
   });
   return Object.freeze({
-    parentTools: Object.freeze(
-      buildParentAgentTools({ taskLedger: input.taskLedger, definitions }),
-    ),
+    parentTools: Object.freeze(buildParentAgentTools({ definitions })),
     childTools: Object.freeze(childTools),
   });
 }
diff --git a/packages/runtime-host/src/server/execution-composition.ts b/packages/runtime-host/src/server/execution-composition.ts
index ee36b4ac2a..66fb16892e 100644
--- a/packages/runtime-host/src/server/execution-composition.ts
+++ b/packages/runtime-host/src/server/execution-composition.ts
@@ -34,7 +34,6 @@ import {
   type SessionHeader,
   WORKHUB_COORDINATION_SESSION_ID,
 } from '@maka/core/session';
-import { filterModelVisibleTaskLedgerTasks } from '@maka/core/task-ledger';
 import { AgentGraphCoordinator } from '@maka/runtime/stream-graph-coordinator';
 import { AgentGraphSupervisorWakeCoordinator } from '@maka/runtime/agent-graph-supervisor-wake';
 import {
@@ -174,7 +173,7 @@ import { SessionContinuityCoordinator } from './session-continuity-coordinator.j
 import { createSessionTranscriptReader } from './session-transcript-reader.js';
 import { HostSkillCatalogCoordinator } from './skill-catalog-coordinator.js';
 import { SkillCatalogRepository } from './skill-catalog-repository.js';
-import { HostTaskLedgerCoordinator } from './task-ledger-coordinator.js';
+import { HostSessionTodoCoordinator } from './session-todo-coordinator.js';
 import { HostTurnControlCoordinator } from './turn-control-coordinator.js';
 import { HostUsagePricingCoordinator } from './usage-pricing-coordinator.js';
 import { HostWebSearchCoordinator } from './web-search-coordinator.js';
@@ -264,7 +263,6 @@ export async function createExecutionRuntimeHostComposition(
   let graphClient: HostAgentGraphCoordinator | undefined;
   let sessionEffects: HostSessionEffectCoordinator | undefined;
   let memoryExtraction: HostMemoryExtractionCoordinator | undefined;
-  let unsubscribeTaskLedger: (() => void) | undefined;
   let unsubscribeTranscriptChanges: (() => void) | undefined;
   let unsubscribeUsageChanges: (() => void) | undefined;
   let workspaceExecution: RuntimeHostWorkspaceExecutionComposition | undefined;
@@ -280,7 +278,7 @@ export async function createExecutionRuntimeHostComposition(
     const openedGoalStore = storage.goal;
     const memoryStore = storage.memoryBundle;
     const longTermMemoryStore = storage.longTermMemory;
-    const taskLedgerStore = storage.taskLedger;
+    const sessionTodoStore = storage.sessionTodo;
     const openedArtifactStore = storage.artifacts;
     const openedContextOffloadStore = storage.contextOffload;
     const openedContextOffloadReader = openedContextOffloadStore
@@ -356,10 +354,12 @@ export async function createExecutionRuntimeHostComposition(
     workspaceExecution = createRuntimeHostWorkspaceExecutionComposition({
       ...(workspaceFilesystemWorker ? { filesystemWorker: workspaceFilesystemWorker } : {}),
     });
-    const taskLedger = new HostTaskLedgerCoordinator(
-      taskLedgerStore,
+    const sessionTodo = new HostSessionTodoCoordinator(
+      sessionTodoStore,
       sessionAdmission,
       stores.sessionStore,
+      (sessionId) => requireContinuity(continuity).enqueueSessionDomainChanged(sessionId, 'todo'),
+      context.requestDrain,
     );
     runtimeResources = new HostRuntimeResourceCoordinator({
       manager: shellRuns,
@@ -424,7 +424,6 @@ export async function createExecutionRuntimeHostComposition(
     ];
     const hostTools = [...childHostTools, ...historyTools];
     const childAgentTools = createHostChildAgentToolComposition({
-      taskLedger,
       builtinTools,
       hostTools: childHostTools,
       worktreePatchWriteBackAvailable: true,
@@ -565,9 +564,6 @@ export async function createExecutionRuntimeHostComposition(
     unsubscribeUsageChanges = openedUsageStores.subscribeSessionUsageChanges((sessionId) =>
       continuityCoordinator.enqueueSessionDomainChanged(sessionId, 'usage'),
     );
-    unsubscribeTaskLedger = taskLedger.subscribe(({ sessionId }) =>
-      continuityCoordinator.enqueueSessionDomainChanged(sessionId, 'task'),
-    );
     deepResearch = new HostDeepResearchCoordinator({
       store: openedDeepResearchStore,
       artifacts: openedArtifactStore,
@@ -672,7 +668,7 @@ export async function createExecutionRuntimeHostComposition(
       createRunComposer: createInteractiveRunComposerFactory({
         skills,
         memory: requireMemory(memory),
-        taskLedger,
+        sessionTodo,
         clientCapabilities: requireClientCapabilities(clientCapabilities),
         resolveTavilyWebSearchReadiness: () =>
           resolveHostTavilyWebSearchReadiness(runtimePolicyStores.operations),
@@ -757,7 +753,6 @@ export async function createExecutionRuntimeHostComposition(
           ...(input.boundTools ? { boundTools: input.boundTools } : {}),
           ...(input.childTools ? { childTools: input.childTools } : {}),
           ...(input.parentAgentTools ? { parentAgentTools: input.parentAgentTools } : {}),
-          taskLedger,
           worktreePatchWriteBackAvailable: true,
           tavilyReady,
         }),
@@ -808,7 +803,7 @@ export async function createExecutionRuntimeHostComposition(
           shell: resolveTurnShellPlan(runtimePolicy.policy.shell),
           skills,
           memory: requireMemory(memory),
-          taskLedger,
+          sessionTodo,
           ...(runProfile ? { toolProfile: header.toolProfile } : {}),
           ...(capabilitySnapshot ? { clientCapabilities: capabilitySnapshot } : {}),
           builtinTools,
@@ -873,7 +868,7 @@ export async function createExecutionRuntimeHostComposition(
             shell: resolveTurnShellPlan(runtimePolicy.policy.shell),
             skills,
             memory: requireMemory(memory),
-            taskLedger,
+            sessionTodo,
             ...(capabilitySnapshot ? { clientCapabilities: capabilitySnapshot } : {}),
             builtinTools,
             hostTools: surface.hostTools,
@@ -924,7 +919,6 @@ export async function createExecutionRuntimeHostComposition(
         (await runtimePolicyStores.runtimePolicy.getSnapshot()).policy.shell,
       );
       const childTools = createHostChildAgentToolComposition({
-        taskLedger,
         builtinTools: { ...builtinTools, shell },
         hostTools,
         worktreePatchWriteBackAvailable: true,
@@ -1249,16 +1243,6 @@ export async function createExecutionRuntimeHostComposition(
       }),
       admitTurn: (sessionId, text, checkpoint, controlLease) =>
         goalExecutionCoordinator.admitTurn(sessionId, text, checkpoint, controlLease),
-      listActionableTaskKeys: async (sessionId) => {
-        const tasks = await taskLedger.list(sessionId, {
-          includeTerminal: false,
-          includeArchived: false,
-          classifyResumeTrust: true,
-        });
-        return filterModelVisibleTaskLedgerTasks(tasks)
-          .filter((task) => task.status === 'pending' || task.status === 'in_progress')
-          .map((task) => task.key);
-      },
       acquireResidency: () => context.acquireResidency('goal'),
       onProjectionChanged: (sessionId) => continuityCoordinator.enqueueCanonicalRefresh(sessionId),
       requestDrain: context.requestDrain,
@@ -1432,6 +1416,7 @@ export async function createExecutionRuntimeHostComposition(
       discardImportedSession: async (sessionId) => {
         const outcomes = await Promise.allSettled([
           stores.purgeConversationOperationalState(sessionId),
+          sessionTodoStore.purgeSessionState(sessionId),
           stores.sessionStore.remove(sessionId),
         ]);
         for (const outcome of outcomes) {
@@ -1457,7 +1442,7 @@ export async function createExecutionRuntimeHostComposition(
     const sessionRevisions = new HostSessionRevisionCoordinator({
       stores,
       artifacts: openedArtifactStore,
-      taskLedger: taskLedgerStore,
+      sessionTodo: sessionTodoStore,
       manager,
       admission: sessionAdmission,
       continuity: continuityCoordinator,
@@ -1481,7 +1466,7 @@ export async function createExecutionRuntimeHostComposition(
       capabilities: clientCapabilities,
       continuity: continuityCoordinator,
       artifacts: openedArtifactStore,
-      taskLedger: taskLedgerStore,
+      sessionTodo: sessionTodoStore,
       assertNoContextOffloadReferences: async (sessionIds) => {
         if (!openedContextOffloadStore) {
           throw new Error('Context-offload reader is unavailable during Session removal', {
@@ -1597,7 +1582,7 @@ export async function createExecutionRuntimeHostComposition(
         handlers: [
           runtimePolicy.handlers,
           connectionEffects.handlers,
-          taskLedger.handlers,
+          sessionTodo.handlers,
           artifacts.handlers,
           skills.handlers,
           usagePricing.handlers,
@@ -1625,7 +1610,6 @@ export async function createExecutionRuntimeHostComposition(
           () => {
             unsubscribeTranscriptChanges?.();
             unsubscribeUsageChanges?.();
-            unsubscribeTaskLedger?.();
           },
         ],
         releaseConnection: [(connectionId) => artifacts.releaseConnection(connectionId)],
@@ -1841,7 +1825,6 @@ export async function createExecutionRuntimeHostComposition(
     try {
       unsubscribeTranscriptChanges?.();
       unsubscribeUsageChanges?.();
-      unsubscribeTaskLedger?.();
     } catch (closeError) {
       errors.push(closeError);
     }
diff --git a/packages/runtime-host/src/server/goal-coordinator.ts b/packages/runtime-host/src/server/goal-coordinator.ts
index 9949bae516..7104a83076 100644
--- a/packages/runtime-host/src/server/goal-coordinator.ts
+++ b/packages/runtime-host/src/server/goal-coordinator.ts
@@ -30,7 +30,6 @@ import {
   GoalContinuationCoordinator,
   type GoalSessionCloseOperation,
   type GoalObservedTurnStart,
-  type GoalTaskGateTrace,
   type GoalTurnAdmission,
   type GoalTurnOutcome,
 } from '@maka/runtime/goal-continuation';
@@ -85,7 +84,6 @@ export interface HostGoalCoordinatorOptions {
     checkpoint: GoalCheckpoint,
     controlLease: GoalControlLease,
   ) => GoalTurnAdmission;
-  readonly listActionableTaskKeys: (sessionId: string) => Promise<string[]>;
   readonly acquireResidency: () => RuntimeHostResidency;
   readonly onProjectionChanged: (sessionId: string) => void;
   readonly requestDrain: () => void;
@@ -164,10 +162,6 @@ export class HostGoalCoordinator {
       },
       getTokenCount: (sessionId) => tokenCache.get(sessionId) ?? 0,
       admitTurn: options.admitTurn,
-      taskGate: {
-        listActionableTaskKeys: options.listActionableTaskKeys,
-        recordDecision: (trace) => this.#recordTaskGateDecision(trace, now),
-      },
       durability: {
         flush: (sessionId) => this.#flushGoalState(sessionId),
         recordCurrentExecution: (current) => this.#recordCurrentExecution(current),
@@ -599,28 +593,6 @@ export class HostGoalCoordinator {
     retained?.release();
     this.#residencies.delete(goal.sessionId);
   }
-
-  async #recordTaskGateDecision(trace: GoalTaskGateTrace, now: () => number): Promise<void> {
-    const admission = await this.#stores.agentRunStore.readRootTurnAdmission(
-      trace.sessionId,
-      trace.turnId,
-    );
-    if (!admission) return;
-    await this.#stores.agentRunStore.appendEvent(trace.sessionId, admission.runId, {
-      type: 'task_gate_decided',
-      id: this.#newId(),
-      runId: admission.runId,
-      sessionId: trace.sessionId,
-      turnId: trace.turnId,
-      ts: now(),
-      message: `Task gate: ${trace.decision}`,
-      data: {
-        goalId: trace.goalId,
-        decision: trace.decision,
-        taskKeys: trace.taskKeys,
-      },
-    });
-  }
 }
 
 function recentContext(messages: readonly StoredMessage[]): string {
diff --git a/packages/runtime-host/src/server/interactive-run-composer.ts b/packages/runtime-host/src/server/interactive-run-composer.ts
index 71004bf7bf..dc35d96779 100644
--- a/packages/runtime-host/src/server/interactive-run-composer.ts
+++ b/packages/runtime-host/src/server/interactive-run-composer.ts
@@ -31,7 +31,6 @@ import type { PermissionMode } from '@maka/core/permission';
 import type { RuntimeExecutionConnection } from '@maka/core/llm-connections';
 import type { RuntimePolicySnapshot } from '@maka/core/runtime-policy';
 import type { SessionToolProfile } from '@maka/core/session';
-import { type TaskLedgerStore } from '@maka/core/task-ledger';
 import { assembleMainSessionSystemPrompt } from '@maka/runtime/system-prompt/main-session-prompt';
 import { buildAskUserQuestionTool } from '@maka/runtime/ask-user-question-tool';
 import { buildBuiltinTools, type BuildBuiltinToolsOptions } from '@maka/runtime/builtin-tools';
@@ -52,7 +51,7 @@ import {
   type SkillCatalogBudgetOptions,
   type SkillInventoryResolver,
 } from '@maka/runtime/skills';
-import { buildTaskLedgerTools } from '@maka/runtime/task-ledger-tools';
+import { buildSessionTodoTools, type SessionTodoToolStore } from '@maka/runtime/session-todo-tools';
 import { buildWorkspaceInstructionsPromptFragment } from '@maka/runtime/system-prompt/workspace-instructions';
 import { isDeepResearchToolAllowed } from '@maka/runtime/deep-research-tools';
 import { listRunnableBuiltinAgentDefinitions } from '@maka/runtime/agent-catalog';
@@ -94,7 +93,7 @@ export interface InteractiveRunComposerInput {
   readonly runtimePolicy: RuntimePolicySnapshot;
   readonly skills: HostSkillCatalogCoordinator;
   readonly memory: HostMemoryCoordinator;
-  readonly taskLedger: TaskLedgerStore;
+  readonly sessionTodo: SessionTodoToolStore;
   readonly childInstruction?: string;
   readonly sideConversation?: boolean;
   readonly boundTools?: readonly MakaTool[];
@@ -136,7 +135,7 @@ export function createInteractiveRunComposer(input: InteractiveRunComposerInput)
   const defaultTools = input.boundTools
     ? input.boundTools
     : buildDefaultHostTools(
-        input.taskLedger,
+        input.sessionTodo,
         inventoryFor,
         builtinTools,
         input.hostTools,
@@ -287,7 +286,6 @@ export interface InteractiveRunToolSurfaceInput {
   readonly boundTools?: readonly MakaTool[];
   readonly childTools?: readonly MakaTool[];
   readonly parentAgentTools?: readonly MakaTool[];
-  readonly taskLedger: TaskLedgerStore;
   readonly worktreePatchWriteBackAvailable?: boolean;
   readonly tavilyReady: boolean;
 }
@@ -321,7 +319,6 @@ export function routeInteractiveRunToolSurface(input: InteractiveRunToolSurfaceI
     ...(childTools
       ? {
           parentAgentTools: buildParentAgentTools({
-            taskLedger: input.taskLedger,
             definitions: listRunnableBuiltinAgentDefinitions({
               tools: childTools,
               worktreeChildExecutorAvailable: input.worktreePatchWriteBackAvailable,
@@ -377,7 +374,6 @@ export function createInteractiveRunComposerFactory(
         ...(backendContext.tools ? { boundTools: backendContext.tools } : {}),
         ...(input.childTools ? { childTools: input.childTools } : {}),
         ...(input.parentAgentTools ? { parentAgentTools: input.parentAgentTools } : {}),
-        taskLedger: input.taskLedger,
         worktreePatchWriteBackAvailable: input.worktreePatchWriteBackAvailable,
         tavilyReady,
       });
@@ -386,7 +382,7 @@ export function createInteractiveRunComposerFactory(
         runtimePolicy,
         skills: input.skills,
         memory: input.memory,
-        taskLedger: input.taskLedger,
+        sessionTodo: input.sessionTodo,
         ...(backendContext.systemPrompt ? { childInstruction: backendContext.systemPrompt } : {}),
         ...(isSideConversationSession(backendContext.header.labels)
           ? { sideConversation: true }
@@ -447,7 +443,7 @@ function assertUniqueToolNames(tools: readonly MakaTool[]): void {
 }
 
 function buildDefaultHostTools(
-  taskLedger: TaskLedgerStore,
+  sessionTodo: SessionTodoToolStore,
   inventoryFor: SkillInventoryResolver,
   builtinOptions?: BuildBuiltinToolsOptions,
   hostTools: readonly MakaTool[] = [],
@@ -460,7 +456,7 @@ function buildDefaultHostTools(
   const builtins = builtinOptions ? buildBuiltinTools(builtinOptions) : [];
   const question = buildAskUserQuestionTool();
   const sandboxBoundary = buildRequestSandboxBoundaryTool();
-  const taskTools = buildTaskLedgerTools({ store: taskLedger });
+  const todoTools = buildSessionTodoTools(sessionTodo);
   const activeExecution = plan ? activePlanExecution(plan.state) : undefined;
   const interruptedExecution = plan
     ? [...plan.state.executions].reverse().find((execution) => execution.status === 'interrupted')
@@ -482,7 +478,7 @@ function buildDefaultHostTools(
     sandboxBoundary.name,
     'Skill',
     'SkillSearch',
-    ...taskTools.map((tool) => tool.name),
+    ...todoTools.map((tool) => tool.name),
     ...(scheduledTaskTool ? [scheduledTaskTool.name] : []),
     ...goalTools.map((tool) => tool.name),
     ...parentAgentTools.map((tool) => tool.name),
@@ -498,7 +494,7 @@ function buildDefaultHostTools(
     sandboxBoundary,
     buildSkillAgentToolFromInventory(inventoryFor, skillHost, { shadowTracker }),
     buildSkillSearchAgentToolFromInventory(inventoryFor, skillHost, { shadowTracker }),
-    ...taskTools,
+    ...todoTools,
     ...(scheduledTaskTool ? [scheduledTaskTool] : []),
     ...goalTools,
     ...parentAgentTools,
diff --git a/packages/runtime-host/src/server/operation-dispatcher.ts b/packages/runtime-host/src/server/operation-dispatcher.ts
index 18bf74b3c1..a75524acb9 100644
--- a/packages/runtime-host/src/server/operation-dispatcher.ts
+++ b/packages/runtime-host/src/server/operation-dispatcher.ts
@@ -128,14 +128,15 @@ export type SessionRetirementOperationKey = Extract<
   'session.lifecycle.set' | 'session.remove'
 >;
 export type SessionEffectOperationKey = Extract<OperationKey, 'session.recap.generate'>;
+export type SessionTodoOperationKey = Extract<OperationKey, 'session.todo.query'>;
 export type SessionCatalogOperationKey = Exclude<
   Extract<OperationKey, `session.${string}`>,
   | SessionContinuityOperationKey
   | SessionRevisionOperationKey
   | SessionRetirementOperationKey
   | SessionEffectOperationKey
+  | SessionTodoOperationKey
 >;
-export type TaskLedgerOperationKey = Extract<OperationKey, 'task.ledger.query'>;
 export type ArtifactOperationKey = Extract<OperationKey, `artifact.${string}`>;
 export type SkillCatalogOperationKey = Extract<OperationKey, `skill.catalog.${string}`>;
 export type UsagePricingOperationKey = Extract<OperationKey, 'usage.query' | `pricing.${string}`>;
@@ -193,7 +194,7 @@ export type SessionRetirementOperationHandlerMap = Pick<
   SessionRetirementOperationKey
 >;
 export type SessionEffectOperationHandlerMap = Pick<OperationHandlerMap, SessionEffectOperationKey>;
-export type TaskLedgerOperationHandlerMap = Pick<OperationHandlerMap, TaskLedgerOperationKey>;
+export type SessionTodoOperationHandlerMap = Pick<OperationHandlerMap, SessionTodoOperationKey>;
 export type ArtifactOperationHandlerMap = Pick<OperationHandlerMap, ArtifactOperationKey>;
 export type SkillCatalogOperationHandlerMap = Pick<OperationHandlerMap, SkillCatalogOperationKey>;
 export type UsagePricingOperationHandlerMap = Pick<OperationHandlerMap, UsagePricingOperationKey>;
diff --git a/packages/runtime-host/src/server/session-retirement-coordinator.ts b/packages/runtime-host/src/server/session-retirement-coordinator.ts
index c3cb224fe7..22233de377 100644
--- a/packages/runtime-host/src/server/session-retirement-coordinator.ts
+++ b/packages/runtime-host/src/server/session-retirement-coordinator.ts
@@ -31,7 +31,7 @@ import {
   type SessionHeaderSnapshot,
 } from '@maka/storage/execution-stores';
 import { type SessionManager } from '@maka/runtime/session-manager';
-import type { InteractiveTaskLedgerWriter } from '@maka/storage/task-ledger-authority';
+import type { InteractiveSessionTodoWriter } from '@maka/storage/session-todo-authority';
 import {
   type OperationOutcome,
   type SessionCatalogItem,
@@ -120,7 +120,7 @@ export interface HostSessionRetirementCoordinatorOptions {
   readonly capabilities: RetirementCapabilities;
   readonly continuity: RetirementContinuity;
   readonly artifacts: Pick<InteractiveArtifactStoreWriter, 'purgeSessionArtifacts'>;
-  readonly taskLedger: Pick<InteractiveTaskLedgerWriter, 'purgeConversationTaskLedger'>;
+  readonly sessionTodo: Pick<InteractiveSessionTodoWriter, 'purgeSessionState'>;
   readonly assertNoContextOffloadReferences?: (sessionIds: readonly string[]) => Promise<void>;
   readonly purgeOperationalState: (sessionId: string) => Promise<void>;
   readonly purgeAgentGraphState: (sessionId: string) => Promise<void>;
@@ -191,7 +191,7 @@ export class HostSessionRetirementCoordinator {
   readonly #capabilities: RetirementCapabilities;
   readonly #continuity: RetirementContinuity;
   readonly #artifacts: HostSessionRetirementCoordinatorOptions['artifacts'];
-  readonly #taskLedger: HostSessionRetirementCoordinatorOptions['taskLedger'];
+  readonly #sessionTodo: HostSessionRetirementCoordinatorOptions['sessionTodo'];
   readonly #assertNoContextOffloadReferences: HostSessionRetirementCoordinatorOptions['assertNoContextOffloadReferences'];
   readonly #purgeOperationalState: HostSessionRetirementCoordinatorOptions['purgeOperationalState'];
   readonly #purgeAgentGraphState: HostSessionRetirementCoordinatorOptions['purgeAgentGraphState'];
@@ -219,7 +219,7 @@ export class HostSessionRetirementCoordinator {
     this.#capabilities = options.capabilities;
     this.#continuity = options.continuity;
     this.#artifacts = options.artifacts;
-    this.#taskLedger = options.taskLedger;
+    this.#sessionTodo = options.sessionTodo;
     this.#assertNoContextOffloadReferences = options.assertNoContextOffloadReferences;
     this.#purgeOperationalState = options.purgeOperationalState;
     this.#purgeAgentGraphState = options.purgeAgentGraphState;
@@ -671,7 +671,7 @@ export class HostSessionRetirementCoordinator {
       purgeSessionSidecars(
         {
           artifacts: this.#artifacts,
-          taskLedger: this.#taskLedger,
+          sessionTodo: this.#sessionTodo,
           purgeOperationalState: this.#purgeOperationalState,
         },
         sessionId,
diff --git a/packages/runtime-host/src/server/session-revision-coordinator.ts b/packages/runtime-host/src/server/session-revision-coordinator.ts
index 29c11860fc..16c918fff4 100644
--- a/packages/runtime-host/src/server/session-revision-coordinator.ts
+++ b/packages/runtime-host/src/server/session-revision-coordinator.ts
@@ -58,9 +58,9 @@ import {
   type ExecutionStoresWriter,
 } from '@maka/storage/execution-stores';
 import {
-  authenticateInteractiveTaskLedgerWriter,
-  type InteractiveTaskLedgerWriter,
-} from '@maka/storage/task-ledger-authority';
+  authenticateInteractiveSessionTodoWriter,
+  type InteractiveSessionTodoWriter,
+} from '@maka/storage/session-todo-authority';
 import type {
   OperationOutcome,
   SessionConversationCopyInput,
@@ -105,7 +105,7 @@ type ConversationCopyCreateInput = CreateSessionInput & {
 export interface HostSessionRevisionCoordinatorOptions {
   readonly stores: ExecutionStoresWriter<'interactive'>;
   readonly artifacts: InteractiveArtifactStoreWriter;
-  readonly taskLedger: InteractiveTaskLedgerWriter;
+  readonly sessionTodo: InteractiveSessionTodoWriter;
   readonly manager: SessionManager;
   readonly admission: SessionAdmissionGate;
   readonly continuity: SessionContinuityCoordinator;
@@ -127,12 +127,12 @@ export class HostSessionRevisionCoordinator {
 
   readonly #stores: ExecutionStoresWriter<'interactive'>;
   readonly #artifacts: InteractiveArtifactStoreWriter;
-  readonly #taskLedger: InteractiveTaskLedgerWriter;
+  readonly #sessionTodo: InteractiveSessionTodoWriter;
 
   constructor(private readonly options: HostSessionRevisionCoordinatorOptions) {
     this.#stores = authenticateExecutionStoresWriter(options.stores, 'interactive');
     this.#artifacts = authenticateInteractiveArtifactStoreWriter(options.artifacts);
-    this.#taskLedger = authenticateInteractiveTaskLedgerWriter(options.taskLedger);
+    this.#sessionTodo = authenticateInteractiveSessionTodoWriter(options.sessionTodo);
   }
 
   async recover(): Promise<void> {
@@ -550,13 +550,10 @@ export class HostSessionRevisionCoordinator {
         newId: randomUUID,
       });
       const copiedMessages = runtimeCopy.copiedMessages;
-      await this.#taskLedger.copyConversationTaskLedger({
+      await this.#sessionTodo.initializeCopy({
         sourceSessionId: input.sourceSessionId,
         targetSessionId: input.targetSessionId,
-        turnIds: copyTurnIds,
-        ...(slice.beforeTs === undefined ? {} : { beforeTs: slice.beforeTs }),
-        runIdMap: runtimeCopy.runIdMap,
-        ...(kind === 'side_conversation' ? { linkedChildren: 'snapshot' as const } : {}),
+        copyCurrent: kind === 'branch' && slice.beforeTs === undefined,
       });
       if (copiedMessages.length > 0) {
         await this.#stores.sessionStore.appendMessages(input.targetSessionId, [...copiedMessages]);
@@ -821,7 +818,7 @@ export class HostSessionRevisionCoordinator {
     await purgeSessionSidecars(
       {
         artifacts: this.#artifacts,
-        taskLedger: this.#taskLedger,
+        sessionTodo: this.#sessionTodo,
         purgeOperationalState: (sessionId) =>
           this.#stores.purgeConversationOperationalState(sessionId),
       },
diff --git a/packages/runtime-host/src/server/session-sidecar-purge.ts b/packages/runtime-host/src/server/session-sidecar-purge.ts
index b1cdbcf2e2..54a09e4676 100644
--- a/packages/runtime-host/src/server/session-sidecar-purge.ts
+++ b/packages/runtime-host/src/server/session-sidecar-purge.ts
@@ -18,11 +18,11 @@
  */
 
 import type { InteractiveArtifactStoreWriter } from '@maka/storage/artifact-stores';
-import type { InteractiveTaskLedgerWriter } from '@maka/storage/task-ledger-authority';
+import type { InteractiveSessionTodoWriter } from '@maka/storage/session-todo-authority';
 
 export interface SessionSidecarPurgeAuthority {
   readonly artifacts: Pick<InteractiveArtifactStoreWriter, 'purgeSessionArtifacts'>;
-  readonly taskLedger: Pick<InteractiveTaskLedgerWriter, 'purgeConversationTaskLedger'>;
+  readonly sessionTodo: Pick<InteractiveSessionTodoWriter, 'purgeSessionState'>;
   readonly purgeOperationalState: (sessionId: string) => Promise<void>;
 }
 
@@ -32,7 +32,7 @@ export async function purgeSessionSidecars(
 ): Promise<void> {
   const outcomes = await Promise.allSettled([
     authority.artifacts.purgeSessionArtifacts(sessionId),
-    authority.taskLedger.purgeConversationTaskLedger(sessionId),
+    authority.sessionTodo.purgeSessionState(sessionId),
     authority.purgeOperationalState(sessionId),
   ]);
   const failures = outcomes.flatMap((outcome) =>
diff --git a/packages/runtime-host/src/server/session-todo-coordinator.ts b/packages/runtime-host/src/server/session-todo-coordinator.ts
new file mode 100644
index 0000000000..f46ee073a7
--- /dev/null
+++ b/packages/runtime-host/src/server/session-todo-coordinator.ts
@@ -0,0 +1,94 @@
+/*
+ * 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 { SessionTodoSnapshot } from '@maka/core/session-todo';
+import {
+  authenticateInteractiveSessionTodoWriter,
+  type InteractiveSessionTodoWriter,
+} from '@maka/storage/session-todo-authority';
+import type { OperationOutcome, SessionTodoQueryResult } from '../protocol/index.js';
+import type { SessionTodoOperationHandlerMap } from './operation-dispatcher.js';
+import { SessionAdmissionGate } from './session-admission-gate.js';
+import type { SessionPresenceReader } from './session-presence.js';
+
+export interface SessionTodoPort {
+  read(sessionId: string): Promise<SessionTodoSnapshot>;
+  replace(sessionId: string, items: unknown): Promise<SessionTodoSnapshot>;
+}
+
+/** Host-owned admission and publication boundary for the SessionTodo document. */
+export class HostSessionTodoCoordinator implements SessionTodoPort {
+  readonly handlers: SessionTodoOperationHandlerMap = {
+    'session.todo.query': (input) => this.#query(input.sessionId),
+  };
+
+  readonly #writer: InteractiveSessionTodoWriter;
+
+  constructor(
+    writer: InteractiveSessionTodoWriter,
+    private readonly sessionAdmission: SessionAdmissionGate,
+    private readonly sessions: SessionPresenceReader,
+    private readonly onChanged: (sessionId: string) => void,
+    private readonly requestDrain: () => void,
+  ) {
+    this.#writer = authenticateInteractiveSessionTodoWriter(writer);
+  }
+
+  read(sessionId: string): Promise<SessionTodoSnapshot> {
+    return this.sessionAdmission.run(sessionId, async () => {
+      await this.#requirePresent(sessionId);
+      return this.#writer.readOrBootstrap(sessionId);
+    });
+  }
+
+  replace(sessionId: string, items: unknown): Promise<SessionTodoSnapshot> {
+    return this.sessionAdmission.run(sessionId, async () => {
+      await this.#requirePresent(sessionId);
+      const snapshot = await this.#writer.replaceAll(sessionId, items);
+      try {
+        this.onChanged(sessionId);
+      } catch {
+        // The document is already committed. A projection failure drains the
+        // Host but must never turn a successful whole-document write into an
+        // ambiguous retry that could overwrite a later writer.
+        this.requestDrain();
+      }
+      return snapshot;
+    });
+  }
+
+  async #query(sessionId: string): Promise<OperationOutcome<'session.todo.query'>> {
+    try {
+      const snapshot = await this.read(sessionId);
+      const result: SessionTodoQueryResult = { sessionId, items: snapshot.items };
+      return { ok: true, result };
+    } catch (error) {
+      if ((await this.sessions.probeSessionRemoval(sessionId)).kind !== 'present') {
+        return { ok: false, error: { code: 'not_found', message: 'Session was not found' } };
+      }
+      throw error;
+    }
+  }
+
+  async #requirePresent(sessionId: string): Promise<void> {
+    if ((await this.sessions.probeSessionRemoval(sessionId)).kind !== 'present') {
+      throw new Error(`Session was not found: ${sessionId}`);
+    }
+  }
+}
diff --git a/packages/runtime-host/src/server/task-ledger-coordinator.ts b/packages/runtime-host/src/server/task-ledger-coordinator.ts
deleted file mode 100644
index e447fd995a..0000000000
--- a/packages/runtime-host/src/server/task-ledger-coordinator.ts
+++ /dev/null
@@ -1,249 +0,0 @@
-/*
- * 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 { createHash } from 'node:crypto';
-import {
-  findTaskByRef,
-  type Task,
-  type TaskAgentOutcome,
-  type TaskAvailableClaimScope,
-  type TaskLedgerChangedEvent,
-  type TaskLedgerListOptions,
-  type TaskLedgerMutationContext,
-  type TaskLedgerStore,
-  type TaskOwner,
-} from '@maka/core/task-ledger';
-import {
-  authenticateInteractiveTaskLedgerWriter,
-  type InteractiveTaskLedgerWriter,
-} from '@maka/storage/task-ledger-authority';
-import {
-  encodeTaskLedgerTask,
-  encodeTaskLedgerQueryResult,
-  TASK_LEDGER_PAGE_MAX_BYTES,
-  TASK_LEDGER_PAGE_MAX_ITEMS,
-  type OperationOutcome,
-  type TaskLedgerQueryInput,
-  type TaskLedgerQueryResult,
-  type TaskLedgerRevision,
-  type TaskLedgerTask,
-} from '../protocol/index.js';
-import type { TaskLedgerOperationHandlerMap } from './operation-dispatcher.js';
-import { SessionAdmissionGate } from './session-admission-gate.js';
-import type { SessionPresenceReader } from './session-presence.js';
-
-const CANONICAL_LIST_OPTIONS = Object.freeze({
-  includeTerminal: true,
-  includeArchived: false,
-  classifyResumeTrust: true,
-});
-
-/** The Host-owned Task Ledger authority shared by Client queries and Runtime tools. */
-export class HostTaskLedgerCoordinator implements TaskLedgerStore {
-  readonly handlers: TaskLedgerOperationHandlerMap = {
-    'task.ledger.query': (input) => this.#query(input),
-  };
-
-  readonly #writer: InteractiveTaskLedgerWriter;
-
-  constructor(
-    writer: InteractiveTaskLedgerWriter,
-    private readonly sessionAdmission: SessionAdmissionGate,
-    private readonly sessions: SessionPresenceReader,
-  ) {
-    this.#writer = authenticateInteractiveTaskLedgerWriter(writer);
-  }
-
-  list(sessionId: string, options?: TaskLedgerListOptions): Promise<Task[]> {
-    return this.sessionAdmission.run(sessionId, () => this.#writer.list(sessionId, options));
-  }
-
-  get(sessionId: string, id: string, options?: TaskLedgerListOptions): Promise<Task | undefined> {
-    return this.sessionAdmission.run(sessionId, () => this.#writer.get(sessionId, id, options));
-  }
-
-  create(
-    sessionId: string,
-    drafts: unknown,
-    context?: TaskLedgerMutationContext,
-  ): Promise<{ created: Task[]; total: number }> {
-    return this.sessionAdmission.run(sessionId, () =>
-      this.#writer.create(sessionId, drafts, context),
-    );
-  }
-
-  update(
-    sessionId: string,
-    id: string,
-    patch: unknown,
-    context?: TaskLedgerMutationContext,
-  ): Promise<{ updated: Task; total: number }> {
-    return this.sessionAdmission.run(sessionId, () =>
-      this.#writer.update(sessionId, id, patch, context),
-    );
-  }
-
-  claim(
-    sessionId: string,
-    id: string,
-    owner: TaskOwner,
-    context?: TaskLedgerMutationContext,
-  ): Promise<{ updated: Task; total: number }> {
-    return this.sessionAdmission.run(sessionId, () =>
-      this.#writer.claim(sessionId, id, owner, context),
-    );
-  }
-
-  claimAvailable(
-    sessionId: string,
-    id: string,
-    owner: TaskOwner,
-    scope: TaskAvailableClaimScope,
-    context?: TaskLedgerMutationContext,
-  ): Promise<{ updated: Task; total: number }> {
-    return this.sessionAdmission.run(sessionId, () =>
-      this.#writer.claimAvailable(sessionId, id, owner, scope, context),
-    );
-  }
-
-  settleAgentOutcome(
-    sessionId: string,
-    id: string,
-    outcome: TaskAgentOutcome,
-    context?: TaskLedgerMutationContext,
-  ): Promise<{ updated: Task; total: number }> {
-    return this.sessionAdmission.run(sessionId, () =>
-      this.#writer.settleAgentOutcome(sessionId, id, outcome, context),
-    );
-  }
-
-  subscribe(listener: (event: TaskLedgerChangedEvent) => void): () => void {
-    return this.#writer.subscribe(listener);
-  }
-
-  #query(input: TaskLedgerQueryInput): Promise<OperationOutcome<'task.ledger.query'>> {
-    return this.sessionAdmission.run(input.sessionId, async () => {
-      if ((await this.sessions.probeSessionRemoval(input.sessionId)).kind !== 'present') {
-        return notFound('Session was not found');
-      }
-      const tasks = (await this.#writer.list(input.sessionId, CANONICAL_LIST_OPTIONS)).map(
-        encodeTaskLedgerTask,
-      );
-      const revision = taskLedgerRevision(tasks);
-
-      if (input.kind === 'get') {
-        return success(
-          encodeTaskLedgerQueryResult({
-            kind: 'task',
-            sessionId: input.sessionId,
-            revision,
-            task: findTaskByRef(tasks, input.taskRef) ?? null,
-          }),
-        );
-      }
-
-      if (input.kind === 'list_continue' && input.revision !== revision) {
-        return success({
-          kind: 'revision_changed',
-          expected: input.revision,
-          actual: revision,
-        });
-      }
-
-      const offset = input.kind === 'list_start' ? 0 : decodeCursor(input.cursor);
-      if (
-        offset === undefined ||
-        offset > tasks.length ||
-        (input.kind === 'list_continue' && offset === tasks.length)
-      ) {
-        return invalidRequest('Task ledger cursor is invalid');
-      }
-      return success(createPage(input.sessionId, revision, tasks, offset));
-    });
-  }
-}
-
-function taskLedgerRevision(tasks: readonly TaskLedgerTask[]): TaskLedgerRevision {
-  return `sha256:${createHash('sha256').update(JSON.stringify(tasks)).digest('hex')}`;
-}
-
-function createPage(
-  sessionId: string,
-  revision: TaskLedgerRevision,
-  tasks: readonly TaskLedgerTask[],
-  offset: number,
-): TaskLedgerQueryResult {
-  const pageTasks: TaskLedgerTask[] = [];
-  for (let index = offset; index < tasks.length; index += 1) {
-    if (pageTasks.length >= TASK_LEDGER_PAGE_MAX_ITEMS) break;
-    const task = tasks[index];
-    if (!task) throw invariantFailure('Task projection index was out of bounds');
-    const candidateTasks = [...pageTasks, task];
-    const nextOffset = index + 1;
-    const candidate = {
-      kind: 'page' as const,
-      sessionId,
-      revision,
-      tasks: candidateTasks,
-      nextCursor: nextOffset < tasks.length ? encodeCursor(nextOffset) : null,
-    };
-    if (Buffer.byteLength(JSON.stringify(candidate), 'utf8') > TASK_LEDGER_PAGE_MAX_BYTES) {
-      break;
-    }
-    pageTasks.push(task);
-  }
-
-  if (pageTasks.length === 0 && offset < tasks.length) {
-    throw invariantFailure('A canonical Task exceeded the page result byte limit');
-  }
-  const nextOffset = offset + pageTasks.length;
-  return encodeTaskLedgerQueryResult({
-    kind: 'page',
-    sessionId,
-    revision,
-    tasks: pageTasks,
-    nextCursor: nextOffset < tasks.length ? encodeCursor(nextOffset) : null,
-  });
-}
-
-function encodeCursor(offset: number): string {
-  return String(offset);
-}
-
-function decodeCursor(cursor: string): number | undefined {
-  if (!/^(?:0|[1-9]\d*)$/.test(cursor)) return undefined;
-  const offset = Number(cursor);
-  return Number.isSafeInteger(offset) ? offset : undefined;
-}
-
-function success(result: TaskLedgerQueryResult): OperationOutcome<'task.ledger.query'> {
-  return { ok: true, result };
-}
-
-function invalidRequest(message: string): OperationOutcome<'task.ledger.query'> {
-  return { ok: false, error: { code: 'invalid_request', message } };
-}
-
-function notFound(message: string): OperationOutcome<'task.ledger.query'> {
-  return { ok: false, error: { code: 'not_found', message } };
-}
-
-function invariantFailure(message: string): Error {
-  return new Error(`Task ledger coordinator invariant failed: ${message}`);
-}
diff --git a/packages/runtime/package.json b/packages/runtime/package.json
index a5ac78b2e7..5ed51233f8 100644
--- a/packages/runtime/package.json
+++ b/packages/runtime/package.json
@@ -21,6 +21,7 @@
     "./test-connection": "./dist/test-connection.js",
     "./model-fetcher": "./dist/model-fetcher.js",
     "./session-manager": "./dist/session-manager.js",
+    "./session-todo-tools": "./dist/session-todo-tools.js",
     "./test-only/fake-backend": "./dist/test-only/fake-backend.js",
     "./test-only/observation-text-reader": "./dist/__tests__/observation-text-reader.js",
     "./filesystem-worker": "./dist/filesystem-worker/index.js",
@@ -101,7 +102,6 @@
     "./system-prompt/personalization-prompt": "./dist/system-prompt/personalization-prompt.js",
     "./system-prompt/project-context": "./dist/system-prompt/project-context.js",
     "./system-prompt/workspace-instructions": "./dist/system-prompt/workspace-instructions.js",
-    "./task-ledger-tools": "./dist/task-ledger-tools.js",
     "./tavily-search": "./dist/tavily-search.js",
     "./terminal-run-commit": "./dist/terminal-run-commit.js",
     "./tool-availability": "./dist/tool-availability.js",
diff --git a/packages/runtime/src/__tests__/session-todo-tools.test.ts b/packages/runtime/src/__tests__/session-todo-tools.test.ts
new file mode 100644
index 0000000000..cefd048c25
--- /dev/null
+++ b/packages/runtime/src/__tests__/session-todo-tools.test.ts
@@ -0,0 +1,116 @@
+/*
+ * 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 assert from 'node:assert/strict';
+import { test } from 'node:test';
+import { SESSION_TODO_CONTENT_MAX_CHARS } from '@maka/core/session-todo';
+import { z } from 'zod';
+import { buildSessionTodoTools, type SessionTodoToolStore } from '../session-todo-tools.js';
+
+const context = {
+  sessionId: 'session-1',
+  turnId: 'turn-1',
+  cwd: '/tmp',
+  toolCallId: 'tool-1',
+  abortSignal: new AbortController().signal,
+  emitOutput: () => {},
+};
+
+test('todo_read returns the complete committed snapshot and names empty distinctly', async () => {
+  const store: SessionTodoToolStore = {
+    read: async () => ({ items: [] }),
+    replace: async () => assert.fail('read must not write'),
+  };
+  const read = buildSessionTodoTools(store).find((tool) => tool.name === 'todo_read')!;
+  assert.equal(await read.impl({}, context), 'Todo list is empty.');
+});
+
+test('todo_write renders only the store-returned committed snapshot', async () => {
+  const store: SessionTodoToolStore = {
+    read: async () => ({ items: [] }),
+    replace: async () => ({
+      items: [{ content: 'committed result', status: 'in_progress' }],
+    }),
+  };
+  const write = buildSessionTodoTools(store).find((tool) => tool.name === 'todo_write')!;
+  const result = await write.impl(
+    { todos: [{ content: 'uncommitted args', status: 'pending' }] },
+    context,
+  );
+  assert.match(String(result), /committed result/);
+  assert.doesNotMatch(String(result), /uncommitted args/);
+});
+
+test('todo_write names a successful clear and propagates failure', async () => {
+  let fail = false;
+  const store: SessionTodoToolStore = {
+    read: async () => ({ items: [] }),
+    replace: async () => {
+      if (fail) throw new Error('write failed');
+      return { items: [] };
+    },
+  };
+  const write = buildSessionTodoTools(store).find((tool) => tool.name === 'todo_write')!;
+  assert.equal(await write.impl({ todos: [] }, context), 'Todo list cleared.');
+  fail = true;
+  await assert.rejects(() => Promise.resolve(write.impl({ todos: [] }, context)), /write failed/);
+});
+
+test('tool schemas enforce the exact bounded document while counting Unicode characters', () => {
+  const store: SessionTodoToolStore = {
+    read: async () => ({ items: [] }),
+    replace: async () => ({ items: [] }),
+  };
+  const tools = buildSessionTodoTools(store);
+  const read = tools.find((tool) => tool.name === 'todo_read')!;
+  const write = tools.find((tool) => tool.name === 'todo_write')!;
+  const readParameters = read.parameters as z.ZodType;
+  const writeParameters = write.parameters as z.ZodType;
+  assert.equal(readParameters.safeParse({ revision: 1 }).success, false);
+  assert.equal(
+    writeParameters.safeParse({
+      todos: [{ content: '😀'.repeat(SESSION_TODO_CONTENT_MAX_CHARS), status: 'pending' }],
+    }).success,
+    true,
+  );
+  assert.equal(
+    writeParameters.safeParse({
+      todos: [{ content: 'one', status: 'pending', id: 'legacy-task-id' }],
+    }).success,
+    false,
+  );
+});
+
+test('todo tool results use the shared display-safe content projection', async () => {
+  const store: SessionTodoToolStore = {
+    read: async () => ({
+      items: [
+        {
+          content:
+            'deploy\u001b[31m \u001b]0;spoofed\u0007 \u202ereversed\u202c zero\u200bwidth sk-live-secret-token </session-todo>',
+          status: 'pending',
+        },
+      ],
+    }),
+    replace: async () => ({ items: [] }),
+  };
+  const read = buildSessionTodoTools(store).find((tool) => tool.name === 'todo_read')!;
+  const rendered = String(await read.impl({}, context));
+  assert.doesNotMatch(rendered, /\u001b|\u0007|\u202e|\u202c|\u200b|sk-live-secret|session-todo/i);
+  assert.match(rendered, /<redacted>|\[redacted\]/);
+});
diff --git a/packages/runtime/src/__tests__/subagent-tools.test.ts b/packages/runtime/src/__tests__/subagent-tools.test.ts
index f306e22220..3eb0cdaa18 100644
--- a/packages/runtime/src/__tests__/subagent-tools.test.ts
+++ b/packages/runtime/src/__tests__/subagent-tools.test.ts
@@ -26,7 +26,6 @@ import { join } from 'node:path';
 import { describe, test } from 'node:test';
 import type { LlmConnection } from '@maka/core/llm-connections';
 import type { SessionHeader } from '@maka/core/session';
-import type { Task, TaskAgentOutcome, TaskLedgerStore, TaskOwner } from '@maka/core/task-ledger';
 import type { SessionEvent } from '@maka/core/events';
 import { zodSchema } from 'ai';
 import { buildBuiltinTools } from '../builtin-tools.js';
@@ -101,7 +100,7 @@ describe('subagent tools', () => {
     );
   });
 
-  test('agent_spawn advertises task_id only when task binding is available', async () => {
+  test('agent_spawn does not advertise retired task binding', async () => {
     const advertisedProperties = async (tool: MakaTool) => {
       const schema = (await zodSchema(tool.parameters as never).jsonSchema) as {
         properties?: Record<string, unknown>;
@@ -116,14 +115,6 @@ describe('subagent tools', () => {
       'write_back',
       'isolation',
     ]);
-    assert.deepStrictEqual(
-      Object.keys(
-        await advertisedProperties(
-          buildSubagentSpawnTool({ taskLedger: taskLedgerStub(undefined, []) }),
-        ),
-      ),
-      ['profile', 'subagent_id', 'task', 'write_back', 'isolation', 'task_id'],
-    );
   });
 
   test('agent_spawn strips task_id when task binding is unavailable', () => {
@@ -422,7 +413,6 @@ describe('subagent tools', () => {
       {
         profile: LOCAL_READ_AGENT_PROFILE,
         task: 'Inspect the runtime tests.',
-        task_id: 'ignored-without-task-binding',
       },
       {
         sessionId: 'session-1',
@@ -632,258 +622,6 @@ describe('subagent tools', () => {
     assert.strictEqual((output[1]?.length ?? Number.POSITIVE_INFINITY) < 1_100, true);
   });
 
-  test('agent_spawn binds a current-session task and records real child refs without auto-completing', async () => {
-    const task: Task = {
-      id: 'task-uuid',
-      key: 'T1',
-      subject: 'inspect runtime',
-      status: 'pending',
-      createdAt: 1,
-      updatedAt: 1,
-    };
-    const calls: string[] = [];
-    const ledger = taskLedgerStub(task, calls);
-    const tool = buildSubagentSpawnTool({ taskLedger: ledger });
-    const result = await tool.impl(
-      {
-        profile: LOCAL_READ_AGENT_PROFILE,
-        task: 'Inspect the runtime tests.',
-        task_id: 'T1',
-      },
-      {
-        sessionId: 'session-1',
-        turnId: 'parent-turn',
-        cwd: '/tmp/cwd',
-        toolCallId: 'tool-1',
-        abortSignal: new AbortController().signal,
-        emitOutput: () => {},
-        spawnChildSession: async (input) => {
-          await input.onReady?.({
-            childSessionId: 'child-session',
-            runId: 'child-run',
-            turnId: 'child-turn',
-            agentId: requireBuiltinAgentDefinitionByProfile(input.agentProfile).id,
-            agentName: requireBuiltinAgentDefinitionByProfile(input.agentProfile).name,
-            permissionMode: 'explore',
-          });
-          return {
-            agentId: requireBuiltinAgentDefinitionByProfile(input.agentProfile).id,
-            agentName: requireBuiltinAgentDefinitionByProfile(input.agentProfile).name,
-            runId: 'child-run',
-            turnId: 'child-turn',
-            status: 'completed',
-            permissionMode: 'explore',
-            summary: 'inspection complete',
-            artifactIds: [],
-          };
-        },
-      },
-    );
-    assert.deepStrictEqual(calls, [
-      'get:session-1:T1',
-      'claim:child-turn',
-      'settle:completed:child-run',
-    ]);
-    assert.strictEqual(task.status, 'in_progress');
-    assert.deepStrictEqual(task.owner, {
-      actor: 'child_agent',
-      sessionId: 'child-session',
-      agentId: LOCAL_READ_AGENT_ID,
-      runId: 'child-run',
-      turnId: 'child-turn',
-    });
-    assert.partialDeepStrictEqual(result, {
-      kind: 'subagent',
-      runId: 'child-run',
-      status: 'completed',
-    });
-  });
-
-  test('agent_spawn rejects a forged task reference before starting a child', async () => {
-    let spawned = false;
-    const ledger = taskLedgerStub(undefined, []);
-    const tool = buildSubagentSpawnTool({ taskLedger: ledger });
-    await expectRejects(
-      Promise.resolve(
-        tool.impl(
-          {
-            profile: LOCAL_READ_AGENT_PROFILE,
-            task: 'Inspect.',
-            task_id: 'T99',
-          },
-          {
-            sessionId: 'session-1',
-            turnId: 'parent-turn',
-            cwd: '/tmp',
-            toolCallId: 'tool-1',
-            abortSignal: new AbortController().signal,
-            emitOutput: () => {},
-            spawnChildSession: async () => {
-              spawned = true;
-              return {};
-            },
-          },
-        ),
-      ),
-      /No such task in this session/,
-    );
-    assert.strictEqual(spawned, false);
-  });
-
-  test('agent_spawn records failed and cancelled child outcomes with real refs', async () => {
-    for (const status of ['failed', 'cancelled'] as const) {
-      const task: Task = {
-        id: `task-${status}`,
-        key: 'T1',
-        subject: status,
-        status: 'pending',
-        createdAt: 1,
-        updatedAt: 1,
-      };
-      const calls: string[] = [];
-      const tool = buildSubagentSpawnTool({ taskLedger: taskLedgerStub(task, calls) });
-      const result = await tool.impl(
-        {
-          profile: LOCAL_READ_AGENT_PROFILE,
-          task: `Run child that becomes ${status}.`,
-          task_id: task.key,
-        },
-        {
-          sessionId: 'session-1',
-          turnId: 'parent-turn',
-          cwd: '/tmp',
-          toolCallId: 'tool-1',
-          abortSignal: new AbortController().signal,
-          emitOutput: () => {},
-          spawnChildSession: async (input) => {
-            await input.onReady?.({
-              childSessionId: 'child-session',
-              runId: 'child-run',
-              turnId: `child-${status}`,
-              agentId: requireBuiltinAgentDefinitionByProfile(input.agentProfile).id,
-              agentName: requireBuiltinAgentDefinitionByProfile(input.agentProfile).name,
-              permissionMode: 'explore',
-            });
-            return {
-              agentId: requireBuiltinAgentDefinitionByProfile(input.agentProfile).id,
-              agentName: requireBuiltinAgentDefinitionByProfile(input.agentProfile).name,
-              runId: `run-${status}`,
-              turnId: `child-${status}`,
-              status,
-              permissionMode: 'explore',
-              summary: `${status} summary`,
-              artifactIds: [],
-            };
-          },
-        },
-      );
-      assert.deepStrictEqual(calls, [
-        'get:session-1:T1',
-        `claim:child-${status}`,
-        `settle:${status}:run-${status}`,
-      ]);
-      assert.strictEqual(task.status, status);
-      assert.deepStrictEqual(task.owner, {
-        actor: 'child_agent',
-        sessionId: 'child-session',
-        agentId: LOCAL_READ_AGENT_ID,
-        runId: `run-${status}`,
-        turnId: `child-${status}`,
-      });
-      assert.partialDeepStrictEqual(result, { kind: 'subagent', status, runId: `run-${status}` });
-    }
-  });
-
-  test('agent_spawn marks a claimed task failed when child startup throws', async () => {
-    const task: Task = {
-      id: 'task-startup-failure',
-      key: 'T1',
-      subject: 'startup',
-      status: 'pending',
-      createdAt: 1,
-      updatedAt: 1,
-    };
-    const calls: string[] = [];
-    const tool = buildSubagentSpawnTool({ taskLedger: taskLedgerStub(task, calls) });
-    await expectRejects(
-      Promise.resolve(
-        tool.impl(
-          {
-            profile: LOCAL_READ_AGENT_PROFILE,
-            task: 'Fail after allocating the child turn.',
-            task_id: task.key,
-          },
-          {
-            sessionId: 'session-1',
-            turnId: 'parent-turn',
-            cwd: '/tmp',
-            toolCallId: 'tool-1',
-            abortSignal: new AbortController().signal,
-            emitOutput: () => {},
-            spawnChildSession: async (input) => {
-              await input.onReady?.({
-                childSessionId: 'child-session',
-                runId: 'child-run',
-                turnId: 'child-turn',
-                agentId: requireBuiltinAgentDefinitionByProfile(input.agentProfile).id,
-                agentName: requireBuiltinAgentDefinitionByProfile(input.agentProfile).name,
-                permissionMode: 'explore',
-              });
-              throw new Error('child startup failed');
-            },
-          },
-        ),
-      ),
-      /child startup failed/,
-    );
-    assert.deepStrictEqual(calls, [
-      'get:session-1:T1',
-      'claim:child-turn',
-      'settle:failed:undefined',
-    ]);
-    assert.strictEqual(task.status, 'failed');
-  });
-
-  test('agent_spawn rejects a task reference that only exists in another session', async () => {
-    const task: Task = {
-      id: 'other-task',
-      key: 'T1',
-      subject: 'other',
-      status: 'pending',
-      createdAt: 1,
-      updatedAt: 1,
-    };
-    const ledger = taskLedgerStub(task, []);
-    ledger.get = async (sessionId) => (sessionId === 'session-2' ? task : undefined);
-    let spawned = false;
-    const tool = buildSubagentSpawnTool({ taskLedger: ledger });
-    await expectRejects(
-      Promise.resolve(
-        tool.impl(
-          {
-            profile: LOCAL_READ_AGENT_PROFILE,
-            task: 'Inspect.',
-            task_id: task.key,
-          },
-          {
-            sessionId: 'session-1',
-            turnId: 'parent-turn',
-            cwd: '/tmp',
-            toolCallId: 'tool-1',
-            abortSignal: new AbortController().signal,
-            emitOutput: () => {},
-            spawnChildSession: async () => {
-              spawned = true;
-              return {};
-            },
-          },
-        ),
-      ),
-      /No such task in this session/,
-    );
-    assert.strictEqual(spawned, false);
-  });
-
   test('agent_spawn validates profile contracts and delegates worktree availability to runtime', async () => {
     const tool = buildSubagentSpawnTool();
     const schema = tool.parameters as {
@@ -1379,40 +1117,3 @@ function testConnection(): LlmConnection {
     updatedAt: 1,
   };
 }
-function taskLedgerStub(task: Task | undefined, calls: string[]): TaskLedgerStore {
-  return {
-    list: async () => (task ? [task] : []),
-    get: async (sessionId, id) => {
-      calls.push(`get:${sessionId}:${id}`);
-      return task && (task.id === id || task.key === id) ? task : undefined;
-    },
-    create: async () => ({ created: [], total: task ? 1 : 0 }),
-    update: async () => {
-      if (!task) throw new Error('No such task');
-      return { updated: task, total: 1 };
-    },
-    claim: async (_sessionId, _id, owner: TaskOwner) => {
-      if (!task) throw new Error('No such task');
-      calls.push(`claim:${owner.turnId}`);
-      task.status = 'in_progress';
-      task.owner = owner;
-      return { updated: task, total: 1 };
-    },
-    claimAvailable: async (_sessionId, _id, owner: TaskOwner) => {
-      if (!task) throw new Error('No such task');
-      calls.push(`claimAvailable:${owner.turnId}`);
-      task.status = 'in_progress';
-      task.owner = owner;
-      return { updated: task, total: 1 };
-    },
-    settleAgentOutcome: async (_sessionId, _id, outcome: TaskAgentOutcome) => {
-      if (!task) throw new Error('No such task');
-      calls.push(`settle:${outcome.status}:${outcome.owner.runId}`);
-      task.owner = outcome.owner;
-      if (outcome.status === 'failed') task.status = 'failed';
-      if (outcome.status === 'cancelled') task.status = 'cancelled';
-      return { updated: task, total: 1 };
-    },
-    subscribe: () => () => {},
-  };
-}
diff --git a/packages/runtime/src/__tests__/task-ledger-tools.test.ts b/packages/runtime/src/__tests__/task-ledger-tools.test.ts
deleted file mode 100644
index c86d0c4439..0000000000
--- a/packages/runtime/src/__tests__/task-ledger-tools.test.ts
+++ /dev/null
@@ -1,574 +0,0 @@
-/*
- * 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 { describe, test } from 'node:test';
-import assert from 'node:assert/strict';
-import { z } from 'zod';
-import {
-  TASK_EVIDENCE_MAX_CHARS,
-  TASK_LEDGER_MAX_TASKS,
-  TASK_SUBJECT_MAX_CHARS,
-  type Task,
-  type TaskAgentOutcome,
-  type TaskLedgerListOptions,
-  type TaskLedgerMutationContext,
-  type TaskLedgerStore,
-  type TaskOwner,
-} from '@maka/core/task-ledger';
-import {
-  TASK_CREATE_TOOL_NAME,
-  TASK_GET_TOOL_NAME,
-  TASK_LIST_TOOL_NAME,
-  TASK_UPDATE_TOOL_NAME,
-  buildTaskLedgerTools,
-} from '../task-ledger-tools.js';
-import type { MakaTool, MakaToolContext } from '../tool-runtime.js';
-
-const SESSION_ID = 'sess-1';
-
-class FakeTaskLedgerStore implements TaskLedgerStore {
-  private tasks: Task[] = [];
-  public createCalls: Array<{
-    sessionId: string;
-    drafts: unknown;
-    context?: TaskLedgerMutationContext;
-  }> = [];
-  public updateCalls: Array<{
-    sessionId: string;
-    id: string;
-    patch: unknown;
-    context?: TaskLedgerMutationContext;
-  }> = [];
-  public listCalls: Array<{ sessionId: string; options?: TaskLedgerListOptions }> = [];
-
-  seed(tasks: Task[]): void {
-    this.tasks = tasks.map((task) => ({ ...task }));
-  }
-
-  async list(sessionId: string, options?: TaskLedgerListOptions): Promise<Task[]> {
-    this.listCalls.push({ sessionId, options });
-    return this.tasks
-      .filter((task) => {
-        if (options?.status && task.status !== options.status) return false;
-        if (
-          options?.includeTerminal === false &&
-          ['completed', 'failed', 'cancelled'].includes(task.status)
-        )
-          return false;
-        return true;
-      })
-      .map((t) => ({
-        ...t,
-        ...(options?.classifyResumeTrust === true && t.status === 'in_progress'
-          ? { resumeTrust: 'stale' as const }
-          : {}),
-      }));
-  }
-
-  async get(
-    _sessionId: string,
-    id: string,
-    options?: TaskLedgerListOptions,
-  ): Promise<Task | undefined> {
-    const task = this.tasks.find((t) => t.id === id || t.key === id);
-    return task
-      ? {
-          ...task,
-          ...(options?.classifyResumeTrust === true && task.status === 'in_progress'
-            ? { resumeTrust: 'stale' as const }
-            : {}),
-        }
-      : undefined;
-  }
-
-  async create(
-    sessionId: string,
-    drafts: unknown,
-    context?: TaskLedgerMutationContext,
-  ): Promise<{ created: Task[]; total: number }> {
-    this.createCalls.push({ sessionId, drafts, context });
-    const now = Date.now();
-    const created = (drafts as Array<{ subject: string }>).map((d, i) => ({
-      id: `id-${this.tasks.length + i}`,
-      key: `T${this.tasks.length + i + 1}`,
-      subject: d.subject,
-      status: 'pending' as const,
-      createdAt: now,
-      updatedAt: now,
-    }));
-    this.tasks.push(...created);
-    return { created, total: this.tasks.length };
-  }
-
-  async update(
-    sessionId: string,
-    id: string,
-    patch: unknown,
-    context?: TaskLedgerMutationContext,
-  ): Promise<{ updated: Task; total: number }> {
-    this.updateCalls.push({ sessionId, id, patch, context });
-    const task = this.tasks.find((t) => t.id === id);
-    if (!task) throw new Error(`No such task: ${id}`);
-    Object.assign(task, patch, { updatedAt: Date.now() });
-    return { updated: { ...task }, total: this.tasks.length };
-  }
-
-  async claim(
-    _sessionId: string,
-    id: string,
-    owner: TaskOwner,
-  ): Promise<{ updated: Task; total: number }> {
-    const task = this.tasks.find((item) => item.id === id || item.key === id);
-    if (!task) throw new Error(`No such task: ${id}`);
-    Object.assign(task, { status: 'in_progress', owner });
-    return { updated: { ...task }, total: this.tasks.length };
-  }
-
-  async claimAvailable(
-    sessionId: string,
-    id: string,
-    owner: TaskOwner,
-  ): Promise<{ updated: Task; total: number }> {
-    return this.claim(sessionId, id, owner);
-  }
-
-  async settleAgentOutcome(
-    _sessionId: string,
-    id: string,
-    outcome: TaskAgentOutcome,
-  ): Promise<{ updated: Task; total: number }> {
-    const task = this.tasks.find((item) => item.id === id || item.key === id);
-    if (!task) throw new Error(`No such task: ${id}`);
-    Object.assign(task, { owner: outcome.owner });
-    return { updated: { ...task }, total: this.tasks.length };
-  }
-
-  subscribe(): () => void {
-    return () => {};
-  }
-}
-
-function fakeContext(sessionId: string, runId?: string): MakaToolContext {
-  return {
-    sessionId,
-    turnId: 'turn-1',
-    ...(runId ? { runId } : {}),
-    cwd: '/tmp',
-    toolCallId: 'call-1',
-    abortSignal: new AbortController().signal,
-    emitOutput: () => {},
-  };
-}
-
-function findTool(tools: MakaTool[], name: string): MakaTool {
-  const tool = tools.find((t) => t.name === name);
-  assert.ok(tool, `expected tool ${name}`);
-  return tool;
-}
-
-describe('task ledger tools', () => {
-  test('task_create schema rejects a batch larger than the ledger cap and accepts the cap boundary', () => {
-    const create = findTool(
-      buildTaskLedgerTools({ store: new FakeTaskLedgerStore() }),
-      TASK_CREATE_TOOL_NAME,
-    );
-    const params = create.parameters as z.ZodType;
-    const atCap = {
-      tasks: Array.from({ length: TASK_LEDGER_MAX_TASKS }, () => ({ subject: 'x' })),
-    };
-    assert.equal(
-      params.safeParse(atCap).success,
-      true,
-      `${TASK_LEDGER_MAX_TASKS} tasks (cap) must pass`,
-    );
-    const overCap = {
-      tasks: Array.from({ length: TASK_LEDGER_MAX_TASKS + 1 }, () => ({ subject: 'x' })),
-    };
-    assert.equal(
-      params.safeParse(overCap).success,
-      false,
-      `${TASK_LEDGER_MAX_TASKS + 1} tasks must be rejected at the schema`,
-    );
-  });
-
-  test('task_update schema rejects ids that are not stable tokens and accepts UUID-shaped / simple ids', () => {
-    const update = findTool(
-      buildTaskLedgerTools({ store: new FakeTaskLedgerStore() }),
-      TASK_UPDATE_TOOL_NAME,
-    );
-    const params = update.parameters as z.ZodType;
-    const reject = [
-      'a<task-ledger/>b',
-      'abc\ndef',
-      'a b',
-      'X'.repeat(5000),
-      '',
-      'ghp_abcdefghijklmnopqrstuvwxyz',
-      'sk-abcdefghi',
-      'a'.repeat(40),
-    ];
-    for (const id of reject) {
-      assert.equal(
-        params.safeParse({ id, status: 'completed', completionEvidence: 'done' }).success,
-        false,
-        `id ${JSON.stringify(id)} must be rejected`,
-      );
-    }
-    const accept = ['123e4567-e89b-12d3-a456-426614174000', 'good-id_1:2'];
-    for (const id of accept) {
-      assert.equal(
-        params.safeParse({ id, status: 'completed', completionEvidence: 'done' }).success,
-        true,
-        `id ${id} must pass`,
-      );
-    }
-  });
-
-  test('task_create result shows only the created tasks (with ids) and total, not the pre-existing ledger', async () => {
-    const store = new FakeTaskLedgerStore();
-    const tools = buildTaskLedgerTools({ store });
-    const create = findTool(tools, TASK_CREATE_TOOL_NAME);
-    // a pre-existing task that must NOT be replayed in the create result
-    await create.impl({ tasks: [{ subject: 'pre-existing' }] }, fakeContext(SESSION_ID));
-    const result = String(
-      await create.impl({ tasks: [{ subject: 'new-task' }] }, fakeContext(SESSION_ID)),
-    );
-    assert.match(result, /new-task/, 'result must include the created task');
-    assert.match(result, /ledger total: 2/, 'result must include the ledger total');
-    assert.equal(
-      result.includes('pre-existing'),
-      false,
-      'result must not replay the pre-existing ledger',
-    );
-    // the new task's id is present so the model can update it next
-    const all = await store.list(SESSION_ID);
-    const newId = all.find((t) => t.subject === 'new-task')?.id;
-    assert.ok(newId, 'new task must have been created');
-    assert.equal(result.includes(newId), true, 'result must include the new task id');
-  });
-
-  test('task_update result shows only the updated task and total, not the rest of the ledger', async () => {
-    const store = new FakeTaskLedgerStore();
-    const tools = buildTaskLedgerTools({ store });
-    const create = findTool(tools, TASK_CREATE_TOOL_NAME);
-    const update = findTool(tools, TASK_UPDATE_TOOL_NAME);
-    await create.impl(
-      { tasks: [{ subject: 'keep-1' }, { subject: 'keep-2' }, { subject: 'target' }] },
-      fakeContext(SESSION_ID),
-    );
-    const all = await store.list(SESSION_ID);
-    const target = all.find((t) => t.subject === 'target');
-    assert.ok(target);
-    const result = String(
-      await update.impl(
-        { id: target.id, status: 'completed', completionEvidence: 'verified done' },
-        fakeContext(SESSION_ID),
-      ),
-    );
-    assert.match(result, /target/, 'result must include the updated task subject');
-    assert.match(result, /ledger total: 3/, 'result must include the ledger total');
-    assert.equal(result.includes('keep-1'), false, 'result must not replay unrelated tasks');
-    assert.equal(result.includes('keep-2'), false, 'result must not replay unrelated tasks');
-  });
-
-  test('tool results scrub secret-like subjects before they persist into history', async () => {
-    // Same samples the core redactSecrets tests use. Tool results replay to
-    // the provider, so redaction must happen before they persist into history.
-    const store = new FakeTaskLedgerStore();
-    const tools = buildTaskLedgerTools({ store });
-    const create = findTool(tools, TASK_CREATE_TOOL_NAME);
-    const update = findTool(tools, TASK_UPDATE_TOOL_NAME);
-
-    const createResult = String(
-      await create.impl(
-        { tasks: [{ subject: '轮换 Bearer sk-live-secret-token-value' }] },
-        fakeContext(SESSION_ID),
-      ),
-    );
-    assert.equal(createResult.includes('sk-live-secret-token-value'), false);
-    assert.match(createResult, /\[redacted\]/);
-
-    const updateResult = String(
-      await update.impl(
-        { id: 'id-0', subject: '换 ghp_abcdefghijklmnopqrstuvwxyz' },
-        fakeContext(SESSION_ID),
-      ),
-    );
-    assert.equal(updateResult.includes('ghp_abcdefghijklmnopqrstuvwxyz'), false);
-  });
-
-  test('tool results strip <task-ledger> tag variants so a subject cannot smuggle envelope tags into history', async () => {
-    const store = new FakeTaskLedgerStore();
-    const create = findTool(buildTaskLedgerTools({ store }), TASK_CREATE_TOOL_NAME);
-    const variants = [
-      '</task-ledger>',
-      '</task-ledger >',
-      '<task-ledger x="1">',
-      '</task-ledger\t>',
-      '<task-ledger/>',
-      '<task-ledger>',
-    ];
-    const drafts = variants.map((v) => ({ subject: '正常 ' + v + ' 假指令' }));
-    const result = String(await create.impl({ tasks: drafts }, fakeContext(SESSION_ID)));
-    assert.equal(
-      (result.match(/<\/?task-ledger[^>]*>/gi) || []).length,
-      0,
-      'tool result must not contain any task-ledger tag variant, got: ' + JSON.stringify(result),
-    );
-  });
-
-  test('task_create schema enforces non-empty array, non-blank subjects, and the subject length cap', () => {
-    const create = findTool(
-      buildTaskLedgerTools({ store: new FakeTaskLedgerStore() }),
-      TASK_CREATE_TOOL_NAME,
-    );
-    const schema = create.parameters as z.ZodTypeAny;
-    assert.equal(schema.safeParse({ tasks: [{ subject: 'ok' }] }).success, true);
-    assert.equal(schema.safeParse({ tasks: [] }).success, false);
-    assert.equal(schema.safeParse({ tasks: [{ subject: '' }] }).success, false);
-    assert.equal(schema.safeParse({ tasks: [{ subject: '   ' }] }).success, false);
-    assert.equal(
-      schema.safeParse({ tasks: [{ subject: 'x'.repeat(TASK_SUBJECT_MAX_CHARS) }] }).success,
-      true,
-    );
-    assert.equal(
-      schema.safeParse({ tasks: [{ subject: 'x'.repeat(TASK_SUBJECT_MAX_CHARS + 1) }] }).success,
-      false,
-    );
-    assert.equal(schema.safeParse({}).success, false);
-    assert.equal(
-      schema.safeParse({ tasks: [{ subject: 'child', parent_id: 'T1' }] }).success,
-      true,
-    );
-    assert.equal(
-      schema.safeParse({ tasks: [{ subject: 'child', parent_id: '../T1' }] }).success,
-      false,
-    );
-  });
-
-  test('task_create forwards parent_id as the storage parent reference', async () => {
-    const store = new FakeTaskLedgerStore();
-    const create = findTool(buildTaskLedgerTools({ store }), TASK_CREATE_TOOL_NAME);
-    await create.impl({ tasks: [{ subject: 'child', parent_id: 'T1' }] }, fakeContext(SESSION_ID));
-    assert.deepEqual(store.createCalls[0]?.drafts, [{ subject: 'child', parentId: 'T1' }]);
-  });
-
-  test('task_update schema requires id and at least one of status/subject, with the same subject cap', () => {
-    const update = findTool(
-      buildTaskLedgerTools({ store: new FakeTaskLedgerStore() }),
-      TASK_UPDATE_TOOL_NAME,
-    );
-    const schema = update.parameters as z.ZodTypeAny;
-    assert.equal(
-      schema.safeParse({ id: 'x', status: 'completed', completionEvidence: 'done' }).success,
-      true,
-    );
-    assert.equal(schema.safeParse({ id: 'x', subject: 'new' }).success, true);
-    assert.equal(
-      schema.safeParse({ id: 'x', status: 'blocked', blockedReason: 'waiting' }).success,
-      true,
-    );
-    assert.equal(
-      schema.safeParse({ id: 'x', status: 'failed', failureReason: 'cannot proceed' }).success,
-      true,
-    );
-    assert.equal(
-      schema.safeParse({ id: 'x', status: 'in_progress', explicitReopen: true }).success,
-      true,
-    );
-    assert.equal(schema.safeParse({ id: 'x' }).success, false);
-    assert.equal(
-      schema.safeParse({ status: 'completed', completionEvidence: 'done' }).success,
-      false,
-    );
-    assert.equal(schema.safeParse({ id: 'x', status: 'bogus' }).success, false);
-    assert.equal(schema.safeParse({ id: 'x', status: 'completed' }).success, false);
-    assert.equal(schema.safeParse({ id: 'x', status: 'blocked' }).success, false);
-    assert.equal(schema.safeParse({ id: 'x', status: 'failed' }).success, false);
-    assert.equal(
-      schema.safeParse({ id: 'x', subject: 'x'.repeat(TASK_SUBJECT_MAX_CHARS + 1) }).success,
-      false,
-    );
-    assert.equal(
-      schema.safeParse({ id: 'x', completionEvidence: 'x'.repeat(TASK_EVIDENCE_MAX_CHARS + 1) })
-        .success,
-      false,
-    );
-  });
-
-  test('task_update forwards evidence fields to the store', async () => {
-    const store = new FakeTaskLedgerStore();
-    const tools = buildTaskLedgerTools({ store });
-    const create = findTool(tools, TASK_CREATE_TOOL_NAME);
-    const update = findTool(tools, TASK_UPDATE_TOOL_NAME);
-    await create.impl({ tasks: [{ subject: '原始' }] }, fakeContext(SESSION_ID));
-
-    const result = await update.impl(
-      {
-        id: 'id-0',
-        status: 'blocked',
-        blockedReason: 'waiting for approval',
-      },
-      fakeContext(SESSION_ID, 'run-2'),
-    );
-    assert.deepEqual(store.updateCalls.at(-1)?.patch, {
-      status: 'blocked',
-      blockedReason: 'waiting for approval',
-    });
-    assert.deepEqual(store.updateCalls.at(-1)?.context, {
-      turnId: 'turn-1',
-      runId: 'run-2',
-      toolCallId: 'call-1',
-      source: 'tool',
-      actor: 'main_agent',
-    });
-    assert.match(String(result), /blockedReason/);
-  });
-
-  test('task_update forwards explicitReopen without requiring evidence', async () => {
-    const store = new FakeTaskLedgerStore();
-    const tools = buildTaskLedgerTools({ store });
-    const create = findTool(tools, TASK_CREATE_TOOL_NAME);
-    const update = findTool(tools, TASK_UPDATE_TOOL_NAME);
-    await create.impl({ tasks: [{ subject: 'reopen' }] }, fakeContext(SESSION_ID));
-
-    await update.impl(
-      {
-        id: 'id-0',
-        status: 'in_progress',
-        explicitReopen: true,
-      },
-      fakeContext(SESSION_ID, 'run-3'),
-    );
-
-    assert.deepEqual(store.updateCalls.at(-1)?.patch, {
-      status: 'in_progress',
-      explicitReopen: true,
-    });
-    assert.deepEqual(store.updateCalls.at(-1)?.context, {
-      runId: 'run-3',
-      turnId: 'turn-1',
-      toolCallId: 'call-1',
-      source: 'tool',
-      actor: 'main_agent',
-    });
-  });
-
-  test('task_list and task_get return compact task summaries', async () => {
-    const store = new FakeTaskLedgerStore();
-    const tools = buildTaskLedgerTools({ store });
-    const create = findTool(tools, TASK_CREATE_TOOL_NAME);
-    const update = findTool(tools, TASK_UPDATE_TOOL_NAME);
-    const list = findTool(tools, TASK_LIST_TOOL_NAME);
-    const get = findTool(tools, TASK_GET_TOOL_NAME);
-    await create.impl(
-      { tasks: [{ subject: 'first' }, { subject: 'second' }] },
-      fakeContext(SESSION_ID),
-    );
-    await update.impl({ id: 'id-1', status: 'in_progress' }, fakeContext(SESSION_ID));
-
-    const listResult = String(await list.impl({}, fakeContext(SESSION_ID)));
-    assert.match(listResult, /Task ledger total: 2/);
-    assert.match(listResult, /first/);
-    assert.match(listResult, /second/);
-    assert.equal(listResult.includes('resumeTrust='), false);
-
-    const getResult = String(await get.impl({ id: 'id-1' }, fakeContext(SESSION_ID)));
-    assert.match(getResult, /second/);
-    assert.equal(getResult.includes('first'), false);
-    assert.equal(getResult.includes('resumeTrust='), false);
-    assert.equal(
-      await get.impl({ id: 'missing' }, fakeContext(SESSION_ID)),
-      'No such task: missing',
-    );
-  });
-
-  test('task_list forwards filters and rejects contradictory terminal options', async () => {
-    const store = new FakeTaskLedgerStore();
-    store.seed([
-      { id: 'a', key: 'T1', subject: 'active', status: 'pending', createdAt: 1, updatedAt: 1 },
-      {
-        id: 'b',
-        key: 'T2',
-        subject: 'done',
-        status: 'completed',
-        completionEvidence: 'ok',
-        createdAt: 2,
-        updatedAt: 3,
-      },
-    ]);
-    const list = findTool(buildTaskLedgerTools({ store }), TASK_LIST_TOOL_NAME);
-    const schema = list.parameters as z.ZodTypeAny;
-    assert.equal(schema.safeParse({ status: 'completed', include_terminal: false }).success, false);
-    const result = String(
-      await list.impl(
-        {
-          status: 'pending',
-          include_terminal: false,
-          include_archived: false,
-        },
-        fakeContext(SESSION_ID),
-      ),
-    );
-    assert.match(result, /active/);
-    assert.doesNotMatch(result, /done/);
-    assert.deepEqual(store.listCalls.at(-1)?.options, {
-      status: 'pending',
-      includeTerminal: false,
-      includeArchived: false,
-    });
-  });
-
-  test('task_list and task_get hide untrusted fallback tasks from model-visible output', async () => {
-    const store = new FakeTaskLedgerStore();
-    store.seed([
-      {
-        id: 'safe-task',
-        key: 'T1',
-        subject: 'visible task',
-        status: 'pending',
-        createdAt: 1,
-        updatedAt: 1,
-      },
-      {
-        id: 'fallback-task',
-        key: 'T2',
-        subject: 'corrupt cache fallback',
-        status: 'pending',
-        createdAt: 2,
-        updatedAt: 2,
-        resumeTrust: 'untrusted',
-      },
-    ]);
-    const tools = buildTaskLedgerTools({ store });
-    const list = findTool(tools, TASK_LIST_TOOL_NAME);
-    const get = findTool(tools, TASK_GET_TOOL_NAME);
-
-    const listResult = String(await list.impl({}, fakeContext(SESSION_ID)));
-    assert.match(listResult, /visible task/);
-    assert.equal(listResult.includes('corrupt cache fallback'), false);
-    assert.equal(listResult.includes('fallback-task'), false);
-    assert.equal(listResult.includes('resumeTrust='), false);
-
-    assert.equal(
-      await get.impl({ id: 'fallback-task' }, fakeContext(SESSION_ID)),
-      'No such task: fallback-task',
-    );
-  });
-});
diff --git a/packages/runtime/src/deep-research-tools.ts b/packages/runtime/src/deep-research-tools.ts
index 7d06374c27..2a8c5a4883 100644
--- a/packages/runtime/src/deep-research-tools.ts
+++ b/packages/runtime/src/deep-research-tools.ts
@@ -648,7 +648,6 @@ function buildCheckpointTool(deps: BuildDeepResearchToolsDeps): MakaTool<
     summary: string;
     open_questions?: string[];
     next_steps?: string[];
-    task_ids?: string[];
     artifact_ids?: string[];
   },
   string
@@ -662,7 +661,7 @@ function buildCheckpointTool(deps: BuildDeepResearchToolsDeps): MakaTool<
     displayName: 'Checkpoint Research',
     description:
       'Record a durable research checkpoint after a meaningful round or before context compaction. ' +
-      'Include unresolved questions, next steps, task ids, and the artifacts needed to resume.',
+      'Include unresolved questions, next steps, and the artifacts needed to resume.',
     parameters: z.object({
       round: z.number().int().min(1).describe('Monotonic research round number.'),
       stage: z.enum(DEEP_RESEARCH_ACTIVE_STAGES).describe('Current two-stage workflow phase.'),
@@ -679,7 +678,6 @@ function buildCheckpointTool(deps: BuildDeepResearchToolsDeps): MakaTool<
         .optional()
         .describe('Questions still requiring evidence or resolution.'),
       next_steps: itemArray.optional().describe('Concrete continuation steps.'),
-      task_ids: refArray.optional().describe('Related ids from the session Task Ledger.'),
       artifact_ids: refArray.optional().describe('Known research artifact ids required to resume.'),
     }),
     impl: async (input, ctx) => {
@@ -693,7 +691,7 @@ function buildCheckpointTool(deps: BuildDeepResearchToolsDeps): MakaTool<
           summary: input.summary,
           openQuestions: dedupe(input.open_questions ?? []),
           nextSteps: dedupe(input.next_steps ?? []),
-          taskIds: dedupe(input.task_ids ?? []),
+          taskIds: [],
           artifactIds: dedupe(input.artifact_ids ?? []),
         },
         mutationContext(ctx),
diff --git a/packages/runtime/src/session-todo-tools.ts b/packages/runtime/src/session-todo-tools.ts
new file mode 100644
index 0000000000..14cac0be95
--- /dev/null
+++ b/packages/runtime/src/session-todo-tools.ts
@@ -0,0 +1,102 @@
+/*
+ * 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 {
+  SESSION_TODO_CONTENT_MAX_CHARS,
+  SESSION_TODO_MAX_ITEMS,
+  SESSION_TODO_STATUSES,
+  sessionTodoContentForDisplay,
+  type SessionTodoSnapshot,
+} from '@maka/core/session-todo';
+import { z } from 'zod';
+import type { MakaTool } from './tool-runtime.js';
+
+export const TODO_READ_TOOL_NAME = 'todo_read';
+export const TODO_WRITE_TOOL_NAME = 'todo_write';
+
+export interface SessionTodoToolStore {
+  read(sessionId: string): Promise<SessionTodoSnapshot>;
+  replace(sessionId: string, items: unknown): Promise<SessionTodoSnapshot>;
+}
+
+export function buildSessionTodoTools(store: SessionTodoToolStore): MakaTool[] {
+  return [buildTodoReadTool(store), buildTodoWriteTool(store)];
+}
+
+function buildTodoReadTool(store: SessionTodoToolStore): MakaTool<Record<string, never>, string> {
+  return {
+    name: TODO_READ_TOOL_NAME,
+    displayName: 'Todo Read',
+    description:
+      'Read the complete current session Todo list. Use this when you need the latest checklist.',
+    parameters: z.object({}).strict(),
+    impl: async (_input, ctx) => renderTodoSnapshot(await store.read(ctx.sessionId), 'read'),
+  };
+}
+
+function buildTodoWriteTool(
+  store: SessionTodoToolStore,
+): MakaTool<
+  { todos: Array<{ content: string; status: (typeof SESSION_TODO_STATUSES)[number] }> },
+  string
+> {
+  return {
+    name: TODO_WRITE_TOOL_NAME,
+    displayName: 'Todo Write',
+    description:
+      'Atomically replace the complete current session Todo list. Include every item that should remain. ' +
+      'Completed means model-reported progress; it is not independently verified execution evidence.',
+    parameters: z
+      .object({
+        todos: z
+          .array(
+            z
+              .object({
+                content: z
+                  .string()
+                  .trim()
+                  .min(1)
+                  .refine(
+                    (content) =>
+                      [...content.normalize('NFC')].length <= SESSION_TODO_CONTENT_MAX_CHARS,
+                    `Todo content must be ${SESSION_TODO_CONTENT_MAX_CHARS} characters or fewer`,
+                  ),
+                status: z.enum(SESSION_TODO_STATUSES),
+              })
+              .strict(),
+          )
+          .max(SESSION_TODO_MAX_ITEMS),
+      })
+      .strict(),
+    impl: async (input, ctx) =>
+      renderTodoSnapshot(await store.replace(ctx.sessionId, input.todos), 'write'),
+  };
+}
+
+function renderTodoSnapshot(snapshot: SessionTodoSnapshot, operation: 'read' | 'write'): string {
+  if (snapshot.items.length === 0) {
+    return operation === 'write' ? 'Todo list cleared.' : 'Todo list is empty.';
+  }
+  const lines = snapshot.items.map(
+    (item, index) =>
+      `${index + 1}. [${item.status}] ${JSON.stringify(sessionTodoContentForDisplay(item.content))}`,
+  );
+  const prefix = operation === 'write' ? 'Todo list updated' : 'Todo list';
+  return `${prefix} (${snapshot.items.length} items):\n${lines.join('\n')}`;
+}
diff --git a/packages/runtime/src/subagent-tools.ts b/packages/runtime/src/subagent-tools.ts
index 0df610cacd..19a479c17d 100644
--- a/packages/runtime/src/subagent-tools.ts
+++ b/packages/runtime/src/subagent-tools.ts
@@ -18,7 +18,6 @@
  */
 
 import { z } from 'zod';
-import { TASK_ID_MAX_CHARS, isSafeTaskId, type TaskLedgerStore } from '@maka/core/task-ledger';
 import { decodeCanonicalToolResultContent } from '@maka/core/tool-result-record-schema';
 import { isSafeSubagentPresetId } from '@maka/core/subagent-settings';
 import { type ToolResultContent } from '@maka/core/events';
@@ -89,7 +88,7 @@ export function buildChildAgentTools(tools: readonly MakaTool[]): MakaTool[] {
 }
 
 export function buildSubagentSpawnTool(
-  deps: { taskLedger?: TaskLedgerStore; definitions?: readonly AgentDefinition[] } = {},
+  deps: { definitions?: readonly AgentDefinition[] } = {},
 ): MakaTool<
   {
     profile?: string;
@@ -97,7 +96,6 @@ export function buildSubagentSpawnTool(
     task: string;
     write_back?: string;
     isolation?: string;
-    task_id?: string;
   },
   unknown
 > {
@@ -109,7 +107,7 @@ export function buildSubagentSpawnTool(
     description:
       'Run one bounded foreground child task. Prefer agent_list, then select the user-approved subagent_id whose description fits the task; profile is retained for legacy callers. If both selectors are present, subagent_id wins and profile is ignored.',
     parameters: z.preprocess(
-      (input) => cleanSubagentSpawnInput(input, deps.taskLedger !== undefined),
+      cleanSubagentSpawnInput,
       z
         .object({
           profile: z.enum(profiles).optional().describe('Legacy child capability profile.'),
@@ -137,17 +135,6 @@ export function buildSubagentSpawnTool(
             .describe(
               'Requested child workspace isolation. Worktree profiles fail closed until a worktree child executor is available.',
             ),
-          ...(deps.taskLedger
-            ? {
-                task_id: z
-                  .string()
-                  .min(1)
-                  .max(TASK_ID_MAX_CHARS)
-                  .refine(isSafeTaskId)
-                  .optional()
-                  .describe('Existing task UUID or short key to bind to this child run.'),
-              }
-            : {}),
         })
         .strip()
         .superRefine((input, ctx) => {
@@ -208,20 +195,6 @@ export function buildSubagentSpawnTool(
           },
         );
       }
-      // task_id is meaningful only in compositions that advertise task
-      // binding. Older or over-eager callers may still send it elsewhere;
-      // ignore it instead of turning an optional integration into a refusal.
-      const taskId = deps.taskLedger ? input.task_id : undefined;
-      const boundTask = taskId ? await deps.taskLedger!.get(ctx.sessionId, taskId) : undefined;
-      if (taskId && !boundTask) throw new Error(`No such task in this session: ${taskId}`);
-      let claimedOwner:
-        | {
-            actor: 'child_agent';
-            sessionId: string;
-            agentId: string;
-            turnId: string;
-          }
-        | undefined;
       let result: Omit<SubagentToolResult, 'kind'>;
       const progress = new ChildAgentProgressProjector(ctx);
       ctx.emitOutput('stdout', `Starting child agent: ${definition.name}\n`);
@@ -231,27 +204,6 @@ export function buildSubagentSpawnTool(
             agentProfile: definition.profile,
             ...(input.subagent_id ? { subagentId: input.subagent_id } : {}),
             prompt: input.task,
-            ...(boundTask
-              ? {
-                  onReady: async ({ childSessionId, turnId, agentId }) => {
-                    const owner = {
-                      actor: 'child_agent' as const,
-                      sessionId: childSessionId,
-                      agentId,
-                      turnId,
-                    };
-                    await deps.taskLedger!.claim(ctx.sessionId, boundTask.id, owner, {
-                      runId: ctx.runId,
-                      turnId: ctx.turnId,
-                      toolCallId: ctx.toolCallId,
-                      source: 'system',
-                      actor: 'main_agent',
-                      reason: `assigned to child agent ${agentId}`,
-                    });
-                    claimedOwner = owner;
-                  },
-                }
-              : {}),
             onEvent: (event) => progress.observe(event),
           }),
         );
@@ -260,52 +212,9 @@ export function buildSubagentSpawnTool(
           'stderr',
           `Child agent ${definition.name} failed: ${boundedChildError(error)}\n`,
         );
-        if (boundTask && claimedOwner) {
-          await deps.taskLedger!.settleAgentOutcome(
-            ctx.sessionId,
-            boundTask.id,
-            {
-              status: 'failed',
-              owner: claimedOwner,
-              reason:
-                error instanceof Error
-                  ? error.message
-                  : 'Child agent failed before returning a result',
-            },
-            {
-              turnId: claimedOwner.turnId,
-              toolCallId: ctx.toolCallId,
-              source: 'system',
-              actor: 'child_agent',
-            },
-          );
-        }
         throw error;
       }
       ctx.emitOutput('stdout', `Child agent ${definition.name}: ${result.status}\n`);
-      if (boundTask && claimedOwner) {
-        const owner = {
-          ...claimedOwner,
-          ...(result.runId ? { runId: result.runId } : {}),
-          turnId: result.turnId,
-        };
-        await deps.taskLedger!.settleAgentOutcome(
-          ctx.sessionId,
-          boundTask.id,
-          {
-            status: result.status,
-            owner,
-            reason: result.failureClass ?? result.summary,
-          },
-          {
-            runId: result.runId,
-            turnId: result.turnId,
-            toolCallId: ctx.toolCallId,
-            source: 'system',
-            actor: 'child_agent',
-          },
-        );
-      }
       return {
         kind: 'subagent',
         ...result,
@@ -314,11 +223,10 @@ export function buildSubagentSpawnTool(
   };
 }
 
-function cleanSubagentSpawnInput(input: unknown, taskBindingAvailable: boolean): unknown {
+function cleanSubagentSpawnInput(input: unknown): unknown {
   if (!input || typeof input !== 'object' || Array.isArray(input)) return input;
   const cleaned = { ...(input as Record<string, unknown>) };
   if (cleaned.subagent_id !== undefined) delete cleaned.profile;
-  if (!taskBindingAvailable) delete cleaned.task_id;
   return cleaned;
 }
 
@@ -753,7 +661,7 @@ export function buildSubagentProjectionTools(): MakaTool[] {
 }
 
 export function buildParentAgentTools(
-  deps: { taskLedger?: TaskLedgerStore; definitions?: readonly AgentDefinition[] } = {},
+  deps: { definitions?: readonly AgentDefinition[] } = {},
 ): MakaTool[] {
   const definitions = deps.definitions ?? BUILTIN_AGENT_DEFINITIONS;
   return [
diff --git a/packages/runtime/src/task-ledger-tools.ts b/packages/runtime/src/task-ledger-tools.ts
deleted file mode 100644
index 36c88b92a9..0000000000
--- a/packages/runtime/src/task-ledger-tools.ts
+++ /dev/null
@@ -1,323 +0,0 @@
-/*
- * 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 { z } from 'zod';
-import {
-  TASK_STATUSES,
-  TASK_EVIDENCE_MAX_CHARS,
-  TASK_SUBJECT_MAX_CHARS,
-  TASK_LEDGER_MAX_TASKS,
-  TASK_ID_MAX_CHARS,
-  filterModelVisibleTaskLedgerTasks,
-  isSafeTaskId,
-  isTerminalTaskStatus,
-  renderSafeTaskLedgerText,
-  type TaskLedgerStore,
-} from '@maka/core/task-ledger';
-import type { MakaTool } from './tool-runtime.js';
-
-export const TASK_CREATE_TOOL_NAME = 'task_create';
-export const TASK_UPDATE_TOOL_NAME = 'task_update';
-export const TASK_LIST_TOOL_NAME = 'task_list';
-export const TASK_GET_TOOL_NAME = 'task_get';
-
-export function buildTaskLedgerTools(deps: { store: TaskLedgerStore }): MakaTool[] {
-  return [
-    buildTaskCreateTool(deps.store, TASK_CREATE_TOOL_NAME, 'task_update'),
-    buildTaskUpdateTool(deps.store, TASK_UPDATE_TOOL_NAME),
-    buildTaskListTool(deps.store),
-    buildTaskGetTool(deps.store),
-  ];
-}
-
-function buildTaskCreateTool(
-  store: TaskLedgerStore,
-  name: string,
-  updateToolName: string,
-): MakaTool<{ tasks: Array<{ subject: string; parent_id?: string }> }, string> {
-  return {
-    name,
-    displayName: 'Task Create',
-    description:
-      'Add one or more tasks to the session task ledger. ' +
-      `Use task_list or task_get to read it later, and update status with ${updateToolName} as you progress.`,
-    parameters: z.object({
-      tasks: z
-        .array(
-          z.object({
-            subject: z
-              .string()
-              .trim()
-              .min(1)
-              .max(TASK_SUBJECT_MAX_CHARS)
-              .describe(
-                `Short imperative description of the task (max ${TASK_SUBJECT_MAX_CHARS} characters).`,
-              ),
-            parent_id: z
-              .string()
-              .min(1)
-              .max(TASK_ID_MAX_CHARS)
-              .refine(isSafeTaskId)
-              .optional()
-              .describe('Existing parent task UUID or short key (for example T1).'),
-          }),
-        )
-        .min(1)
-        .max(TASK_LEDGER_MAX_TASKS)
-        .describe('One or more tasks to add. Each starts in the pending state.'),
-    }),
-    impl: async (input, ctx) => {
-      const { created, total } = await store.create(
-        ctx.sessionId,
-        input.tasks.map((task) => ({
-          subject: task.subject,
-          ...(task.parent_id ? { parentId: task.parent_id } : {}),
-        })),
-        {
-          runId: ctx.runId,
-          turnId: ctx.turnId,
-          toolCallId: ctx.toolCallId,
-          source: 'tool',
-          actor: 'main_agent',
-        },
-      );
-      return `Created ${created.length} task(s); ledger total: ${total}.\n${renderSafeTaskLedgerText(created)}`;
-    },
-  };
-}
-
-function buildTaskUpdateTool(
-  store: TaskLedgerStore,
-  name: string,
-): MakaTool<
-  {
-    id: string;
-    status?: (typeof TASK_STATUSES)[number];
-    subject?: string;
-    blockedReason?: string;
-    failureReason?: string;
-    completionEvidence?: string;
-    explicitReopen?: boolean;
-  },
-  string
-> {
-  return {
-    name,
-    displayName: 'Task Update',
-    description:
-      'Update a task in the session task ledger by id. Mark tasks in_progress when you start them; ' +
-      'blocked, failed, and completed updates require a reason or evidence field. ' +
-      'Reopening completed/cancelled tasks requires explicitReopen=true.',
-    parameters: z
-      .object({
-        id: z
-          .string()
-          .min(1)
-          .max(TASK_ID_MAX_CHARS)
-          .refine(
-            isSafeTaskId,
-            'Task reference must be a UUID or short key from the current ledger.',
-          )
-          .describe('Task UUID or short key.'),
-        status: z.enum(TASK_STATUSES).optional().describe('New task status.'),
-        subject: z
-          .string()
-          .trim()
-          .min(1)
-          .max(TASK_SUBJECT_MAX_CHARS)
-          .optional()
-          .describe(`Revised task description (max ${TASK_SUBJECT_MAX_CHARS} characters).`),
-        blockedReason: z
-          .string()
-          .trim()
-          .min(1)
-          .max(TASK_EVIDENCE_MAX_CHARS)
-          .optional()
-          .describe(
-            'Required when setting status to blocked. Explain the external input, dependency, or permission needed.',
-          ),
-        failureReason: z
-          .string()
-          .trim()
-          .min(1)
-          .max(TASK_EVIDENCE_MAX_CHARS)
-          .optional()
-          .describe(
-            'Required when setting status to failed. Explain why the task cannot be completed.',
-          ),
-        completionEvidence: z
-          .string()
-          .trim()
-          .min(1)
-          .max(TASK_EVIDENCE_MAX_CHARS)
-          .optional()
-          .describe(
-            'Required when setting status to completed. Cite the check, tool result, artifact, or user confirmation.',
-          ),
-        explicitReopen: z
-          .boolean()
-          .optional()
-          .describe(
-            'Required only when reopening completed -> in_progress or cancelled -> pending.',
-          ),
-      })
-      .superRefine((input, ctx) => {
-        if (
-          input.status === undefined &&
-          input.subject === undefined &&
-          input.blockedReason === undefined &&
-          input.failureReason === undefined &&
-          input.completionEvidence === undefined &&
-          input.explicitReopen === undefined
-        ) {
-          ctx.addIssue({
-            code: z.ZodIssueCode.custom,
-            message: 'Provide at least one task field to update.',
-          });
-        }
-        if (input.status === 'blocked' && input.blockedReason === undefined) {
-          ctx.addIssue({
-            code: z.ZodIssueCode.custom,
-            message: 'blockedReason is required when status is blocked.',
-            path: ['blockedReason'],
-          });
-        }
-        if (input.status === 'failed' && input.failureReason === undefined) {
-          ctx.addIssue({
-            code: z.ZodIssueCode.custom,
-            message: 'failureReason is required when status is failed.',
-            path: ['failureReason'],
-          });
-        }
-        if (input.status === 'completed' && input.completionEvidence === undefined) {
-          ctx.addIssue({
-            code: z.ZodIssueCode.custom,
-            message: 'completionEvidence is required when status is completed.',
-            path: ['completionEvidence'],
-          });
-        }
-      }),
-    impl: async (input, ctx) => {
-      const { updated, total } = await store.update(
-        ctx.sessionId,
-        input.id,
-        {
-          ...(input.status !== undefined ? { status: input.status } : {}),
-          ...(input.subject !== undefined ? { subject: input.subject } : {}),
-          ...(input.blockedReason !== undefined ? { blockedReason: input.blockedReason } : {}),
-          ...(input.failureReason !== undefined ? { failureReason: input.failureReason } : {}),
-          ...(input.completionEvidence !== undefined
-            ? { completionEvidence: input.completionEvidence }
-            : {}),
-          ...(input.explicitReopen !== undefined ? { explicitReopen: input.explicitReopen } : {}),
-        },
-        {
-          runId: ctx.runId,
-          turnId: ctx.turnId,
-          toolCallId: ctx.toolCallId,
-          source: 'tool',
-          actor: 'main_agent',
-        },
-      );
-      return `Updated 1 task; ledger total: ${total}.\n${renderSafeTaskLedgerText([updated])}`;
-    },
-  };
-}
-
-function buildTaskListTool(store: TaskLedgerStore): MakaTool<
-  {
-    status?: (typeof TASK_STATUSES)[number];
-    include_terminal?: boolean;
-    include_archived?: boolean;
-  },
-  string
-> {
-  return {
-    name: TASK_LIST_TOOL_NAME,
-    displayName: 'Task List',
-    description: 'List the current session task ledger in compact form.',
-    parameters: z
-      .object({
-        status: z.enum(TASK_STATUSES).optional().describe('Optional exact status filter.'),
-        include_terminal: z
-          .boolean()
-          .optional()
-          .describe('Include terminal tasks. Defaults to true for compatibility.'),
-        include_archived: z
-          .boolean()
-          .optional()
-          .describe(
-            'Include terminal tasks older than seven days. Defaults to true for compatibility.',
-          ),
-      })
-      .superRefine((input, ctx) => {
-        if (
-          input.status &&
-          isTerminalTaskStatus(input.status) &&
-          input.include_terminal === false
-        ) {
-          ctx.addIssue({
-            code: z.ZodIssueCode.custom,
-            path: ['include_terminal'],
-            message: 'A terminal status filter conflicts with include_terminal=false.',
-          });
-        }
-      }),
-    impl: async (input, ctx) => {
-      const tasks = filterModelVisibleTaskLedgerTasks(
-        await store.list(ctx.sessionId, {
-          ...(input.status ? { status: input.status } : {}),
-          ...(input.include_terminal !== undefined
-            ? { includeTerminal: input.include_terminal }
-            : {}),
-          ...(input.include_archived !== undefined
-            ? { includeArchived: input.include_archived }
-            : {}),
-        }),
-      );
-      return tasks.length === 0
-        ? 'Task ledger is empty.'
-        : `Task ledger total: ${tasks.length}.\n${renderSafeTaskLedgerText(tasks)}`;
-    },
-  };
-}
-
-function buildTaskGetTool(store: TaskLedgerStore): MakaTool<{ id: string }, string> {
-  return {
-    name: TASK_GET_TOOL_NAME,
-    displayName: 'Task Get',
-    description: 'Get one task from the current session task ledger by id.',
-    parameters: z.object({
-      id: z
-        .string()
-        .min(1)
-        .max(TASK_ID_MAX_CHARS)
-        .refine(
-          isSafeTaskId,
-          'Task reference must be a UUID or short key from the current ledger.',
-        ),
-    }),
-    impl: async (input, ctx) => {
-      const task = await store.get(ctx.sessionId, input.id);
-      if (task?.resumeTrust === 'untrusted') return `No such task: ${input.id}`;
-      if (!task) return `No such task: ${input.id}`;
-      return renderSafeTaskLedgerText([task]);
-    },
-  };
-}
diff --git a/packages/storage/package.json b/packages/storage/package.json
index fd6e6e0aa0..771f3d9d8d 100644
--- a/packages/storage/package.json
+++ b/packages/storage/package.json
@@ -43,6 +43,7 @@
     "./scheduled-task-store": "./dist/scheduled-task-store.js",
     "./session-bundle-policy": "./dist/session-bundle-policy.js",
     "./session-copy-cleanup": "./dist/session-copy-cleanup.js",
+    "./session-todo-authority": "./dist/session-todo-authority.js",
     "./session-store": "./dist/session-store.js",
     "./settings-store": "./dist/settings-store.js",
     "./shell-run-authority": "./dist/shell-run-authority.js",
@@ -52,7 +53,6 @@
     "./stable-storage": "./dist/stable-storage.js",
     "./state-root-composition": "./dist/state-root-composition.js",
     "./storage-writer-composition": "./dist/storage-writer-composition.js",
-    "./task-ledger-authority": "./dist/task-ledger-authority.js",
     "./usage-stores": "./dist/usage-stores.js",
     "./work-board-store": "./dist/work-board-store.js",
     "./workspace-identity": "./dist/workspace-identity.js",
diff --git a/packages/storage/src/__tests__/public-entrypoints.test.ts b/packages/storage/src/__tests__/public-entrypoints.test.ts
index bdbb85bf7c..1e07c0c858 100644
--- a/packages/storage/src/__tests__/public-entrypoints.test.ts
+++ b/packages/storage/src/__tests__/public-entrypoints.test.ts
@@ -65,11 +65,11 @@ const SQLITE_BACKED_ENTRYPOINTS = [
   './session-bundle-policy',
   './session-copy-cleanup',
   './session-store',
+  './session-todo-authority',
   './shell-run-authority',
   './shell-run-store',
   './sqlite-session-metadata-store',
   './storage-writer-composition',
-  './task-ledger-authority',
   './usage-stores',
   './work-board-store',
 ];
diff --git a/packages/storage/src/__tests__/session-todo-store.test.ts b/packages/storage/src/__tests__/session-todo-store.test.ts
index a643482453..e1ecc82fac 100644
--- a/packages/storage/src/__tests__/session-todo-store.test.ts
+++ b/packages/storage/src/__tests__/session-todo-store.test.ts
@@ -146,7 +146,7 @@ describe('SQLite SessionTodo store', () => {
           { content: 'first', status: 'pending' },
         ],
       });
-      await reopened.purge(SESSION_ID);
+      await reopened.purgeSessionState(SESSION_ID);
       assert.deepEqual(await reopened.readOrBootstrap(SESSION_ID), { items: [] });
       reopened.close();
     });
@@ -246,6 +246,150 @@ describe('SQLite SessionTodo store', () => {
       todos.close();
     });
   });
+
+  test('initializes latest copies atomically and accepts only an identical retry', async () => {
+    await withRoot(async (root) => {
+      const todos = createSqliteSessionTodoStore(root);
+      await todos.replaceAll('source', [{ content: 'current work', status: 'in_progress' }]);
+      const input = { sourceSessionId: 'source', targetSessionId: 'target', copyCurrent: true };
+      const expected = { items: [{ content: 'current work', status: 'in_progress' as const }] };
+      assert.deepEqual(await todos.initializeCopy(input), expected);
+      assert.deepEqual(await todos.initializeCopy(input), expected);
+      await todos.replaceAll('target', [{ content: 'different', status: 'pending' }]);
+      await assert.rejects(() => todos.initializeCopy(input), /different state/);
+      todos.close();
+    });
+  });
+
+  test('fails closed when a copy source or target document is corrupt', async () => {
+    await withRoot(async (root) => {
+      const todos = createSqliteSessionTodoStore(root);
+      await todos.replaceAll('source', [{ content: 'current work', status: 'pending' }]);
+      await todos.replaceAll('corrupt-target', []);
+
+      const database = new DatabaseSync(join(root, 'runtime.sqlite'));
+      try {
+        database
+          .prepare(
+            'UPDATE workflow_session_todo_documents SET record_json = ? WHERE session_id = ?',
+          )
+          .run('{not-json', 'corrupt-target');
+      } finally {
+        database.close();
+      }
+
+      await assert.rejects(
+        () =>
+          todos.initializeCopy({
+            sourceSessionId: 'source',
+            targetSessionId: 'corrupt-target',
+            copyCurrent: true,
+          }),
+        /Invalid SessionTodo document JSON/,
+      );
+
+      const corruptSource = new DatabaseSync(join(root, 'runtime.sqlite'));
+      try {
+        corruptSource
+          .prepare(
+            'UPDATE workflow_session_todo_documents SET record_json = ? WHERE session_id = ?',
+          )
+          .run('{not-json', 'source');
+      } finally {
+        corruptSource.close();
+      }
+      await assert.rejects(
+        () =>
+          todos.initializeCopy({
+            sourceSessionId: 'source',
+            targetSessionId: 'new-target',
+            copyCurrent: true,
+          }),
+        /Invalid SessionTodo document JSON/,
+      );
+
+      const verified = new DatabaseSync(join(root, 'runtime.sqlite'), { readOnly: true });
+      try {
+        assert.equal(
+          verified
+            .prepare(
+              'SELECT COUNT(*) AS count FROM workflow_session_todo_documents WHERE session_id = ?',
+            )
+            .get('new-target')!.count,
+          0,
+        );
+      } finally {
+        verified.close();
+      }
+      todos.close();
+    });
+  });
+
+  test('writes an explicit empty copy marker that later legacy events cannot revive', async () => {
+    await withRoot(async (root) => {
+      const todos = createSqliteSessionTodoStore(root);
+      assert.deepEqual(
+        await todos.initializeCopy({
+          sourceSessionId: 'source',
+          targetSessionId: 'historical-target',
+          copyCurrent: false,
+        }),
+        { items: [] },
+      );
+      const tasks = createSqliteTaskLedgerStore(root);
+      await tasks.create('historical-target', [{ subject: 'must not revive' }]);
+      tasks.close();
+      assert.deepEqual(await todos.readOrBootstrap('historical-target'), { items: [] });
+      todos.close();
+    });
+  });
+
+  test('purges Todo and legacy bootstrap rows in one lifecycle operation', async () => {
+    await withRoot(async (root) => {
+      const tasks = createSqliteTaskLedgerStore(root);
+      await tasks.create(SESSION_ID, [{ subject: 'legacy' }]);
+      tasks.close();
+      const todos = createSqliteSessionTodoStore(root);
+      await todos.replaceAll(SESSION_ID, [{ content: 'current', status: 'pending' }]);
+      await todos.purgeSessionState(SESSION_ID);
+      const database = new DatabaseSync(join(root, 'runtime.sqlite'), { readOnly: true });
+      try {
+        assert.equal(
+          database
+            .prepare(
+              'SELECT COUNT(*) AS count FROM workflow_session_todo_documents WHERE session_id = ?',
+            )
+            .get(SESSION_ID)!.count,
+          0,
+        );
+        assert.equal(
+          database
+            .prepare(
+              'SELECT COUNT(*) AS count FROM workflow_task_ledger_events WHERE session_id = ?',
+            )
+            .get(SESSION_ID)!.count,
+          0,
+        );
+      } finally {
+        database.close();
+      }
+      todos.close();
+    });
+  });
+
+  test('linearizes concurrent whole-document replacements', async () => {
+    await withRoot(async (root) => {
+      const todos = createSqliteSessionTodoStore(root);
+      const writes = Array.from({ length: 128 }, (_, index) =>
+        todos.replaceAll(SESSION_ID, [{ content: `write ${index}`, status: 'pending' }]),
+      );
+      await Promise.all(writes);
+      assert.deepEqual(await todos.readOrBootstrap(SESSION_ID), {
+        items: [{ content: 'write 127', status: 'pending' }],
+      });
+      todos.close();
+    });
+  });
 });
 
 async function withRoot(run: (root: string) => Promise<void>): Promise<void> {
diff --git a/packages/storage/src/session-todo-authority.ts b/packages/storage/src/session-todo-authority.ts
index fb0fb6b855..03b059db9f 100644
--- a/packages/storage/src/session-todo-authority.ts
+++ b/packages/storage/src/session-todo-authority.ts
@@ -117,7 +117,8 @@ function createWriterFacade(
     [writerBrand]: true,
     readOrBootstrap: (sessionId) => run(() => store.readOrBootstrap(sessionId)),
     replaceAll: (sessionId, items) => run(() => store.replaceAll(sessionId, items)),
-    purge: (sessionId) => run(() => store.purge(sessionId)),
+    initializeCopy: (input) => run(() => store.initializeCopy(input)),
+    purgeSessionState: (sessionId) => run(() => store.purgeSessionState(sessionId)),
     close: () => {
       if (closed) return;
       closed = true;
diff --git a/packages/storage/src/session-todo-store.ts b/packages/storage/src/session-todo-store.ts
index 781fe373ab..9f99deb3ec 100644
--- a/packages/storage/src/session-todo-store.ts
+++ b/packages/storage/src/session-todo-store.ts
@@ -52,7 +52,14 @@ export interface SessionTodoStore {
   readOrBootstrap(sessionId: string): Promise<SessionTodoSnapshot>;
   /** Replace the complete document without consulting legacy Task state. */
   replaceAll(sessionId: string, items: unknown): Promise<SessionTodoSnapshot>;
-  purge(sessionId: string): Promise<void>;
+  /** Initialize one conversation-copy target without overwriting conflicting state. */
+  initializeCopy(input: {
+    sourceSessionId: string;
+    targetSessionId: string;
+    copyCurrent: boolean;
+  }): Promise<SessionTodoSnapshot>;
+  /** Purge current state and the legacy events that could bootstrap it. */
+  purgeSessionState(sessionId: string): Promise<void>;
 }
 
 export interface SqliteSessionTodoStore extends SessionTodoStore {
@@ -83,7 +90,7 @@ class SqliteSessionTodoStoreImpl implements SqliteSessionTodoStore {
   async readOrBootstrap(sessionId: string): Promise<SessionTodoSnapshot> {
     assertSafeSessionId(sessionId);
     let snapshot: SessionTodoSnapshot | undefined;
-    await chainWrite(this.writeQueues, sessionId, async () => {
+    await this.#write(async () => {
       snapshot = this.#lease.transaction('write', () => {
         const existing = readStoredDocument(this.#lease.database, sessionId);
         if (existing) return snapshotFromDocument(existing);
@@ -104,7 +111,7 @@ class SqliteSessionTodoStoreImpl implements SqliteSessionTodoStore {
       schemaVersion: SESSION_TODO_DOCUMENT_SCHEMA_VERSION,
       items: normalized.value.items,
     };
-    await chainWrite(this.writeQueues, sessionId, async () => {
+    await this.#write(async () => {
       this.#lease.transaction('write', () =>
         upsertDocument(this.#lease.database, sessionId, document),
       );
@@ -112,16 +119,70 @@ class SqliteSessionTodoStoreImpl implements SqliteSessionTodoStore {
     return snapshotFromDocument(document);
   }
 
-  async purge(sessionId: string): Promise<void> {
+  async initializeCopy(input: {
+    sourceSessionId: string;
+    targetSessionId: string;
+    copyCurrent: boolean;
+  }): Promise<SessionTodoSnapshot> {
+    assertSafeSessionId(input.sourceSessionId);
+    assertSafeSessionId(input.targetSessionId);
+    if (input.sourceSessionId === input.targetSessionId) {
+      throw new Error('SessionTodo copy source and target must differ');
+    }
+    let snapshot: SessionTodoSnapshot | undefined;
+    await this.#write(async () => {
+      snapshot = this.#lease.transaction('write', () => {
+        const source = input.copyCurrent
+          ? (readStoredDocument(this.#lease.database, input.sourceSessionId) ??
+            bootstrapAndInsert(this.#lease.database, input.sourceSessionId))
+          : emptyDocument();
+        const existing = readStoredDocument(this.#lease.database, input.targetSessionId);
+        if (existing) {
+          if (!sameDocument(existing, source)) {
+            throw new Error('SessionTodo copy target already has different state');
+          }
+          return snapshotFromDocument(existing);
+        }
+        insertDocument(this.#lease.database, input.targetSessionId, source);
+        return snapshotFromDocument(source);
+      });
+    });
+    return snapshot!;
+  }
+
+  async purgeSessionState(sessionId: string): Promise<void> {
     assertSafeSessionId(sessionId);
-    await chainWrite(this.writeQueues, sessionId, async () => {
+    await this.#write(async () => {
       this.#lease.transaction('write', () => {
         this.#lease.database
           .prepare('DELETE FROM workflow_session_todo_documents WHERE session_id = ?')
           .run(sessionId);
+        this.#lease.database
+          .prepare('DELETE FROM workflow_task_ledger_events WHERE session_id = ?')
+          .run(sessionId);
       });
     });
   }
+
+  #write(operation: () => Promise<void>): Promise<void> {
+    // Todo documents are small and infrequently mutated. One queue makes
+    // cross-Session initialization linearizable without lock ordering.
+    return chainWrite(this.writeQueues, 'session-todo', operation);
+  }
+}
+
+function bootstrapAndInsert(database: DatabaseSync, sessionId: string): StoredSessionTodoDocument {
+  const document = bootstrapLegacyTasks(database, sessionId);
+  insertDocument(database, sessionId, document);
+  return document;
+}
+
+function emptyDocument(): StoredSessionTodoDocument {
+  return { schemaVersion: SESSION_TODO_DOCUMENT_SCHEMA_VERSION, items: [] };
+}
+
+function sameDocument(left: StoredSessionTodoDocument, right: StoredSessionTodoDocument): boolean {
+  return JSON.stringify(left) === JSON.stringify(right);
 }
 
 function readStoredDocument(
diff --git a/packages/storage/src/storage-writer-composition.ts b/packages/storage/src/storage-writer-composition.ts
index 9ed4ba4680..aa55029a42 100644
--- a/packages/storage/src/storage-writer-composition.ts
+++ b/packages/storage/src/storage-writer-composition.ts
@@ -31,8 +31,8 @@ import { openInteractiveProjectCatalogForWrite } from './project-catalog-authori
 import { assertStorageRootLease, type StorageRootLease } from './root-authority.js';
 import { openInteractiveRuntimePolicyStoresForWrite } from './runtime-policy-stores.js';
 import { openInteractiveScheduledTaskStoreForWrite } from './scheduled-task-store.js';
+import { openInteractiveSessionTodoStoreForWrite } from './session-todo-authority.js';
 import { openInteractiveShellRunStoreForWrite } from './shell-run-authority.js';
-import { openInteractiveTaskLedgerStoreForWrite } from './task-ledger-authority.js';
 import { openInteractiveUsageStoresForWrite } from './usage-stores.js';
 
 export interface OpenStorageWriterCompositionOptions {
@@ -55,7 +55,7 @@ export interface StorageWriterComposition {
   readonly goal: Awaited<ReturnType<typeof openInteractiveGoalAuthorityForWrite>>;
   readonly memoryBundle: Awaited<ReturnType<typeof openInteractiveMemoryBundleStoreForWrite>>;
   readonly longTermMemory: Awaited<ReturnType<typeof openInteractiveLongTermMemoryStoreForWrite>>;
-  readonly taskLedger: Awaited<ReturnType<typeof openInteractiveTaskLedgerStoreForWrite>>;
+  readonly sessionTodo: Awaited<ReturnType<typeof openInteractiveSessionTodoStoreForWrite>>;
   readonly artifacts: Awaited<ReturnType<typeof openInteractiveArtifactStoreForWrite>>;
   readonly contextOffload?: Awaited<ReturnType<typeof openInteractiveContextOffloadStoreForWrite>>;
   /** Present when the optional context-offload capability could not be opened. */
@@ -152,8 +152,8 @@ async function createComposition(
     () => openInteractiveLongTermMemoryStoreForWrite(lease),
     closeWriter,
   );
-  const taskLedger = await openWriter(
-    () => openInteractiveTaskLedgerStoreForWrite(lease),
+  const sessionTodo = await openWriter(
+    () => openInteractiveSessionTodoStoreForWrite(lease),
     closeWriter,
   );
   const artifacts = await openWriter(
@@ -192,7 +192,7 @@ async function createComposition(
     goal,
     memoryBundle,
     longTermMemory,
-    taskLedger,
+    sessionTodo,
     artifacts,
     ...(contextOffload ? { contextOffload } : {}),
     ...(contextOffloadUnavailable ? { contextOffloadUnavailable } : {}),
diff --git a/packages/ui/src/__tests__/session-todo-panel.test.tsx b/packages/ui/src/__tests__/session-todo-panel.test.tsx
new file mode 100644
index 0000000000..94ff4f71b2
--- /dev/null
+++ b/packages/ui/src/__tests__/session-todo-panel.test.tsx
@@ -0,0 +1,43 @@
+/*
+ * 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 assert from 'node:assert/strict';
+import { test } from 'node:test';
+import { renderToStaticMarkup } from 'react-dom/server';
+import { LocaleProvider } from '../locale-context.js';
+import { SessionTodoPanel, sessionTodoActiveCount } from '../session-todo-panel.js';
+
+test('renders the Host snapshot as one flat ordered list', () => {
+  const items = [
+    { content: 'First pending item', status: 'pending' as const },
+    { content: 'Second completed item', status: 'completed' as const },
+    { content: 'Third active item', status: 'in_progress' as const },
+  ];
+  assert.equal(sessionTodoActiveCount(items), 2);
+
+  const markup = renderToStaticMarkup(
+    <LocaleProvider locale="en">
+      <SessionTodoPanel items={items} />
+    </LocaleProvider>,
+  );
+  assert.ok(markup.indexOf('First pending item') < markup.indexOf('Second completed item'));
+  assert.ok(markup.indexOf('Second completed item') < markup.indexOf('Third active item'));
+  assert.equal(markup.includes('Task Create'), false);
+  assert.equal(markup.includes('T1'), false);
+});
diff --git a/packages/ui/src/__tests__/task-ledger-panel.test.ts b/packages/ui/src/__tests__/task-ledger-panel.test.ts
deleted file mode 100644
index ed2934d8e8..0000000000
--- a/packages/ui/src/__tests__/task-ledger-panel.test.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * 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 assert from 'node:assert/strict';
-import { describe, test } from 'node:test';
-import type { Task } from '@maka/core/task-ledger';
-import { deriveTaskLedgerPanelModel } from '../task-ledger-panel.js';
-
-function task(input: Partial<Task> & Pick<Task, 'id' | 'key' | 'status'>): Task {
-  return {
-    subject: input.id,
-    createdAt: 1,
-    updatedAt: 1,
-    ...input,
-  };
-}
-
-describe('task ledger panel model', () => {
-  test('keeps terminal ancestors around active descendants', () => {
-    const parent = task({ id: 'parent', key: 'T1', status: 'failed', failureReason: 'failed' });
-    const child = task({ id: 'child', key: 'T1.1', parentId: parent.id, status: 'pending' });
-    const model = deriveTaskLedgerPanelModel([parent, child]);
-    assert.equal(model.activeCount, 1);
-    assert.deepEqual(model.activeTree.map((item) => item.key), ['T1', 'T1.1']);
-  });
-
-  test('selects three recent terminal seeds and adds their ancestors without changing the count', () => {
-    const root = task({ id: 'root', key: 'T1', status: 'in_progress' });
-    const completedChild = task({
-      id: 'child', key: 'T1.1', parentId: root.id, status: 'completed',
-      completionEvidence: 'done', endedAt: 5,
-    });
-    const terminals = [2, 3, 4].map((index) => task({
-      id: `terminal-${index}`,
-      key: `T${index}`,
-      status: 'cancelled',
-      endedAt: index,
-    }));
-    const model = deriveTaskLedgerPanelModel([root, completedChild, ...terminals]);
-    assert.equal(model.recentTerminalCount, 3);
-    assert.deepEqual(model.recentTerminalTree.map((item) => item.key), ['T1', 'T1.1', 'T3', 'T4']);
-  });
-});
diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts
index cfa924716c..d9bf1e4f0d 100644
--- a/packages/ui/src/index.ts
+++ b/packages/ui/src/index.ts
@@ -57,7 +57,7 @@ export * from './user-question-prompt.js';
 export * from './user-question-prompt-state.js';
 export * from './redact.js';
 export * from './thinking-stream.js';
-export * from './task-ledger-panel.js';
+export * from './session-todo-panel.js';
 export * from './toast.js';
 export * from './tool-output-stream.js';
 export * from './ui.js';
diff --git a/packages/ui/src/session-todo-panel.tsx b/packages/ui/src/session-todo-panel.tsx
new file mode 100644
index 0000000000..5ae0fcd94c
--- /dev/null
+++ b/packages/ui/src/session-todo-panel.tsx
@@ -0,0 +1,90 @@
+/*
+ * 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 { Banner, EmptyState, IconButton, Spinner } from '@astryxdesign/core';
+import type { SessionTodoItem, SessionTodoStatus } from '@maka/core/session-todo';
+import { CheckCircle2, CircleGauge, Clock, ICON_SIZE, ListTodo, RefreshCcw } from './icons.js';
+import { useUiLocale } from './locale-context.js';
+import { getSharedUiCopy } from './shared-ui-copy.js';
+
+const STATUS_ICONS = {
+  pending: Clock,
+  in_progress: CircleGauge,
+  completed: CheckCircle2,
+} satisfies Record<SessionTodoStatus, typeof Clock>;
+
+export interface SessionTodoPanelProps {
+  items: readonly SessionTodoItem[];
+  loading?: boolean;
+  error?: string;
+  onRetry?: () => void;
+}
+
+export function sessionTodoActiveCount(items: readonly SessionTodoItem[]): number {
+  return items.filter((item) => item.status !== 'completed').length;
+}
+
+/** Read-only flat projection of the Host-owned current Todo document. */
+export function SessionTodoPanel(props: SessionTodoPanelProps) {
+  const copy = getSharedUiCopy(useUiLocale()).sessionTodo;
+  return (
+    <section className="maka-task-ledger-panel" aria-label={copy.ariaLabel}>
+      {props.error ? (
+        <Banner
+          status="error"
+          role="alert"
+          className="maka-task-ledger-message"
+          title={props.error}
+          endContent={props.onRetry ? (
+            <IconButton
+              variant="ghost"
+              size="sm"
+              className="maka-task-ledger-retry"
+              onClick={props.onRetry}
+              label={copy.retry}
+              tooltip={copy.retry}
+              icon={<RefreshCcw size={ICON_SIZE.control} aria-hidden="true" />}
+            />
+          ) : undefined}
+        />
+      ) : props.loading && props.items.length === 0 ? (
+        <Spinner size="sm" shade="subtle" label={copy.loading} className="maka-task-ledger-message" />
+      ) : props.items.length === 0 ? (
+        <EmptyState
+          isCompact
+          className="maka-task-ledger-empty"
+          icon={<ListTodo size={ICON_SIZE.empty} aria-hidden="true" />}
+          title={copy.empty}
+        />
+      ) : (
+        <ol className="maka-task-ledger-tree" aria-label={copy.activeAriaLabel}>
+          {props.items.map((item, index) => {
+            const StatusIcon = STATUS_ICONS[item.status];
+            return (
+              <li className="maka-task-ledger-row" key={`${index}:${item.status}:${item.content}`}>
+                <StatusIcon size={ICON_SIZE.control} aria-hidden="true" />
+                <span>{item.content}</span>
+              </li>
+            );
+          })}
+        </ol>
+      )}
+    </section>
+  );
+}
diff --git a/packages/ui/src/shared-ui-copy.ts b/packages/ui/src/shared-ui-copy.ts
index ff7cce7cb3..5002ac9414 100644
--- a/packages/ui/src/shared-ui-copy.ts
+++ b/packages/ui/src/shared-ui-copy.ts
@@ -17,8 +17,6 @@
  * under the License.
  */
 
-import type { TaskStatus } from '@maka/core/task-ledger';
-
 import type { UiCatalog, UiLocale } from '@maka/core/ui-locale';
 
 export interface SharedUiCopy {
@@ -97,17 +95,12 @@ export interface SharedUiCopy {
     close: string;
     resizeHandle: string;
   };
-  taskLedger: {
-    status: Record<TaskStatus, string>;
+  sessionTodo: {
     ariaLabel: string;
     retry: string;
     loading: string;
     activeAriaLabel: string;
     empty: string;
-    recent: string;
-    recentAriaLabel: string;
-    childAgent: (agentId?: string) => string;
-    mainAgent: string;
   };
   toast: {
     notifications: string;
@@ -199,17 +192,12 @@ const SHARED_UI_COPY = {
       dailyReviewDisconnectedBody: '桌面端数据桥当前未连接。',
     },
     primitives: { loading: '加载中', close: '关闭', resizeHandle: '调整宽度' },
-    taskLedger: {
-      status: { pending: '待处理', in_progress: '进行中', blocked: '已阻塞', completed: '已完成', failed: '失败', cancelled: '已取消' },
+    sessionTodo: {
       ariaLabel: '任务待办',
       retry: '重新载入待办',
       loading: '正在载入待办…',
       activeAriaLabel: '进行中的待办',
       empty: '这个任务还没有待办',
-      recent: '最近结束',
-      recentAriaLabel: '最近结束的待办',
-      childAgent: (agentId) => `子代理${agentId ? ` ${agentId}` : ''}`,
-      mainAgent: '主代理',
     },
     toast: { notifications: '通知', closeNotification: '关闭通知', confirm: '确定', cancel: '取消' },
     stream: { assistantChunkTruncated: '\n[…单条 delta 已截断]\n', assistantTailTruncated: '\n\n[…后续已截断]', thinkingHeadTruncated: '[…已截断早期 reasoning]\n', thinkingChunkTruncated: '\n[…单条 delta 已截断]\n', toolChunkTruncated: '\n[…已截断]\n' },
@@ -288,17 +276,12 @@ const SHARED_UI_COPY = {
       dailyReviewDisconnectedBody: 'The desktop data bridge is not connected.',
     },
     primitives: { loading: 'Loading', close: 'Close', resizeHandle: 'Resize handle' },
-    taskLedger: {
-      status: { pending: 'Pending', in_progress: 'In progress', blocked: 'Blocked', completed: 'Completed', failed: 'Failed', cancelled: 'Cancelled' },
+    sessionTodo: {
       ariaLabel: 'To-do list',
       retry: 'Reload the to-do list',
       loading: 'Loading the to-do list…',
       activeAriaLabel: 'In-progress to-dos',
       empty: 'This task has no to-dos yet',
-      recent: 'Recently finished',
-      recentAriaLabel: 'Recently finished to-dos',
-      childAgent: (agentId) => `Child agent${agentId ? ` ${agentId}` : ''}`,
-      mainAgent: 'Main agent',
     },
     toast: { notifications: 'Notifications', closeNotification: 'Close notification', confirm: 'Confirm', cancel: 'Cancel' },
     stream: { assistantChunkTruncated: '\n[…single delta truncated]\n', assistantTailTruncated: '\n\n[…remaining output truncated]', thinkingHeadTruncated: '[…earlier reasoning truncated]\n', thinkingChunkTruncated: '\n[…single delta truncated]\n', toolChunkTruncated: '\n[…truncated]\n' },
diff --git a/packages/ui/src/task-ledger-panel.tsx b/packages/ui/src/task-ledger-panel.tsx
deleted file mode 100644
index 068bcdb091..0000000000
--- a/packages/ui/src/task-ledger-panel.tsx
+++ /dev/null
@@ -1,236 +0,0 @@
-/*
- * 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 { useMemo, type CSSProperties, type ReactNode } from 'react';
-import { Banner, Collapsible, EmptyState, IconButton, Spinner } from '@astryxdesign/core';
-import type { Task, TaskStatus } from '@maka/core/task-ledger';
-import {
-  ICON_SIZE,
-  AlertCircle,
-  Ban,
-  CheckCircle2,
-  CircleGauge,
-  Clock,
-  ListTodo,
-  RefreshCcw,
-  X,
-} from './icons.js';
-import { useUiLocale } from './locale-context.js';
-import { getSharedUiCopy, type SharedUiCopy } from './shared-ui-copy.js';
-
-const STATUS_ICONS = {
-  pending: Clock,
-  in_progress: CircleGauge,
-  blocked: AlertCircle,
-  completed: CheckCircle2,
-  failed: X,
-  cancelled: Ban,
-} satisfies Record<TaskStatus, typeof Clock>;
-
-export interface TaskLedgerPanelProps {
-  tasks: readonly Task[];
-  loading?: boolean;
-  error?: string;
-  onRetry?: () => void;
-}
-
-export interface TaskLedgerPanelModel {
-  activeCount: number;
-  activeTree: Task[];
-  recentTerminalCount: number;
-  recentTerminalTree: Task[];
-}
-
-export function deriveTaskLedgerPanelModel(tasks: readonly Task[]): TaskLedgerPanelModel {
-  const activeSeeds = tasks.filter((task) => !isTerminal(task.status));
-  const recentTerminalSeeds = tasks
-    .filter((task) => isTerminal(task.status))
-    .sort((a, b) => (b.endedAt ?? b.updatedAt) - (a.endedAt ?? a.updatedAt))
-    .slice(0, 3);
-  return {
-    activeCount: activeSeeds.length,
-    activeTree: orderTaskTree(withAncestors(tasks, activeSeeds)),
-    recentTerminalCount: recentTerminalSeeds.length,
-    recentTerminalTree: orderTaskTree(withAncestors(tasks, recentTerminalSeeds)),
-  };
-}
-
-export function TaskLedgerPanel(props: TaskLedgerPanelProps) {
-  const copy = getSharedUiCopy(useUiLocale()).taskLedger;
-  const model = useMemo(() => deriveTaskLedgerPanelModel(props.tasks), [props.tasks]);
-
-  return (
-    <section className="maka-task-ledger-panel" aria-label={copy.ariaLabel}>
-      {props.error ? (
-        <Banner
-          status="error"
-          role="alert"
-          className="maka-task-ledger-message"
-          title={props.error}
-          endContent={props.onRetry ? (
-            <IconButton
-              variant="ghost"
-              size="sm"
-              className="maka-task-ledger-retry"
-              onClick={props.onRetry}
-              label={copy.retry}
-              tooltip={copy.retry}
-              icon={<RefreshCcw size={ICON_SIZE.control} aria-hidden="true" />}
-            />
-          ) : undefined}
-        />
-      ) : props.loading && props.tasks.length === 0 ? (
-        <Spinner size="sm" shade="subtle" label={copy.loading} className="maka-task-ledger-message" />
-      ) : (
-        <>
-          {model.activeCount > 0 ? (
-            <div className="maka-task-ledger-tree" role="tree" aria-label={copy.activeAriaLabel}>
-              <TaskLedgerTree tasks={model.activeTree} copy={copy} />
-            </div>
-          ) : (
-            <EmptyState
-              isCompact
-              className="maka-task-ledger-empty"
-              icon={<ListTodo size={ICON_SIZE.empty} aria-hidden="true" />}
-              title={copy.empty}
-            />
-          )}
-          {model.recentTerminalCount > 0 && (
-            <Collapsible
-              className="maka-task-ledger-terminal"
-              defaultIsOpen={false}
-              trigger={(
-                <span className="maka-task-ledger-terminal-trigger">
-                  <span>{copy.recent}</span>
-                  <span>{model.recentTerminalCount}</span>
-                </span>
-              )}
-            >
-              <div className="maka-task-ledger-tree" role="tree" aria-label={copy.recentAriaLabel}>
-                <TaskLedgerTree tasks={model.recentTerminalTree} copy={copy} />
-              </div>
-            </Collapsible>
-          )}
-        </>
-      )}
-    </section>
-  );
-}
-
-function TaskLedgerTree({ tasks, copy }: { tasks: readonly Task[]; copy: SharedUiCopy['taskLedger'] }) {
-  const included = new Set(tasks.map((task) => task.id));
-  const childrenByParent = new Map<string | undefined, Task[]>();
-  for (const task of tasks) {
-    const parent = task.parentId && included.has(task.parentId) ? task.parentId : undefined;
-    const siblings = childrenByParent.get(parent) ?? [];
-    siblings.push(task);
-    childrenByParent.set(parent, siblings);
-  }
-  const renderLevel = (parentId: string | undefined, level: number): ReactNode => {
-    const siblings = childrenByParent.get(parentId) ?? [];
-    return siblings.map((task, index) => (
-      <TaskLedgerRow
-        key={task.id}
-        task={task}
-        copy={copy}
-        level={level}
-        position={index + 1}
-        setSize={siblings.length}
-      >
-        {childrenByParent.has(task.id) ? (
-          <div role="group" className="maka-task-ledger-group">
-            {renderLevel(task.id, level + 1)}
-          </div>
-        ) : null}
-      </TaskLedgerRow>
-    ));
-  };
-  return renderLevel(undefined, 1);
-}
-
-function TaskLedgerRow({ task, copy, level, position, setSize, children }: {
-  task: Task;
-  copy: SharedUiCopy['taskLedger'];
-  level: number;
-  position: number;
-  setSize: number;
-  children?: ReactNode;
-}) {
-  const StatusIcon = STATUS_ICONS[task.status];
-  const depth = level - 1;
-  const detail = task.blockedReason ?? task.failureReason ?? task.completionEvidence;
-  const owner = task.owner?.actor === 'child_agent'
-    ? copy.childAgent(task.owner.agentId)
-    : task.owner?.actor === 'main_agent' ? copy.mainAgent : undefined;
-  return (
-    <div
-      className="maka-task-ledger-row"
-      role="treeitem"
-      aria-level={level}
-      aria-posinset={position}
-      aria-setsize={setSize}
-      data-status={task.status}
-      style={{ '--task-depth': Math.min(depth, 6) } as CSSProperties}
-    >
-      <StatusIcon size={ICON_SIZE.control} aria-hidden="true" />
-      <code className="maka-task-ledger-key">{task.key}</code>
-      <span className="maka-task-ledger-subject" title={task.subject}>{task.subject}</span>
-      <span className="maka-task-ledger-meta">
-        <span>{copy.status[task.status]}</span>
-        {owner && <span title={owner}>{owner}</span>}
-      </span>
-      {detail && <span className="maka-task-ledger-detail" title={detail}>{detail}</span>}
-      {children}
-    </div>
-  );
-}
-
-function isTerminal(status: TaskStatus): boolean {
-  return status === 'completed' || status === 'failed' || status === 'cancelled';
-}
-
-function orderTaskTree(tasks: readonly Task[]): Task[] {
-  return [...tasks].sort((a, b) => compareKeys(a.key, b.key));
-}
-
-function withAncestors(allTasks: readonly Task[], seeds: readonly Task[]): Task[] {
-  const byId = new Map(allTasks.map((task) => [task.id, task]));
-  const selected = new Map<string, Task>();
-  for (const seed of seeds) {
-    let current: Task | undefined = seed;
-    const seen = new Set<string>();
-    while (current && !seen.has(current.id)) {
-      seen.add(current.id);
-      selected.set(current.id, current);
-      current = current.parentId ? byId.get(current.parentId) : undefined;
-    }
-  }
-  return [...selected.values()];
-}
-
-function compareKeys(left: string, right: string): number {
-  const a = left.slice(1).split('.').map(Number);
-  const b = right.slice(1).split('.').map(Number);
-  for (let index = 0; index < Math.max(a.length, b.length); index += 1) {
-    if (a[index] === undefined) return -1;
-    if (b[index] === undefined) return 1;
-    if (a[index] !== b[index]) return a[index]! - b[index]!;
-  }
-  return 0;
-}


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