@zhushanwen/subagent-core 0.7.1 → 0.9.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 (176) hide show
  1. package/dist/{chunk-VURUAGMM.js → chunk-LZJOEDGQ.js} +603 -154
  2. package/dist/engine-discovery-scan-Da4Dkzsi.d.cts +818 -0
  3. package/dist/engine-discovery-scan-Da4Dkzsi.d.ts +818 -0
  4. package/dist/execution/engine/engine-discovery-scan.cjs +325 -154
  5. package/dist/execution/engine/engine-discovery-scan.d.cts +1 -2
  6. package/dist/execution/engine/engine-discovery-scan.d.ts +1 -2
  7. package/dist/execution/engine/engine-discovery-scan.js +1 -1
  8. package/dist/execution/relay-env.js +5 -4
  9. package/dist/index.cjs +9799 -7954
  10. package/dist/index.d.cts +1896 -1360
  11. package/dist/index.d.ts +1896 -1360
  12. package/dist/index.js +8952 -7513
  13. package/dist.bundle/index.cjs +7897 -6054
  14. package/package.json +5 -5
  15. package/src/__tests__/manifest-store.test.ts +16 -15
  16. package/src/__tests__/record-store-last-line.test.ts +12 -0
  17. package/src/__tests__/robustness-medium-batch1.test.ts +10 -14
  18. package/src/__tests__/robustness-medium-batch2.test.ts +7 -2
  19. package/src/core/logger.ts +1 -1
  20. package/src/execution/__tests__/alive-store.test.ts +117 -1
  21. package/src/execution/__tests__/batch-finalized.test.ts +206 -0
  22. package/src/execution/__tests__/chat-engine-routing.test.ts +117 -1
  23. package/src/execution/__tests__/{cold-resurrect.test.ts → cold-lookup.test.ts} +133 -33
  24. package/src/execution/__tests__/collect-coordinator-service.test.ts +7 -5
  25. package/src/execution/__tests__/conversation-continuation.test.ts +1452 -0
  26. package/src/execution/__tests__/delivery-methods.test.ts +90 -176
  27. package/src/execution/__tests__/dialog-queue.test.ts +70 -3
  28. package/src/execution/__tests__/dispose-manifest-recovery.test.ts +475 -0
  29. package/src/execution/__tests__/execute-and-await-worktree.test.ts +3 -3
  30. package/src/execution/__tests__/execute-nesting.test.ts +8 -5
  31. package/src/execution/__tests__/execution-record.test.ts +8 -148
  32. package/src/execution/__tests__/finalize-record.test.ts +295 -129
  33. package/src/execution/__tests__/get-record-for-action-restart.test.ts +21 -29
  34. package/src/execution/__tests__/helpers/fake-engine-port.ts +13 -68
  35. package/src/execution/__tests__/helpers/legacy-sidecar.ts +33 -0
  36. package/src/execution/__tests__/helpers/subagent-service-mocks.ts +9 -5
  37. package/src/execution/__tests__/idle-gc.test.ts +60 -0
  38. package/src/execution/__tests__/inflight-production-wiring.test.ts +223 -0
  39. package/src/execution/__tests__/lifecycle-manager.test.ts +3 -254
  40. package/src/execution/__tests__/manifest-store-tmp-recovery.test.ts +43 -22
  41. package/src/execution/__tests__/model-config-service.test.ts +111 -0
  42. package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +10 -6
  43. package/src/execution/__tests__/nested-visibility.test.ts +13 -1
  44. package/src/execution/__tests__/notify-ledger.test.ts +42 -1
  45. package/src/execution/__tests__/rebuild-indexes.test.ts +329 -0
  46. package/src/execution/__tests__/record-binding.test.ts +613 -0
  47. package/src/execution/__tests__/record-origin.test.ts +318 -0
  48. package/src/execution/__tests__/record-store-intent-api.test.ts +600 -0
  49. package/src/execution/__tests__/record-store-orphan-revive.test.ts +71 -3
  50. package/src/execution/__tests__/record-store.test.ts +72 -54
  51. package/src/execution/__tests__/recursive-visibility-baseline.test.ts +8 -5
  52. package/src/execution/__tests__/round-supervisor-workflow-origin.test.ts +205 -0
  53. package/src/execution/__tests__/run-orchestration-write-lease.test.ts +293 -0
  54. package/src/execution/__tests__/run-orchestration.test.ts +199 -0
  55. package/src/execution/__tests__/session-file-gc.test.ts +35 -4
  56. package/src/execution/__tests__/session-reconstructor.test.ts +78 -0
  57. package/src/execution/__tests__/state-marker.test.ts +288 -0
  58. package/src/execution/__tests__/stream-sink-retirement.test.ts +8 -5
  59. package/src/execution/__tests__/subagent-actions-core.test.ts +67 -16
  60. package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +0 -1
  61. package/src/execution/__tests__/subagent-service-notify-gate.test.ts +27 -11
  62. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +17 -11
  63. package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +130 -53
  64. package/src/execution/__tests__/subagent-service.test.ts +17 -215
  65. package/src/execution/__tests__/subprocess-agent-runner-no-progress-killall.test.ts +138 -0
  66. package/src/execution/__tests__/subprocess-agent-runner.test.ts +105 -660
  67. package/src/execution/__tests__/sync-collect-recovery.test.ts +23 -16
  68. package/src/execution/__tests__/terminal-write-latency.test.ts +79 -0
  69. package/src/execution/__tests__/workflow-agent-dispatch.test.ts +782 -0
  70. package/src/execution/alive-store.ts +37 -28
  71. package/src/execution/{cold-resurrect.ts → cold-lookup.ts} +48 -45
  72. package/src/execution/config.ts +1 -1
  73. package/src/execution/conversation-continuation.ts +541 -0
  74. package/src/execution/dialog-queue.ts +60 -12
  75. package/src/execution/engine/__tests__/common/capability-gate.test.ts +1 -1
  76. package/src/execution/engine/__tests__/common/journal-wiring.test.ts +16 -1
  77. package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/fake-engine-protocol.mjs +17 -51
  78. package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/smoke-run.fixture.json +0 -5
  79. package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +1 -1
  80. package/src/execution/engine/__tests__/conformance/protocol-blackbox.test.ts +5 -9
  81. package/src/execution/engine/__tests__/conformance/registry-fork-filter.test.ts +55 -53
  82. package/src/execution/engine/__tests__/engine-discovery-scan.test.ts +0 -3
  83. package/src/execution/engine/__tests__/engine-discovery.test.ts +0 -3
  84. package/src/execution/engine/__tests__/inflight-snapshot.test.ts +180 -0
  85. package/src/execution/engine/__tests__/model-prompt.test.ts +0 -3
  86. package/src/execution/engine/__tests__/registry.test.ts +0 -1
  87. package/src/execution/engine/__tests__/routing.test.ts +0 -1
  88. package/src/execution/engine/__tests__/run-failure-worktree-cleanup.test.ts +0 -3
  89. package/src/execution/engine/client/__tests__/__fixtures__/fake-engine.mjs +0 -3
  90. package/src/execution/engine/client/__tests__/engine-client-reap.test.ts +212 -0
  91. package/src/execution/engine/client/__tests__/mirror.test.ts +73 -1
  92. package/src/execution/engine/client/__tests__/protocol-closure.test.ts +2 -9
  93. package/src/execution/engine/client/__tests__/remote-engine.test.ts +46 -12
  94. package/src/execution/engine/client/engine-client.ts +111 -26
  95. package/src/execution/engine/client/mirror.ts +9 -0
  96. package/src/execution/engine/client/remote-engine.ts +52 -34
  97. package/src/execution/engine/client/reverse-router.ts +6 -27
  98. package/src/execution/engine/common/__tests__/run-signals.test.ts +79 -0
  99. package/src/execution/engine/common/capability-gate.ts +27 -0
  100. package/src/execution/engine/common/data-dir.ts +3 -2
  101. package/src/execution/engine/common/errors.ts +1 -1
  102. package/src/execution/engine/common/journal-wiring.ts +18 -9
  103. package/src/execution/engine/common/run-signals.ts +83 -0
  104. package/src/execution/engine/d8-compat.ts +1 -1
  105. package/src/execution/engine/host/__tests__/host-bridge.test.ts +3 -18
  106. package/src/execution/engine/host/host-bridge.ts +26 -41
  107. package/src/execution/engine/host/pi-host-binding.ts +5 -5
  108. package/src/execution/engine/host/spawned-children.ts +1 -1
  109. package/src/execution/engine/inflight-snapshot.ts +92 -0
  110. package/src/execution/engine/port.ts +52 -54
  111. package/src/execution/engine/types.ts +11 -14
  112. package/src/execution/execution-record.ts +15 -47
  113. package/src/execution/finalize-record.ts +94 -197
  114. package/src/execution/idle-gc.ts +9 -3
  115. package/src/execution/lifecycle-manager.ts +25 -361
  116. package/src/execution/lifecycle-predicates.ts +22 -19
  117. package/src/execution/manifest-store.ts +40 -42
  118. package/src/execution/model-config-service.ts +1 -1
  119. package/src/execution/notifier.ts +39 -1
  120. package/src/execution/notify-host.ts +13 -4
  121. package/src/execution/path-encoding.ts +6 -0
  122. package/src/execution/record-entry.ts +20 -2
  123. package/src/execution/record-store.ts +925 -160
  124. package/src/execution/relay-env.ts +8 -3
  125. package/src/execution/round-supervisor/domain.ts +17 -5
  126. package/src/execution/round-supervisor/service-binding.test.ts +89 -53
  127. package/src/execution/round-supervisor/service-binding.ts +71 -21
  128. package/src/execution/round-supervisor/supervisor.ts +23 -9
  129. package/src/execution/service/record-access.ts +547 -0
  130. package/src/execution/service/record-lifecycle.ts +542 -0
  131. package/src/execution/service/run-orchestration.ts +1647 -0
  132. package/src/execution/service/service-bootstrap.ts +111 -0
  133. package/src/execution/service/service-constants.ts +27 -0
  134. package/src/execution/service/session-baselines.ts +401 -0
  135. package/src/execution/service/sync-collect-domain.ts +442 -0
  136. package/src/execution/service/workflow-dispatch.ts +567 -0
  137. package/src/execution/session-file-gc.ts +11 -8
  138. package/src/execution/session-reconstructor.ts +44 -1
  139. package/src/execution/sessions-index.ts +20 -3
  140. package/src/execution/settled-watchdog.ts +86 -43
  141. package/src/execution/state-marker.ts +516 -0
  142. package/src/execution/stream-sink.ts +1 -1
  143. package/src/execution/subagent-actions-core.ts +53 -31
  144. package/src/execution/subagent-service.ts +643 -2955
  145. package/src/execution/subprocess-agent-runner.ts +46 -323
  146. package/src/execution/types.ts +60 -43
  147. package/src/execution/ui-request-handler-factory.ts +5 -0
  148. package/src/execution/worktree-manager.ts +1 -1
  149. package/src/execution/worktree-registry.ts +12 -8
  150. package/src/index.ts +16 -5
  151. package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +2 -3
  152. package/src/orchestration/__tests__/agent-call-stream.test.ts +24 -49
  153. package/src/orchestration/__tests__/error-recovery-terminal-hardening.test.ts +4 -4
  154. package/src/orchestration/__tests__/file-run-store.test.ts +5 -7
  155. package/src/orchestration/__tests__/lifecycle-abort-broadcast-signal.test.ts +2 -3
  156. package/src/orchestration/__tests__/run-snapshot.test.ts +27 -43
  157. package/src/orchestration/__tests__/worker-message-pump-workflow-dispatch.test.ts +274 -0
  158. package/src/orchestration/execute-agent-call.ts +1 -1
  159. package/src/orchestration/file-run-store.ts +1 -1
  160. package/src/orchestration/models/ports.ts +20 -13
  161. package/src/orchestration/models/types.ts +11 -13
  162. package/src/orchestration/run-snapshot.ts +18 -22
  163. package/src/orchestration/worker-message-pump.ts +33 -57
  164. package/src/shared/atomic-write.ts +10 -8
  165. package/dist/chunk-T2SYBW3I.js +0 -24
  166. package/dist/engine-discovery-scan-ocpM8FmI.d.cts +0 -1608
  167. package/dist/engine-discovery-scan-ocpM8FmI.d.ts +0 -1608
  168. package/src/execution/__tests__/chat-round-first-round-watchdog.test.ts +0 -255
  169. package/src/execution/__tests__/finalized-marker.test.ts +0 -104
  170. package/src/execution/__tests__/lifecycle-manager-lock.test.ts +0 -211
  171. package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +0 -488
  172. package/src/execution/__tests__/subprocess-agent-runner-timeout.test.ts +0 -75
  173. package/src/execution/__tests__/tombstone-store.test.ts +0 -73
  174. package/src/execution/engine/__tests__/conformance/chat-round-protocol.test.ts +0 -230
  175. package/src/execution/finalized-marker.ts +0 -70
  176. package/src/execution/tombstone-store.ts +0 -72
