@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
@@ -27,7 +27,6 @@ import {
27
27
  finalizedMarkerModule,
28
28
  fsSyncModule,
29
29
  manifestStoreModule,
30
- tempPromptModule,
31
30
  } from "./helpers/subagent-service-mocks.ts";
32
31
 
33
32
  // ── mock modules(同 execute-nesting.test.ts)──
@@ -37,11 +36,11 @@ vi.mock("node:fs", async (importOriginal) => fsSyncModule(await importOriginal<t
37
36
  vi.mock("../alive-store.ts", async (importOriginal) => aliveStoreModule(await importOriginal<typeof import("../alive-store.ts")>()));
38
37
  vi.mock("../finalized-marker.ts", () => finalizedMarkerModule());
39
38
  vi.mock("../manifest-store.ts", () => manifestStoreModule());
40
- vi.mock("../engine/engines/pi/temp-prompt.ts", () => tempPromptModule());
41
39
 
42
40
  import { spawn } from "node:child_process";
43
41
 
44
- import { waitForSpawn, lastSpawnedChild } from "./helpers/spawn-mock.ts";
42
+ import { registerFakePiEngine, type FakePiEnginePort } from "./helpers/fake-engine-port.ts";
43
+ import { clearEngines } from "../engine/registry.ts";
45
44
  import { ModelConfigService } from "../model-config-service.ts";
46
45
  import type { ModelInfo, ModelRegistryLike } from "../model-resolver.ts";
47
46
  import { ManifestStore } from "../manifest-store.ts";
@@ -62,6 +61,13 @@ function getLastSpawnEnv(): Record<string, string | undefined> {
62
61
  return (mockSpawn.mock.calls.at(-1)?.[2]?.env as Record<string, string | undefined>) ?? {};
63
62
  }
64
63
 
64
+ /** [W3] 协议替身:execute → engine.run 挂起不 settle——record 保持 running 在 store
65
+ * 可观察(原 FakeChild 挂起不推进的等价形态)。 */
66
+ function registerHangingEngine(): FakePiEnginePort {
67
+ clearEngines();
68
+ return registerFakePiEngine();
69
+ }
70
+
65
71
  // ── 辅助:service 构造 ──
66
72
 
67
73
  function makeEmptyRegistry(): ModelRegistryLike {
@@ -122,6 +128,7 @@ describe("进程级基线兜底(ALS 断裂修复,pi 事件回调模型)",
122
128
  process.env[ENV_DEPTH] = "1";
123
129
 
124
130
  const { service, store } = setup({});
131
+ registerHangingEngine();
125
132
 
126
133
  // 不在 execCtxAls.run 内调用(模拟 ALS 断裂:事件回调上下文读不到 store)
127
134
  const handle = await service.execute({ task: "child of parent", slug: "test", ctxModel });
@@ -143,6 +150,7 @@ describe("进程级基线兜底(ALS 断裂修复,pi 事件回调模型)",
143
150
 
144
151
  it("[顶层] 无 env(根进程):parentRecordId undefined / depth 0", async () => {
145
152
  const { service, store } = setup({});
153
+ registerHangingEngine();
146
154
 
147
155
  const handle = await service.execute({ task: "top level", slug: "test", ctxModel });
148
156
 
@@ -158,6 +166,7 @@ describe("进程级基线兜底(ALS 断裂修复,pi 事件回调模型)",
158
166
  process.env[ENV_DEPTH] = "0";
159
167
 
160
168
  const { service, store } = setup({});
169
+ registerHangingEngine();
161
170
  const execNesting = Reflect.get(service, "execNesting") as ExecCtxAls;
162
171
 
163
172
  const handle = await execNesting.run({ recordId: "sa-inline-parent", depth: 3 }, () =>
@@ -170,24 +179,22 @@ describe("进程级基线兜底(ALS 断裂修复,pi 事件回调模型)",
170
179
  expect(rec!.depth).toBe(4);
171
180
  });
172
181
 
173
- it("[forkDepth 基线] env PI_SUBAGENT_FORK_DEPTH=1 + fork:spawn env 递增为 2(读点兜底基线生效)", async () => {
182
+ it("[forkDepth 基线] env PI_SUBAGENT_FORK_DEPTH=1 initSession 建立基线(读点兜底基线生效)", async () => {
183
+ // [W3 改写] spawn env 注入(PI_SUBAGENT_FORK_DEPTH 递增写子 env)属 inproc
184
+ // runSpawn 机制,随 inproc pi 引擎目录 删除迁入引擎包(pi-subagent-cli spawn 链);core
185
+ // 侧存留语义 = initSession 从 env 建立基线(ALS 断裂时的读点兜底),本用例锁定它。
174
186
  process.env[ENV_ROOT_SESSION_ID] = "root-main";
175
187
  process.env[ENV_SELF_RECORD_ID] = "sa-fork-parent";
176
188
  process.env[ENV_DEPTH] = "0";
177
189
  process.env[ENV_FORK_DEPTH] = "1";
178
190
 
179
191
  const { service } = setup({});
192
+ registerHangingEngine();
180
193
 
181
- const execPromise = service.execute({ task: "fork child", slug: "test", ctxModel, fork: true });
182
- await waitForSpawn(mockSpawn);
183
- const childEnv = getLastSpawnEnv();
184
-
185
- // 742 行 parentDepth = forkDepthAls.getStore() ?? forkDepthBaseline —— ALS 断裂时基线=1,+1 → 2
186
- expect(childEnv.PI_SUBAGENT_FORK_DEPTH).toBe("2");
194
+ expect(Reflect.get(service, "forkDepthBaseline")).toBe(1);
187
195
 
188
- const child = lastSpawnedChild(mockSpawn);
189
- child.emit("close", 0);
190
- await execPromise;
196
+ // fork 请求照常派发(引擎侧承载深度传递)——不因基线读取抛错
197
+ await expect(service.execute({ task: "fork child", slug: "test", ctxModel, fork: true })).resolves.toBeDefined();
191
198
  });
192
199
 
193
200
  it("[嵌套护栏] 基线 depth 参与 execute 入口嵌套护栏(depth>MAX 拒绝)", async () => {
@@ -0,0 +1,293 @@
1
+ // src/execution/__tests__/round-supervisor.test.ts
2
+ //
3
+ // [W4] 轮次活性监督器单测:三态判定(该等/该唤醒/该放弃)、纳管模型(死亡事件
4
+ // 纳管、重建不解管——镜像置死只作触发信号不作持续判据)、boot 分区(重认领 vs
5
+ // conversation 豁免 vs in-flight 跳过)、通知对账两级启发式(高置信撤销改送终止
6
+ // 通知 / 低置信豁免声明)。timer 用 fake timers;deps 全部替身注入(不触真实
7
+ // 数据目录——监督器对 fs 零依赖)。
8
+
9
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
10
+
11
+ import type { ExecutionRecord } from "../types.ts";
12
+ import { RoundSupervisor, ROUND_SUPERVISOR_WATCHDOG_DEFAULT_MS, type RoundSupervisorDeps, type SupervisorCandidateRecord, type SupervisorRecordView } from "../round-supervisor/index.ts";
13
+
14
+ function makeDeps(overrides: Partial<RoundSupervisorDeps> = {}): RoundSupervisorDeps & {
15
+ notices: string[];
16
+ guidances: Array<{ record: SupervisorRecordView; exemptionDisclaimer: boolean }>;
17
+ replaced: Array<{ record: SupervisorRecordView; replacementId: string }>;
18
+ givenUp: Array<{ recordId: string; kind: string; detail: { replacementId?: string } }>;
19
+ views: Map<string, SupervisorRecordView>;
20
+ candidates: SupervisorCandidateRecord[];
21
+ live: Set<string>;
22
+ } {
23
+ const views = new Map<string, SupervisorRecordView>();
24
+ const candidates: SupervisorCandidateRecord[] = [];
25
+ const live = new Set<string>();
26
+ const notices: string[] = [];
27
+ const guidances: Array<{ record: SupervisorRecordView; exemptionDisclaimer: boolean }> = [];
28
+ const replaced: Array<{ record: SupervisorRecordView; replacementId: string }> = [];
29
+ const givenUp: Array<{ recordId: string; kind: string; detail: { replacementId?: string } }> = [];
30
+ const deps: RoundSupervisorDeps = {
31
+ now: () => Date.now(),
32
+ getRecordView: (id) => views.get(id),
33
+ listCandidateRecords: () => candidates,
34
+ hasLiveProcess: (recordId) => live.has(recordId),
35
+ sendMergedFailureNotice: (record) => notices.push(`merged:${record.id}`),
36
+ sendDecisionGuidance: (record, opts) => guidances.push({ record, exemptionDisclaimer: opts.exemptionDisclaimer }),
37
+ sendReplacedNotice: (record, replacementId) => replaced.push({ record, replacementId }),
38
+ giveUp: (recordId, kind, detail) => givenUp.push({ recordId, kind, detail }),
39
+ };
40
+ return Object.assign(deps, { notices, guidances, replaced, givenUp, views, candidates, live });
41
+ }
42
+
43
+ /** 内存 record 替身(adoptOnProcessDeath 入参形态——字段访问面最小化)。 */
44
+ function makeRecord(overrides: Partial<ExecutionRecord> = {}): ExecutionRecord {
45
+ return {
46
+ id: "bg-1",
47
+ agent: "worker",
48
+ model: "m",
49
+ mode: "background",
50
+ task: "t",
51
+ slug: "fix-bug",
52
+ startedAt: Date.now() - 1000,
53
+ status: "running",
54
+ resumable: true,
55
+ rootSessionId: "sess-root",
56
+ turnCount: 0,
57
+ ...overrides,
58
+ } as ExecutionRecord;
59
+ }
60
+
61
+ describe("RoundSupervisor 三态判定", () => {
62
+ beforeEach(() => {
63
+ vi.useFakeTimers();
64
+ });
65
+ afterEach(() => {
66
+ vi.useRealTimers();
67
+ });
68
+
69
+ it("该等:有活进程驱动 → 不干预(无指引、无放弃)", () => {
70
+ const deps = makeDeps();
71
+ const supervisor = new RoundSupervisor(deps);
72
+ deps.views.set("bg-1", { id: "bg-1", status: "running", resumable: true, hasResult: false, chatMode: false, rootSessionId: "r", agent: "worker", slug: "s", startedAt: 1, closedReason: undefined });
73
+ deps.live.add("bg-1");
74
+ supervisor.adoptOnProcessDeath(makeRecord(), "boom");
75
+ expect(deps.guidances).toHaveLength(0);
76
+ expect(deps.givenUp).toHaveLength(0);
77
+ });
78
+
79
+ it("该等:在途 run 记账(noteRunStarted)→ 不干预;noteRunEnded 后重评估", () => {
80
+ const deps = makeDeps();
81
+ const supervisor = new RoundSupervisor(deps);
82
+ deps.views.set("bg-1", { id: "bg-1", status: "running", resumable: true, hasResult: false, chatMode: false, rootSessionId: "r", agent: "worker", slug: "s", startedAt: 1, closedReason: undefined });
83
+ supervisor.noteRunStarted("bg-1");
84
+ supervisor.adoptOnProcessDeath(makeRecord(), "boom");
85
+ expect(deps.guidances).toHaveLength(0);
86
+ supervisor.noteRunEnded("bg-1");
87
+ expect(deps.guidances).toHaveLength(1);
88
+ });
89
+
90
+ it("该唤醒:resumable 未终态 且 无在途 run/无进程驱动 → 决策指引(一窗一次)+ 看门狗 armed", () => {
91
+ const deps = makeDeps();
92
+ const supervisor = new RoundSupervisor(deps);
93
+ deps.views.set("bg-1", { id: "bg-1", status: "running", resumable: true, hasResult: false, chatMode: false, rootSessionId: "r", agent: "worker", slug: "s", startedAt: 1, closedReason: undefined });
94
+ supervisor.adoptOnProcessDeath(makeRecord(), "engine crashed");
95
+ expect(deps.notices).toEqual(["merged:bg-1"]); // 合并单条通知(failed 如实 + 接管契约)
96
+ expect(deps.guidances).toHaveLength(1);
97
+ expect(deps.guidances[0].exemptionDisclaimer).toBe(false);
98
+ // 重复死亡事件不重复通知、不重复指引(幂等)
99
+ supervisor.adoptOnProcessDeath(makeRecord(), "again");
100
+ expect(deps.notices).toHaveLength(1);
101
+ expect(deps.guidances).toHaveLength(1);
102
+ expect(supervisor.supervisedIds()).toEqual(["bg-1"]);
103
+ });
104
+
105
+ it("[重建不解管] 镜像重填再置死(引擎被动重建)不翻转判定:指引仍只送一次、纳管持续", () => {
106
+ const deps = makeDeps();
107
+ const supervisor = new RoundSupervisor(deps);
108
+ deps.views.set("bg-1", { id: "bg-1", status: "running", resumable: true, hasResult: false, chatMode: false, rootSessionId: "r", agent: "worker", slug: "s", startedAt: 1, closedReason: undefined });
109
+ // 死亡事件纳管时镜像已置死(hasLiveProcess=false)→ 该唤醒。
110
+ supervisor.adoptOnProcessDeath(makeRecord(), "engine crashed");
111
+ expect(deps.guidances).toHaveLength(1);
112
+ // 模拟引擎被动重建:任一会话的下个请求触发 ensureConnected → 镜像重填(live=true)
113
+ deps.live.add("bg-1");
114
+ supervisor.noteRunEnded("bg-1"); // 重建后的重评估——镜像判「该等」仅解除看门狗
115
+ expect(deps.guidances).toHaveLength(1); // 指引不重发(已在窗内)
116
+ expect(supervisor.supervisedIds()).toEqual(["bg-1"]); // 纳管未解除(重建不解管)
117
+ // 镜像再置死(重建的进程又死):record 级状态未变 → 监督器继续管辖(不重复指引)
118
+ deps.live.delete("bg-1");
119
+ supervisor.noteRunEnded("bg-1");
120
+ expect(deps.guidances).toHaveLength(1);
121
+ expect(supervisor.supervisedIds()).toEqual(["bg-1"]);
122
+ });
123
+
124
+ it("已有完成产出(SP-5 upgrade 等待态)→ 不唤醒", () => {
125
+ const deps = makeDeps();
126
+ const supervisor = new RoundSupervisor(deps);
127
+ deps.views.set("bg-1", { id: "bg-1", status: "running", resumable: true, hasResult: true, chatMode: false, rootSessionId: "r", agent: "worker", slug: "s", startedAt: 1, closedReason: undefined });
128
+ supervisor.adoptOnProcessDeath(makeRecord({ result: "done text" }), "x");
129
+ expect(deps.guidances).toHaveLength(0);
130
+ });
131
+
132
+ it("conversation 形态(chatMode)豁免:不纳管不通知", () => {
133
+ const deps = makeDeps();
134
+ const supervisor = new RoundSupervisor(deps);
135
+ supervisor.adoptOnProcessDeath(makeRecord({ chatMode: true, resumable: true }), "x");
136
+ expect(deps.notices).toHaveLength(0);
137
+ expect(supervisor.supervisedIds()).toEqual([]);
138
+ });
139
+
140
+ it("该放弃:决策看门狗到期 → giveUp(watchdog-expired) 并解除纳管;record 已终态则跳过", async () => {
141
+ const deps = makeDeps();
142
+ const supervisor = new RoundSupervisor(deps);
143
+ deps.views.set("bg-1", { id: "bg-1", status: "running", resumable: true, hasResult: false, chatMode: false, rootSessionId: "r", agent: "worker", slug: "s", startedAt: 1, closedReason: undefined });
144
+ supervisor.adoptOnProcessDeath(makeRecord(), "x");
145
+ expect(deps.givenUp).toHaveLength(0);
146
+ await vi.advanceTimersByTimeAsync(ROUND_SUPERVISOR_WATCHDOG_DEFAULT_MS + 1);
147
+ expect(deps.givenUp).toEqual([{ recordId: "bg-1", kind: "watchdog-expired", detail: {} }]);
148
+ expect(supervisor.supervisedIds()).toEqual([]);
149
+ });
150
+
151
+ it("看门狗等待期内 run 恢复(决策收敛)→ 解除看门狗,不放弃", async () => {
152
+ const deps = makeDeps();
153
+ const supervisor = new RoundSupervisor(deps);
154
+ deps.views.set("bg-1", { id: "bg-1", status: "running", resumable: true, hasResult: false, chatMode: false, rootSessionId: "r", agent: "worker", slug: "s", startedAt: 1, closedReason: undefined });
155
+ supervisor.adoptOnProcessDeath(makeRecord(), "x");
156
+ supervisor.noteRunStarted("bg-1"); // 主 agent resume = 决策收敛
157
+ await vi.advanceTimersByTimeAsync(ROUND_SUPERVISOR_WATCHDOG_DEFAULT_MS + 1);
158
+ expect(deps.givenUp).toHaveLength(0);
159
+ });
160
+
161
+ it("[F5] 挂账转完成(SP-5 result 回填)→ 解除看门狗:到期不误 giveUp 已完成挂账 record", async () => {
162
+ const deps = makeDeps();
163
+ const supervisor = new RoundSupervisor(deps);
164
+ deps.views.set("bg-1", { id: "bg-1", status: "running", resumable: true, hasResult: false, chatMode: false, rootSessionId: "r", agent: "worker", slug: "s", startedAt: 1, closedReason: undefined });
165
+ supervisor.adoptOnProcessDeath(makeRecord(), "x"); // 该唤醒 → 指引 + 看门狗 armed
166
+ expect(deps.guidances).toHaveLength(1);
167
+ // upgrade 完成 → result 回填(挂账态),重评估触发(noteRunEnded 携带视图更新)
168
+ deps.views.set("bg-1", { id: "bg-1", status: "running", resumable: true, hasResult: true, chatMode: false, rootSessionId: "r", agent: "worker", slug: "s", startedAt: 1, closedReason: undefined });
169
+ supervisor.noteRunEnded("bg-1");
170
+ // armed 的看门狗必须已解除——2h 到期不得把已完成挂账 record 判死
171
+ await vi.advanceTimersByTimeAsync(ROUND_SUPERVISOR_WATCHDOG_DEFAULT_MS + 1);
172
+ expect(deps.givenUp).toHaveLength(0);
173
+ expect(supervisor.supervisedIds()).toEqual(["bg-1"]); // 挂账态保持纳管(归 idle-gc 收口)
174
+ });
175
+
176
+ it("env ≤0 关闭该放弃路径:只指引永不放弃(回收层 opt-out)", async () => {
177
+ vi.stubEnv("XYZ_SUBAGENT_ROUND_SUPERVISOR_WATCHDOG_MS", "0");
178
+ RoundSupervisor._resetEnvCacheForTest();
179
+ try {
180
+ const deps = makeDeps();
181
+ const supervisor = new RoundSupervisor(deps);
182
+ deps.views.set("bg-1", { id: "bg-1", status: "running", resumable: true, hasResult: false, chatMode: false, rootSessionId: "r", agent: "worker", slug: "s", startedAt: 1, closedReason: undefined });
183
+ supervisor.adoptOnProcessDeath(makeRecord(), "x");
184
+ expect(deps.guidances).toHaveLength(1);
185
+ await vi.advanceTimersByTimeAsync(ROUND_SUPERVISOR_WATCHDOG_DEFAULT_MS * 3);
186
+ expect(deps.givenUp).toHaveLength(0);
187
+ } finally {
188
+ vi.unstubAllEnvs();
189
+ RoundSupervisor._resetEnvCacheForTest();
190
+ }
191
+ });
192
+ });
193
+
194
+ describe("RoundSupervisor boot 分区", () => {
195
+ beforeEach(() => {
196
+ vi.useFakeTimers();
197
+ });
198
+ afterEach(() => {
199
+ vi.useRealTimers();
200
+ });
201
+
202
+ it("already-resumable-idle(非 conversation)→ 重认领接管 + 送达指引", () => {
203
+ const deps = makeDeps();
204
+ const supervisor = new RoundSupervisor(deps);
205
+ deps.candidates.push({ id: "bg-1", rootSessionId: "r", agent: "worker", slug: "s", startedAt: 1 });
206
+ deps.views.set("bg-1", { id: "bg-1", status: "running", resumable: true, hasResult: false, chatMode: false, rootSessionId: "r", agent: "worker", slug: "s", startedAt: 1, closedReason: undefined });
207
+ const { readopted } = supervisor.bootPartition();
208
+ expect(readopted).toEqual(["bg-1"]);
209
+ expect(deps.guidances).toHaveLength(1);
210
+ expect(supervisor.supervisedIds()).toEqual(["bg-1"]);
211
+ });
212
+
213
+ it("in-flight 形态(无 resumable 信号)→ 跳过(直断归孤儿恢复,防双收尾)", () => {
214
+ const deps = makeDeps();
215
+ const supervisor = new RoundSupervisor(deps);
216
+ deps.candidates.push({ id: "bg-2", rootSessionId: "r", agent: "worker", slug: "s", startedAt: 1 });
217
+ deps.views.set("bg-2", { id: "bg-2", status: "running", resumable: false, hasResult: false, chatMode: false, rootSessionId: "r", agent: "worker", slug: "s", startedAt: 1, closedReason: undefined });
218
+ const { readopted } = supervisor.bootPartition();
219
+ expect(readopted).toEqual([]);
220
+ expect(deps.guidances).toHaveLength(0);
221
+ expect(deps.givenUp).toHaveLength(0);
222
+ });
223
+
224
+ it("conversation 形态 → 豁免(现状机制管辖)", () => {
225
+ const deps = makeDeps();
226
+ const supervisor = new RoundSupervisor(deps);
227
+ deps.candidates.push({ id: "bg-3", rootSessionId: "r", agent: "chat", slug: "s", startedAt: 1 });
228
+ deps.views.set("bg-3", { id: "bg-3", status: "running", resumable: true, hasResult: false, chatMode: true, rootSessionId: "r", agent: "chat", slug: "s", startedAt: 1, closedReason: undefined });
229
+ const { readopted } = supervisor.bootPartition();
230
+ expect(readopted).toEqual([]);
231
+ expect(supervisor.supervisedIds()).toEqual([]);
232
+ });
233
+ });
234
+
235
+ describe("RoundSupervisor 通知对账(送指引前查替代)", () => {
236
+ beforeEach(() => {
237
+ vi.useFakeTimers();
238
+ });
239
+ afterEach(() => {
240
+ vi.useRealTimers();
241
+ });
242
+
243
+ function setupDeadRecord(): { deps: ReturnType<typeof makeDeps>; supervisor: RoundSupervisor } {
244
+ const deps = makeDeps();
245
+ const supervisor = new RoundSupervisor(deps);
246
+ deps.views.set("bg-1", { id: "bg-1", status: "running", resumable: true, hasResult: false, chatMode: false, rootSessionId: "r", agent: "worker", slug: "fix-bug", startedAt: 1, closedReason: undefined });
247
+ return { deps, supervisor };
248
+ }
249
+
250
+ it("高置信(同 root + 同 agent + 同 slug + 看门狗窗内新建)→ 撤销指引改送替代终止通知 + 该放弃", () => {
251
+ const { deps, supervisor } = setupDeadRecord();
252
+ deps.candidates.push({ id: "bg-2", rootSessionId: "r", agent: "worker", slug: "fix-bug", startedAt: Date.now() - 1000 });
253
+ supervisor.adoptOnProcessDeath(makeRecord(), "x");
254
+ expect(deps.guidances).toHaveLength(0); // 指引撤销
255
+ expect(deps.replaced).toHaveLength(1);
256
+ expect(deps.replaced[0].replacementId).toBe("bg-2");
257
+ expect(deps.givenUp).toEqual([{ recordId: "bg-1", kind: "superseded", detail: { replacementId: "bg-2" } }]);
258
+ expect(supervisor.supervisedIds()).toEqual([]);
259
+ });
260
+
261
+ it("低置信(仅 agent 同名)→ 不撤销指引,指引自带豁免声明", () => {
262
+ const { deps, supervisor } = setupDeadRecord();
263
+ deps.candidates.push({ id: "bg-2", rootSessionId: "r", agent: "worker", slug: "other-task", startedAt: Date.now() - 1000 });
264
+ supervisor.adoptOnProcessDeath(makeRecord(), "x");
265
+ expect(deps.guidances).toHaveLength(1);
266
+ expect(deps.guidances[0].exemptionDisclaimer).toBe(true);
267
+ expect(deps.givenUp).toHaveLength(0);
268
+ });
269
+
270
+ it("同名但超出看门狗窗(无时序关联)→ 无替代命中,指引不带豁免声明", () => {
271
+ const { deps, supervisor } = setupDeadRecord();
272
+ deps.candidates.push({ id: "bg-2", rootSessionId: "r", agent: "worker", slug: "fix-bug", startedAt: Date.now() - ROUND_SUPERVISOR_WATCHDOG_DEFAULT_MS - 10_000 });
273
+ supervisor.adoptOnProcessDeath(makeRecord(), "x");
274
+ expect(deps.guidances).toHaveLength(1);
275
+ expect(deps.guidances[0].exemptionDisclaimer).toBe(false);
276
+ expect(deps.givenUp).toHaveLength(0);
277
+ });
278
+
279
+ it("无候选 → 指引不带豁免声明", () => {
280
+ const { deps, supervisor } = setupDeadRecord();
281
+ supervisor.adoptOnProcessDeath(makeRecord(), "x");
282
+ expect(deps.guidances).toHaveLength(1);
283
+ expect(deps.guidances[0].exemptionDisclaimer).toBe(false);
284
+ });
285
+
286
+ it("高置信对账按 slug 改写漏判 → 落入低置信(残余双执行风险面,豁免声明兜住)", () => {
287
+ const { deps, supervisor } = setupDeadRecord();
288
+ deps.candidates.push({ id: "bg-2", rootSessionId: "r", agent: "worker", slug: "rewritten-slug", startedAt: Date.now() - 1000 });
289
+ supervisor.adoptOnProcessDeath(makeRecord(), "x");
290
+ expect(deps.guidances).toHaveLength(1);
291
+ expect(deps.guidances[0].exemptionDisclaimer).toBe(true);
292
+ });
293
+ });
@@ -27,9 +27,11 @@ import * as path from "node:path";
27
27
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
28
28
  import { configureNotifyDomain, resetNotifyDomainForTests } from "../../core/notify-ports.ts";
29
29
 
30
+ // [D5 消亡] listActivePendingFromSessionFile(后代补杀清单口径)随 inproc pi 引擎目录
31
+ // 删除消亡(消费方全在待删 session-runner;止损语义由 W4 监督器该放弃承接)——
32
+ // 本文件覆盖收敛为 count 口径 + 游标剪枝原语。
30
33
  import {
31
34
  clearPendingCursors,
32
- listActivePendingFromSessionFile,
33
35
  prunePendingCursor,
34
36
  readActivePendingFromSessionFile,
35
37
  } from "../session-pending.ts";
@@ -175,7 +177,10 @@ describe("readActivePendingFromSessionFile", () => {
175
177
  });
176
178
 
177
179
  it("fork 继承的主 session register 已被 expired unregister 抵消 → 不干扰", () => {
178
- // 模拟 P fork 主 session:继承 register(sessionId 不匹配),session_start 重建补 unregister(expired)
180
+ // 模拟 P fork 主 session:继承 register 且文件里已有对应 unregister 抵消行
181
+ // (历史 session 档 U4 产物)。翻 process 档后 U4 不再补写抵消行,fork 残留的
182
+ // 现行收口 = 读侧过滤②(见文末 [F1] describe)——本用例锁「文件内抵消行仍生效」
183
+ // 的差集语义本身,不锁清理机制。
179
184
  const file = makeTmpSessionFile([
180
185
  mkRegister("parent-bg"),
181
186
  mkUnregister("parent-bg", "expired"),
@@ -319,9 +324,6 @@ describe("[LC-6] 差集计数语义等值", () => {
319
324
  expect(r.count).toBe(1); // 2 register − 1 unregister
320
325
  expect(counter.mock.calls[0]?.[0]).toHaveLength(1); // 端口收到差集(非全量 3 行)
321
326
  expect((counter.mock.calls[0]?.[0] as unknown[])[0]).toMatchObject({ customType: "pending:register" });
322
-
323
- const list = listActivePendingFromSessionFile(f);
324
- expect(list.items.map((i) => i.id)).toEqual(["bg-2"]);
325
327
  });
326
328
 
327
329
  it("同 id 重 register(未 unregister)覆盖 → 仍 1 个活跃", () => {
@@ -330,9 +332,7 @@ describe("[LC-6] 差集计数语义等值", () => {
330
332
  fs.writeFileSync(f, entryLine("pending:register", registerData("bg-1", "sess-1")));
331
333
  fs.appendFileSync(f, entryLine("pending:register", registerData("bg-1", "sess-1-moved")));
332
334
 
333
- const list = listActivePendingFromSessionFile(f);
334
- expect(list.items).toHaveLength(1);
335
- expect(list.items[0]?.sessionId).toBe("sess-1-moved");
335
+ expect(readActivePendingFromSessionFile(f).count).toBe(1);
336
336
  });
337
337
 
338
338
  it("unregister 后同 id 再 register → 恢复活跃", () => {
@@ -345,9 +345,6 @@ describe("[LC-6] 差集计数语义等值", () => {
345
345
  entryLine("pending:register", registerData("bg-1", "sess-2")),
346
346
  );
347
347
 
348
- const list = listActivePendingFromSessionFile(f);
349
- expect(list.items.map((i) => i.id)).toEqual(["bg-1"]);
350
- expect(list.items[0]?.sessionId).toBe("sess-2");
351
348
  expect(readActivePendingFromSessionFile(f).count).toBe(1);
352
349
  });
353
350
 
@@ -382,8 +379,7 @@ describe("[LC-6] 差集计数语义等值", () => {
382
379
  entryLine("pending:register", registerData("bg-2", "sess-2")),
383
380
  );
384
381
 
385
- const list = listActivePendingFromSessionFile(f);
386
- expect(list.items.map((i) => i.id)).toEqual(["bg-2"]);
382
+ expect(readActivePendingFromSessionFile(f).count).toBe(1); // 畸形行丢弃,好行入差集
387
383
  });
388
384
  });
389
385
 
@@ -410,8 +406,7 @@ describe("[LC-6] 游标剪枝", () => {
410
406
  expect(Buffer.byteLength(gen2, "utf-8")).toBeGreaterThan(150);
411
407
  fs.writeFileSync(f, gen2);
412
408
 
413
- const list = listActivePendingFromSessionFile(f);
414
- expect(list.items.map((i) => i.id)).toEqual(["bg-x"]); // 剪枝生效:从头全量读
409
+ expect(readActivePendingFromSessionFile(f).count).toBe(1); // 剪枝生效:从头全量读
415
410
  });
416
411
 
417
412
  it("prunePendingCursor:回收后重新判定等值(全量重读差集不变)", () => {
@@ -429,7 +424,6 @@ describe("[LC-6] 游标剪枝", () => {
429
424
  fs.appendFileSync(f, entryLine("pending:unregister", { id: "bg-1" }));
430
425
  const r = readActivePendingFromSessionFile(f);
431
426
  expect(r.count).toBe(1); // 重建后差集正确(无重复无丢失)
432
- expect(listActivePendingFromSessionFile(f).items.map((i) => i.id)).toEqual(["bg-2"]);
433
427
  });
434
428
 
435
429
  it("prunePendingCursor:prune 后覆写文件(size ≥ 旧 offset)→ 新内容完整可见", () => {
@@ -446,7 +440,7 @@ describe("[LC-6] 游标剪枝", () => {
446
440
  fs.writeFileSync(f, gen2); // 覆写(模拟文件替换),size > 旧 offset
447
441
  prunePendingCursor(f);
448
442
 
449
- expect(listActivePendingFromSessionFile(f).items.map((i) => i.id)).toEqual(["bg-y"]);
443
+ expect(readActivePendingFromSessionFile(f).count).toBe(1);
450
444
  });
451
445
 
452
446
  it("prunePendingCursor 不存在的条目为 no-op(不抛错)", () => {
@@ -469,3 +463,111 @@ describe("[LC-6] 游标剪枝", () => {
469
463
  expect(readActivePendingFromSessionFile(f).count).toBe(1);
470
464
  });
471
465
  });
466
+
467
+ // ─── [F1 读侧过滤②] 跨 session 残留基准 = 被读文件所属 session ───
468
+ //
469
+ // fork 继承的父级 register 残留(翻 process 档后 U4 补注销不再中性化,永久留存)
470
+ // 由读侧过滤收口:本文件读侧以「被读文件所属 session id」(pi session 文件首行
471
+ // SessionHeader.id)为基准经端口第二参传入,端口实现按 entry sessionId ≠ 基准
472
+ // 跳过——后代判定差集不含跨 session 残留,层主不被残留误判「尚有活跃后代」。
473
+
474
+ describe("[F1 读侧过滤②] 后代判定差集的跨 session 残留基准", () => {
475
+ const tmpFiles: string[] = [];
476
+
477
+ afterEach(() => {
478
+ for (const f of tmpFiles.splice(0)) {
479
+ try {
480
+ fs.rmSync(path.dirname(f), { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
481
+ } catch {
482
+ // 清理失败不影响断言
483
+ }
484
+ }
485
+ });
486
+
487
+ /** pi session 文件首行 SessionHeader(session-manager.js newSession 形态)。 */
488
+ const mkSessionHeader = (sessionId: string) =>
489
+ JSON.stringify({ type: "session", version: 3, id: sessionId, timestamp: "2026-09-01T00:00:00.000Z", cwd: "/tmp" });
490
+
491
+ /** 带 sessionId 的 register(pending-notifications listener 落盘形态)。 */
492
+ const mkRegisterIn = (id: string, sessionId: string) =>
493
+ JSON.stringify({ type: "custom", customType: "pending:register", data: { id, type: "subagent", name: id, registeredAt: 1, sessionId } });
494
+
495
+ /** 端口等价实现(消费基准):按 currentSessionId 过滤后直数——pending-notifications
496
+ * filterActiveRegisters 的最小转写(缺 sessionId 的旧形态条目视为本 session,不过滤)。 */
497
+ function filteredCountingPort(): ReturnType<typeof vi.fn<(entries: unknown[], opts?: { currentSessionId?: string }) => number>> {
498
+ const counter = vi.fn((entries: unknown[], opts?: { currentSessionId?: string }) => {
499
+ const seen = new Set<string>();
500
+ let count = 0;
501
+ for (const raw of entries) {
502
+ if (!raw || typeof raw !== "object") continue;
503
+ const data = (raw as { data?: { id?: unknown; sessionId?: unknown } }).data ?? {};
504
+ if (typeof data.id !== "string" || seen.has(data.id)) continue;
505
+ seen.add(data.id);
506
+ if (
507
+ opts?.currentSessionId !== undefined &&
508
+ typeof data.sessionId === "string" &&
509
+ data.sessionId !== opts.currentSessionId
510
+ ) {
511
+ continue;
512
+ }
513
+ count += 1;
514
+ }
515
+ return count;
516
+ }) as ReturnType<typeof vi.fn<(entries: unknown[], opts?: { currentSessionId?: string }) => number>>;
517
+ configureNotifyDomain({ countActiveFromEntries: counter });
518
+ return counter;
519
+ }
520
+
521
+ it("带 session header 的文件 → 端口收到的基准 = header.id(被读文件所属 session)", () => {
522
+ const counter = filteredCountingPort();
523
+ const file = makeTmpSessionFile([
524
+ mkSessionHeader("sess-child"),
525
+ mkRegisterIn("bg-own", "sess-child"),
526
+ ]);
527
+ tmpFiles.push(file);
528
+ expect(readActivePendingFromSessionFile(file).count).toBe(1);
529
+ const call = counter.mock.calls[0] as unknown[];
530
+ expect(call[1]).toEqual({ currentSessionId: "sess-child" });
531
+ });
532
+
533
+ it("端到端:fork 继承的父级 register 残留(sessionId = 父 session)不进后代判定差集", () => {
534
+ filteredCountingPort();
535
+ // 子 session 文件 = header(sess-child) + fork 继承的父级注册残留 + 子进程自己的注册
536
+ const file = makeTmpSessionFile([
537
+ mkSessionHeader("sess-child"),
538
+ mkRegisterIn("bg-parent", "sess-parent"),
539
+ mkRegisterIn("wf-parent", "sess-parent"),
540
+ mkRegisterIn("bg-own", "sess-child"),
541
+ ]);
542
+ tmpFiles.push(file);
543
+ // 无读侧过滤时差集为 3(层主误判「尚有活跃后代」空等);过滤后只数本 session = 1
544
+ expect(readActivePendingFromSessionFile(file)).toEqual({ count: 1, recentUnregister: false });
545
+ });
546
+
547
+ it("无 session header(旧 fixture 形态)→ 基准 undefined = 不过滤(向后兼容)", () => {
548
+ const counter = filteredCountingPort();
549
+ const file = makeTmpSessionFile([
550
+ mkRegisterIn("bg-parent", "sess-parent"),
551
+ mkRegisterIn("bg-own", "sess-child"),
552
+ ]);
553
+ tmpFiles.push(file);
554
+ expect(readActivePendingFromSessionFile(file).count).toBe(2);
555
+ const call = counter.mock.calls[0] as unknown[];
556
+ expect(call[1]).toBeUndefined();
557
+ });
558
+
559
+ it("增量读(cursor 已热)时基准仍从 cursor 缓存透传(不依赖重读首行)", () => {
560
+ const counter = filteredCountingPort();
561
+ const file = makeTmpSessionFile([
562
+ mkSessionHeader("sess-child"),
563
+ mkRegisterIn("bg-own", "sess-child"),
564
+ ]);
565
+ tmpFiles.push(file);
566
+ expect(readActivePendingFromSessionFile(file).count).toBe(1); // 首读建 cursor
567
+ // 追加新 register 后增量读——offset > 0 不重读首行,基准取 cursor 缓存
568
+ fs.appendFileSync(file, mkRegisterIn("bg-own-2", "sess-child") + "\n", "utf-8");
569
+ expect(readActivePendingFromSessionFile(file).count).toBe(2);
570
+ const lastCall = counter.mock.calls[counter.mock.calls.length - 1] as unknown[];
571
+ expect(lastCall[1]).toEqual({ currentSessionId: "sess-child" });
572
+ });
573
+ });
@@ -103,14 +103,6 @@ vi.mock("../manifest-store.ts", () => {
103
103
  return { ManifestStore: vi.fn(function (_recordsDir: string) { return new FakeManifestStore(); }) };
104
104
  });
105
105
 
106
- vi.mock("../engine/engines/pi/temp-prompt.ts", () => ({
107
- writePromptToTempFile: vi.fn(async (agent: string) => {
108
- const safeName = agent.replace(/[^\w.-]+/g, "_");
109
- return { dir: `/tmp/fake-${safeName}`, filePath: `/tmp/fake-${safeName}/prompt-${safeName}.md` };
110
- }),
111
- cleanupTempPrompt: vi.fn(async () => {}),
112
- }));
113
-
114
106
  import { spawn } from "node:child_process";
115
107
 
116
108
  import { ModelConfigService } from "../model-config-service.ts";
@@ -233,6 +225,11 @@ describe("退役步骤 2 判定层:createBackgroundStream", () => {
233
225
  });
234
226
 
235
227
  // ── 接线层:service → 工厂的参数透传(mode 传错 = 判定失效)──
228
+ // [W3 改写] 驱动方式从 inproc spawn(FakeChild close)改为协议 seam
229
+ //(registerFakePiEngine 替身)——kickOffChatRound 的 stream 创建先于 pool acquire,
230
+ // 替身 run 挂起即可观察工厂入参。
231
+ import { registerFakePiEngine } from "./helpers/fake-engine-port.ts";
232
+ import { clearEngines } from "../engine/registry.ts";
236
233
 
237
234
  describe("退役步骤 2 接线层:kickOffChatRound 透传 mode/sink 给工厂", () => {
238
235
  beforeEach(() => {
@@ -242,6 +239,7 @@ describe("退役步骤 2 接线层:kickOffChatRound 透传 mode/sink 给工厂
242
239
 
243
240
  afterEach(() => {
244
241
  for (const key of RELAY_ENV_KEYS) delete process.env[key];
242
+ clearEngines();
245
243
  vi.restoreAllMocks();
246
244
  });
247
245
 
@@ -250,9 +248,11 @@ describe("退役步骤 2 接线层:kickOffChatRound 透传 mode/sink 给工厂
250
248
  ["tui", "tui"],
251
249
  ["undefined(headless)", undefined],
252
250
  ])("mode=%s:工厂收到 initSession 注入的 mode 与 streamSink", async (_label, mode) => {
251
+ clearEngines();
252
+ registerFakePiEngine();
253
253
  const service = setupService(mode);
254
- await executeBackground(service);
255
- expect(mockCreateStream).toHaveBeenCalledTimes(1);
254
+ await service.execute({ task: "retire test", slug: "test", ctxModel, conversation: true });
255
+ await vi.waitFor(() => expect(mockCreateStream).toHaveBeenCalledTimes(1));
256
256
  const [recordId, sinkArg, modeArg] = mockCreateStream.mock.calls[0]!;
257
257
  expect(typeof recordId).toBe("string");
258
258
  expect(sinkArg).toHaveProperty("setWidget");
@@ -21,8 +21,7 @@ const { loggerMock } = vi.hoisted(() => ({
21
21
  }));
22
22
  vi.mock("../../core/logger.ts", () => ({ getLogger: () => loggerMock }));
23
23
 
24
- const { killChildSpy, foreignLiveSpy } = vi.hoisted(() => ({
25
- killChildSpy: vi.fn(),
24
+ const { foreignLiveSpy } = vi.hoisted(() => ({
26
25
  foreignLiveSpy: vi.fn<() => { pid: number; id: string; startedAt: number } | undefined>(),
27
26
  }));
28
27
  vi.mock("../alive-store.ts", () => ({
@@ -34,15 +33,6 @@ vi.mock("../alive-store.ts", () => ({
34
33
  findForeignLiveInstance: foreignLiveSpy,
35
34
  }));
36
35
 
37
- vi.mock("../engine/engines/pi/session-runner.ts", () => ({
38
- runSpawn: vi.fn(),
39
- killAllSpawnedChildren: vi.fn(),
40
- getChildByRecord: vi.fn(() => undefined),
41
- registerSpawnedChildForRecord: vi.fn(),
42
- spawnedChildren: new Map(),
43
- killRecordChildWithEscalation: killChildSpy,
44
- }));
45
-
46
36
  import { ModelConfigService } from "../model-config-service.ts";
47
37
  import type { RecordStore } from "../record-store.ts";
48
38
  import { SubagentService } from "../subagent-service.ts";