@zhushanwen/subagent-core 0.6.0 → 0.7.1

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 (278) hide show
  1. package/README.md +1 -1
  2. package/dist/chunk-KXVBIVOU.js +16 -0
  3. package/dist/chunk-T2SYBW3I.js +24 -0
  4. package/dist/chunk-VURUAGMM.js +2305 -0
  5. package/dist/{types-C3fE3R9x.d.cts → engine-discovery-scan-ocpM8FmI.d.cts} +770 -668
  6. package/dist/{types-C3fE3R9x.d.ts → engine-discovery-scan-ocpM8FmI.d.ts} +770 -668
  7. package/dist/execution/engine/engine-discovery-scan.cjs +2210 -0
  8. package/dist/execution/engine/engine-discovery-scan.d.cts +4 -0
  9. package/dist/execution/engine/engine-discovery-scan.d.ts +4 -0
  10. package/dist/execution/engine/engine-discovery-scan.js +20 -0
  11. package/dist/execution/engine/paths.cjs +6 -23
  12. package/dist/execution/engine/paths.d.cts +1 -8
  13. package/dist/execution/engine/paths.d.ts +1 -8
  14. package/dist/execution/engine/paths.js +1 -1
  15. package/dist/execution/relay-env.cjs +12 -28
  16. package/dist/execution/relay-env.d.cts +1 -33
  17. package/dist/execution/relay-env.d.ts +1 -33
  18. package/dist/execution/relay-env.js +5 -3
  19. package/dist/index.cjs +14045 -16059
  20. package/dist/index.d.cts +1035 -1579
  21. package/dist/index.d.ts +1035 -1579
  22. package/dist/index.js +9838 -13800
  23. package/dist.bundle/index.cjs +28875 -30323
  24. package/package.json +20 -26
  25. package/src/__tests__/record-store-last-line.test.ts +16 -7
  26. package/src/core/error-message.ts +7 -6
  27. package/src/core/host-services.ts +8 -2
  28. package/src/core/notify-ports.ts +17 -2
  29. package/src/execution/__tests__/chat-engine-routing.test.ts +62 -28
  30. package/src/execution/__tests__/chat-round-first-round-watchdog.test.ts +255 -0
  31. package/src/execution/__tests__/collect-coordinator-service.test.ts +36 -47
  32. package/src/execution/__tests__/collect-mixed-dispatch.test.ts +20 -53
  33. package/src/execution/__tests__/delivery-methods.test.ts +179 -237
  34. package/src/execution/__tests__/execute-and-await-worktree.test.ts +0 -2
  35. package/src/execution/__tests__/execute-nesting.test.ts +20 -13
  36. package/src/execution/__tests__/explicit-agent-ref-guard.test.ts +20 -33
  37. package/src/execution/__tests__/gc-timer.test.ts +7 -22
  38. package/src/execution/__tests__/get-record-for-action-restart.test.ts +20 -10
  39. package/src/execution/__tests__/helpers/fake-engine-port.ts +187 -0
  40. package/src/execution/__tests__/helpers/spawn-mock.ts +36 -9
  41. package/src/execution/__tests__/helpers/subagent-service-mocks.ts +1 -1
  42. package/src/execution/__tests__/idle-gc.test.ts +200 -0
  43. package/src/execution/__tests__/lifecycle-predicates.test.ts +26 -21
  44. package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +5 -2
  45. package/src/execution/__tests__/reconcile-sweep.test.ts +200 -0
  46. package/src/execution/__tests__/record-store.test.ts +60 -6
  47. package/src/execution/__tests__/recursive-visibility-baseline.test.ts +20 -13
  48. package/src/execution/__tests__/round-supervisor.test.ts +293 -0
  49. package/src/execution/__tests__/session-pending.test.ts +119 -17
  50. package/src/execution/__tests__/stream-sink-retirement.test.ts +10 -10
  51. package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +1 -11
  52. package/src/execution/__tests__/subagent-service-notify-gate.test.ts +33 -30
  53. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +2 -9
  54. package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +59 -129
  55. package/src/execution/__tests__/subagent-service.test.ts +2 -2
  56. package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +109 -7
  57. package/src/execution/__tests__/subprocess-agent-runner.test.ts +108 -36
  58. package/src/execution/__tests__/sync-collect-recovery.test.ts +21 -36
  59. package/src/execution/__tests__/workflow-state-root.test.ts +232 -0
  60. package/src/execution/agent-registry.ts +6 -2
  61. package/src/execution/dialog-queue.ts +6 -73
  62. package/src/execution/engine/__tests__/common/capability-gate.test.ts +115 -15
  63. package/src/execution/engine/__tests__/common/errors.test.ts +11 -18
  64. package/src/execution/engine/__tests__/common/kill-chain.test.ts +7 -0
  65. package/src/execution/engine/__tests__/common/nesting-guard.test.ts +9 -3
  66. package/src/execution/engine/__tests__/common/session-view-service.test.ts +90 -3
  67. package/src/execution/engine/__tests__/conformance/H9-test-disposition.md +116 -0
  68. package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/fake-engine-protocol.mjs +377 -0
  69. package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/recorded.fixture.json +331 -0
  70. package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/smoke-run.fixture.json +213 -0
  71. package/src/execution/engine/__tests__/conformance/chat-round-protocol.test.ts +230 -0
  72. package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +69 -42
  73. package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +4 -81
  74. package/src/execution/engine/__tests__/conformance/contract.probe.test.ts +67 -44
  75. package/src/execution/engine/__tests__/conformance/contract.read-degradation.test.ts +81 -54
  76. package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +133 -70
  77. package/src/execution/engine/__tests__/conformance/engine-crash.test.ts +143 -0
  78. package/src/execution/engine/__tests__/conformance/fake-engine-capabilities.ts +19 -0
  79. package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +7 -2
  80. package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +6 -5
  81. package/src/execution/engine/__tests__/conformance/protocol-blackbox.test.ts +398 -0
  82. package/src/execution/engine/__tests__/conformance/registry-fork-filter.test.ts +221 -0
  83. package/src/execution/engine/__tests__/conformance/round-liveness-supervisor.test.ts +302 -0
  84. package/src/execution/engine/__tests__/conformance/static-spawn-guard.test.ts +58 -0
  85. package/src/execution/engine/__tests__/engine-discovery-scan.test.ts +558 -0
  86. package/src/execution/engine/__tests__/host-task-spec.test.ts +45 -0
  87. package/src/execution/engine/__tests__/model-ref-split.test.ts +74 -0
  88. package/src/execution/engine/__tests__/registry.test.ts +206 -1
  89. package/src/execution/engine/__tests__/routing.test.ts +97 -0
  90. package/src/execution/engine/__tests__/run-failure-worktree-cleanup.test.ts +97 -0
  91. package/src/execution/engine/client/__tests__/__fixtures__/fake-engine.mjs +294 -0
  92. package/src/execution/engine/client/__tests__/engine-client.test.ts +516 -0
  93. package/src/execution/engine/client/__tests__/mirror.test.ts +67 -0
  94. package/src/execution/engine/client/__tests__/pid-file.test.ts +257 -0
  95. package/src/execution/engine/client/__tests__/protocol-closure.test.ts +95 -0
  96. package/src/execution/engine/client/__tests__/remote-engine.test.ts +457 -0
  97. package/src/execution/engine/client/client-options.ts +103 -0
  98. package/src/execution/engine/client/engine-client.ts +687 -0
  99. package/src/execution/engine/client/mirror.ts +167 -0
  100. package/src/execution/engine/client/pid-file.ts +392 -0
  101. package/src/execution/engine/client/reaper.ts +95 -0
  102. package/src/execution/engine/client/remote-engine.ts +441 -0
  103. package/src/execution/engine/client/reverse-router.ts +255 -0
  104. package/src/execution/engine/common/capability-gate.ts +109 -22
  105. package/src/execution/engine/common/errors.ts +9 -25
  106. package/src/execution/engine/common/event-journal.ts +4 -50
  107. package/src/execution/engine/common/journal-replay.ts +3 -1
  108. package/src/execution/engine/common/kill-chain.ts +18 -251
  109. package/src/execution/engine/common/nesting-guard.ts +18 -114
  110. package/src/execution/engine/common/pool-manager.ts +4 -0
  111. package/src/execution/engine/common/session-view-service.ts +56 -84
  112. package/src/execution/engine/config.ts +151 -0
  113. package/src/execution/engine/d8-compat.ts +346 -0
  114. package/src/execution/engine/engine-discovery-roots.ts +107 -0
  115. package/src/execution/engine/engine-discovery-scan.ts +269 -0
  116. package/src/execution/engine/engine-discovery.ts +30 -7
  117. package/src/execution/engine/engine-inspect-package.ts +282 -0
  118. package/src/execution/engine/engine-manifest.ts +244 -0
  119. package/src/execution/engine/host/__tests__/host-askuser-endpoint.test.ts +123 -0
  120. package/src/execution/engine/host/__tests__/host-bridge.test.ts +135 -0
  121. package/src/execution/engine/host/__tests__/spawned-children.test.ts +73 -0
  122. package/src/execution/engine/host/host-bridge.ts +150 -0
  123. package/src/execution/engine/host/host-ui-endpoint.ts +47 -0
  124. package/src/execution/engine/host/pi-host-binding.ts +91 -0
  125. package/src/execution/engine/host/spawned-children.ts +139 -0
  126. package/src/execution/engine/host-task-spec.ts +29 -2
  127. package/src/execution/engine/model-validation.ts +55 -2
  128. package/src/execution/engine/paths.ts +16 -41
  129. package/src/execution/engine/port.ts +48 -2
  130. package/src/execution/engine/registry.ts +153 -34
  131. package/src/execution/engine/routing.ts +225 -52
  132. package/src/execution/engine/types.ts +25 -200
  133. package/src/execution/finalize-record.ts +25 -0
  134. package/src/execution/idle-gc.ts +89 -14
  135. package/src/execution/lifecycle-manager.ts +10 -7
  136. package/src/execution/lifecycle-predicates.ts +10 -8
  137. package/src/execution/record-store.ts +29 -2
  138. package/src/execution/relay-env.ts +19 -36
  139. package/src/execution/round-supervisor/domain.ts +89 -0
  140. package/src/execution/round-supervisor/index.ts +33 -0
  141. package/src/execution/round-supervisor/notify-accounting.ts +87 -0
  142. package/src/execution/round-supervisor/reconcile-sweep.ts +235 -0
  143. package/src/execution/round-supervisor/service-binding.test.ts +661 -0
  144. package/src/execution/round-supervisor/service-binding.ts +273 -0
  145. package/src/execution/round-supervisor/supervisor.ts +426 -0
  146. package/src/execution/session-pending.ts +68 -74
  147. package/src/execution/settled-watchdog.ts +39 -0
  148. package/src/execution/subagent-service.ts +746 -400
  149. package/src/execution/subprocess-agent-runner.ts +16 -8
  150. package/src/execution/types.ts +41 -117
  151. package/src/execution/ui-channels.ts +13 -214
  152. package/src/execution/ui-request-handler-factory.ts +7 -4
  153. package/src/execution/ui-request-observability.ts +2 -2
  154. package/src/execution/workflow-state-root.ts +67 -0
  155. package/src/execution/worktree-git-ops.ts +5 -0
  156. package/src/execution/worktree-manager.ts +11 -1
  157. package/src/index.ts +55 -26
  158. package/src/orchestration/__tests__/execute-agent-call.test.ts +1 -1
  159. package/src/orchestration/execute-agent-call.ts +1 -1
  160. package/src/orchestration/file-run-store.ts +144 -59
  161. package/src/orchestration/models/types.ts +43 -42
  162. package/src/shared/zcode-model-ref.ts +31 -0
  163. package/dist/chunk-43ONBFZX.js +0 -240
  164. package/dist/chunk-A4IVZWVX.js +0 -31
  165. package/dist/chunk-APZY4IME.js +0 -27
  166. package/dist/chunk-V3VHZ2VX.js +0 -59
  167. package/dist/execution/engine/engines/zcode/constants.cjs +0 -107
  168. package/dist/execution/engine/engines/zcode/constants.d.cts +0 -135
  169. package/dist/execution/engine/engines/zcode/constants.d.ts +0 -135
  170. package/dist/execution/engine/engines/zcode/constants.js +0 -54
  171. package/dist/execution/engine/engines/zcode/reader.cjs +0 -276
  172. package/dist/execution/engine/engines/zcode/reader.d.cts +0 -21
  173. package/dist/execution/engine/engines/zcode/reader.d.ts +0 -21
  174. package/dist/execution/engine/engines/zcode/reader.js +0 -9
  175. package/src/__tests__/append-system-prompt-assembly.test.ts +0 -243
  176. package/src/__tests__/fr4-get-state-handshake.test.ts +0 -125
  177. package/src/__tests__/session-runner.test.ts +0 -69
  178. package/src/execution/__tests__/ask-user-transit-e2e.test.ts +0 -490
  179. package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +0 -191
  180. package/src/execution/__tests__/descendant-sweep-guards.test.ts +0 -220
  181. package/src/execution/__tests__/descendant-sweep.test.ts +0 -269
  182. package/src/execution/__tests__/engine-model-validation.test.ts +0 -741
  183. package/src/execution/__tests__/epipe-fallback.test.ts +0 -248
  184. package/src/execution/__tests__/execution-runtime-face.test.ts +0 -272
  185. package/src/execution/__tests__/get-state-handshake.test.ts +0 -127
  186. package/src/execution/__tests__/helpers/session-runner-mocks.ts +0 -113
  187. package/src/execution/__tests__/keep-alive-no-progress.test.ts +0 -280
  188. package/src/execution/__tests__/kill-all-escalation.test.ts +0 -196
  189. package/src/execution/__tests__/max-turns-to-watchdog-ms.test.ts +0 -113
  190. package/src/execution/__tests__/output-collector.test.ts +0 -469
  191. package/src/execution/__tests__/pi-invocation.test.ts +0 -166
  192. package/src/execution/__tests__/recursive-visibility-env.test.ts +0 -149
  193. package/src/execution/__tests__/rpc-mode.test.ts +0 -89
  194. package/src/execution/__tests__/run-and-finalize-anchoring.test.ts +0 -387
  195. package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +0 -277
  196. package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +0 -254
  197. package/src/execution/__tests__/run-spawn-edges.test.ts +0 -1079
  198. package/src/execution/__tests__/run-spawn-integration.test.ts +0 -933
  199. package/src/execution/__tests__/run-spawn-resume.test.ts +0 -322
  200. package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +0 -196
  201. package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +0 -207
  202. package/src/execution/__tests__/service-kill-escalation.test.ts +0 -91
  203. package/src/execution/__tests__/session-runner-branch-cache-lru.test.ts +0 -110
  204. package/src/execution/__tests__/session-runner-close-prune.test.ts +0 -181
  205. package/src/execution/__tests__/session-runner-dispatch.test.ts +0 -489
  206. package/src/execution/__tests__/session-runner-epipe.test.ts +0 -179
  207. package/src/execution/__tests__/session-runner-heartbeat-idle-fallback.test.ts +0 -227
  208. package/src/execution/__tests__/session-runner-lifecycle-helpers.test.ts +0 -290
  209. package/src/execution/__tests__/session-runner-schema-env.test.ts +0 -352
  210. package/src/execution/__tests__/settled-watchdog.test.ts +0 -524
  211. package/src/execution/__tests__/spawn-args.test.ts +0 -446
  212. package/src/execution/__tests__/spawn-event-adapter-rpc.test.ts +0 -192
  213. package/src/execution/__tests__/spawn-event-adapter.test.ts +0 -167
  214. package/src/execution/__tests__/spawn-worktree-guidance.test.ts +0 -208
  215. package/src/execution/__tests__/spawned-children.test.ts +0 -236
  216. package/src/execution/__tests__/start-sync-model-guard.test.ts +0 -150
  217. package/src/execution/__tests__/stdin-writer.test.ts +0 -464
  218. package/src/execution/__tests__/subagent-service-message-close.test.ts +0 -693
  219. package/src/execution/__tests__/temp-prompt.test.ts +0 -53
  220. package/src/execution/__tests__/timeout-integration.test.ts +0 -616
  221. package/src/execution/__tests__/turn-limiter-semantics.test.ts +0 -194
  222. package/src/execution/__tests__/turn-limiter.test.ts +0 -65
  223. package/src/execution/__tests__/ui-request-handler.test.ts +0 -205
  224. package/src/execution/__tests__/ui-request-queue.test.ts +0 -298
  225. package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +0 -230
  226. package/src/execution/engine/__tests__/common/schema-emulation.test.ts +0 -128
  227. package/src/execution/engine/__tests__/common/session-view-service-zcode-dbpath.test.ts +0 -149
  228. package/src/execution/engine/__tests__/conformance/__fixtures__/pi-golden-events.json +0 -28
  229. package/src/execution/engine/__tests__/conformance/zcode-appserver-harness.ts +0 -128
  230. package/src/execution/engine/__tests__/paths.test.ts +0 -39
  231. package/src/execution/engine/common/schema-emulation.ts +0 -189
  232. package/src/execution/engine/common/session-view-projection.ts +0 -51
  233. package/src/execution/engine/engines/pi/__tests__/pi-engine.test.ts +0 -615
  234. package/src/execution/engine/engines/pi/__tests__/reader.test.ts +0 -155
  235. package/src/execution/engine/engines/pi/__tests__/spawn-opts-direct.test.ts +0 -257
  236. package/src/execution/engine/engines/pi/argv-mirror.ts +0 -135
  237. package/src/execution/engine/engines/pi/get-state-handshake.ts +0 -179
  238. package/src/execution/engine/engines/pi/output-collector.ts +0 -317
  239. package/src/execution/engine/engines/pi/pi-engine.ts +0 -811
  240. package/src/execution/engine/engines/pi/pi-invocation.ts +0 -120
  241. package/src/execution/engine/engines/pi/reader.ts +0 -59
  242. package/src/execution/engine/engines/pi/registration.ts +0 -37
  243. package/src/execution/engine/engines/pi/session-runner.ts +0 -2840
  244. package/src/execution/engine/engines/pi/spawn-event-adapter.ts +0 -404
  245. package/src/execution/engine/engines/pi/stdin-writer.ts +0 -199
  246. package/src/execution/engine/engines/pi/temp-prompt.ts +0 -62
  247. package/src/execution/engine/engines/pi/turn-limiter.ts +0 -102
  248. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/fake-appserver.mjs +0 -301
  249. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-appserver.json +0 -36
  250. package/src/execution/engine/engines/zcode/__tests__/appserver-launcher.test.ts +0 -321
  251. package/src/execution/engine/engines/zcode/__tests__/connection.test.ts +0 -473
  252. package/src/execution/engine/engines/zcode/__tests__/parser.test.ts +0 -43
  253. package/src/execution/engine/engines/zcode/__tests__/preparer.test.ts +0 -124
  254. package/src/execution/engine/engines/zcode/__tests__/reader.test.ts +0 -210
  255. package/src/execution/engine/engines/zcode/__tests__/registration.test.ts +0 -71
  256. package/src/execution/engine/engines/zcode/__tests__/session-channel-dispose-harvest.test.ts +0 -247
  257. package/src/execution/engine/engines/zcode/__tests__/session-channel-turn-timers.test.ts +0 -451
  258. package/src/execution/engine/engines/zcode/__tests__/session-channel.test.ts +0 -792
  259. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-appserver.test.ts +0 -782
  260. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-degrade.test.ts +0 -168
  261. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-dispose.test.ts +0 -210
  262. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-retry.test.ts +0 -353
  263. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-status.test.ts +0 -282
  264. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-timeout.test.ts +0 -372
  265. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +0 -145
  266. package/src/execution/engine/engines/zcode/appserver-launcher.ts +0 -167
  267. package/src/execution/engine/engines/zcode/connection.ts +0 -598
  268. package/src/execution/engine/engines/zcode/constants.ts +0 -176
  269. package/src/execution/engine/engines/zcode/golden-sample.ts +0 -47
  270. package/src/execution/engine/engines/zcode/parser.ts +0 -102
  271. package/src/execution/engine/engines/zcode/preparer.ts +0 -235
  272. package/src/execution/engine/engines/zcode/reader.ts +0 -381
  273. package/src/execution/engine/engines/zcode/registration.ts +0 -37
  274. package/src/execution/engine/engines/zcode/session-channel.ts +0 -968
  275. package/src/execution/engine/engines/zcode/zcode-engine.ts +0 -1431
  276. package/src/execution/round-settlement.ts +0 -93
  277. package/src/execution/ui-request-queue.ts +0 -215
  278. package/src/shared/schema-env.ts +0 -44