@@ -1,20 +1,18 @@
1
1
  // src/execution/__tests__/subagent-service-recovery-bounds.test.ts
2
2
  //
3
3
  // [u-svc / T2] 服务侧回收上界与 kill 收敛:
4
- // - T2④/LC-2:closeChatIdle / closeAfterRoundSettled / cancelBackground 三条终止路径
5
- // 收敛到 killRecordChildWithEscalation(spy 断言调用点与参数)+ [W3] 引擎侧终止
6
- // 意图(协议 interact cancel/close fire-and-forget);
4
+ // - T2④/LC-2:closeChatIdle / cancelBackground 终止路径收敛到
5
+ // killRecordChildWithEscalation(spy 断言调用点与参数);
7
6
  // - T2⑥/PS-1:disposeAllRecords 补三回收面(controller.abort + kill + disarm idle timer
8
7
  // + disarm settled watchdog);
9
- // - T2③/LC-1 + D9:热路径投递 armMidRoundNoProgress(挂载点在编排层
10
- // deliverChatMessage interact 受理点;中段无进展检测 + 到期处置:kill + 失败
11
- // 终态化 + error 含 'settled watchdog' 标记与恢复指引)。
8
+ // - T2③/LC-1 + D9:Continuation armMidRoundNoProgress(挂载点在 kickOffChatRound
9
+ // 轮开跑;中段无进展检测 + 到期处置经 Continuation.onWatchdogFire run 收敛失败
10
+ // 分支统一收口,error 含 'settled watchdog' 标记与恢复指引)。
12
11
  //
