@zhushanwen/subagent-core 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/README.md +45 -2
  2. package/agents/analyst.md +60 -0
  3. package/agents/coder.md +69 -0
  4. package/agents/debugger.md +66 -0
  5. package/agents/doc-reviewer.md +49 -0
  6. package/agents/explorer.md +63 -0
  7. package/agents/general-purpose.md +32 -0
  8. package/agents/orchestrator.md +61 -0
  9. package/agents/planner.md +53 -0
  10. package/agents/researcher.md +67 -0
  11. package/agents/reviewer.md +73 -0
  12. package/dist/chunk-4KN4TTG7.js +240 -0
  13. package/dist/chunk-APZY4IME.js +27 -0
  14. package/dist/chunk-X7SZ5HLQ.js +66 -0
  15. package/dist/execution/engine/engines/zcode/constants.cjs +65 -0
  16. package/dist/execution/engine/engines/zcode/constants.d.cts +93 -1
  17. package/dist/execution/engine/engines/zcode/constants.d.ts +93 -1
  18. package/dist/execution/engine/engines/zcode/constants.js +43 -1
  19. package/dist/execution/engine/engines/zcode/reader.d.cts +1 -1
  20. package/dist/execution/engine/engines/zcode/reader.d.ts +1 -1
  21. package/dist/execution/engine/engines/zcode/reader.js +4 -234
  22. package/dist/execution/engine/paths.js +7 -19
  23. package/dist/index.cjs +14709 -100
  24. package/dist/index.d.cts +4053 -200
  25. package/dist/index.d.ts +4053 -200
  26. package/dist/index.js +14358 -102
  27. package/dist/{types-BxyAidGf.d.cts → types-DpUO16pj.d.cts} +464 -1
  28. package/dist/{types-BxyAidGf.d.ts → types-DpUO16pj.d.ts} +464 -1
  29. package/package.json +6 -3
  30. package/src/__tests__/review-fix-loop-script.test.ts +90 -2
  31. package/src/__tests__/review-fix-loop-utils.test.ts +21 -0
  32. package/src/__tests__/smoke.test.ts +9 -2
  33. package/src/execution/__tests__/agent-profile.test.ts +232 -0
  34. package/src/execution/__tests__/agents-assembly.test.ts +219 -0
  35. package/src/execution/__tests__/chat-engine-routing.test.ts +71 -0
  36. package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +1 -0
  37. package/src/execution/__tests__/create-concurrency-pool.test.ts +194 -0
  38. package/src/execution/__tests__/delivery-methods.test.ts +2 -0
  39. package/src/execution/__tests__/descendant-sweep.test.ts +269 -0
  40. package/src/execution/__tests__/dialog-queue.test.ts +199 -1
  41. package/src/execution/__tests__/epipe-fallback.test.ts +2 -0
  42. package/src/execution/__tests__/execution-runtime-face.test.ts +272 -0
  43. package/src/execution/__tests__/finalize-record.test.ts +106 -0
  44. package/src/execution/__tests__/gc-timer.test.ts +2 -0
  45. package/src/execution/__tests__/get-record-for-action-restart.test.ts +2 -0
  46. package/src/execution/__tests__/get-state-handshake.test.ts +127 -0
  47. package/src/execution/__tests__/keep-alive-no-progress.test.ts +378 -0
  48. package/src/execution/__tests__/kill-all-escalation.test.ts +196 -0
  49. package/src/execution/__tests__/lifecycle-manager-idle-timer-identity.test.ts +117 -0
  50. package/src/execution/__tests__/lifecycle-manager.test.ts +39 -0
  51. package/src/execution/__tests__/manifest-store-tmp-recovery.test.ts +99 -0
  52. package/src/execution/__tests__/max-turns-to-watchdog-ms.test.ts +113 -0
  53. package/src/execution/__tests__/notify-ledger.test.ts +294 -0
  54. package/src/execution/__tests__/record-store-orphan-revive.test.ts +159 -0
  55. package/src/execution/__tests__/record-store.test.ts +83 -0
  56. package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +4 -0
  57. package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +1 -0
  58. package/src/execution/__tests__/run-spawn-edges.test.ts +407 -17
  59. package/src/execution/__tests__/run-spawn-integration.test.ts +1 -1
  60. package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +12 -4
  61. package/src/execution/__tests__/service-kill-escalation.test.ts +89 -0
  62. package/src/execution/__tests__/session-pending.test.ts +228 -0
  63. package/src/execution/__tests__/session-runner-branch-cache-lru.test.ts +110 -0
  64. package/src/execution/__tests__/session-runner-close-prune.test.ts +181 -0
  65. package/src/execution/__tests__/session-runner-epipe.test.ts +1 -0
  66. package/src/execution/__tests__/session-runner-heartbeat-idle-fallback.test.ts +226 -0
  67. package/src/execution/__tests__/settled-watchdog.test.ts +289 -0
  68. package/src/execution/__tests__/spawned-children.test.ts +153 -10
  69. package/src/execution/__tests__/subagent-actions-core.test.ts +997 -0
  70. package/src/execution/__tests__/subagent-service-message-close.test.ts +71 -10
  71. package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +174 -0
  72. package/src/execution/__tests__/subagent-service-notify-gate.test.ts +266 -0
  73. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +3 -0
  74. package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +316 -0
  75. package/src/execution/__tests__/timeout-integration.test.ts +7 -7
  76. package/src/execution/__tests__/ui-request-handler-factory.test.ts +95 -1
  77. package/src/execution/__tests__/ui-request-queue.test.ts +25 -0
  78. package/src/execution/__tests__/worktree-git-ops.test.ts +571 -0
  79. package/src/execution/__tests__/worktree-reconcile-aging.test.ts +204 -0
  80. package/src/execution/agent-registry.ts +225 -1
  81. package/src/execution/agents-assembly.ts +88 -0
  82. package/src/execution/concurrency-pool.ts +71 -9
  83. package/src/execution/dialog-queue.ts +101 -3
  84. package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +49 -2
  85. package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +87 -1
  86. package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +26 -0
  87. package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +83 -1
  88. package/src/execution/engine/__tests__/conformance/zcode-appserver-harness.ts +131 -0
  89. package/src/execution/engine/__tests__/registry.test.ts +90 -1
  90. package/src/execution/engine/engine-discovery.ts +12 -16
  91. package/src/execution/engine/engines/pi/task-spec-mapper.ts +3 -3
  92. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/fake-appserver.mjs +274 -0
  93. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-appserver.json +36 -0
  94. package/src/execution/engine/engines/zcode/__tests__/connection.test.ts +472 -0
  95. package/src/execution/engine/engines/zcode/__tests__/preparer-appserver.test.ts +387 -0
  96. package/src/execution/engine/engines/zcode/__tests__/session-channel.test.ts +780 -0
  97. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-appserver.test.ts +815 -0
  98. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-degrade.test.ts +462 -0
  99. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +119 -1
  100. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +136 -6
  101. package/src/execution/engine/engines/zcode/appserver-home.ts +442 -0
  102. package/src/execution/engine/engines/zcode/appserver-probe.ts +141 -0
  103. package/src/execution/engine/engines/zcode/connection.ts +585 -0
  104. package/src/execution/engine/engines/zcode/constants.ts +129 -0
  105. package/src/execution/engine/engines/zcode/golden-sample.ts +54 -8
  106. package/src/execution/engine/engines/zcode/preparer.ts +25 -22
  107. package/src/execution/engine/engines/zcode/session-channel.ts +656 -0
  108. package/src/execution/engine/engines/zcode/zcode-engine.ts +909 -41
  109. package/src/execution/engine/host-task-spec.ts +3 -3
  110. package/src/execution/engine/port.ts +41 -1
  111. package/src/execution/engine/registry.ts +53 -0
  112. package/src/execution/finalize-record.ts +63 -5
  113. package/src/execution/get-state-handshake.ts +85 -10
  114. package/src/execution/lifecycle-manager.ts +27 -3
  115. package/src/execution/manifest-store.ts +53 -62
  116. package/src/execution/notifier.ts +10 -10
  117. package/src/execution/notify-ledger.ts +117 -16
  118. package/src/execution/record-entry.ts +8 -2
  119. package/src/execution/record-store.ts +29 -15
  120. package/src/execution/session-pending.ts +213 -62
  121. package/src/execution/session-runner.ts +974 -128
  122. package/src/execution/sessions-index.ts +10 -55
  123. package/src/execution/settled-watchdog.ts +99 -0
  124. package/src/execution/subagent-actions-core.ts +686 -0
  125. package/src/execution/subagent-service.ts +396 -49
  126. package/src/execution/ui-request-handler-factory.ts +27 -4
  127. package/src/execution/worktree-git-ops.ts +397 -0
  128. package/src/execution/worktree-manager.ts +72 -10
  129. package/src/execution/worktree-registry.ts +6 -12
  130. package/src/index.ts +413 -6
  131. package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +42 -3
  132. package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +2 -4
  133. package/src/orchestration/__tests__/args-meta.test.ts +358 -0
  134. package/src/orchestration/__tests__/error-recovery-handlers.test.ts +1 -8
  135. package/src/orchestration/__tests__/error-recovery-rebuild-failure.test.ts +312 -0
  136. package/src/orchestration/__tests__/error-recovery-terminal-hardening.test.ts +382 -0
  137. package/src/orchestration/__tests__/file-run-store-prune.test.ts +168 -0
  138. package/src/orchestration/__tests__/file-run-store-throttle.test.ts +168 -0
  139. package/src/orchestration/__tests__/file-run-store.test.ts +389 -0
  140. package/src/orchestration/__tests__/helpers/flush-microtasks.ts +13 -0
  141. package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +34 -0
  142. package/src/orchestration/__tests__/lifecycle-abort-broadcast-signal.test.ts +357 -0
  143. package/src/orchestration/__tests__/lifecycle-recover-crashed.test.ts +289 -0
  144. package/src/orchestration/__tests__/lifecycle.test.ts +77 -0
  145. package/src/orchestration/__tests__/run-snapshot.test.ts +353 -0
  146. package/src/orchestration/__tests__/script-generate.test.ts +314 -0
  147. package/src/orchestration/__tests__/worker-pending-timeout-abort.test.ts +275 -0
  148. package/src/orchestration/__tests__/worker-script-builder-runtime.test.ts +78 -1
  149. package/src/orchestration/__tests__/workflow-files.test.ts +186 -0
  150. package/src/orchestration/__tests__/workflow-run-summary.test.ts +119 -0
  151. package/src/orchestration/__tests__/workflow-script-registry-impl.test.ts +124 -0
  152. package/src/orchestration/agent-opts-resolver.ts +7 -7
  153. package/src/orchestration/args-meta.ts +198 -0
  154. package/src/orchestration/error-recovery.ts +416 -146
  155. package/src/orchestration/execute-agent-call.ts +9 -9
  156. package/src/orchestration/file-run-store.ts +327 -0
  157. package/src/orchestration/launcher.ts +35 -26
  158. package/src/orchestration/lifecycle.ts +355 -79
  159. package/src/orchestration/models/agent-call.ts +7 -7
  160. package/src/orchestration/models/budget.ts +5 -5
  161. package/src/orchestration/models/run-runtime.ts +13 -13
  162. package/src/orchestration/models/trace.ts +8 -8
  163. package/src/orchestration/models/workflow-run.ts +27 -27
  164. package/src/orchestration/models/workflow-script.ts +4 -4
  165. package/src/orchestration/run-snapshot.ts +266 -0
  166. package/src/orchestration/script-generate.ts +154 -0
  167. package/src/orchestration/script-lint.ts +33 -33
  168. package/src/orchestration/worker-handle.ts +10 -10
  169. package/src/orchestration/worker-host.ts +10 -10
  170. package/src/orchestration/worker-script-builder.ts +401 -346
  171. package/src/orchestration/workflow-files.ts +37 -11
  172. package/src/orchestration/workflow-run-summary.ts +69 -0
  173. package/src/orchestration/workflow-script-registry-impl.ts +31 -9
  174. package/src/shared/__tests__/agent-ref.test.ts +209 -2
  175. package/src/shared/__tests__/atomic-write.test.ts +267 -0
  176. package/src/shared/__tests__/bounded-serialize.test.ts +236 -0
  177. package/src/shared/__tests__/injection-render.test.ts +518 -0
  178. package/src/shared/__tests__/resource-discovery-host-roots.test.ts +474 -0
  179. package/src/shared/__tests__/resource-discovery.test.ts +3 -2
  180. package/src/shared/agent-ref.ts +143 -2
  181. package/src/shared/atomic-write.ts +320 -0
  182. package/src/shared/bounded-serialize.ts +154 -0
  183. package/src/shared/injection-render.ts +279 -0
  184. package/src/shared/meta-parser.ts +41 -1
  185. package/src/shared/resource-discovery.ts +104 -37
  186. package/src/shared/resource-meta.ts +14 -0
  187. package/src/shared/xml-injection.ts +9 -9
  188. package/workflows/README.md +9 -9
  189. package/workflows/chain.js +4 -2
  190. package/workflows/map-reduce.js +5 -3
  191. package/workflows/parallel.js +5 -3
  192. package/workflows/review-fix-loop-utils.cjs +14 -3
  193. package/workflows/review-fix-loop.js +18 -7
  194. package/workflows/scatter-gather.js +4 -2
  195. package/dist/chunk-3VOERJPJ.js +0 -22
