@zhushanwen/subagent-core 0.6.0 → 0.7.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 (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
@@ -22,16 +22,10 @@ const { loggerMock } = vi.hoisted(() => ({
22
22
  }));
23
23
  vi.mock("../../core/logger.ts", () => ({ getLogger: () => loggerMock }));
24
24
 
25
- const { killChildSpy } = vi.hoisted(() => ({ killChildSpy: vi.fn() }));
26
- vi.mock("../engine/engines/pi/session-runner.ts", () => ({
27
- runSpawn: vi.fn(),
28
- killAllSpawnedChildren: vi.fn(),
29
- getChildByRecord: vi.fn(() => undefined),
30
- registerSpawnedChildForRecord: vi.fn(),
31
- spawnedChildren: new Map(),
32
- killRecordChildWithEscalation: killChildSpy,
33
- }));
34
-
25
+ // [W3 改写] vi.mock(inproc pi 引擎目录/session-runner) 随删件消亡——chat 轮次走协议 seam
26
+ //(registerFakePiEngine 替身,kickOffChatRound 的 notify 门经 engine.run 应答驱动)。
27
+ import { registerFakePiEngine } from "./helpers/fake-engine-port.ts";
28
+ import { clearEngines } from "../engine/registry.ts";
35
29
  import { bindNotifyLedgerHost, NOTIFY_LEDGER_CUSTOM_TYPE, _resetNotifyLedgerForTest } from "../notify-ledger.ts";
36
30
  import { createRecord } from "../execution-record.ts";
37
31
  import { ModelConfigService } from "../model-config-service.ts";
@@ -95,8 +89,10 @@ describe("T4① notify gate closedReason whitelist", () => {
95
89
  expect(notifyGateAllowsDelivery(undefined)).toBe(true);
96
90
  });
97
91
 
98
- it("kickOffChatRound.then does not inject parent-new closed records into the session", async () => {
92
+ it("kickOffChatRound 应答回注不注入 parent-new closed records(notify 门白名单)", async () => {
99
93
  const { agentDir, service, pi } = setup();
94
+ clearEngines();
95
+ const fake = registerFakePiEngine();
100
96
  const record = createRecord("sa-gate-new", {
101
97
  agent: "general-purpose",
102
98
  model: "test/model",
@@ -107,34 +103,33 @@ describe("T4① notify gate closedReason whitelist", () => {
107
103
  rootSessionId: "root-session",
108
104
  controller: new AbortController(),
109
105
  });
110
- // 模拟 disposeAllRecords 先行编排性关闭后,迟到的 kickOffChatRound.then 回注
106
+ // 模拟 disposeAllRecords 先行编排性关闭后,迟到的 kickOffChatRound 应答回注
111
107
  record.closedReason = "parent-new";
112
- const stub = vi.fn().mockResolvedValue({
113
- text: "",
114
- turns: 0,
115
- durationMs: 1,
116
- success: false,
117
- error: "closed due to parent-new",
118
- sessionId: record.id,
119
- toolCalls: [],
120
- });
121
- (service as unknown as Record<string, unknown>).runAndFinalize = stub;
122
- // [D4 后形态] 轮次编排入口 kickOffChatRound(私有,bracket 调用先例见
123
- // subagent-service-recovery-bounds.test.ts privateFn);opts 形参仍是 ExecuteOptions
124
- //(task/slug),chat 分支由 ticket lossless 携带——task 声明形参不参与校验。
108
+ // [W3] 轮次编排入口 kickOffChatRound(私有,bracket 调用先例)——协议 run 发起后
109
+ // 挂起,编排性关闭先行,再模拟引擎应答(迟到回注被门拦)。
125
110
  const kickOffChatRound = (
126
111
  service as unknown as Record<string, (...args: unknown[]) => void>
127
112
  )["kickOffChatRound"];
128
- kickOffChatRound.call(service, record, { task: "t", slug: "gate" }, {}, {}, undefined, 1000, undefined);
129
- await vi.waitFor(() => expect(stub).toHaveBeenCalled());
113
+ const identity = {
114
+ agent: "general-purpose",
115
+ agentConfig: undefined,
116
+ resolved: { model: { id: "model", name: "Model", provider: "test", reasoning: false }, thinkingLevel: undefined },
117
+ };
118
+ kickOffChatRound.call(service, record, { task: "t", slug: "gate" }, identity, undefined, 1000);
119
+ await vi.waitFor(() => expect(fake.runs.length).toBe(1));
120
+ fake.runs[0]!.settle({ content: "late round text" });
130
121
  await Promise.resolve();
131
122
  await Promise.resolve();
132
123
  expect(pi.sendMessage).not.toHaveBeenCalled();
124
+ service.dispose();
125
+ clearEngines();
133
126
  fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
134
127
  });
135
128
 
136
- it("kickOffChatRound.then still notifies for real failure closures (gc)", async () => {
129
+ it("kickOffChatRound 应答回注对真实失败关闭(gc)仍通知", async () => {
137
130
  const { agentDir, service, pi } = setup();
131
+ clearEngines();
132
+ const fake = registerFakePiEngine();
138
133
  const record = createRecord("sa-gate-gc", {
139
134
  agent: "general-purpose",
140
135
  model: "test/model",
@@ -146,12 +141,20 @@ describe("T4① notify gate closedReason whitelist", () => {
146
141
  controller: new AbortController(),
147
142
  });
148
143
  record.closedReason = "gc";
149
- (service as unknown as Record<string, unknown>).runAndFinalize = vi.fn().mockResolvedValue({});
150
144
  const kickOffChatRound = (
151
145
  service as unknown as Record<string, (...args: unknown[]) => void>
152
146
  )["kickOffChatRound"];
153
- kickOffChatRound.call(service, record, { task: "t", slug: "gate" }, {}, {}, undefined, 1000, undefined);
147
+ const identity = {
148
+ agent: "general-purpose",
149
+ agentConfig: undefined,
150
+ resolved: { model: { id: "model", name: "Model", provider: "test", reasoning: false }, thinkingLevel: undefined },
151
+ };
152
+ kickOffChatRound.call(service, record, { task: "t", slug: "gate" }, identity, undefined, 1000);
153
+ await vi.waitFor(() => expect(fake.runs.length).toBe(1));
154
+ fake.runs[0]!.settle({ content: "round" });
154
155
  await vi.waitFor(() => expect(pi.sendMessage).toHaveBeenCalled());
156
+ service.dispose();
157
+ clearEngines();
155
158
  fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
156
159
  });
157
160
  });
@@ -25,15 +25,8 @@ const { loggerMock } = vi.hoisted(() => ({
25
25
  }));
26
26
  vi.mock("../../core/logger.ts", () => ({ getLogger: () => loggerMock }));
27
27
 
28
- // mock session-runner(import 链需要 runSpawn/killAllSpawnedChildren/getChildByRecord 存在)
29
- vi.mock("../engine/engines/pi/session-runner.ts", () => ({
30
- runSpawn: vi.fn(),
31
- killAllSpawnedChildren: vi.fn(),
32
- getChildByRecord: vi.fn(() => undefined),
33
- registerSpawnedChildForRecord: vi.fn(),
34
- killRecordChildWithEscalation: vi.fn(),
35
- spawnedChildren: new Map(),
36
- }));
28
+ // [W3 改写] 原 vi.mock(inproc pi 引擎目录/session-runner) 随删件消亡——本文件只测
29
+ // getRecordForAction 守卫(纯内存判定,不触引擎链路),现存 import 无需再 mock 引擎替身。
37
30
 
38
31
  import { createRecord } from "../execution-record.ts";
39
32
  import { ModelConfigService } from "../model-config-service.ts";
@@ -1,19 +1,23 @@
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 三条裸 SIGTERM
5
- // 收敛到 killRecordChildWithEscalation(spy 断言调用点与参数);
4
+ // - T2④/LC-2:closeChatIdle / closeAfterRoundSettled / cancelBackground 三条终止路径
5
+ // 收敛到 killRecordChildWithEscalation(spy 断言调用点与参数)+ [W3] 引擎侧终止
6
+ // 意图(协议 interact cancel/close fire-and-forget);
6
7
  // - T2⑥/PS-1:disposeAllRecords 补三回收面(controller.abort + kill + disarm idle timer
7
8
  // + disarm settled watchdog);
8
- // - T2③/LC-1 + D9:热路径投递 armMidRoundNoProgress(挂载点 D2 下沉后在编排层
9
- // deliverChatMessage 的 interact 返回点;挂载中段无进展检测 + 到期 onMidTimeout/
10
- // onSettleTimeout 处置:kill + 失败终态化 + error 含 'settled watchdog' 标记与恢复
11
- // 指引;agent_end 交棒收尾段的接线在 session-runner stdout pump,两路径共用);
12
- // - T2⑧/PS-3:非 EPIPE 热路径写失败 re-arm idle timer(进程不再裸奔;挂载位 D2
13
- // 下沉后在 PiEngine.deliverPrompt 的非 EPIPE catch——编排层无法区分错误类别)。
9
+ // - T2③/LC-1 + D9:热路径投递 armMidRoundNoProgress(挂载点在编排层
10
+ // deliverChatMessage 的 interact 受理点;中段无进展检测 + 到期处置:kill + 失败
11
+ // 终态化 + error 含 'settled watchdog' 标记与恢复指引)。
14
12
  //
15
- // mock 形态沿用 subagent-service-message-close.test.ts(真实 SubagentService + ServiceInternals
16
- // cast 暴露 store + mock session-runner/stdin-writer/logger)。
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 侧不再可等价改写。
18
+ //
19
+ // mock 形态:真实 SubagentService + ServiceInternals cast 暴露 store + logger +
20
+ // spawned-children 的 kill 入口换 spy(importOriginal 保留镜像真实现)。
17
21
 
18
22
  import * as fs from "node:fs";
19
23
  import * as os from "node:os";
@@ -26,37 +30,15 @@ const { loggerMock } = vi.hoisted(() => ({
26
30
  }));
27
31
  vi.mock("../../core/logger.ts", () => ({ getLogger: () => loggerMock }));
28
32
 
29
- // killRecordChildWithEscalation 换 spy(收敛调用点断言);spawnedChildren 用真 Map
30
- //(热路径投递判定 / EPIPE 兜底按值删除的消费者);getChildByRecord 从同一 Map
31
- //(mock 工厂与测试共享同一实例)。D2 后模块物理下沉 engines/pi/(mock 路径随迁)。
32
- const { killChildSpy, spawnedMap, getChildByRecordMock } = vi.hoisted(() => {
33
- const map = new Map<string, unknown>();
34
- return {
35
- killChildSpy: vi.fn(),
36
- spawnedMap: map,
37
- getChildByRecordMock: vi.fn((recordId: string) => map.get(recordId)),
38
- };
33
+ // killRecordChildWithEscalation 换 spy(收敛调用点断言);镜像真实现保留。
34
+ const { killChildSpy } = vi.hoisted(() => ({ killChildSpy: vi.fn() }));
35
+ vi.mock("../engine/host/spawned-children.ts", async (importOriginal) => {
36
+ const actual = await importOriginal<typeof import("../engine/host/spawned-children.ts")>();
37
+ return { ...actual, killRecordChildWithEscalation: killChildSpy };
39
38
  });
40
- vi.mock("../engine/engines/pi/session-runner.ts", () => ({
41
- runSpawn: vi.fn(),
42
- killAllSpawnedChildren: vi.fn(),
43
- getChildByRecord: getChildByRecordMock,
44
- registerSpawnedChildForRecord: vi.fn(),
45
- spawnedChildren: spawnedMap,
46
- killRecordChildWithEscalation: killChildSpy,
47
- }));
48
-
49
- // sendPromptCommand 换 spy(控制热路径写成功 / 抛非 EPIPE 错误)。
50
- const { sendPromptCommandMock } = vi.hoisted(() => ({ sendPromptCommandMock: vi.fn() }));
51
- vi.mock("../engine/engines/pi/stdin-writer.ts", () => ({
52
- sendPromptCommand: sendPromptCommandMock,
53
- clearEpipeFailure: vi.fn(),
54
- recordEpipeFailure: vi.fn(() => 1),
55
- resetAllEpipeFailures: vi.fn(),
56
- EPIPE_FAILURE_THRESHOLD: 3,
57
- }));
58
39
 
59
- import { FakeChild } from "./helpers/spawn-mock.ts";
40
+ import { clearEngines } from "../engine/registry.ts";
41
+ import { registerFakePiEngine, type FakePiEnginePort } from "./helpers/fake-engine-port.ts";
60
42
  import { createRecord } from "../execution-record.ts";
61
43
  import { ModelConfigService } from "../model-config-service.ts";
62
44
  import type { RecordStore } from "../record-store.ts";
@@ -64,26 +46,19 @@ import { SubagentService } from "../subagent-service.ts";
64
46
  import type { PiLike } from "../subagent-service.ts";
65
47
  import { armSettledWatchdog, hasSettledWatchdog, SETTLED_MID_ROUND_NO_PROGRESS_MS, _resetSettledWatchdogsForTest } from "../settled-watchdog.ts";
66
48
  import { armIdleTimer, hasIdleTimer, _resetLifecycleState } from "../lifecycle-manager.ts";
49
+ import { _resetCoreSpawnedChildrenMirrorForTest } from "../engine/host/spawned-children.ts";
67
50
  import type { ExecutionRecord } from "../types.ts";
68
51
 
69
52
  function makeTmpAgentDir(): string {
70
53
  return fs.mkdtempSync(path.join(os.tmpdir(), "svc-recovery-bounds-"));
71
54
  }
72
55
 
73
- function makePi(): PiLike & {
74
- appendEntry: ReturnType<typeof vi.fn>;
75
- events: { emit: ReturnType<typeof vi.fn> };
76
- sendMessage: ReturnType<typeof vi.fn>;
77
- } {
56
+ function makePi(): PiLike {
78
57
  return {
79
58
  appendEntry: vi.fn(),
80
59
  events: { emit: vi.fn() },
81
60
  sendMessage: vi.fn(),
82
- } as unknown as PiLike & {
83
- appendEntry: ReturnType<typeof vi.fn>;
84
- events: { emit: ReturnType<typeof vi.fn> };
85
- sendMessage: ReturnType<typeof vi.fn>;
86
- };
61
+ } as unknown as PiLike;
87
62
  }
88
63
 
89
64
  function makeRecord(overrides: Partial<ExecutionRecord> & { id?: string } = {}): ExecutionRecord {
@@ -109,14 +84,16 @@ interface ServiceInternals {
109
84
 
110
85
  type MockPi = ReturnType<typeof makePi>;
111
86
 
112
- function setup(): { agentDir: string; service: SubagentService; store: RecordStore; pi: MockPi } {
87
+ function setup(): { agentDir: string; service: SubagentService; store: RecordStore; pi: MockPi; fake: FakePiEnginePort } {
113
88
  const agentDir = makeTmpAgentDir();
89
+ clearEngines();
90
+ const fake = registerFakePiEngine();
114
91
  const modelService = new ModelConfigService({ agentDir, cwd: agentDir });
115
92
  const service = new SubagentService({ cwd: agentDir, modelService });
116
93
  const pi = makePi();
117
94
  service.initSession({ pi, sessionId: "root-session" });
118
95
  const store = (service as unknown as ServiceInternals).store;
119
- return { agentDir, service, store, pi };
96
+ return { agentDir, service, store, pi, fake };
120
97
  }
121
98
 
122
99
  /** bracket 调 private 方法(ServiceInternals cast 先例的扩展形态)。 */
@@ -125,8 +102,8 @@ function privateFn<K extends string>(service: SubagentService, key: K): (...args
125
102
  }
126
103
 
127
104
  /**
128
- * message 投递入口(D2 后形态:编排层私有 deliverChatMessage → PiEngine.interactRecord
129
- * → deliverPrompt;旧直调形态 deliverMessage 已随协议知识下沉删除)。
105
+ * message 投递入口([W3] 协议形态:编排层私有 deliverChatMessage → engine.interact(message);
106
+ * 冷路径 = engine_session_not_resumable run chat + resume)。
130
107
  */
131
108
  async function deliverChat(
132
109
  service: SubagentService,
@@ -149,40 +126,49 @@ describe("T2④ service-side kill convergence", () => {
149
126
  let service: SubagentService;
150
127
  let store: RecordStore;
151
128
  let pi: MockPi;
129
+ let fake: FakePiEnginePort;
152
130
 
153
131
  beforeEach(() => {
154
132
  vi.restoreAllMocks();
155
133
  killChildSpy.mockClear();
156
- sendPromptCommandMock.mockReset();
157
- ({ agentDir, service, store, pi } = setup());
134
+ ({ agentDir, service, store, pi, fake } = setup());
158
135
  });
159
136
 
160
137
  afterEach(() => {
138
+ service.dispose();
139
+ clearEngines();
161
140
  _resetLifecycleState();
162
141
  _resetSettledWatchdogsForTest();
163
- spawnedMap.clear();
142
+ _resetCoreSpawnedChildrenMirrorForTest();
164
143
  fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
165
144
  });
166
145
 
167
- it("cancelBackground (via cancel) routes through killRecordChildWithEscalation", async () => {
146
+ it("cancelBackground (via cancel) routes through killRecordChildWithEscalation + 引擎侧 cancel", async () => {
168
147
  const record = makeRecord({ id: "sa-cancel" });
169
148
  store.register(record);
170
- spawnedMap.set(record.id, new FakeChild());
171
149
  const ok = service.cancel(record.id);
172
150
  expect(ok).toBe(true);
173
151
  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
+ });
174
156
  });
175
157
 
176
158
  it("closeChatIdle (via closeSubagent force:false on idle) routes through killRecordChildWithEscalation and disarms timers", async () => {
177
159
  const record = makeRecord({ id: "sa-close-idle" });
178
160
  store.register(record);
179
- spawnedMap.set(record.id, new FakeChild());
180
161
  armIdleTimer(record.id, () => {}); // Path A:idle timer armed(进程保活)
181
162
  armSettledWatchdog(record.id, () => {});
182
163
  await service["closeSubagent"](record, false);
183
164
  expect(killChildSpy).toHaveBeenCalledWith(record.id, "closeChatIdle");
184
165
  expect(hasIdleTimer(record.id)).toBe(false);
185
166
  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
+ });
186
172
  });
187
173
 
188
174
  it("closeAfterRoundSettled routes through killRecordChildWithEscalation", async () => {
@@ -198,7 +184,6 @@ describe("T2④ service-side kill convergence", () => {
198
184
  const idle = makeRecord({ id: "sa-dispose-idle" });
199
185
  store.register(running);
200
186
  store.register(idle);
201
- spawnedMap.set(running.id, new FakeChild());
202
187
  armIdleTimer(idle.id, () => {});
203
188
  armSettledWatchdog(idle.id, () => {});
204
189
 
@@ -226,49 +211,42 @@ describe("T2③ hot-path settled watchdog", () => {
226
211
  let service: SubagentService;
227
212
  let store: RecordStore;
228
213
  let pi: MockPi;
214
+ let fake: FakePiEnginePort;
229
215
 
230
216
  beforeEach(() => {
231
217
  vi.restoreAllMocks();
232
218
  killChildSpy.mockClear();
233
- // 模拟真 kill 的语义面(置 killed 标记):isResumable 判定(无活进程)与失败通知
234
- // 放行依赖它——真实 kill 后 close 事件把句柄移出 spawnedChildren,语义相同。
235
- killChildSpy.mockImplementation((recordId: string) => {
236
- const c = spawnedMap.get(recordId) as FakeChild | undefined;
237
- if (c) c.killed = true;
238
- });
239
- sendPromptCommandMock.mockReset();
240
- sendPromptCommandMock.mockImplementation(() => {});
241
- ({ agentDir, service, store, pi } = setup());
219
+ ({ agentDir, service, store, pi, fake } = setup());
242
220
  });
243
221
 
244
222
  afterEach(() => {
223
+ service.dispose();
224
+ clearEngines();
245
225
  vi.useRealTimers();
246
226
  _resetLifecycleState();
247
227
  _resetSettledWatchdogsForTest();
248
- spawnedMap.clear();
228
+ _resetCoreSpawnedChildrenMirrorForTest();
249
229
  fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
250
230
  });
251
231
 
252
232
  it("arms the settled watchdog after a successful hot-path prompt", async () => {
253
233
  const record = makeRecord({ id: "sa-hot-arm" });
254
234
  store.register(record);
255
- spawnedMap.set(record.id, new FakeChild());
256
235
  await deliverChat(service, record, "hello", false);
257
- expect(sendPromptCommandMock).toHaveBeenCalledTimes(1);
236
+ expect(fake.interacts.length).toBe(1);
258
237
  expect(hasSettledWatchdog(record.id)).toBe(true);
259
238
  });
260
239
 
261
- it("onMidTimeout: kills the child, fails the round, error carries 'settled watchdog' marker and recovery hint", async () => {
240
+ it("onMidTimeout: kills the child (engine cancel), fails the round, error carries 'settled watchdog' marker and recovery hint", async () => {
262
241
  // fake timers 必须先于 arm 生效(useFakeTimers 不接管已存在的真实 timer)
263
242
  vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] });
264
243
  const record = makeRecord({ id: "sa-hot-timeout" });
265
244
  store.register(record);
266
- spawnedMap.set(record.id, new FakeChild());
267
245
  await deliverChat(service, record, "hello", false);
268
246
  expect(hasSettledWatchdog(record.id)).toBe(true);
269
247
 
270
- // [D9 两段式] 热路径 prompt 发出后挂中段:静默满中段窗长触发(本测试 mock 了
271
- // session-runner,无 stdout pump 驱动事件刷新/交棒,中段静默形态直达)
248
+ // [D9 两段式] 热路径 prompt 发出后挂中段:静默满中段窗长触发(本测试替身不驱动
249
+ // 协议事件,中段静默形态直达)
272
250
  await vi.advanceTimersByTimeAsync(SETTLED_MID_ROUND_NO_PROGRESS_MS + 1);
273
251
 
274
252
  // kill 收敛入口被触发(onTimeout 第一步)
@@ -283,59 +261,11 @@ describe("T2③ hot-path settled watchdog", () => {
283
261
  expect(record.result).toContain("action:'list'");
284
262
  // 失败通知送达(内核直发路径)
285
263
  expect(pi.sendMessage).toHaveBeenCalled();
286
- const notifyContent = (pi.sendMessage.mock.calls[0]?.[0] as { content?: string } | undefined)?.content ?? "";
264
+ const sendMessageCalls = (pi.sendMessage as unknown as ReturnType<typeof vi.fn>).mock.calls as Array<[{ content?: string }]>;
265
+ const notifyContent = sendMessageCalls[0]?.[0]?.content ?? "";
287
266
  expect(notifyContent).toContain("settled watchdog");
288
- });
289
- });
290
-
291
- describe("T2⑧ non-EPIPE hot-path failure re-arms idle timer", () => {
292
- let agentDir: string;
293
- let service: SubagentService;
294
- let store: RecordStore;
295
-
296
- beforeEach(() => {
297
- vi.restoreAllMocks();
298
- killChildSpy.mockClear();
299
- ({ agentDir, service, store } = setup());
300
- });
301
-
302
- afterEach(() => {
303
- _resetLifecycleState();
304
- _resetSettledWatchdogsForTest();
305
- spawnedMap.clear();
306
- fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
307
- });
308
-
309
- it("re-arms the idle timer (default duration) and rethrows on non-EPIPE write failure", async () => {
310
- const record = makeRecord({ id: "sa-nonpipe" });
311
- store.register(record);
312
- spawnedMap.set(record.id, new FakeChild());
313
- sendPromptCommandMock.mockImplementation(() => {
314
- throw new Error("write after end: ERR_STREAM_DESTROYED");
315
- });
316
-
317
- await expect(deliverChat(service, record, "hello", false)).rejects.toThrow(/ERR_STREAM_DESTROYED/);
318
-
319
- // 防泄漏前提恢复:入口 disarm 过的 idle timer 被 re-arm(进程不再裸奔)
320
- expect(hasIdleTimer(record.id)).toBe(true);
321
- expect(loggerMock.warn).toHaveBeenCalledWith(
322
- expect.stringContaining("idle timer re-armed"),
323
- expect.objectContaining({ detail: expect.stringContaining("ERR_STREAM_DESTROYED") }),
324
- );
325
- });
326
-
327
- it("falls back to DEFAULT idle duration (visible) when record.idleTimeoutMs is invalid", async () => {
328
- const record = makeRecord({ id: "sa-nonpipe-bad" });
329
- // 直接注入非法值模拟「入口校验前已存在的 record」/ 回归形态:re-arm 首选值非法 →
330
- // 降级挂 DEFAULT(防御性兜底可见,不静默不挂)
331
- (record as { idleTimeoutMs: number }).idleTimeoutMs = 3_000_000_000;
332
- store.register(record);
333
- spawnedMap.set(record.id, new FakeChild());
334
- sendPromptCommandMock.mockImplementation(() => {
335
- throw new Error("ERR_STREAM_DESTROYED");
336
- });
337
-
338
- await expect(deliverChat(service, record, "hello", false)).rejects.toThrow(/ERR_STREAM_DESTROYED/);
339
- expect(hasIdleTimer(record.id)).toBe(true);
267
+ // [W3] 引擎侧终止意图(fire-and-forget;fake timers 下只断言受理动作已发出)
268
+ const cancel = fake.interacts.find((c) => c.action.kind === "cancel");
269
+ expect(cancel).toBeDefined();
340
270
  });
341
271
  });
@@ -323,7 +323,7 @@ describe("SubagentService", () => {
323
323
  // [背景] Pi 单进程 session 串行接管。session A shutdown 发 SIGTERM 后、子进程彻底
324
324
  // close 前(graceful shutdown 窗口几十~几百 ms),子进程的 trailing extension_ui_request
325
325
  // 仍会被父进程 pump 解析,调到 A 的 handler 闭包(仍持有 A 的 ctx)。旧实现 dispose 不清
326
- // uiRequestHandler,该闭包触发 ui-request-queue.ts catch 分支打 `[subagents] uiRequestHandler
326
+ // uiRequestHandler,该闭包触发 inproc UI 请求队列(已删) catch 分支打 `[subagents] uiRequestHandler
327
327
  // threw` 误导性 console.error。dispose 现注入 stub(始终返回 {cancelled:true}),让 trailing
328
328
  // ui_request 干净降级,不再走旧 handler 闭包。
329
329
  //
@@ -376,7 +376,7 @@ describe("SubagentService", () => {
376
376
 
377
377
  // 关键价值断言:无 "[subagents] uiRequestHandler threw" 误导性噪声
378
378
  // (旧实现 dispose 不清 handler 时,trailing ui_request 走 staleHandler 抛错 →
379
- // ui-request-queue.ts catch 打该日志。stub 覆盖后该路径不再触发。)
379
+ // inproc UI 请求队列(已删) catch 打该日志。stub 覆盖后该路径不再触发。)
380
380
  const subagentsErrors = errorSpy.mock.calls.filter(
381
381
  (args: unknown[]) => typeof args[0] === "string" && args[0].includes("[subagents]"),
382
382
  );
@@ -15,11 +15,16 @@ import * as path from "node:path";
15
15
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
16
16
 
17
17
  import type { AgentCallOpts, AgentResult } from "../../orchestration/models/types.ts";
18
- import type { RunContext, EnginePort } from "../engine/port.ts";
18
+ import type { EnginePort, RunContext } from "../engine/port.ts";
19
19
  import { clearEngines, registerEngine } from "../engine/registry.ts";
20
- import type { ProbeReport } from "../engine/types.ts";
20
+ import type { AgentOutcome, EngineCapabilities, ProbeReport } from "../engine/types.ts";
21
21
  import { ModelConfigService, setModelConfigService } from "../model-config-service.ts";
22
- import { getChildByRecord } from "../engine/engines/pi/session-runner.ts";
22
+ // W10(§2.10 ②):子进程句柄断言改读 core 侧状态镜像(host/spawned-children——
23
+ // 协议化后 spawnedChildren 持有方在引擎进程,core 消费镜像面;判据 pid 同构)。
24
+ import {
25
+ coreSpawnedChildrenMirror,
26
+ _resetCoreSpawnedChildrenMirrorForTest,
27
+ } from "../engine/host/spawned-children.ts";
23
28
  import { SubprocessAgentRunner } from "../subprocess-agent-runner.ts";
24
29
  import type { SubagentService } from "../subagent-service.ts";
25
30
  import type { ExecuteOptions } from "../types.ts";
@@ -88,13 +93,86 @@ function makeMockPiService() {
88
93
  executeOpts.push(opts);
89
94
  return { content: "from-pi", durationMs: 1, toolCalls: [] };
90
95
  });
91
- // [D4 聚合连带] SAR 构造器经 asEngineService 显式视图取 PiEngineService——fake 的
96
+ // [D4 聚合连带] SAR 构造器经 asEngineService 显式视图取引擎服务面——fake 的
92
97
  // face 即自身,getter 直接返回 self。
93
98
  const service = { executeAndAwait } as unknown as SubagentService & { asEngineService: unknown };
94
99
  (service as { asEngineService: unknown }).asEngineService = service;
100
+ // [W3] 登记当前 mock 服务——委托式替身 port 的工厂闭包按它路由(getEngine 惰性)。
101
+ currentPiService = service as unknown as SubagentService;
95
102
  return { service, executeOpts, executeAndAwait };
96
103
  }
97
104
 
105
+ // ── [W3] 委托式替身 pi port(原 inproc PiEngine 的 workflow 分支形态)──
106
+ // run() 还原引擎内 task→ExecuteOptions 映射(含 engine='pi' 留痕与 engineFallback
107
+ // 透传——两断言的契约面)后委托 mock 服务的 executeAndAwait。
108
+
109
+ let currentPiService: SubagentService | undefined;
110
+
111
+ function piTaskToExecuteOptions(task: AgentCallOpts, ctx: RunContext): ExecuteOptions {
112
+ const rawSlug = task.description ?? task.agent ?? "workflow-agent";
113
+ return {
114
+ task: task.prompt,
115
+ slug: rawSlug.length > 35 ? rawSlug.slice(0, 35) : rawSlug,
116
+ agent: task.agent,
117
+ model: task.model,
118
+ thinkingLevel: task.thinkingLevel,
119
+ skillPath: task.skillPath,
120
+ appendSystemPrompt: task.appendSystemPrompt,
121
+ schema: task.schema,
122
+ schemaEnv: task.schemaEnv,
123
+ maxTurns: task.maxTurns,
124
+ graceTurns: task.graceTurns,
125
+ ctxModel: ctx.ctxModel,
126
+ fork: task.fork,
127
+ worktree: task.worktree,
128
+ cwd: task.cwd,
129
+ conversation: task.conversation,
130
+ idleTimeoutMs: task.idleTimeoutMs,
131
+ // P4 引擎留痕(D9①):实际执行引擎 id 恒 pi(原 PiEngine.run workflow 分支同款)
132
+ engine: "pi",
133
+ ...(ctx.engineFallback !== undefined ? { engineFallback: ctx.engineFallback } : {}),
134
+ };
135
+ }
136
+
137
+ function makeDelegatingPiPort(getService: () => SubagentService | undefined): EnginePort {
138
+ return {
139
+ id: "pi",
140
+ capabilities: (): EngineCapabilities => ({
141
+ schemaEnforcement: "native",
142
+ steer: "unsupported",
143
+ conversation: "native",
144
+ personaInjection: "flag",
145
+ eventGranularity: "stream",
146
+ sandbox: "emulated",
147
+ sessionRead: "full",
148
+ resume: "native",
149
+ interrupt: "kill-only",
150
+ permissionMode: "native",
151
+ maxTurns: true,
152
+ }),
153
+ probe: async (): Promise<ProbeReport> => ({ ok: true, engineVersion: "fake", checks: [] }),
154
+ run: async (task, ctx) => {
155
+ const service = getService();
156
+ if (service === undefined) throw new Error("mock pi service not installed");
157
+ const result = await (service as unknown as {
158
+ executeAndAwait: (opts: ExecuteOptions, signal?: AbortSignal) => Promise<AgentResult>;
159
+ }).executeAndAwait(piTaskToExecuteOptions(task, ctx), ctx.signal);
160
+ const outcome: AgentOutcome = {
161
+ content: result.content,
162
+ durationMs: result.durationMs,
163
+ error: result.error,
164
+ engineId: "pi",
165
+ };
166
+ return {
167
+ handle: { data: { v: 1, engineId: "pi", sessionRef: {}, poolKey: "shared", adapterVersion: "fake-sar" } },
168
+ outcome,
169
+ };
170
+ },
171
+ interact: async () => ({ ok: false, code: "engine_interact_failed", message: "not supported in this test" }),
172
+ read: async () => ({ engineId: "pi", turns: [], source: "outcome-only" }),
173
+ };
174
+ }
175
+
98
176
  // ── 环境 ──
99
177
 
100
178
  let tmpRoot: string;
@@ -128,6 +206,7 @@ function installModelService(cfg?: Record<string, unknown>): void {
128
206
  }
129
207
 
130
208
  beforeEach(() => {
209
+ _resetCoreSpawnedChildrenMirrorForTest();
131
210
  tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "sar-routing-"));
132
211
  agentDir = path.join(tmpRoot, "pi-agent");
133
212
  fs.mkdirSync(agentDir, { recursive: true });
@@ -135,6 +214,10 @@ beforeEach(() => {
135
214
  prevDataDirEnv = process.env["XYZ_AGENT_DATA_DIR"];
136
215
  process.env["XYZ_AGENT_DATA_DIR"] = path.join(tmpRoot, "engine-data");
137
216
  clearEngines();
217
+ currentPiService = undefined;
218
+ // [U-2 一致性修复 → W3] resolveHostPiEnginePort 对已注册 port 原样返回——登记
219
+ // 委托式替身 pi port(registry 命中即路由命中;mock 服务的 DI 语义保持不变)。
220
+ registerEngine("pi", () => makeDelegatingPiPort(() => currentPiService));
138
221
  });
139
222
 
140
223
  afterEach(() => {
@@ -327,10 +410,11 @@ describe("SAR 路由集成(P4 验收 1/2/3)", () => {
327
410
  // 引擎回调(真实引擎在 spawn 成功后同步调)→ 按 taskId('sa-' 记账 key)注册可见
328
411
  const child = spawn(process.execPath, ["-e", ""]);
329
412
  ctx?.onChildSpawned?.(child);
330
- expect(getChildByRecord(ctx.taskId)).toBe(child);
331
- // 子进程退出(真实 close 事件)后记账按句移除——不残留死句柄
413
+ expect(coreSpawnedChildrenMirror().getChildByRecord(ctx.taskId)?.pid).toBe(child.pid);
414
+ // 按句移除断言(W10 注):inproc 双模下子进程退出不回灌 host/childStateChanged
415
+ // (该通道是 cli 协议形态专属)——镜像移除断言由 protocol-blackbox 的
416
+ // childStateChanged 用例承载;此处等待 close 仅保证进程面收口不悬挂。
332
417
  await new Promise<void>((resolve) => child.once("close", () => resolve()));
333
- expect(getChildByRecord(ctx.taskId)).toBeUndefined();
334
418
  });
335
419
 
336
420
  // ── [D3-④] SAR 路径预检(capabilities 驱动;唯一有意行为变化 = zcode+worktree)──
@@ -384,3 +468,21 @@ describe("SAR 路由集成(P4 验收 1/2/3)", () => {
384
468
  expect(pi.executeOpts[0]?.maxTurns).toBe(3);
385
469
  });
386
470
  });
471
+
472
+ // [U-2 一致性修复] D4 错误契约:pi 未注册(引擎包未装/发现失败)时普通 run 路径
473
+ // 不再静默直构 inproc——派发期显式 engine_not_found + 安装指引(与 zcode 对称)。
474
+ describe("pi 未注册 → engine_not_found(D4 契约,U-2 修复锚定)", () => {
475
+ it("pi 不在 registry → run 结果 error 含 engine_not_found + 安装指引,不静默走 inproc", async () => {
476
+ clearEngines(); // 覆盖 beforeEach 的 inproc pi 登记——模拟发现失败/未装包
477
+ installModelService();
478
+ const pi = makeMockPiService();
479
+ const sar = new SubprocessAgentRunner({ subagentService: pi.service });
480
+
481
+ const result = await sar.run(makeOpts(), new AbortController().signal);
482
+
483
+ expect(result.error).toContain("engine_not_found");
484
+ expect(result.error).toContain("No engine packages were discovered");
485
+ expect(result.error).toMatch(/install an engine package/i);
486
+ expect(pi.executeAndAwait).not.toHaveBeenCalled();
487
+ });
488
+ });