13
- // [W3 改写] 替身从 inproc session-runner/stdin-writer mock 改为协议 seam
14
- //(registerFakePiEngine):投递 = engine.interact(message) 受理;终止意图 =
15
- // engine.interact(cancel/close)。T2⑧(非 EPIPE 写失败 re-arm idle timer)随
16
- // inproc pi 引擎目录 删除归 pi-subagent-cli 包内(写失败语义在引擎进程内,core 已无
17
- // 可泄漏的本地方案进程),core 侧不再可等价改写。
12
+ // [W3 改写 → H1 U6] 替身 = 协议 seam(registerFakePiEngine)。[H1 U6] interact 断言
13
+ //(cancel/close 终止意图受理)随 interact 面退役删除:真实杀链 = 轮级 abort signal →
14
+ // 协议 cancel 帧(引擎侧杀链)+ 镜像置死记账 + 引擎退出链收割(红线①);
15
+ // closeAfterRoundSettled 用例随载体退役删除(T2⑧ 段同前批登记归 pi 包)。
18
16
  //
19
17
  // mock 形态:真实 SubagentService + ServiceInternals cast 暴露 store + logger +
20
18
  // spawned-children 的 kill 入口换 spy(importOriginal 保留镜像真实现)。
@@ -47,7 +45,9 @@ import type { PiLike } from "../subagent-service.ts";
47
45
  import { armSettledWatchdog, hasSettledWatchdog, SETTLED_MID_ROUND_NO_PROGRESS_MS, _resetSettledWatchdogsForTest } from "../settled-watchdog.ts";
48
46
  import { armIdleTimer, hasIdleTimer, _resetLifecycleState } from "../lifecycle-manager.ts";
49
47
  import { _resetCoreSpawnedChildrenMirrorForTest } from "../engine/host/spawned-children.ts";
50
- import type { ExecutionRecord } from "../types.ts";
48
+ // AgentResult = execution 层那份(execution/types.ts:259,主字段 text/turns/sessionId),
49
+ // 与 acquirePoolOrFinalize 签名一致;engine/types.ts 只有消歧注释无此导出。
50
+ import type { AgentResult, ExecutionRecord } from "../types.ts";
51
51
 