@@ -0,0 +1,255 @@
1
+ // src/execution/__tests__/chat-round-first-round-watchdog.test.ts
2
+ //
3
+ // [F-2 修复] 首轮/冷续轮 settled-watchdog arm 重接 + [F-5 修复] chat 轮路由终态化注销。
4
+ //
5
+ // 背景:armMidRoundNoProgress 此前唯一调用点在 deliverChatMessage(热路径续聊轮);
6
+ // kickOffChatRound(spawn 首轮 + 冷续 resume 轮)内无任何 arm——被删的 inproc
7
+ // stdout-pump 是首轮唯一中段守护,协议化后引擎侧 spawn-runner 仅 turn 计数无墙钟,
8
+ // 首轮 wedged(LC-1 场景①:无事件行输出)无任何熔断。修复 = run 派发前补 arm
9
+ //(refresh 源 = runId 键 ctx.onEvent 事件行 + ctx.onRoundLifecycle settled 交棒——
10
+ // 引擎侧 spawn-runner 对 text_delta 同时走 onEvent 与 onDelta,刷新面与热路径等效)。
11
+ //
12
+ // [F-5] chat 轮路由(chatRoundRoutes)此前仅 cancelBackground 注销;finalize 链终态化
13
+ // (closeChatIdle / closeAfterRoundSettled / finalizeRecord)不注销 → 路由闭包持
14
+ // record/stream/守护引用泄漏。修复 = doFinalizeRecord 的 onFinalized 钩子单一汇聚点。
15
+ //
16
+ // 替身形态与 subagent-service-recovery-bounds.test.ts 同源(fake-engine-port 协议 seam)。
17
+
18
+ import * as fs from "node:fs";
19
+ import * as os from "node:os";
20
+ import * as path from "node:path";
21
+
22
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
23
+
24
+ const { loggerMock } = vi.hoisted(() => ({
25
+ loggerMock: { debug: vi.fn(), warn: vi.fn(), error: vi.fn() },
26
+ }));
27
+ vi.mock("../../core/logger.ts", () => ({ getLogger: () => loggerMock }));
28
+
29
+ const { killChildSpy } = vi.hoisted(() => ({ killChildSpy: vi.fn() }));
30
+ vi.mock("../engine/host/spawned-children.ts", async (importOriginal) => {
31
+ const actual = await importOriginal<typeof import("../engine/host/spawned-children.ts")>();
32
+ return { ...actual, killRecordChildWithEscalation: killChildSpy };
33
+ });
34
+
35
+ import { clearEngines } from "../engine/registry.ts";
36
+ import { registerFakePiEngine, type FakePiEnginePort } from "./helpers/fake-engine-port.ts";
37
+ import { createRecord } from "../execution-record.ts";
38
+ import { ModelConfigService } from "../model-config-service.ts";
39
+ import type { ModelInfo, ModelRegistryLike } from "../model-resolver.ts";
40
+ import type { RecordStore } from "../record-store.ts";
41
+ import { SubagentService } from "../subagent-service.ts";
42
+ import type { PiLike } from "../subagent-service.ts";
43
+ import {
44
+ getSettledWatchdogPhase,
45
+ hasSettledWatchdog,
46
+ SETTLED_MID_ROUND_NO_PROGRESS_MS,
47
+ SETTLED_WATCHDOG_TIMEOUT_MS,
48
+ _resetSettledWatchdogsForTest,
49
+ } from "../settled-watchdog.ts";
50
+ import { _resetLifecycleState } from "../lifecycle-manager.ts";
51
+ import { _resetCoreSpawnedChildrenMirrorForTest } from "../engine/host/spawned-children.ts";
52
+ import type { ExecutionRecord } from "../types.ts";
53
+
54
+ function makePi(): PiLike {
55
+ return {
56
+ appendEntry: vi.fn(),
57
+ events: { emit: vi.fn() },
58
+ sendMessage: vi.fn(),
59
+ } as unknown as PiLike;
60
+ }
61
+
62
+ interface ServiceInternals {
63
+ store: RecordStore;
64
+ }
65
+
66
+ const CTX_MODEL: ModelInfo = { id: "m", name: "M", provider: "p", reasoning: false };
67
+
68
+ function makeEmptyRegistry(): ModelRegistryLike {
69
+ return { getAvailable: () => [], find: () => undefined, hasConfiguredAuth: () => true };
70
+ }
71
+
72
+ function setup(): { agentDir: string; service: SubagentService; store: RecordStore; pi: PiLike; fake: FakePiEnginePort } {
73
+ const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "chat-round-watchdog-"));
74
+ clearEngines();
75
+ const fake = registerFakePiEngine();
76
+ const modelService = new ModelConfigService({ agentDir, cwd: agentDir });
77
+ modelService.initModel({ modelRegistry: makeEmptyRegistry(), sessionId: "root-session", ctxModel: CTX_MODEL });
78
+ const service = new SubagentService({ cwd: agentDir, modelService });
79
+ const pi = makePi();
80
+ service.initSession({ pi, sessionId: "root-session" });
81
+ const store = (service as unknown as ServiceInternals).store;
82
+ return { agentDir, service, store, pi, fake };
83
+ }
84
+
85
+ /** 模拟 kickOffChatRound 的路由注册面(Service map + fake 引擎两端同挂)。 */
86
+ function registerRoute(service: SubagentService, fake: FakePiEnginePort, recordId: string): void {
87
+ const unregister = fake.registerChatRoundRoute(recordId, {
88
+ onStreamDelta: () => {},
89
+ onRoundLifecycle: () => {},
90
+ });
91
+ (
92
+ service as unknown as { chatRoundRoutes: Map<string, () => void> }
93
+ ).chatRoundRoutes.set(recordId, unregister);
94
+ }
95
+
96
+ function makeChatRecord(id: string): ExecutionRecord {
97
+ return createRecord(id, {
98
+ agent: "general-purpose",
99
+ model: "test/model",
100
+ mode: "background",
101
+ task: "test",
102
+ slug: "test",
103
+ startedAt: 1000,
104
+ rootSessionId: "root-session",
105
+ chatMode: true,
106
+ controller: new AbortController(),
107
+ });
108
+ }
109
+
110
+ describe("[F-2] 首轮/冷续轮 settled-watchdog arm(kickOffChatRound run 派发点)", () => {
111
+ let agentDir: string;
112
+ let service: SubagentService;
113
+ let store: RecordStore;
114
+ let pi: PiLike;
115
+ let fake: FakePiEnginePort;
116
+
117
+ beforeEach(() => {
118
+ vi.restoreAllMocks();
119
+ killChildSpy.mockClear();
120
+ ({ agentDir, service, store, pi, fake } = setup());
121
+ });
122
+
123
+ afterEach(() => {
124
+ service.dispose();
125
+ clearEngines();
126
+ vi.useRealTimers();
127
+ _resetLifecycleState();
128
+ _resetSettledWatchdogsForTest();
129
+ _resetCoreSpawnedChildrenMirrorForTest();
130
+ fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
131
+ });
132
+
133
+ it("首轮 run 派发后中段守护 armed(修复前首轮无任何守护)", async () => {
134
+ const handle = await service.execute({ task: "first round", slug: "t", ctxModel: CTX_MODEL, conversation: true });
135
+ expect(fake.runs).toHaveLength(1);
136
+ expect(hasSettledWatchdog(handle.subagentId)).toBe(true);
137
+ expect(getSettledWatchdogPhase(handle.subagentId)).toBe("mid-round");
138
+ });
139
+
140
+ it("冷续轮进行中无产出 → 中段守护按既有超时语义触发:kill + 引擎 cancel + 回退 idle-resumable(fake timers)", async () => {
141
+ // fake timers 必须先于 arm 生效(useFakeTimers 不接管已存在的真实 timer)
142
+ vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] });
143
+ const record = makeChatRecord("sa-first");
144
+ record.sessionFile = path.join(agentDir, "prior-session.jsonl");
145
+ fs.writeFileSync(record.sessionFile, "{}\n", "utf-8");
146
+ store.register(record);
147
+ await (
148
+ service as unknown as { resumeColdRound: (r: ExecutionRecord, t: string) => void }
149
+ ).resumeColdRound(record, "cold resume text");
150
+ expect(fake.runs).toHaveLength(1); // 冷续轮 run 已派发
151
+ expect(hasSettledWatchdog(record.id)).toBe(true);
152
+
153
+ // 冷续轮全程静默(无协议事件行)满中段窗长
154
+ await vi.advanceTimersByTimeAsync(SETTLED_MID_ROUND_NO_PROGRESS_MS + 1);
155
+
156
+ expect(killChildSpy).toHaveBeenCalledWith(record.id, "settled watchdog (hot path)");
157
+ expect(hasSettledWatchdog(record.id)).toBe(false); // 到期自清
158
+ // chatMode MF-6:回退 idle-resumable(不销毁对话)
159
+ expect(record.resumable).toBe(true);
160
+ expect(record.status).toBe("running");
161
+ expect(record.result).toContain("settled watchdog");
162
+ expect(record.result).toContain("Recovery");
163
+ // 引擎侧终止意图(协议 interact cancel)
164
+ const cancel = fake.interacts.find((c) => c.action.kind === "cancel");
165
+ expect(cancel).toBeDefined();
166
+ void pi;
167
+ });
168
+
169
+ it("中段事件行(text_delta 经 onEvent)刷新静默计时:半窗 + 刷新 + 半窗不触发,再满窗才触发", async () => {
170
+ vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] });
171
+ const handle = await service.execute({ task: "streaming round", slug: "t", ctxModel: CTX_MODEL, conversation: true });
172
+ const run = fake.runs[0]!;
173
+
174
+ await vi.advanceTimersByTimeAsync(SETTLED_MID_ROUND_NO_PROGRESS_MS / 2);
175
+ // 首轮 runId 键协议事件行到达(text_delta——引擎侧同时走 onEvent 通道)
176
+ run.emitEvent({ type: "text_delta", delta: "chunk" });
177
+ await vi.advanceTimersByTimeAsync(SETTLED_MID_ROUND_NO_PROGRESS_MS / 2 + 1);
178
+ expect(hasSettledWatchdog(handle.subagentId)).toBe(true); // 刷新后未到期
179
+ expect(killChildSpy).not.toHaveBeenCalled();
180
+
181
+ // 刷新后再满整个静默窗才触发
182
+ await vi.advanceTimersByTimeAsync(SETTLED_MID_ROUND_NO_PROGRESS_MS + 1);
183
+ expect(killChildSpy).toHaveBeenCalledWith(handle.subagentId, "settled watchdog (hot path)");
184
+ });
185
+
186
+ it("首轮 settled 相位交棒:中段窗内相位到达 → 切收尾段(中段窗不再触发,收尾段硬顶触发)", async () => {
187
+ vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] });
188
+ const handle = await service.execute({ task: "stuck after settle", slug: "t", ctxModel: CTX_MODEL, conversation: true });
189
+ const run = fake.runs[0]!;
190
+
191
+ await vi.advanceTimersByTimeAsync(SETTLED_MID_ROUND_NO_PROGRESS_MS / 2);
192
+ // 首轮 runId 键 roundLifecycle settled 相位(W3 报告称已接——本用例核实链路生效)
193
+ run.emitLifecycle({ phase: "settled", usage: { input: 1, output: 1, cacheRead: 0, cacheWrite: 0 } });
194
+ expect(getSettledWatchdogPhase(handle.subagentId)).toBe("settled"); // 已交棒收尾段
195
+
196
+ // 中段不再计时(交棒清掉,不继承——30min 静默判据对收尾段失效);收尾段未满(<600s)不触发
197
+ await vi.advanceTimersByTimeAsync(SETTLED_WATCHDOG_TIMEOUT_MS - 1);
198
+ expect(killChildSpy).not.toHaveBeenCalled();
199
+ expect(hasSettledWatchdog(handle.subagentId)).toBe(true); // 收尾段仍在计时
200
+
201
+ // 收尾段硬顶(600s 默认)到期触发
202
+ await vi.advanceTimersByTimeAsync(2);
203
+ expect(killChildSpy).toHaveBeenCalledWith(handle.subagentId, "settled watchdog (hot path)");
204
+ });
205
+ });
206
+
207
+ describe("[F-5] chat 轮路由终态化注销(doFinalizeRecord onFinalized 汇聚点)", () => {
208
+ let agentDir: string;
209
+ let service: SubagentService;
210
+ let store: RecordStore;
211
+ let fake: FakePiEnginePort;
212
+
213
+ beforeEach(() => {
214
+ vi.restoreAllMocks();
215
+ killChildSpy.mockClear();
216
+ ({ agentDir, service, store, fake } = setup());
217
+ });
218
+
219
+ afterEach(() => {
220
+ service.dispose();
221
+ clearEngines();
222
+ _resetLifecycleState();
223
+ _resetSettledWatchdogsForTest();
224
+ _resetCoreSpawnedChildrenMirrorForTest();
225
+ fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
226
+ });
227
+
228
+ it("closeChatIdle 终态化路径注销 chat 轮路由(修复前仅 cancelBackground 注销 → 泄漏)", async () => {
229
+ const record = makeChatRecord("sa-route-leak");
230
+ store.register(record);
231
+ // 模拟首轮已注册的 recordId 键路由(kickOffChatRound 注册面:Service map + fake 引擎两端)
232
+ registerRoute(service, fake, record.id);
233
+ // Path A:idle timer armed(进程保活等待续聊)→ closeSubagent(false) 走 closeChatIdle
234
+ const { armIdleTimer } = (await import("../lifecycle-manager.ts")) as typeof import("../lifecycle-manager.ts");
235
+ armIdleTimer(record.id, () => {}, 60_000);
236
+
237
+ await service.chatActions.closeSubagent(record, false);
238
+
239
+ expect(fake.chatRouteUnregisters).toContain(record.id);
240
+ });
241
+
242
+ it("closeAfterRoundSettled(优雅关闭消费)路径注销 chat 轮路由", async () => {
243
+ const record = makeChatRecord("sa-route-close-round");
244
+ store.register(record);
245
+ registerRoute(service, fake, record.id);
246
+ record.closeAfterRound = true;
247
+
248
+ await (
249
+ service as unknown as { closeAfterRoundSettled: (r: ExecutionRecord) => Promise<void> }
250
+ ).closeAfterRoundSettled(record);
251
+
252
+ expect(fake.chatRouteUnregisters).toContain(record.id);
253
+ expect(record.status).toBe("closed");
254
+ });
255
+ });
@@ -12,8 +12,9 @@
12
12
  // 5. flush 屏障时序:manifest 写完成先于 notifyBatch 写账(通知可达 ⇒ 索引就位
