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

(maka) branch main updated: fix(desktop): localize expected errors (#4457)

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

Astro-Han 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 42fa4d0705 fix(desktop): localize expected errors (#4457)
42fa4d0705 is described below

commit 42fa4d070504e672173c46dd75f7fcabb88789f5
Author: NekoPunch <95...@users.noreply.github.com>
AuthorDate: Tue Sep 8 03:28:36 2026 -0700

    fix(desktop): localize expected errors (#4457)
    
    Replace raw exception copy with producer-owned error codes and domain-local presenters. Preserve expected Plan failures across IPC where the panel consumes structured results, keep the existing proposal-exit throwing contract, unify attachment rejection tokens, and route unexpected failures to redacted deduplicated diagnostics. Remove prose-based WorkHub classification and the obsolete Session read marker. Declare existing Plan error codes as compatible at epoch 133.
    
    Refs #2672
    
    Generated-by: Claude Code
    Generated-by: OpenCode
    Generated-by: pi (GLM)
---
 apps/desktop/renderer-architecture.json            |  37 ++-
 .../app-shell-attachment-preflight-send.test.ts    |  99 +++++++
 .../__tests__/attachment-ingest-payload.test.ts    |   6 +-
 .../__tests__/attachment-ingest-resolve.test.ts    |  26 +-
 .../main/__tests__/attachment-preflight.test.ts    |  48 ++--
 .../__tests__/expected-error-presentation.test.ts  | 217 +++++++++++++++
 .../src/main/__tests__/plan-mode-copy.test.ts      |   2 +
 .../main/__tests__/plan-mode-panel-pending.test.ts | 125 +++++++++
 .../runtime-host-guest-session-mounts.test.ts      |  21 +-
 .../runtime-host-session-domains-ipc-main.test.ts  | 160 +++++++++--
 .../src/main/__tests__/session-local.test.ts       |   2 +-
 .../src/main/__tests__/work-board-ipc-main.test.ts |  61 ++++-
 .../main/__tests__/workhub-controller-fixture.ts   |   2 +-
 .../src/main/__tests__/workhub-controller.test.ts  | 116 ++++++--
 .../__tests__/workhub-routing-strategy.test.ts     |   6 +-
 .../main/__tests__/workhub-surface-flow.test.ts    |  68 +++--
 apps/desktop/src/main/attachment-ingest.ts         |  31 +--
 .../src/main/runtime-host-guest-session-mounts.ts  |  22 +-
 .../main/runtime-host-session-domains-ipc-main.ts  | 134 +++++++---
 apps/desktop/src/main/work-board-ipc-main.ts       |  15 +-
 .../src/preload/attachment-ingest-payload.ts       |  12 +-
 apps/desktop/src/preload/bridge-contract.d.ts      |  16 +-
 apps/desktop/src/preload/preload.ts                |  25 +-
 .../desktop/src/renderer/app-shell-chat-actions.ts |   2 +-
 apps/desktop/src/renderer/app-shell-copy.ts        |  41 ++-
 apps/desktop/src/renderer/app-shell.tsx            |   2 +-
 .../application/contracts/operation-diagnostics.ts |  57 ++++
 apps/desktop/src/renderer/attachment-preflight.ts  |  25 +-
 .../features/session-collaboration/testing.ts      |   1 +
 .../ui/session-collaboration-join-dialog.tsx       |  40 +--
 .../tools/side-chat/quote-companion-panel.tsx      |   4 +-
 .../features/workhub/model/route-policy.ts         |  12 +-
 .../src/renderer/locales/conversation-copy.ts      |   4 -
 .../desktop/src/renderer/locales/plan-mode-copy.ts |  61 ++++-
 .../renderer/locales/session-collaboration-copy.ts |  12 +
 apps/desktop/src/renderer/locales/shell-copy.ts    |  56 +++-
 .../src/renderer/locales/work-board-error-copy.ts  |  62 +++++
 apps/desktop/src/renderer/locales/workhub-copy.ts  | 296 +++++++++++++++++++++
 apps/desktop/src/renderer/plan-mode-panel.tsx      |  71 +++--
 .../renderer/remote-project-directory-dialog.tsx   |  28 +-
 .../src/renderer/session-collaboration-dialog.tsx  |  20 +-
 apps/desktop/src/renderer/use-work-board.ts        |  34 ++-
 apps/desktop/src/renderer/work-board-panel.tsx     |  27 +-
 apps/desktop/src/renderer/workhub-controller.ts    |  16 +-
 apps/desktop/src/renderer/workhub-surface.tsx      | 280 ++-----------------
 .../{work-board-ipc.ts => plan-mode-ipc.d.ts}      |  25 +-
 apps/desktop/src/shared/session-collaboration.d.ts |   6 +-
 apps/desktop/src/shared/work-board-ipc.ts          |  10 +-
 apps/desktop/stories/workhub.stories.tsx           |   3 +-
 packages/core/src/attachments.ts                   |  12 +
 packages/core/src/redaction.ts                     |  26 ++
 .../plan-control-error-codes.json                  |   5 +
 packages/runtime-host/src/protocol/plan.ts         |   4 +
 53 files changed, 1867 insertions(+), 626 deletions(-)

diff --git a/apps/desktop/renderer-architecture.json b/apps/desktop/renderer-architecture.json
index 801122ab24..4712d5e0c1 100644
--- a/apps/desktop/renderer-architecture.json
+++ b/apps/desktop/renderer-architecture.json
@@ -90,6 +90,7 @@
     "src/renderer/locales/shell-copy.ts",
     "src/renderer/locales/shell-remaining-copy.ts",
     "src/renderer/locales/task-readiness-copy.ts",
+    "src/renderer/locales/work-board-error-copy.ts",
     "src/renderer/locales/workhub-copy.ts",
     "src/renderer/main.tsx",
     "src/renderer/mcp-brand-contrast.ts",
@@ -341,7 +342,7 @@
           "@maka/ui": 1
         },
         "importSpecifiers": 13,
-        "nonTriviaTokens": 4057
+        "nonTriviaTokens": 4055
       },
       "src/renderer/app-shell-chrome-actions.tsx": {
         "importDeclarations": 4,
@@ -418,11 +419,12 @@
         "unresolvedDependencies": 0,
         "actionFactories": [],
         "dependencyPaths": {
+          "./application/contracts/operation-diagnostics.js": 1,
           "./locales/shell-copy.js": 1,
           "@maka/core/redaction": 1
         },
         "importSpecifiers": 1,
-        "nonTriviaTokens": 504
+        "nonTriviaTokens": 393
       },
       "src/renderer/app-shell-detail-panel.tsx": {
         "importDeclarations": 0,
@@ -893,7 +895,7 @@
           "react": 1
         },
         "importSpecifiers": 116,
-        "nonTriviaTokens": 14342
+        "nonTriviaTokens": 14338
       },
       "src/renderer/use-app-shell-composer-quotes.ts": {
         "importDeclarations": 2,
@@ -1100,7 +1102,6 @@
         "unresolvedDependencies": 0,
         "actionFactories": [],
         "dependencyPaths": {
-          "./locales/conversation-copy.js": 1,
           "@maka/core/attachments": 1
         }
       },
@@ -1621,7 +1622,9 @@
         "lifecycleMethods": {},
         "unresolvedDependencies": 0,
         "actionFactories": [],
-        "dependencyPaths": {}
+        "dependencyPaths": {
+          "@maka/core/ui-locale": 1
+        }
       },
       "src/renderer/locales/session-collaboration-copy.ts": {
         "bridgePaths": {},
@@ -1783,7 +1786,8 @@
         "unresolvedDependencies": 0,
         "actionFactories": [],
         "dependencyPaths": {
-          "@maka/core/redaction": 1
+          "@maka/core/redaction": 1,
+          "@maka/core/ui-locale": 1
         }
       },
       "src/renderer/locales/shell-remaining-copy.ts": {
@@ -1804,6 +1808,17 @@
         "actionFactories": [],
         "dependencyPaths": {}
       },
+      "src/renderer/locales/work-board-error-copy.ts": {
+        "bridgePaths": {},
+        "environmentCapabilities": {},
+        "hookCalls": {},
+        "lifecycleMethods": {},
+        "unresolvedDependencies": 0,
+        "actionFactories": [],
+        "dependencyPaths": {
+          "@maka/core/ui-locale": 1
+        }
+      },
       "src/renderer/locales/workhub-copy.ts": {
         "bridgePaths": {},
         "environmentCapabilities": {},
@@ -2084,6 +2099,7 @@
         "unresolvedDependencies": 0,
         "actionFactories": [],
         "dependencyPaths": {
+          "./application/contracts/operation-diagnostics.js": 1,
           "./locales/plan-mode-copy.js": 1,
           "@astryxdesign/core/Banner": 1,
           "@astryxdesign/core/Collapsible": 1,
@@ -2120,6 +2136,7 @@
         "unresolvedDependencies": 0,
         "actionFactories": [],
         "dependencyPaths": {
+          "./application/contracts/operation-diagnostics.js": 1,
           "./locales/shell-copy.js": 1,
           "@astryxdesign/core/Button": 1,
           "@astryxdesign/core/Dialog": 1,
@@ -2185,6 +2202,7 @@
         "unresolvedDependencies": 0,
         "actionFactories": [],
         "dependencyPaths": {
+          "./application/contracts/operation-diagnostics.js": 1,
           "./features/session-collaboration": 1,
           "./locales/session-collaboration-copy.js": 1,
           "@astryxdesign/core": 1,
@@ -4532,6 +4550,7 @@
         "unresolvedDependencies": 0,
         "actionFactories": [],
         "dependencyPaths": {
+          "./application/contracts/operation-diagnostics.js": 1,
           "@maka/core/work-board": 1,
           "react": 1
         }
@@ -4550,7 +4569,9 @@
         "unresolvedDependencies": 0,
         "actionFactories": [],
         "dependencyPaths": {
+          "./application/contracts/operation-diagnostics.js": 1,
           "./locales/conversation-copy.js": 1,
+          "./locales/work-board-error-copy.js": 1,
           "./use-work-board.js": 1,
           "@astryxdesign/core": 1,
           "@astryxdesign/core/Button": 1,
@@ -4568,6 +4589,7 @@
         "unresolvedDependencies": 0,
         "actionFactories": [],
         "dependencyPaths": {
+          "./application/contracts/operation-diagnostics.js": 1,
           "./features/workhub/index.js": 1
         }
       },
@@ -4648,8 +4670,9 @@
         "unresolvedDependencies": 0,
         "actionFactories": [],
         "dependencyPaths": {
+          "./application/contracts/operation-diagnostics.js": 1,
           "./features/workhub/index.js": 1,
-          "./locales/workhub-copy.js": 1,
+          "./locales/workhub-copy.js": 2,
           "./workhub-coordination-port.js": 1,
           "./workhub-send-lease.js": 1,
           "@astryxdesign/core": 1,
diff --git a/apps/desktop/src/main/__tests__/app-shell-attachment-preflight-send.test.ts b/apps/desktop/src/main/__tests__/app-shell-attachment-preflight-send.test.ts
new file mode 100644
index 0000000000..b8ca804cdc
--- /dev/null
+++ b/apps/desktop/src/main/__tests__/app-shell-attachment-preflight-send.test.ts
@@ -0,0 +1,99 @@
+/*
+ * 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.
+ */
+
+/**
+ * Review regression on #4457: the new-task send path runs the renderer-side
+ * attachment preflight BEFORE `newTasks.create`, so the main-side token
+ * validation never sees an over-limit request. The preflight must reject with
+ * the same stable `attachment_ingest:<code>` tokens main rejects with, so the
+ * send catch maps the real reason through the locale catalog instead of the
+ * generic "try again later" fallback (retrying nine attachments can never
+ * succeed) — and an expected rejection must not log an unexpected diagnostic.
+ */
+
+import { MAX_ATTACHMENT_COUNT } from '@maka/core/attachments';
+import { strict as assert } from 'node:assert';
+import { test } from 'node:test';
+import type { PendingAttachment } from '../../renderer/composer-attachments.js';
+import { createAppShellChatActions } from '../../renderer/app-shell-chat-actions.js';
+import { getShellCopy } from '../../renderer/locales/shell-copy.js';
+import {
+  createActionsDeps,
+  installWindow,
+} from './app-shell-chat-actions-fixture.js';
+
+function fileAttachment(size: number, index: number): PendingAttachment {
+  return {
+    stagingKey: `staged-file-${index}`,
+    displayName: 'report.txt',
+    kind: 'other',
+    size,
+    source: { type: 'file', file: { size } },
+  } as PendingAttachment;
+}
+
+test('a nine-attachment new-task send shows the count reason, creates no session, and logs no unexpected diagnostic', async (context) => {
+  const errorLog = context.mock.method(console, 'error', () => undefined);
+  const created: unknown[] = [];
+  const toasts: Array<{ title: string; description?: string }> = [];
+  const restoreWindow = installWindow({
+    newTasks: {
+      create: async () => {
+        created.push(true);
+        return { id: 'session-created-anyway' };
+      },
+    },
+  });
+
+  try {
+    for (const locale of ['zh-CN', 'en'] as const) {
+      toasts.length = 0;
+      const actions = createAppShellChatActions({
+        ...createActionsDeps(),
+        uiLocale: locale,
+        toastApi: {
+          error: (title, description) => {
+            toasts.push({ title, description });
+          },
+          info: () => undefined,
+        },
+      });
+      const pending = Array.from(
+        { length: MAX_ATTACHMENT_COUNT + 1 },
+        (_, index) => fileAttachment(10, index),
+      );
+      const accepted = await actions.send('hello', pending);
+      assert.equal(accepted, false, `${locale}: the send must be rejected`);
+      assert.equal(created.length, 0, `${locale}: preflight runs before the session exists`);
+      assert.equal(toasts.length, 1, `${locale}: exactly one rejection toast`);
+      assert.equal(
+        toasts[0].description,
+        getShellCopy(locale).sessionSettingsActions.attachmentIngestBlocked.count_limit,
+        `${locale}: the toast carries the count reason, not the generic fallback`,
+      );
+    }
+  } finally {
+    restoreWindow();
+  }
+  assert.equal(
+    errorLog.mock.callCount(),
+    0,
+    'an expected preflight rejection must not land the unexpected-error diagnostic',
+  );
+});
\ No newline at end of file
diff --git a/apps/desktop/src/main/__tests__/attachment-ingest-payload.test.ts b/apps/desktop/src/main/__tests__/attachment-ingest-payload.test.ts
index 1c045ea75d..9d706bfe17 100644
--- a/apps/desktop/src/main/__tests__/attachment-ingest-payload.test.ts
+++ b/apps/desktop/src/main/__tests__/attachment-ingest-payload.test.ts
@@ -24,7 +24,7 @@ import { encodeIngestItems } from '../../preload/attachment-ingest-payload.js';
 describe('encodeIngestItems', () => {
   test('rejects more than 8 items without reading any file bytes', async () => {
     const items = Array.from({ length: 9 }, (_, i) => ({ approvalId: `a${i}`, name: `f${i}.txt` }));
-    await assert.rejects(encodeIngestItems(items as never), /8/);
+    await assert.rejects(encodeIngestItems(items as never), /attachment_ingest:count_limit/);
   });
 
   test('rejects a File over 50MB without calling arrayBuffer', async () => {
@@ -38,7 +38,7 @@ describe('encodeIngestItems', () => {
         return new ArrayBuffer(0);
       },
     } as unknown as File;
-    await assert.rejects(encodeIngestItems([{ file: bigFile }]), /50/);
+    await assert.rejects(encodeIngestItems([{ file: bigFile }]), /attachment_ingest:item_too_large/);
     assert.equal(arrayBufferCalls, 0, 'arrayBuffer must not be called for an oversized file');
   });
 
@@ -67,7 +67,7 @@ describe('encodeIngestItems', () => {
   test('rejects a raw base64 item that is neither a File nor an approval token', async () => {
     await assert.rejects(
       encodeIngestItems([{ name: 'forged', base64: 'AAAA' }] as never),
-      /无效/,
+      /attachment_ingest:items_invalid/,
     );
   });
 });
diff --git a/apps/desktop/src/main/__tests__/attachment-ingest-resolve.test.ts b/apps/desktop/src/main/__tests__/attachment-ingest-resolve.test.ts
index 3924e493f4..c050c5aa09 100644
--- a/apps/desktop/src/main/__tests__/attachment-ingest-resolve.test.ts
+++ b/apps/desktop/src/main/__tests__/attachment-ingest-resolve.test.ts
@@ -45,7 +45,7 @@ describe('resolveIngestItems (pre-read validation)', () => {
           approvals,
           stat: async () => (statCalls++, { size: 1 }),
         }),
-      /最多/,
+      /attachment_ingest:count_limit/,
     );
     assert.equal(statCalls, 0);
   });
@@ -61,7 +61,7 @@ describe('resolveIngestItems (pre-read validation)', () => {
           approvals,
           stat: async () => (statCalls++, { size: 1 }),
         }),
-      /过期|无效/,
+      /attachment_ingest:(source_expired|items_invalid)/,
     );
     assert.equal(statCalls, 0);
   });
@@ -78,7 +78,7 @@ describe('resolveIngestItems (pre-read validation)', () => {
           approvals,
           stat: async () => (statCalls++, { size: 1 }),
         }),
-      /过期|无效/,
+      /attachment_ingest:(source_expired|items_invalid)/,
     );
     assert.equal(statCalls, 0);
   });
@@ -96,7 +96,7 @@ describe('resolveIngestItems (pre-read validation)', () => {
           stat: async () => (statCalls++, { size: 200 }),
           maxBytes: 100,
         }),
-      /超出大小限制/,
+      /attachment_ingest:item_too_large/,
     );
     assert.equal(statCalls, 1);
   });
@@ -121,7 +121,7 @@ describe('resolveIngestItems (pre-read validation)', () => {
             stat: async () => (statCalls++, { size: 1 }),
             maxBytes: 100,
           }),
-        /超出大小限制/,
+        /attachment_ingest:item_too_large/,
       );
       assert.equal(statCalls, 0);
       assert.equal(decodeCalls, 0, 'must reject by base64 string length before Buffer.from');
@@ -149,7 +149,7 @@ describe('resolveIngestItems (pre-read validation)', () => {
           approvals,
           stat: async () => ({ size: 10 }),
         }),
-      /过期|无效/,
+      /attachment_ingest:(source_expired|items_invalid)/,
     );
   });
 
@@ -165,11 +165,11 @@ describe('resolveIngestItems (pre-read validation)', () => {
     for (const item of items) assert.ok(approvals.peekApproval(1, item.approvalId));
     const competing = await prepareIngestItems({ ...input, items: [items[1]] });
     assert.equal(competing.commit(() => 'admitted'), 'admitted');
-    assert.throws(() => plan.commit(() => assert.fail('must not admit an invalid plan')), /过期|无效/);
+    assert.throws(() => plan.commit(() => assert.fail('must not admit an invalid plan')), /attachment_ingest:source_expired/);
     assert.ok(approvals.peekApproval(1, items[0]!.approvalId), 'a lost race must not burn the other approval');
     const remaining = await prepareIngestItems({ ...input, items: [items[0]] });
     approvals.clearSender(1);
-    assert.throws(() => remaining.commit(() => assert.fail('must not admit after sender teardown')), /过期|无效/);
+    assert.throws(() => remaining.commit(() => assert.fail('must not admit after sender teardown')), /attachment_ingest:source_expired/);
   });
 
   test('resolves a mix of approved paths and blobs into ingest files', async () => {
@@ -204,7 +204,7 @@ describe('resolveIngestItems (pre-read validation)', () => {
         approvals,
         stat: async () => (statCalls++, { size: 10 }),
       }),
-      /无效/,
+      /attachment_ingest:items_invalid/,
     );
     assert.notEqual(
       approvals.consumeApproval(1, issued.approvalId),
@@ -227,7 +227,7 @@ describe('resolveIngestItems (pre-read validation)', () => {
         approvals,
         stat: async () => (statCalls++, { size: 10 }),
       }),
-      /重复/,
+      /attachment_ingest:duplicate_source/,
     );
     assert.notEqual(
       approvals.consumeApproval(1, issued.approvalId),
@@ -246,7 +246,7 @@ describe('resolveIngestItems (pre-read validation)', () => {
           approvals,
           stat: async () => ({ size: 1 }),
         }),
-      /无效/,
+      /attachment_ingest:items_invalid/,
     );
     await assert.rejects(
       () =>
@@ -256,7 +256,7 @@ describe('resolveIngestItems (pre-read validation)', () => {
           approvals,
           stat: async () => ({ size: 1 }),
         }),
-      /无效/,
+      /attachment_ingest:items_invalid/,
     );
   });
 });
@@ -440,7 +440,7 @@ describe('resolveAttachmentRefs', () => {
             throw new Error('snapshot must not run');
           },
         }),