52
52
  function makeTmpAgentDir(): string {
53
53
  return fs.mkdtempSync(path.join(os.tmpdir(), "svc-recovery-bounds-"));
@@ -102,23 +102,21 @@ function privateFn<K extends string>(service: SubagentService, key: K): (...args
102
102
  }
103
103
 
104
104
  /**
105
- * message 投递入口([W3] 协议形态:编排层私有 deliverChatMessage → engine.interact(message);
106
- * 冷路径 = engine_session_not_resumable → run chat + resume)。
105
+ * message 投递入口([H1 U6] Continuation 编排:deliverChatMessage → onMessage →
106
+ * run + resume 锚点)。
107
107
  */
108
108
  async function deliverChat(
109
109
  service: SubagentService,
110
110
  record: ExecutionRecord,
111
111
  text: string,
112
- interrupt: boolean,
113
112
  ): Promise<void> {
114
113
  return (
115
114
  privateFn(service, "deliverChatMessage") as (
116
115
  this: SubagentService,
117
116
  r: ExecutionRecord,
118
117
  t: string,
119
- i: boolean,
120
118
  ) => Promise<void>
121
- ).call(service, record, text, interrupt);
119
+ ).call(service, record, text);
122
120
  }
123
121
 
124
122
  describe("T2④ service-side kill convergence", () => {
@@ -149,10 +147,8 @@ describe("T2④ service-side kill convergence", () => {
149
147
  const ok = service.cancel(record.id);
150
148
  expect(ok).toBe(true);
151
149
  expect(killChildSpy).toHaveBeenCalledWith(record.id, "cancelBackground");
152
- // [W3] 实际终止在引擎进程内:协议 interact cancel fire-and-forget
153
- await vi.waitFor(() => {
154
- expect(fake.interacts.some((c) => c.action.kind === "cancel")).toBe(true);
155
- });
150
+ // [H1 U6] 在途 run 的实际终止经 controller.abort → 轮级 signal(cancel 帧 + 杀链);
151
+ // interact cancel 受理断言随 interact 面退役。
156
152
  });
157
153
 
158
154
  it("closeChatIdle (via closeSubagent force:false on idle) routes through killRecordChildWithEscalation and disarms timers", async () => {
@@ -164,19 +160,8 @@ describe("T2④ service-side kill convergence", () => {
164
160
  expect(killChildSpy).toHaveBeenCalledWith(record.id, "closeChatIdle");
165
161
  expect(hasIdleTimer(record.id)).toBe(false);
166
162
  expect(hasSettledWatchdog(record.id)).toBe(false);
167
- // [W3] 引擎侧 close force(idle 进程回收)
168
- await vi.waitFor(() => {
169
- const close = fake.interacts.find((c) => c.action.kind === "close");
170
- expect(close?.action).toMatchObject({ kind: "close", payload: { force: true } });
171
- });
172
- });
173
-
174
- it("closeAfterRoundSettled routes through killRecordChildWithEscalation", async () => {
175
- const record = makeRecord({ id: "sa-close-round" });
176
- await (
177
- privateFn(service, "closeAfterRoundSettled") as (this: SubagentService, r: ExecutionRecord) => Promise<void>
178
- ).call(service, record);
179
- expect(killChildSpy).toHaveBeenCalledWith(record.id, "closeAfterRoundSettled");
163
+ // [H1 U6] 旧引擎侧 close force 受理断言随 interact 面退役(无在跑轮无需进程回收,
164
+ // Path A 保活进程由镜像记账 + reaper 兜底回收)。
180
165
  });
181
166
 
182
167
  it("disposeAllRecords applies all recovery surfaces: abort + escalation kill + disarm idle/settled timers", () => {
@@ -230,42 +215,134 @@ describe("T2③ hot-path settled watchdog", () => {
230
215
  });
231
216
 
232
217
  it("arms the settled watchdog after a successful hot-path prompt", async () => {
233
- const record = makeRecord({ id: "sa-hot-arm" });
218
+ // [H1 U2] deliverChatMessage = Continuation 派发(续聊轮需 sessionFile 锚点)
219
+ const record = makeRecord({ id: "sa-hot-arm", sessionFile: path.join(agentDir, "sa-hot-arm.jsonl") });
220
+ fs.writeFileSync(record.sessionFile!, "{}\n", "utf-8");
234
221
  store.register(record);
235
- await deliverChat(service, record, "hello", false);
236
- expect(fake.interacts.length).toBe(1);
222
+ await deliverChat(service, record, "hello");
223
+ await vi.waitFor(() => expect(fake.runs.length).toBe(1));
237
224
  expect(hasSettledWatchdog(record.id)).toBe(true);
238
225
  });
239
226
 
240
- it("onMidTimeout: kills the child (engine cancel), fails the round, error carries 'settled watchdog' marker and recovery hint", async () => {
227
+ it("onMidTimeout: kills the child (engine cancel + round abort), fails the round via run settlement, error carries 'settled watchdog' marker and recovery hint", async () => {
241
228
  // fake timers 必须先于 arm 生效(useFakeTimers 不接管已存在的真实 timer)
242
229
  vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] });
243
- const record = makeRecord({ id: "sa-hot-timeout" });
230
+ // [H1 U2] Continuation 轮需要 sessionFile 锚点
231
+ const record = makeRecord({ id: "sa-hot-timeout", sessionFile: path.join(agentDir, "sa-hot-timeout.jsonl") });
232
+ fs.writeFileSync(record.sessionFile!, "{}\n", "utf-8");
244
233
  store.register(record);
245
- await deliverChat(service, record, "hello", false);
234
+ await deliverChat(service, record, "hello");
235
+ await vi.waitFor(() => expect(fake.runs.length).toBe(1));
246
236
  expect(hasSettledWatchdog(record.id)).toBe(true);
247
237
 
248
- // [D9 两段式] 热路径 prompt 发出后挂中段:静默满中段窗长触发(本测试替身不驱动
249
- // 协议事件,中段静默形态直达)
238
+ // [D9 两段式] 轮开跑后挂中段:静默满中段窗长触发(本测试替身不驱动协议事件,
239
+ // 中段静默形态直达)
250
240
  await vi.advanceTimersByTimeAsync(SETTLED_MID_ROUND_NO_PROGRESS_MS + 1);
251
241
 
252
- // kill 收敛入口被触发(onTimeout 第一步)
253
- expect(killChildSpy).toHaveBeenCalledWith(record.id, "settled watchdog (hot path)");
242
+ // kill 收敛入口被触发([H1 U2] Continuation watchdog fire → killRoundChildForWatchdog,
243
+ // source phase 名——旧 onHotPathSettledWatchdogTimeout 的 "(hot path)" 标记退役)
244
+ expect(killChildSpy).toHaveBeenCalledWith(record.id, "settled watchdog (mid-round)");
254
245
  // watchdog 到期自清(armedTimers 先删条目再执行回调)
255
246
  expect(hasSettledWatchdog(record.id)).toBe(false);
256
- // 该轮失败终态化(chatModeMF-6 回退 resumable)+ 失败通知
257
- expect(record.resumable).toBe(true);
247
+ // [H1 U2] fire abort 轮 signal 在途 run 收敛(替身模拟 abort 合成失败终态)→
248
+ // onRunSettled 失败分支簿记(chatMode → MF-6 回退 resumable)+ 失败通知
249
+ fake.runs[0]!.settle({
250
+ content: "",
251
+ error: "engine_run_failed: run aborted (settled watchdog mid-round no-progress); the process was terminated to bound the wait. Recovery: check state with subagents action:'list', then re-send your message to continue.",
252
+ exitCode: null,
253
+ });
254
+ await vi.waitFor(() => expect(record.resumable).toBe(true));
258
255
  expect(record.status).toBe("running");
259
- expect(record.result).toContain("settled watchdog");
260
- expect(record.result).toContain("Recovery");
261
- expect(record.result).toContain("action:'list'");
262
- // 失败通知送达(内核直发路径)
256
+ // [H1 U2 / D7] 失败轮 lastError 写失败原因(result = 前值 ?? 失败摘要——失败摘要
257
+ // 由 Continuation 失败通知独立承载)
258
+ expect(record.lastError).toContain("settled watchdog");
259
+ expect(record.lastError).toContain("Recovery");
260
+ expect(record.lastError).toContain("action:'list'");
261
+ // 失败通知送达(Continuation 独立载荷——正文带失败摘要与恢复指引)
263
262
  expect(pi.sendMessage).toHaveBeenCalled();
264
263
  const sendMessageCalls = (pi.sendMessage as unknown as ReturnType<typeof vi.fn>).mock.calls as Array<[{ content?: string }]>;
265
264
  const notifyContent = sendMessageCalls[0]?.[0]?.content ?? "";
266
265
  expect(notifyContent).toContain("settled watchdog");
267
- // [W3] 引擎侧终止意图(fire-and-forget;fake timers 下只断言受理动作已发出)
268
- const cancel = fake.interacts.find((c) => c.action.kind === "cancel");
269
- expect(cancel).toBeDefined();
266
+ // [H1 U6] 引擎侧终止意图(watchdog fire cancel 受理断言)随 interact 面退役——
267
+ // fire 的真实 kill = killRoundChildForWatchdog(镜像置死)+ abort signal cancel 帧。
268
+ });
269
+ });
270
+
271
+ // ── [S11] 残余守卫分支:cancel tombstone 幂等 / 池排队 abort / record-access 越界 ──
272
+
273
+ describe("S11 守卫分支组", () => {
274
+ let agentDir: string;
275
+ let service: SubagentService;
276
+ let store: RecordStore;
277
+ let pi: MockPi;
278
+ let fake: FakePiEnginePort;
279
+
280
+ beforeEach(() => {
281
+ vi.restoreAllMocks();
282
+ killChildSpy.mockClear();
283
+ ({ agentDir, service, store, pi, fake } = setup());
284
+ });
285
+
286
+ afterEach(() => {
287
+ service.dispose();
288
+ clearEngines();
289
+ _resetLifecycleState();
290
+ _resetSettledWatchdogsForTest();
291
+ _resetCoreSpawnedChildrenMirrorForTest();
292
+ fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
293
+ });
294
+
295
+ it("cancel tombstone 幂等:已终态 record 再 cancel → CAS 未抢到返回 false(stop 手段仍执行,零收尾副作用)", async () => {
296
+ const record = makeRecord({ id: "sa-tombstone" });
297
+ record.status = "closed";
298
+ record.closedReason = "cancelled";
299
+ store.register(record);
300
+ const emitSpy = vi.fn();
301
+ store.onChange(emitSpy);
302
+
303
+ const ok = service.cancel(record.id);
304
+
305
+ expect(ok).toBe(false); // CAS 未抢到:覆写终态 + tombstone 双标 + notify 双发不可执行
306
+ expect(record.status).toBe("closed"); // 终态不被覆写
307
+ expect(record.closedReason).toBe("cancelled");
308
+ // stop 手段无条件先执行(cancel 语义 = 进程必死,幂等无害)
309
+ expect(killChildSpy).toHaveBeenCalledWith(record.id, "cancelBackground");
310
+ // 收尾副作用归 CAS 赢家:无 markCancelled 落盘 entry(state-write 面)
311
+ const stateWrites = (pi.appendEntry as unknown as ReturnType<typeof vi.fn>).mock.calls.filter(
312
+ (c: unknown[]) => String(c[0]).includes("state-write-failed"),
313
+ );
314
+ expect(stateWrites).toHaveLength(0);
315
+ });
316
+
317
+ it("排队被 abort 池分支:池满排队 + signal 已 aborted → finalizeAborted(cancelled 终态)", async () => {
318
+ const record = makeRecord({ id: "sa-pool-abort" });
319
+ store.register(record);
320
+ const ro = privateFn(service, "runOrchestration") as unknown as {
321
+ acquirePoolOrFinalize: (r: ExecutionRecord, s: AbortSignal | undefined, p: number) => Promise<AgentResult | undefined>;
322
+ };
323
+ // 先占满池(占满 effective = max(1, maxConcurrent - depth) 个槽位),迫使目标 record 排队
324
+ const pool = privateFn(service, "pool") as unknown as {
325
+ maxConcurrent: number;
326
+ acquire: (p: number, max?: number, s?: AbortSignal) => Promise<void>;
327
+ release: () => void;
328
+ };
329
+ const slots = Math.max(1, pool.maxConcurrent - record.depth);
330
+ for (let i = 0; i < slots; i++) await pool.acquire(0);
331
+ const controller = new AbortController();
332
+ controller.abort(); // 排队窗内 abort(pre-aborted signal → acquire reject AbortError)
333
+
334
+ const result = await ro.acquirePoolOrFinalize(record, controller.signal, 0);
335
+ for (let i = 0; i < slots; i++) pool.release();
336
+
337
+ // S1:排队中被 abort 走 cancelled(与已运行被 abort 一致),返回终态 result 供 early-return
338
+ expect(result).toBeDefined();
339
+ expect(record.status).toBe("closed");
340
+ expect(record.closedReason).toBe("cancelled");
341
+ });
342
+
343
+ it("record-access 越界守卫:disposed 后 lookupRecordAnyState → undefined(不抛)", async () => {
344
+ service.dispose();
345
+ const record = service.queries.lookupRecordAnyState("any-id");
346
+ expect(record).toBeUndefined();
270
347
  });
271
348
  });
@@ -1,6 +1,9 @@
1
1
  // src/__tests__/subagent-service.test.ts
2
2
  //
3
- // SubagentService 生命周期 + 公共 API 边界测试。
3
+ // SubagentService 生命周期 + 公共 API 边界测试([H3/R5 测试归位] 本文件为壳级测试:
4
+ // 装配 / dispose 编排 / 转发面 / 跨域 emit 记账。run 域 execute 入口用例已迁
5
+ // run-orchestration.test.ts;ModelConfigService ctxModel 缓存已迁
6
+ // model-config-service.test.ts)。
4
7
  //
5
8
  // 范围:initSession / dispose / query / cancel / listRunning / collectRecords /
6
9
  // onChange / assertReady -- 这些不依赖动态 import Pi SDK(getSdk)。
@@ -19,13 +22,14 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
19
22
 
20
23
  import { createRecord } from "../execution-record.ts";
21
24
  import { ModelConfigService } from "../model-config-service.ts";
22
- import type { ModelInfo, ModelRegistryLike } from "../model-resolver.ts";
25
+ import type { ModelInfo } from "../model-resolver.ts";
23
26
  import type { RecordStore } from "../record-store.ts";
24
27
  import type { UiRequest, UiRequestHandler } from "../dialog-queue.ts";
25
28
  import type { PiLike } from "../subagent-service.ts";
26
- import { getSubagentService, setSubagentService,SubagentService } from "../subagent-service.ts";
29
+ import { SubagentService } from "../subagent-service.ts";
30
+ // [H3/R6] 单例访问器外移支撑文件 service/service-bootstrap.ts(壳不再导出)。
31
+ import { getSubagentService, setSubagentService } from "../service/service-bootstrap.ts";
27
32
  import type { ExecutionRecord } from "../types.ts";
28
- import type { WorktreeManager } from "../worktree-manager.ts";
29
33
 
30
34
  // ── 工具:建临时 agentDir + 真实 ModelConfigService ──
31
35
 
@@ -331,9 +335,11 @@ describe("SubagentService", () => {
331
335
  // private 字段,用 Reflect.get 取(与 R0-D 块访问 store 同模式)。
332
336
 
333
337
  describe("dispose uiRequestHandler stub (dispose-cleanup)", () => {
334
- /** 从 service 取出 private uiRequestHandler(trailing ui_request 实际调用入口)。 */
338
+ /** 从 service 取出 private uiRequestHandler(trailing ui_request 实际调用入口)。
339
+ * [R1 深绑改写] uiRequestHandler 随域 #2 聚合迁入 SessionBaselines,深绑路径改为
340
+ * service → baselines 聚合实例(断言对象与强度不变,路径对齐终态结构)。 */
335
341
  function getHandler(service: SubagentService): UiRequestHandler | undefined {
336
- return Reflect.get(service, "uiRequestHandler") as UiRequestHandler | undefined;
342
+ return (Reflect.get(service, "baselines") as { uiRequestHandler: UiRequestHandler | undefined }).uiRequestHandler;
337
343
  }
338
344
 
339
345
  /** 最小 UiRequest(method 无关紧要——stub 不论 method 一律返回 cancelled)。 */
@@ -387,136 +393,11 @@ describe("SubagentService", () => {
387
393
  });
388
394
 
389
395
  // ============================================================
390
- // execute() worktree 路径(worktree 与 fork 解耦后)
396
+ // execute() worktree 路径(worktree 与 fork 解耦)
397
+ // → [H3/R5 测试归位] 整块迁至 run-orchestration.test.ts(run 域聚合同名域测试
398
+ // 文件;用例名与断言零改动)。此处不留转发桩——vitest 按文件发现,无注册面。
391
399
  // ============================================================
392
- //
393
- // worktree(文件隔离)与 fork(上下文继承)已解耦:worktree:true 可独立于 fork 工作
394
- // (worktreeManager.create 只看 opts.worktree,不读 fork)。此组验证三种 fork/worktree
395
- // 组合下 worktree 路径的行为(均不应抛 'requires fork'——该 guard 已移除):
396
- // 1. worktree:true + fork:false → 解耦后正常(创建 worktree 路径,不抛 requires fork)
397
- // 2. worktree:true + fork:true → 创建 worktree 路径(测试环境 git 失败,抛非 requires fork 错)
398
- // 3. worktree:false + fork:false → 默认路径(不创建 worktree)
399
- //
400
- // 本文件不 mock spawn(保持与文件头声明一致——execute 集成测试在 execute-nesting /
401
- // run-spawn-integration),因此 case 验证「不抛 requires fork」而非「执行完成」:
402
- // 执行越过 worktree 创建后在后续步骤(worktreeManager.create 调 git / runSpawn 调
403
- // spawn)抛与 fork/worktree 无关的错。用 try/catch 断言抛出的不是 requires fork。
404
-
405
- describe("execute() worktree 路径(worktree 与 fork 解耦)", () => {
406
- /** 构造已就绪的 service(initSession + initModel 注入 ctxModel,使 resolveIdentity 不因 model 拗错)。 */
407
- function makeReadyService(): SubagentService {
408
- const service = new SubagentService({ cwd: agentDir, modelService });
409
- service.initSession({ pi: makePi(), sessionId: "s1" });
410
- // 注入 modelRegistry + ctxModel:让 resolveIdentity 越过 resolveModel,
411
- // 使 guard 之后的失败点稳定在 worktreeManager.create(git)或 runSpawn(spawn),
412
- // 而非 modelService.resolveModel——避免与 guard 无关的 model 错误掩盖被测点。
413
- modelService.initModel({
414
- modelRegistry: {
415
- getAvailable: () => [],
416
- find: () => undefined,
417
- hasConfiguredAuth: () => false,
418
- },
419
- sessionId: "s1",
420
- ctxModel: { id: "ctx-model", name: "Ctx", provider: "p", reasoning: false },
421
- });
422
- return service;
423
- }
424
-
425
- it("worktree:true + fork:false → 解耦后不抛 'requires fork'(worktree 独立于 fork)", async () => {
426
- const service = makeReadyService();
427
- // 解耦后 worktree:true+fork:false 不再 throw requires fork(worktreeManager.create 只看 worktree)
428
- try {
429
- await service.execute({
430
- task: "worktree without fork (decoupled)",
431
- slug: "test",
432
- worktree: true,
433
- fork: false,
434
- ctxModel: { id: "ctx-model", name: "Ctx", provider: "p", reasoning: false },
435
- });
436
- } catch (err) {
437
- // 解耦后绝不抛 requires fork(执行继续到 worktreeManager.create/spawn 才可能抛其他错)
438
- expect((err as Error).message).not.toMatch(/requires fork/);
439
- }
440
- });
441
-
442
- it("worktree:true + fork:true → 创建 worktree 路径(不抛 'requires fork')", async () => {
443
- const service = makeReadyService();
444
- // 执行继续:先创建 record,然后 worktreeManager.create 调 git(测试环境无 repo → 抛与 fork 无关的错)
445
- try {
446
- await service.execute({
447
- task: "worktree with fork",
448
- slug: "test",
449
- worktree: true,
450
- fork: true,
451
- ctxModel: { id: "ctx-model", name: "Ctx", provider: "p", reasoning: false },
452
- });
453
- // 若未抛(理论上 worktreeManager.create 在某些环境成功)也 OK——重点是没命中 guard
454
- } catch (err) {
455
- // guard 放行:抛出的错误绝不能是 "requires fork"
456
- expect((err as Error).message).not.toMatch(/requires fork/);
457
- }
458
- });
459
-
460
- it("worktree:false + fork:false → 默认路径(不创建 worktree,不抛 'requires fork')", async () => {
461
- const service = makeReadyService();
462
- // 默认路径:runSpawn 调 child_process.spawn(测试环境无真实 pi → 抛与 fork 无关的错)
463
- try {
464
- await service.execute({
465
- task: "default path",
466
- slug: "test",
467
- worktree: false,
468
- fork: false,
469
- ctxModel: { id: "ctx-model", name: "Ctx", provider: "p", reasoning: false },
470
- });
471
- } catch (err) {
472
- // guard 放行:抛出的错误绝不能是 "requires fork"
473
- expect((err as Error).message).not.toMatch(/requires fork/);
474
- }
475
- });
476
400
 
477
- // ============================================================
478
- // create-await 竞态守卫(Phase 2):create 的 await 窗口内 dispose/cancel
479
- // 可把 record CAS 成 closed——守卫须主动 cleanup + early-failed 返回,不 kickOff。
480
- // 实现约束固化:「赋值 record.worktreeHandle → 终态检查 → 轮次 kick-off」
481
- // 必须同一同步段(中间禁止 await),本用例即该不变量的回归锚点。
482
- // ============================================================
483
- it("守卫:create await 窗口内 dispose 抢先 → cleanup 被调 + early-failed 返回(不 kickOff)", async () => {
484
- const service = makeReadyService();
485
- const wtm = Reflect.get(service, "worktreeManager") as WorktreeManager;
486
- const handle = Object.freeze({
487
- path: "/tmp/wt-guard",
488
- branch: "pi-sub-guard",
489
- baseCommit: "abc123",
490
- mainCwd: "/repo",
491
- }) as Parameters<WorktreeManager["cleanup"]>[0];
492
- let resolveCreate!: (h: unknown) => void;
493
- vi.spyOn(wtm, "create").mockImplementation(
494
- () => new Promise<unknown>((r) => { resolveCreate = r; }) as ReturnType<WorktreeManager["create"]>,
495
- );
496
- const cleanupSpy = vi.spyOn(wtm, "cleanup").mockResolvedValue(undefined);
497
-
498
- const execP = service.execute({
499
- task: "guard test",
500
- slug: "test",
501
- worktree: true,
502
- fork: false,
503
- ctxModel: { id: "ctx-model", name: "Ctx", provider: "p", reasoning: false },
504
- });
505
- // 微任务推进:record 已建 + execute 挂在 pending create 上
506
- await new Promise((r) => { setTimeout(r, 0); });
507
- // dispose 抢先(无需 record id):CAS running → closed,此时 worktreeHandle 仍
508
- // undefined(dispose 的 fire-and-forget cleanup 跳过)——守卫是唯一的清理点
509
- service.disposeAllRecords("parent-shutdown");
510
- resolveCreate(handle);
511
-
512
- const ret = await execP;
513
- // 守卫生效:handle 被主动清理(不等 60s reaper)
514
- expect(cleanupSpy).toHaveBeenCalledWith(handle);
515
- // 返回 early-failed 形态(details.status 已 closed),而非 kickOff 的 running 形态
516
- expect(ret.mode).toBe("background");
517
- expect(ret.details).toMatchObject({ status: "closed" });
518
- });
519
- });
520
401
  //
521
402
  // [背景] PR #82 在 subagent-service.ts 新增 emitPendingRegister/Unregister 调用,
522
403
  // 5 个 emit 点:register(execute L309)、unregister(failed)(finalizeFailed 经
@@ -698,88 +579,9 @@ describe("SubagentService", () => {
698
579
 
699
580
  // ============================================================
700
581
  // ModelConfigService ctxModel 缓存(renderCall 标题行 model 显示的核心)
582
+ // → [H3/R5 测试归位] 整块迁至 model-config-service.test.ts(被测主体 =
583
+ // ModelConfigService 模块,非 SubagentService;用例名与断言零改动)。
701
584
  // ============================================================
702
- //
703
- // [HISTORICAL] 99f20da1e 后 renderCall 拿不到主 agent model(ToolRenderContext 无 model),
704
- // resolveModel 第三层拗错→降级不显示 model。修复:session_start 缓存 ctxModel,
705
- // resolveModel 第三参默认用缓存。此测试验证该透传链路。
706
-
707
- describe("ModelConfigService ctxModel 缓存", () => {
708
- let agentDir: string;
709
-
710
- beforeEach(() => {
711
- agentDir = makeTmpAgentDir();
712
- });
713
- afterEach(() => {
714
- fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
715
- });
716
-
717
- /** 最小 mock registry:空可用列表(ctxModel 路径不需要 lookup)。 */
718
- function makeEmptyRegistry(): ModelRegistryLike {
719
- return {
720
- getAvailable: () => [],
721
- find: () => undefined,
722
- hasConfiguredAuth: () => false,
723
- };
724
- }
725
-
726
- it("initModel 传 ctxModel 后,resolveModel 不传第三参返回缓存 model", () => {
727
- const svc = makeModelService(agentDir);
728
- const mainModel: ModelInfo = {
729
- id: "main-model",
730
- name: "Main",
731
- provider: "anthropic",
732
- reasoning: false,
733
- };
734
- svc.initModel({
735
- modelRegistry: makeEmptyRegistry(),
736
- sessionId: "sess-1",
737
- ctxModel: mainModel,
738
- });
739
-
740
- // agent 无 model 声明 + 无 override → 走第三层 ctxModel 缓存
741
- const r = svc.resolveModel("general-purpose");
742
- expect(r.model).toBe(mainModel);
743
- expect(r.model.provider).toBe("anthropic");
744
- });
745
-
746
- it("显式 ctxModel 参数优先于缓存(execute 路径覆盖 renderCall 缓存)", () => {
747
- const svc = makeModelService(agentDir);
748
- const cached: ModelInfo = { id: "cached", name: "C", provider: "p1", reasoning: false };
749
- const explicit: ModelInfo = { id: "explicit", name: "E", provider: "p2", reasoning: false };
750
- svc.initModel({
751
- modelRegistry: makeEmptyRegistry(),
752
- sessionId: "sess-1",
753
- ctxModel: cached,
754
- });
755
-
756
- // execute 传显式 ctxModel → 用显式,不用缓存
757
- const r = svc.resolveModel("general-purpose", undefined, explicit);
758
- expect(r.model).toBe(explicit);
759
- });
760
-
761
- it("setCtxModel 刷新缓存(model_select 后 renderCall 能看到新 model)", () => {
762
- const svc = makeModelService(agentDir);
763
- const m1: ModelInfo = { id: "m1", name: "1", provider: "p", reasoning: false };
764
- const m2: ModelInfo = { id: "m2", name: "2", provider: "p", reasoning: false };
765
- svc.initModel({ modelRegistry: makeEmptyRegistry(), sessionId: "s", ctxModel: m1 });
766
-
767
- expect(svc.resolveModel("general-purpose").model).toBe(m1);
768
- svc.setCtxModel(m2); // 模拟 model_select 刷新
769
- expect(svc.resolveModel("general-purpose").model).toBe(m2);
770
- });
771
-
772
- it("缓存为空且无 override/agentConfig.model → 拗错(不静默降级)", () => {
773
- const svc = makeModelService(agentDir);
774
- svc.initModel({
775
- modelRegistry: makeEmptyRegistry(),
776
- sessionId: "s",
777
- // ctxModel 不传 → 缓存为空
778
- });
779
- // 空 registry + 空 ctxModel → 第三层不可用 → 拗错
780
- expect(() => svc.resolveModel("general-purpose")).toThrow(/No available model/);
781
- });
782
- });
783
585
 
784
586
  // ============================================================
785
587
  // execute() 集成测试 — 已由 execute-integration.test.ts 覆盖