13
13
  // 的构造性保证,时序竞态修复)。
14
14
  //
15
- // mock 手法对齐 ended-message-and-fork-from.test.ts:mock session-runner(不 spawn
16
- // 真子进程)+ logger;record-store / config 走真实实现(tmpdir 自建自删,红线)。
15
+ // mock 手法对齐 ended-message-and-fork-from.test.ts:[W3 改写] 替身从
16
+ // vi.mock session-runner(不 spawn 真子进程)改为协议 seam(registerFakePiEngine 替身,
17
+ // 测试显式 settle 应答)+ logger;record-store / config 走真实实现(tmpdir 自建自删,红线)。
17
18
 
18
19
  import * as fs from "node:fs";
19
20
  import * as os from "node:os";
@@ -21,35 +22,16 @@ import * as path from "node:path";
21
22
 
22
23
  import { afterEach, beforeEach, describe, expect, it, vi, type Mock } from "vitest";
23
24
 
24
- const { loggerMock, runSpawnMock } = vi.hoisted(() => ({
25
+ const { loggerMock } = vi.hoisted(() => ({
25
26
  loggerMock: { debug: vi.fn(), warn: vi.fn(), error: vi.fn(), info: vi.fn() },
26
- /** runSpawn mock 实例提升:测试内直接引用控制 resolve 时序(不依赖 import 路径解析——
27
- * vi.mock 字符串与本文件既有写法保持一致,mock 实例经 hoisted 闭包共享)。 */
28
- runSpawnMock: vi.fn(async () => ({
29
- text: "ok",
30
- turns: 1,
31
- durationMs: 10,
32
- success: true,
33
- sessionId: "spawned",
34
- toolCalls: [],
35
- })),
36
27
  }));
37
28
  vi.mock("../../core/logger.ts", () => ({ getLogger: () => loggerMock }));
38
29
 
39
- // mock session-runner:execute 链经 kickOffBackgroundrunAndFinalizerunSpawn。
40
- // runSpawn 返回最小成功 AgentResult,后台收尾链完整走完(finalize + archive + notify)。
41
- // 路径必须命中生产 import 的真实模块——merge session-runner 落位
42
- // src/execution/engine/engines/pi/session-runner.ts(subagent-service.ts 的 import 源),
43
- // 旧路径 "../session-runner.ts" 模块已不存在,拦截静默失效(真 runSpawn 被执行,
44
- // runSpawnMock.calls 恒 0 → 受控 promise 用例 until 超时)。
45
- vi.mock("../engine/engines/pi/session-runner.ts", () => ({
46
- runSpawn: runSpawnMock,
47
- killAllSpawnedChildren: vi.fn(),
48
- killRecordChildWithEscalation: vi.fn(),
49
- getChildByRecord: vi.fn(() => undefined),
50
- registerSpawnedChildForRecord: vi.fn(),
51
- spawnedChildren: new Map(),
52
- }));
30
+ // [W3] execute 链经 executeViaEnginekickOffChatRoundengine.run(协议替身)。
31
+ // 替身 run 挂起不自动 settle:测试取 fake.runs.at(-1) 显式 settle(受控时序——
32
+ // runSpawnMock resolve 时序控制等价面)。
33
+ import { clearEngines } from "../engine/registry.ts";
34
+ import { registerFakePiEngine, type FakePiEnginePort } from "./helpers/fake-engine-port.ts";
53
35
 
54
36
  import { getSubagentSessionDir, getSubagentRecordsDir } from "../path-encoding.ts";
55
37
  import { ModelConfigService } from "../model-config-service.ts";
@@ -116,9 +98,12 @@ describe("collectCoordinator service integration (U2)", () => {
116
98
  let agentDir: string;
117
99
  let service: SubagentService;
118
100
  let pi: ReturnType<typeof makePi>;
101
+ let fake: FakePiEnginePort;
119
102
 
120
103
  beforeEach(() => {
121
104
  for (const k of IDENTITY_ENV_KEYS) delete process.env[k];
105
+ clearEngines();
106
+ fake = registerFakePiEngine();
122
107
  agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "core-collect-coord-"));
123
108
  fs.mkdirSync(getSubagentSessionDir(agentDir, agentDir), { recursive: true });
124
109
  pi = makePi();
@@ -127,6 +112,7 @@ describe("collectCoordinator service integration (U2)", () => {
127
112
 
128
113
  afterEach(() => {
129
114
  service.dispose();
115
+ clearEngines();
130
116
  fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
131
117
  });
132
118
 
@@ -154,6 +140,8 @@ describe("collectCoordinator service integration (U2)", () => {
154
140
  // 批投递(下方用例),async 成员直通。本条记录 execute 链零异常完成。
155
141
  const spy = spyNotifier(service);
156
142
  const handle = await service.execute({ task: "collect me", slug: "collect-me", collect: "sync" });
143
+ await until(() => fake.runs.length >= 1);
144
+ await settleLast();
157
145
  await until(() => spy.notifyBatch.mock.calls.length > 0);
158
146
  expect(handle.subagentId).toMatch(/^sa-/);
159
147
  });
@@ -161,6 +149,8 @@ describe("collectCoordinator service integration (U2)", () => {
161
149
  it("routes a finished sync member into a single-member batch (U3 接线:单成员闭合 → notifyBatch)", async () => {
162
150
  const spy = spyNotifier(service);
163
151
  const handle = await service.execute({ task: "collect me", slug: "collect-me", collect: "sync" });
152
+ await until(() => fake.runs.length >= 1);
153
+ await settleLast();
164
154
  await until(() => spy.notifyBatch.mock.calls.length > 0);
165
155
  // U3 接线后:sync 成员走单条批投递(不再逐条降级 notify)
166
156
  expect(spy.notify).not.toHaveBeenCalled();
@@ -173,6 +163,8 @@ describe("collectCoordinator service integration (U2)", () => {
173
163
  const spy = spyNotifier(service);
174
164
  spy.notifyBatch.mockReturnValue(false); // 模拟同成员集批已在账(E1 重建重发形态)
175
165
  const handle = await service.execute({ task: "collect me", slug: "collect-me", collect: "sync" });
166
+ await until(() => fake.runs.length >= 1);
167
+ await settleLast();
176
168
  await until(() => spy.notifyBatch.mock.calls.length > 0);
177
169
  expect(handle.subagentId).toMatch(/^sa-/);
178
170
  // 落标出口①绑「写账成功」:accepted=false → 无带 batchFinalized 标记的 subagent-record
@@ -188,6 +180,8 @@ describe("collectCoordinator service integration (U2)", () => {
188
180
  it("routes an async member through the same notifyAsync path (现状直通)", async () => {
189
181
  const spy = spyNotifier(service);
190
182
  const handle = await service.execute({ task: "plain", slug: "plain" });
183
+ await until(() => fake.runs.length >= 1);
184
+ await settleLast();
191
185
  await until(() => spy.notify.mock.calls.length > 0);
192
186
  const notified = spy.notify.mock.calls.map((c) => c[0] as { id: string });
193
187
  expect(notified.some((n) => n.id === handle.subagentId)).toBe(true);
@@ -247,28 +241,27 @@ describe("collectCoordinator service integration (U2)", () => {
247
241
  fs.writeFileSync(path.join(sessionsDir, `${recordId}.jsonl`), `${header}\n${identity}\n${recordEntry}\n`, "utf-8");
248
242
  }
249
243
 
250
- const spawnOk = () => ({
251
- text: "ok", turns: 1, durationMs: 10, success: true, sessionId: "spawned", toolCalls: [],
252
- });
244
+ /** 受控替身 settle:等替身 run 到位(execute 返回后的异步链内发起),按需 settle。
245
+ * 原 runSpawnMock 受控 promise 的等价面(content "ok" 对齐原 AgentResult.text)。 */
246
+ const settleLast = async (): Promise<void> => {
247
+ await until(() => fake.runs.length >= 1);
248
+ fake.runs[fake.runs.length - 1]!.settle({ content: "ok" });
249
+ };
253
250
 
254
251
  it("跨轮续累(D2 service 层):两轮派 2 sync → 闭合时单批 2 成员", async () => {
255
- let resolve1!: (v: ReturnType<typeof spawnOk>) => void;
256
- let resolve2!: (v: ReturnType<typeof spawnOk>) => void;
257
- runSpawnMock.mockImplementationOnce(() => new Promise((res) => { resolve1 = res; }));
258
- runSpawnMock.mockImplementationOnce(() => new Promise((res) => { resolve2 = res; }));
259
252
  const spy = spyNotifier(service);
260
253
  const h1 = await service.execute({ task: "t1", slug: "one", collect: "sync" });
261
254
  const h2 = await service.execute({ task: "t2", slug: "two", collect: "sync" });
262
- // runSpawn 在 execute 返回后的异步链内才被调用——等两个受控 promise 都构造完
263
- await until(() => runSpawnMock.mock.calls.length >= 2);
255
+ // engine.run 在 execute 返回后的异步链内才被发起——等两个替身 run 都到位
256
+ await until(() => fake.runs.length >= 2);
264
257
 
265
258
  // 第一台终态:第二台仍 running → 缓冲不闭合(零投递)
266
- resolve1(spawnOk());
259
+ fake.runs[0]!.settle({ content: "ok" });
267
260
  await new Promise((resolve) => setTimeout(resolve, 50)); // microtask 链排空
268
261
  expect(spy.notifyBatch.mock.calls).toHaveLength(0);
269
262
 
270
263
  // 第二台终态:闭合 → 单批 2 成员(含第一台的快照,跨轮续累不丢)
271
- resolve2(spawnOk());
264
+ fake.runs[1]!.settle({ content: "ok" });
272
265
  await until(() => spy.notifyBatch.mock.calls.length > 0);
273
266
  expect(spy.notifyBatch.mock.calls).toHaveLength(1);
274
267
  const batch = spy.notifyBatch.mock.calls[0]![0] as { id: string }[];
@@ -277,14 +270,12 @@ describe("collectCoordinator service integration (U2)", () => {
277
270
  });
278
271
 
279
272
  it("batchFinalized 落标(真文件通路):闭合 flush 写账成功后末条 entry 带标记(E1 排除判据)", async () => {
280
- let resolveSpawn!: (v: ReturnType<typeof spawnOk>) => void;
281
- runSpawnMock.mockImplementationOnce(() => new Promise((res) => { resolveSpawn = res; }));
282
273
  const spy = spyNotifier(service);
283
274
  const handle = await service.execute({ task: "collect me", slug: "collect-me", collect: "sync" });
284
- await until(() => runSpawnMock.mock.calls.length >= 1); // 受控 promise 已构造
285
- // 子 session 文件在闭合判定/flush 前就位(真实生产链中 runSpawn 已写好 identity)
275
+ await until(() => fake.runs.length >= 1); // 替身 run 已到位
276
+ // 子 session 文件在闭合判定/flush 前就位(真实生产链中引擎轮次已写好 identity)
286
277
  writeChildSessionFile(handle.subagentId, "/agents/worker.md", "ok");
287
- resolveSpawn(spawnOk());
278
+ await settleLast();
288
279
  await until(() => spy.notifyBatch.mock.calls.length > 0);
289
280
 
290
281
  // 观察者形态:主 session 落盘的末条 subagent-record entry 带 batchFinalized=true +
@@ -307,11 +298,9 @@ describe("collectCoordinator service integration (U2)", () => {
307
298
  // 构造性保证(by construction)。断言打在 notifyBatch(写账入口)被调用的时刻:
308
299
  // 磁盘上 records/<sa-id>.json 必已存在(修复前该时刻大概率 false,探针实测 mtime
309
300
  // 相对 notify entry ±2/3ms 方向不定)。
310
- let resolveSpawn!: (v: ReturnType<typeof spawnOk>) => void;
311
- runSpawnMock.mockImplementationOnce(() => new Promise((res) => { resolveSpawn = res; }));
312
301
  const spy = spyNotifier(service);
313
302
  const handle = await service.execute({ task: "collect me", slug: "collect-me", collect: "sync" });
314
- await until(() => runSpawnMock.mock.calls.length >= 1); // 受控 promise 已构造
303
+ await until(() => fake.runs.length >= 1); // 替身 run 已到位
315
304
  writeChildSessionFile(handle.subagentId, "/agents/worker.md", "ok");
316
305
  const manifestPath = path.join(getSubagentRecordsDir(agentDir, agentDir), `${handle.subagentId}.json`);
317
306
  // 写账时刻钩子:mock 内联捕获 manifest 存在性(此刻即指针行消费可用性)
@@ -320,7 +309,7 @@ describe("collectCoordinator service integration (U2)", () => {
320
309
  manifestExistsAtLedgerWrite = fs.existsSync(manifestPath);
321
310
  return true;
322
311
  });
323
- resolveSpawn(spawnOk());
312
+ await settleLast();
324
313
  await until(() => spy.notifyBatch.mock.calls.length > 0);
325
314
  expect(manifestExistsAtLedgerWrite).toBe(true);
326
315
  });
@@ -27,32 +27,15 @@ import * as path from "node:path";
27
27
 
28
28
  import { afterEach, beforeEach, describe, expect, it, vi, type Mock } from "vitest";
29
29
 
30
- const { loggerMock, runSpawnMock } = vi.hoisted(() => ({
30
+ const { loggerMock } = vi.hoisted(() => ({
31
31
  loggerMock: { debug: vi.fn(), warn: vi.fn(), error: vi.fn(), info: vi.fn() },
32
- runSpawnMock: vi.fn(async () => ({
33
- text: "ok",
34
- turns: 1,
35
- durationMs: 10,
36
- success: true,
37
- sessionId: "spawned",
38
- toolCalls: [],
39
- })),
40
32
  }));
41
33
  vi.mock("../../core/logger.ts", () => ({ getLogger: () => loggerMock }));
42
34
 
43
- // mock 路径自检(impl-plan 偏差 #6 教训):从 __tests__ 解析必须命中生产 import 的
44
- // 真实模块——merge session-runner 落位 src/execution/engine/engines/pi/session-runner.ts
45
- //(subagent-service.ts import 源),旧路径 "../session-runner.ts" 模块已不存在,
46
- // 拦截静默失效(真 runSpawn 被执行,runSpawnMock.calls 0 → 受控 promise 用例超时)。
47
- vi.mock("../engine/engines/pi/session-runner.ts", () => ({
48
- runSpawn: runSpawnMock,
49
- killAllSpawnedChildren: vi.fn(),
50
- killRecordChildWithEscalation: vi.fn(),
51
- getChildByRecord: vi.fn(() => undefined),
52
- registerSpawnedChildForRecord: vi.fn(),
53
- spawnedChildren: new Map(),
54
- }));
55
-
35
+ // [W3 改写] 替身从 vi.mock session-runner 的受控 runSpawn 改为协议 seam 的受控
36
+ // engine.run(registerFakePiEngine 替身 + 显式 settle)。
37
+ import { registerFakePiEngine, type FakePiEnginePort } from "./helpers/fake-engine-port.ts";
38
+ import { clearEngines } from "../engine/registry.ts";
56
39
  import { getSubagentSessionDir } from "../path-encoding.ts";
57
40
  import { ModelConfigService } from "../model-config-service.ts";
58
41
  import type { ModelRegistryLike } from "../model-resolver.ts";
@@ -106,31 +89,10 @@ async function until(cond: () => boolean, timeoutMs = 3000): Promise<void> {
106
89
  }
107
90
  }
108
91
 
109
- const spawnOk = () => ({
110
- text: "ok",
111
- turns: 1,
112
- durationMs: 10,
113
- success: true,
114
- sessionId: "spawned",
115
- toolCalls: [],
116
- });
117
-
118
- /** 受控 runSpawn:每次调用挂起返回 resolver,测试按成员序号放行终态。
119
- * resolver 在 runSpawn 实际调用时(execute 异步链内)才构造——release 函数按
120
- * 索引延迟取用,不能在挂起注册时立刻闭包捕获(彼时尚为 undefined)。 */
121
- function controlledSpawns(n: number): Array<(v?: ReturnType<typeof spawnOk>) => void> {
122
- const resolvers: Array<(v: ReturnType<typeof spawnOk>) => void> = [];
123
- for (let i = 0; i < n; i += 1) {
124
- runSpawnMock.mockImplementationOnce(
125
- () =>
126
- new Promise((res) => {
127
- resolvers[i] = res;
128
- }),
129
- );
130
- }
131
- return Array.from({ length: n }, (_, i) => (v?: ReturnType<typeof spawnOk>) =>
132
- resolvers[i]!(v ?? spawnOk()),
133
- );
92
+ /** 受控替身 run:等 n 个替身 run 到位(execute 异步链内发起),按索引放行 settle。
93
+ * 与原受控 runSpawn 的 resolver 形态同构(索引 = execute 派发序)。 */
94
+ function makeRelease(fk: FakePiEnginePort, n: number): Array<() => void> {
95
+ return Array.from({ length: n }, (_, i) => () => fk.runs[i]!.settle({ content: "ok" }));
134
96
  }
135
97
 
136
98
  describe("A8 混派正交 service e2e(同轮 2 sync + 1 async,U8)", () => {
@@ -138,8 +100,12 @@ describe("A8 混派正交 service e2e(同轮 2 sync + 1 async,U8)", () =>
138
100
  let service: SubagentService;
139
101
  let pi: ReturnType<typeof makePi>;
140
102
 
103
+ let fake: FakePiEnginePort;
104
+
141
105
  beforeEach(() => {
142
106
  for (const k of IDENTITY_ENV_KEYS) delete process.env[k];
107
+ clearEngines();
108
+ fake = registerFakePiEngine();
143
109
  agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "core-mixed-dispatch-"));
144
110
  fs.mkdirSync(getSubagentSessionDir(agentDir, agentDir), { recursive: true });
145
111
  pi = makePi();
@@ -160,6 +126,7 @@ describe("A8 混派正交 service e2e(同轮 2 sync + 1 async,U8)", () =>
160
126
 
161
127
  afterEach(() => {
162
128
  service.dispose();
129
+ clearEngines();
163
130
  fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
164
131
  });
165
132
 
@@ -167,11 +134,11 @@ describe("A8 混派正交 service e2e(同轮 2 sync + 1 async,U8)", () =>
167
134
  const spy = spyNotifier(service);
168
135
  // 同轮三连派发:2 sync + 1 async(派发顺序 sync, sync, async——同一条消息内的
169
136
  // 混派形态;runSpawn 调用序与 execute 序一致)
170
- const [doneSync1, doneSync2, doneAsync] = controlledSpawns(3);
137
+ const [doneSync1, doneSync2, doneAsync] = makeRelease(fake, 3);
171
138
  const hSync1 = await service.execute({ task: "s1", slug: "sync-one", collect: "sync" });
172
139
  const hSync2 = await service.execute({ task: "s2", slug: "sync-two", collect: "sync" });
173
140
  const hAsync = await service.execute({ task: "a1", slug: "async-one" });
174
- await until(() => runSpawnMock.mock.calls.length >= 3); // 三个受控 promise 均已构造
141
+ await until(() => fake.runs.length >= 3); // 三个替身 run 均已到位
175
142
 
176
143
  // async 成员先终态:立即单条 notify(现状路径),批零投递、零扣留
177
144
  doneAsync();
@@ -198,11 +165,11 @@ describe("A8 混派正交 service e2e(同轮 2 sync + 1 async,U8)", () =>
198
165
 
199
166
  it("sync 批先闭合(async 仍在跑):背靠背紧窗口合批单批;async 随后独立单条 notify", async () => {
200
167
  const spy = spyNotifier(service);
201
- const [doneSync1, doneSync2, doneAsync] = controlledSpawns(3);
168
+ const [doneSync1, doneSync2, doneAsync] = makeRelease(fake, 3);
202
169
  const hSync1 = await service.execute({ task: "s1", slug: "sync-one", collect: "sync" });
203
170
  const hSync2 = await service.execute({ task: "s2", slug: "sync-two", collect: "sync" });
204
171
  const hAsync = await service.execute({ task: "a1", slug: "async-one" });
205
- await until(() => runSpawnMock.mock.calls.length >= 3);
172
+ await until(() => fake.runs.length >= 3);
206
173
 
207
174
  // 两个 sync 同步段背靠背终态(窄于 finalize 链宽度——U8 拆批盲窗的精确复现形态:
208
175
  // 先 archive 的成员 route 时,后者的 notifyComplete 尚在 finalize 链间隙,闭合
@@ -228,11 +195,11 @@ describe("A8 混派正交 service e2e(同轮 2 sync + 1 async,U8)", () =>
228
195
 
229
196
  it("观察者形态:同轮混派三 record 的落盘 entry 各自携带正确 collectMode", async () => {
230
197
  const spy = spyNotifier(service);
231
- const [, , doneAsync] = controlledSpawns(3);
198
+ const [, , doneAsync] = makeRelease(fake, 3);
232
199
  const hSync1 = await service.execute({ task: "s1", slug: "sync-one", collect: "sync" });
233
200
  const hSync2 = await service.execute({ task: "s2", slug: "sync-two", collect: "sync" });
234
201
  const hAsync = await service.execute({ task: "a1", slug: "async-one" });
235
- await until(() => runSpawnMock.mock.calls.length >= 3);
202
+ await until(() => fake.runs.length >= 3);
236
203
  doneAsync();
237
204
  await until(() => spy.notify.mock.calls.length > 0);
238
205
  // 批不必闭合即可断言落盘(register 期 entry 即带 collectMode):