-        /超出大小限制/,
+        /attachment_ingest:item_too_large/,
       );
       assert.equal(snapshots, 0);
     } finally {
diff --git a/apps/desktop/src/main/__tests__/attachment-preflight.test.ts b/apps/desktop/src/main/__tests__/attachment-preflight.test.ts
index cf3971b199..f928f31c55 100644
--- a/apps/desktop/src/main/__tests__/attachment-preflight.test.ts
+++ b/apps/desktop/src/main/__tests__/attachment-preflight.test.ts
@@ -19,50 +19,48 @@
 
 import assert from 'node:assert/strict';
 import { describe, test } from 'node:test';
+import { MAX_ATTACHMENT_BYTES, MAX_ATTACHMENT_COUNT } from '@maka/core/attachments';
 import { preflightAttachmentItems } from '../../renderer/attachment-preflight.js';
 
-const CAP = 50 * 1024 * 1024;
-
 describe('attachment preflight (before session create)', () => {
   test('rejects more than 8 items before any session is created', () => {
-    const items = Array.from({ length: 9 }, () => ({
+    const items = Array.from({ length: MAX_ATTACHMENT_COUNT + 1 }, () => ({
       size: 100,
       source: { type: 'file' as const, file: { size: 100 } },
     }));
-    assert.throws(() => preflightAttachmentItems(items, 'zh-CN'), /8/);
+    assert.throws(() => preflightAttachmentItems(items), {
+      message: 'attachment_ingest:count_limit',
+    });
   });
 
   test('rejects an oversized File so no empty session is created', () => {
     assert.throws(
-      () => preflightAttachmentItems([{ size: CAP + 1, source: { type: 'file', file: { size: CAP + 1 } } }], 'zh-CN'),
-      /50MB/,
+      () =>
+        preflightAttachmentItems([
+          { size: MAX_ATTACHMENT_BYTES + 1, source: { type: 'file', file: { size: MAX_ATTACHMENT_BYTES + 1 } } },
+        ]),
+      { message: 'attachment_ingest:item_too_large' },
     );
   });
 
   test('rejects an oversized approval-token attachment by pending size', () => {
     assert.throws(
-      () => preflightAttachmentItems([{ size: CAP + 1, source: { type: 'approval', approvalId: 'a1' } }], 'zh-CN'),
-      /50MB/,
+      () =>
+        preflightAttachmentItems([
+          { size: MAX_ATTACHMENT_BYTES + 1, source: { type: 'approval', approvalId: 'a1' } },
+        ]),
+      { message: 'attachment_ingest:item_too_large' },
     );
   });
 
   test('rejects a duplicate approvalId', () => {
-    assert.throws(
-      () =>
-        preflightAttachmentItems([
-          { size: 10, source: { type: 'approval', approvalId: 'dup' } },
-          { size: 10, source: { type: 'approval', approvalId: 'dup' } },
-        ], 'zh-CN'),
-      /重复/,
-    );
-    assert.throws(
-      () =>
-        preflightAttachmentItems([
-          { size: 10, source: { type: 'approval', approvalId: 'dup' } },
-          { size: 10, source: { type: 'approval', approvalId: 'dup' } },
-        ], 'en'),
-      /already added/,
-    );
+    const duplicate = [
+      { size: 10, source: { type: 'approval' as const, approvalId: 'dup' } },
+      { size: 10, source: { type: 'approval' as const, approvalId: 'dup' } },
+    ];
+    assert.throws(() => preflightAttachmentItems(duplicate), {
+      message: 'attachment_ingest:duplicate_source',
+    });
   });
 
   test('passes approval tokens and files under the cap', () => {
@@ -70,7 +68,7 @@ describe('attachment preflight (before session create)', () => {
       preflightAttachmentItems([
         { size: 100, source: { type: 'approval', approvalId: 'a1' } },
         { size: 100, source: { type: 'file', file: { size: 100 } } },
-      ], 'zh-CN'),
+      ]),
     );
   });
 });
\ No newline at end of file
diff --git a/apps/desktop/src/main/__tests__/expected-error-presentation.test.ts b/apps/desktop/src/main/__tests__/expected-error-presentation.test.ts
new file mode 100644
index 0000000000..db89623789
--- /dev/null
+++ b/apps/desktop/src/main/__tests__/expected-error-presentation.test.ts
@@ -0,0 +1,217 @@
+/*
+ * 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 { getWorkBoardErrorCopy } from '../../renderer/locales/work-board-error-copy.js';
+import { workBoardActionErrorText } from '../../renderer/work-board-panel.js';
+import { ExpectedOperationError, reportUnexpectedError } from '../../renderer/application/contracts/operation-diagnostics.js';
+import { getSessionCollaborationCopy } from '../../renderer/locales/session-collaboration-copy.js';
+import { sessionCollaborationImportErrorMessage } from '../../renderer/features/session-collaboration/testing.js';
+import {
+  commandPaletteActionErrorMessage,
+  commandPaletteConnectionTestFailureMessage,
+  messageReadErrorMessage,
+  messageRefreshErrorMessage,
+  openPathActionErrorMessage,
+} from '../../renderer/app-shell-copy.js';
+import { getShellCopy, localizedShellErrorMessage } from '../../renderer/locales/shell-copy.js';
+import { getPlanModeCopy, planControlFailureCopy } from '../../renderer/locales/plan-mode-copy.js';
+
+test('routes Work Board codes through the shared presenter per locale', (context) => {
+  context.mock.method(console, 'error', () => undefined);
+  for (const locale of ['zh-CN', 'zh-TW', 'en'] as const) {
+    const errorCopy = getWorkBoardErrorCopy(locale);
+    assert.equal(
+      workBoardActionErrorText(
+        new ExpectedOperationError('not_found'),
+        locale,
+        'action-failed-fallback',
+      ),
+      errorCopy.not_found,
+    );
+  }
+  // An unexpected failure keeps the caller fallback (and logs redacted
+  // diagnostics) instead of rendering the raw error.
+  assert.equal(
+    workBoardActionErrorText(new Error('raw internal detail'), 'zh-CN', '操作失败兜底'),
+    '操作失败兜底',
+  );
+});
+
+test('maps attachment-ingest tokens per locale at the shared entry', () => {
+  const blocked = new Error("Error invoking remote method 'attachments': Error: attachment_ingest:count_limit");
+  assert.equal(localizedShellErrorMessage(blocked, 'fallback', 'zh-CN'), '一次最多添加 8 个附件。');
+  assert.equal(
+    localizedShellErrorMessage(blocked, 'fallback', 'en'),
+    'At most 8 attachments per message.',
+  );
+});
+
+test('the ingest token only matches at the message tail', (context) => {
+  context.mock.method(console, 'error', () => undefined);
+  const bare = 'attachment_ingest:count_limit';
+  const wrapped = "Error invoking remote method 'sessions:send': Error: attachment_ingest:count_limit";
+  assert.equal(localizedShellErrorMessage(new Error(bare), 'fallback', 'zh-CN'), '一次最多添加 8 个附件。');
+  assert.equal(localizedShellErrorMessage(new Error(wrapped), 'fallback', 'en'), 'At most 8 attachments per message.');
+  // Unrelated messages that merely contain the substring keep the fallback
+  // and take the unexpected-error diagnostics path.
+  const sneaky = 'Unable to open /tmp/attachment_ingest:count_limit/report.txt';
+  assert.equal(localizedShellErrorMessage(new Error(sneaky), 'fallback', 'zh-CN'), 'fallback');
+  assert.equal(localizedShellErrorMessage('path attachment_ingest:count_limit extra', 'fallback', 'en'), 'fallback');
+});
+
+test('a classified shell failure renders its category without an unexpected diagnostic', (context) => {
+  const errors = context.mock.method(console, 'error', () => undefined);
+  assert.equal(localizedShellErrorMessage(new Error('request timeout'), 'fallback', 'zh-CN'), '请求超时');
+  assert.equal(errors.mock.callCount(), 0);
+  assert.equal(localizedShellErrorMessage(new Error('boom'), 'fallback', 'zh-CN'), 'fallback');
+  assert.equal(errors.mock.callCount(), 1);
+});
+
+test('maps plan control envelopes per locale at the panel', () => {
+  for (const locale of ['zh-CN', 'zh-TW', 'en'] as const) {
+    const copy = getPlanModeCopy(locale);
+    for (const code of ['session_busy', 'operation_conflict', 'not_found', 'persistence_failed', 'unauthorized'] as const) {
+      // The wire shape the panel actually receives: the main process returns
+      // the structured envelope; Electron would have stripped a thrown
+      // typed error's custom fields, which is why the envelope exists.
+      const failure = {
+        ok: false,
+        error: { code, message: 'provider detail' },
+      } as const;
+      assert.equal(
+        planControlFailureCopy(failure.error, copy),
+        copy.controlFailure[code],
+        `${locale}: ${code}`,
+      );
+    }
+    const unknownCode = { ok: false, error: { code: 'future_code', message: '' } } as const;
+    assert.equal(
+      planControlFailureCopy(unknownCode.error, copy),
+      copy.operationFailed,
+    );
+  }
+});
+
+
+test('unknown and inherited reason tokens retain the caller fallback', (context) => {
+  context.mock.method(console, 'error', () => undefined);
+  for (const locale of ['zh-CN', 'zh-TW', 'en'] as const) {
+    for (const code of ['future_code', 'constructor']) {
+      const token = `attachment_ingest:${code}`;
+      for (const error of [token, new Error(token)]) {
+        assert.equal(localizedShellErrorMessage(error, 'fallback', locale), 'fallback');
+      }
+    }
+  }
+});
+
+test('routes structured collaboration failures through each locale catalog', () => {
+  const cases = [
+    [{ kind: 'error', reason: 'invalid_code' } as const, 'invalidCode'],
+    [{ kind: 'error', reason: 'peer_path_unavailable' } as const, 'directPathUnavailable'],
+    [{ kind: 'error', reason: 'connection_failed' } as const, 'connectionFailed'],
+  ] as const;
+  for (const locale of ['zh-CN', 'zh-TW', 'en'] as const) {
+    const copy = getSessionCollaborationCopy(locale);
+    for (const [result, key] of cases) {
+      assert.equal(sessionCollaborationImportErrorMessage(copy, result), copy[key]);
+    }
+    assert.equal(
+      sessionCollaborationImportErrorMessage(copy, {
+        kind: 'error',
+        reason: 'mount_limit_reached',
+        params: { max: 12 },
+      }),
+      copy.mountLimit(12),
+    );
+  }
+});
+
+test('shell errors keep the generalized classifier over raw text', (context) => {
+  context.mock.method(console, 'error', () => undefined);
+  const raw = new Error('timeout 401 网络失败 MAKA_SESSION_READ_MESSAGES_ERROR: 后端中文');
+  assert.equal(messageReadErrorMessage(raw, 'en'), 'Request timed out');
+  assert.equal(messageReadErrorMessage(raw, 'zh-CN'), '请求超时');
+  assert.equal(
+    commandPaletteActionErrorMessage(raw, 'English fallback', 'en'),
+    'Request timed out',
+  );
+  assert.equal(commandPaletteActionErrorMessage(raw, '中文兜底', 'zh-CN'), '请求超时');
+  assert.equal(
+    localizedShellErrorMessage(raw, 'English fallback', 'en'),
+    'Request timed out',
+  );
+  assert.equal(
+    localizedShellErrorMessage(raw, '中文兜底', 'zh-CN'),
+    '请求超时',
+  );
+  const unclassifiable = new Error('no category here');
+  assert.equal(localizedShellErrorMessage(unclassifiable, 'English fallback', 'en'), 'English fallback');
+  assert.equal(commandPaletteActionErrorMessage(unclassifiable, 'English fallback', 'en'), 'English fallback');
+  assert.equal(
+    messageReadErrorMessage(unclassifiable, 'zh-CN'),
+    '任务内容暂时无法读取,请稍后重试。',
+  );
+});
+
+test('every shell error-copy entry classifies, and keeps its contextual fallback', (context) => {
+  const errors = context.mock.method(console, 'error', () => undefined);
+  const timeout = new Error('request timeout');
+  const opaque = new Error('no category here');
+  const copy = getShellCopy('zh-CN');
+
+  assert.equal(messageRefreshErrorMessage(timeout, 'zh-CN'), '请求超时');
+  assert.equal(messageRefreshErrorMessage(opaque, 'zh-CN'), copy.errors.messageRefresh);
+
+  assert.equal(openPathActionErrorMessage(timeout, 'skills', 'zh-CN'), '请求超时');
+  assert.equal(
+    openPathActionErrorMessage(opaque, 'skills', 'zh-CN'),
+    copy.errors.openPath(copy.paths.skills),
+  );
+
+  // The connection test derives its own category from the status code, so an
+  // unrecognized message is not a defect: contextual copy, and no diagnostic.
+  const logged = errors.mock.callCount();
+  assert.equal(
+    commandPaletteConnectionTestFailureMessage(
+      { ok: false, statusCode: 429, errorMessage: 'quota exceeded for organization' },
+      'zh-CN',
+    ),
+    copy.commandActions.connectionFailures.rateLimit,
+  );
+  assert.equal(errors.mock.callCount(), logged, 'a classified connection failure logs no diagnostic');
+});
+
+test('one failure yields one diagnostic however many layers format it', (context) => {
+  const errors = context.mock.method(console, 'error', () => undefined);
+  const cause = new Error('an opaque backend fault');
+  localizedShellErrorMessage(cause, 'inner fallback', 'en');
+  localizedShellErrorMessage(cause, 'outer fallback', 'zh-CN');
+  assert.equal(errors.mock.callCount(), 1);
+});
+
+test('a classified failure never reaches the diagnostics channel', (context) => {
+  const errors = context.mock.method(console, 'error', () => undefined);
+  reportUnexpectedError('remote-directory:list', new Error('request timeout'));
+  assert.equal(errors.mock.callCount(), 0);
+  reportUnexpectedError('remote-directory:list', new Error('an opaque backend fault'));
+  assert.equal(errors.mock.callCount(), 1);
+});
diff --git a/apps/desktop/src/main/__tests__/plan-mode-copy.test.ts b/apps/desktop/src/main/__tests__/plan-mode-copy.test.ts
index af8cc19329..1d6b5b050a 100644
--- a/apps/desktop/src/main/__tests__/plan-mode-copy.test.ts
+++ b/apps/desktop/src/main/__tests__/plan-mode-copy.test.ts
@@ -29,6 +29,8 @@ test('localizes Plan Mode chrome and abandon confirmation without rewriting plan
   assert.equal(en.proposal.statuses.approved, 'Approved');
   assert.equal(zh.execution.stepCount(2, 3), '2/3 步');
   assert.equal(en.execution.stepCount(1, 1), '1/1 step');
+  assert.equal(zh.operationFailed, '计划操作失败,请稍后重试。');
+  assert.equal(en.operationFailed, 'The plan action failed. Try again later.');
   assert.deepEqual(
     {
       title: en.abandonConfirmation.title,
diff --git a/apps/desktop/src/main/__tests__/plan-mode-panel-pending.test.ts b/apps/desktop/src/main/__tests__/plan-mode-panel-pending.test.ts
new file mode 100644
index 0000000000..23b9101763
--- /dev/null
+++ b/apps/desktop/src/main/__tests__/plan-mode-panel-pending.test.ts
@@ -0,0 +1,125 @@
+/*
+ * 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 { deferred } from '@maka/core/test-only/async-primitives';
+import assert from 'node:assert/strict';
+import { afterEach, test } from 'node:test';
+import { act, createElement } from 'react';
+import { createRoot, type Root } from 'react-dom/client';
+import { parseHTML } from 'linkedom';
+import type { PlanSessionState } from '@maka/core/plan';
+import type { SessionSummary } from '@maka/core/session';
+import type { PlanControlIpcResult } from '../../shared/plan-mode-ipc.js';
+import { AstryxLocaleProvider, LocaleProvider, ToastProvider } from '@maka/ui';
+import { usePlanModeState, type PlanModeState } from '../../renderer/plan-mode-panel.js';
+
+const originalGlobals = {
+  document: globalThis.document,
+  window: globalThis.window,
+  HTMLElement: globalThis.HTMLElement,
+  HTMLIFrameElement: globalThis.HTMLIFrameElement,
+  Event: globalThis.Event,
+  Node: globalThis.Node,
+  CSS: globalThis.CSS,
+  matchMedia: globalThis.matchMedia,
+  requestAnimationFrame: globalThis.requestAnimationFrame,
+  cancelAnimationFrame: globalThis.cancelAnimationFrame,
+  IS_REACT_ACT_ENVIRONMENT: (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean })
+    .IS_REACT_ACT_ENVIRONMENT,
+};
+
+let mountedRoot: Root | undefined;
+
+afterEach(async () => {
+  if (mountedRoot) await act(() => mountedRoot?.unmount());
+  mountedRoot = undefined;
+  Object.assign(globalThis, originalGlobals);
+});
+
+test('plan controls stay pending until the bridge promise settles', async () => {
+  const planState = { storeVersion: 1 } as unknown as PlanSessionState;
+  const revision = deferred<PlanControlIpcResult<PlanSessionState>>();
+  const { document, window } = parseHTML('<html><body><div id="root"></div></body></html>');
+  const matchMedia = (media: string) => ({
+    matches: false,
+    media,
+    onchange: null,
+    addListener() {},
+    removeListener() {},
+    addEventListener() {},
+    removeEventListener() {},
+    dispatchEvent: () => false,
+  });
+  Object.assign(window, {
+    matchMedia,
+    scrollTo() {},
+    maka: {
+      sessions: {
+        getPlanState: async () => planState,
+        subscribeEvents: () => () => {},
+        subscribePlanChanges: () => () => {},
+        requestPlanRevision: () => revision.promise,
+      },
+    },
+  });
+  Object.assign(globalThis, {
+    document,
+    window,
+    matchMedia,
+    HTMLElement: window.HTMLElement,
+    HTMLIFrameElement: window.HTMLIFrameElement ?? class HTMLIFrameElement {},
+    Event: window.Event,
+    Node: window.Node,
+    CSS: { escape: (value: string) => value },
+    requestAnimationFrame: (callback: FrameRequestCallback) => setTimeout(callback, 0),
+    cancelAnimationFrame: (handle: number) => clearTimeout(handle),
+    IS_REACT_ACT_ENVIRONMENT: true,
+  });
+  const container = document.querySelector('#root');
+  assert.ok(container);
+  const root = createRoot(container);
+  mountedRoot = root;
+
+  let controller: PlanModeState | undefined;
+  function Harness() {
+    controller = usePlanModeState({ id: 'session-1' } as SessionSummary);
+    return null;
+  }
+  await act(async () => {
+    root.render(createElement(LocaleProvider, {
+      locale: 'en',
+      children: createElement(AstryxLocaleProvider, {
+        children: createElement(ToastProvider, { children: createElement(Harness) }),
+      }),
+    }));
+  });
+  assert.equal(controller?.pending, false);
+
+  let inFlight!: Promise<void>;
+  await act(async () => {
+    inFlight = controller!.requestRevision('proposal-1');
+  });
+  assert.equal(controller?.pending, true, 'pending must hold while the bridge call is unsettled');
+
+  revision.resolve({ ok: true, value: planState });
+  await act(async () => {
+    await inFlight;
+  });
+  assert.equal(controller?.pending, false);
+});
diff --git a/apps/desktop/src/main/__tests__/runtime-host-guest-session-mounts.test.ts b/apps/desktop/src/main/__tests__/runtime-host-guest-session-mounts.test.ts
index c7a55ff1b4..a319a12ac9 100644
--- a/apps/desktop/src/main/__tests__/runtime-host-guest-session-mounts.test.ts
+++ b/apps/desktop/src/main/__tests__/runtime-host-guest-session-mounts.test.ts
@@ -331,7 +331,7 @@ test('removes failed activation desire instead of creating recoverable profile s
   });
 
   const result = await mounts.importInvitation(invitation('guest-two'), false, 'import-two');
-  assert.deepEqual(result.kind === 'error' ? result.reason : result.kind, 'peer_path_unavailable');
+  assert.deepEqual(result, { kind: 'error', reason: 'peer_path_unavailable' });
   assert.deepEqual(await store.read(), []);
   assert.equal(unmounted.length, 1);
 });
@@ -740,6 +740,25 @@ test('does not lose a catalog invalidation that races Guest activation', async (
   await mounts.close();
 });
 
+test('logs unexpected activation failures without returning Host details', async (context) => {
+  const diagnostics: string[] = [];
+  context.mock.method(console, 'error', (...values: unknown[]) => {
+    diagnostics.push(values.map(String).join(' '));
+  });
+  const mounts = service(memoryStore(), {
+    mount: async () => {
+      throw new Error('Authorization: Bearer very-secret-token');
+    },
+  });
+
+  const result = await mounts.importInvitation(invitation('guest-failed'), false, 'import-failed');
+
+  assert.deepEqual(result, { kind: 'error', reason: 'connection_failed' });
+  assert.equal(diagnostics.length, 1);
+  assert.match(diagnostics[0]!, /session-collaboration.*import failed/u);
+  assert.doesNotMatch(diagnostics[0]!, /very-secret-token/u);
+});
+
 test('settles admitted finalization before committing unmount desire', async () => {
   const store = memoryStore();
   let started!: () => void;
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 91fd55a180..4b1d67b178 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
@@ -22,7 +22,7 @@ import test from 'node:test';
 import type { IpcMain } from 'electron';
 import { projectDeepResearchClientProgress } from '@maka/core/deep-research-client-progress';
 import { type DeepResearchRun } from '@maka/core/deep-research-run';
-import { type PlanSessionState } from '@maka/core/plan';
+import { emptyPlanSessionState, type PlanSessionState } from '@maka/core/plan';
 import { type ShellRunUpdate } from '@maka/core/events';
 import {
   encodeDeepResearchSnapshot,
@@ -887,35 +887,32 @@ test('adapts Plan controls and starts approved execution through one Host comman
     ipc,
   );
 
-  assert.deepEqual(await ipc.invoke('plan-mode:requestRevision', 'session-1', 'proposal-1'), state);
+  assert.deepEqual(
+    await ipc.invoke('plan-mode:requestRevision', 'session-1', 'proposal-1'),
+    { ok: true, value: state },
+  );
   const approvalInput = {
     proposalId: 'proposal-1',
     expectedRevision: 2,
     expectedStoreVersion: 3,
     turnId: 'approval-turn',
   };
-  assert.deepEqual(
-    await ipc.invoke('plan-mode:approve', 'session-1', approvalInput),
-    { turnId: 'approval-turn', executionId: 'execution-1' },
-  );
-  assert.deepEqual(
-    await ipc.invoke('plan-mode:approve', 'session-1', approvalInput),
-    { turnId: 'approval-turn', executionId: 'execution-1' },
-  );
-  assert.deepEqual(
-    await ipc.invoke('plan-mode:resume', 'session-1', 'execution-1', 'resume-turn'),
-    {
-      turnId: 'resume-turn',
-      executionId: 'execution-1',
-    },
-  );
-  assert.deepEqual(
-    await ipc.invoke('plan-mode:resume', 'session-1', 'execution-1', 'resume-turn'),
-    {
-      turnId: 'resume-turn',
-      executionId: 'execution-1',
-    },
-  );
+  assert.deepEqual(await ipc.invoke('plan-mode:approve', 'session-1', approvalInput), {
+    ok: true,
+    value: { turnId: 'approval-turn', executionId: 'execution-1' },
+  });
+  assert.deepEqual(await ipc.invoke('plan-mode:approve', 'session-1', approvalInput), {
+    ok: true,
+    value: { turnId: 'approval-turn', executionId: 'execution-1' },
+  });
+  assert.deepEqual(await ipc.invoke('plan-mode:resume', 'session-1', 'execution-1', 'resume-turn'), {
+    ok: true,
+    value: { turnId: 'resume-turn', executionId: 'execution-1' },
+  });
+  assert.deepEqual(await ipc.invoke('plan-mode:resume', 'session-1', 'execution-1', 'resume-turn'), {
+    ok: true,
+    value: { turnId: 'resume-turn', executionId: 'execution-1' },
+  });
   assert.deepEqual(calls, [
     {
       kind: 'control',
@@ -1407,3 +1404,118 @@ function registerDomainsIpc(
     ipcMain,
   );
 }
+
+test('plan control channels rethrow failures outside the expected plan-control set', async () => {
+  const ipc = ipcHarness();
+  const boom = new Error('socket exploded');
+  registerDomainsIpc({
+    client: domainClient({
+      getPlanState: async () => emptyPlanSessionState('session-1'),
+      controlPlan: async () => {
+        throw boom;
+      },
+      startPlanTurn: async () => {
+        throw boom;
+      },
+    }),
+    emitModeChanged: () => {},
+    newId: () => 'fixed-id',
+  }, ipc);
+  await assert.rejects(
+    () => ipc.invoke('plan-mode:requestRevision', 'session-1', 'proposal-1'),
+    (error: unknown) => error === boom,
+  );
+  await assert.rejects(
+    () => ipc.invoke('plan-mode:abandon', 'session-1', 'proposal-1'),
+    (error: unknown) => error === boom,
+  );
+  await assert.rejects(
+    () => ipc.invoke('plan-mode:approve', 'session-1', {
+      proposalId: 'proposal-1',
+      expectedRevision: 2,
+      expectedStoreVersion: 3,
+      turnId: 'turn-1',
+    }),
+    (error: unknown) => error === boom,
+  );
+  await assert.rejects(
+    () => ipc.invoke('plan-mode:resume', 'session-1', 'execution-1', 'turn-1'),
+    (error: unknown) => error === boom,
+  );
+  await assert.rejects(
+    () => ipc.invoke('plan-mode:abandonExecution', 'session-1', 'execution-1'),
+    (error: unknown) => error === boom,
+  );
+});
+
+test('plan control channels return the Host error code across the IPC boundary', async () => {
+  const cases = [
+    {
+      channel: 'plan-mode:requestRevision',
+      args: ['session-1', 'proposal-1'],
+      operation: 'plan.control',
+      code: 'session_busy',
+    },
+    {
+      channel: 'plan-mode:approve',
+      args: [
+        'session-1',
+        { proposalId: 'proposal-1', expectedRevision: 2, expectedStoreVersion: 3, turnId: 'turn-1' },
+      ],
+      operation: 'plan.turn.start',
+      code: 'operation_conflict',
+    },
+    {
+      channel: 'plan-mode:resume',
+      args: ['session-1', 'execution-1', 'turn-1'],
+      operation: 'plan.turn.start',
+      code: 'session_busy',
+    },
+    {
+      channel: 'plan-mode:abandonExecution',
+      args: ['session-1', 'execution-1'],
+      operation: 'plan.control',
+      code: 'persistence_failed',
+    },
+  ] as const;
+  for (const scenario of cases) {
+    const ipc = ipcHarness();
+    const changed: string[] = [];
+    const reject = async () => {
+      throw new RuntimeHostOperationError(scenario.operation, scenario.code, 'Host refused the plan control');
+    };
+    registerDomainsIpc({
+      client: domainClient({
+        getPlanState: async () => emptyPlanSessionState('session-1'),
+        controlPlan: reject,
+        startPlanTurn: reject,
+      }),
+      emitModeChanged: (sessionId) => changed.push(sessionId),
+      newId: () => 'fixed-id',
+    }, ipc);
+    assert.deepEqual(
+      await ipc.invoke(scenario.channel, ...scenario.args),
+      { ok: false, error: { code: scenario.code, message: 'Host refused the plan control' } },
+      `${scenario.channel} must carry the Host error code`,
+    );
+    assert.deepEqual(changed, [], `${scenario.channel} must not report a mode change`);
+  }
+});
+
+test('the plan proposal exit channel rejects instead of returning an envelope', async () => {
+  const ipc = ipcHarness();
+  const changed: string[] = [];
+  const cause = new RuntimeHostOperationError('plan.control', 'operation_conflict', 'Host refused the plan control');
+  registerDomainsIpc({
+    client: domainClient({
+      getPlanState: async () => emptyPlanSessionState('session-1'),
+      controlPlan: async () => {
+        throw cause;
+      },
+    }),
+    emitModeChanged: (sessionId) => changed.push(sessionId),
+    newId: () => 'fixed-id',
+  }, ipc);
+  await assert.rejects(() => ipc.invoke('plan-mode:abandon', 'session-1', 'proposal-1'), (error) => error === cause);
+  assert.deepEqual(changed, []);
+});
diff --git a/apps/desktop/src/main/__tests__/session-local.test.ts b/apps/desktop/src/main/__tests__/session-local.test.ts
index fbfff3e121..8c5b7f8677 100644
--- a/apps/desktop/src/main/__tests__/session-local.test.ts
+++ b/apps/desktop/src/main/__tests__/session-local.test.ts
@@ -617,7 +617,7 @@ test('local submit preserves picked-file approvals until durable admission succe
         'current_turn',
         { ...draft, messageId: 'too-large', attachmentItems: largePicked },
       ),
-    /附件总量超出大小限制/,
+    /attachment_ingest:total_size_exceeded/,
   );
   assert.equal(resizeCalls, 0);
   assert.equal(store.get('authority', 'too-large'), undefined);
diff --git a/apps/desktop/src/main/__tests__/work-board-ipc-main.test.ts b/apps/desktop/src/main/__tests__/work-board-ipc-main.test.ts
index aaabba2f51..3ad8a30af6 100644
--- a/apps/desktop/src/main/__tests__/work-board-ipc-main.test.ts
+++ b/apps/desktop/src/main/__tests__/work-board-ipc-main.test.ts
@@ -23,6 +23,7 @@ import { tmpdir } from 'node:os';
 import { join } from 'node:path';
 import { describe, test } from 'node:test';
 import type { IpcMain } from 'electron';
+import { createWorkBoardStore, WorkBoardStoreError } from '@maka/storage/work-board-store';
 import {
   registerWorkBoardIpc,
   type WorkBoardChangedEvent,
@@ -88,6 +89,54 @@ async function withTempRoot(run: (root: string) => Promise<void>): Promise<void>
 }
 
 describe('Work Board IPC', () => {
+  for (const [code, detail] of [
+    ['corrupt_record', 'Work Board item item-1 has invalid record_json'],
+    ['corrupt_record', 'Work Board item item-2 failed contract validation'],
+    ['corrupt_record', 'Work Board item item-3 has indexed columns that disagree with record_json'],
+    ['unknown', 'Database read failed'],
+    ['invalid_input', 'Work Board item id is invalid'],
+    ['not_found', 'Work Board item item-4 was not found'],
+    ['operation_conflict', 'Work Board item revision changed'],
+    ['must_archive_first', 'Only archived Work Board items can be deleted'],
+  ] as const) {
+    test(`returns only ${code} and preserves diagnostic policy: ${detail}`, async (t) => {
+      await withTempRoot(async (root) => {
+        const ipc = createFakeIpcMain();
+        const window = createFakeWindowController();
+        const store = createWorkBoardStore(root);
+        const message = `${detail}; token=board-diagnostic-secret`;
+        t.mock.method(store, 'list', async () => {
+          throw code === 'unknown' ? new Error(message) : new WorkBoardStoreError(code, message);
+        });
+        const logger = t.mock.method(console, 'error', () => {});
+        const registration = registerWorkBoardIpc({
+          ipcMain: ipc as unknown as Pick<IpcMain, 'handle'>,
+          workspaceRoot: root,
+          mainWindowController: window,
+          store,
+        });
+        try {
+          const response = await ipc.invoke<WorkBoardIpcResult<unknown>>('workBoard:list', {});
+          assert.deepEqual(response, { ok: false, error: { code } });
+          assert.deepEqual(window.events, []);
+          if (code === 'corrupt_record' || code === 'unknown') {
+            assert.equal(logger.mock.callCount(), 1);
+            const [prefix, diagnostic] = logger.mock.calls[0]!.arguments;
+            assert.equal(prefix, '[work-board] operation failed:');
+            assert.equal(typeof diagnostic, 'string');
+            assert.ok(String(diagnostic).includes(detail));
+            assert.match(String(diagnostic), /token=\[redacted\]/);
+            assert.doesNotMatch(JSON.stringify(logger.mock.calls), /board-diagnostic-secret/);
+          } else {
+            assert.equal(logger.mock.callCount(), 0);
+          }
+        } finally {
+          registration.close();
+        }
+      });
+    });
+  }
+
   test('creates and lists items and emits change signals', async () => {
     await withTempRoot(async (root) => {
       const ipc = createFakeIpcMain();
@@ -164,8 +213,10 @@ describe('Work Board IPC', () => {
           { title: 'stale write' },
           { expectedRevision: 1 },
         );
-        assert.equal(staleRename.ok, false);
-        if (!staleRename.ok) assert.equal(staleRename.code, 'operation_conflict');
+        assert.deepEqual(staleRename, {
+          ok: false,
+          error: { code: 'operation_conflict' },
+        });
 
       const removedBeforeArchive = await ipc.invoke<WorkBoardIpcResult<null>>(
         'workBoard:remove',
@@ -173,7 +224,7 @@ describe('Work Board IPC', () => {
       );
       assert.equal(removedBeforeArchive.ok, false);
       if (!removedBeforeArchive.ok) {
-        assert.equal(removedBeforeArchive.code, 'must_archive_first');
+        assert.equal(removedBeforeArchive.error.code, 'must_archive_first');
       }
 
       const archived = await ipc.invoke<
@@ -194,14 +245,14 @@ describe('Work Board IPC', () => {
         { titel: 'x' },
       );
       assert.equal(invalidPatch.ok, false);
-      if (!invalidPatch.ok) assert.equal(invalidPatch.code, 'invalid_input');
+      if (!invalidPatch.ok) assert.equal(invalidPatch.error.code, 'invalid_input');
 
       const invalidCreate = await ipc.invoke<WorkBoardIpcResult<unknown>>(
         'workBoard:create',
         { ...itemInput(), notes: null },
       );
       assert.equal(invalidCreate.ok, false);
-      if (!invalidCreate.ok) assert.equal(invalidCreate.code, 'invalid_input');
+      if (!invalidCreate.ok) assert.equal(invalidCreate.error.code, 'invalid_input');
 
       await ipc.invoke('workBoard:archive', id);
       const removed = await ipc.invoke<WorkBoardIpcResult<null>>('workBoard:remove', id);
diff --git a/apps/desktop/src/main/__tests__/workhub-controller-fixture.ts b/apps/desktop/src/main/__tests__/workhub-controller-fixture.ts
index 4f859c6817..e0dcbb3c1a 100644
--- a/apps/desktop/src/main/__tests__/workhub-controller-fixture.ts
+++ b/apps/desktop/src/main/__tests__/workhub-controller-fixture.ts
@@ -222,7 +222,7 @@ export async function runRoutingComparison(input: {
       try {
         for (const entry of input.cases) {
           const start = proposals.length;
-          const result = await controller.submit({ requestId: `${repetition}:${routingStrategy.strategyId}:${entry.caseId}`, text: entry.text });
+          const result = await controller.submit({ newSessionFallbackTitle: 'New work', requestId: `${repetition}:${routingStrategy.strategyId}:${entry.caseId}`, text: entry.text });
           observations.push({ repetition, caseId: entry.caseId, result, proposals: proposals.slice(start) });
         }
       } finally { await conversation.close(); }
diff --git a/apps/desktop/src/main/__tests__/workhub-controller.test.ts b/apps/desktop/src/main/__tests__/workhub-controller.test.ts
index cddaaeb527..98b68f833d 100644
--- a/apps/desktop/src/main/__tests__/workhub-controller.test.ts
+++ b/apps/desktop/src/main/__tests__/workhub-controller.test.ts
@@ -32,7 +32,7 @@ import {
 } from '../../renderer/workhub-controller.js';
 import {
   createWorkHubRoutePolicy,
-  workHubNewSessionName,
+  workHubNewSessionName as workHubNewSessionNameForLocale,
 } from '../../renderer/features/workhub/index.js';
 import {
   createWorkHubR24RoutingStrategy,
@@ -42,6 +42,8 @@ import {
   type WorkHubRoutingStrategy,
 } from '../../renderer/features/workhub/index.js';
 
+const workHubNewSessionName = (text: string) => workHubNewSessionNameForLocale(text, '新工作');
+
 const appShellUrl = [
   new URL('../../renderer/app-shell.tsx', import.meta.url),
   new URL('../../../src/renderer/app-shell.tsx', import.meta.url),
@@ -217,7 +219,7 @@ test('direct stop bypasses routing candidates and preserves a not_owned delegati
   });
   const handle = await controller.openConversation(() => undefined, () => undefined);
 
-  const result = await controller.submit({ requestId: 'stop-1', text: 'Stop Payments' });
+  const result = await controller.submit({ newSessionFallbackTitle: 'New work', requestId: 'stop-1', text: 'Stop Payments' });
   assert.deepEqual(result, {
     kind: 'stop',
     strategyId: WORKHUB_ROUTING_STRATEGY_ID,
@@ -240,7 +242,7 @@ test('direct stop bypasses routing candidates and preserves a not_owned delegati
   }]);
   assert.equal(candidateReads, 0);
 
-  const retry = await controller.submit({ requestId: 'stop-2', text: 'Stop Payments' });
+  const retry = await controller.submit({ newSessionFallbackTitle: 'New work', requestId: 'stop-2', text: 'Stop Payments' });
   assert.equal(retry.kind, 'stop');
   assert.equal(actions.length, 2);
   await handle.close();
@@ -261,7 +263,7 @@ test('an anaphoric stop asks for a fresh named imperative without offering a rou
     },
   });
   const handle = await controller.openConversation(() => undefined, () => undefined);
-  assert.deepEqual(await controller.submit({ requestId: 'stop-it', text: 'Stop it' }), {
+  assert.deepEqual(await controller.submit({ newSessionFallbackTitle: 'New work', requestId: 'stop-it', text: 'Stop it' }), {
     kind: 'clarification',
     strategyId: WORKHUB_ROUTING_STRATEGY_ID,
     requestId: 'stop-it',
@@ -300,7 +302,7 @@ test('a named resume submits and reports what the Host did', async () => {
   });
   const handle = await controller.openConversation(() => undefined, () => undefined);
 
-  const result = await controller.submit({ requestId: 'resume-1', text: 'Resume Payments' });
+  const result = await controller.submit({ newSessionFallbackTitle: 'New work', requestId: 'resume-1', text: 'Resume Payments' });
 
   assert.deepEqual(result, {
     kind: 'resume',
@@ -334,7 +336,7 @@ test('an anaphoric resume asks for a named work item', async () => {
   });
   const handle = await controller.openConversation(() => undefined, () => undefined);
 
-  assert.deepEqual(await controller.submit({ requestId: 'resume-it', text: 'Resume it' }), {
+  assert.deepEqual(await controller.submit({ newSessionFallbackTitle: 'New work', requestId: 'resume-it', text: 'Resume it' }), {
     kind: 'clarification',
     strategyId: WORKHUB_ROUTING_STRATEGY_ID,
     requestId: 'resume-it',
@@ -369,7 +371,7 @@ test('a resume the Host will not admit becomes its clarification', async () => {
   });
   const handle = await controller.openConversation(() => undefined, () => undefined);
 
-  assert.deepEqual(await controller.submit({ requestId: 'resume-2', text: 'Resume Payments' }), {
+  assert.deepEqual(await controller.submit({ newSessionFallbackTitle: 'New work', requestId: 'resume-2', text: 'Resume Payments' }), {
     kind: 'clarification',
     strategyId: WORKHUB_ROUTING_STRATEGY_ID,
     requestId: 'resume-2',
@@ -405,7 +407,7 @@ test('a resume identity conflict is not mislabeled as a missing target', async (
   const handle = await controller.openConversation(() => undefined, () => undefined);
 
   await assert.rejects(
-    controller.submit({ requestId: 'resume-conflict', text: 'Resume Payments' }),
+    controller.submit({ newSessionFallbackTitle: 'New work', requestId: 'resume-conflict', text: 'Resume Payments' }),
     (error) => error === conflict,
   );
   await handle.close();
@@ -434,7 +436,7 @@ test('a Runtime Host without safe-boundary resume explains why it cannot resume'
   });
   const handle = await controller.openConversation(() => undefined, () => undefined);
 
-  assert.deepEqual(await controller.submit({ requestId: 'resume-disabled', text: 'Resume Payments' }), {
+  assert.deepEqual(await controller.submit({ newSessionFallbackTitle: 'New work', requestId: 'resume-disabled', text: 'Resume Payments' }), {
     kind: 'clarification',
     strategyId: WORKHUB_ROUTING_STRATEGY_ID,
     requestId: 'resume-disabled',
@@ -465,7 +467,7 @@ test('a recovering Runtime Host tells the user to retry resume', async () => {
   });
   const handle = await controller.openConversation(() => undefined, () => undefined);
 
-  const result = await controller.submit({ requestId: 'resume-recovering', text: 'Resume Payments' });
+  const result = await controller.submit({ newSessionFallbackTitle: 'New work', requestId: 'resume-recovering', text: 'Resume Payments' });
   assert.equal(result.kind, 'clarification');
   if (result.kind === 'clarification') assert.equal(result.reason, 'resume_host_recovering');
   await handle.close();
@@ -497,7 +499,7 @@ test('a named stop reports the Gate refusal instead of judging the target itself
   });
   const handle = await controller.openConversation(() => undefined, () => undefined);
 
-  assert.deepEqual(await controller.submit({ requestId: 'stop-payments', text: 'Stop Payments' }), {
+  assert.deepEqual(await controller.submit({ newSessionFallbackTitle: 'New work', requestId: 'stop-payments', text: 'Stop Payments' }), {
     kind: 'clarification',
     strategyId: WORKHUB_ROUTING_STRATEGY_ID,
     requestId: 'stop-payments',
@@ -528,7 +530,7 @@ test('a stop that fails for any other reason is a fault, not a clarification', a
   const handle = await controller.openConversation(() => undefined, () => undefined);
 
   await assert.rejects(
-    () => controller.submit({ requestId: 'stop-payments', text: 'Stop Payments' }),
+    () => controller.submit({ newSessionFallbackTitle: 'New work', requestId: 'stop-payments', text: 'Stop Payments' }),
     /WorkHub stop state is unavailable/,
   );
   await handle.close();
@@ -575,7 +577,7 @@ test('stop-shaped ordinary work routes normally instead of looping on clarificat
     });
     const handle = await controller.openConversation(() => undefined, () => undefined);
 
-    const result = await controller.submit({ requestId: `work-${sessionName}`, text });
+    const result = await controller.submit({ newSessionFallbackTitle: 'New work', requestId: `work-${sessionName}`, text });
     assert.equal(result.kind, 'submitted', text);
     assert.deepEqual(
       actions.map((action) => action.proposal.disposition),
@@ -683,6 +685,7 @@ test('archived Sessions stay inspectable but are excluded from routing targets',
 
   const projection = await controller.read();
   const result = await controller.submit({
+    newSessionFallbackTitle: '新工作',
     requestId: 'archived-target',
     text: '支付回调幂等性现在是什么状态?',
   });
@@ -703,6 +706,7 @@ test('submit sends an explicitly targeted request to that Session', async () =>
   const controller = createWorkHubController({ sessions });
 
   const result = await controller.submit({
+    newSessionFallbackTitle: '新工作',
     requestId: 'request-1',
     text: '补充重复投递测试',
     explicitTarget: { sessionId: 'payment' },
@@ -734,6 +738,7 @@ test('submit routes a unique complete Session name without asking', async () =>
   const controller = createWorkHubController({ sessions });
 
   const result = await controller.submit({
+    newSessionFallbackTitle: '新工作',
     requestId: 'request-exact',
     text: '在支付回调幂等性里补充重复投递测试',
   });
@@ -770,6 +775,7 @@ test('an injected R3 strategy still delegates through the shared controller and
   const controller = createWorkHubController({ sessions, routingStrategy });
 
   const result = await controller.submit({
+    newSessionFallbackTitle: 'New work',
     requestId: 'request-r3-a',
     text: '请实现账本边界检查器',
   });
@@ -797,6 +803,7 @@ test('a unique longer Session name outranks a generic contained Session name', a
   };
 
   const result = await createWorkHubController({ sessions }).submit({
+    newSessionFallbackTitle: '新工作',
     requestId: 'request-layout',
     text: '优化WorkHub移动端消息布局:补充横屏注意点。',
   });
@@ -821,6 +828,7 @@ test('a short Latin Session name does not match inside another word', async () =
   };
 
   const result = await createWorkHubController({ sessions }).submit({
+    newSessionFallbackTitle: '新工作',
     requestId: 'request-parser',
     text: '修复 repair parser 的错误',
   });
@@ -847,6 +855,7 @@ test('a one-character Latin discriminator prevents routing to a different Sessio
     };
 
     const result = await createWorkHubController({ sessions }).submit({
+    newSessionFallbackTitle: '新工作',
       requestId: `request-${requestedName}`,
       text: `请处理 ${requestedName} 的问题`,
     });
@@ -879,6 +888,7 @@ test('submit asks the user when weak relevance matches more than one Session', a
   const controller = createWorkHubController({ sessions });
 
   const result = await controller.submit({
+    newSessionFallbackTitle: '新工作',
     requestId: 'request-ambiguous',
     text: '继续处理重复问题',
   });
@@ -930,12 +940,14 @@ test('submit keeps origin prompts as stable evidence after latest results change
   sessions.submit = async () => ({ turnId: 'turn-focus-login' });
   const controller = createWorkHubController({ sessions });
   await controller.submit({
+    newSessionFallbackTitle: '新工作',
     requestId: 'request-focus-login',
     text: '先看登录',
     explicitTarget: { sessionId: 'login' },
   });
 
   const result = await controller.submit({
+    newSessionFallbackTitle: '新工作',
     requestId: 'request-origin-ambiguity',
     text: '继续处理重复问题',
   });
@@ -966,7 +978,7 @@ test('submit creates a new executable topic instead of following one weak old cl
   const controller = createWorkHubController({ sessions });
   const text = '检查支付回调重复投递时的幂等性,先只分析风险和测试点,不修改文件。';
 
-  const result = await controller.submit({ requestId: 'request-payment-new', text });
+  const result = await controller.submit({ newSessionFallbackTitle: '新工作', requestId: 'request-payment-new', text });
 
   assert.equal(result.kind, 'submitted');
   assert.deepEqual(result.kind === 'submitted' ? result.target : undefined, {
@@ -996,7 +1008,7 @@ test('submit does not treat a project name as strong topic evidence', async () =
   const controller = createWorkHubController({ sessions });
   const text = '优化 WorkHub 在移动端窄屏下的消息布局,先给设计建议,不修改文件。';
 
-  const result = await controller.submit({ requestId: 'request-layout-new', text });
+  const result = await controller.submit({ newSessionFallbackTitle: '新工作', requestId: 'request-layout-new', text });
 
   assert.equal(result.kind, 'submitted');
   assert.deepEqual(result.kind === 'submitted' ? result.target : undefined, {
@@ -1018,12 +1030,14 @@ test('submit follows an unambiguous reference to the most recent Work', async ()
   };
   const controller = createWorkHubController({ sessions });
   await controller.submit({
+    newSessionFallbackTitle: '新工作',
     requestId: 'request-focus',
     text: '先处理支付',
     explicitTarget: { sessionId: 'payment' },
   });
 
   const result = await controller.submit({
+    newSessionFallbackTitle: '新工作',
     requestId: 'request-pronoun',
     text: '继续它',
   });
@@ -1054,10 +1068,12 @@ test('read seeds current and previous focus from pre-existing ordinary Sessions'
 
   await controller.read();
   const current = await controller.submit({
+    newSessionFallbackTitle: '新工作',
     requestId: 'request-current-seed',
     text: '继续这个工作',
   });
   const previous = await controller.submit({
+    newSessionFallbackTitle: '新工作',
     requestId: 'request-previous-seed',
     text: '回到上一个工作',
   });
@@ -1085,6 +1101,7 @@ test('read prefers the Session active when WorkHub opens over raw recency', asyn
 
   await controller.read({ focus: { sessionId: 'login' } });
   const result = await controller.submit({
+    newSessionFallbackTitle: '新工作',
     requestId: 'request-active-seed',
     text: '继续这个工作',
   });
@@ -1128,6 +1145,7 @@ test('a stale opening read cannot overwrite a newer WorkHub focus', async () =>
   await older;
   sessions.list = async () => facts;
   const result = await controller.submit({
+    newSessionFallbackTitle: '新工作',
     requestId: 'request-after-stale-read',
     text: '继续这个工作',
   });
@@ -1153,6 +1171,7 @@ test('an unavailable opening focus falls back to recent routable Sessions', asyn
 
   await controller.read({ focus: { sessionId: 'archived' } });
   const result = await controller.submit({
+    newSessionFallbackTitle: '新工作',
     requestId: 'request-fallback-focus',
     text: '继续这个工作',
   });
@@ -1182,6 +1201,7 @@ test('focus falls back when the current Session is archived after WorkHub opens'
     : entry);
 
   const result = await controller.submit({
+    newSessionFallbackTitle: '新工作',
     requestId: 'request-after-current-archive',
     text: '继续这个工作',
   });
@@ -1213,6 +1233,7 @@ test('resetVisitContext discards focus from a previous WorkHub mount', async ()
   await controller.read();
 
   const result = await controller.submit({
+    newSessionFallbackTitle: '新工作',
     requestId: 'request-after-remount',
     text: '继续这个工作',
   });
@@ -1242,6 +1263,7 @@ test('an in-flight submit cannot restore visit focus after WorkHub unmounts', as
   const controller = createWorkHubController({ sessions });
   await controller.read({ focus: { sessionId: 'login' } });
   const inFlight = controller.submit({
+    newSessionFallbackTitle: '新工作',
     requestId: 'request-before-unmount',
     text: '继续这个工作',
   });
@@ -1257,6 +1279,7 @@ test('an in-flight submit cannot restore visit focus after WorkHub unmounts', as
   };
   await controller.read();
   const result = await controller.submit({
+    newSessionFallbackTitle: '新工作',
     requestId: 'request-after-in-flight',
     text: '继续这个工作',
   });
@@ -1298,6 +1321,7 @@ test('an old submit resolves against the visit focus captured before an await',
   };
 
   const oldSubmission = controller.submit({
+    newSessionFallbackTitle: '新工作',
     requestId: 'request-old-visit',
     text: '继续这个工作',
   });
@@ -1331,12 +1355,14 @@ test('submit routes strong core evidence instead of reusing recent focus', async
   };
   const controller = createWorkHubController({ sessions });
   await controller.submit({
+    newSessionFallbackTitle: '新工作',
     requestId: 'request-login-focus',
     text: '先看登录',
     explicitTarget: { sessionId: 'login' },
   });
 
   const result = await controller.submit({
+    newSessionFallbackTitle: '新工作',
     requestId: 'request-topic-shift',
     text: '继续处理支付回调重复投递',
   });
@@ -1371,6 +1397,7 @@ test('submit routes unique strong core evidence without asking', async () => {
   const controller = createWorkHubController({ sessions });
 
   const result = await controller.submit({
+    newSessionFallbackTitle: '新工作',
     requestId: 'request-core',
     text: '刷新令牌过期时,重复登录的观测日志应该记录哪些字段?',
   });
@@ -1399,7 +1426,7 @@ test('submit ignores shared boilerplate when an executable request names a new t
   const controller = createWorkHubController({ sessions });
   const text = '请创建新任务,检查支付回调重复投递;先只分析风险和测试点,不修改文件。';
 
-  const result = await controller.submit({ requestId: 'request-new-topic', text });
+  const result = await controller.submit({ newSessionFallbackTitle: '新工作', requestId: 'request-new-topic', text });
 
   assert.equal(result.kind, 'submitted');
   if (result.kind !== 'submitted') return;
@@ -1417,6 +1444,7 @@ test('submit keeps a foreign two-character clue behind clarification', async ()
   const controller = createWorkHubController({ sessions });
 
   const result = await controller.submit({
+    newSessionFallbackTitle: '新工作',
     requestId: 'request-weak',
     text: '继续登录',
   });
@@ -1438,6 +1466,7 @@ test('submit treats explicit user uncertainty as clarification instead of a new
   const controller = createWorkHubController({ sessions });
 
   const result = await controller.submit({
+    newSessionFallbackTitle: '新工作',
     requestId: 'request-uncertain',
     text: '继续处理稳定性问题,但我不确定具体是哪一个。',
   });
@@ -1461,6 +1490,7 @@ test('English target uncertainty uses clarification as the routing safety valve'
   };
 
   const result = await createWorkHubController({ sessions }).submit({
+    newSessionFallbackTitle: '新工作',
     requestId: 'english-uncertainty',
     text: "I'm not sure which one this belongs to; continue the cleanup.",
   });
@@ -1488,6 +1518,7 @@ test('English routing matches whole words instead of substrings in another ident
   };
 
   const result = await createWorkHubController({ sessions }).submit({
+    newSessionFallbackTitle: '新工作',
     requestId: 'english-word-boundary',
     text: 'check the file parser',
   });
@@ -1516,6 +1547,7 @@ test('English core evidence requires a distinctive word or multiple whole-word m
   };
 
   const result = await createWorkHubController({ sessions }).submit({
+    newSessionFallbackTitle: '新工作',
     requestId: 'english-core-evidence',
     text: 'fix the parser tokenizer crash',
   });
@@ -1540,6 +1572,7 @@ test('waiting Session rejects a second root request without calling submit', asy
   const controller = createWorkHubController({ sessions });
 
   const result = await controller.submit({
+    newSessionFallbackTitle: '新工作',
     requestId: 'request-waiting',
     text: '排查令牌过期重复登录问题:补充一条等待状态下的新请求。',
   });
@@ -1566,17 +1599,20 @@ test('submit returns to the previous focused Session', async () => {
   };
   const controller = createWorkHubController({ sessions });
   await controller.submit({
+    newSessionFallbackTitle: '新工作',
     requestId: 'request-login',
     text: '先看登录',
     explicitTarget: { sessionId: 'login' },
   });
   await controller.submit({
+    newSessionFallbackTitle: '新工作',
     requestId: 'request-payment',
     text: '再看支付',
     explicitTarget: { sessionId: 'payment' },
   });
 
   const result = await controller.submit({
+    newSessionFallbackTitle: '新工作',
     requestId: 'request-previous',
     text: '回到上一个工作',
   });
@@ -1604,12 +1640,14 @@ test('submit lets strong foreign core evidence override a vague focus word', asy
   };
   const controller = createWorkHubController({ sessions });
   await controller.submit({
+    newSessionFallbackTitle: '新工作',
     requestId: 'request-payment-focus',
     text: '先看支付',
     explicitTarget: { sessionId: 'payment' },
   });
 
   const result = await controller.submit({
+    newSessionFallbackTitle: '新工作',
     requestId: 'request-foreign-core',
     text: '继续处理刷新令牌过期',
   });
@@ -1647,6 +1685,7 @@ test('submit keeps unmatched non-executable conversation in WorkHub', async () =
   });
 
   const result = await controller.submit({
+    newSessionFallbackTitle: '新工作',
     requestId: 'request-discussion',
     text: '你觉得统一入口最重要的价值是什么?',
   });
@@ -1704,6 +1743,7 @@ test('production submission delegates only through the Runtime-owned candidate r
   });
 
   const result = await controller.submit({
+    newSessionFallbackTitle: '新工作',
     requestId: 'delegate-action',
     text: '继续支付工作',
     explicitTarget: { sessionId: 'payment' },
@@ -1758,6 +1798,7 @@ test('production retry reaches durable Action Gate replay while target is waitin
   });
 
   const result = await controller.submit({
+    newSessionFallbackTitle: '新工作',
     requestId: 'summary-recovery-action',
     text: '继续支付工作',
     explicitTarget: { sessionId: 'payment' },
@@ -1817,6 +1858,7 @@ test('production sends an explicit correction as a linked replacement', async ()
   });
 
   const result = await controller.submit({
+    newSessionFallbackTitle: '新工作',
     requestId: 'linked-correction',
     text: 'No, use target instead',
     explicitTarget: { sessionId: 'target' },
@@ -1956,11 +1998,13 @@ test('production natural-language corrections retain the prior delegation link',
   });
   await controller.read();
   await controller.submit({
+    newSessionFallbackTitle: '新工作',
     requestId: 'production-wrong-payment',
     text: '继续这个工作,补充验收项',
   });
 
   const corrected = await controller.submit({
+    newSessionFallbackTitle: '新工作',
     requestId: 'production-natural-correction',
     text: '不是这个,换成登录稳定性,补充刷新令牌失败判定',
   });
@@ -1968,7 +2012,7 @@ test('production natural-language corrections retain the prior delegation link',
 
   const [creationRequestId, creationText] = PRODUCTION_CORRECTION_CREATION_CASES[0];
   assert.equal(
-    (await controller.submit({ requestId: creationRequestId, text: creationText })).kind,
+    (await controller.submit({ newSessionFallbackTitle: '新工作', requestId: creationRequestId, text: creationText })).kind,
     'submitted',
   );
 
@@ -2016,7 +2060,7 @@ test('production correction-shaped creation stays create_new without an existing
   });
 
   for (const [requestId, text] of PRODUCTION_CORRECTION_CREATION_CASES) {
-    const result = await controller.submit({ requestId: `without-focus-${requestId}`, text });
+    const result = await controller.submit({ newSessionFallbackTitle: '新工作', requestId: `without-focus-${requestId}`, text });
     assert.equal(result.kind, 'submitted');
   }
 
@@ -2090,6 +2134,7 @@ test('production creation leaves Session identity and workspace authority to mai
   });
 
   const result = await controller.submit({
+    newSessionFallbackTitle: '新工作',
     requestId: 'create-action',
     text: '请创建新任务,检查支付回调重复投递。',
   });
@@ -2118,6 +2163,7 @@ test('submit treats a design question containing an action word as discussion',
   const controller = createWorkHubController({ sessions });
 
   const result = await controller.submit({
+    newSessionFallbackTitle: '新工作',
     requestId: 'request-design-question',
     text: '我们应该怎么实现统一入口?',
   });
@@ -2136,6 +2182,7 @@ test('an executable English request may contain what without becoming discussion
   sessions.submit = async () => ({ turnId: 'turn-parser-fix' });
 
   const result = await createWorkHubController({ sessions }).submit({
+    newSessionFallbackTitle: '新工作',
     requestId: 'english-what-object',
     text: 'fix what is broken in the parser',
   });
@@ -2160,6 +2207,7 @@ test('submit creates an ordinary Session for a clear unmatched executable goal',
   const controller = createWorkHubController({ sessions });
 
   const result = await controller.submit({
+    newSessionFallbackTitle: '新工作',
     requestId: 'request-new-work',
     text: '实现导出发票 PDF 功能',
   });
@@ -2191,6 +2239,7 @@ test('explicit new-Session intent outranks generic evidence from existing work',
   sessions.submit = async () => ({ turnId: 'turn-new-session' });
 
   const result = await createWorkHubController({ sessions }).submit({
+    newSessionFallbackTitle: '新工作',
     requestId: 'request-explicit-new',
     text: '创建一个全新的普通 Session,标题为 R2.3 新建工作验收,只记录测试计划。',
   });
@@ -2210,6 +2259,7 @@ test('English explicit creation extracts the requested Session name', async () =
   sessions.submit = async () => ({ turnId: 'turn-parser-cleanup' });
 
   const result = await createWorkHubController({ sessions }).submit({
+    newSessionFallbackTitle: '新工作',
     requestId: 'english-explicit-new',
     text: 'Create a new session called Parser Cleanup.',
   });
@@ -2347,6 +2397,7 @@ test('English routing boilerplate does not make an old analysis look related', a
   sessions.submit = async () => ({ turnId: 'turn-payment-new' });
 
   const result = await createWorkHubController({ sessions }).submit({
+    newSessionFallbackTitle: '新工作',
     requestId: 'english-boilerplate',
     text: "Check payment callback duplicate delivery; just analyze the risks and test cases; don't modify any files.",
   });
@@ -2366,10 +2417,12 @@ test('negated and deliberative creation language never creates a Session', async
   const controller = createWorkHubController({ sessions });
 
   const negated = await controller.submit({
+    newSessionFallbackTitle: '新工作',
     requestId: 'negated-create',
     text: '不要创建一个新任务,我们先讨论这个方向。',
   });
   const deliberative = await controller.submit({
+    newSessionFallbackTitle: '新工作',
     requestId: 'question-create',
     text: '是否应该新建一个任务?',
   });
@@ -2483,6 +2536,7 @@ test('polite executable questions and file-level constraints still create new wo
   for (const text of cases) {
     assert.equal(
       createWorkHubRoutePolicy().resolve({
+      newSessionFallbackTitle: '新工作',
         text,
         sessions: [],
         originPromptBySessionId: new Map(),
@@ -2509,6 +2563,7 @@ test('advisory how-to ambiguity asks for a direct instruction', () => {
   ]) {
     assert.deepEqual(
       createWorkHubRoutePolicy().resolve({
+      newSessionFallbackTitle: '新工作',
         text,
         sessions: [],
         originPromptBySessionId: new Map(),
@@ -2526,6 +2581,7 @@ test('advisory ambiguity overrides explicit, exact-name, and recent-focus routin
 
   assert.deepEqual(
     createWorkHubRoutePolicy().resolve({
+      newSessionFallbackTitle: '新工作',
       text,
       sessions: [login],
       originPromptBySessionId: new Map(),
@@ -2535,6 +2591,7 @@ test('advisory ambiguity overrides explicit, exact-name, and recent-focus routin
   );
   assert.deepEqual(
     createWorkHubRoutePolicy().resolve({
+      newSessionFallbackTitle: '新工作',
       text,
       sessions: [login],
       originPromptBySessionId: new Map(),
@@ -2546,6 +2603,7 @@ test('advisory ambiguity overrides explicit, exact-name, and recent-focus routin
   focusedPolicy.rememberTarget(login.target);
   assert.deepEqual(
     focusedPolicy.resolve({
+      newSessionFallbackTitle: '新工作',
       text: 'Explain how to diagnose this, then fix it.',
       sessions: [login],
       originPromptBySessionId: new Map(),
@@ -2588,6 +2646,7 @@ test('literal negator targets still create new work', () => {
   ]) {
     assert.equal(
       createWorkHubRoutePolicy().resolve({
+      newSessionFallbackTitle: '新工作',
         text,
         sessions: [],
         originPromptBySessionId: new Map(),
@@ -2649,6 +2708,7 @@ test('withdrawing the requested action keeps the input in WorkHub', () => {
   ]) {
     assert.equal(
       createWorkHubRoutePolicy().resolve({
+      newSessionFallbackTitle: '新工作',
         text,
         sessions: [],
         originPromptBySessionId: new Map(),
@@ -2675,6 +2735,7 @@ test('a later affirmative clause creates work after withdrawing an earlier actio
   ]) {
     assert.equal(
       createWorkHubRoutePolicy().resolve({
+      newSessionFallbackTitle: '新工作',
         text,
         sessions: [],
         originPromptBySessionId: new Map(),
@@ -2768,6 +2829,7 @@ test('a correction with a negated creation tail never proposes a new Session', (
     const policy = createWorkHubRoutePolicy();
     policy.rememberTarget(payment.target);
     const decision = policy.resolve({
+      newSessionFallbackTitle: '新工作',
       text,
       sessions: [login, payment],
       originPromptBySessionId: new Map(),
@@ -2785,6 +2847,7 @@ test('a pronoun correction uses the shared affirmative target span', () => {
 
   assert.deepEqual(
     policy.resolve({
+      newSessionFallbackTitle: '新工作',
       text: 'Not this session; move it to Payments',
       sessions: [source, payments],
       originPromptBySessionId: new Map(),
@@ -2815,6 +2878,7 @@ test('correction routing preserves quoted and punctuated Session identities', ()
     const policy = createWorkHubRoutePolicy();
     policy.rememberTarget(source.target);
     const decision = policy.resolve({
+      newSessionFallbackTitle: '新工作',
       text,
       sessions: [source, target],
       originPromptBySessionId: new Map(),
@@ -2853,6 +2917,7 @@ test('a negated existing-target correction never proposes destructive replacemen
     const policy = createWorkHubRoutePolicy();
     policy.rememberTarget(payment.target);
     const decision = policy.resolve({
+      newSessionFallbackTitle: '新工作',
       text,
       sessions: [login, payment],
       originPromptBySessionId: new Map(),
@@ -3071,6 +3136,7 @@ test('indirect questions containing action words stay in WorkHub', () => {
   ]) {
     assert.equal(
       createWorkHubRoutePolicy().resolve({
+      newSessionFallbackTitle: '新工作',
         text,
         sessions: [],
         originPromptBySessionId: new Map(),
@@ -3091,6 +3157,7 @@ test('a fuzzy correction target never becomes destructive routing authority', ()
   policy.rememberTarget(source.target);
 
   const decision = policy.resolve({
+      newSessionFallbackTitle: '新工作',
     text: '不是这个,换成支付页面',
     sessions: [source, paymentCallback],
     originPromptBySessionId: new Map(),
@@ -3135,6 +3202,7 @@ test('a candidate name cannot absorb unquoted withdrawal semantics', () => {
     const policy = createWorkHubRoutePolicy();
     policy.rememberTarget(source.target);
     const decision = policy.resolve({
+      newSessionFallbackTitle: '新工作',
       text,
       sessions: [source, candidate],
       originPromptBySessionId: new Map(),
@@ -3171,6 +3239,7 @@ test('malformed or unbound creation naming stays in WorkHub discussion', () => {
   ]) {
     assert.equal(
       createWorkHubRoutePolicy().resolve({
+      newSessionFallbackTitle: '新工作',
         text,
         sessions: [],
         originPromptBySessionId: new Map(),
@@ -3221,7 +3290,7 @@ for (const createStrategy of [createWorkHubR24RoutingStrategy, () => createWorkH
         },
       },
     });
-    const result = await controller.submit({ requestId: 'resume-strategy', text: 'Resume Payments' });
+    const result = await controller.submit({ newSessionFallbackTitle: 'New work', requestId: 'resume-strategy', text: 'Resume Payments' });
     assert.equal(result.kind, 'resume');
     assert.equal(result.strategyId, routingStrategy.strategyId);
     if (result.kind === 'resume') assert.equal(result.outcome, 'resume_started');
@@ -3232,7 +3301,7 @@ for (const makeStrategy of [createWorkHubR24RoutingStrategy, () => createWorkHub
   test(`all combinations preserve Policy exact naming outside model recall budget: ${makeStrategy().strategyId}`, async () => {
     const entries = Array.from({ length: 14 }, (_, i) => session(`work-${i}`, { sessionName: `任务编号${i}边界`, updatedAt: 14 - i }));
     const controller = createWorkHubController({ sessions: port(entries), routingStrategy: makeStrategy() });
-    const result = await controller.submit({ requestId: 'outside-recall-budget', text: '任务编号13边界:补充测试' });
+    const result = await controller.submit({ newSessionFallbackTitle: 'New work', requestId: 'outside-recall-budget', text: '任务编号13边界:补充测试' });
     assert.equal(result.kind, 'submitted');
     if (result.kind === 'submitted') assert.equal(result.target.sessionId, 'work-13');
   });
@@ -3253,7 +3322,7 @@ test('Policy freezes visit focus before awaiting replaceable Intent', async () =
     } } },
   });
   await controller.read({ focus: { sessionId: 'login' } });
-  const result = controller.submit({ requestId: 'frozen-focus', text: '继续它' });
+  const result = controller.submit({ newSessionFallbackTitle: 'New work', requestId: 'frozen-focus', text: '继续它' });
   await entered;
   await controller.read({ focus: { sessionId: 'payment' } });
   release();
@@ -3267,6 +3336,7 @@ test('deterministic routing preserves executable instructions after the model te
   sessions.create = async () => session('ledger');
   const controller = createWorkHubController({ sessions });
   const result = await controller.submit({
+    newSessionFallbackTitle: 'New work',
     requestId: 'long-executable-input',
     text: '背景资料:' + '日志内容。'.repeat(450) + '\n请实现账本边界检查器',
   });
@@ -3280,14 +3350,14 @@ test('composer defaults apply only to creation while attachments follow explicit
   const newWorkDefaults = { model: { llmConnectionId: 'chosen', llmConnectionSlug: 'chosen', model: 'chosen-model' }, permissionMode: 'bypass' as const };
   const attachments: NonNullable<WorkHubCoordinationActInput['attachments']> = [{ name: 'requirements.txt', kind: 'other', mimeType: 'text/plain', bytes: 12, ref: { kind: 'session_file', sessionId: 'maka_workhub_coordination', relativePath: 'file-1' } }];
   const existing = createWorkHubController({ sessions: port([session('payments')]), onAct: (input) => actions.push(input) });
-  await existing.submit({ requestId: 'explicit-composer', text: 'Continue payments', explicitTarget: { sessionId: 'payments' }, newWorkDefaults, attachments });
+  await existing.submit({ newSessionFallbackTitle: 'New work', requestId: 'explicit-composer', text: 'Continue payments', explicitTarget: { sessionId: 'payments' }, newWorkDefaults, attachments });
   assert.equal(actions[0]?.proposal.disposition, 'delegate_existing');
   assert.equal(actions[0]?.newWorkDefaults, undefined);
   assert.deepEqual(actions[0]?.attachments, attachments);
   const freshPort = port([]);
   freshPort.create = async () => session('created-work');
   const fresh = createWorkHubController({ sessions: freshPort, onAct: (input) => actions.push(input) });
-  await fresh.submit({ requestId: 'new-composer', text: 'Create a new Session for an accessibility audit', newWorkDefaults, attachments });
+  await fresh.submit({ newSessionFallbackTitle: 'New work', requestId: 'new-composer', text: 'Create a new Session for an accessibility audit', newWorkDefaults, attachments });
   assert.equal(actions[1]?.proposal.disposition, 'create_new');
   assert.deepEqual(actions[1]?.newWorkDefaults, newWorkDefaults);
   assert.deepEqual(actions[1]?.attachments, attachments);
diff --git a/apps/desktop/src/main/__tests__/workhub-routing-strategy.test.ts b/apps/desktop/src/main/__tests__/workhub-routing-strategy.test.ts
index fd34d9fd0d..3f1f5cd54d 100644
--- a/apps/desktop/src/main/__tests__/workhub-routing-strategy.test.ts
+++ b/apps/desktop/src/main/__tests__/workhub-routing-strategy.test.ts
@@ -64,7 +64,7 @@ async function run(strategy: WorkHubRoutingStrategy, raw = fixture()) {
   sessions.routingEvidence = async () => [...raw.originPromptBySessionId].map(([sessionId, originPrompt]) => ({ target: { sessionId }, originPrompt }));
   sessions.create = async ({ name }) => session('created', { sessionName: name });
   const controller = createWorkHubController({ sessions, routingStrategy: strategy });
-  return controller.submit({ requestId: 'combination', text: raw.text });
+  return controller.submit({ newSessionFallbackTitle: 'New work', requestId: 'combination', text: raw.text });
 }
 const model = {
   async decide(input: WorkHubModelRoutingRequest) {
@@ -217,10 +217,10 @@ test('Policy retains ambiguity, exact naming and focus with model recall in the
   const strategy = createWorkHubR3ARoutingStrategy({ model });
   assert.equal((await run(strategy, fixture('创建一个新任务,不过我还不确定是否要做'))).kind, 'clarification');
   const controller = createWorkHubController({ sessions: port(sessions.map((value) => session(value.target.sessionId, value))), routingStrategy: strategy });
-  const exact = await controller.submit({ requestId: 'exact', text: '登录刷新令牌:补充测试' });
+  const exact = await controller.submit({ newSessionFallbackTitle: 'New work', requestId: 'exact', text: '登录刷新令牌:补充测试' });
   assert.equal(exact.kind, 'submitted');
   if (exact.kind === 'submitted') assert.equal(exact.target.sessionId, 'login');
-  const focused = await controller.submit({ requestId: 'focused', text: '继续它' });
+  const focused = await controller.submit({ newSessionFallbackTitle: 'New work', requestId: 'focused', text: '继续它' });
   assert.equal(focused.kind, 'submitted');
   if (focused.kind === 'submitted') assert.equal(focused.target.sessionId, 'login');
 });
diff --git a/apps/desktop/src/main/__tests__/workhub-surface-flow.test.ts b/apps/desktop/src/main/__tests__/workhub-surface-flow.test.ts
index fceaff86c7..0644ad6553 100644
--- a/apps/desktop/src/main/__tests__/workhub-surface-flow.test.ts
+++ b/apps/desktop/src/main/__tests__/workhub-surface-flow.test.ts
@@ -32,6 +32,7 @@ import {
   submitWorkHubSurfaceInput,
   visibleWorkHubConversation,
   workHubAmbiguousCommandPrompt,
+  workHubCoordinationSummary,
   workHubSurfaceFailure,
   workHubSubmissionClearsDraft,
 } from '../../renderer/workhub-surface.js';
@@ -43,30 +44,37 @@ import {
   type WorkHubDelegationExecutionState,
   type WorkHubSubmitInput,
 } from '../../renderer/workhub-controller.js';
+import { ExpectedOperationError } from '../../renderer/application/contracts/operation-diagnostics.js';
+import { WorkHubCoordinationFailure } from '../../renderer/workhub-coordination-port.js';
 import { WorkHubSendLease } from '../../renderer/workhub-send-lease.js';
+import { getWorkHubCopy } from '../../renderer/locales/workhub-copy.js';
 import {
   createDesktopWorkHubSessionPort,
   type WorkHubDesktopSession,
 } from '../../renderer/workhub-session-port.js';
 
-test('surface turns Action Gate rejections into safe actionable failures', () => {
+test('surface turns Action Gate rejections into safe actionable failures', (context) => {
+  context.mock.method(console, 'error', () => undefined);
   assert.equal(
-    workHubSurfaceFailure(
-      new Error('WorkHub Session candidates changed; refresh before delegating'),
-    ),
+    workHubSurfaceFailure(new ExpectedOperationError('candidates_changed')),
     'candidates_changed',
   );
   assert.equal(
-    workHubSurfaceFailure(
-      new Error('WorkHub linked correction requires an active durable delegation'),
-    ),
+    workHubSurfaceFailure(new ExpectedOperationError('linked_correction_unavailable')),
     'linked_correction_unavailable',
   );
   assert.equal(
-    workHubSurfaceFailure(new Error('Target Session is waiting for user input')),
+    workHubSurfaceFailure(new WorkHubCoordinationFailure('session_busy', 'Host diagnostic')),
     'target_waiting',
   );
-  assert.equal(workHubSurfaceFailure(new Error('private transport detail')), 'delivery_failed');
+  assert.equal(
+    workHubSurfaceFailure(new WorkHubCoordinationFailure('operation_conflict', 'Host diagnostic')),
+    'action_changed',
+  );
+  assert.equal(
+    workHubSurfaceFailure(new Error('WorkHub Session candidates changed; private detail')),
+    'delivery_failed',
+  );
 });
 
 test('surface route gate rejects same-frame duplicate operations and reopens after settle', async () => {
@@ -284,6 +292,31 @@ test('surface keeps the Composer draft when routing fails or the target is waiti
   );
 });
 
+for (const [locale, expected] of [
+  ['zh-CN', '这项工作正在等待你的决定。新请求尚未发送;处理原 Session 中的交互后可以再次发送。'],
+  ['zh-TW', '這項工作正在等待你的決定。新請求尚未傳送;處理原 Session 中的互動後可以再次傳送。'],
+  ['en', 'This work is waiting for your decision. The new request was not sent. Resolve the interaction in its Session, then send again.'],
+] as const) {
+  test(`${locale} waiting summary is a complete message independent of the separate UI paragraphs`, () => {
+    const result = {
+      kind: 'waiting' as const,
+      strategyId: WORKHUB_ROUTING_STRATEGY_ID,
+      requestId: 'waiting',
+      text: 'Continue payments',
+      target: { sessionId: 'payment' },
+    };
+    const projection = { sessions: [], turns: [] };
+    const copy = getWorkHubCopy(locale);
+
+    assert.equal(workHubCoordinationSummary(result, projection, copy), expected);
+    assert.equal(workHubCoordinationSummary(result, projection, {
+      ...copy,
+      waitingForDecision: 'Standalone status paragraph',
+      requestNotSent: 'Standalone retry hint',
+    }), expected);
+  });
+}
+
 test('surface replaces a local discussion placeholder with its durable model answer', () => {
   const local = [
     {
@@ -449,13 +482,14 @@ test('surface keeps clarification and successful routing in WorkHub', async () =
 
   const clarification = await submitWorkHubSurfaceInput({
     controller,
-    input: { requestId: 'request-1', text: '继续处理重复问题' },
+    input: { newSessionFallbackTitle: '新工作', requestId: 'request-1', text: '继续处理重复问题' },
   });
   assert.equal(clarification.kind, 'clarification');
 
   const submitted = await submitWorkHubSurfaceInput({
     controller,
     input: {
+      newSessionFallbackTitle: '新工作',
       requestId: 'request-1',
       text: '继续处理重复问题',
       explicitTarget: { sessionId: 'payment' },
@@ -514,7 +548,7 @@ test('ambiguous creation is durably clarified before a fresh imperative creates
   for (const [index, text] of ambiguousTexts.entries()) {
     const ambiguous = await submitAndRecordWorkHubSurfaceInput({
       controller,
-      request: { requestId: `ambiguous-request-${index}`, text },
+      request: { newSessionFallbackTitle: '新工作', requestId: `ambiguous-request-${index}`, text },
       recordedUserText: text,
       summary: () => workHubAmbiguousCommandPrompt('en'),
       onSummaryError: () => assert.fail('clarification should be durable'),
@@ -537,7 +571,7 @@ test('ambiguous creation is durably clarified before a fresh imperative creates
 
   const submitted = await submitAndRecordWorkHubSurfaceInput({
     controller,
-    request: { requestId: 'direct-request', text: 'Fix login.' },
+    request: { newSessionFallbackTitle: '新工作', requestId: 'direct-request', text: 'Fix login.' },
     recordedUserText: 'Fix login.',
     summary: () => 'unused',
     onSummaryError: () => assert.fail('submitted work is projected from its assignment'),
@@ -572,7 +606,7 @@ test('surface leaves discussion in WorkHub instead of creating a task view', asy
 
   const result = await submitWorkHubSurfaceInput({
     controller,
-    input: { requestId: 'discussion', text: '这个方向的价值是什么?' },
+    input: { newSessionFallbackTitle: '新工作', requestId: 'discussion', text: '这个方向的价值是什么?' },
   });
 
   assert.equal(result.kind, 'discussion');
@@ -747,18 +781,22 @@ test('real Session projection creates new guide topics and preserves origin ambi
   });
 
   const payment = await controller.submit({
+    newSessionFallbackTitle: '新工作',
     requestId: 'setup-payment',
     text: '检查支付回调重复投递时的幂等性,先只分析风险和测试点,不修改文件。',
   });
   const layout = await controller.submit({
+    newSessionFallbackTitle: '新工作',
     requestId: 'setup-layout',
     text: '优化 WorkHub 在移动端窄屏下的消息布局,先给设计建议,不修改文件。',
   });
   await controller.submit({
+    newSessionFallbackTitle: '新工作',
     requestId: 'focus-login',
     text: '刷新令牌过期致重复登录的排查计划:补充观测日志字段。',
   });
   const ambiguous = await controller.submit({
+    newSessionFallbackTitle: '新工作',
     requestId: 'ambiguous-repeat',
     text: '继续处理重复问题',
   });
@@ -832,7 +870,7 @@ test('successful delegated submission needs no renderer summary write', async ()
   });
   const result = await submitAndRecordWorkHubSurfaceInput({
     controller,
-    request: { requestId: 'action-1', text: 'Continue payments' },
+    request: { newSessionFallbackTitle: '新工作', requestId: 'action-1', text: 'Continue payments' },
     recordedUserText: 'Continue payments',
     summary: () => 'Sent to Payments',
     onSummaryError: () => assert.fail('no summary write is expected'),
@@ -852,7 +890,7 @@ test('resume records ordinary conversation text without persisting execution fie
   });
   await submitAndRecordWorkHubSurfaceInput({
     controller,
-    request: { requestId: 'resume-1', text: 'Resume Payments' },
+    request: { newSessionFallbackTitle: '新工作', requestId: 'resume-1', text: 'Resume Payments' },
     recordedUserText: 'Resume Payments',
     summary: () => 'Resume requested. See the target Session for current progress.',
     onSummaryError: () => assert.fail('conversation write must succeed'),
diff --git a/apps/desktop/src/main/attachment-ingest.ts b/apps/desktop/src/main/attachment-ingest.ts
index 302b68188b..6bffd28d20 100644
--- a/apps/desktop/src/main/attachment-ingest.ts
+++ b/apps/desktop/src/main/attachment-ingest.ts
@@ -21,6 +21,7 @@ import { Buffer } from 'node:buffer';
 import { open } from 'node:fs/promises';
 import { basename } from 'node:path';
 import {
+  attachmentIngestBlocked,
   attachmentKindFromMimeType,
   MAX_ATTACHMENT_BYTES,
   MAX_ATTACHMENT_COUNT,
@@ -67,7 +68,7 @@ export async function resolveAttachmentRefs<T = AttachmentRef>(input: {
       ? await readFileCapped(file.path, Math.min(maxBytes, maxTotalBytes - readBytes))
       : file.content;
     readBytes += bytes.byteLength;
-    if (readBytes > maxTotalBytes) throw new Error('附件总量超出大小限制。');
+    if (readBytes > maxTotalBytes) throw attachmentIngestBlocked('total_size_exceeded');
     let mimeType = resolveAttachmentMimeType(bytes, file.mimeType, name);
     const kind = attachmentKindFromMimeType(mimeType, name);
 
@@ -76,7 +77,7 @@ export async function resolveAttachmentRefs<T = AttachmentRef>(input: {
       mimeType = sniffAttachmentMimeType(bytes) ?? mimeType;
     }
     snapshotBytes += bytes.byteLength;
-    if (snapshotBytes > maxTotalBytes) throw new Error('附件总量超出大小限制。');
+    if (snapshotBytes > maxTotalBytes) throw attachmentIngestBlocked('total_size_exceeded');
     const artifactKind: ArtifactKind =
       kind === 'image' ? 'image' : kind === 'pdf' ? 'pdf' : 'file';
     refs.push(
@@ -164,7 +165,7 @@ export async function readFileCapped(path: string, maxBytes: number): Promise<Ui
   try {
     const buf = Buffer.alloc(maxBytes + 1);
     const { bytesRead } = await fh.read(buf, 0, maxBytes + 1, 0);
-    if (bytesRead > maxBytes) throw new Error('单个附件超出大小限制。');
+    if (bytesRead > maxBytes) throw attachmentIngestBlocked('item_too_large');
     return buf.subarray(0, bytesRead);
   } finally {
     await fh.close();
@@ -205,24 +206,24 @@ export async function prepareIngestItems(input: {
   const maxAttachments = input.maxAttachments ?? MAX_ATTACHMENT_COUNT;
   const maxBytes = input.maxBytes ?? MAX_ATTACHMENT_BYTES;
   let remainingBytes = input.maxTotalBytes ?? Infinity;
-  if (!Array.isArray(input.items)) throw new Error('附件信息无效,请重新选择文件后再发送。');
-  if (input.items.length > maxAttachments) throw new Error('一次最多添加 8 个附件。');
+  if (!Array.isArray(input.items)) throw attachmentIngestBlocked('items_invalid');
+  if (input.items.length > maxAttachments) throw attachmentIngestBlocked('count_limit');
   // Phase 1: validate every item with no side effects. Approval tokens are
   // peeked (not consumed) so a later invalid item does not burn earlier ones.
   const planned: AttachmentIngestFile[] = [];
   const approvalIds: string[] = [];
   const seenApprovalIds = new Set<string>();
   for (const item of input.items) {
-    if (!item || typeof item !== 'object') throw new Error('附件信息无效,请重新选择文件后再发送。');
+    if (!item || typeof item !== 'object') throw attachmentIngestBlocked('items_invalid');
     const record = item as Record<string, unknown>;
     if (typeof record.approvalId === 'string' && typeof record.name === 'string') {
-      if (seenApprovalIds.has(record.approvalId)) throw new Error('附件来源重复,请勿重复添加同一文件。');
+      if (seenApprovalIds.has(record.approvalId)) throw attachmentIngestBlocked('duplicate_source');
       seenApprovalIds.add(record.approvalId);
       const approved = input.approvals.peekApproval(input.senderId, record.approvalId);
-      if (!approved) throw new Error('附件来源已过期或无效,请重新选择文件后再发送。');
+      if (!approved) throw attachmentIngestBlocked('source_expired');
       const statResult = await input.stat(approved.path);
-      if (statResult.size > maxBytes) throw new Error('单个附件超出大小限制。');
-      if (statResult.size > remainingBytes) throw new Error('附件总量超出大小限制。');
+      if (statResult.size > maxBytes) throw attachmentIngestBlocked('item_too_large');
+      if (statResult.size > remainingBytes) throw attachmentIngestBlocked('total_size_exceeded');
       remainingBytes -= statResult.size;
       const mimeType = pickMimeType(record.mimeType, approved.mimeType);
       planned.push({ path: approved.path, ...(mimeType ? { mimeType } : {}), size: statResult.size });
@@ -234,17 +235,17 @@ export async function prepareIngestItems(input: {
       // string must not be decoded into main memory. base64 encodes 3 bytes
       // per 4 chars, so ceil(maxBytes*4/3)+padding is a safe upper bound.
       const maxBase64Len = Math.ceil((maxBytes * 4) / 3) + 4;
-      if (record.base64.length > maxBase64Len) throw new Error('单个附件超出大小限制。');
+      if (record.base64.length > maxBase64Len) throw attachmentIngestBlocked('item_too_large');
       if (Buffer.byteLength(record.base64, 'base64') > remainingBytes)
-        throw new Error('附件总量超出大小限制。');
+        throw attachmentIngestBlocked('total_size_exceeded');
       const content = Buffer.from(record.base64, 'base64');
-      if (content.byteLength > maxBytes) throw new Error('单个附件超出大小限制。');
+      if (content.byteLength > maxBytes) throw attachmentIngestBlocked('item_too_large');
       remainingBytes -= content.byteLength;
       const mimeType = typeof record.mimeType === 'string' && record.mimeType.length > 0 ? record.mimeType : undefined;
       planned.push({ name: record.name, ...(mimeType ? { mimeType } : {}), size: content.byteLength, content });
       continue;
     }
-    throw new Error('附件信息无效,请重新选择文件后再发送。');
+    throw attachmentIngestBlocked('items_invalid');
   }
   return {
     files: planned,
@@ -253,7 +254,7 @@ export async function prepareIngestItems(input: {
       // teardown or expiry during preparation must not burn another token.
       for (const id of approvalIds) {
         if (!input.approvals.peekApproval(input.senderId, id))
-          throw new Error('附件来源已过期或无效,请重新选择文件后再发送。');
+          throw attachmentIngestBlocked('source_expired');
       }
       const result = admit();
       for (const id of approvalIds) input.approvals.consumeApproval(input.senderId, id);
diff --git a/apps/desktop/src/main/runtime-host-guest-session-mounts.ts b/apps/desktop/src/main/runtime-host-guest-session-mounts.ts
index 53bc28bc98..e6ea6bafa0 100644
--- a/apps/desktop/src/main/runtime-host-guest-session-mounts.ts
+++ b/apps/desktop/src/main/runtime-host-guest-session-mounts.ts
@@ -18,6 +18,7 @@
  */
 
 import { randomUUID } from 'node:crypto';
+import { redactSecrets } from '@maka/core/redaction';
 import {
   abortable,
   decodeRemoteRuntimeHostProfile,
@@ -655,8 +656,8 @@ export function createDesktopGuestSessionMountService(input: {
     if (!retained) {
       return {
         kind: 'error',
-        reason: 'connection_failed',
-        message: `At most ${MAX_MOUNTS} shared Sessions can be retained`,
+        reason: 'mount_limit_reached',
+        params: { max: MAX_MOUNTS },
       };
     }
     let reconcile = false;
@@ -673,6 +674,8 @@ export function createDesktopGuestSessionMountService(input: {
         mountId: mount.mountId,
       };
     } catch (error) {
+      const importWasCancelled = activation.controller.signal.aborted;
+      const peerPathUnavailable = isPeerPathUnavailable(error);
       if (
         (activation.stage === 'finalizing' &&
           error instanceof RuntimeHostPairingFinalizationInterruptedError) ||
@@ -689,13 +692,22 @@ export function createDesktopGuestSessionMountService(input: {
         await input.unmount(mount.mountId).catch(() => undefined);
         invalidatingAccess.delete(mount.mountId);
       }
+      if (!reconcile && !importWasCancelled && !peerPathUnavailable) {
+        const diagnostic = asError(error);
+        console.error(
+          '[session-collaboration] import failed:',
+          redactSecrets(diagnostic.stack ?? diagnostic.message),
+        );
+      }
       return reconcile
         ? { kind: 'recovering', mountId: mount.mountId }
         : {
             kind: 'error',
-            reason: error instanceof RuntimeHostRemoteCompatibilityError ? 'incompatible_host'
-              : isPeerPathUnavailable(error) ? 'peer_path_unavailable' : 'connection_failed',
-            message: asError(error).message,
+            reason: error instanceof RuntimeHostRemoteCompatibilityError
+              ? 'incompatible_host'
+              : peerPathUnavailable
+                ? 'peer_path_unavailable'
+                : 'connection_failed',
           };
     } finally {
       activations.delete(activation);
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 a68664aac7..348551646a 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
@@ -26,11 +26,15 @@ import type {
 } from '@maka/runtime/stream-graph-read-model';
 import { DEFAULT_MAX_ITERATIONS, type GoalState } from '@maka/runtime/goal-state';
 import type { ShellRunPtyDataEvent } from '@maka/runtime/shell-run-contract';
-import type {
-  GoalProjection,
-  SessionDomainChange,
+import type { PlanSessionState } from '@maka/core/plan';
+import {
+  type GoalProjection,
+  type OperationOutput,
+  type PlanControlErrorCode,
+  type SessionDomainChange,
 } from '@maka/runtime-host/protocol';
-import type { AgentGraphEpochDirectory } from '@maka/runtime-host/client';
+import type { PlanControlIpcResult } from '../shared/plan-mode-ipc.js';
+import { type AgentGraphEpochDirectory, RuntimeHostOperationError } from '@maka/runtime-host/client';
 import type { DesktopRuntimeHostClient } from './runtime-host-client.js';
 import type { RuntimeHostSessionObserver } from './runtime-host-session-observer.js';
 import {
@@ -189,21 +193,29 @@ export function registerRuntimeHostSessionDomainsIpc(
   );
   ipcMain.handle(
     'plan-mode:requestRevision',
-    async (_event, sessionId: unknown, proposalId: unknown) => {
+    async (_event, sessionId: unknown, proposalId: unknown): Promise<PlanControlIpcResult<PlanSessionState>> => {
       const normalizedSessionId = requiredId(sessionId, 'Session');
-      await deps.client.controlPlan({
-        kind: 'request_revision',
-        sessionId: normalizedSessionId,
-        proposalId: requiredId(proposalId, 'Plan proposal'),
-        operationId: newId(),
-      });
+      try {
+        await deps.client.controlPlan({
+          kind: 'request_revision',
+          sessionId: normalizedSessionId,
+          proposalId: requiredId(proposalId, 'Plan proposal'),
+          operationId: newId(),
+        });
+      } catch (error) {
+        const failure = planControlIpcFailure(error);
+        if (failure) return failure;
+        throw error;
+      }
       deps.emitModeChanged(normalizedSessionId);
-      return deps.client.getPlanState(normalizedSessionId);
+      return { ok: true, value: await deps.client.getPlanState(normalizedSessionId) };
     },
   );
   ipcMain.handle(
     'plan-mode:abandon',
-    async (_event, sessionId: unknown, proposalId: unknown) => {
+    // The app-shell exit path is the only caller and is token-frozen, so this
+    // channel keeps its throwing shape: an envelope here would reach no reader.
+    async (_event, sessionId: unknown, proposalId: unknown): Promise<PlanSessionState> => {
       const normalizedSessionId = requiredId(sessionId, 'Session');
       await deps.client.controlPlan({
         kind: 'abandon_proposal',
@@ -217,58 +229,79 @@ export function registerRuntimeHostSessionDomainsIpc(
   );
   ipcMain.handle(
     'plan-mode:approve',
-    async (_event, sessionId: unknown, value: unknown) => {
+    async (_event, sessionId: unknown, value: unknown): Promise<PlanControlIpcResult<{ turnId: string; executionId: string }>> => {
       const normalizedSessionId = requiredId(sessionId, 'Session');
       const input = planApprovalInput(value);
-      const result = await deps.client.startPlanTurn({
-        kind: 'approve_proposal',
-        sessionId: normalizedSessionId,
-        proposalId: input.proposalId,
-        expectedRevision: input.expectedRevision,
-        expectedStoreVersion: input.expectedStoreVersion,
-        turnId: input.turnId,
-      });
-      if (result.plan.executionId === null) {
+      const result = await deps.client
+        .startPlanTurn({
+          kind: 'approve_proposal',
+          sessionId: normalizedSessionId,
+          proposalId: input.proposalId,
+          expectedRevision: input.expectedRevision,
+          expectedStoreVersion: input.expectedStoreVersion,
+          turnId: input.turnId,
+        })
+        .then(
+          (output): PlanControlIpcResult<OperationOutput<'plan.turn.start'>> => ({
+            ok: true,
+            value: output,
+          }),
+          (error: unknown) => planControlIpcFailure(error) ?? Promise.reject(error),
+        );
+      if (!result.ok) return result;
+      if (result.value.plan.executionId === null) {
         throw new Error('Plan approval did not create an execution');
       }
       deps.emitModeChanged(normalizedSessionId);
       return {
-        turnId: input.turnId,
-        executionId: result.plan.executionId,
+        ok: true,
+        value: { turnId: input.turnId, executionId: result.value.plan.executionId },
       };
     },
   );
   ipcMain.handle(
     'plan-mode:resume',
-    async (_event, sessionId: unknown, executionId: unknown, turnId: unknown) => {
+    async (_event, sessionId: unknown, executionId: unknown, turnId: unknown): Promise<PlanControlIpcResult<{ turnId: string; executionId: string }>> => {
       const normalizedSessionId = requiredId(sessionId, 'Session');
       const normalizedExecutionId = requiredId(executionId, 'Plan execution');
       const normalizedTurnId = requiredId(turnId, 'Turn');
-      await deps.client.startPlanTurn({
-        kind: 'resume_execution',
-        sessionId: normalizedSessionId,
-        executionId: normalizedExecutionId,
-        turnId: normalizedTurnId,
-      });
+      try {
+        await deps.client.startPlanTurn({
+          kind: 'resume_execution',
+          sessionId: normalizedSessionId,
+          executionId: normalizedExecutionId,
+          turnId: normalizedTurnId,
+        });
+      } catch (error) {
+        const failure = planControlIpcFailure(error);
+        if (failure) return failure;
+        throw error;
+      }
       deps.emitModeChanged(normalizedSessionId);
       return {
-        turnId: normalizedTurnId,
-        executionId: normalizedExecutionId,
+        ok: true,
+        value: { turnId: normalizedTurnId, executionId: normalizedExecutionId },
       };
     },
   );
   ipcMain.handle(
     'plan-mode:abandonExecution',
-    async (_event, sessionId: unknown, executionId: unknown) => {
+    async (_event, sessionId: unknown, executionId: unknown): Promise<PlanControlIpcResult<PlanSessionState>> => {
       const normalizedSessionId = requiredId(sessionId, 'Session');
-      await deps.client.controlPlan({
-        kind: 'cancel_execution',
-        sessionId: normalizedSessionId,
-        executionId: requiredId(executionId, 'Plan execution'),
-        operationId: newId(),
-      });
+      try {
+        await deps.client.controlPlan({
+          kind: 'cancel_execution',
+          sessionId: normalizedSessionId,
+          executionId: requiredId(executionId, 'Plan execution'),
+          operationId: newId(),
+        });
+      } catch (error) {
+        const failure = planControlIpcFailure(error);
+        if (failure) return failure;
+        throw error;
+      }
       deps.emitModeChanged(normalizedSessionId);
-      return deps.client.getPlanState(normalizedSessionId);
+      return { ok: true, value: await deps.client.getPlanState(normalizedSessionId) };
     },
   );
   handleReconnectableRead(
@@ -555,3 +588,20 @@ function planApprovalInput(value: unknown): {
     turnId: requiredId(record.turnId, 'Turn'),
   };
 }
+
+/**
+ * Convert a thrown plan-control rejection into the IPC envelope: expected
+ * plan-control codes keep their structured code across the boundary, and any
+ * other failure keeps rejecting so it takes the unexpected-diagnostics path.
+ */
+function planControlIpcFailure(
+  error: unknown,
+): { readonly ok: false; readonly error: { readonly code: PlanControlErrorCode; readonly message: string } } | undefined {
+  if (error instanceof RuntimeHostOperationError && error.operation.startsWith('plan.')) {
+    return {
+      ok: false,
+      error: { code: error.code as PlanControlErrorCode, message: error.message },
+    };
+  }
+  return undefined;
+}
diff --git a/apps/desktop/src/main/work-board-ipc-main.ts b/apps/desktop/src/main/work-board-ipc-main.ts
index 780c35e196..10d7a85c9d 100644
--- a/apps/desktop/src/main/work-board-ipc-main.ts
+++ b/apps/desktop/src/main/work-board-ipc-main.ts
@@ -19,6 +19,7 @@
 
 import type { IpcMain } from 'electron';
 import type { WorkBoardItem, WorkBoardPage } from '@maka/core/work-board';
+import { redactSecrets } from '@maka/core/redaction';
 import {
   createWorkBoardStore,
   WorkBoardStoreError,
@@ -166,14 +167,12 @@ function requireWorkBoardId(id: unknown): string {
 }
 
 function workBoardFailure(error: unknown): {
-  readonly code: WorkBoardStoreErrorCode | 'unknown';
-  readonly message: string;
+  readonly error: { readonly code: WorkBoardStoreErrorCode | 'unknown' };
 } {
-  if (error instanceof WorkBoardStoreError) {
-    return { code: error.code, message: error.message };
+  if (error instanceof WorkBoardStoreError && error.code !== 'corrupt_record') {
+    return { error: { code: error.code } };
   }
-  return {
-    code: 'unknown',
-    message: error instanceof Error ? error.message : 'Work Board operation failed',
-  };
+  const detail = error instanceof Error ? error.stack ?? error.message : String(error);
+  console.error('[work-board] operation failed:', redactSecrets(detail));
+  return { error: { code: error instanceof WorkBoardStoreError ? error.code : 'unknown' } };
 }
diff --git a/apps/desktop/src/preload/attachment-ingest-payload.ts b/apps/desktop/src/preload/attachment-ingest-payload.ts
index c36848a7f0..15ab2966d2 100644
--- a/apps/desktop/src/preload/attachment-ingest-payload.ts
+++ b/apps/desktop/src/preload/attachment-ingest-payload.ts
@@ -17,7 +17,11 @@
  * under the License.
  */
 
-import { MAX_ATTACHMENT_BYTES, MAX_ATTACHMENT_COUNT } from '@maka/core/attachments';
+import {
+  attachmentIngestBlocked,
+  MAX_ATTACHMENT_BYTES,
+  MAX_ATTACHMENT_COUNT,
+} from '@maka/core/attachments';
 
 export type IngestInput =
   | { approvalId: string; name: string; mimeType?: string }
@@ -37,14 +41,14 @@ function bytesToBase64(bytes: Uint8Array): string {
 }
 
 export async function encodeIngestItems(items: IngestInput[]): Promise<IngestPayload[]> {
-  if (items.length > MAX_ATTACHMENT_COUNT) throw new Error('附件数量超过 8 个');
+  if (items.length > MAX_ATTACHMENT_COUNT) throw attachmentIngestBlocked('count_limit');
   const out: IngestPayload[] = [];
   for (const item of items) {
     if ('file' in item) {
       // Reject oversized blobs before arrayBuffer() so the renderer never
       // loads the bytes into memory. Main-side resolveIngestItems is the
       // authoritative backstop; this guard exists only to avoid renderer OOM.
-      if (item.file.size > MAX_ATTACHMENT_BYTES) throw new Error('附件大小超过 50MB');
+      if (item.file.size > MAX_ATTACHMENT_BYTES) throw attachmentIngestBlocked('item_too_large');
       const bytes = new Uint8Array(await item.file.arrayBuffer());
       const mimeType = item.file.type || undefined;
       out.push({
@@ -55,7 +59,7 @@ export async function encodeIngestItems(items: IngestInput[]): Promise<IngestPay
     } else if (typeof item.approvalId === 'string') {
       out.push(item);
     } else {
-      throw new Error('附件信息无效。');
+      throw attachmentIngestBlocked('items_invalid');
     }
   }
   return out;
diff --git a/apps/desktop/src/preload/bridge-contract.d.ts b/apps/desktop/src/preload/bridge-contract.d.ts
index 62dc175fad..68465dac92 100644
--- a/apps/desktop/src/preload/bridge-contract.d.ts
+++ b/apps/desktop/src/preload/bridge-contract.d.ts
@@ -188,6 +188,7 @@ export type AppIconImportResult =
 
 export type { DesktopSessionSummary } from '../shared/desktop-session-projection.js';
 export type { WorkBoardChangedEvent, WorkBoardIpcResult } from '../shared/work-board-ipc.js';
+import type { PlanControlIpcResult } from '../shared/plan-mode-ipc.js';
 import type { DesktopConnectionSnapshot } from '../shared/desktop-connection-snapshot.js';
 import type { DesktopExternalSessionCatalogItem } from './external-session-catalog.js';
 import type { DesktopDiagnosticInput } from './diagnostics-contract.js';
@@ -1219,22 +1220,19 @@ export interface MakaBridge {
     setOrchestrationMode(sessionId: string, mode: OrchestrationMode): Promise<DesktopSessionSummary>;
     getPlanState(sessionId: string): Promise<PlanSessionState>;
     subscribePlanChanges(sessionId: string, handler: () => void): () => void;
-    requestPlanRevision(sessionId: string, proposalId: string): Promise<PlanSessionState>;
-    abandonPlanProposal(
-      sessionId: string,
-      proposalId: string,
-    ): Promise<PlanSessionState>;
+    requestPlanRevision(sessionId: string, proposalId: string): Promise<PlanControlIpcResult<PlanSessionState>>;
+    abandonPlanProposal(sessionId: string, proposalId: string): Promise<PlanSessionState>;
     approvePlan(sessionId: string, input: {
       proposalId: string;
       expectedRevision: number;
       expectedStoreVersion: number;
       turnId: string;
-    }): Promise<{ turnId: string; executionId: string }>;
-    resumePlan(sessionId: string, executionId: string, turnId: string): Promise<{
+    }): Promise<PlanControlIpcResult<{ turnId: string; executionId: string }>>;
+    resumePlan(sessionId: string, executionId: string, turnId: string): Promise<PlanControlIpcResult<{
       turnId: string;
       executionId: string;
-    }>;
-    abandonPlanExecution(sessionId: string, executionId: string): Promise<PlanSessionState>;
+    }>>;
+    abandonPlanExecution(sessionId: string, executionId: string): Promise<PlanControlIpcResult<PlanSessionState>>;
     setModelConfiguration(sessionId: string, input: {
       llmConnectionId: string;
       llmConnectionSlug: string;
diff --git a/apps/desktop/src/preload/preload.ts b/apps/desktop/src/preload/preload.ts
index 69c41347e1..23164f1de7 100644
--- a/apps/desktop/src/preload/preload.ts
+++ b/apps/desktop/src/preload/preload.ts
@@ -233,6 +233,7 @@ import {
   type CollaborationTurnRequestWithdrawResult,
   type SessionTurnAccessRequest,
 } from '@maka/runtime-host/protocol';
+import type { PlanControlIpcResult } from '../shared/plan-mode-ipc.js';
 import type { AgentGraphEpochDirectory } from '@maka/runtime-host/client';
 import {
   desktopSessionKey,
@@ -1261,12 +1262,16 @@ function executeWebSearchQuery(input: {
     return Promise.resolve({
       ok: false,
       reason: 'unsupported_provider',
-      message: '原生联网搜索由任务中的主模型请求执行,不支持从设置页单独调用。',
+      message: 'web search runs through the primary model inside tasks',
     });
   }
   const query = normalizeWebSearchQuery(input.query);
   if (!query) {
-    return Promise.resolve({ ok: false, reason: 'invalid_query', message: '请输入有效的搜索关键词。' });
+    return Promise.resolve({
+      ok: false,
+      reason: 'invalid_query',
+      message: 'the query is empty after normalization',
+    });
   }
   const apiKey = webSearchCredentialOverride(input.apiKey);
   return selectedRuntimeHostScope(host).then((scope) =>
@@ -1289,7 +1294,7 @@ function executeWebSearchTest(input: {
     return Promise.resolve({
       ok: false,
       reason: 'unsupported_provider',
-      message: '原生联网搜索由任务中的主模型请求执行,不需要单独测试搜索凭据。',
+      message: 'web search runs through the primary model inside tasks',
     });
   }
   const apiKey = webSearchCredentialOverride(input.apiKey);
@@ -1305,7 +1310,7 @@ function unsupportedWebSearchProvider(): WebSearchResponse {
   return {
     ok: false,
     reason: 'unsupported_provider',
-    message: '当前配置不支持这个搜索引擎,请选择 Tavily 后重试。',
+    message: 'no web search provider is configured',
   };
 }
 
@@ -2414,30 +2419,30 @@ const makaBridge = {
         unsubscribe();
       };
     },
-    requestPlanRevision(sessionId: string, proposalId: string): Promise<PlanSessionState> {
+    requestPlanRevision(sessionId: string, proposalId: string): Promise<PlanControlIpcResult<PlanSessionState>> {
       return invokeProjectedSessionRuntimeHost('plan-mode:requestRevision', sessionId, proposalId);
     },
     abandonPlanProposal(
       sessionId: string,
       proposalId: string,
     ): Promise<PlanSessionState> {
-      return invokeProjectedSessionRuntimeHost('plan-mode:abandon', sessionId, proposalId);
+      return invokeProjectedSessionRuntimeHost<PlanSessionState>('plan-mode:abandon', sessionId, proposalId);
     },
     approvePlan(sessionId: string, input: {
       proposalId: string;
       expectedRevision: number;
       expectedStoreVersion: number;
       turnId: string;
-    }): Promise<{ turnId: string; executionId: string }> {
+    }): Promise<PlanControlIpcResult<{ turnId: string; executionId: string }>> {
       return invokeSessionRuntimeHost('plan-mode:approve', sessionId, input);
     },
-    resumePlan(sessionId: string, executionId: string, turnId: string): Promise<{
+    resumePlan(sessionId: string, executionId: string, turnId: string): Promise<PlanControlIpcResult<{
       turnId: string;
       executionId: string;
-    }> {
+    }>> {
       return invokeSessionRuntimeHost('plan-mode:resume', sessionId, executionId, turnId);
     },
-    abandonPlanExecution(sessionId: string, executionId: string): Promise<PlanSessionState> {
+    abandonPlanExecution(sessionId: string, executionId: string): Promise<PlanControlIpcResult<PlanSessionState>> {
       return invokeProjectedSessionRuntimeHost('plan-mode:abandonExecution', sessionId, executionId);
     },
     setModelConfiguration(sessionId: string, input: {
diff --git a/apps/desktop/src/renderer/app-shell-chat-actions.ts b/apps/desktop/src/renderer/app-shell-chat-actions.ts
index 1739b1a35f..dea6fdaee0 100644
--- a/apps/desktop/src/renderer/app-shell-chat-actions.ts
+++ b/apps/desktop/src/renderer/app-shell-chat-actions.ts
@@ -502,7 +502,7 @@ export function createAppShellChatActions(deps: {
       }
       if (!initialSessionId) {
         if (!initialNewTaskTarget) return false;
-        if (pending?.length) preflightAttachmentItems(pending, uiLocale);
+        if (pending?.length) preflightAttachmentItems(pending);
         const session = await window.maka.newTasks.create(initialNewTaskTarget, {
           name: DEFAULT_SESSION_NAME,
           ...(newChatModel
diff --git a/apps/desktop/src/renderer/app-shell-copy.ts b/apps/desktop/src/renderer/app-shell-copy.ts
index 8e5d7c82fb..783eec74d4 100644
--- a/apps/desktop/src/renderer/app-shell-copy.ts
+++ b/apps/desktop/src/renderer/app-shell-copy.ts
@@ -21,32 +21,25 @@ import type { ConnectionTestResult } from '@maka/core/llm-connections';
 import type { TextFileImportPreflightFailureReason } from '@maka/core/text-file-import';
 import type { UiLocale } from '@maka/core/ui-locale';
 import { generalizedErrorMessageForLocale } from '@maka/core/redaction';
+import { classifiedErrorFallback } from './application/contracts/operation-diagnostics.js';
 import { getShellCopy } from './locales/shell-copy.js';
 
-const SESSION_READ_MESSAGES_ERROR_MARKER = 'MAKA_SESSION_READ_MESSAGES_ERROR:';
-
 export function messageReadErrorMessage(error: unknown, locale: UiLocale): string {
-  return sessionMessageErrorMessage(error, getShellCopy(locale).errors.messageRead, locale);
+  const copy = getShellCopy(locale);
+  return classifiedErrorFallback(error, copy.errors.messageRead, locale, 'message-read');
 }
 
 export function messageRefreshErrorMessage(error: unknown, locale: UiLocale): string {
-  return sessionMessageErrorMessage(error, getShellCopy(locale).errors.messageRefresh, locale);
-}
-
-function sessionMessageErrorMessage(error: unknown, fallback: string, locale: UiLocale): string {
-  const raw = error instanceof Error ? error.message : String(error);
-  const markerIndex = raw.indexOf(SESSION_READ_MESSAGES_ERROR_MARKER);
-  if (markerIndex < 0 || locale !== 'zh-CN') return localizedErrorMessage(error, fallback, locale);
-  const marked = raw.slice(markerIndex + SESSION_READ_MESSAGES_ERROR_MARKER.length).trim();
-  return marked.split(/\r?\n/, 1)[0]?.trim() || fallback;
-}
-
-function localizedErrorMessage(error: unknown, fallback: string, locale: UiLocale): string {
-  return generalizedErrorMessageForLocale(error, fallback, locale);
+  const copy = getShellCopy(locale);
+  return classifiedErrorFallback(error, copy.errors.messageRefresh, locale, 'message-refresh');
 }
 
-export function commandPaletteActionErrorMessage(error: unknown, fallback: string, locale: UiLocale): string {
-  return localizedErrorMessage(error, fallback, locale);
+export function commandPaletteActionErrorMessage(
+  error: unknown,
+  fallback: string,
+  locale: UiLocale,
+): string {
+  return classifiedErrorFallback(error, fallback, locale, 'command-palette');
 }
 
 export function openPathActionErrorMessage(
@@ -55,13 +48,19 @@ export function openPathActionErrorMessage(
   locale: UiLocale,
 ): string {
   const copy = getShellCopy(locale);
-  return localizedErrorMessage(error, copy.errors.openPath(copy.paths[key]), locale);
+  return classifiedErrorFallback(
+    error,
+    copy.errors.openPath(copy.paths[key]),
+    locale,
+    `open-path:${key}`,
+  );
 }
 
 export function commandPaletteConnectionTestFailureMessage(result: ConnectionTestResult, locale: UiLocale): string {
   const fallback = commandPaletteConnectionTestFailureFallback(result, locale);
-  if (!result.errorMessage) return fallback;
-  return localizedErrorMessage(new Error(result.errorMessage), fallback, locale);
+  return result.errorMessage
+    ? generalizedErrorMessageForLocale(result.errorMessage, fallback, locale)
+    : fallback;
 }
 
 function commandPaletteConnectionTestFailureFallback(result: ConnectionTestResult, locale: UiLocale): string {
diff --git a/apps/desktop/src/renderer/app-shell.tsx b/apps/desktop/src/renderer/app-shell.tsx
index 6c0a35b7ce..cdd11d6866 100644
--- a/apps/desktop/src/renderer/app-shell.tsx
+++ b/apps/desktop/src/renderer/app-shell.tsx
@@ -2636,7 +2636,7 @@ function AppShellContent({
                     {workHubEnabled && navSelection.section === 'sessions' && activeId ? (
                       <Button
                         className="workhub-return"
-                        label={uiLocale !== 'en' ? '返回 WorkHub' : 'Return to WorkHub'}
+                        label={shellCopy.returnToWorkHub}
                         variant="secondary"
                         size="sm"
                         onClick={openWorkHub}
diff --git a/apps/desktop/src/renderer/application/contracts/operation-diagnostics.ts b/apps/desktop/src/renderer/application/contracts/operation-diagnostics.ts
new file mode 100644
index 0000000000..2699e75164
--- /dev/null
+++ b/apps/desktop/src/renderer/application/contracts/operation-diagnostics.ts
@@ -0,0 +1,57 @@
+/*
+ * 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 { UiLocale } from '@maka/core/ui-locale';
+import {
+  classifyGeneralizedError,
+  generalizedErrorMessageForLocale,
+  reportUnexpectedOperation,
+} from '@maka/core/redaction';
+
+/** Transport for expected operation failures carried as stable codes. */
+export class ExpectedOperationError<Code extends string> extends Error {
+  constructor(readonly code: Code) {
+    super(code);
+    this.name = 'ExpectedOperationError';
+  }
+}
+
+export function reportUnexpectedError(scope: string, error: unknown): void {
+  // A category the classifier recognizes is an ordinary outcome of a network
+  // call, not a defect, so it never reaches the diagnostics channel.
+  if (classifyGeneralizedError(error)) return;
+  reportUnexpectedOperation(scope, error);
+}
+
+export function unexpectedErrorFallback(error: unknown, fallback: string, scope: string): string {
+  reportUnexpectedError(scope, error);
+  return fallback;
+}
+
+/** Same rule, but a recognized category renders instead of the caller's line. */
+export function classifiedErrorFallback(
+  error: unknown,
+  fallback: string,
+  locale: UiLocale,
+  scope: string,
+): string {
+  return classifyGeneralizedError(error)
+    ? generalizedErrorMessageForLocale(error, fallback, locale)
+    : unexpectedErrorFallback(error, fallback, scope);
+}
diff --git a/apps/desktop/src/renderer/attachment-preflight.ts b/apps/desktop/src/renderer/attachment-preflight.ts
index 33891c2fdc..a0b820d810 100644
--- a/apps/desktop/src/renderer/attachment-preflight.ts
+++ b/apps/desktop/src/renderer/attachment-preflight.ts
@@ -17,9 +17,11 @@
  * under the License.
  */
 
-import { MAX_ATTACHMENT_BYTES, MAX_ATTACHMENT_COUNT } from '@maka/core/attachments';
-import { type UiLocale } from '@maka/core/ui-locale';
-import { getDesktopConversationCopy } from './locales/conversation-copy.js';
+import {
+  attachmentIngestBlocked,
+  MAX_ATTACHMENT_BYTES,
+  MAX_ATTACHMENT_COUNT,
+} from '@maka/core/attachments';
 
 type PreflightItem = {
   size: number;
@@ -30,23 +32,24 @@ type PreflightItem = {
 };
 
 /**
- * Reject count/size/duplicate-token violations before a new-chat session is
+ * Reject count/size/duplicate-source violations before a new-chat session is
  * created, so an encode/resolve-time failure does not leave an empty session
- * behind. This is a renderer-side UX guard mirroring main-side
- * resolveIngestItems pre-validation; main remains the authoritative cap.
+ * behind. Rejects with the same stable `attachment_ingest:<code>` tokens the
+ * main-side resolveIngestItems pre-validation rejects with, so the shell
+ * presenter maps the reason through the locale catalogs instead of the
+ * generic send fallback; main remains the authoritative cap.
  *
  * File blobs are sized by the browser File object; approval-token attachments
  * are sized by the pending size stamped at pick time (main re-stats).
  */
-export function preflightAttachmentItems(items: readonly PreflightItem[], locale: UiLocale): void {
-  const copy = getDesktopConversationCopy(locale).attachments;
-  if (items.length > MAX_ATTACHMENT_COUNT) throw new Error(copy.tooMany);
+export function preflightAttachmentItems(items: readonly PreflightItem[]): void {
+  if (items.length > MAX_ATTACHMENT_COUNT) throw attachmentIngestBlocked('count_limit');
   const seen = new Set<string>();
   for (const item of items) {
     const bytes = item.source.type === 'file' ? item.source.file.size : item.size;
-    if (bytes > MAX_ATTACHMENT_BYTES) throw new Error(copy.tooLarge);
+    if (bytes > MAX_ATTACHMENT_BYTES) throw attachmentIngestBlocked('item_too_large');
     if (item.source.type === 'approval') {
-      if (seen.has(item.source.approvalId)) throw new Error(copy.duplicate);
+      if (seen.has(item.source.approvalId)) throw attachmentIngestBlocked('duplicate_source');
       seen.add(item.source.approvalId);
     }
   }
diff --git a/apps/desktop/src/renderer/features/session-collaboration/testing.ts b/apps/desktop/src/renderer/features/session-collaboration/testing.ts
index a89fc474f1..0768269e19 100644
--- a/apps/desktop/src/renderer/features/session-collaboration/testing.ts
+++ b/apps/desktop/src/renderer/features/session-collaboration/testing.ts
@@ -30,3 +30,4 @@ export { SessionCollaborationServicesProvider } from './services-context.js';
 export { SessionCollaborationJoinDialog } from './ui/session-collaboration-join-dialog.js';
 export { SessionCollaborationNavigation } from './ui/session-collaboration-navigation.js';
 export { SessionTurnRequestComposer } from './ui/session-turn-request-composer.js';
+export { sessionCollaborationImportErrorMessage } from './ui/session-collaboration-join-dialog.js';
diff --git a/apps/desktop/src/renderer/features/session-collaboration/ui/session-collaboration-join-dialog.tsx b/apps/desktop/src/renderer/features/session-collaboration/ui/session-collaboration-join-dialog.tsx
index f3efe52217..0a7f4e36a3 100644
--- a/apps/desktop/src/renderer/features/session-collaboration/ui/session-collaboration-join-dialog.tsx
+++ b/apps/desktop/src/renderer/features/session-collaboration/ui/session-collaboration-join-dialog.tsx
@@ -31,6 +31,7 @@ import {
   TextArea,
   useToast,
 } from '@maka/ui';
+import { reportUnexpectedError } from '../../../application/contracts/operation-diagnostics.js';
 import { SessionCollaborationAliasAction } from './session-collaboration-alias-action.js';
 import { useSessionCollaborationServices } from '../services-context.js';
 import type {
@@ -47,6 +48,8 @@ export interface SessionCollaborationJoinCopy {
   readonly joinInsecure: string;
   readonly close: string;
   readonly connectionFailed: string;
+  readonly cancelFailed: string;
+  readonly mountLimit: (max: number) => string;
   readonly invalidCode: string;
   readonly directPathUnavailable: string;
   readonly code: string;
@@ -164,7 +167,7 @@ export function SessionCollaborationJoinDialog(props: {
           finishClose();
           return;
         }
-        const message = importError(props.copy, result.reason, result.message);
+        const message = sessionCollaborationImportErrorMessage(props.copy, result);
         setJoinState({ kind: 'failed', message });
         toast.error(props.copy.joinTitle, message);
         return;
@@ -180,7 +183,8 @@ export function SessionCollaborationJoinDialog(props: {
         finishClose();
         return;
       }
-      const message = errorMessage(error);
+      reportUnexpectedError('session-collaboration:import', error);
+      const message = props.copy.connectionFailed;
       setJoinState({ kind: 'failed', message });
       toast.error(props.copy.joinTitle, message);
     } finally {
@@ -217,7 +221,8 @@ export function SessionCollaborationJoinDialog(props: {
       finishClose();
     } catch (error) {
       if (!open.current || activeOperationId.current !== operationId) return;
-      const message = errorMessage(error);
+      reportUnexpectedError('session-collaboration:cancel-import', error);
+      const message = props.copy.cancelFailed;
       setJoinState({ kind: 'failed', message });
       toast.error(props.copy.joinTitle, message);
     }
@@ -229,7 +234,8 @@ export function SessionCollaborationJoinDialog(props: {
       await services.removeMount(mountId);
       setMounts((current) => current.filter((mount) => mount.mountId !== mountId));
     } catch (error) {
-      toast.error(props.copy.disconnectFailed, errorMessage(error));
+      reportUnexpectedError('session-collaboration:disconnect', error);
+      toast.error(props.copy.disconnectFailed);
     } finally {
       setRemovingMountId(undefined);
     }
@@ -349,8 +355,10 @@ export function SessionCollaborationJoinDialog(props: {
                           {mount.readiness !== 'ready' && !mountAccessLost(mount) ? (
                             <Button variant="secondary" size="sm" label={props.copy.retryConnection}
                               isDisabled={working || removingMountId !== undefined}
-                              onClick={() => void services.retryMount(mount.mountId).catch((error) =>
-                                toast.error(props.copy.connectionFailed, errorMessage(error)))} />
+                              onClick={() => void services.retryMount(mount.mountId).catch((error) => {
+                                reportUnexpectedError('session-collaboration:retry-mount', error);
+                                toast.error(props.copy.connectionFailed);
+                              })} />
                           ) : null}
                           <SessionCollaborationAliasAction name={mount.name}
                             disabled={working || removingMountId !== undefined}
@@ -448,16 +456,16 @@ function abbreviatePeerId(peerId: string): string {
   return peerId.length <= 20 ? peerId : `${peerId.slice(0, 10)}…${peerId.slice(-6)}`;
 }
 
-function importError(
+export function sessionCollaborationImportErrorMessage(
   copy: SessionCollaborationJoinCopy,
-  reason: Extract<SessionCollaborationImportResult, { kind: 'error' }>['reason'],
-  message?: string,
+  result: Extract<SessionCollaborationImportResult, { kind: 'error' }>,
 ): string {
-  if (reason === 'invalid_code') return copy.invalidCode;
-  if (reason === 'insecure_confirmation_required') return copy.insecureBody;
-  if (reason === 'peer_path_unavailable') return copy.directPathUnavailable;
-  if (reason === 'incompatible_host') return copy.incompatibleHost;
-  return message ?? copy.connectionFailed;
+  if (result.reason === 'invalid_code') return copy.invalidCode;
+  if (result.reason === 'insecure_confirmation_required') return copy.insecureBody;
+  if (result.reason === 'peer_path_unavailable') return copy.directPathUnavailable;
+  if (result.reason === 'incompatible_host') return copy.incompatibleHost;
+  if (result.reason === 'mount_limit_reached') return copy.mountLimit(result.params.max);
+  return copy.connectionFailed;
 }
 
 function importPhaseLabel(
@@ -474,7 +482,3 @@ function importPhaseLabel(
     case 'loading_session': return copy.loadingSession;
   }
 }
-
-function errorMessage(error: unknown): string {
-  return error instanceof Error ? error.message : String(error);
-}
diff --git a/apps/desktop/src/renderer/features/workbar/tools/side-chat/quote-companion-panel.tsx b/apps/desktop/src/renderer/features/workbar/tools/side-chat/quote-companion-panel.tsx
index 353fe2f510..ff97628181 100644
--- a/apps/desktop/src/renderer/features/workbar/tools/side-chat/quote-companion-panel.tsx
+++ b/apps/desktop/src/renderer/features/workbar/tools/side-chat/quote-companion-panel.tsx
@@ -350,11 +350,11 @@ export function QuoteCompanionPanel(props: {
                   steer: companion.steer,
                   send: async () => {
                     try {
-                      preflightAttachmentItems(pendingAttachments, locale);
+                      preflightAttachmentItems(pendingAttachments);
                     } catch (error) {
                       toast.error(
                         copy.errors.sendRejected,
-                        error instanceof Error ? error.message : String(error),
+                        localizedShellErrorMessage(error, copy.errors.sendRejected, locale),
                       );
                       return false;
                     }
diff --git a/apps/desktop/src/renderer/features/workhub/model/route-policy.ts b/apps/desktop/src/renderer/features/workhub/model/route-policy.ts
index 16e0a5fa96..5df36c3c3a 100644
--- a/apps/desktop/src/renderer/features/workhub/model/route-policy.ts
+++ b/apps/desktop/src/renderer/features/workhub/model/route-policy.ts
@@ -121,6 +121,7 @@ export interface WorkHubRoutePolicy {
       readonly resolution: 'none' | 'ranked' | 'ambiguous';
       readonly recalledSessionIds: readonly string[];
     };
+    newSessionFallbackTitle: string;
   }): WorkHubRouteDecision;
   initializeFocus(targets: readonly WorkHubRouteTarget[]): void;
   focusSnapshot(): {
@@ -134,6 +135,7 @@ export interface WorkHubRoutePolicy {
 
 export function workHubNewSessionName(
   text: string,
+  fallbackTitle: string,
   parsedIntent?: WorkHubRequestIntent,
 ): string {
   const intent = typeof parsedIntent === 'object'
@@ -145,7 +147,7 @@ export function workHubNewSessionName(
     '',
   );
   const firstClause = withoutCreationPrefix.split(/[,。;;\n]/u)[0]?.trim();
-  return firstClause?.slice(0, 48) || '新工作';
+  return firstClause?.slice(0, 48) || fallbackTitle;
 }
 
 export function boundedWorkHubText(value: string, maxChars: number): string {
@@ -257,7 +259,7 @@ function createWorkHubRoutePolicyVisit(
         'resume_target_ambiguous',
       );
     },
-    resolve({ text, sessions, originPromptBySessionId, explicitTarget, interpretation }) {
+    resolve({ text, sessions, originPromptBySessionId, explicitTarget, interpretation, newSessionFallbackTitle }) {
       const intent = readWorkHubRequestIntent(text);
       if (intent.execution === 'ambiguous') {
         return { kind: 'clarification', options: [], reason: 'ambiguous_command' };
@@ -285,7 +287,7 @@ function createWorkHubRoutePolicyVisit(
       }
       if (correctionText && correctedFrom) {
         if (looksLikeCorrectionCreation(intent)) {
-          return { kind: 'new_session', title: workHubNewSessionName(text, intent), correctedFrom };
+          return { kind: 'new_session', title: workHubNewSessionName(text, newSessionFallbackTitle, intent), correctedFrom };
         }
         const alternatives = sessions.filter((session) =>
           session.target.sessionId !== correctedFrom.sessionId);
@@ -322,7 +324,7 @@ function createWorkHubRoutePolicyVisit(
       }
 
       if (looksLikeExplicitNewSession(intent)) {
-        return { kind: 'new_session', title: workHubNewSessionName(text, intent) };
+        return { kind: 'new_session', title: workHubNewSessionName(text, newSessionFallbackTitle, intent) };
       }
 
       // A failed or uncertain interpretation never authorizes a guessed target.
@@ -423,7 +425,7 @@ function createWorkHubRoutePolicyVisit(
         }
       }
       return looksExecutable(intent) && interpretation?.classification !== 'discussion'
-        ? { kind: 'new_session', title: workHubNewSessionName(text, intent) }
+        ? { kind: 'new_session', title: workHubNewSessionName(text, newSessionFallbackTitle, intent) }
         : { kind: 'discussion' };
     },
     initializeFocus(targets) {
diff --git a/apps/desktop/src/renderer/locales/conversation-copy.ts b/apps/desktop/src/renderer/locales/conversation-copy.ts
index 7ead977b08..b83cddc75b 100644
--- a/apps/desktop/src/renderer/locales/conversation-copy.ts
+++ b/apps/desktop/src/renderer/locales/conversation-copy.ts
@@ -61,7 +61,6 @@ export interface DesktopConversationCopy {
     messageReadFailedTitle: string;
     scrollMainToBottom: string;
   };
-  attachments: { tooMany: string; tooLarge: string; duplicate: string };
   model: {
     fakeBackendLabel: string;
     setupTitle: string;
@@ -447,7 +446,6 @@ function enDetail(parts: readonly string[]): string {
 const COPY = {
   'zh-CN': {
     actions: { stopFailedTitle: '停止失败', stopFailedFallback: '任务操作失败,请稍后重试。', refreshSessionsFailedTitle: '刷新任务列表失败', refreshSessionsFailedFallback: '刷新任务列表失败,请稍后重试。', conversationErrorTitle: '任务出错', conversationErrorFallback: '任务运行失败,请稍后重试。', regenerateStartedTitle: '已发起重新生成', regenerateStartedDescription: '正在生成新的一轮回答', branchCreatedTitle: '已创建分支', branchCreatedDescription: (name) => `新任务 ${name}`, revisionStartedTitle: '已创建修改版草稿', revisionStartedDescription: '原任务仍会保留;修改后发送将在新版本中继续', rev [...]
-    attachments: { tooMany: '附件数量超过 8 个', tooLarge: '附件大小超过 50MB', duplicate: '附件来源重复,请勿重复添加同一文件。' },
     model: {
       fakeBackendLabel: '本地模拟连接',
       setupTitle: '等待配置真实模型',
@@ -687,7 +685,6 @@ const COPY = {
   },
   'zh-TW': {
     actions: { stopFailedTitle: '停止失敗', stopFailedFallback: '任務操作失敗,請稍後重試。', refreshSessionsFailedTitle: '重新整理任務列表失敗', refreshSessionsFailedFallback: '重新整理任務列表失敗,請稍後重試。', conversationErrorTitle: '任務出錯', conversationErrorFallback: '任務執行失敗,請稍後重試。', regenerateStartedTitle: '已發起重新生成', regenerateStartedDescription: '正在生成新的一輪迴答', branchCreatedTitle: '已建立分支', branchCreatedDescription: (name) => `新任務 ${name}`, revisionStartedTitle: '已建立修改版草稿', revisionStartedDescription: '原任務仍會保留;修改後傳送將在新版本中繼續', [...]
-    attachments: { tooMany: '附件數量超過 8 個', tooLarge: '附件大小超過 50MB', duplicate: '附件來源重複,請勿重複新增同一檔案。' },
     model: {
       fakeBackendLabel: '本地模擬連線',
       setupTitle: '等待設定真實模型',
@@ -918,7 +915,6 @@ const COPY = {
   },
   en: {
     actions: { stopFailedTitle: 'Failed to stop', stopFailedFallback: 'The task action failed. Try again later.', refreshSessionsFailedTitle: 'Failed to refresh tasks', refreshSessionsFailedFallback: 'The task list could not be refreshed. Try again later.', conversationErrorTitle: 'Task error', conversationErrorFallback: 'The task run failed. Try again later.', regenerateStartedTitle: 'Regeneration started', regenerateStartedDescription: 'Generating a new response', branchCreatedTitle: ' [...]
-    attachments: { tooMany: 'You can attach at most 8 files', tooLarge: 'Attachments must be 50 MB or smaller', duplicate: 'This attachment was already added.' },
     model: {
       fakeBackendLabel: 'Local simulation',
       setupTitle: 'Configure a real model',
diff --git a/apps/desktop/src/renderer/locales/plan-mode-copy.ts b/apps/desktop/src/renderer/locales/plan-mode-copy.ts
index 29f000c729..71a035ea11 100644
--- a/apps/desktop/src/renderer/locales/plan-mode-copy.ts
+++ b/apps/desktop/src/renderer/locales/plan-mode-copy.ts
@@ -18,9 +18,17 @@
  */
 
 import type { PlanExecutionStep, PlanProposal } from '@maka/core/plan';
-import type { UiCatalog, UiLocale } from '@maka/core/ui-locale';
+import { lookupCopy, type UiCatalog, type UiLocale } from '@maka/core/ui-locale';
+import type { PlanControlErrorCode } from '@maka/runtime-host/protocol';
+import type { PlanControlIpcResult } from '../../shared/plan-mode-ipc.js';
+
+export type { PlanControlErrorCode, PlanControlIpcResult };
 
 export interface PlanModeCopy {
+  readonly operationFailed: string;
+  /** Actionable copy for the structured `plan.control` / `plan.turn.start`
+   * rejection codes; anything else keeps `operationFailed`. */
+  readonly controlFailure: Record<PlanControlErrorCode, string>;
   readonly abandonConfirmation: {
     readonly title: string;
     description(title: string): string;
@@ -51,6 +59,20 @@ export interface PlanModeCopy {
 
 const COPY = {
   'zh-CN': {
+    operationFailed: '计划操作失败,请稍后重试。',
+    controlFailure: {
+      not_found: '计划方案已不存在,请刷新后重试',
+      session_busy: '当前任务正在运行,等结束后再切换计划',
+      operation_conflict: '计划已发生变化,请刷新后重试',
+      persistence_failed: '计划状态暂时无法保存,请稍后重试',
+      host_not_ready: '模型服务尚未就绪,请稍后重试',
+      unauthorized: '没有操作这个计划的权限',
+      host_draining: '模型服务正在维护,请稍后重试',
+      operation_unavailable: '计划服务暂时不可用,请稍后重试',
+      session_archived: '会话已归档,不能修改计划',
+      invalid_request: '计划操作无效,请刷新后重试',
+      internal_failure: '计划操作失败,请稍后重试',
+    },
     abandonConfirmation: {
       title: '放弃这个计划?',
       description: (title) => `“${title}”的执行记录会保留,但之后不能继续恢复。`,
@@ -70,6 +92,20 @@ const COPY = {
     },
   },
   'zh-TW': {
+    operationFailed: '計劃操作失敗,請稍後重試。',
+    controlFailure: {
+      not_found: '計劃方案已不存在,請重新整理後重試',
+      session_busy: '目前任務正在執行,等結束後再切換計劃',
+      operation_conflict: '計劃已發生變化,請重新整理後重試',
+      persistence_failed: '計劃狀態暫時無法儲存,請稍後重試',
+      host_not_ready: '模型服務尚未就緒,請稍後重試',
+      host_draining: '模型服務正在維護,請稍後重試',
+      unauthorized: '沒有操作這個計劃的權限',
+      operation_unavailable: '計劃服務暫時無法使用,請稍後重試',
+      session_archived: '會話已封存,不能修改計劃',
+      invalid_request: '計劃操作無效,請重新整理後重試',
+      internal_failure: '計劃操作失敗,請稍後重試',
+    },
     abandonConfirmation: {
       title: '放棄這個計劃?',
       description: (title) => `“${title}”的執行記錄會保留,但之後不能繼續恢復。`,
@@ -89,6 +125,20 @@ const COPY = {
     },
   },
   en: {
+    operationFailed: 'The plan action failed. Try again later.',
+    controlFailure: {
+      not_found: 'This plan proposal no longer exists. Refresh and try again.',
+      session_busy: 'A task is running in this Session. Wait for it to finish before changing the plan.',
+      operation_conflict: 'The plan changed. Refresh and try again.',
+      persistence_failed: 'The plan state could not be saved. Try again later.',
+      host_not_ready: 'The model service is not ready yet. Try again later.',
+      host_draining: 'The model service is under maintenance. Try again later.',
+      operation_unavailable: 'The plan service is temporarily unavailable. Try again later.',
+      unauthorized: 'This connection is not authorized to change the plan.',
+      session_archived: 'This Session is archived; its plan cannot change.',
+      invalid_request: 'This plan action is invalid. Refresh and try again.',
+      internal_failure: 'The plan action failed. Try again later.',
+    },
     abandonConfirmation: {
       title: 'Abandon this plan?',
       description: (title) => `The execution record for “${title}” will remain, but it cannot be resumed.`,
@@ -113,3 +163,12 @@ const COPY = {
 export function getPlanModeCopy(locale: UiLocale): PlanModeCopy {
   return COPY[locale];
 }
+
+/** Lives in the copy catalog because a validated catalog's bare package
+ * imports are exempt from the dependency-debt ratchet. */
+export function planControlFailureCopy(
+  error: Extract<PlanControlIpcResult<unknown>, { ok: false }>['error'],
+  copy: PlanModeCopy,
+): string {
+  return lookupCopy(copy.controlFailure, error.code) ?? copy.operationFailed;
+}
diff --git a/apps/desktop/src/renderer/locales/session-collaboration-copy.ts b/apps/desktop/src/renderer/locales/session-collaboration-copy.ts
index 22052bb196..100240a654 100644
--- a/apps/desktop/src/renderer/locales/session-collaboration-copy.ts
+++ b/apps/desktop/src/renderer/locales/session-collaboration-copy.ts
@@ -45,6 +45,8 @@ const ZH_CN = {
       '这枚邀请码当前只包含直接路径,不同网络中的设备可能无法连接。可稍后撤销并重新创建。',
     copy: '复制邀请码',
     copied: '邀请码已复制',
+    copyFailed: '无法复制邀请码',
+    operationFailed: '操作失败,请稍后重试',
     close: '完成',
     activeAccess: '当前访问',
     accessUnavailable: '共享管理暂时不可用;此窗口会自动重试。',
@@ -72,6 +74,8 @@ const ZH_CN = {
     recoveryStartedBody: '任务已保存在此设备;连接恢复后会自动出现在任务列表中。',
     invalidCode: '邀请码无效',
     connectionFailed: '无法加入共享任务',
+    cancelFailed: '无法取消加入共享任务',
+    mountLimit: (max: number) => `最多可保留 ${max} 个已加入的共享任务`,
     directPathUnavailable: '尚未建立直连。请确认分享者在线,并检查双方的 VPN、代理或防火墙;若网络无法打洞,可使用已知 Mesh 节点的转发。',
     insecureTitle: '此连接未加密',
     insecureBody: '访客凭据、完整任务内容和轮次请求可能被同一网络中的第三方截获。仅在你了解并接受风险时继续。',
@@ -159,6 +163,8 @@ const ZH_TW = {
       '此邀請碼目前只包含直接路徑,不同網路中的裝置可能無法連線。可稍後撤銷並重新建立。',
     copy: '複製邀請碼',
     copied: '已複製邀請碼',
+    copyFailed: '無法複製邀請碼',
+    operationFailed: '操作失敗,請稍後重試',
     close: '完成',
     activeAccess: '目前存取',
     accessUnavailable: '分享管理暫時無法使用;此視窗會自動重試。',
@@ -184,6 +190,8 @@ const ZH_TW = {
     loadingSession: '正在載入分享任務…',
     invalidCode: '邀請碼無效',
     connectionFailed: '無法加入分享任務',
+    cancelFailed: '無法取消加入分享任務',
+    mountLimit: (max: number) => `最多可保留 ${max} 個已加入的分享任務`,
     directPathUnavailable: '尚未建立直連。請確認分享者在線,並檢查雙方的 VPN、代理或防火牆;若網路無法打洞,可使用已知 Mesh 節點的轉送。',
     insecureTitle: '此連線未加密',
     insecureBody: '訪客憑證、完整任務內容和輪次請求可能被同一網路中的第三方攔截。僅在你了解並接受風險時繼續。',
@@ -266,6 +274,8 @@ const EN = {
       'This invitation currently contains direct routes only, so devices on different networks may not connect. Revoke and recreate it later.',
     copy: 'Copy invitation',
     copied: 'Invitation copied',
+    copyFailed: 'Could not copy the invitation',
+    operationFailed: 'The operation failed. Try again later.',
     close: 'Done',
     activeAccess: 'Current access',
     accessUnavailable: 'Sharing controls are temporarily unavailable. This window will retry.',
@@ -293,6 +303,8 @@ const EN = {
     recoveryStartedBody: 'The task is saved on this device and will appear in the task list when the connection recovers.',
     invalidCode: 'The invitation code is invalid',
     connectionFailed: 'Could not join the shared task',
+    cancelFailed: 'Could not cancel joining the shared task',
+    mountLimit: (max: number) => `You can retain up to ${max} joined shared tasks`,
     directPathUnavailable: 'No direct connection yet. Check that the owner is online and review VPN, proxy, or firewall settings on both devices. If hole punching is unavailable, use an approved Mesh member for transit.',
     insecureTitle: 'This connection is not encrypted',
     insecureBody: 'The Guest credential, complete task content, and Turn requests may be intercepted by others on the network. Continue only if you understand and accept the risk.',
diff --git a/apps/desktop/src/renderer/locales/shell-copy.ts b/apps/desktop/src/renderer/locales/shell-copy.ts
index 80407441a1..037fcae303 100644
--- a/apps/desktop/src/renderer/locales/shell-copy.ts
+++ b/apps/desktop/src/renderer/locales/shell-copy.ts
@@ -17,9 +17,7 @@
  * under the License.
  */
 
-import { generalizedErrorMessageForLocale } from '@maka/core/redaction';
-
-import { type UiCatalog, type UiLocale } from '@maka/core/ui-locale';
+import { type UiCatalog, type UiLocale, lookupCopy } from '@maka/core/ui-locale';
 
 import { type PermissionMode } from '@maka/core/permission';
 
@@ -28,6 +26,12 @@ import { type SettingsSection } from '@maka/core/settings';
 import { type SlashCommandIdForSurface } from '@maka/core/slash-command-catalog';
 
 import { type GoalStatus } from '@maka/core/goal';
+import {
+  classifyGeneralizedError,
+  generalizedErrorMessageForLocale,
+  unexpectedOperationFallback,
+} from '@maka/core/redaction';
+import type { AttachmentIngestBlockedCode } from '@maka/core/attachments';
 
 export const STATIC_COMMAND_IDS = [
   'action:new-chat',
@@ -344,6 +348,7 @@ type ShellCopy = {
     bypassCancelLabel: string;
     permissionFailedTitle: string;
     permissionFallback: string;
+    attachmentIngestBlocked: Record<AttachmentIngestBlockedCode, string>;
     modelFailedTitle: string;
     modelFallback: string;
     thinkingFailedTitle: string;
@@ -435,6 +440,7 @@ type ShellCopy = {
     workspaceActions: string;
   };
   app: {
+    returnToWorkHub: string;
     loadingWorkbarLabel: string;
     loadingWorkbar: string;
     useSkillPrompt(skillName: string): string;
@@ -996,6 +1002,14 @@ const SHELL_COPY_BY_LOCALE = {
       bypassCancelLabel: '保持自动',
       permissionFailedTitle: '切换权限模式失败',
       permissionFallback: '权限模式暂时无法切换,请稍后重试。',
+      attachmentIngestBlocked: {
+        item_too_large: '单个附件超出大小限制。',
+        items_invalid: '附件信息无效,请重新选择文件后再发送。',
+        count_limit: '一次最多添加 8 个附件。',
+        duplicate_source: '附件来源重复,请勿重复添加同一文件。',
+        total_size_exceeded: '附件总量超出大小限制。',
+        source_expired: '附件来源已过期或无效,请重新选择文件后再发送。',
+      },
       modelFailedTitle: '切换模型失败',
       modelFallback: '模型暂时无法切换,请稍后重试。',
       thinkingFailedTitle: '切换思考级别失败',
@@ -1160,6 +1174,7 @@ const SHELL_COPY_BY_LOCALE = {
       workspaceActions: '工作区辅助操作',
     },
     app: {
+      returnToWorkHub: '返回 WorkHub',
       loadingWorkbarLabel: '正在加载任务工作栏',
       loadingWorkbar: '正在加载任务工作栏…',
       useSkillPrompt: (skillName: string) => `使用 ${skillName} 技能:`,
@@ -1489,6 +1504,14 @@ const SHELL_COPY_BY_LOCALE = {
       bypassCancelLabel: '保持自動',
       permissionFailedTitle: '切換權限模式失敗',
       permissionFallback: '權限模式暫時無法切換,請稍後重試。',
+      attachmentIngestBlocked: {
+        item_too_large: '單一附件超出大小限制。',
+        items_invalid: '附件資訊無效,請重新選擇檔案後再傳送。',
+        count_limit: '一次最多新增 8 個附件。',
+        duplicate_source: '附件來源重複,請勿重複新增同一檔案。',
+        total_size_exceeded: '附件總量超出大小限制。',
+        source_expired: '附件來源已過期或無效,請重新選擇檔案後再傳送。',
+      },
       modelFailedTitle: '切換模型失敗',
       modelFallback: '模型暫時無法切換,請稍後重試。',
       thinkingFailedTitle: '切換思考級別失敗',
@@ -1653,6 +1676,7 @@ const SHELL_COPY_BY_LOCALE = {
       workspaceActions: '工作區輔助操作',
     },
     app: {
+      returnToWorkHub: '返回 WorkHub',
       loadingWorkbarLabel: '正在載入任務工作欄',
       loadingWorkbar: '正在載入任務工作欄…',
       useSkillPrompt: (skillName: string) => `使用 ${skillName} 技能:`,
@@ -1988,6 +2012,14 @@ const SHELL_COPY_BY_LOCALE = {
       bypassCancelLabel: 'Keep Auto',
       permissionFailedTitle: 'Could not change permission mode',
       permissionFallback: 'The permission mode could not be changed. Try again later.',
+      attachmentIngestBlocked: {
+        item_too_large: 'One attachment exceeds the size limit.',
+        items_invalid: 'The attachment list is invalid. Pick the files again and resend.',
+        count_limit: 'At most 8 attachments per message.',
+        duplicate_source: 'Duplicate attachment source. Do not add the same file twice.',
+        total_size_exceeded: 'The total attachment size exceeds the limit.',
+        source_expired: 'The attachment source expired or is invalid. Pick the files again and resend.',
+      },
       modelFailedTitle: 'Could not change model',
       modelFallback: 'The model could not be changed. Try again later.',
       thinkingFailedTitle: 'Could not change thinking level',
@@ -2191,6 +2223,7 @@ const SHELL_COPY_BY_LOCALE = {
       workspaceActions: 'Workspace actions',
     },
     app: {
+      returnToWorkHub: 'Return to WorkHub',
       loadingWorkbarLabel: 'Loading task workbar',
       loadingWorkbar: 'Loading task workbar…',
       useSkillPrompt: (skillName: string) => `Use the ${skillName} skill: `,
@@ -2284,7 +2317,22 @@ export function getShellCopy(locale: UiLocale): ShellCopy {
 }
 
 export function localizedShellErrorMessage(error: unknown, fallback: string, locale: UiLocale): string {
-  return generalizedErrorMessageForLocale(error, fallback, locale);
+  const message = error instanceof Error ? error.message : typeof error === 'string' ? error : '';
+  const maps = getShellCopy(locale).sessionSettingsActions;
+  // The reason token survives the Electron IPC wrapper and is always the
+  // message tail: a bare `attachment_ingest:<code>` from the preload probe or
+  // the IPC-wrapped error line. End-anchored so an unrelated path that merely
+  // contains the substring never matches.
+  const blocked = lookupCopy(
+    maps.attachmentIngestBlocked,
+    message.match(/(?:^|[ :"'])attachment_ingest:([a-z_]+)$/u)?.[1],
+  );
+  if (blocked) return blocked;
+  // A classified failure (timeout / rate limit / auth / provider / network)
+  // is expected; only an unrecognized one lands the redacted diagnostic.
+  return classifyGeneralizedError(error)
+    ? generalizedErrorMessageForLocale(error, fallback, locale)
+    : unexpectedOperationFallback(error, fallback, 'desktop');
 }
 
 export function sessionSettingFailureCopy(
diff --git a/apps/desktop/src/renderer/locales/work-board-error-copy.ts b/apps/desktop/src/renderer/locales/work-board-error-copy.ts
new file mode 100644
index 0000000000..c6a28da90a
--- /dev/null
+++ b/apps/desktop/src/renderer/locales/work-board-error-copy.ts
@@ -0,0 +1,62 @@
+/*
+ * 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 { UiCatalog, UiLocale } from '@maka/core/ui-locale';
+import { lookupCopy } from '@maka/core/ui-locale';
+import type { WorkBoardErrorCode } from '../../shared/work-board-ipc.js';
+
+type WorkBoardErrorCopy = Record<WorkBoardErrorCode, string>;
+
+const WORK_BOARD_ERROR_COPY = {
+  'zh-CN': {
+    invalid_input: '这项操作无效,请检查后重试',
+    not_found: '这条事项已不存在',
+    operation_conflict: '这条事项已发生变化,请刷新后重试',
+    corrupt_record: '这条事项暂时无法读取',
+    must_archive_first: '请先归档,再删除这条事项',
+    unknown: '操作失败,请稍后重试',
+  },
+  'zh-TW': {
+    invalid_input: '這項操作無效,請檢查後重試',
+    not_found: '這則事項已不存在',
+    operation_conflict: '這則事項已發生變化,請重新整理後重試',
+    corrupt_record: '這則事項暫時無法讀取',
+    must_archive_first: '請先封存,再刪除這則事項',
+    unknown: '操作失敗,請稍後重試',
+  },
+  en: {
+    invalid_input: 'This action is invalid. Check it and try again.',
+    not_found: 'This item no longer exists.',
+    operation_conflict: 'This item changed. Refresh and try again.',
+    corrupt_record: 'This item cannot be read right now.',
+    must_archive_first: 'Archive this item before deleting it.',
+    unknown: 'The action failed. Try again later.',
+  },
+} satisfies UiCatalog<WorkBoardErrorCopy>;
+
+export function getWorkBoardErrorCopy(locale: UiLocale): WorkBoardErrorCopy {
+  return WORK_BOARD_ERROR_COPY[locale];
+}
+
+export function workBoardErrorCodeCopy(
+  locale: UiLocale,
+  code: WorkBoardErrorCode,
+): string | undefined {
+  return lookupCopy(WORK_BOARD_ERROR_COPY[locale], code);
+}
diff --git a/apps/desktop/src/renderer/locales/workhub-copy.ts b/apps/desktop/src/renderer/locales/workhub-copy.ts
index 2c717ef125..86f67eedfa 100644
--- a/apps/desktop/src/renderer/locales/workhub-copy.ts
+++ b/apps/desktop/src/renderer/locales/workhub-copy.ts
@@ -148,3 +148,299 @@ const COMPOSER_COPY = {
 export function getWorkHubComposerCopy(locale: UiLocale): WorkHubComposerCopy {
   return COMPOSER_COPY[locale];
 }
+
+export interface WorkHubCopy {
+  readonly locale: UiLocale;
+  readonly subtitle: string;
+  readonly emptyTitle: string;
+  emptyBody(count: number): string;
+  workCount(count: number): string;
+  readonly clarification: string;
+  readonly chooseWork: string;
+  readonly confirmCommand: string;
+  readonly stopTargetRequired: string;
+  readonly stopTargetAmbiguous: string;
+  readonly stopTargetUnavailable: string;
+  readonly discussionStayed: string;
+  readonly discussionHint: string;
+  readonly answering: string;
+  choseWork(name: string): string;
+  readonly sentTo: string;
+  readonly createdWork: string;
+  readonly accepted: string;
+  readonly sessionFallback: string;
+  readonly newSessionFallbackTitle: string;
+  readonly stoppingWork: string;
+  readonly stopping: string;
+  readonly stopRecorded: string;
+  readonly openSessionToStop: string;
+  readonly stopOutcomes: Record<
+    'cancelled_pending' | 'stop_delivered' | 'already_terminal' | 'not_owned',
+    string
+  >;
+  readonly resumeRequested: string;
+  readonly resumeOutcomes: Record<'resume_started' | 'already_running', string>;
+  readonly resumeTargetRequired: string;
+  readonly resumeTargetAmbiguous: string;
+  readonly resumeTargetUnavailable: string;
+  readonly resumeOperationUnavailable: string;
+  readonly resumeHostRecovering: string;
+  readonly waitingForDecision: string;
+  readonly requestNotSent: string;
+  readonly waitingSummary: string;
+  readonly routing: string;
+  readonly loadFailed: string;
+  readonly loading: string;
+  readonly preparing: string;
+  readonly unavailable: string;
+  readonly coordinationFailedTitle: string;
+  readonly coordinationFailedBody: string;
+  readonly retry: string;
+  readonly submitFailures: Record<
+    'candidates_changed' | 'linked_correction_unavailable' | 'target_waiting' | 'action_changed' | 'delivery_failed',
+    string
+  >;
+  readonly scrollToBottom: string;
+  readonly archived: string;
+  readonly states: Record<'active' | 'running' | 'waiting_for_user' | 'blocked' | 'aborted', string>;
+  readonly delegationStates: Record<
+    'accepted' | 'running' | 'waiting_for_user' | 'completed' | 'failed' | 'aborted' | 'recovering',
+    string
+  >;
+  readonly assignmentLinkStates: {
+    active(execution: string): string;
+    readonly superseded: string;
+    readonly aborted: string;
+    readonly stopped: string;
+  };
+  readonly turnStates: Record<'running' | 'completed' | 'aborted' | 'failed', string>;
+}
+
+const WORKHUB_COPY = {
+  'zh-CN': {
+    locale: 'zh-CN',
+    subtitle: '在一个入口里继续、创建和查看普通 Session',
+    emptyTitle: '从这里继续所有工作',
+    emptyBody: (count: number) => count > 0
+      ? `WorkHub 会根据已有 ${count} 个 Session 判断目标;不确定时会先询问你。`
+      : '提出一个明确目标,WorkHub 会创建普通 Session 并把结果带回这里。',
+    workCount: (count: number) => `${count} 项工作`, clarification: '选择工作',
+    chooseWork: '这条输入可能与多项工作有关,请选择目标:',
+    confirmCommand: '没有开始新工作。如果需要我直接执行,请给出明确指令,例如“修复登录”。',
+    stopTargetRequired: '请明确说出要停止的工作名称,例如“停止 支付任务”。',
+    stopTargetAmbiguous: '这个名称对应多项工作;请打开具体的 Session 停止对应委托。',
+    stopTargetUnavailable: '这项工作现在没有可以停止的单个 WorkHub 委托;请打开该 Session 查看。',
+    discussionStayed: '这条内容暂时保留在 WorkHub,没有创建或改动 Session。',
+    discussionHint: '提出明确的执行目标后,我会把它交给对应的 Session。',
+    answering: '正在回答…',
+    choseWork: (name: string) => `选择“${name}”`,
+    sentTo: '已交给:', createdWork: '已创建新工作:', accepted: '已接收', sessionFallback: '普通 Session',
+    newSessionFallbackTitle: '新工作',
+    stoppingWork: '正在请求停止:', stopping: '正在处理', stopRecorded: '结果已记录',
+    openSessionToStop: '这个 Turn 不由该委托独占;请打开 Session 处理',
+    stopOutcomes: {
+      cancelled_pending: '已取消尚未开始的工作:',
+      stop_delivered: '已向运行中的工作发出停止请求:',
+      already_terminal: '这项工作已经结束:',
+      not_owned: '未停止共享或用户拥有的 Turn:',
+    },
+    resumeRequested: '已请求恢复;当前进度请查看目标 Session。',
+    resumeOutcomes: {
+      resume_started: '已让中断的工作继续:',
+      already_running: '这项工作还在跑,不需要恢复:',
+    },
+    resumeTargetRequired: '请明确说出要继续的工作名称,例如“恢复 支付任务”。',
+    resumeTargetAmbiguous: '这个名称对应多项工作;请打开具体的 Session 继续它。',
+    resumeTargetUnavailable: '这项工作当前没有可恢复的单个 WorkHub 委派。',
+    resumeOperationUnavailable: '当前 Runtime Host 未启用安全边界恢复;请打开原 Session 继续处理。',
+    resumeHostRecovering: 'Runtime Host 仍在恢复;请稍后重试。',
+    waitingForDecision: '这项工作正在等待你的决定。',
+    requestNotSent: '新请求尚未发送;处理原 Session 中的交互后可以再次发送。',
+    waitingSummary: '这项工作正在等待你的决定。新请求尚未发送;处理原 Session 中的交互后可以再次发送。',
+    routing: '正在判断应该交给哪个 Session…', loadFailed: '无法读取已有工作。',
+    loading: '正在读取已有工作…',
+    preparing: '正在准备 WorkHub…', unavailable: '暂不可用',
+    coordinationFailedTitle: 'WorkHub 暂时无法启动',
+    coordinationFailedBody: '请检查当前 Runtime Host 的默认模型配置,然后重试。',
+    retry: '重试',
+    submitFailures: {
+      candidates_changed: '工作列表已变化,请重新发送以使用最新目标。',
+      linked_correction_unavailable: '找不到可更正的有效委托关联;请重新发送,或打开原 Session 确认当前工作。',
+      target_waiting: '目标 Session 正在等待你的处理;请先打开并完成该交互。',
+      action_changed: '这次操作已发生变化,请重新发送。',
+      delivery_failed: '输入未能送达,请重试。',
+    }, scrollToBottom: '滚动到底部', archived: '已归档',
+    states: { active: '活跃', running: '进行中', waiting_for_user: '等待你', blocked: '受阻', aborted: '已中止' },
+    delegationStates: {
+      accepted: '已接收',
+      running: '进行中',
+      waiting_for_user: '等待你',
+      completed: '已完成',
+      failed: '失败',
+      aborted: '已中止',
+      recovering: '正在恢复',
+    },
+    assignmentLinkStates: {
+      active: (execution: string) => `关联有效 · ${execution}`,
+      superseded: '已被更正',
+      aborted: '更正已中止',
+      stopped: '已停止关联',
+    },
+    turnStates: { running: '进行中', completed: '已完成', aborted: '已中止', failed: '失败' },
+  },
+  'zh-TW': {
+    locale: 'zh-TW',
+    subtitle: '在一個入口繼續、建立和檢視一般 Session',
+    emptyTitle: '從這裡繼續所有工作',
+    emptyBody: (count: number) => count > 0
+      ? `WorkHub 會根據現有 ${count} 個 Session 判斷目標;不確定時會先詢問你。`
+      : '提出一個明確目標,WorkHub 會建立一般 Session 並將結果帶回這裡。',
+    workCount: (count: number) => `${count} 項工作`, clarification: '選擇工作',
+    chooseWork: '這則輸入可能與多項工作有關,請選擇目標:',
+    confirmCommand: '沒有開始新工作。如果需要我直接執行,請給出明確指令,例如「修復登入」。',
+    stopTargetRequired: '請明確說出要停止的工作名稱,例如「停止 支付任務」。',
+    stopTargetAmbiguous: '這個名稱對應多項工作;請開啟具體的 Session 停止對應委派。',
+    stopTargetUnavailable: '這項工作現在沒有可以停止的單一 WorkHub 委派;請開啟該 Session 檢視。',
+    discussionStayed: '這則內容暫時保留在 WorkHub,沒有建立或變更 Session。',
+    discussionHint: '提出明確的執行目標後,我會將它交給對應的 Session。',
+    answering: '正在回答…',
+    choseWork: (name: string) => `選擇「${name}」`,
+    sentTo: '已交給:', createdWork: '已建立新工作:', accepted: '已接收', sessionFallback: '一般 Session',
+    newSessionFallbackTitle: '新工作',
+    stoppingWork: '正在請求停止:', stopping: '正在處理', stopRecorded: '結果已記錄',
+    openSessionToStop: '這個 Turn 不由該委派獨佔;請開啟 Session 處理',
+    stopOutcomes: {
+      cancelled_pending: '已取消尚未開始的工作:',
+      stop_delivered: '已向執行中的工作發出停止請求:',
+      already_terminal: '這項工作已經結束:',
+      not_owned: '未停止共享或使用者擁有的 Turn:',
+    },
+    resumeRequested: '已請求恢復;目前進度請查看目標 Session。',
+    resumeOutcomes: {
+      resume_started: '已讓中斷的工作繼續:',
+      already_running: '這項工作仍在執行,不需要恢復:',
+    },
+    resumeTargetRequired: '請明確說出要繼續的工作名稱,例如「恢復 支付任務」。',
+    resumeTargetAmbiguous: '這個名稱對應多項工作;請開啟具體的 Session 繼續它。',
+    resumeTargetUnavailable: '這項工作目前沒有可恢復的單一 WorkHub 委派。',
+    resumeOperationUnavailable: '目前 Runtime Host 未啟用安全邊界恢復;請開啟原 Session 繼續處理。',
+    resumeHostRecovering: 'Runtime Host 仍在恢復;請稍後重試。',
+    waitingForDecision: '這項工作正在等待你的決定。',
+    requestNotSent: '新請求尚未傳送;處理原 Session 中的互動後可以再次傳送。',
+    waitingSummary: '這項工作正在等待你的決定。新請求尚未傳送;處理原 Session 中的互動後可以再次傳送。',
+    routing: '正在判斷應該交給哪個 Session…', loadFailed: '無法讀取現有工作。',
+    loading: '正在讀取現有工作…',
+    preparing: '正在準備 WorkHub…', unavailable: '暫時無法使用',
+    coordinationFailedTitle: 'WorkHub 暫時無法啟動',
+    coordinationFailedBody: '請檢查目前 Runtime Host 的預設模型設定,然後重試。',
+    retry: '重試',
+    submitFailures: {
+      candidates_changed: '工作清單已變更,請重新傳送以使用最新目標。',
+      linked_correction_unavailable: '找不到可更正的有效委派關聯;請重新傳送,或開啟原 Session 確認目前工作。',
+      target_waiting: '目標 Session 正在等待你的處理;請先開啟並完成該互動。',
+      action_changed: '這次操作已變更,請重新傳送。',
+      delivery_failed: '輸入未能送達,請重試。',
+    }, scrollToBottom: '捲動到底部', archived: '已封存',
+    states: { active: '使用中', running: '進行中', waiting_for_user: '等待你', blocked: '受阻', aborted: '已中止' },
+    delegationStates: {
+      accepted: '已接收',
+      running: '進行中',
+      waiting_for_user: '等待你',
+      completed: '已完成',
+      failed: '失敗',
+      aborted: '已中止',
+      recovering: '正在恢復',
+    },
+    assignmentLinkStates: {
+      active: (execution: string) => `關聯有效 · ${execution}`,
+      superseded: '已被更正',
+      aborted: '更正已中止',
+      stopped: '已停止關聯',
+    },
+    turnStates: { running: '進行中', completed: '已完成', aborted: '已中止', failed: '失敗' },
+  },
+  en: {
+    locale: 'en',
+    subtitle: 'Continue, create, and review ordinary Sessions from one place',
+    emptyTitle: 'Continue all work from here',
+    emptyBody: (count: number) => count > 0
+      ? `WorkHub routes against ${count} existing Session${count === 1 ? '' : 's'} and asks when the target is unclear.`
+      : 'State a clear goal and WorkHub will create an ordinary Session and bring its result back here.',
+    workCount: (count: number) => `${count} work item${count === 1 ? '' : 's'}`, clarification: 'Choose work',
+    chooseWork: 'This input may relate to more than one task. Choose a target:',
+    confirmCommand: 'I did not start new work. If you want me to do it, give a direct instruction, for example “Fix login”.',
+    stopTargetRequired: 'Name the work explicitly, for example “Stop Payments”.',
+    stopTargetAmbiguous:
+      'That name matches more than one work item. Open the exact Session to stop its delegation.',
+    stopTargetUnavailable:
+      'This work has no single WorkHub delegation to stop right now. Open its Session to see what is running.',
+    discussionStayed: 'This stayed in WorkHub without creating or changing a Session.',
+    discussionHint: 'State an executable goal and I will hand it to the owning Session.',
+    answering: 'Answering…',
+    choseWork: (name: string) => `Choose “${name}”`,
+    sentTo: 'Sent to:', createdWork: 'Created new work:', accepted: 'Accepted', sessionFallback: 'Ordinary Session',
+    newSessionFallbackTitle: 'New work',
+    stoppingWork: 'Requesting stop:', stopping: 'Stopping', stopRecorded: 'Result recorded',
+    openSessionToStop: 'This Turn is shared or user-owned. Open the Session to stop it.',
+    stopOutcomes: {
+      cancelled_pending: 'Cancelled work that had not started:',
+      stop_delivered: 'Asked the running work to stop:',
+      already_terminal: 'This work had already ended:',
+      not_owned: 'Did not stop a shared or user-owned Turn:',
+    },
+    resumeRequested: 'Resume requested. See the target Session for current progress.',
+    resumeOutcomes: {
+      resume_started: 'Carried on the interrupted work:',
+      already_running: 'This work is still running, so there was nothing to resume:',
+    },
+    resumeTargetRequired: 'Name the work explicitly, for example “Resume Payments”.',
+    resumeTargetAmbiguous:
+      'That name matches more than one work item. Open the exact Session to resume it.',
+    resumeTargetUnavailable:
+      'This work has no single WorkHub delegation that can be resumed.',
+    resumeOperationUnavailable:
+      'Safe-boundary resume is not enabled on this Runtime Host. Open the original Session to continue.',
+    resumeHostRecovering: 'The Runtime Host is still recovering. Try again shortly.',
+    waitingForDecision: 'This work is waiting for your decision.',
+    requestNotSent: 'The new request was not sent. Resolve the interaction in its Session, then send again.',
+    waitingSummary: 'This work is waiting for your decision. The new request was not sent. Resolve the interaction in its Session, then send again.',
+    routing: 'Choosing the right Session…', loadFailed: 'Could not read existing work.',
+    loading: 'Loading existing work…',
+    preparing: 'Preparing WorkHub…', unavailable: 'Unavailable',
+    coordinationFailedTitle: 'WorkHub could not start',
+    coordinationFailedBody: 'Check the default model for the current Runtime Host, then retry.',
+    retry: 'Retry',
+    submitFailures: {
+      candidates_changed: 'The work list changed. Send again to use the latest targets.',
+      linked_correction_unavailable: 'No active delegation link is available to correct. Send again, or open the original Session to confirm its current work.',
+      target_waiting: 'The target Session needs your input. Open it and resolve that interaction first.',
+      action_changed: 'This action changed. Send it again.',
+      delivery_failed: 'The input could not be delivered. Try again.',
+    }, scrollToBottom: 'Scroll to bottom', archived: 'Archived',
+    states: { active: 'Active', running: 'Running', waiting_for_user: 'Waiting for you', blocked: 'Blocked', aborted: 'Aborted' },
+    delegationStates: {
+      accepted: 'Accepted',
+      running: 'Running',
+      waiting_for_user: 'Waiting for you',
+      completed: 'Completed',
+      failed: 'Failed',
+      aborted: 'Aborted',
+      recovering: 'Recovering',
+    },
+    assignmentLinkStates: {
+      active: (execution: string) => `Active link · ${execution}`,
+      superseded: 'Superseded link',
+      aborted: 'Aborted replacement',
+      stopped: 'Stopped link',
+    },
+    turnStates: { running: 'Running', completed: 'Completed', aborted: 'Aborted', failed: 'Failed' },
+  },
+} satisfies UiCatalog<WorkHubCopy>;
+
+export function workHubAmbiguousCommandPrompt(locale: UiLocale): string {
+  return WORKHUB_COPY[locale].confirmCommand;
+}
+
+export function getWorkHubCopy(locale: UiLocale): WorkHubCopy {
+  return WORKHUB_COPY[locale];
+}
diff --git a/apps/desktop/src/renderer/plan-mode-panel.tsx b/apps/desktop/src/renderer/plan-mode-panel.tsx
index 37c5b3d850..9aad7d493e 100644
--- a/apps/desktop/src/renderer/plan-mode-panel.tsx
+++ b/apps/desktop/src/renderer/plan-mode-panel.tsx
@@ -23,8 +23,14 @@ import type { SessionEvent } from '@maka/core/events';
 import type { SessionSummary } from '@maka/core/session';
 import { Banner } from '@astryxdesign/core/Banner';
 import { Collapsible } from '@astryxdesign/core/Collapsible';
-import { Badge, type BadgeVariant, Button as UiButton, useToast, useUiLocale } from '@maka/ui';
-import { getPlanModeCopy, type PlanModeCopy } from './locales/plan-mode-copy.js';
+import { Badge, type BadgeVariant, Button as UiButton, useToast, useUiLocale, type UiLocale } from '@maka/ui';
+import { reportUnexpectedError } from './application/contracts/operation-diagnostics.js';
+import type { PlanControlIpcResult } from '../shared/plan-mode-ipc.js';
+import {
+  getPlanModeCopy,
+  planControlFailureCopy,
+  type PlanModeCopy,
+} from './locales/plan-mode-copy.js';
 
 export interface PlanModeState {
   state: PlanSessionState | undefined;
@@ -38,7 +44,8 @@ export interface PlanModeState {
 
 export function usePlanModeState(session: SessionSummary | undefined): PlanModeState {
   const toastApi = useToast();
-  const copy = getPlanModeCopy(useUiLocale());
+  const locale = useUiLocale();
+  const copy = getPlanModeCopy(locale);
   const [state, setState] = useState<PlanSessionState>();
   const [pending, setPending] = useState(false);
   const [error, setError] = useState<string>();
@@ -64,7 +71,10 @@ export function usePlanModeState(session: SessionSummary | undefined): PlanModeS
     setState(undefined);
     setError(undefined);
     if (!session) return;
-    const refreshOrReport = () => void refresh().catch((cause) => setError(message(cause)));
+    const refreshOrReport = () => void refresh().catch((cause) => {
+      reportUnexpectedError('plan-mode:refresh', cause);
+      setError(copy.operationFailed);
+    });
     refreshOrReport();
     const unsubscribeEvents = window.maka.sessions.subscribeEvents(session.id, (event: SessionEvent) => {
       if (
@@ -83,25 +93,32 @@ export function usePlanModeState(session: SessionSummary | undefined): PlanModeS
       unsubscribeEvents();
       unsubscribePlanChanges();
     };
-  }, [session?.id, session?.collaborationMode, refresh]);
-
-  const run = useCallback(async (action: () => Promise<void>): Promise<void> => {
-    setPending(true);
-    setError(undefined);
-    try {
-      await action();
-      await refresh();
-    } catch (cause) {
-      setError(message(cause));
-    } finally {
-      setPending(false);
-    }
-  }, [refresh]);
+  }, [copy.operationFailed, session?.id, session?.collaborationMode, refresh]);
+  const run = useCallback(
+    async (action: () => Promise<PlanControlIpcResult<unknown>>): Promise<void> => {
+      setPending(true);
+      setError(undefined);
+      try {
+        const result = await action();
+        if (!result.ok) {
+          setError(planControlFailureCopy(result.error, copy));
+          return;
+        }
+        await refresh();
+      } catch (cause) {
+        reportUnexpectedError('plan-mode:action', cause);
+        setError(copy.operationFailed);
+      } finally {
+        setPending(false);
+      }
+    },
+    [copy, refresh],
+  );
 
   const requestRevision = useCallback(async (proposalId: string): Promise<void> => {
     if (!session) return;
     await run(async () => {
-      await window.maka.sessions.requestPlanRevision(session.id, proposalId);
+      return window.maka.sessions.requestPlanRevision(session.id, proposalId);
     });
   }, [run, session?.id]);
 
@@ -123,13 +140,14 @@ export function usePlanModeState(session: SessionSummary | undefined): PlanModeS
           };
     approvalRetry.current = input;
     await run(async () => {
-      await window.maka.sessions.approvePlan(session.id, {
+      const result = await window.maka.sessions.approvePlan(session.id, {
         proposalId: input.proposalId,
         expectedRevision: input.expectedRevision,
         expectedStoreVersion: input.expectedStoreVersion,
         turnId: input.turnId,
       });
-      approvalRetry.current = undefined;
+      if (result.ok) approvalRetry.current = undefined;
+      return result;
     });
   }, [run, session?.id, state]);
 
@@ -142,8 +160,9 @@ export function usePlanModeState(session: SessionSummary | undefined): PlanModeS
         : { sessionId: session.id, executionId, turnId: crypto.randomUUID() };
     resumeRetry.current = input;
     await run(async () => {
-      await window.maka.sessions.resumePlan(session.id, executionId, input.turnId);
-      resumeRetry.current = undefined;
+      const result = await window.maka.sessions.resumePlan(session.id, executionId, input.turnId);
+      if (result.ok) resumeRetry.current = undefined;
+      return result;
     });
   }, [run, session?.id]);
 
@@ -158,7 +177,7 @@ export function usePlanModeState(session: SessionSummary | undefined): PlanModeS
     });
     if (!confirmed) return;
     await run(async () => {
-      await window.maka.sessions.abandonPlanExecution(session.id, executionId);
+      return window.maka.sessions.abandonPlanExecution(session.id, executionId);
     });
   }, [copy, run, session?.id, toastApi]);
 
@@ -366,7 +385,3 @@ function executionStepMark(status: PlanExecutionStep['status']): string {
   if (status === 'skipped') return '–';
   return '';
 }
-
-function message(error: unknown): string {
-  return error instanceof Error ? error.message : String(error);
-}
diff --git a/apps/desktop/src/renderer/remote-project-directory-dialog.tsx b/apps/desktop/src/renderer/remote-project-directory-dialog.tsx
index ec57af4206..2209482e0f 100644
--- a/apps/desktop/src/renderer/remote-project-directory-dialog.tsx
+++ b/apps/desktop/src/renderer/remote-project-directory-dialog.tsx
@@ -26,13 +26,14 @@ import { Layout, LayoutContent, LayoutFooter } from '@astryxdesign/core/Layout';
 import { HStack } from '@astryxdesign/core/Stack';
 import { Text } from '@astryxdesign/core/Text';
 import { useUiLocale } from '@maka/ui';
+import { reportUnexpectedError } from './application/contracts/operation-diagnostics.js';
 import { Check, Eye, EyeOff, FolderOpen } from '@maka/ui/icons';
 import type {
   DesktopProjectDirectoryEntry,
   DesktopProjectDirectoryRoot,
   DesktopRuntimeHostRef,
 } from '../preload/bridge-contract.js';
-import { getShellCopy, localizedShellErrorMessage } from './locales/shell-copy.js';
+import { getShellCopy } from './locales/shell-copy.js';
 
 type DirectoryHost = DesktopRuntimeHostRef & { readonly name?: string };
 type DirectoryLoad =
@@ -43,10 +44,7 @@ type DirectoryLoad =
       readonly root: DesktopProjectDirectoryRoot;
       readonly segments: readonly string[];
     };
-type DirectoryError = {
-  readonly message: string;
-  readonly retryable: boolean;
-};
+type DirectoryError = 'read_failed' | 'registration_failed';
 
 export function RemoteProjectDirectoryDialog(props: {
   host?: DirectoryHost;
@@ -133,10 +131,8 @@ export function RemoteProjectDirectoryDialog(props: {
       setEntries(next);
     } catch (cause) {
       if (request.current !== sequence) return;
-      setError({
-        message: localizedShellErrorMessage(cause, copy.readPathFailedFallback, locale),
-        retryable: true,
-      });
+      reportUnexpectedError('project-directory:list', cause);
+      setError('read_failed');
     } finally {
       if (request.current === sequence) setLoading(false);
     }
@@ -174,10 +170,8 @@ export function RemoteProjectDirectoryDialog(props: {
       props.onRegistered(project, host);
     } catch (cause) {
       if (request.current !== sequence) return;
-      setError({
-        message: localizedShellErrorMessage(cause, copy.projectUpdateFailedFallback, locale),
-        retryable: false,
-      });
+      reportUnexpectedError('project-directory:register', cause);
+      setError('registration_failed');
     } finally {
       if (request.current === sequence) setRegistering(false);
     }
@@ -276,8 +270,12 @@ export function RemoteProjectDirectoryDialog(props: {
               </nav>
               {error ? (
                 <div className="remoteProjectDirectoryError" role="alert">
-                  <Text type="body" color="secondary">{error.message}</Text>
-                  {error.retryable ? (
+                  <Text type="body" color="secondary">
+                    {error === 'read_failed'
+                      ? copy.readPathFailedFallback
+                      : copy.projectUpdateFailedFallback}
+                  </Text>
+                  {error === 'read_failed' ? (
                     <Button label={copy.remoteDirectoryRetry} variant="ghost" onClick={retry} />
                   ) : null}
                 </div>
diff --git a/apps/desktop/src/renderer/session-collaboration-dialog.tsx b/apps/desktop/src/renderer/session-collaboration-dialog.tsx
index d2a6425761..99f4305907 100644
--- a/apps/desktop/src/renderer/session-collaboration-dialog.tsx
+++ b/apps/desktop/src/renderer/session-collaboration-dialog.tsx
@@ -30,6 +30,7 @@ import {
   useToast,
   useUiLocale,
 } from '@maka/ui';
+import { reportUnexpectedError } from './application/contracts/operation-diagnostics.js';
 import type {
   CollaborationAccessQueryResult,
   CollaborationInvitationPrepareResult,
@@ -182,7 +183,8 @@ function ShareSessionDialog(props: ShareSessionDialogProps) {
       setInvitation(created.invitation);
       await refresh();
     } catch (error) {
-      toast.error(copy.shareTitle, errorMessage(error));
+      reportUnexpectedError('session-collaboration:prepare', error);
+      toast.error(copy.shareTitle, copy.operationFailed);
     } finally {
       setWorking(false);
     }
@@ -200,7 +202,8 @@ function ShareSessionDialog(props: ShareSessionDialogProps) {
       await navigator.clipboard.writeText(invitation.invitationCode);
       toast.success(copy.copied);
     } catch (error) {
-      toast.error(copy.shareTitle, errorMessage(error));
+      reportUnexpectedError('session-collaboration:copy', error);
+      toast.error(copy.shareTitle, copy.copyFailed);
     }
   }
 
@@ -210,7 +213,8 @@ function ShareSessionDialog(props: ShareSessionDialogProps) {
       await window.maka.sessionCollaboration.revokePrincipal(props.sessionId, principalId);
       await refresh();
     } catch (error) {
-      toast.error(copy.shareTitle, errorMessage(error));
+      reportUnexpectedError('session-collaboration:revoke-principal', error);
+      toast.error(copy.shareTitle, copy.operationFailed);
     } finally {
       setWorking(false);
     }
@@ -225,7 +229,8 @@ function ShareSessionDialog(props: ShareSessionDialogProps) {
       );
       await refresh();
     } catch (error) {
-      toast.error(copy.shareTitle, errorMessage(error));
+      reportUnexpectedError('session-collaboration:revoke-grant', error);
+      toast.error(copy.shareTitle, copy.operationFailed);
     } finally {
       setWorking(false);
     }
@@ -244,7 +249,8 @@ function ShareSessionDialog(props: ShareSessionDialogProps) {
       );
       await refresh();
     } catch (error) {
-      toast.error(copy.turnRequests, errorMessage(error));
+      reportUnexpectedError('session-collaboration:decide-turn-request', error);
+      toast.error(copy.turnRequests, copy.operationFailed);
     } finally {
       setWorking(false);
     }
@@ -436,7 +442,3 @@ function guestIdentityLabel(principalId: string, label: string): string {
   const identity = principalId.includes(':') ? principalId.slice(principalId.lastIndexOf(':') + 1) : principalId;
   return `${label} ${identity.slice(0, 8)}`;
 }
-
-function errorMessage(error: unknown): string {
-  return error instanceof Error ? error.message : String(error);
-}
diff --git a/apps/desktop/src/renderer/use-work-board.ts b/apps/desktop/src/renderer/use-work-board.ts
index 3589f430c6..e83c351d25 100644
--- a/apps/desktop/src/renderer/use-work-board.ts
+++ b/apps/desktop/src/renderer/use-work-board.ts
@@ -26,15 +26,23 @@ import {
   type WorkBoardListQuery,
   type WorkBoardPage,
 } from '@maka/core/work-board';
-import type { WorkBoardMutationOptions } from '@maka/storage/work-board-store';
-import type { WorkBoardChangedEvent, WorkBoardIpcResult } from '../shared/work-board-ipc.js';
+import type {
+  WorkBoardChangedEvent,
+  WorkBoardErrorCode,
+  WorkBoardIpcResult,
+  WorkBoardMutationOptions,
+} from '../shared/work-board-ipc.js';
+import {
+  ExpectedOperationError,
+  reportUnexpectedError,
+} from './application/contracts/operation-diagnostics.js';
 
 interface WorkBoardSnapshot {
   items: WorkBoardItem[];
   nextCursor?: string;
   loading: boolean;
-  error?: string;
-  continuationError?: string;
+  error?: WorkBoardErrorCode;
+  continuationError?: WorkBoardErrorCode;
   continuationCursor?: string;
 }
 
@@ -94,7 +102,7 @@ async function listWindow(
 
 function requireResult<T>(result: WorkBoardIpcResult<T>): T {
   if (result.ok) return result.value;
-  throw new Error(result.message);
+  throw new ExpectedOperationError(result.error.code);
 }
 
 /**
@@ -137,20 +145,20 @@ export function useWorkBoard(query?: WorkBoardListQuery): UseWorkBoardResult {
             items: current.items,
             nextCursor: current.nextCursor,
             loading: false,
-            error: current.items.length === 0 ? result.message : undefined,
-            continuationError: current.items.length > 0 ? result.message : undefined,
+            error: current.items.length === 0 ? result.error.code : undefined,
+            continuationError: current.items.length > 0 ? result.error.code : undefined,
             continuationCursor: undefined,
           }));
         },
         (error: unknown) => {
           if (revision !== revisionRef.current) return;
-          const message = error instanceof Error ? error.message : 'Work Board load failed';
+          reportUnexpectedError('work-board:list', error);
           setSnapshot((current) => ({
             items: current.items,
             nextCursor: current.nextCursor,
             loading: false,
-            error: current.items.length === 0 ? message : undefined,
-            continuationError: current.items.length > 0 ? message : undefined,
+            error: current.items.length === 0 ? 'unknown' : undefined,
+            continuationError: current.items.length > 0 ? 'unknown' : undefined,
             continuationCursor: undefined,
           }));
         },
@@ -188,7 +196,7 @@ export function useWorkBoard(query?: WorkBoardListQuery): UseWorkBoardResult {
           setSnapshot((current) => ({
             ...current,
             loading: false,
-            continuationError: result.message,
+            continuationError: result.error.code,
             continuationCursor: cursor,
           }));
           return;
@@ -211,11 +219,11 @@ export function useWorkBoard(query?: WorkBoardListQuery): UseWorkBoardResult {
       },
       (error: unknown) => {
         if (revision !== revisionRef.current) return;
-        const message = error instanceof Error ? error.message : 'Work Board load failed';
+        reportUnexpectedError('work-board:continuation', error);
         setSnapshot((current) => ({
           ...current,
           loading: false,
-          continuationError: message,
+          continuationError: 'unknown',
           continuationCursor: cursor,
         }));
       },
diff --git a/apps/desktop/src/renderer/work-board-panel.tsx b/apps/desktop/src/renderer/work-board-panel.tsx
index cec058ea1c..3025648e61 100644
--- a/apps/desktop/src/renderer/work-board-panel.tsx
+++ b/apps/desktop/src/renderer/work-board-panel.tsx
@@ -21,7 +21,9 @@ import { useEffect, useMemo, useRef, useState } from 'react';
 import { Banner, EmptyState, Spinner } from '@astryxdesign/core';
 import { Button } from '@astryxdesign/core/Button';
 import { TextInput } from '@astryxdesign/core/TextInput';
-import { useUiLocale } from '@maka/ui';
+import { useUiLocale, type UiLocale } from '@maka/ui';
+
+import { ExpectedOperationError, unexpectedErrorFallback } from './application/contracts/operation-diagnostics.js';
 import type {
   CreateWorkBoardItemInput,
   WorkBoardItem,
@@ -31,6 +33,9 @@ import type {
 import { ListTodo } from '@maka/ui/icons';
 import { useWorkBoard } from './use-work-board.js';
 import { getDesktopConversationCopy } from './locales/conversation-copy.js';
+import {
+  workBoardErrorCodeCopy,
+} from './locales/work-board-error-copy.js';
 
 type WorkBoardPanelCopy = ReturnType<typeof getDesktopConversationCopy>['workBoardPanel'];
 
@@ -139,7 +144,8 @@ export function WorkBoardPanel(props: {
   projectId: string | null;
   projectAliases?: readonly string[];
 }) {
-  const copy = getDesktopConversationCopy(useUiLocale()).workBoardPanel;
+  const locale = useUiLocale();
+  const copy = getDesktopConversationCopy(locale).workBoardPanel;
   const [filter, setFilter] = useState<'inbox' | 'project'>('inbox');
   const projectScopeIds = useMemo(() => {
     if (props.projectId === null) return undefined;
@@ -175,7 +181,7 @@ export function WorkBoardPanel(props: {
       await action();
       return true;
     } catch (error) {
-      setActionError(error instanceof Error ? error.message : copy.actionFailed);
+      setActionError(workBoardActionErrorText(error, locale, copy.actionFailed));
       return false;
     }
   };
@@ -283,7 +289,7 @@ export function WorkBoardPanel(props: {
           role="alert"
           className="maka-work-board-message"
           title={copy.loadFailed}
-          description={board.error}
+          description={workBoardErrorCodeCopy(locale, board.error) ?? copy.actionFailed}
           endContent={
             <Button size="sm" variant="ghost" label={copy.retry} onClick={board.retry} />
           }
@@ -296,7 +302,7 @@ export function WorkBoardPanel(props: {
               role="alert"
               className="maka-work-board-message"
               title={copy.loadFailed}
-              description={board.continuationError}
+              description={workBoardErrorCodeCopy(locale, board.continuationError) ?? copy.actionFailed}
               endContent={
                 <Button
                   size="sm"
@@ -388,3 +394,14 @@ export function WorkBoardPanel(props: {
     </section>
   );
 }
+
+/** Resolve one thrown action failure into Work Board copy: expected codes map
+ * through the catalog, everything else logs redacted diagnostics and shows
+ * the action-failed fallback. Module-level so the presentation test drives the
+ * exact branch `runAction` uses instead of re-implementing it. */
+export function workBoardActionErrorText(error: unknown, locale: UiLocale, fallback: string): string {
+  if (error instanceof ExpectedOperationError) {
+    return workBoardErrorCodeCopy(locale, error.code) ?? fallback;
+  }
+  return unexpectedErrorFallback(error, fallback, 'work-board');
+}
diff --git a/apps/desktop/src/renderer/workhub-controller.ts b/apps/desktop/src/renderer/workhub-controller.ts
index 56f4fbbab0..5c00022927 100644
--- a/apps/desktop/src/renderer/workhub-controller.ts
+++ b/apps/desktop/src/renderer/workhub-controller.ts
@@ -43,6 +43,8 @@ import type {
   WorkHubCoordinationActResult,
   WorkHubCoordinationCandidatesResult,
 } from '@maka/runtime-host/protocol';
+import { ExpectedOperationError } from './application/contracts/operation-diagnostics.js';
+
 
 /**
  * A Host operation the Coordination port could not complete. It lives beside
@@ -165,6 +167,7 @@ export interface WorkHubSubmitInput {
   newWorkDefaults?: WorkHubCoordinationActInput['newWorkDefaults'];
   requestId: string;
   text: string;
+  newSessionFallbackTitle: string;
   retryAction?: true;
   explicitTarget?: WorkHubSessionTarget;
   correction?: WorkHubCorrectionContext;
@@ -289,6 +292,10 @@ export interface WorkHubController {
   resetVisitContext(): void;
 }
 
+export type WorkHubExpectedFailureCode =
+  | 'candidates_changed'
+  | 'linked_correction_unavailable';
+
 export function createWorkHubController(deps: {
   sessions: WorkHubSessionPort;
   coordination: WorkHubCoordinationPort;
@@ -306,7 +313,9 @@ export function createWorkHubController(deps: {
   ): WorkHubCorrectionContext => {
     const sourceActionId = candidateBySessionId.get(from.sessionId)?.latestDelegationActionId;
     if (!sourceActionId) {
-      throw new Error('WorkHub linked correction requires an active durable delegation');
+      throw new ExpectedOperationError<WorkHubExpectedFailureCode>(
+        'linked_correction_unavailable',
+      );
     }
     return { from, sourceActionId };
   };
@@ -631,6 +640,7 @@ export function createWorkHubController(deps: {
         sessions: routable,
         originPromptBySessionId: new Map(routingEvidence.map((entry) => [entry.target.sessionId, entry.originPrompt])),
         ...(input.explicitTarget ? { explicitTarget: input.explicitTarget } : {}),
+        newSessionFallbackTitle: input.newSessionFallbackTitle,
         ...(evidence ? { interpretation: {
           classification: evidence.classification,
           resolution: evidence.resolution.kind,
@@ -720,7 +730,7 @@ export function createWorkHubController(deps: {
         (session) => session.target.sessionId === target.sessionId,
       );
       if (!targetSession) {
-        throw new Error('WorkHub target Session is unavailable');
+        throw new ExpectedOperationError<WorkHubExpectedFailureCode>('candidates_changed');
       }
       if (targetSession?.state === 'waiting_for_user' && !input.retryAction) {
         return {
@@ -733,7 +743,7 @@ export function createWorkHubController(deps: {
       }
       const candidate = candidateBySessionId.get(target.sessionId);
       if (!candidate) {
-        throw new Error('WorkHub target Session is unavailable');
+        throw new ExpectedOperationError<WorkHubExpectedFailureCode>('candidates_changed');
       }
       const action: WorkHubCoordinationActInput = correction
         ? {
diff --git a/apps/desktop/src/renderer/workhub-surface.tsx b/apps/desktop/src/renderer/workhub-surface.tsx
index b62a4f5955..a215fab538 100644
--- a/apps/desktop/src/renderer/workhub-surface.tsx
+++ b/apps/desktop/src/renderer/workhub-surface.tsx
@@ -27,6 +27,10 @@ import {
 import { Button } from '@astryxdesign/core/Button';
 import type { UiLocale } from '@maka/core/ui-locale';
 import { ChatSurfaceLayout } from '@maka/ui';
+import {
+  ExpectedOperationError,
+  reportUnexpectedError,
+} from './application/contracts/operation-diagnostics.js';
 import {
   type WorkHubController,
   type WorkHubCoordinationTurn,
@@ -43,7 +47,9 @@ import {
   type WorkHubSendAttempt,
 } from './workhub-send-lease.js';
 import { WorkHubHighlightContext, WorkHubHighlightProvider, workHubIdentityHue, WorkHubNavigationRail, WorkHubPromptRail, WorkHubComposer, type WorkHubComposerServices, type WorkHubComposerSelection } from './features/workhub/index.js';
-import { getWorkHubRailCopy } from './locales/workhub-copy.js';
+import { getWorkHubCopy, getWorkHubRailCopy } from './locales/workhub-copy.js';
+
+export { workHubAmbiguousCommandPrompt } from './locales/workhub-copy.js';
 
 export interface WorkHubConversationTurn {
   requestId: string;
@@ -99,6 +105,12 @@ export function workHubSubmissionClearsDraft(
 }
 
 export function workHubSurfaceFailure(error: unknown): WorkHubSurfaceFailure {
+  if (
+    error instanceof ExpectedOperationError &&
+    (error.code === 'candidates_changed' || error.code === 'linked_correction_unavailable')
+  ) {
+    return error.code;
+  }
   if (error instanceof WorkHubCoordinationFailure) {
     if (error.code === 'operation_conflict') return 'action_changed';
     if (error.code === 'not_found' || error.code === 'session_archived') {
@@ -107,21 +119,7 @@ export function workHubSurfaceFailure(error: unknown): WorkHubSurfaceFailure {
     if (error.code === 'session_busy') return 'target_waiting';
     return 'delivery_failed';
   }
-  const message = error instanceof Error ? error.message : '';
-  if (
-    /candidates changed|not in the admitted candidate set|source or target is not in/iu.test(
-      message,
-    )
-  ) {
-    return 'candidates_changed';
-  }
-  if (/linked correction requires (?:persistent delegation support|an active durable delegation)/iu.test(message)) {
-    return 'linked_correction_unavailable';
-  }
-  if (/waiting for user input/iu.test(message)) return 'target_waiting';
-  if (/identity belongs to a different proposal/iu.test(message)) {
-    return 'action_changed';
-  }
+  reportUnexpectedError('workhub:submission', error);
   return 'delivery_failed';
 }
 
@@ -232,7 +230,7 @@ interface WorkHubSurfaceProps {
 }
 
 export function WorkHubSurface(props: WorkHubSurfaceProps) {
-  const copy = workHubCopy(props.locale);
+  const copy = getWorkHubCopy(props.locale);
   const railCopy = getWorkHubRailCopy(props.locale);
   const [projection, setProjection] = useState<WorkHubProjection>({ sessions: [], turns: [] });
   const [coordination, setCoordination] = useState<{
@@ -310,7 +308,7 @@ export function WorkHubSurface(props: WorkHubSurfaceProps) {
   }, [props.controller]);
 
   const route = useCallback(async (
-    input: WorkHubSubmitInput,
+    input: Omit<WorkHubSubmitInput, 'newSessionFallbackTitle'>,
     localRequestId: string = input.requestId,
     recordedUserText: string = input.text,
   ): Promise<WorkHubSubmission | undefined> => {
@@ -324,7 +322,7 @@ export function WorkHubSurface(props: WorkHubSurfaceProps) {
       try {
         const result = await submitAndRecordWorkHubSurfaceInput({
           controller: props.controller,
-          request: input,
+          request: { ...input, newSessionFallbackTitle: copy.newSessionFallbackTitle },
           recordedUserText,
           summary: (result) => workHubCoordinationSummary(result, projection, copy),
           // Clarification remains a local transcript write; delegated sends
@@ -535,7 +533,7 @@ export function WorkHubCoordinationStatus(props: {
   state: 'resolving' | 'failed';
   onRetry(): void;
 }) {
-  const copy = workHubCopy(props.locale);
+  const copy = getWorkHubCopy(props.locale);
   const resolving = props.state === 'resolving';
   return (
     <ChatSurfaceLayout
@@ -618,7 +616,7 @@ export function WorkHubCoordinationTurnView(props: {
   locale: UiLocale;
   onOpenSession(sessionId: string): void;
 }) {
-  const copy = workHubCopy(props.locale);
+  const copy = getWorkHubCopy(props.locale);
   const assignment = props.turn.assignment;
   const session = assignment
     ? props.projection.sessions.find(
@@ -698,7 +696,7 @@ export function WorkHubCoordinationTurnView(props: {
  */
 function workHubClarificationPrompt(
   reason: Extract<WorkHubSubmission, { kind: 'clarification' }>['reason'],
-  copy: ReturnType<typeof workHubCopy>,
+  copy: ReturnType<typeof getWorkHubCopy>,
 ): string | undefined {
   if (reason === 'ambiguous_command') return copy.confirmCommand;
   if (reason === 'stop_target_required') return copy.stopTargetRequired;
@@ -715,7 +713,7 @@ function workHubClarificationPrompt(
 export function workHubCoordinationSummary(
   result: Exclude<WorkHubSubmission, { kind: 'discussion' }>,
   projection: WorkHubProjection,
-  copy: ReturnType<typeof workHubCopy>,
+  copy: ReturnType<typeof getWorkHubCopy>,
 ): string {
   if (result.kind === 'clarification') {
     const prompt = workHubClarificationPrompt(result.reason, copy);
@@ -727,7 +725,7 @@ export function workHubCoordinationSummary(
     return `${copy.chooseWork} ${result.options.map(({ sessionName }) => sessionName).join('、')}`;
   }
   if (result.kind === 'waiting') {
-    return `${copy.waitingForDecision} ${copy.requestNotSent}`;
+    return copy.waitingSummary;
   }
   if (result.kind === 'stop') return copy.stopOutcomes[result.outcome];
   if (result.kind === 'resume') return copy.resumeRequested;
@@ -746,7 +744,7 @@ export function workHubCoordinationSummary(
 function WorkHubTurnView(props: {
   turn: WorkHubConversationTurn;
   projection: WorkHubProjection;
-  copy: ReturnType<typeof workHubCopy>;
+  copy: ReturnType<typeof getWorkHubCopy>;
   pending: boolean;
   onChoose(target: { sessionId: string }): void;
   onOpenSession(sessionId: string): void;
@@ -928,7 +926,7 @@ function SubmittedWorkView(props: {
   heading: string;
   state: string;
   result: string | undefined;
-  copy: ReturnType<typeof workHubCopy>;
+  copy: ReturnType<typeof getWorkHubCopy>;
   onOpenSession(sessionId: string): void;
 }) {
   const { session, copy } = props;
@@ -951,233 +949,3 @@ function SubmittedWorkView(props: {
     </div>
   );
 }
-
-export function workHubAmbiguousCommandPrompt(locale: UiLocale): string {
-  if (locale === 'zh-CN') {
-    return '没有开始新工作。如果需要我直接执行,请给出明确指令,例如“修复登录”。';
-  }
-  if (locale === 'zh-TW') {
-    return '沒有開始新工作。如果需要我直接執行,請給出明確指令,例如「修復登入」。';
-  }
-  return 'I did not start new work. If you want me to do it, give a direct instruction, for example “Fix login”.';
-}
-
-function workHubCopy(locale: UiLocale) {
-  if (locale === 'zh-CN') {
-    return {
-      locale,
-      subtitle: '在一个入口里继续、创建和查看普通 Session',
-      emptyTitle: '从这里继续所有工作',
-      emptyBody: (count: number) => count > 0
-        ? `WorkHub 会根据已有 ${count} 个 Session 判断目标;不确定时会先询问你。`
-        : '提出一个明确目标,WorkHub 会创建普通 Session 并把结果带回这里。',
-      workCount: (count: number) => `${count} 项工作`, clarification: '选择工作',
-      chooseWork: '这条输入可能与多项工作有关,请选择目标:',
-      confirmCommand: workHubAmbiguousCommandPrompt(locale),
-      stopTargetRequired: '请明确说出要停止的工作名称,例如“停止 支付任务”。',
-      stopTargetAmbiguous: '这个名称对应多项工作;请打开具体的 Session 停止对应委托。',
-      stopTargetUnavailable: '这项工作现在没有可以停止的单个 WorkHub 委托;请打开该 Session 查看。',
-      discussionStayed: '这条内容暂时保留在 WorkHub,没有创建或改动 Session。',
-      discussionHint: '提出明确的执行目标后,我会把它交给对应的 Session。',
-      answering: '正在回答…',
-      choseWork: (name: string) => `选择“${name}”`,
-      sentTo: '已交给:', createdWork: '已创建新工作:', accepted: '已接收', sessionFallback: '普通 Session',
-      stoppingWork: '正在请求停止:', stopping: '正在处理', stopRecorded: '结果已记录',
-      openSessionToStop: '这个 Turn 不由该委托独占;请打开 Session 处理',
-      stopOutcomes: {
-        cancelled_pending: '已取消尚未开始的工作:',
-        stop_delivered: '已向运行中的工作发出停止请求:',
-        already_terminal: '这项工作已经结束:',
-        not_owned: '未停止共享或用户拥有的 Turn:',
-      },
-      resumeRequested: '已请求恢复;当前进度请查看目标 Session。',
-      resumeOutcomes: {
-        resume_started: '已让中断的工作继续:',
-        already_running: '这项工作还在跑,不需要恢复:',
-      },
-      resumeTargetRequired: '请明确说出要继续的工作名称,例如“恢复 支付任务”。',
-      resumeTargetAmbiguous: '这个名称对应多项工作;请打开具体的 Session 继续它。',
-      resumeTargetUnavailable: '这项工作当前没有可恢复的单个 WorkHub 委派。',
-      resumeOperationUnavailable: '当前 Runtime Host 未启用安全边界恢复;请打开原 Session 继续处理。',
-      resumeHostRecovering: 'Runtime Host 仍在恢复;请稍后重试。',
-      waitingForDecision: '这项工作正在等待你的决定。',
-      requestNotSent: '新请求尚未发送;处理原 Session 中的交互后可以再次发送。',
-      routing: '正在判断应该交给哪个 Session…', loadFailed: '无法读取已有工作。',
-      loading: '正在读取已有工作…',
-      preparing: '正在准备 WorkHub…', unavailable: '暂不可用',
-      coordinationFailedTitle: 'WorkHub 暂时无法启动',
-      coordinationFailedBody: '请检查当前 Runtime Host 的默认模型配置,然后重试。',
-      retry: '重试',
-      submitFailures: {
-        candidates_changed: '工作列表已变化,请重新发送以使用最新目标。',
-        linked_correction_unavailable: '找不到可更正的有效委托关联;请重新发送,或打开原 Session 确认当前工作。',
-        target_waiting: '目标 Session 正在等待你的处理;请先打开并完成该交互。',
-        action_changed: '这次操作已发生变化,请重新发送。',
-        delivery_failed: '输入未能送达,请重试。',
-      }, scrollToBottom: '滚动到底部', archived: '已归档',
-      states: { active: '活跃', running: '进行中', waiting_for_user: '等待你', blocked: '受阻', aborted: '已中止' },
-      delegationStates: {
-        accepted: '已接收',
-        running: '进行中',
-        waiting_for_user: '等待你',
-        completed: '已完成',
-        failed: '失败',
-        aborted: '已中止',
-        recovering: '正在恢复',
-      },
-      assignmentLinkStates: {
-        active: (execution: string) => `关联有效 · ${execution}`,
-        superseded: '已被更正',
-        aborted: '更正已中止',
-        stopped: '已停止关联',
-      },
-      turnStates: { running: '进行中', completed: '已完成', aborted: '已中止', failed: '失败' },
-    } as const;
-  }
-  if (locale === 'zh-TW') {
-    return {
-      locale,
-      subtitle: '在一個入口繼續、建立和檢視一般 Session',
-      emptyTitle: '從這裡繼續所有工作',
-      emptyBody: (count: number) => count > 0
-        ? `WorkHub 會根據現有 ${count} 個 Session 判斷目標;不確定時會先詢問你。`
-        : '提出一個明確目標,WorkHub 會建立一般 Session 並將結果帶回這裡。',
-      workCount: (count: number) => `${count} 項工作`, clarification: '選擇工作',
-      chooseWork: '這則輸入可能與多項工作有關,請選擇目標:',
-      confirmCommand: workHubAmbiguousCommandPrompt(locale),
-      discussionStayed: '這則內容暫時保留在 WorkHub,沒有建立或變更 Session。',
-      discussionHint: '提出明確的執行目標後,我會將它交給對應的 Session。',
-      answering: '正在回答…',
-      choseWork: (name: string) => `選擇「${name}」`,
-      sentTo: '已交給:', createdWork: '已建立新工作:', accepted: '已接收', sessionFallback: '一般 Session',
-      stopTargetRequired: '請明確說出要停止的工作名稱,例如「停止 支付任務」。',
-      stopTargetAmbiguous: '這個名稱對應多項工作;請開啟具體的 Session 停止對應委派。',
-      stopTargetUnavailable: '這項工作現在沒有可以停止的單一 WorkHub 委派;請開啟該 Session 檢視。',
-      waitingForDecision: '這項工作正在等待你的決定。',
-      requestNotSent: '新請求尚未傳送;處理原 Session 中的互動後可以再次傳送。',
-      routing: '正在判斷應該交給哪個 Session…', loadFailed: '無法讀取現有工作。',
-      loading: '正在讀取現有工作…',
-      preparing: '正在準備 WorkHub…', unavailable: '暫時無法使用',
-      coordinationFailedTitle: 'WorkHub 暫時無法啟動',
-      coordinationFailedBody: '請檢查目前 Runtime Host 的預設模型設定,然後重試。',
-      retry: '重試',
-      stoppingWork: '正在請求停止:', stopping: '正在處理', stopRecorded: '結果已記錄',
-      openSessionToStop: '這個 Turn 不由該委派獨佔;請開啟 Session 處理',
-      stopOutcomes: {
-        cancelled_pending: '已取消尚未開始的工作:',
-        stop_delivered: '已向執行中的工作發出停止請求:',
-        already_terminal: '這項工作已經結束:',
-        not_owned: '未停止共享或使用者擁有的 Turn:',
-      },
-      resumeRequested: '已請求恢復;目前進度請查看目標 Session。',
-      resumeOutcomes: {
-        resume_started: '已讓中斷的工作繼續:',
-        already_running: '這項工作仍在執行,不需要恢復:',
-      },
-      resumeTargetRequired: '請明確說出要繼續的工作名稱,例如「恢復 支付任務」。',
-      resumeTargetAmbiguous: '這個名稱對應多項工作;請開啟具體的 Session 繼續它。',
-      resumeTargetUnavailable: '這項工作目前沒有可恢復的單一 WorkHub 委派。',
-      resumeOperationUnavailable: '目前 Runtime Host 未啟用安全邊界恢復;請開啟原 Session 繼續處理。',
-      resumeHostRecovering: 'Runtime Host 仍在恢復;請稍後重試。',
-      submitFailures: {
-        candidates_changed: '工作清單已變更,請重新傳送以使用最新目標。',
-        linked_correction_unavailable: '跨 Session 更正將於持久委派關聯完成後開放;請先開啟原 Session 並停止目前工作。',
-        target_waiting: '目標 Session 正在等待你的處理;請先開啟並完成該互動。',
-        action_changed: '這次操作已變更,請重新傳送。',
-        delivery_failed: '輸入未能送達,請重試。',
-      },
-      scrollToBottom: '捲動到底部', archived: '已封存',
-      states: { active: '使用中', running: '進行中', waiting_for_user: '等待你', blocked: '受阻', aborted: '已中止' },
-      delegationStates: {
-        accepted: '已接收',
-        running: '進行中',
-        waiting_for_user: '等待你',
-        completed: '已完成',
-        failed: '失敗',
-        aborted: '已中止',
-        recovering: '正在恢復',
-      },
-      assignmentLinkStates: {
-        active: (execution: string) => `關聯有效 · ${execution}`,
-        superseded: '已被更正',
-        aborted: '更正已中止',
-        stopped: '已停止關聯',
-      },
-      turnStates: { running: '進行中', completed: '已完成', aborted: '已中止', failed: '失敗' },
-    } as const;
-  }
-  return {
-    locale,
-    subtitle: 'Continue, create, and review ordinary Sessions from one place',
-    emptyTitle: 'Continue all work from here',
-    emptyBody: (count: number) => count > 0
-      ? `WorkHub routes against ${count} existing Session${count === 1 ? '' : 's'} and asks when the target is unclear.`
-      : 'State a clear goal and WorkHub will create an ordinary Session and bring its result back here.',
-    workCount: (count: number) => `${count} work item${count === 1 ? '' : 's'}`, clarification: 'Choose work',
-    chooseWork: 'This input may relate to more than one task. Choose a target:',
-    confirmCommand: workHubAmbiguousCommandPrompt(locale),
-    stopTargetRequired: 'Name the work explicitly, for example “Stop Payments”.',
-    stopTargetAmbiguous:
-      'That name matches more than one work item. Open the exact Session to stop its delegation.',
-    stopTargetUnavailable:
-      'This work has no single WorkHub delegation to stop right now. Open its Session to see what is running.',
-    discussionStayed: 'This stayed in WorkHub without creating or changing a Session.',
-    discussionHint: 'State an executable goal and I will hand it to the owning Session.',
-    answering: 'Answering…',
-    choseWork: (name: string) => `Choose “${name}”`,
-    sentTo: 'Sent to:', createdWork: 'Created new work:', accepted: 'Accepted', sessionFallback: 'Ordinary Session',
-    stoppingWork: 'Requesting stop:', stopping: 'Stopping', stopRecorded: 'Result recorded',
-    openSessionToStop: 'This Turn is shared or user-owned. Open the Session to stop it.',
-    stopOutcomes: {
-      cancelled_pending: 'Cancelled work that had not started:',
-      stop_delivered: 'Asked the running work to stop:',
-      already_terminal: 'This work had already ended:',
-      not_owned: 'Did not stop a shared or user-owned Turn:',
-    },
-    resumeRequested: 'Resume requested. See the target Session for current progress.',
-    resumeOutcomes: {
-      resume_started: 'Carried on the interrupted work:',
-      already_running: 'This work is still running, so there was nothing to resume:',
-    },
-    resumeTargetRequired: 'Name the work explicitly, for example “Resume Payments”.',
-    resumeTargetAmbiguous:
-      'That name matches more than one work item. Open the exact Session to resume it.',
-    resumeTargetUnavailable:
-      'This work has no single WorkHub delegation that can be resumed.',
-    resumeOperationUnavailable:
-      'Safe-boundary resume is not enabled on this Runtime Host. Open the original Session to continue.',
-    resumeHostRecovering: 'The Runtime Host is still recovering. Try again shortly.',
-    waitingForDecision: 'This work is waiting for your decision.',
-    requestNotSent: 'The new request was not sent. Resolve the interaction in its Session, then send again.',
-    routing: 'Choosing the right Session…', loadFailed: 'Could not read existing work.',
-    loading: 'Loading existing work…',
-    preparing: 'Preparing WorkHub…', unavailable: 'Unavailable',
-    coordinationFailedTitle: 'WorkHub could not start',
-    coordinationFailedBody: 'Check the default model for the current Runtime Host, then retry.',
-    retry: 'Retry',
-    submitFailures: {
-      candidates_changed: 'The work list changed. Send again to use the latest targets.',
-      linked_correction_unavailable: 'No active delegation link is available to correct. Send again, or open the original Session to confirm its current work.',
-      target_waiting: 'The target Session needs your input. Open it and resolve that interaction first.',
-      action_changed: 'This action changed. Send it again.',
-      delivery_failed: 'The input could not be delivered. Try again.',
-    }, scrollToBottom: 'Scroll to bottom', archived: 'Archived',
-    states: { active: 'Active', running: 'Running', waiting_for_user: 'Waiting for you', blocked: 'Blocked', aborted: 'Aborted' },
-    delegationStates: {
-      accepted: 'Accepted',
-      running: 'Running',
-      waiting_for_user: 'Waiting for you',
-      completed: 'Completed',
-      failed: 'Failed',
-      aborted: 'Aborted',
-      recovering: 'Recovering',
-    },
-    assignmentLinkStates: {
-      active: (execution: string) => `Active link · ${execution}`,
-      superseded: 'Superseded link',
-      aborted: 'Aborted replacement',
-      stopped: 'Stopped link',
-    },
-    turnStates: { running: 'Running', completed: 'Completed', aborted: 'Aborted', failed: 'Failed' },
-  } as const;
-}
diff --git a/apps/desktop/src/shared/work-board-ipc.ts b/apps/desktop/src/shared/plan-mode-ipc.d.ts
similarity index 51%
copy from apps/desktop/src/shared/work-board-ipc.ts
copy to apps/desktop/src/shared/plan-mode-ipc.d.ts
index 20c9ea9f64..46d449e307 100644
--- a/apps/desktop/src/shared/work-board-ipc.ts
+++ b/apps/desktop/src/shared/plan-mode-ipc.d.ts
@@ -17,17 +17,22 @@
  * under the License.
  */
 
-import type { WorkBoardStoreErrorCode } from '@maka/storage/work-board-store';
+import type { PlanControlErrorCode } from '@maka/runtime-host/protocol';
 
-export type WorkBoardIpcResult<T> =
+/** Envelope the plan-mode control channels return across the Desktop IPC
+ * boundary: the main process returns structured outcomes instead of throwing
+ * typed errors, whose custom fields Electron strips. Desktop-only transport
+ * shape — no Host wire codec uses it — so it lives beside the WorkBoardIpcResult
+ * seam instead of under the Host protocol compatibility gate. As a type-only
+ * `.d.ts` (the session-collaboration.d.ts precedent) it stays outside the
+ * renderer architecture debt closure. Unknown codes keep version-skew safety,
+ * so the failure branch admits undeclared strings. */
+export type PlanControlIpcResult<T> =
   | { readonly ok: true; readonly value: T }
   | {
       readonly ok: false;
-      readonly code: WorkBoardStoreErrorCode | 'unknown';
-      readonly message: string;
-    };
-
-export interface WorkBoardChangedEvent {
-  readonly type: 'work_board_changed';
-  readonly ts: number;
-}
+      readonly error: {
+        readonly code: PlanControlErrorCode | (string & {});
+        readonly message: string;
+      };
+    };
\ No newline at end of file
diff --git a/apps/desktop/src/shared/session-collaboration.d.ts b/apps/desktop/src/shared/session-collaboration.d.ts
index eb848824d1..3f56eae932 100644
--- a/apps/desktop/src/shared/session-collaboration.d.ts
+++ b/apps/desktop/src/shared/session-collaboration.d.ts
@@ -31,7 +31,11 @@ export type SessionCollaborationImportResult =
         | 'peer_path_unavailable'
         | 'incompatible_host'
         | 'connection_failed';
-      readonly message?: string;
+    }
+  | {
+      readonly kind: 'error';
+      readonly reason: 'mount_limit_reached';
+      readonly params: { readonly max: number };
     };
 
 export type SessionCollaborationImportPhase =
diff --git a/apps/desktop/src/shared/work-board-ipc.ts b/apps/desktop/src/shared/work-board-ipc.ts
index 20c9ea9f64..591b123bd7 100644
--- a/apps/desktop/src/shared/work-board-ipc.ts
+++ b/apps/desktop/src/shared/work-board-ipc.ts
@@ -17,14 +17,18 @@
  * under the License.
  */
 
-import type { WorkBoardStoreErrorCode } from '@maka/storage/work-board-store';
+import type { WorkBoardMutationOptions, WorkBoardStoreErrorCode } from '@maka/storage/work-board-store';
+export type { WorkBoardMutationOptions };
+
+export type WorkBoardErrorCode = WorkBoardStoreErrorCode | 'unknown';
 
 export type WorkBoardIpcResult<T> =
   | { readonly ok: true; readonly value: T }
   | {
       readonly ok: false;
-      readonly code: WorkBoardStoreErrorCode | 'unknown';
-      readonly message: string;
+      readonly error: {
+        readonly code: WorkBoardErrorCode;
+      };
     };
 
 export interface WorkBoardChangedEvent {
diff --git a/apps/desktop/stories/workhub.stories.tsx b/apps/desktop/stories/workhub.stories.tsx
index 6fa53ec942..afc79d125a 100644
--- a/apps/desktop/stories/workhub.stories.tsx
+++ b/apps/desktop/stories/workhub.stories.tsx
@@ -23,6 +23,7 @@ import type { SessionSummary } from '@maka/core/session';
 import { WorkHubComposerServicesProvider } from '../src/renderer/features/workhub/index.js';
 import type { Meta, StoryObj } from '@storybook/react-vite';
 import { expect, fn, userEvent, within, waitFor } from 'storybook/test';
+import { WorkHubCoordinationFailure } from '../src/renderer/workhub-controller';
 import type {
   WorkHubController,
   WorkHubCoordinationTurn,
@@ -348,7 +349,7 @@ function ConfiguredComposerSurface({ failFirst = false }: { failFirst?: boolean
     ...controller([submittedTurn()]),
     submit: async (input) => {
       composerWrites.send(input);
-      if (failures.remaining-- > 0) throw new Error('Temporary Host failure');
+      if (failures.remaining-- > 0) throw new WorkHubCoordinationFailure('host_not_ready', 'Temporary Host failure');
       return { kind: 'discussion', requestId: input.requestId, text: input.text, strategyId: 'wh-r2.4-session-context-continuity' };
     },
   }));
diff --git a/packages/core/src/attachments.ts b/packages/core/src/attachments.ts
index 6725e44472..ca709b7aef 100644
--- a/packages/core/src/attachments.ts
+++ b/packages/core/src/attachments.ts
@@ -284,3 +284,15 @@ export function attachmentKindFromMimeType(
   }
   return 'other';
 }
+
+export type AttachmentIngestBlockedCode =
+  | 'item_too_large'
+  | 'items_invalid'
+  | 'count_limit'
+  | 'duplicate_source'
+  | 'source_expired'
+  | 'total_size_exceeded';
+
+export function attachmentIngestBlocked(code: AttachmentIngestBlockedCode): Error {
+  return new Error(`attachment_ingest:${code}`);
+}
diff --git a/packages/core/src/redaction.ts b/packages/core/src/redaction.ts
index 54c5644c6a..b84f55d095 100644
--- a/packages/core/src/redaction.ts
+++ b/packages/core/src/redaction.ts
@@ -296,3 +296,29 @@ export function generalizedErrorMessage(error: unknown, fallback = 'Operation fa
 export function isAuthenticationErrorText(message: string): boolean {
   return message.replace(/\bauthorit\w*/g, '').includes('auth');
 }
+
+const reportedFailures = new WeakSet<object>();
+
+/** Redacted diagnostics channel for unexpected operation failures. Copy
+ * catalogs live here (bare-importable) because a depended-on copy catalog may
+ * only hold bare package runtime imports. */
+export function reportUnexpectedOperation(scope: string, error: unknown): void {
+  // One failure, one diagnostic: a rejection formatted again by an outer layer
+  // is the same defect, not a second one.
+  if (typeof error === 'object' && error !== null) {
+    if (reportedFailures.has(error)) return;
+    reportedFailures.add(error);
+  }
+  const detail =
+    error instanceof Error ? (error.stack ?? `${error.name}: ${error.message}`) : String(error);
+  console.error(`[${scope}] operation failed:`, redactSecrets(detail));
+}
+
+export function unexpectedOperationFallback(
+  error: unknown,
+  fallback: string,
+  scope: string,
+): string {
+  reportUnexpectedOperation(scope, error);
+  return fallback;
+}
diff --git a/packages/runtime-host/protocol-compatible-changes/plan-control-error-codes.json b/packages/runtime-host/protocol-compatible-changes/plan-control-error-codes.json
new file mode 100644
index 0000000000..0eb5144520
--- /dev/null
+++ b/packages/runtime-host/protocol-compatible-changes/plan-control-error-codes.json
@@ -0,0 +1,5 @@
+{
+  "epoch": 133,
+  "files": ["packages/runtime-host/src/protocol/plan.ts"],
+  "reason": "plan.ts: declares the pre-existing 'unauthorized' code on the plan.control / plan.turn.start error specs (the dispatcher already admitted it unconditionally; now the spec matches) and adds the exported PlanControlErrorCode alias over the file-private CONTROL_ERRORS list. No wire codec, frame, or error-code shape changes"
+}
diff --git a/packages/runtime-host/src/protocol/plan.ts b/packages/runtime-host/src/protocol/plan.ts
index 100f054096..cde3f07926 100644
--- a/packages/runtime-host/src/protocol/plan.ts
+++ b/packages/runtime-host/src/protocol/plan.ts
@@ -63,11 +63,15 @@ const QUERY_ERRORS = [
 ] as const;
 const CONTROL_ERRORS = [
   ...QUERY_ERRORS,
+  'unauthorized',
   'session_busy',
   'operation_conflict',
   'persistence_failed',
 ] as const;
 
+/** Failure codes `plan.control` / `plan.turn.start` reject with. */
+export type PlanControlErrorCode = (typeof CONTROL_ERRORS)[number];
+
 export type PlanQueryInput =
   | { readonly kind: 'list_start'; readonly sessionId: string }
   | {


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