@zhushanwen/subagent-core 0.7.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/dist/{chunk-VURUAGMM.js → chunk-LZJOEDGQ.js} +603 -154
  2. package/dist/engine-discovery-scan-Da4Dkzsi.d.cts +818 -0
  3. package/dist/engine-discovery-scan-Da4Dkzsi.d.ts +818 -0
  4. package/dist/execution/engine/engine-discovery-scan.cjs +325 -154
  5. package/dist/execution/engine/engine-discovery-scan.d.cts +1 -2
  6. package/dist/execution/engine/engine-discovery-scan.d.ts +1 -2
  7. package/dist/execution/engine/engine-discovery-scan.js +1 -1
  8. package/dist/execution/relay-env.js +5 -4
  9. package/dist/index.cjs +9799 -7954
  10. package/dist/index.d.cts +1896 -1360
  11. package/dist/index.d.ts +1896 -1360
  12. package/dist/index.js +8952 -7513
  13. package/dist.bundle/index.cjs +7897 -6054
  14. package/package.json +5 -5
  15. package/src/__tests__/manifest-store.test.ts +16 -15
  16. package/src/__tests__/record-store-last-line.test.ts +12 -0
  17. package/src/__tests__/robustness-medium-batch1.test.ts +10 -14
  18. package/src/__tests__/robustness-medium-batch2.test.ts +7 -2
  19. package/src/core/logger.ts +1 -1
  20. package/src/execution/__tests__/alive-store.test.ts +117 -1
  21. package/src/execution/__tests__/batch-finalized.test.ts +206 -0
  22. package/src/execution/__tests__/chat-engine-routing.test.ts +117 -1
  23. package/src/execution/__tests__/{cold-resurrect.test.ts → cold-lookup.test.ts} +133 -33
  24. package/src/execution/__tests__/collect-coordinator-service.test.ts +7 -5
  25. package/src/execution/__tests__/conversation-continuation.test.ts +1452 -0
  26. package/src/execution/__tests__/delivery-methods.test.ts +90 -176
  27. package/src/execution/__tests__/dialog-queue.test.ts +70 -3
  28. package/src/execution/__tests__/dispose-manifest-recovery.test.ts +475 -0
  29. package/src/execution/__tests__/execute-and-await-worktree.test.ts +3 -3
  30. package/src/execution/__tests__/execute-nesting.test.ts +8 -5
  31. package/src/execution/__tests__/execution-record.test.ts +8 -148
  32. package/src/execution/__tests__/finalize-record.test.ts +295 -129
  33. package/src/execution/__tests__/get-record-for-action-restart.test.ts +21 -29
  34. package/src/execution/__tests__/helpers/fake-engine-port.ts +13 -68
  35. package/src/execution/__tests__/helpers/legacy-sidecar.ts +33 -0
  36. package/src/execution/__tests__/helpers/subagent-service-mocks.ts +9 -5
  37. package/src/execution/__tests__/idle-gc.test.ts +60 -0
  38. package/src/execution/__tests__/inflight-production-wiring.test.ts +223 -0
  39. package/src/execution/__tests__/lifecycle-manager.test.ts +3 -254
  40. package/src/execution/__tests__/manifest-store-tmp-recovery.test.ts +43 -22
  41. package/src/execution/__tests__/model-config-service.test.ts +111 -0
  42. package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +10 -6
  43. package/src/execution/__tests__/nested-visibility.test.ts +13 -1
  44. package/src/execution/__tests__/notify-ledger.test.ts +42 -1
  45. package/src/execution/__tests__/rebuild-indexes.test.ts +329 -0
  46. package/src/execution/__tests__/record-binding.test.ts +613 -0
  47. package/src/execution/__tests__/record-origin.test.ts +318 -0
  48. package/src/execution/__tests__/record-store-intent-api.test.ts +600 -0
  49. package/src/execution/__tests__/record-store-orphan-revive.test.ts +71 -3
  50. package/src/execution/__tests__/record-store.test.ts +72 -54
  51. package/src/execution/__tests__/recursive-visibility-baseline.test.ts +8 -5
  52. package/src/execution/__tests__/round-supervisor-workflow-origin.test.ts +205 -0
  53. package/src/execution/__tests__/run-orchestration-write-lease.test.ts +293 -0
  54. package/src/execution/__tests__/run-orchestration.test.ts +199 -0
  55. package/src/execution/__tests__/session-file-gc.test.ts +35 -4
  56. package/src/execution/__tests__/session-reconstructor.test.ts +78 -0
  57. package/src/execution/__tests__/state-marker.test.ts +288 -0
  58. package/src/execution/__tests__/stream-sink-retirement.test.ts +8 -5
  59. package/src/execution/__tests__/subagent-actions-core.test.ts +67 -16
  60. package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +0 -1
  61. package/src/execution/__tests__/subagent-service-notify-gate.test.ts +27 -11
  62. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +17 -11
  63. package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +130 -53
  64. package/src/execution/__tests__/subagent-service.test.ts +17 -215
  65. package/src/execution/__tests__/subprocess-agent-runner-no-progress-killall.test.ts +138 -0
  66. package/src/execution/__tests__/subprocess-agent-runner.test.ts +105 -660
  67. package/src/execution/__tests__/sync-collect-recovery.test.ts +23 -16
  68. package/src/execution/__tests__/terminal-write-latency.test.ts +79 -0
  69. package/src/execution/__tests__/workflow-agent-dispatch.test.ts +782 -0
  70. package/src/execution/alive-store.ts +37 -28
  71. package/src/execution/{cold-resurrect.ts → cold-lookup.ts} +48 -45
  72. package/src/execution/config.ts +1 -1
  73. package/src/execution/conversation-continuation.ts +541 -0
  74. package/src/execution/dialog-queue.ts +60 -12
  75. package/src/execution/engine/__tests__/common/capability-gate.test.ts +1 -1
  76. package/src/execution/engine/__tests__/common/journal-wiring.test.ts +16 -1
  77. package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/fake-engine-protocol.mjs +17 -51
  78. package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/smoke-run.fixture.json +0 -5
  79. package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +1 -1
  80. package/src/execution/engine/__tests__/conformance/protocol-blackbox.test.ts +5 -9
  81. package/src/execution/engine/__tests__/conformance/registry-fork-filter.test.ts +55 -53
  82. package/src/execution/engine/__tests__/engine-discovery-scan.test.ts +0 -3
  83. package/src/execution/engine/__tests__/engine-discovery.test.ts +0 -3
  84. package/src/execution/engine/__tests__/inflight-snapshot.test.ts +180 -0
  85. package/src/execution/engine/__tests__/model-prompt.test.ts +0 -3
  86. package/src/execution/engine/__tests__/registry.test.ts +0 -1
  87. package/src/execution/engine/__tests__/routing.test.ts +0 -1
  88. package/src/execution/engine/__tests__/run-failure-worktree-cleanup.test.ts +0 -3
  89. package/src/execution/engine/client/__tests__/__fixtures__/fake-engine.mjs +0 -3
  90. package/src/execution/engine/client/__tests__/engine-client-reap.test.ts +212 -0
  91. package/src/execution/engine/client/__tests__/mirror.test.ts +73 -1
  92. package/src/execution/engine/client/__tests__/protocol-closure.test.ts +2 -9
  93. package/src/execution/engine/client/__tests__/remote-engine.test.ts +46 -12
  94. package/src/execution/engine/client/engine-client.ts +111 -26
  95. package/src/execution/engine/client/mirror.ts +9 -0
  96. package/src/execution/engine/client/remote-engine.ts +52 -34
  97. package/src/execution/engine/client/reverse-router.ts +6 -27
  98. package/src/execution/engine/common/__tests__/run-signals.test.ts +79 -0
  99. package/src/execution/engine/common/capability-gate.ts +27 -0
  100. package/src/execution/engine/common/data-dir.ts +3 -2
  101. package/src/execution/engine/common/errors.ts +1 -1
  102. package/src/execution/engine/common/journal-wiring.ts +18 -9
  103. package/src/execution/engine/common/run-signals.ts +83 -0
  104. package/src/execution/engine/d8-compat.ts +1 -1
  105. package/src/execution/engine/host/__tests__/host-bridge.test.ts +3 -18
  106. package/src/execution/engine/host/host-bridge.ts +26 -41
  107. package/src/execution/engine/host/pi-host-binding.ts +5 -5
  108. package/src/execution/engine/host/spawned-children.ts +1 -1
  109. package/src/execution/engine/inflight-snapshot.ts +92 -0
  110. package/src/execution/engine/port.ts +52 -54
  111. package/src/execution/engine/types.ts +11 -14
  112. package/src/execution/execution-record.ts +15 -47
  113. package/src/execution/finalize-record.ts +94 -197
  114. package/src/execution/idle-gc.ts +9 -3
  115. package/src/execution/lifecycle-manager.ts +25 -361
  116. package/src/execution/lifecycle-predicates.ts +22 -19
  117. package/src/execution/manifest-store.ts +40 -42
  118. package/src/execution/model-config-service.ts +1 -1
  119. package/src/execution/notifier.ts +39 -1
  120. package/src/execution/notify-host.ts +13 -4
  121. package/src/execution/path-encoding.ts +6 -0
  122. package/src/execution/record-entry.ts +20 -2
  123. package/src/execution/record-store.ts +925 -160
  124. package/src/execution/relay-env.ts +8 -3
  125. package/src/execution/round-supervisor/domain.ts +17 -5
  126. package/src/execution/round-supervisor/service-binding.test.ts +89 -53
  127. package/src/execution/round-supervisor/service-binding.ts +71 -21
  128. package/src/execution/round-supervisor/supervisor.ts +23 -9
  129. package/src/execution/service/record-access.ts +547 -0
  130. package/src/execution/service/record-lifecycle.ts +542 -0
  131. package/src/execution/service/run-orchestration.ts +1647 -0
  132. package/src/execution/service/service-bootstrap.ts +111 -0
  133. package/src/execution/service/service-constants.ts +27 -0
  134. package/src/execution/service/session-baselines.ts +401 -0
  135. package/src/execution/service/sync-collect-domain.ts +442 -0
  136. package/src/execution/service/workflow-dispatch.ts +567 -0
  137. package/src/execution/session-file-gc.ts +11 -8
  138. package/src/execution/session-reconstructor.ts +44 -1
  139. package/src/execution/sessions-index.ts +20 -3
  140. package/src/execution/settled-watchdog.ts +86 -43
  141. package/src/execution/state-marker.ts +516 -0
  142. package/src/execution/stream-sink.ts +1 -1
  143. package/src/execution/subagent-actions-core.ts +53 -31
  144. package/src/execution/subagent-service.ts +643 -2955
  145. package/src/execution/subprocess-agent-runner.ts +46 -323
  146. package/src/execution/types.ts +60 -43
  147. package/src/execution/ui-request-handler-factory.ts +5 -0
  148. package/src/execution/worktree-manager.ts +1 -1
  149. package/src/execution/worktree-registry.ts +12 -8
  150. package/src/index.ts +16 -5
  151. package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +2 -3
  152. package/src/orchestration/__tests__/agent-call-stream.test.ts +24 -49
  153. package/src/orchestration/__tests__/error-recovery-terminal-hardening.test.ts +4 -4
  154. package/src/orchestration/__tests__/file-run-store.test.ts +5 -7
  155. package/src/orchestration/__tests__/lifecycle-abort-broadcast-signal.test.ts +2 -3
  156. package/src/orchestration/__tests__/run-snapshot.test.ts +27 -43
  157. package/src/orchestration/__tests__/worker-message-pump-workflow-dispatch.test.ts +274 -0
  158. package/src/orchestration/execute-agent-call.ts +1 -1
  159. package/src/orchestration/file-run-store.ts +1 -1
  160. package/src/orchestration/models/ports.ts +20 -13
  161. package/src/orchestration/models/types.ts +11 -13
  162. package/src/orchestration/run-snapshot.ts +18 -22
  163. package/src/orchestration/worker-message-pump.ts +33 -57
  164. package/src/shared/atomic-write.ts +10 -8
  165. package/dist/chunk-T2SYBW3I.js +0 -24
  166. package/dist/engine-discovery-scan-ocpM8FmI.d.cts +0 -1608
  167. package/dist/engine-discovery-scan-ocpM8FmI.d.ts +0 -1608
  168. package/src/execution/__tests__/chat-round-first-round-watchdog.test.ts +0 -255
  169. package/src/execution/__tests__/finalized-marker.test.ts +0 -104
  170. package/src/execution/__tests__/lifecycle-manager-lock.test.ts +0 -211
  171. package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +0 -488
  172. package/src/execution/__tests__/subprocess-agent-runner-timeout.test.ts +0 -75
  173. package/src/execution/__tests__/tombstone-store.test.ts +0 -73
  174. package/src/execution/engine/__tests__/conformance/chat-round-protocol.test.ts +0 -230
  175. package/src/execution/finalized-marker.ts +0 -70
  176. package/src/execution/tombstone-store.ts +0 -72
