From e56e37a91d4f2c5205e1e4e4f26eb3a80138885a Mon Sep 17 00:00:00 2001 From: yi111 <153097222+Yi-111-a@users.noreply.github.com> Date: Thu, 10 Sep 2026 13:02:01 +0800 Subject: [PATCH 1/4] fix(chat): explain a failed subagent in its detail panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A delegate that ended failed, timed_out, or aborted showed only an outcome capsule and an elapsed time. The reason existed but had no reader: the runtime reports `error: { code, message }` on the delegation roster entry, while a node and its dock render the `Task` row's own result, which ADR 0089 fixes at `running` the moment the delegate starts. A delegate that died before emitting a message row therefore left a panel of steps and no explanation (issue #161). Collect the failure from the lifecycle rows' `details.delegations[]` and `details.stopped[]` with the same last-write-wins rule as the settled status, and close the dock with an error card that reuses the transcript error card's visual language and its `errors.` vocabulary, falling back to the localized `chat.subagentStatus.*` outcome when a code is not registered. The card follows a non-success terminal outcome rather than the error field alone, so a completed or still-running delegate never shows one. Its disclosure control sits in the heading so it stays reachable while the details are collapsed, and a code-only error stays a one-line card instead of opening onto an empty box. No runtime, IPC, storage, or tool-result change — the runtime already reported the error and this stops discarding it — and no new i18n keys. Verified: node --test test/subagent-topology.test.mjs test/subagent-panel.test.mjs (23 passed, 0 failed). --- .../desktop/src/components/ChatTranscript.tsx | 91 +++++++++++++++ .../components/workpanel/SubagentPanel.tsx | 10 ++ apps/desktop/src/lib/subagent-topology.ts | 55 +++++++++ apps/desktop/src/styles/work-panel.css | 8 ++ apps/desktop/test/subagent-panel.test.mjs | 38 +++++++ apps/desktop/test/subagent-topology.test.mjs | 106 ++++++++++++++++++ 6 files changed, 308 insertions(+) diff --git a/apps/desktop/src/components/ChatTranscript.tsx b/apps/desktop/src/components/ChatTranscript.tsx index f5a0bc1379..f7184b3b34 100644 --- a/apps/desktop/src/components/ChatTranscript.tsx +++ b/apps/desktop/src/components/ChatTranscript.tsx @@ -48,6 +48,7 @@ import { toolResultChips, } from "../lib/tool-presentation"; import { + collectDelegationFailures, collectDelegationStatuses, collectDelegationTimings, delegationRoster, @@ -59,6 +60,7 @@ import { subagentOutcome, summarizeSubagentActivity, type DelegationActivityItem, + type DelegationFailure, type SubagentOutcome, type SubagentTiming, } from "../lib/subagent-topology"; @@ -1168,6 +1170,87 @@ function delegateTaskDescription(message: UiMessage): string { return typeof task === "string" ? task.trim() : ""; } +/** + * Why a settled delegate failed, at the foot of its detail panel (issue #161). + * + * The step stream ends on `Failed` / `Timed out` / `Aborted` without saying + * why: a delegate that dies before emitting a message row has no other carrier + * for its reason, and the badge plus a duration is all a reader gets. The + * runtime already reports `error: { code, message }` on the delegation roster + * entry, so it is rendered here with the same visual language as the parent + * reply's error card instead of being reachable only by reading the raw tool + * result. + */ +function SubagentFailureCard({ + outcome, + failure, +}: { + outcome: SubagentOutcome; + failure: DelegationFailure; +}) { + const { t } = useTranslation(); + const [open, setOpen] = useState(true); + const detailsId = useId(); + const headingId = useId(); + // A known runtime code already has a localized sentence; otherwise the + // outcome's own label is the summary, which stays truthful and localized. + const localizedKey = `errors.${failure.code}`; + const localized = failure.code ? t(localizedKey) : localizedKey; + const summary = + failure.code && localized !== localizedKey + ? localized + : t(`chat.subagentStatus.${outcome}`); + // A code-only error carries no detail to disclose, so it stays a one-line + // card rather than opening onto an empty box. + const hasMessage = failure.message.length > 0; + + return ( +
+
+ + + +
+ {summary} + {failure.code ? {failure.code} : null} +
+
+ {/* The toggle stays outside the collapsed region, otherwise hiding + * the details would take away the control that brings them back. */} + {hasMessage ? ( + + ) : null} +
+
+ {hasMessage ? ( + + ) : null} +
+ ); +} + /** * The side-sheet view for a selected delegate. It shows a sticky identity * header, the task as an inset grouped card, and the live process timeline. @@ -1177,11 +1260,13 @@ export function SubagentDetail({ message, delegate, delegationStatuses, + delegationFailures, delegationTimings, }: { message: UiMessage; delegate?: SubagentRun; delegationStatuses?: ReadonlyMap; + delegationFailures?: ReadonlyMap; delegationTimings?: ReadonlyMap; }) { const { t } = useTranslation(); @@ -1201,6 +1286,7 @@ export function SubagentDetail({ ? payloadRecord.delegationId : message.toolCallId || message.id; const timing = delegationTimings?.get(delegationId); + const failure = delegationFailures?.get(delegationId); const startedAt = timing?.startedAt ?? (typeof payloadRecord?.startedAt === "number" ? payloadRecord.startedAt : undefined); @@ -1329,6 +1415,11 @@ export function SubagentDetail({ variant="dock" /> ) : null} + {/* A completed delegate has nothing to explain, so the card is tied to a + * non-success terminal outcome rather than to the error field alone. */} + {failure && outcome !== "completed" && outcome !== "running" ? ( + + ) : null} ); } diff --git a/apps/desktop/src/components/workpanel/SubagentPanel.tsx b/apps/desktop/src/components/workpanel/SubagentPanel.tsx index 8416dc9616..7b3c63bef3 100644 --- a/apps/desktop/src/components/workpanel/SubagentPanel.tsx +++ b/apps/desktop/src/components/workpanel/SubagentPanel.tsx @@ -6,10 +6,12 @@ import { type AssistantActivityItem, } from "../../lib/assistant-turns"; import { + collectDelegationFailures, collectDelegationStatuses, collectDelegationTimings, isDelegationActivityItem, type DelegationActivityItem, + type DelegationFailure, type SubagentOutcome, type SubagentTiming, } from "../../lib/subagent-topology"; @@ -78,6 +80,13 @@ export function SubagentPanel({ selection }: { selection: SubagentPanelSelection : new Map(), [isRunning, selected], ); + const delegationFailures = useMemo>( + () => + selected + ? collectDelegationFailures(selected.turnActivityItems) + : new Map(), + [selected], + ); const delegationTimings = useMemo>( () => selected @@ -126,6 +135,7 @@ export function SubagentPanel({ selection }: { selection: SubagentPanelSelection ? { delegate: selected.item.delegate } : {})} delegationStatuses={delegationStatuses} + delegationFailures={delegationFailures} delegationTimings={delegationTimings} /> ) : ( diff --git a/apps/desktop/src/lib/subagent-topology.ts b/apps/desktop/src/lib/subagent-topology.ts index fdc7d16603..6ca86efb8d 100644 --- a/apps/desktop/src/lib/subagent-topology.ts +++ b/apps/desktop/src/lib/subagent-topology.ts @@ -217,6 +217,61 @@ export function collectDelegationStatuses( return statuses; } +/** + * A settled subagent's failure as the runtime reported it. + * + * `Task` returns the moment a delegate starts (ADR 0089), so its own result can + * never carry one. `TaskWait`/`TaskList` report `details.delegations[]` and + * `TaskStop` reports `details.stopped[]`, and those entries do carry + * `error: { code, message }` from `SubagentRunResult.error`. + */ +export type DelegationFailure = { + code: string; + message: string; +}; + +function readDelegationFailure(entry: unknown): DelegationFailure | null { + const record = asRecord(entry); + const error = asRecord(record?.error); + if (!error) return null; + const code = typeof error.code === "string" ? error.code.trim() : ""; + const message = typeof error.message === "string" ? error.message.trim() : ""; + if (!code && !message) return null; + return { code, message }; +} + +/** + * Why each settled delegation failed, keyed by delegation id. + * + * {@link collectDelegationStatuses} says *that* a subagent ended as `failed`, + * `timed_out`, or `aborted`; this says why, from the same lifecycle rows and + * with the same last-write-wins rule. A delegate that fails before emitting a + * message row has no other carrier for its reason, which is what left a failed + * subagent's detail panel showing steps and no explanation. + */ +export function collectDelegationFailures( + items: readonly AssistantActivityItem[], +): ReadonlyMap { + const failures = new Map(); + for (const item of items) { + if (item.kind !== "tool") continue; + if (isDelegationActivityItem(item)) continue; + const payload = asRecord(toolResultPayload(item.message)); + if (!payload) continue; + for (const entries of [payload.delegations, payload.stopped]) { + if (!Array.isArray(entries)) continue; + for (const entry of entries) { + const record = asRecord(entry); + const id = record?.delegationId; + if (typeof id !== "string" || !id) continue; + const failure = readDelegationFailure(entry); + if (failure) failures.set(id, failure); + } + } + } + return failures; +} + /** One subagent named on a lifecycle row's roster (ADR 0089). */ export type DelegationRosterEntry = { delegationId: string; diff --git a/apps/desktop/src/styles/work-panel.css b/apps/desktop/src/styles/work-panel.css index 78c07eb3c8..4f41350275 100644 --- a/apps/desktop/src/styles/work-panel.css +++ b/apps/desktop/src/styles/work-panel.css @@ -628,6 +628,14 @@ padding: 0 16px; } +/* Issue #161: the failure card closes out a settled delegate, so it lines up + with the task card and the run rows instead of the panel's own edges. The + card carries its own padding, so it is inset with margins. */ +.subagent-detail > .subagent-failure { + width: auto; + margin: 0 16px; +} + .subagent-detail-section-label, .subagent-detail > .subagent-run .subagent-run-heading.is-dock { min-height: 20px; diff --git a/apps/desktop/test/subagent-panel.test.mjs b/apps/desktop/test/subagent-panel.test.mjs index d0aad45eec..00ff81a5a7 100644 --- a/apps/desktop/test/subagent-panel.test.mjs +++ b/apps/desktop/test/subagent-panel.test.mjs @@ -22,6 +22,10 @@ const detailSource = transcriptSource.slice( transcriptSource.indexOf("export function SubagentDetail"), transcriptSource.indexOf("/**\n * A truthful one-level graph", transcriptSource.indexOf("export function SubagentDetail")), ); +const failureCardSource = transcriptSource.slice( + transcriptSource.indexOf("function SubagentFailureCard("), + transcriptSource.indexOf("export function SubagentDetail"), +); const storeSource = await readFile( new URL("../src/stores/app-store.ts", import.meta.url), "utf8", @@ -152,3 +156,37 @@ test("the subagent dock uses a grouped identity, task card, and process timeline assert.match(transcriptSource, /t\("chat.subagentProcess"\)/); assert.match(detailSource, /variant="dock"/); }); + +test("a settled delegate that failed explains itself at the foot of the dock", () => { + // The status says *that* a delegate failed; only the lifecycle rows can say + // why (ADR 0089), and the dock is the surface that renders the outcome. + assert.match(panelSource, /collectDelegationFailures\(selected\.turnActivityItems\)/); + assert.match(panelSource, /delegationFailures=\{delegationFailures\}/); + assert.match( + detailSource, + /delegationFailures\?: ReadonlyMap/, + ); + assert.match(detailSource, /delegationFailures\?\.get\(delegationId\)/); + // Tied to a non-success terminal outcome, not to the error field alone, so a + // completed or still-running delegate never shows an error card. + assert.match( + detailSource, + /failure && outcome !== "completed" && outcome !== "running"/, + ); + assert.match(failureCardSource, /function SubagentFailureCard\(/); + assert.match(failureCardSource, /data-testid="subagent-failure"/); + assert.match(failureCardSource, /className="message-error subagent-failure"/); + assert.match(failureCardSource, /t\(`chat\.subagentStatus\.\$\{outcome\}`\)/); + assert.match(failureCardSource, /t\(localizedKey\)/); + assert.match(failureCardSource, / \.subagent-failure\s*\{[\s\S]*?margin:\s*0 16px;/, + ); +}); diff --git a/apps/desktop/test/subagent-topology.test.mjs b/apps/desktop/test/subagent-topology.test.mjs index 265a98ade5..1f98e6bc3a 100644 --- a/apps/desktop/test/subagent-topology.test.mjs +++ b/apps/desktop/test/subagent-topology.test.mjs @@ -7,6 +7,7 @@ import { fileURLToPath, pathToFileURL } from "node:url"; const here = dirname(fileURLToPath(import.meta.url)); register(pathToFileURL(join(here, "helpers/ts-import-hooks.mjs"))); const { + collectDelegationFailures, collectDelegationStatuses, collectDelegationTimings, delegationRoster, @@ -176,6 +177,111 @@ test("reads stopped status from TaskStop, including a running snapshot", () => { ); }); +// Issue #161: the status says *that* a delegate failed; the error says why. +// `Task` returns before the delegate settles (ADR 0089), so only the lifecycle +// rows can carry the reason. +test("reads a failed delegation's error from a TaskWait roster entry", () => { + const failures = collectDelegationFailures([ + task("d1", "success", "running"), + lifecycle("TaskWait", { + delegations: [ + { + delegationId: "d1", + status: "failed", + error: { code: "SUBAGENT_FAILED", message: "Provider returned 500." }, + }, + ], + }), + ]); + + assert.deepEqual(failures.get("d1"), { + code: "SUBAGENT_FAILED", + message: "Provider returned 500.", + }); +}); + +test("reads a failed delegation's error from TaskStop's `stopped` list", () => { + const failures = collectDelegationFailures([ + lifecycle("TaskStop", { + stopped: [ + { + delegationId: "d9", + status: "aborted", + error: { + code: "DELEGATION_ABORTED", + message: "Parent turn ended.", + }, + }, + ], + }), + ]); + + assert.deepEqual(failures.get("d9"), { + code: "DELEGATION_ABORTED", + message: "Parent turn ended.", + }); +}); + +test("a later lifecycle row replaces an earlier failure for the same delegate", () => { + const failures = collectDelegationFailures([ + lifecycle("TaskWait", { + delegations: [ + { + delegationId: "d1", + status: "failed", + error: { code: "PROVIDER_ERROR", message: "first" }, + }, + ], + }), + lifecycle("TaskList", { + delegations: [ + { + delegationId: "d1", + status: "failed", + error: { code: "PROVIDER_ERROR", message: "second" }, + }, + ], + }), + ]); + + assert.equal(failures.get("d1").message, "second"); +}); + +test("ignores roster entries that report no error at all", () => { + const failures = collectDelegationFailures([ + lifecycle("TaskWait", { + delegations: [ + { delegationId: "ok", status: "completed" }, + { delegationId: "empty", status: "failed", error: {} }, + { delegationId: "blank", status: "failed", error: { message: " " } }, + { status: "failed", error: { code: "X", message: "no id" } }, + ], + }), + ]); + + assert.equal(failures.size, 0); +}); + +test("a Task row never carries its own failure, only lifecycle rows do", () => { + const failures = collectDelegationFailures([ + { + kind: "tool", + message: { + ...task("d1", "error", "failed").message, + toolResult: { + details: { + delegationId: "d1", + status: "failed", + error: { code: "NOPE", message: "not a carrier" }, + }, + }, + }, + }, + ]); + + assert.equal(failures.size, 0); +}); + test("a finished turn treats leftover running delegates as aborted", () => { const live = collectDelegationStatuses( [task("d1", "success", "running")], From 992cdce87a315878a53bf6495de20d75685b304a Mon Sep 17 00:00:00 2001 From: yi111 <153097222+Yi-111-a@users.noreply.github.com> Date: Thu, 10 Sep 2026 13:02:09 +0800 Subject: [PATCH 2/4] docs(spec): record the subagent failure card (D381) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Synchronize the surfaces that describe a settled delegate: - spec 04-ux 08-component-spec §5.7 gains the failure card, its summary fallback, and the non-success terminal-outcome gate. - E2E-198 gains step 6 and the matching expectation, and its status records the new unit coverage. - The decisions log records D381, and the zh-CN mirror of the decisions log carries the same entry. No behavior change beyond the code commit. --- docs/spec/04-ux/08-component-spec.md | 12 ++++++++++++ docs/spec/06-delivery/04-e2e-test-plan.md | 18 ++++++++++++++++-- docs/spec/08-meta/decisions-log.md | 19 +++++++++++++++++++ docs/zh-CN/spec/08-meta/decisions-log.md | 15 +++++++++++++++ 4 files changed, 62 insertions(+), 2 deletions(-) diff --git a/docs/spec/04-ux/08-component-spec.md b/docs/spec/04-ux/08-component-spec.md index 76a84115ce..6008197b46 100644 --- a/docs/spec/04-ux/08-component-spec.md +++ b/docs/spec/04-ux/08-component-spec.md @@ -988,6 +988,18 @@ It does not render separate Details or Output tabs. process uses an **Activity** section label (it does not repeat the agent name), a trailing step count, and one subtle vertical timeline with no nested card, so unused panel space reads as one continuous work surface. +- A delegate that settles without completing explains why, because the process + alone does not: a `Failed`, `Timed out`, or `Aborted` capsule with no reason + is all a reader gets, and a delegate that dies before emitting a message row + has no other carrier for its failure. When the delegation roster entry + reports `error: { code, message }` (ADR 0089), the panel closes with an error + card in the transcript's error visual language — the localized + `errors.` sentence when that code is registered and the localized + `chat.subagentStatus.*` outcome otherwise, the stable code, and the raw + message behind a Show details / Hide details disclosure with a copy action. + The disclosure control sits in the card's heading so it stays reachable while + the details are collapsed, and the card follows a **non-success** terminal + outcome, so a completed or still-running delegate never shows one. - The dock header identifies the view as **Subagent** and offers close and collapse controls. Closing returns to the previously selected work-panel resource, if any; `Cmd/Ctrl + J` hides the whole dock. Selecting another node diff --git a/docs/spec/06-delivery/04-e2e-test-plan.md b/docs/spec/06-delivery/04-e2e-test-plan.md index 536d33997a..6b036fb8b5 100644 --- a/docs/spec/06-delivery/04-e2e-test-plan.md +++ b/docs/spec/06-delivery/04-e2e-test-plan.md @@ -7729,7 +7729,9 @@ This test plan spec is accepted when: right-side dock closes, then click it once more to reopen it. 3) Observe the right-side dock while the delegate streams. 4) Scroll the task/process conversation upward and then return to the latest output. 5) Switch sessions - and return to the original session. + and return to the original session. 6) Let a delegate start and then fail, + open its node, and read the foot of the dock; repeat with a delegate that + completes and one that is still running. - **Expected**: The right dock shows a sticky identity header (avatar, name, and model caption on the left; status capsule and elapsed time trailing on the same row without wrapping), the Task call's description @@ -7748,11 +7750,23 @@ This test plan spec is accepted when: jump-to-latest. The transcript remains the same height and keeps its own scroll state. Session switching hides the selection and returning never shows another session's task. + In step 6, a delegate that starts and then fails closes the dock with an + error card rather than a bare `Failed` capsule: the localized summary (the + registered `errors.` sentence when the runtime reported a known code, + the localized `chat.subagentStatus.*` outcome otherwise), the stable code, + the raw provider message behind a Show details / Hide details disclosure, and + a copy control. The disclosure control stays reachable while the details are + collapsed, and the delegate that completed or is still running shows no card + at all. - **Specs linked**: `04-ux/08-component-spec.md` §5.7, `04-ux/09-interaction-patterns.md` §9.1 - **Acceptance**: C (conversation), Quality - **Milestone**: M6+ -- **Status**: Documented; desktop journey pending +- **Status**: Documented; desktop journey pending. The failure card's data + source is unit-tested in `subagent-topology.test.mjs`: a settled delegation's + `error: { code, message }` read from `TaskWait` `delegations[]` and `TaskStop` + `stopped[]`, last-write-wins across rows, entries without an error, and the + `Task` row that must never carry one. #### E2E-161: A delegation lifecycle row reads as a subagent row diff --git a/docs/spec/08-meta/decisions-log.md b/docs/spec/08-meta/decisions-log.md index 19b2707355..20d9da3da1 100644 --- a/docs/spec/08-meta/decisions-log.md +++ b/docs/spec/08-meta/decisions-log.md @@ -4240,3 +4240,22 @@ D193, and D194. a `tools.execute` for an unknown session returns `SESSION_NOT_FOUND` instead of inheriting the global workspace. See E2E-234 through E2E-238. + +## 2026-09-10 — A settled subagent reads back why it failed (D381) + +- A delegate that ended `failed`, `timed_out`, or `aborted` showed only its + outcome capsule and an elapsed time. The reason existed but had no reader: + `SubagentRunResult.error` travels on the delegation roster entry, while a + node and its dock render the `Task` row's own result, which ADR 0089 fixes at + `running` the moment the delegate starts. A delegate that died before + emitting a message row therefore left a panel of steps and no explanation. +- Decision D381: the delegation's `error: { code, message }` is collected from + the lifecycle rows' `details.delegations[]` and `details.stopped[]` with the + same last-write-wins rule as the settled status, and the dock closes with an + error card that reuses the transcript error card's visual language and its + `errors.` vocabulary, falling back to the localized + `chat.subagentStatus.*` outcome when a code is not registered. The card + follows a non-success terminal outcome rather than the error field alone, so + a completed or running delegate never shows one. No runtime, IPC, storage, or + tool-result change: the runtime already reported the error, and this stops + discarding it. See `04-ux/08-component-spec.md` §5.7 and E2E-198. diff --git a/docs/zh-CN/spec/08-meta/decisions-log.md b/docs/zh-CN/spec/08-meta/decisions-log.md index cef56e26d4..9bb708bf4a 100644 --- a/docs/zh-CN/spec/08-meta/decisions-log.md +++ b/docs/zh-CN/spec/08-meta/decisions-log.md @@ -3633,3 +3633,18 @@ D193 和 D194。 `/ignore`;路径解析器会跟随悬空符号链接,因此经由它的写入无法离开工作区; 对未知会话的 `tools.execute` 返回 `SESSION_NOT_FOUND`,而不是继承全局工作区。 参见 E2E-234 至 E2E-238。 + +## 2026-09-10 —— 已结束的 Subagent 会读回自己失败的原因(D381) + +- 以 `failed`、`timed_out` 或 `aborted` 结束的委派,只显示结果胶囊和耗时。原因其实 + 存在但没有读者:`SubagentRunResult.error` 挂在委派名册条目上,而拓扑节点及其侧边 + 栏详情渲染的是 `Task` 行自身的结果——按 ADR 0089,它在委派启动的那一刻就被固定为 + `running`。因此在发出任何消息行之前就死掉的委派,只会留下一个满是步骤、毫无解释的 + 面板。 +- 决策 D381:从生命周期行的 `details.delegations[]` 与 `details.stopped[]` 收集委派的 + `error: { code, message }`,使用与已定状态相同的"后写覆盖"规则;侧边栏详情的结尾 + 用一张错误卡片收束,复用转录错误卡片的视觉语言与 `errors.` 词表,代码未登记时 + 回退到本地化的 `chat.subagentStatus.*` 结果标签。卡片跟随"非成功终态"而非仅跟随错误 + 字段,因此已完成或仍在运行的委派永远不会显示它。不改运行时、IPC、存储或工具结果: + 运行时早就在上报该错误,这里只是不再丢弃它。参见 `04-ux/08-component-spec.md` §5.7 + 与 E2E-198。 From ab0f3acbd2124efc5836d91ada1eb2d331c9b5c3 Mon Sep 17 00:00:00 2001 From: yi111 <153097222+Yi-111-a@users.noreply.github.com> Date: Thu, 10 Sep 2026 13:44:09 +0800 Subject: [PATCH 3/4] docs(spec): renumber the settled-subagent failure card decision to D382 D381 is now taken by the selectable Windows shell change (#191), so this entry moves to D382 and the two PRs stay distinct. Both decisions logs follow. --- docs/spec/08-meta/decisions-log.md | 4 ++-- docs/zh-CN/spec/08-meta/decisions-log.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/spec/08-meta/decisions-log.md b/docs/spec/08-meta/decisions-log.md index 20d9da3da1..b200bc4ff1 100644 --- a/docs/spec/08-meta/decisions-log.md +++ b/docs/spec/08-meta/decisions-log.md @@ -4241,7 +4241,7 @@ D193, and D194. instead of inheriting the global workspace. See E2E-234 through E2E-238. -## 2026-09-10 — A settled subagent reads back why it failed (D381) +## 2026-09-10 — A settled subagent reads back why it failed (D382) - A delegate that ended `failed`, `timed_out`, or `aborted` showed only its outcome capsule and an elapsed time. The reason existed but had no reader: @@ -4249,7 +4249,7 @@ D193, and D194. node and its dock render the `Task` row's own result, which ADR 0089 fixes at `running` the moment the delegate starts. A delegate that died before emitting a message row therefore left a panel of steps and no explanation. -- Decision D381: the delegation's `error: { code, message }` is collected from +- Decision D382: the delegation's `error: { code, message }` is collected from the lifecycle rows' `details.delegations[]` and `details.stopped[]` with the same last-write-wins rule as the settled status, and the dock closes with an error card that reuses the transcript error card's visual language and its diff --git a/docs/zh-CN/spec/08-meta/decisions-log.md b/docs/zh-CN/spec/08-meta/decisions-log.md index 9bb708bf4a..35d6305183 100644 --- a/docs/zh-CN/spec/08-meta/decisions-log.md +++ b/docs/zh-CN/spec/08-meta/decisions-log.md @@ -3634,14 +3634,14 @@ D193 和 D194。 对未知会话的 `tools.execute` 返回 `SESSION_NOT_FOUND`,而不是继承全局工作区。 参见 E2E-234 至 E2E-238。 -## 2026-09-10 —— 已结束的 Subagent 会读回自己失败的原因(D381) +## 2026-09-10 —— 已结束的 Subagent 会读回自己失败的原因(D382) - 以 `failed`、`timed_out` 或 `aborted` 结束的委派,只显示结果胶囊和耗时。原因其实 存在但没有读者:`SubagentRunResult.error` 挂在委派名册条目上,而拓扑节点及其侧边 栏详情渲染的是 `Task` 行自身的结果——按 ADR 0089,它在委派启动的那一刻就被固定为 `running`。因此在发出任何消息行之前就死掉的委派,只会留下一个满是步骤、毫无解释的 面板。 -- 决策 D381:从生命周期行的 `details.delegations[]` 与 `details.stopped[]` 收集委派的 +- 决策 D382:从生命周期行的 `details.delegations[]` 与 `details.stopped[]` 收集委派的 `error: { code, message }`,使用与已定状态相同的"后写覆盖"规则;侧边栏详情的结尾 用一张错误卡片收束,复用转录错误卡片的视觉语言与 `errors.` 词表,代码未登记时 回退到本地化的 `chat.subagentStatus.*` 结果标签。卡片跟随"非成功终态"而非仅跟随错误 From 79fa90f4a0625692510ec5220087b7bd1c4da4b9 Mon Sep 17 00:00:00 2001 From: vastsa Date: Thu, 10 Sep 2026 15:34:23 +0800 Subject: [PATCH 4/4] fix(chat): copy the tool row before the Task type guard collectDelegationFailures skipped Task rows the same way collectDelegationStatuses does, but read item.message after the predicate. That guard narrows a tool item to never, so typecheck failed. Pull the message off first, matching the existing collector. --- apps/desktop/src/lib/subagent-topology.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/lib/subagent-topology.ts b/apps/desktop/src/lib/subagent-topology.ts index 6ca86efb8d..8e544deb94 100644 --- a/apps/desktop/src/lib/subagent-topology.ts +++ b/apps/desktop/src/lib/subagent-topology.ts @@ -255,8 +255,11 @@ export function collectDelegationFailures( const failures = new Map(); for (const item of items) { if (item.kind !== "tool") continue; + // Read the row before the guard narrows `item` away: every tool item is a + // potential delegation node, so excluding them leaves TS with `never`. + const { message } = item; if (isDelegationActivityItem(item)) continue; - const payload = asRecord(toolResultPayload(item.message)); + const payload = asRecord(toolResultPayload(message)); if (!payload) continue; for (const entries of [payload.delegations, payload.stopped]) { if (!Array.isArray(entries)) continue;