@@ -0,0 +1,686 @@
1
+ // src/execution/subagent-actions-core.ts
2
+ //
3
+ // subagent tool 六 handler 的领域内核(校验 / 守卫链 / 归属判定 / 终态映射)。
4
+ //
5
+ // 来源:pi-sw `src/interface/subagent-actions.ts` 的零 pi-API 部分原样下沉
6
+ //(sink 设计 docs/design/subagent-core-sink-design.md §3.3 D6② / U10②;
7
+ // ⛔4 行为快照等值测试见 __tests__/subagent-actions-core.test.ts——期望值
8
+ // 硬编码自迁移前 pi-sw 实现的实测输出,含错误文案锚)。
9
+ //
10
+ // 分层:本模块产出**领域对象**(StartHandlerResult / ListHandlerResult / ...),
11
+ // 不感知 {content, details} 包装与 TUI/GUI 渲染——宿主 adapter 负责把领域对象
12
+ // 包成工具结果(pi 侧收缩为「参数提取 + core 调用 + TUI 渲染」)。
13
+ //
14
+ // 平台中立性:全部错误文案 / 提示文案面向 LLM(行动语言),无宿主专属词汇,
15
+ // 文案内聚本模块(文案即行为,⛔4 逐字锚定)。
16
+
17
+ import { computeElapsedSeconds, projectOutcome } from "./execution-record.ts";
18
+ import { isResumable } from "./lifecycle-predicates.ts";
19
+ import { SLUG_MAX_LENGTH } from "./execute-options-mapper.ts";
20
+ import type { ModelInfo } from "./model-resolver.ts";
21
+ import type { SubagentService } from "./subagent-service.ts";
22
+ import { displayAgentName } from "../shared/agent-ref.ts";
23
+ import type {
24
+ BgResponse,
25
+ CancelResponse,
26
+ CloseResponse,
27
+ ExecutionRecord,
28
+ ExecutionStatus,
29
+ ExternalState,
30
+ ForkFromResponse,
31
+ ListResponse,
32
+ MessageResponse,
33
+ SubagentListItem,
34
+ SubagentRecord,
35
+ } from "./types.ts";
36
+ import { ResurrectDeniedError } from "./types.ts";
37
+
38
+ // ============================================================
39
+ // 常量
40
+ // ============================================================
41
+
42
+ /** list 默认 limit。 */
43
+ export const DEFAULT_LIST_LIMIT = 20;
44
+ /** list limit 上限。 */
45
+ export const MAX_LIST_LIMIT = 100;
46
+
47
+ /** background 启动提示文案(完成通知经自动注入消息投递,agent 不应轮询)。 */
48
+ export const BG_MESSAGE = "detached, will notify on completion (auto-injected message, do not poll)";
49
+
50
+ /** 通知投递契约回显恒值(契约声明;值语义由 execution/notify-ledger.ts 兑现)。 */
51
+ export const NOTIFY_CONTRACT = "ledger+at-least-once" as const;
52
+
53
+ /**
54
+ * fork-from 开场引导语框架(prompt 未指定时注入):先从继承的历史重建状态
55
+ * 再继续,防猜。
56
+ */
57
+ export const FORK_FROM_DEFAULT_PROMPT =
58
+ "You are taking over work from a previous subagent whose full conversation history you inherited (--fork). " +
59
+ "First reconstruct state from that history: list what was already done, decided, and left unfinished (a few bullet lines). " +
60
+ "Then continue the remaining work to completion.";
61
+
62
+ /**
63
+ * 有显式接续指令时的包裹框架:指令在前、上下文重建要求在后——指令首见即达,
64
+ * 不湮没在元说明里(弱模型友好)。
65
+ */
66
+ export function wrapForkFromPrompt(prompt: string): string {
67
+ return (
68
+ prompt.trim() +
69
+ "\n\n(You are continuing a previous subagent's inherited conversation via --fork. " +
70
+ "Reconstruct state from that history first — what was done, decided, and remains — then execute the instruction above.)"
71
+ );
72
+ }
73
+
74
+ // ============================================================
75
+ // 入参 / 出参类型
76
+ // ============================================================
77
+
78
+ /** start 入参(拍平后从 tool params 顶层来,task + slug 必填)。
79
+ * StartHandlerInput 是 SubagentExecuteParams 的子集(13 字段全 optional);
80
+ * 调用方传整个 params(含 action/listParam/cancelParam),多余字段被忽略。 */
81
+ export interface StartHandlerInput {
82
+ task?: string;
83
+ /** 短标签(≤35 字符,kebab-case),必填。 */
84
+ slug?: string;
85
+ agent?: string;
86
+ model?: string;
87
+ thinkingLevel?: string;
88
+ skillPath?: string;
89
+ appendSystemPrompt?: string[];
90
+ schema?: Record<string, unknown>;
91
+ maxTurns?: number;
92
+ graceTurns?: number;
93
+ /** fork 模式:继承主 session 上下文。 */
94
+ fork?: boolean;
95
+ /** worktree 模式:文件系统隔离运行。 */
96
+ worktree?: boolean;
97
+ /** 覆盖子 agent 工作目录(默认 mainCwd)。 */
98
+ cwd?: string;
99
+ /** 可持续对话模式(true = chatMode,轮次完成进 idle 等续聊)。 */
100
+ conversation?: boolean;
101
+ /**
102
+ * 空闲超时毫秒数(仅 conversation 模式有意义,覆盖默认 5min)。
103
+ * 显式传 0/负数 = 禁用 idle GC(不挂 timer);不传走 env/默认优先级。
104
+ */
105
+ idleTimeoutMs?: number;
106
+ /** 执行引擎(三层路由第一层:本参数 > agent frontmatter engine > config defaultEngine)。 */
107
+ engine?: string;
108
+ }
109
+
110
+ /** start 领域对象(宿主 adapter 包成 bg 工具结果)。 */
111
+ export type StartHandlerResult = {
112
+ kind: "bg";
113
+ subagentId: string;
114
+ sessionFile: string | undefined;
115
+ /** 短标签,来自 record(handle.details.slug)。用于 result 行展示。 */
116
+ slug: string;
117
+ /**
118
+ * registry 全等回显:handle.details.model = record.model = `${provider}/${id}`,
119
+ * 源头是 resolveModel 裁决放行的条目——通过校验 = 子进程必然按此名执行。
120
+ */
121
+ model: string;
122
+ response: BgResponse;
123
+ };
124
+
125
+ export interface ListHandlerInput {
126
+ includeFinished?: boolean;
127
+ limit?: number;
128
+ }
129
+
130
+ /** list 领域对象(宿主 adapter 包成 list 工具结果,最外层 subagentId/sessionFile 为 null)。 */
131
+ export interface ListHandlerResult {
132
+ response: ListResponse;
133
+ }
134
+
135
+ export interface CancelHandlerInput {
136
+ subagentId?: string;
137
+ }
138
+
139
+ /** cancel 领域对象(宿主 adapter 包成 cancel 工具结果)。 */
140
+ export interface CancelHandlerResult {
141
+ subagentId: string;
142
+ response: CancelResponse;
143
+ }
144
+
145
+ export interface MessageHandlerInput {
146
+ subagentId?: string;
147
+ text?: string;
148
+ interrupt?: boolean;
149
+ }
150
+
151
+ /** message 领域对象(宿主 adapter 包成 message 工具结果)。
152
+ * slug 来自 record(GUI message 通道的留痕 details 需要),
153
+ * 避免调用方二次 getRecordForAction 查询。 */
154
+ export type MessageHandlerResult = {
155
+ kind: "message";
156
+ subagentId: string;
157
+ slug: string;
158
+ response: MessageResponse;
159
+ };
160
+
161
+ export interface CloseHandlerInput {
162
+ subagentId?: string;
163
+ force?: boolean;
164
+ }
165
+
166
+ /** close 领域对象(宿主 adapter 包成 close 工具结果)。 */
167
+ export type CloseHandlerResult = {
168
+ kind: "close";
169
+ subagentId: string;
170
+ response: CloseResponse;
171
+ };
172
+
173
+ export interface ForkFromHandlerInput {
174
+ sourceSubagentId?: string;
175
+ prompt?: string;
176
+ }
177
+
178
+ /** fork-from 领域对象(宿主 adapter 包成 fork-from 工具结果)。 */
179
+ export type ForkFromHandlerResult = {
180
+ kind: "fork-from";
181
+ /** 新 subagent 的 record id(后续续聊用 action:'message')。 */
182
+ subagentId: string;
183
+ /** 作为 --fork 继承源的旧记录 session 文件。 */
184
+ sourceSessionFile: string;
185
+ response: ForkFromResponse;
186
+ };
187
+
188
+ // ============================================================
189
+ // message 拒绝文案分流(endedMessageGuard)
190
+ // ============================================================
191
+
192
+ /**
193
+ * message 拒绝时的可行动文案分流。
194
+ *
195
+ * 背景:getRecordForAction 冷查只认 status==='running'(可续聊重建),任何终态/
196
+ * 异归属记录都落到同一个「not found or not owned」错误,把两类完全不同的场景混为一谈:
197
+ * - user-close/cancelled:用户主动告别,记录真没了 → 引导 start 新的
198
+ * - parent-shutdown/gc/orphan 等:父会话重启/进程退出导致的断联,对话 jsonl 完好,
199
+ * resume/fork 基建现成 → 引导 fork-from 从旧记录接续
200
+ *
201
+ * 分流规则(消费方是 LLM,保持正交简单):
202
+ * - 找不到记录 → 原样透传 getRecordForAction 错误(id 打错最常见,原文案最准)
203
+ * - closed + user-close/cancelled →「已主动关闭,无法续聊」文案
204
+ * - 其余(closed 其他 reason / running 但异归属)→「断联可接续」文案
205
+ */
206
+ export function endedMessageGuard(service: SubagentService, id: string, original: unknown): Error {
207
+ // 透明重生守卫拒绝(worktree/异进程占用)原样透传——错误自带完整行动语言,
208
+ // 若被下方 fork-from 指引改写会误导 agent 走已被判死的通道(types.ts 契约声明)。
209
+ if (original instanceof ResurrectDeniedError) return original;
210
+ let snap: SubagentRecord | undefined;
211
+ try {
212
+ snap = service.lookupRecordAnyState(id);
213
+ } catch {
214
+ snap = undefined;
215
+ }
216
+ if (!snap) {
217
+ return original instanceof Error ? original : new Error(String(original));
218
+ }
219
+ if (snap.status === "closed") {
220
+ if (snap.closedReason === "cancelled" || snap.closedReason === "user-close") {
221
+ return new Error(
222
+ `subagent ${id} was deliberately closed by user (closedReason: ${snap.closedReason}) — ` +
223
+ `it cannot be messaged or resumed; nothing can reattach to it. ` +
224
+ `Recovery: start a new subagent (action:'start'); use action:'list' with includeFinished:true to review its final output.`,
225
+ );
226
+ }
227
+ return new Error(
228
+ `subagent ${id} is ended but reconnectable (closedReason: ${snap.closedReason ?? "unknown"}` +
229
+ `${describeClosedContext(snap)}). Its conversation history is intact at ${snap.sessionFile ?? "(session file unavailable)"}. ` +
230
+ `Recovery: resume from that history with {"action":"fork-from","forkFromParam":{"sourceSubagentId":"${id}"}}, ` +
231
+ `or read key points directly from the session file.`,
232
+ );
233
+ }
234
+ // running 但未通过 getRecordForAction:记录属于当前进程外的另一个 session 树
235
+ //(主会话重启前的遗留态,或其他并发进程的活跃 subagent)。无论哪种,历史
236
+ // jsonl 只读安全,fork-from 快照接续均有效。
237
+ return new Error(
238
+ `subagent ${id} is alive but belongs to a different session tree than this one` +
239
+ `${describeClosedContext(snap)}. You cannot message it from here. ` +
240
+ `Recovery: branch from its history with {"action":"fork-from","forkFromParam":{"sourceSubagentId":"${id}"}}` +
241
+ `${snap.sessionFile ? ` (source session: ${snap.sessionFile})` : ``}; otherwise start a new subagent.`,
242
+ );
243
+ }
244
+
245
+ /** 终态快照的上下文人话短语(仅作补充描述,主分支逻辑在 endedMessageGuard)。 */
246
+ function describeClosedContext(r: SubagentRecord): string {
247
+ switch (r.closedReason) {
248
+ case "parent-shutdown":
249
+ return " — it was disconnected when the previous parent session exited";
250
+ case "parent-fork":
251
+ return " — it was detached when the previous parent session forked";
252
+ case "parent-new":
253
+ return " — it was detached when the previous parent session switched";
254
+ case "disconnected":
255
+ return " — it ended in a previous session (exact cause unknown)";
256
+ default:
257
+ return "";
258
+ }
259
+ }
260
+
261
+ // ============================================================
262
+ // 终态映射 / list 投影
263
+ // ============================================================
264
+
265
+ /** exhaustiveness 兜底:default 分支把 status 收敛为 never,ExecutionStatus 加态时 tsc 报错。 */
266
+ function assertNever(value: never): string {
267
+ return String(value);
268
+ }
269
+
270
+ /**
271
+ * 内部 ExecutionStatus → 对外 state 映射(设计决策 10 细则 3)。
272
+ * 两态收敛后的真实映射只有两条:
273
+ * running → active / closed → ended(closed 统一终态,含 cancelled)
274
+ * ExternalState 仍声明 waiting/error 四态联合(对外契约不变),但当前状态机不产生
275
+ * 这两个值——它们是历史多态映射(idle→waiting / failed+crashed→error)的遗留声明。
276
+ * 未来内部加态必须扩展此处,漏加会在 default 分支编译报错(而非静默返回 undefined
277
+ * 让 state 字段以无主值进入 listResponse JSON)。
278
+ */
279
+ export function mapExternalState(status: ExecutionStatus): ExternalState {
280
+ switch (status) {
281
+ case "running":
282
+ return "active";
283
+ case "closed":
284
+ // closed 统一终态(含 cancelled)。对外映射为 ended。
285
+ return "ended";
286
+ default:
287
+ throw new Error(`mapExternalState: unhandled ExecutionStatus ${assertNever(status)}`);
288
+ }
289
+ }
290
+
291
+ /** SubagentRecord → SubagentListItem(state 四态主字段 + status 调试字段,duration 实时计算)。
292
+ * parent 从 record.parentRecordId 派生(配合直接父守卫),resumable 从 isResumable 派生
293
+ * (「可续聊」对外表达);outcome 一等终态语义(projectOutcome 唯一出口),closedReason
294
+ * 退出对外 JSON(保留为 record 内部诊断字段),对外成败判读收口到 outcome。
295
+ * agent 是 GUI/TUI list 共用的显示名——取 basename 短名(displayAgentName),
296
+ * 完整路径保留在 record.agent(数据层)。 */
297
+ export function recordToListItem(r: SubagentRecord): SubagentListItem {
298
+ return {
299
+ subagentId: r.id,
300
+ agent: displayAgentName(r.agent),
301
+ slug: r.slug,
302
+ state: mapExternalState(r.status),
303
+ status: r.status,
304
+ mode: r.mode,
305
+ duration: computeElapsedSeconds(r),
306
+ model: r.model,
307
+ totalTokens: r.totalTokens,
308
+ sessionFile: r.sessionFile,
309
+ parent: r.parentRecordId,
310
+ resumable: isResumable(r),
311
+ outcome: projectOutcome(r),
312
+ };
313
+ }
314
+
315
+ // ============================================================
316
+ // start handler
317
+ // ============================================================
318
+
319
+ export async function startHandler(
320
+ service: SubagentService,
321
+ input: StartHandlerInput | undefined,
322
+ signal: AbortSignal | undefined,
323
+ ctxModel?: ModelInfo,
324
+ ): Promise<StartHandlerResult> {
325
+ if (!input) throw new Error(
326
+ "action:'start' requires task and slug (top-level fields). " +
327
+ 'Correct: {"action":"start","task":"<your task>","slug":"<kebab-case>"}',
328
+ );
329
+ // task 必填 + 空白校验
330
+ const task = input.task?.trim();
331
+ if (!task) throw new Error(
332
+ "task is required for action:'start' (top-level field, must not be whitespace-only). " +
333
+ 'Correct: {"action":"start","task":"...","slug":"..."}',
334
+ );
335
+ // slug 必填 + 空白校验 + 长度校验(≤ SLUG_MAX_LENGTH 字符)
336
+ const slug = input.slug?.trim();
337
+ if (!slug) throw new Error(
338
+ "slug is required for action:'start' (top-level field, must not be whitespace-only). " +
339
+ 'Correct: {"action":"start","task":"...","slug":"<kebab-case>"}',
340
+ );
341
+ if (slug.length > SLUG_MAX_LENGTH) throw new Error(`slug must be ≤${SLUG_MAX_LENGTH} chars (got ${slug.length}). Shorten to a kebab-case label, e.g. "fix-login", "extract-urls".`);
342
+
343
+ const handle = await service.execute({
344
+ task,
345
+ slug,
346
+ agent: input.agent,
347
+ model: input.model,
348
+ thinkingLevel: input.thinkingLevel,
349
+ skillPath: input.skillPath,
350
+ appendSystemPrompt: input.appendSystemPrompt,
351
+ schema: input.schema,
352
+ maxTurns: input.maxTurns,
353
+ graceTurns: input.graceTurns,
354
+ fork: input.fork,
355
+ worktree: input.worktree,
356
+ cwd: input.cwd,
357
+ conversation: input.conversation,
358
+ idleTimeoutMs: input.idleTimeoutMs,
359
+ engine: input.engine,
360
+ ctxModel,
361
+ signal,
362
+ // background detached 运行,完成由 notify 驱动新 turn。
363
+ });
364
+
365
+ return {
366
+ kind: "bg",
367
+ subagentId: handle.subagentId,
368
+ sessionFile: handle.sessionFile,
369
+ slug: handle.details.slug,
370
+ // registry 全等回显:record.model 由 resolved(裁决放行条目)拼接,原样透出。
371
+ model: handle.details.model,
372
+ response: {
373
+ status: "running",
374
+ mode: "background",
375
+ message: BG_MESSAGE,
376
+ notifyContract: NOTIFY_CONTRACT,
377
+ },
378
+ };
379
+ }
380
+
381
+ // ============================================================
382
+ // list handler
383
+ // ============================================================
384
+
385
+ /**
386
+ * list 数据源(诚实声明):
387
+ * collectRecords(limit, statusFilter) 合并内存(running) + 磁盘(重建)。磁盘源天然
388
+ * 跨 session 可见——/new /resume /fork 后前 session 的终态 record 仍在 sessions
389
+ * 目录里(直到 GC)。内存源仅当前 session 的 running record。
390
+ */
391
+ export function listHandler(
392
+ service: SubagentService,
393
+ input: ListHandlerInput | undefined,
394
+ ): ListHandlerResult {
395
+ const includeFinished = input?.includeFinished === true;
396
+ // limit 夹紧:下限 1,上限 MAX_LIST_LIMIT
397
+ const rawLimit = input?.limit ?? DEFAULT_LIST_LIMIT;
398
+ const limit = Math.max(1, Math.min(rawLimit, MAX_LIST_LIMIT));
399
+
400
+ // collectRecords 是 service 核心能力:statusFilter 决定 running-only 还是全部。
401
+ // 防截断(先多取再过滤)已下沉到 store 层——这里直接传 limit + filter。
402
+ const filter = includeFinished ? "all" : "running";
403
+ const all = service.collectRecords(limit, filter);
404
+ // collectRecords 磁盘源是 light(无 totalTokens/model 等):SubagentListItem 对
405
+ // LLM 消费方暴露 totalTokens/model,逐项 getFullRecord 补全(per-file 缓存,仅首次
406
+ // 全量解析;显式 tool 调用非渲染热路径,成本可接受)。
407
+ const items: SubagentListItem[] = all.map((r) =>
408
+ recordToListItem(service.getFullRecord(r.id) ?? r),
409
+ );
410
+ const running = items.filter((i) => i.status === "running").length;
411
+
412
+ return { response: { running, items } };
413
+ }
414
+
415
+ // ============================================================
416
+ // cancel handler
417
+ // ============================================================
418
+
419
+ export async function cancelHandler(
420
+ service: SubagentService,
421
+ input: CancelHandlerInput | undefined,
422
+ ): Promise<CancelHandlerResult> {
423
+ const id = input?.subagentId?.trim();
424
+ if (!id) throw new Error("cancelParam.subagentId is required for action:'cancel'");
425
+
426
+ // step 1: id 不存在(findRecord 只查内存 running record,不从 session.jsonl 重建)
427
+ const rec = service.findRecord(id);
428
+ if (!rec) {
429
+ // 全树可见后,list/completion 可能列出其他进程(父/兄弟)的 running record
430
+ //(collectRecords 扫共享 sessionsDir 按 rootSessionId 过滤,跨进程互相可见),而 cancel
431
+ // 只作用于本进程内存 record。区分两种失败,避免「may have finished」误导(该 record 正
432
+ // 被列出且未 finished,只是不属于本进程内存)。仅文案区分,不改 cancel 作用域。
433
+ const treeRec = service.collectRecords(DEFAULT_LIST_LIMIT, "all").find((r) => r.id === id);
434
+ if (treeRec && treeRec.status === "running") {
435
+ throw new Error(
436
+ `Subagent record "${id}" is running but owned by another process in the tree ` +
437
+ `(it was spawned by a different subagent process) — this process cannot cancel it; ` +
438
+ `cancel only works for subagents spawned by the current process.`,
439
+ );
440
+ }
441
+ throw new Error(`No subagent record with id "${id}". It may have finished — use action:'list' with includeFinished:true to verify.`);
442
+ }
443
+ // step 2: controller 检查(controller 为 undefined 表示 record 已终态或未启动)
444
+ if (rec.mode !== "background") {
445
+ throw new Error(`Cannot cancel subagent ${id} (unsupported mode: ${rec.mode})`);
446
+ }
447
+ // 对话模式 cancel = close(force:true) 别名:chatMode record(running/idle)
448
+ // 走 close 行为路径(idle 终态化 done;running 立即 SIGTERM cancelled),
449
+ // 返回 cancel 响应(向后兼容 cancel action 的返回类型)。非 chatMode 保持现有 cancel 行为。
450
+ if (rec.chatMode) {
451
+ const chatRecord = service.getRecordForAction(id);
452
+ await service.closeSubagent(chatRecord, true);
453
+ return { subagentId: id, response: { cancelled: true } };
454
+ }
455
+ // step 3: service.cancel boolean(list-view 契约不变);false = 已终态(CAS 抢锁失败)。
456
+ // 注意:不嵌入 rec.status——findRecord 快照可能已过期(TOCTOU:cancel 期间 detached
457
+ // 路径 CAS 到 done/failed)。重新查当前状态,避免「status: running」与「already finished」矛盾。
458
+ if (!service.cancel(id)) {
459
+ // CAS 失败 = record 在 cancel 期间被 detached 路径 finalize(done/failed)。
460
+ // re-query 查当前真实状态。终态 record 被 archive 立即移出内存,
461
+ // 诚实报告 "unknown (evicted from memory)" 而非回落到可能过期的 rec.status。
462
+ const now = service.findRecord(id);
463
+ const statusDesc = now ? now.status : "unknown (evicted from memory)";
464
+ throw new Error(`Subagent ${id} could not be cancelled (it likely just finished; status: ${statusDesc})`);
465
+ }
466
+ return { subagentId: id, response: { cancelled: true } };
467
+ }
468
+
469
+ // ============================================================
470
+ // message handler(对话模式续聊/插入)
471
+ // ============================================================
472
+
473
+ /**
474
+ * message action handler:向对话模式 subagent 续聊/插入消息。
475
+ *
476
+ * 状态 × interrupt 自动映射(agent 只表达意图):
477
+ * running → deliverMessage 热路径(进程活:prompt + streamingBehavior,interrupt=true
478
+ * 抢占 / false 排队)
479
+ * 进程死 → deliverMessage 冷路径(resumeRound 重开 session + prompt,interrupt 自动
480
+ * 退化,agent 无感)
481
+ * 终态 → throw ended(正常路径不命中——终态 record 已 archive,getRecordForAction 先 throw not found)
482
+ *
483
+ * 归属守卫:getRecordForAction 内部校验 rootSessionId。
484
+ *
485
+ * @throws Error subagentId/text 缺失 / 不存在或非本 session 所有 / 已结束
486
+ */
487
+ export async function messageHandler(
488
+ service: SubagentService,
489
+ input: MessageHandlerInput | undefined,
490
+ ): Promise<MessageHandlerResult> {
491
+ const id = input?.subagentId?.trim();
492
+ if (!id) throw new Error("messageParam.subagentId is required for action:'message'");
493
+ const text = input?.text?.trim();
494
+ if (!text) throw new Error(
495
+ "messageParam.text is required for action:'message' (must not be whitespace-only). " +
496
+ 'Correct: {"action":"message","messageParam":{"subagentId":"sa-...","text":"your follow-up"}}',
497
+ );
498
+ const interrupt = input?.interrupt === true;
499
+
500
+ // 归属守卫:getRecordForAction 内部校验 rootSessionId。
501
+ // 拒绝时经 endedMessageGuard 分流:找不到 → 原错误;user-close/cancelled
502
+ // → 「已主动关闭」;断联/已完成/异归属 → fork-from 可行动指引。仅 message action
503
+ // 升级文案——close/cancel 维持原语义(它们不需要恢复通道)。
504
+ let record: ExecutionRecord;
505
+ try {
506
+ record = service.getRecordForAction(id, { allowReconnect: true });
507
+ } catch (err) {
508
+ throw endedMessageGuard(service, id, err);
509
+ }
510
+
511
+ // one-shot upgrade:非 chatMode 的 active record(running/idle)收到 message 时
512
+ // 自动升级为 chatMode,后续走 deliverMessage 统一投递路径(热路径或冷路径 resume)。
513
+ // closed/cancelled 终态 record 不可 upgrade(getRecordForAction 已抛 not found)。
514
+ // chatMode 是 ExecutionRecord 的 readonly 字段,用 Mutable<T> 显式断言绕过 readonly 约束(upgrade 语义)。
515
+ // Object.assign 隐式绕过 readonly 不可追踪,改为单字段显式赋值。
516
+ // 进程内 upgrade 入口——one-shot 首条 message 触发 upgrade 置位 chatMode=true。
517
+ // 与 subagent-service.ts getRecordForAction 磁盘重建(跨重启恢复入口)分工:
518
+ // 本入口服务进程内 one-shot,跨重启路径恒被 getRecordForAction 磁盘重建绕过
519
+ //(该处无条件 chatMode=true)。改动这两处必须协同。
520
+ if (!record.chatMode && record.status === "running") {
521
+ type Mutable<T> = { -readonly [K in keyof T]: T[K] };
522
+ (record as Mutable<ExecutionRecord>).chatMode = true;
523
+ }
524
+
525
+ // chatMode 统一投递:按进程死活分流(热路径 prompt+streamingBehavior / 冷路径 resume),
526
+ // 不按 record.status(进程长驻,idle 态进程仍活,续聊走热路径 prompt 而非重开 session)。
527
+ // upgrade 后 record.chatMode 已为 true,统一进此分支。
528
+ if (record.chatMode) {
529
+ await service.deliverMessage(record, text, interrupt);
530
+ } else {
531
+ // 终态(closed/cancelled):防御性兜底(终态 record 已 archive,正常走 not found)
532
+ throw new Error(
533
+ `subagent ${id} has ended (status: ${record.status}), cannot message. ` +
534
+ `Recovery: use action:'close' to clean up, then action:'start' a new subagent.`,
535
+ );
536
+ }
537
+ return { kind: "message", subagentId: id, slug: record.slug, response: { delivered: true } };
538
+ }
539
+
540
+ // ============================================================
541
+ // close handler(对话模式结束)
542
+ // ============================================================
543
+
544
+ /**
545
+ * close action handler:结束 subagent(对话模式为主,one-shot 同样支持)。
546
+ *
547
+ * force 语义(设计决策 5/10):
548
+ * force:false(默认)= 优雅关闭——
549
+ * 无在跑轮(等待续聊 timer armed / 无活进程)→ 立即终态化(closed + user-close,回收保活进程)
550
+ * 有活进程在跑轮 → 置 closeAfterRound,轮完成时终态化——返回 {closed:true} 即承诺轮结束后资源已释放
551
+ * force:true = 立即终止——running 立即 SIGTERM(cancelBackground 显式 kill)+ closed+cancelled
552
+ *
553
+ * 行为分流委托 service.closeSubagent(归属守卫由 getRecordForAction 把关)。
554
+ * 已终态 record 由 getRecordForAction throw not found(「已结束的不能再操作」语义)。
555
+ */
556
+ export async function closeHandler(
557
+ service: SubagentService,
558
+ input: CloseHandlerInput | undefined,
559
+ ): Promise<CloseHandlerResult> {
560
+ const id = input?.subagentId?.trim();
561
+ if (!id) throw new Error("closeParam.subagentId is required for action:'close'");
562
+ const force = input?.force === true;
563
+
564
+ // 归属守卫(决策 3)+ 行为分流(service.closeSubagent)
565
+ const record = service.getRecordForAction(id);
566
+ await service.closeSubagent(record, force);
567
+
568
+ return { kind: "close", subagentId: id, response: { closed: true } };
569
+ }
570
+
571
+ // ============================================================
572
+ // fork-from handler(断联恢复通道)
573
+ // ============================================================
574
+
575
+ /**
576
+ * fork-from action handler:从旧 subagent 的会话历史 spawn 新 id 接续。
577
+ *
578
+ * 用于 subagent 因会话重启/进程退出而断联后的恢复:新进程以 --fork 指向旧 session
579
+ * 文件(copy-on-write 建分支会话),继承全部对话历史;源文件只读不续写。
580
+ * 旧记录本身不动——closed 单向状态机不变量、tryTransition 语义均不触碰。
581
+ *
582
+ * 守卫链(拒绝原因与行动语言对齐,见 assertAndLookupForkFromSource):
583
+ * 1. 本进程内存 running → 还活着,应走 message(防双写同一子 session 文件)
584
+ * 2. 不存在 → 引导 list 确认
585
+ * 3. 异进程活跃 → 别处正跑,不可从此接续(同 id 双写风险;等其结束或在其所属会话内操作)
586
+ * 4. cancelled/user-close → 用户主动告别,真没了(不提供接续通道)
587
+ * 5. worktree 记录 → checkout 不可复用,fork 子进程 cwd 会回落主仓破坏隔离
588
+ * 6. 无子 session 文件 → 无历史可继承(entry-only 孤儿:spawn 窗口期中断)
589
+ *
590
+ * @throws Error 各守卫命中 / service.execute 失败(引擎不支持等)
591
+ */
592
+ export async function forkFromHandler(
593
+ service: SubagentService,
594
+ input: ForkFromHandlerInput | undefined,
595
+ ): Promise<ForkFromHandlerResult> {
596
+ const id = input?.sourceSubagentId?.trim();
597
+ if (!id) throw new Error("forkFromParam.sourceSubagentId is required for action:'fork-from'");
598
+ const prompt = input?.prompt?.trim() ?? "";
599
+ const task = prompt ? wrapForkFromPrompt(prompt) : FORK_FROM_DEFAULT_PROMPT;
600
+
601
+ const source = assertAndLookupForkFromSource(service, id);
602
+
603
+ // slug 派生:源 slug + -resumed 后缀(截断到上限)。仅展示标签,不需唯一。
604
+ const baseSlug = (source.slug || source.agent || "resumed").slice(0, SLUG_MAX_LENGTH - "-resumed".length);
605
+ const handle = await service.execute({
606
+ task,
607
+ slug: `${baseSlug}-resumed`,
608
+ forkFromSessionFile: source.sessionFile,
609
+ });
610
+
611
+ return {
612
+ kind: "fork-from",
613
+ subagentId: handle.subagentId,
614
+ sourceSessionFile: source.sessionFile,
615
+ response: { newSubagentId: handle.subagentId, sourceSessionFile: source.sessionFile },
616
+ };
617
+ }
618
+
619
+ /** forkFromHandler 的守卫链(fork-from handler doc 的守卫 1–6 原样提取):按序校验
620
+ * 源记录可接续,命中即抛带行动语言的 Error;全部通过则返回源 SubagentRecord
621
+ * (守卫 6 已保证 sessionFile 非空,返回类型随之收窄)。 */
622
+ function assertAndLookupForkFromSource(service: SubagentService, id: string): SubagentRecord & { sessionFile: string } {
623
+ // 守卫 1:本进程内存 running —— 直接 message 即可,fork-from 会双写其 session 文件。
624
+ if (service.findRecord(id)) {
625
+ throw new Error(
626
+ `subagent ${id} is still active in this process — use action:'message' to continue it directly. ` +
627
+ `If you want a parallel branch from its history, close it first (action:'close'), then fork-from.`,
628
+ );
629
+ }
630
+
631
+ // 守卫 2:全态查找(内存 archived + 磁盘重建)。
632
+ const source = service.lookupRecordAnyState(id);
633
+ if (!source) {
634
+ throw new Error(
635
+ `No subagent record with id "${id}". It may never have existed or been garbage-collected — ` +
636
+ `use action:'list' with includeFinished:true to verify the id.`,
637
+ );
638
+ }
639
+
640
+ // 守卫 3:异进程活跃(externalInstance = 另一进程的活 pid marker)。
641
+ // 双写防护:fork 虽 copy-on-write(历史 jsonl 只读),但源仍在异进程运行时接续容易
642
+ // 读到半截历史,等它结束再接更安全。判据只认 externalInstance(真实活 pid 探针
643
+ // 命中),不拦 status==='running' 的快照——后者含跨重启回退重建的 running 记录
644
+ //(无活 pid,历史已完整落盘),它们正是 endedMessageGuard 指引 fork-from 的目标;
645
+ // 拦了会让 agent 在「建议 fork-from」与「fork-from 拒绝 running」两条错误间死循环。
646
+ if (source.externalInstance !== undefined) {
647
+ throw new Error(
648
+ `subagent ${id} is still running in another process (alive pid marker present). ` +
649
+ `Recovery: wait until it finishes, or operate it in its own session; then retry fork-from.`,
650
+ );
651
+ }
652
+
653
+ // 守卫 4:主动告别(cancelled tombstone / user-close 正式关闭)——close 语义无旁路:
654
+ // fork-from 与 message 一致拒绝(guard 一致性规格),文案升级为统一「主动关闭」形态
655
+ //(含 closedReason 显式列入),与 deliverMessage 的同类分支同语系不同落地(此处强调不可 branch)。
656
+ if (source.status === "closed" && (source.closedReason === "cancelled" || source.closedReason === "user-close")) {
657
+ throw new Error(
658
+ `subagent ${id} was deliberately closed by user (closedReason: ${source.closedReason}) — ` +
659
+ `deliberately-closed records cannot be resumed or branched from; nothing can reattach to them. ` +
660
+ `Recovery: start a fresh subagent (action:'start'); use action:'list' with includeFinished:true to review its final output.`,
661
+ );
662
+ }
663
+
664
+ // 守卫 5:worktree 记录 —— WorktreeHandle 不可序列化,checkout 已被 reaper/cleanup
665
+ // 回收;fork 子进程若复用旧路径会回落主 repo(破坏文件隔离)。与 deliverMessage 的
666
+ // hadWorktree 守卫同一判据同一理由。
667
+ if (source.worktree === true) {
668
+ throw new Error(
669
+ `subagent ${id} was created with worktree isolation; that binding was lost when its parent process ended. ` +
670
+ `Resuming from its history would run outside the original worktree isolation. ` +
671
+ `Recovery: start a new subagent with action:'start' and carry over key findings manually ` +
672
+ `(read ${source.sessionFile ?? "its session file"} if needed).`,
673
+ );
674
+ }
675
+
676
+ // 守卫 6:无子 session 文件(entry-born 孤儿:spawn 窗口期中断,从未开跑)。
677
+ const sessionFile = source.sessionFile;
678
+ if (!sessionFile) {
679
+ throw new Error(
680
+ `subagent ${id} has no child session file to inherit from (it never started successfully). ` +
681
+ `Recovery: start a fresh subagent (action:'start') describing the task again.`,
682
+ );
683
+ }
684
+
685
+ return { ...source, sessionFile };
686
+ }