@@ -39,9 +39,6 @@ function makeRecord(worktreeHandle?: WorktreeHandle): ExecutionRecord {
39
39
 
40
40
  function makeDeps(cleanup: ReturnType<typeof vi.fn>): FinalizeDeps {
41
41
  return {
42
- manifestStore: {
43
- writeManifest: vi.fn(async () => undefined),
44
- } as unknown as FinalizeDeps["manifestStore"],
45
42
  worktreeManager: { cleanup } as unknown as FinalizeDeps["worktreeManager"],
46
43
  store: { archive: vi.fn() } as unknown as FinalizeDeps["store"],
47
44
  modelService: { getAgentDir: () => "/tmp/w3-test-agentdir" } as unknown as FinalizeDeps["modelService"],
@@ -255,9 +255,6 @@ if (MODE === "hang") {
255
255
  send({ id: runFrameId, result: runOutcomeAborted() });
256
256
  }
257
257
  break;
258
- case "interact":
259
- send({ id, result: { ok: true, delivered: true } });
260
- break;
261
258
  case "read":
262
259
  send({
263
260
  id,
@@ -0,0 +1,212 @@
1
+ // src/execution/engine/client/__tests__/engine-client-reap.test.ts
2
+ //
3
+ // [H1 U2 / 红线①收割链] 引擎退出链孤儿收割单测(设计
4
+ // docs/design/subagent-chat-run-unification.md §3.3 红线① + §5 U2 验收「收割链各分支」)。
5
+ //
6
+ // 覆盖分支:
7
+ // - 非主动死亡(!intentionalKill && alreadyDead)POSIX → 复用 killProcessTree 负 pid
8
+ // 组杀(fire-and-forget),且仅组杀一次(无外层逐 pid 兜底——grace 窗不被压缩);
9
+ // - intentionalKill 路径跳过(主动 dispose 链已有两层杀——不叠加、优雅窗零压缩);
10
+ // - 引擎进程仍活(握手失败重建路径)不触发收割(下方既有组杀兜底承接,行为不变);
11
+ // - Windows 通道:置死前 mirror.snapshot 过滤活孤儿(state==="running" && !killed——
12
+ // 死 pid 与 killed 项不进清单)后逐 pid **异步 spawn** taskkill(无 spawnSync——
13
+ // 同步逐个会冻结 onEngineExit 回调、推迟 pending reject);
14
+ // - 收割插入点 = mirror.killAll() 置死清空之前(Windows 通道依赖快照活项)。
15
+ //
16
+ // 平台分派说明:Windows 通道经私有方法 reapOrphansViaMirrorSnapshot 直调覆盖
17
+ //(process.platform 判据在 vitest worker 内不可 stub——一行字面分派由 U4 真机 Windows
18
+ // 验收覆盖);POSIX 分支用本机真实平台(darwin/linux)断言。
19
+
20
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
21
+
22
+ const { loggerMock } = vi.hoisted(() => ({
23
+ loggerMock: { debug: vi.fn(), warn: vi.fn(), error: vi.fn() },
24
+ }));
25
+ vi.mock("../../../../core/logger.ts", () => ({ getLogger: () => loggerMock }));
26
+
27
+ // killProcessTree spy(POSIX 组杀断言);waitForChildExit 保留真实现(killAll 消费)。
28
+ const { killProcessTreeSpy } = vi.hoisted(() => ({ killProcessTreeSpy: vi.fn() }));
29
+ vi.mock("../reaper.ts", async (importOriginal) => {
30
+ const actual = await importOriginal<typeof import("../reaper.ts")>();
31
+ return { ...actual, killProcessTree: (...args: unknown[]) => killProcessTreeSpy(...(args as [])) };
32
+ });
33
+
34
+ // node:child_process spawn spy(Windows 通道 taskkill 断言;本文件不走 ensureConnected,
35
+ // 引擎 CLI spawn 不被触发)。
36
+ const { spawnSpy, spawnSyncSpy } = vi.hoisted(() => ({
37
+ spawnSpy: vi.fn(() => ({
38
+ unref: vi.fn(),
39
+ on: vi.fn(),
40
+ pid: 424242,
41
+ })),
42
+ spawnSyncSpy: vi.fn(() => ({ status: 0, error: undefined })),
43
+ }));
44
+ vi.mock("node:child_process", async (importOriginal) => {
45
+ const actual = await importOriginal<typeof import("node:child_process")>();
46
+ return { ...actual, spawn: spawnSpy, spawnSync: spawnSyncSpy };
47
+ });
48
+
49
+ import { EngineClient } from "../engine-client.ts";
50
+ import type { MirrorEntry } from "../mirror.ts";
51
+
52
+ function makeClient(): EngineClient {
53
+ return new EngineClient({
54
+ engineId: "fake",
55
+ command: process.execPath,
56
+ args: ["-e", ""],
57
+ hostKind: "test",
58
+ hostVersion: "test-host-1.0",
59
+ dataDir: "/tmp/engine-client-reap-test",
60
+ envPrefixes: [],
61
+ });
62
+ }
63
+
64
+ /** 塞一个已死的假引擎 child(exitCode 非 null = alreadyDead 判据命中)。 */
65
+ function plantDeadChild(client: EngineClient, pid: number): void {
66
+ (client as unknown as { child: unknown }).child = {
67
+ pid,
68
+ exitCode: 1,
69
+ signalCode: null,
70
+ removeAllListeners: vi.fn(),
71
+ on: vi.fn(),
72
+ };
73
+ }
74
+
75
+ /** 塞一个活着的假引擎 child(alreadyDead 不命中——既有组杀兜底路径)。 */
76
+ function plantLiveChild(client: EngineClient, pid: number): void {
77
+ (client as unknown as { child: unknown }).child = {
78
+ pid,
79
+ exitCode: null,
80
+ signalCode: null,
81
+ removeAllListeners: vi.fn(),
82
+ on: vi.fn(),
83
+ };
84
+ }
85
+
86
+ /** 在途请求 pending 直插(pending reject 时序断言用)。 */
87
+ function plantPendingRequest(client: EngineClient, id: number): { reject: ReturnType<typeof vi.fn> } {
88
+ const pending = { resolve: vi.fn(), reject: vi.fn(), timer: undefined };
89
+ (client as unknown as { pending: Map<number, typeof pending> }).pending.set(id, pending);
90
+ return pending;
91
+ }
92
+
93
+ function mirrorEntry(pid: number, state: "running" | "exited", killed: boolean): MirrorEntry {
94
+ return { pid, recordId: `sa-${pid}`, state, killed, updatedAt: Date.now() };
95
+ }
96
+
97
+ describe("红线①收割链:非主动死亡路径(POSIX 组杀)", () => {
98
+ beforeEach(() => {
99
+ killProcessTreeSpy.mockClear();
100
+ spawnSpy.mockClear();
101
+ spawnSyncSpy.mockClear();
102
+ });
103
+
104
+ afterEach(() => {
105
+ vi.restoreAllMocks();
106
+ });
107
+
108
+ it("引擎意外死(alreadyDead + !intentionalKill)→ teardownProcess 内 mirror.killAll 之前组杀引擎 pid", () => {
109
+ const client = makeClient();
110
+ plantDeadChild(client, 4321);
111
+ client.mirror.recordSpawned(9100, "sa-a"); // 死前 spawn 的任务子进程(活孤儿)
112
+
113
+ (client as unknown as { teardownProcess: (d: string) => void }).teardownProcess("signal SIGKILL");
114
+
115
+ // POSIX:复用 killProcessTree 负 pid 组杀(目标 = 引擎 pid,组内含任务子进程——
116
+ // 不依赖镜像)
117
+ expect(killProcessTreeSpy).toHaveBeenCalledTimes(1);
118
+ expect(killProcessTreeSpy).toHaveBeenCalledWith(
119
+ 4321,
120
+ expect.stringContaining("reap orphaned children"),
121
+ { engineId: "fake" },
122
+ );
123
+ // 收割插入点在镜像置死之前:killAll 后快照已清(此处仅验证组杀发生在 teardown 内
124
+ // 且镜像最终被置死清空)
125
+ expect(client.mirror.size).toBe(0);
126
+ });
127
+
128
+ it("无外层逐 pid 兜底:组杀仅一次(5s grace 窗不被同步校验压缩)", () => {
129
+ const client = makeClient();
130
+ plantDeadChild(client, 4321);
131
+ for (let i = 0; i < 5; i++) client.mirror.recordSpawned(9000 + i, `sa-${i}`); // 多个活孤儿
132
+
133
+ (client as unknown as { teardownProcess: (d: string) => void }).teardownProcess("signal SIGKILL");
134
+
135
+ // 升级链(SIGTERM → 5s grace → SIGKILL)内建覆盖残余职责:不逐 pid 补杀
136
+ expect(killProcessTreeSpy).toHaveBeenCalledTimes(1);
137
+ expect(spawnSpy).not.toHaveBeenCalled();
138
+ });
139
+
140
+ it("intentionalKill 路径跳过收割(主动 dispose 链已有两层杀——不叠加、优雅窗零压缩)", () => {
141
+ const client = makeClient();
142
+ plantDeadChild(client, 4321);
143
+ client.mirror.recordSpawned(9100, "sa-a");
144
+ (client as unknown as { intentionalKill: boolean }).intentionalKill = true;
145
+
146
+ (client as unknown as { teardownProcess: (d: string) => void }).teardownProcess("after intentional kill");
147
+
148
+ expect(killProcessTreeSpy).not.toHaveBeenCalled();
149
+ expect(spawnSpy).not.toHaveBeenCalled();
150
+ expect(client.mirror.size).toBe(0); // 置死清空照常
151
+ });
152
+
153
+ it("引擎进程仍活(握手失败重建路径)→ 不触发收割分支(既有组杀兜底承接,行为不变)", () => {
154
+ const client = makeClient();
155
+ plantLiveChild(client, 4321);
156
+
157
+ (client as unknown as { teardownProcess: (d: string) => void }).teardownProcess("handshake failed");
158
+
159
+ // alreadyDead 不命中 → 收割链跳过;活进程组杀由 teardown 既有兜底承接(reason 原样)
160
+ expect(killProcessTreeSpy).toHaveBeenCalledTimes(1);
161
+ expect(killProcessTreeSpy).toHaveBeenCalledWith(4321, "handshake failed", { engineId: "fake" });
162
+ });
163
+ });
164
+
165
+ describe("红线①收割链:Windows 通道(镜像快照过滤活孤儿 + 异步 taskkill)", () => {
166
+ beforeEach(() => {
167
+ killProcessTreeSpy.mockClear();
168
+ spawnSpy.mockClear();
169
+ spawnSyncSpy.mockClear();
170
+ });
171
+
172
+ afterEach(() => {
173
+ vi.restoreAllMocks();
174
+ });
175
+
176
+ it("置死前快照过滤活孤儿:仅 state=running && !killed 进 taskkill 清单(历史死 pid 与 killed 项排除)", () => {
177
+ const client = makeClient();
178
+ plantDeadChild(client, 4321);
179
+ client.mirror.recordSpawned(9100, "sa-live-orphan"); // 活孤儿 → 进清单
180
+ client.mirror.recordSpawned(9101, "sa-exited");
181
+ client.mirror.recordStateChanged({ pid: 9101, recordId: "sa-exited", state: "exited", killed: false });
182
+ client.mirror.recordSpawned(9102, "sa-killed");
183
+ client.mirror.recordStateChanged({ pid: 9102, recordId: "sa-killed", state: "exited", killed: true });
184
+
185
+ (client as unknown as { reapOrphansViaMirrorSnapshot: (d: string) => void }).reapOrphansViaMirrorSnapshot(
186
+ "signal SIGKILL",
187
+ );
188
+
189
+ expect(spawnSpy).toHaveBeenCalledTimes(1);
190
+ expect(spawnSpy).toHaveBeenCalledWith("taskkill", ["/PID", "9100", "/T", "/F"], { stdio: "ignore" });
191
+ expect(killProcessTreeSpy).not.toHaveBeenCalled(); // win32 不复用引擎树杀(树根已死 = 空操作)
192
+ });
193
+
194
+ it("taskkill 异步 spawn 不阻塞 teardown(pending reject 在同步段完成——Continuation 失败通知链源头不被推迟)", () => {
195
+ const client = makeClient();
196
+ plantDeadChild(client, 4321);
197
+ client.mirror.recordSpawned(9100, "sa-live-orphan");
198
+ const pending = plantPendingRequest(client, 7);
199
+
200
+ // Windows 通道直调 + teardown 全流程(快照 → 异步 spawn → killAll → pending reject)
201
+ (client as unknown as { reapOrphansViaMirrorSnapshot: (d: string) => void }).reapOrphansViaMirrorSnapshot(
202
+ "signal SIGKILL",
203
+ );
204
+ (client as unknown as { teardownProcess: (d: string) => void }).teardownProcess("signal SIGKILL");
205
+
206
+ // spawn 是异步 fire-and-forget(返回即继续,无同步等待);spawnSync 零调用——
207
+ // 逐个同步 N×10s 上界会冻结 onEngineExit 同步回调与宿主事件循环
208
+ expect(spawnSpy).toHaveBeenCalled();
209
+ expect(spawnSyncSpy).not.toHaveBeenCalled();
210
+ expect(pending.reject).toHaveBeenCalledTimes(1); // 同步段完成(engine_crashed)
211
+ });
212
+ });
@@ -4,8 +4,14 @@
4
4
  // 3. 置死后清空,isResumable 回落「无句柄」;
5
5
  // killed 必含判据 = `entry !== undefined && !entry.killed`。
6
6
 
7
- import { describe, expect, it } from "vitest";
7
+ import { afterEach, describe, expect, it } from "vitest";
8
8
 
9
+ import {
10
+ DialogGlobalQueue,
11
+ registerActiveDialogQueue,
12
+ type UiRequest,
13
+ type UiResponse,
14
+ } from "../../../dialog-queue.ts";
9
15
  import { SpawnedChildrenMirror } from "../mirror.ts";
10
16
 
11
17
  describe("SpawnedChildrenMirror", () => {
@@ -65,3 +71,69 @@ describe("SpawnedChildrenMirror", () => {
65
71
  expect(mirror.isResumable("rec-2")).toBe(true);
66
72
  });
67
73
  });
74
+
75
+ // ── [SR-4 接线] 子进程退出 → 取消该 pid 挂起的 dialog ──
76
+ //
77
+ // 原绑定点(宿主 spawn pi 子进程时代的 session-runner child close)随协议化消失;
78
+ // 新链路 = 引擎 host/childStateChanged(exited) → 本镜像 → notifyChildProcessExited。
79
+ describe("SpawnedChildrenMirror — 子进程退出取消挂起 dialog(SR-4 接线)", () => {
80
+ afterEach(() => {
81
+ registerActiveDialogQueue(undefined);
82
+ });
83
+
84
+ /** 入队一个永不 settle 的 dialog(绑定 pid),返回其结果观察 promise。 */
85
+ function enqueuePending(queue: DialogGlobalQueue, pid: number, id: string): Promise<UiResponse> {
86
+ const req: UiRequest = { method: "select", id, title: `q-${id}` };
87
+ return queue.enqueue(req, () => new Promise<UiResponse>(() => {}), { child: { pid } });
88
+ }
89
+
90
+ it("state=exited → 该 pid 的挂起 dialog resolve cancelled", async () => {
91
+ const queue = new DialogGlobalQueue();
92
+ registerActiveDialogQueue(queue);
93
+ const pending = enqueuePending(queue, 101, "req-1");
94
+
95
+ const mirror = new SpawnedChildrenMirror();
96
+ mirror.recordSpawned(101, "rec-1");
97
+ mirror.recordStateChanged({ pid: 101, recordId: "rec-1", state: "exited", killed: true, exitCode: 0 });
98
+
99
+ await expect(pending).resolves.toEqual({ cancelled: true });
100
+ });
101
+
102
+ it("state=running 不触发取消(进程仍在跑,dialog 继续排队)", async () => {
103
+ const queue = new DialogGlobalQueue();
104
+ registerActiveDialogQueue(queue);
105
+ let settled = false;
106
+ void enqueuePending(queue, 102, "req-2").then(() => {
107
+ settled = true;
108
+ });
109
+
110
+ const mirror = new SpawnedChildrenMirror();
111
+ mirror.recordStateChanged({ pid: 102, recordId: "rec-2", state: "running", killed: false });
112
+ await Promise.resolve();
113
+
114
+ expect(settled).toBe(false);
115
+ });
116
+
117
+ it("killAll(引擎进程消亡)→ 逐 pid 取消各自挂起 dialog", async () => {
118
+ const queue = new DialogGlobalQueue();
119
+ registerActiveDialogQueue(queue);
120
+ const p1 = enqueuePending(queue, 201, "req-a");
121
+ const p2 = enqueuePending(queue, 202, "req-b");
122
+
123
+ const mirror = new SpawnedChildrenMirror();
124
+ mirror.recordSpawned(201, "rec-a");
125
+ mirror.recordSpawned(202, "rec-b");
126
+ mirror.killAll();
127
+
128
+ await expect(p1).resolves.toEqual({ cancelled: true });
129
+ await expect(p2).resolves.toEqual({ cancelled: true });
130
+ });
131
+
132
+ it("未登记队列(headless / 无 UI 通道宿主)→ exited 为 no-op,不抛", () => {
133
+ const mirror = new SpawnedChildrenMirror();
134
+ mirror.recordSpawned(301, "rec-3");
135
+ expect(() =>
136
+ mirror.recordStateChanged({ pid: 301, recordId: "rec-3", state: "exited", killed: true }),
137
+ ).not.toThrow();
138
+ });
139
+ });
@@ -7,6 +7,7 @@
7
7
  // UiRequest / UiResponse / UiRequestHandler / UiMethod 的 core 侧出口 =
8
8
  // execution/dialog-queue.ts 的 SDK 再导出(W7 已切换,SSOT = SDK ui-types)——
9
9
  // 本断言的 Core* 别名现在与 Sdk* 同源,保留断言形态防未来回退为本地定义后漂移。
10
+ // [H1 U5/U6] InteractAction / InteractResult 断言对已随 chat 域 interact 面退役删除。
10
11
 
11
12
  import { describe, expect, it } from "vitest";
12
13
 
@@ -17,8 +18,6 @@ import type {
17
18
  AgentOutcome as SdkAgentOutcome,
18
19
  EngineCapabilities as SdkEngineCapabilities,
19
20
  EngineHandleData as SdkEngineHandleData,
20
- InteractAction as SdkInteractAction,
21
- InteractResult as SdkInteractResult,
22
21
  ProbeReport as SdkProbeReport,
23
22
  SessionView as SdkSessionView,
24
23
  UiRequest as SdkUiRequest,
@@ -37,8 +36,6 @@ import type {
37
36
  AgentOutcome as CoreAgentOutcome,
38
37
  EngineCapabilities as CoreEngineCapabilities,
39
38
  EngineHandleData as CoreEngineHandleData,
40
- InteractAction as CoreInteractAction,
41
- InteractResult as CoreInteractResult,
42
39
  ProbeReport as CoreProbeReport,
43
40
  SessionView as CoreSessionView,
44
41
  } from "../../types.ts";
@@ -54,8 +51,6 @@ type _CoreSdkAgentEvent = AssertMutuallyAssignable<CoreAgentEvent, SdkAgentEvent
54
51
  type _CoreSdkAgentOutcome = AssertMutuallyAssignable<CoreAgentOutcome, SdkAgentOutcome>;
55
52
  type _CoreSdkEngineCapabilities = AssertMutuallyAssignable<CoreEngineCapabilities, SdkEngineCapabilities>;
56
53
  type _CoreSdkEngineHandleData = AssertMutuallyAssignable<CoreEngineHandleData, SdkEngineHandleData>;
57
- type _CoreSdkInteractAction = AssertMutuallyAssignable<CoreInteractAction, SdkInteractAction>;
58
- type _CoreSdkInteractResult = AssertMutuallyAssignable<CoreInteractResult, SdkInteractResult>;
59
54
  type _CoreSdkProbeReport = AssertMutuallyAssignable<CoreProbeReport, SdkProbeReport>;
60
55
  type _CoreSdkSessionView = AssertMutuallyAssignable<CoreSessionView, SdkSessionView>;
61
56
  type _CoreSdkWorktreeHandle = AssertMutuallyAssignable<CoreWorktreeHandle, SdkWorktreeHandle>;
@@ -75,8 +70,6 @@ const _assertions: Array<true> = [
75
70
  true as _CoreSdkAgentOutcome,
76
71
  true as _CoreSdkEngineCapabilities,
77
72
  true as _CoreSdkEngineHandleData,
78
- true as _CoreSdkInteractAction,
79
- true as _CoreSdkInteractResult,
80
73
  true as _CoreSdkProbeReport,
81
74
  true as _CoreSdkSessionView,
82
75
  true as _CoreSdkWorktreeHandle,
@@ -89,7 +82,7 @@ const _assertions: Array<true> = [
89
82
 
90
83
  describe("协议契约类型双向可赋值(编译期断言的运行时锚)", () => {
91
84
  it("全部断言成立(漂移会在 tsc --noEmit 报 never → 本文件编译失败)", () => {
92
- expect(_assertions).toHaveLength(14);
85
+ expect(_assertions).toHaveLength(12);
93
86
  expect(_assertions.every((v) => v === true)).toBe(true);
94
87
  });
95
88
  });
@@ -15,6 +15,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
15
15
  import { EngineClient } from "../engine-client.ts";
16
16
  import { RemoteEngine, type RemoteEngineManifestSnapshot } from "../remote-engine.ts";
17
17
  import { SubagentStream } from "../../../stream-sink.ts";
18
+ import { getSubagentSessionDir } from "../../../path-encoding.ts";
18
19
  import { isProcessAlive } from "../pid-file.ts";
19
20
  import { getLogger, type UiRequest } from "@zhushanwen/subagent-engine-sdk";
20
21
 
@@ -253,6 +254,49 @@ describe("RemoteEngine run 帧映射", () => {
253
254
  await cleanup();
254
255
  });
255
256
 
257
+ it("ctx.sessionRootId → wire ctx.sessionRootId 逐字保真;缺省不传 → wire 无该键(F6 additive)", async () => {
258
+ // 正向:根 session id 上 wire(pi 引擎 relay 归属键 SESSION_ID 的权威来源)
259
+ const withRoot = makeEngine();
260
+ const { ctx: ctxRoot, events: eventsRoot } = makeCtx({ sessionRootId: "root-sess-f6" });
261
+ await withRoot.engine.run({ prompt: "p" }, ctxRoot);
262
+ expect(extractRunParams(eventsRoot).ctx.sessionRootId).toBe("root-sess-f6");
263
+
264
+ // 负向:缺省 → wire ctx 不出现该键(undefined 不上 wire,旧引擎 additive 兼容)
265
+ const bare = makeEngine();
266
+ const { ctx: ctxBare, events: eventsBare } = makeCtx();
267
+ await bare.engine.run({ prompt: "p" }, ctxBare);
268
+ expect(extractRunParams(eventsBare).ctx).not.toHaveProperty("sessionRootId");
269
+
270
+ await withRoot.cleanup();
271
+ await bare.cleanup();
272
+ });
273
+
274
+ it("[Option C] wire ctx.sessionDir = getSubagentSessionDir(env 同源推导);ctx 显式注入优先", async () => {
275
+ // stub env 固定推导输入(测试进程可能已带这两键——stub 后期望值确定)
276
+ vi.stubEnv("PI_CODING_AGENT_DIR", join(dataDir, "agent-dir"));
277
+ vi.stubEnv("PI_SUBAGENT_ROOT_CWD", join(dataDir, "root-cwd"));
278
+ try {
279
+ // 正向:env 同源推导值上 wire(宿主单一权威 getSubagentSessionDir,引擎不自推导)
280
+ const derived = makeEngine();
281
+ const { ctx, events } = makeCtx();
282
+ await derived.engine.run({ prompt: "p" }, ctx);
283
+ expect(extractRunParams(events).ctx.sessionDir).toBe(
284
+ getSubagentSessionDir(join(dataDir, "agent-dir"), join(dataDir, "root-cwd")),
285
+ );
286
+
287
+ // 宿主编排层显式注入(ctx.sessionDir)优先于 env 推导
288
+ const injected = makeEngine();
289
+ const { ctx: ctxInj, events: eventsInj } = makeCtx({ sessionDir: "/explicit/session-dir" });
290
+ await injected.engine.run({ prompt: "p" }, ctxInj);
291
+ expect(extractRunParams(eventsInj).ctx.sessionDir).toBe("/explicit/session-dir");
292
+
293
+ await derived.cleanup();
294
+ await injected.cleanup();
295
+ } finally {
296
+ vi.unstubAllEnvs();
297
+ }
298
+ });
299
+
256
300
  it("poolResolved / handleReady → RunContext 回调(journal 路径权威 + 运行中句柄回填)", async () => {
257
301
  const pools: string[] = [];
258
302
  const readies: Array<{ sessionRef: Record<string, string>; poolKey: string }> = [];
@@ -393,7 +437,7 @@ describe("gate 位多声明 run 期判定(W3 契约⑤接线:manifest vs ini
393
437
  });
394
438
  });
395
439
 
396
- describe("interact / read / probe / dispose 门面", () => {
440
+ describe("read / probe / dispose 门面([H1 U6] interact 断言随 interact 面退役删除)", () => {
397
441
  it("read 协议帧携带 dataDir(必填:存量池时代相对 dbPath 定位)", async () => {
398
442
  const { engine, cleanup } = makeEngine();
399
443
  const view = await engine.read({
@@ -411,21 +455,11 @@ describe("interact / read / probe / dispose 门面", () => {
411
455
  await cleanup();
412
456
  });
413
457
 
414
- it("probe → ProbeReport;interact → delivered;dispose → client 停机(幂等)", async () => {
458
+ it("probe → ProbeReport;dispose → client 停机(幂等)", async () => {
415
459
  const { engine, cleanup } = makeEngine();
416
460
  const report = await engine.probe();
417
461
  expect(report.ok).toBe(true);
418
462
  expect(report.engineVersion).toBe("fake-1.0.0");
419
- const interact = await engine.interact({
420
- data: {
421
- v: 1,
422
- engineId: "fake",
423
- sessionRef: { sessionId: "s-1" },
424
- poolKey: "shared",
425
- adapterVersion: "fake-adapter",
426
- },
427
- }, { kind: "message", payload: "hi" });
428
- expect(interact).toEqual({ ok: true, delivered: true });
429
463
  await engine.dispose();
430
464
  await expect(engine.dispose()).resolves.toBeUndefined(); // 幂等
431
465
  await cleanup();