@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
@@ -0,0 +1,205 @@
1
+ // src/execution/__tests__/round-supervisor-workflow-origin.test.ts
2
+ //
3
+ // [H2 W2] 治理决策表逐族——workflow origin record 对 round-supervisor 的豁免面
4
+ //(设计 subagent-workflow-record-unification.md §3.3 决策表 v3):
5
+ //
6
+ // - adopt 接管:豁免(adoptOnProcessDeath 对 workflow record 不纳管——引擎死亡即
7
+ // run 失败即 record 终态化,无脚本可回的 resumable 等待无意义);
8
+ // - boot 分区重认领:豁免负面断言(classifySupervisorDomain 不把 workflow record
9
+ // 归入可 adopt 域 "run");
10
+ // - superseded 分类:workflow origin 候选豁免(parallel 同 slug 并行是 workflow
11
+ // 常态而非替代关系——replacedNotice 不再把并行任务误述为替代);
12
+ // - 运行期监督:照旧纳管(H1 D8「非 chatMode 全量纳管」不因 adopt 豁免收窄——
13
+ // noteRunStarted/noteRunEnded 记账对 workflow record 无 origin 门,只豁免 adopt)。
14
+ //
15
+ // 纯监督器单测:deps 全替身注入(对齐 round-supervisor.test.ts 形态,零 fs 依赖)。
16
+
17
+ import { describe, expect, it, vi } from "vitest";
18
+
19
+ import type { ExecutionRecord } from "../types.ts";
20
+ import {
21
+ RoundSupervisor,
22
+ type RoundSupervisorDeps,
23
+ type SupervisorCandidateRecord,
24
+ type SupervisorRecordView,
25
+ } from "../round-supervisor/index.ts";
26
+
27
+ interface TestDeps extends RoundSupervisorDeps {
28
+ notices: string[];
29
+ guidances: Array<{ record: SupervisorRecordView; exemptionDisclaimer: boolean }>;
30
+ replaced: Array<{ record: SupervisorRecordView; replacementId: string }>;
31
+ givenUp: Array<{ recordId: string; kind: string; detail: { replacementId?: string } }>;
32
+ views: Map<string, SupervisorRecordView>;
33
+ candidates: SupervisorCandidateRecord[];
34
+ live: Set<string>;
35
+ }
36
+
37
+ function makeDeps(): TestDeps {
38
+ const views = new Map<string, SupervisorRecordView>();
39
+ const candidates: SupervisorCandidateRecord[] = [];
40
+ const live = new Set<string>();
41
+ const notices: string[] = [];
42
+ const guidances: Array<{ record: SupervisorRecordView; exemptionDisclaimer: boolean }> = [];
43
+ const replaced: Array<{ record: SupervisorRecordView; replacementId: string }> = [];
44
+ const givenUp: Array<{ recordId: string; kind: string; detail: { replacementId?: string } }> = [];
45
+ const deps: RoundSupervisorDeps = {
46
+ now: () => Date.now(),
47
+ getRecordView: (id) => views.get(id),
48
+ listCandidateRecords: () => candidates,
49
+ hasLiveProcess: (recordId) => live.has(recordId),
50
+ sendMergedFailureNotice: (record) => notices.push(`merged:${record.id}`),
51
+ sendDecisionGuidance: (record, opts) => guidances.push({ record, exemptionDisclaimer: opts.exemptionDisclaimer }),
52
+ sendReplacedNotice: (record, replacementId) => replaced.push({ record, replacementId }),
53
+ giveUp: (recordId, kind, detail) => givenUp.push({ recordId, kind, detail }),
54
+ };
55
+ return Object.assign(deps, { notices, guidances, replaced, givenUp, views, candidates, live });
56
+ }
57
+
58
+ /** 死亡纳管入参形态的内存 record 替身。 */
59
+ function makeRecord(overrides: Partial<ExecutionRecord> = {}): ExecutionRecord {
60
+ return {
61
+ id: "bg-1",
62
+ agent: "worker",
63
+ model: "m",
64
+ mode: "background",
65
+ task: "t",
66
+ slug: "fix-bug",
67
+ startedAt: Date.now() - 1000,
68
+ status: "running",
69
+ resumable: true,
70
+ rootSessionId: "sess-root",
71
+ turnCount: 0,
72
+ ...overrides,
73
+ } as ExecutionRecord;
74
+ }
75
+
76
+ /** 该唤醒形态 view(running + resumable + 无产出 + 无驱动)。 */
77
+ function awakeView(id: string, over: Partial<SupervisorRecordView> = {}): SupervisorRecordView {
78
+ return {
79
+ id,
80
+ status: "running",
81
+ resumable: true,
82
+ hasResult: false,
83
+ chatMode: false,
84
+ rootSessionId: "sess-root",
85
+ agent: "worker",
86
+ slug: "fix-bug",
87
+ startedAt: 1,
88
+ closedReason: undefined,
89
+ ...over,
90
+ };
91
+ }
92
+
93
+ describe("adopt 豁免(决策表 v3 改判)", () => {
94
+ it("workflow record 死亡事件不纳管:零合并通知/零指引/零纳管记账;tool 对照全链照常", () => {
95
+ const deps = makeDeps();
96
+ const supervisor = new RoundSupervisor(deps);
97
+ deps.views.set("bg-wf", awakeView("bg-wf", { origin: "workflow" }));
98
+ deps.views.set("bg-tool", awakeView("bg-tool"));
99
+
100
+ supervisor.adoptOnProcessDeath(
101
+ makeRecord({ id: "bg-wf", origin: "workflow", parentRunId: "run-1" }),
102
+ "engine crashed",
103
+ );
104
+ expect(deps.notices).toEqual([]);
105
+ expect(deps.guidances).toHaveLength(0);
106
+ expect(supervisor.supervisedIds()).toEqual([]);
107
+
108
+ // 对照:origin=tool 同形态照常 adopt(合并通知 + 指引 + 纳管)
109
+ supervisor.adoptOnProcessDeath(makeRecord({ id: "bg-tool" }), "engine crashed");
110
+ expect(deps.notices).toEqual(["merged:bg-tool"]);
111
+ expect(deps.guidances).toHaveLength(1);
112
+ expect(supervisor.supervisedIds()).toEqual(["bg-tool"]);
113
+ });
114
+
115
+ it("conversation 豁免语义不回归:chatMode record 照旧不入监督域", () => {
116
+ const deps = makeDeps();
117
+ const supervisor = new RoundSupervisor(deps);
118
+ supervisor.adoptOnProcessDeath(makeRecord({ id: "bg-chat", chatMode: true }), "x");
119
+ expect(supervisor.supervisedIds()).toEqual([]);
120
+ });
121
+ });
122
+
123
+ describe("boot 分区对 workflow 形态负面断言(重认领豁免)", () => {
124
+ it("running+resumable+无产出的 workflow record 不被 boot 重认领;tool 对照被认领", () => {
125
+ const deps = makeDeps();
126
+ const supervisor = new RoundSupervisor(deps);
127
+ deps.views.set("bg-wf", awakeView("bg-wf", { origin: "workflow" }));
128
+ deps.views.set("bg-tool", awakeView("bg-tool"));
129
+ deps.candidates.push(
130
+ { id: "bg-wf", rootSessionId: "sess-root", agent: "worker", slug: "fix-bug", startedAt: 1, origin: "workflow" },
131
+ { id: "bg-tool", rootSessionId: "sess-root", agent: "worker", slug: "fix-bug", startedAt: 1 },
132
+ );
133
+
134
+ const { readopted } = supervisor.bootPartition();
135
+ expect(readopted).toEqual(["bg-tool"]); // workflow 形态被 classifySupervisorDomain 挡在可 adopt 域外
136
+ expect(supervisor.supervisedIds()).toEqual(["bg-tool"]);
137
+ expect(deps.guidances).toHaveLength(1); // 仅 tool record 的评估推进
138
+ });
139
+ });
140
+
141
+ describe("superseded 豁免(parallel 同 slug 并行非替代)", () => {
142
+ it("adopted tool record 的替代判定忽略 workflow origin 候选:无 replacedNotice/无 superseded giveUp", () => {
143
+ const deps = makeDeps();
144
+ const supervisor = new RoundSupervisor(deps);
145
+ deps.views.set("bg-tool", awakeView("bg-tool"));
146
+ // workflow 候选:同 root/同 agent/同 slug、看门狗窗内新建——旧判定(high-confidence)
147
+ // 会误判替代;豁免后不参与对账(连 low-confidence 的 sameAgentOnly 都不触发)
148
+ deps.candidates.push(
149
+ { id: "bg-wf-1", rootSessionId: "sess-root", agent: "worker", slug: "fix-bug", startedAt: Date.now() - 1000, origin: "workflow" },
150
+ { id: "bg-wf-2", rootSessionId: "sess-root", agent: "worker", slug: "fix-bug", startedAt: Date.now() - 500, origin: "workflow" },
151
+ );
152
+
153
+ supervisor.adoptOnProcessDeath(makeRecord({ id: "bg-tool" }), "engine crashed");
154
+ expect(deps.replaced).toEqual([]);
155
+ expect(deps.givenUp).toEqual([]);
156
+ expect(deps.guidances).toHaveLength(1); // 无替代命中 → 正常送决策指引
157
+ expect(deps.guidances[0]!.exemptionDisclaimer).toBe(false); // verdict=none(连 low-confidence 都不算)
158
+ });
159
+
160
+ it("对照:tool 来源同 slug 候选仍高置信替代(豁免只滤 workflow origin)", () => {
161
+ const deps = makeDeps();
162
+ const supervisor = new RoundSupervisor(deps);
163
+ deps.views.set("bg-tool", awakeView("bg-tool"));
164
+ deps.candidates.push({
165
+ id: "bg-tool-2",
166
+ rootSessionId: "sess-root",
167
+ agent: "worker",
168
+ slug: "fix-bug",
169
+ startedAt: Date.now() - 1000,
170
+ });
171
+
172
+ supervisor.adoptOnProcessDeath(makeRecord({ id: "bg-tool" }), "engine crashed");
173
+ expect(deps.replaced).toEqual([{ record: expect.objectContaining({ id: "bg-tool" }), replacementId: "bg-tool-2" }]);
174
+ expect(deps.givenUp).toEqual([{ recordId: "bg-tool", kind: "superseded", detail: { replacementId: "bg-tool-2" } }]);
175
+ });
176
+ });
177
+
178
+ describe("运行期监督照旧纳管(H1 D8 不收窄——只豁免 adopt)", () => {
179
+ it("noteRunStarted/noteRunEnded 对 workflow record 无 origin 门(在途记账照旧)", () => {
180
+ const deps = makeDeps();
181
+ const supervisor = new RoundSupervisor(deps);
182
+ // 在途记账面无 origin 分支:workflow record id 正常进/出 inFlightRuns,不抛不静默跳过
183
+ expect(() => {
184
+ supervisor.noteRunStarted("bg-wf");
185
+ supervisor.noteRunEnded("bg-wf");
186
+ }).not.toThrow();
187
+ expect(supervisor.supervisedIds()).toEqual([]);
188
+
189
+ // 记账解除后死亡事件仍被 adopt 豁免拦截(豁免面 = adopt 入口,非记账面)
190
+ deps.views.set("bg-wf", awakeView("bg-wf", { origin: "workflow" }));
191
+ supervisor.adoptOnProcessDeath(makeRecord({ id: "bg-wf", origin: "workflow" }), "boom");
192
+ expect(supervisor.supervisedIds()).toEqual([]);
193
+ });
194
+
195
+ it("noteRun* 记账对已纳管 tool record 的「该等→重评估」语义不回归", () => {
196
+ const deps = makeDeps();
197
+ const supervisor = new RoundSupervisor(deps);
198
+ deps.views.set("bg-tool", awakeView("bg-tool"));
199
+ supervisor.noteRunStarted("bg-tool");
200
+ supervisor.adoptOnProcessDeath(makeRecord({ id: "bg-tool" }), "boom"); // 在途 → 该等
201
+ expect(deps.guidances).toHaveLength(0);
202
+ supervisor.noteRunEnded("bg-tool"); // run 收口 → 重评估 → 该唤醒
203
+ expect(deps.guidances).toHaveLength(1);
204
+ });
205
+ });
@@ -0,0 +1,293 @@
1
+ // src/execution/__tests__/run-orchestration-write-lease.test.ts
2
+ //
3
+ // [U2b / C3] spawn 侧写权声明挂钩(D3a v8 时机①——fresh spawn 全程声明,缺口 1
4
+ // 闭合)+ one-shot 成功分支 markRoundIdle 接线(SP-5 与 chat 轮末共享轮终语义)的
5
+ // 单元级断言:
6
+ // 1. one-shot pi 轮(kickOffChatRound 回填点):run 应答 sessionFile 回填后
7
+ // `.alive` 存在且 pid=本进程;轮终(settleOneShotOutcome SP-5 → markRoundIdle)
8
+ // 后 marker 跨轮保留(D3a/B5);
9
+ // 2. 非 pi 引擎死亡 adopt 形态(finalizeEngineOutcome 回填点):sessionFile 回填
10
+ // 即声明写权,record 保持 resumable、marker 在(adopt 不终态化 → 声明不释放);
11
+ // 3. settleOneShotOutcome 直驱:markRoundIdle 簿记(running-resumable / round+1 /
12
+ // result / closedReason 清除)+ `.alive` 不删 + pending:unregister 发射点②
13
+ // store 簿记⑧单轨发射(恰好一次);
14
+ // 4. workflow 域(executeAndAwait → runAndFinalize → outcomeToAgentResult 主回填
15
+ // 点):sessionFile 回填后 `.alive` 存在且 pid=本进程;
16
+ // 5. [U2b 修复轮/D2] adoptEngineDeath 归口(adoptResumableAfterEngineDeath 三写
17
+ // error/result/resumable 语义等价,真实 store 链)。轮始 markRoundStarted 接线
18
+ // 的真实链用例在 conversation-continuation.test.ts 集成面(续聊派发即清
19
+ // result/resumable + round 累加链不断)。
20
+ //
21
+ // 测试纪律:registerFakePiEngine 协议替身(conversation-continuation.test.ts 同源
22
+ // setup 形态,真实 timers + vi.waitFor);sessionFile/marker 全部落 mkdtemp 自建目录;
23
+ // store 走服务内嵌真实 RecordStore(acquireWriteLease/markRoundIdle 真实实现)。
24
+
25
+ import * as fs from "node:fs";
26
+ import * as os from "node:os";
27
+ import * as path from "node:path";
28
+
29
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
30
+
31
+ import type { AgentOutcome } from "../engine/types.ts";
32
+ import { clearEngines } from "../engine/registry.ts";
33
+ import { registerFakePiEngine, type FakePiEnginePort } from "./helpers/fake-engine-port.ts";
34
+ import { createRecord } from "../execution-record.ts";
35
+ import { ModelConfigService } from "../model-config-service.ts";
36
+ import type { RecordStore } from "../record-store.ts";
37
+ import { SubagentService } from "../subagent-service.ts";
38
+ import type { PiLike } from "../subagent-service.ts";
39
+ import { _resetLifecycleState } from "../lifecycle-manager.ts";
40
+ import { _resetSettledWatchdogsForTest } from "../settled-watchdog.ts";
41
+ import { _resetCoreSpawnedChildrenMirrorForTest } from "../engine/host/spawned-children.ts";
42
+ import type { AgentResult, ExecutionRecord } from "../types.ts";
43
+
44
+ interface ServiceInternals {
45
+ store: RecordStore;
46
+ runOrchestration: {
47
+ settleOneShotOutcome: (record: ExecutionRecord, result: AgentResult, aborted: boolean) => Promise<void>;
48
+ finalizeEngineOutcome: (record: ExecutionRecord, outcome: AgentOutcome) => Promise<boolean>;
49
+ };
50
+ }
51
+
52
+ function makePi(): PiLike {
53
+ return {
54
+ appendEntry: vi.fn(),
55
+ events: { emit: vi.fn() },
56
+ sendMessage: vi.fn(),
57
+ } as unknown as PiLike;
58
+ }
59
+
60
+ function makeService(): {
61
+ agentDir: string;
62
+ service: SubagentService;
63
+ store: RecordStore;
64
+ pi: PiLike & { events: { emit: ReturnType<typeof vi.fn> } };
65
+ fake: FakePiEnginePort;
66
+ runOrchestration: ServiceInternals["runOrchestration"];
67
+ } {
68
+ const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "write-lease-"));
69
+ // 引擎数据目录指 tmp(journal 落盘根——runAndFinalize 的 wireEventJournal 消费;
70
+ // 测试红线:不触真实数据目录,workflow-agent-dispatch.test.ts 同款范式)。
71
+ process.env.XYZ_AGENT_DATA_DIR = path.join(agentDir, "engine-data");
72
+ clearEngines();
73
+ const fake = registerFakePiEngine();
74
+ const modelService = new ModelConfigService({ agentDir, cwd: agentDir });
75
+ modelService.initModel({
76
+ modelRegistry: { getAvailable: () => [], find: () => undefined, hasConfiguredAuth: () => true },
77
+ sessionId: "root-session",
78
+ ctxModel: { id: "m", name: "M", provider: "prov", reasoning: false },
79
+ });
80
+ const service = new SubagentService({ cwd: agentDir, modelService });
81
+ const pi = makePi() as PiLike & { events: { emit: ReturnType<typeof vi.fn> } };
82
+ service.initSession({ pi, sessionId: "root-session" });
83
+ const { store, runOrchestration } = service as unknown as ServiceInternals;
84
+ return { agentDir, service, store, pi, fake, runOrchestration };
85
+ }
86
+
87
+ /** 读 `.alive` marker(断言用——存在性 + pid/id 身份域)。 */
88
+ function readAliveMarker(sessionFile: string): { pid: number; id: string; startedAt: number } {
89
+ return JSON.parse(fs.readFileSync(`${sessionFile}.alive`, "utf-8")) as {
90
+ pid: number;
91
+ id: string;
92
+ startedAt: number;
93
+ };
94
+ }
95
+
96
+ let prevDataDirEnv: string | undefined;
97
+
98
+ beforeEach(() => {
99
+ prevDataDirEnv = process.env["XYZ_AGENT_DATA_DIR"];
100
+ });
101
+
102
+ afterEach(() => {
103
+ _resetLifecycleState();
104
+ _resetSettledWatchdogsForTest();
105
+ _resetCoreSpawnedChildrenMirrorForTest();
106
+ if (prevDataDirEnv === undefined) delete process.env["XYZ_AGENT_DATA_DIR"];
107
+ else process.env["XYZ_AGENT_DATA_DIR"] = prevDataDirEnv;
108
+ });
109
+
110
+ describe("spawn 侧写权声明挂钩(D3a v8 时机①——U2b/C3)", () => {
111
+ it("one-shot pi 轮:run 应答 sessionFile 回填 → `.alive` 存在且 pid=本进程;SP-5 轮终后跨轮保留", async () => {
112
+ const h = makeService();
113
+ try {
114
+ const sessionFile = path.join(h.agentDir, "oneshot-session.jsonl");
115
+ const handle = await h.service.execute({ task: "lease probe", slug: "lease" });
116
+ await vi.waitFor(() => expect(h.fake.runs.length).toBe(1));
117
+ h.fake.runs[0]!.settle({ content: "done text", sessionFile });
118
+
119
+ const record = h.store.getMutable(handle.subagentId);
120
+ await vi.waitFor(() => expect(record?.resumable).toBe(true));
121
+ // SP-5:成功轮保持 running-resumable(markRoundIdle 簿记)
122
+ expect(record?.status).toBe("running");
123
+ expect(record?.closedReason).toBeUndefined();
124
+ expect(record?.result).toBe("done text");
125
+ expect(record?.sessionFile).toBe(sessionFile);
126
+ // [D3a 时机①] 锚点确立即声明写权(kickOffChatRound 回填点)
127
+ expect(readAliveMarker(sessionFile)).toMatchObject({ pid: process.pid, id: handle.subagentId });
128
+ // [D3a/B5] 轮终跨轮保留——release 出口只有终态原语/idle-GC 归档
129
+ expect(fs.existsSync(`${sessionFile}.alive`)).toBe(true);
130
+ // [UF-1] 绑定 sidecar 同步落盘(既有行为不回归)
131
+ expect(fs.existsSync(`${sessionFile}.record-binding`)).toBe(true);
132
+ } finally {
133
+ h.service.dispose();
134
+ clearEngines();
135
+ fs.rmSync(h.agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
136
+ }
137
+ });
138
+
139
+ it("非 pi 引擎死亡 adopt 形态(finalizeEngineOutcome 回填点):sessionFile 回填即声明写权,record 保持 resumable 且 marker 在", async () => {
140
+ const h = makeService();
141
+ try {
142
+ const sessionFile = path.join(h.agentDir, "adopt-session.jsonl");
143
+ fs.writeFileSync(sessionFile, "{}\n", "utf-8");
144
+ const record = createRecord("bg-adopt", {
145
+ agent: "general-purpose",
146
+ model: "prov/model-1",
147
+ mode: "background",
148
+ task: "t",
149
+ slug: "adopt",
150
+ startedAt: 1000,
151
+ rootSessionId: "root-session",
152
+ controller: new AbortController(),
153
+ });
154
+ h.store.register(record);
155
+
156
+ const adopted = await h.runOrchestration.finalizeEngineOutcome(record, {
157
+ content: "",
158
+ engineId: "zcode",
159
+ error: "engine crashed: process killed by signal",
160
+ exitCode: null,
161
+ sessionFile,
162
+ });
163
+
164
+ // adopt 分支:record 保持 resumable 交监督器(不终态化 → 写权声明不释放)
165
+ expect(adopted).toBe(true);
166
+ expect(record.error).toContain("engine crashed");
167
+ expect(record.sessionFile).toBe(sessionFile);
168
+ expect(readAliveMarker(sessionFile)).toMatchObject({ pid: process.pid, id: "bg-adopt" });
169
+ } finally {
170
+ h.service.dispose();
171
+ clearEngines();
172
+ fs.rmSync(h.agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
173
+ }
174
+ });
175
+
176
+ it("[U2b 修复轮/D2] adoptEngineDeath 归口:error/result/resumable 三写语义等价(真实 store 链)", async () => {
177
+ const h = makeService();
178
+ try {
179
+ const sessionFile = path.join(h.agentDir, "adopt-writes.jsonl");
180
+ fs.writeFileSync(sessionFile, "{}\n", "utf-8");
181
+ const record = createRecord("bg-adopt-writes", {
182
+ agent: "general-purpose",
183
+ model: "prov/model-1",
184
+ mode: "background",
185
+ task: "t",
186
+ slug: "adopt",
187
+ startedAt: 1000,
188
+ rootSessionId: "root-session",
189
+ controller: new AbortController(),
190
+ });
191
+ // 收养前形态:上一轮 result 在盘 + 无 resumable 信号(被收养对象的典型前置)
192
+ record.result = "previous round output";
193
+ record.resumable = undefined;
194
+ h.store.register(record);
195
+
196
+ const adopted = await h.runOrchestration.finalizeEngineOutcome(record, {
197
+ content: "",
198
+ engineId: "zcode",
199
+ error: "engine crashed: SIGKILL",
200
+ exitCode: null,
201
+ sessionFile,
202
+ });
203
+
204
+ expect(adopted).toBe(true);
205
+ // 三写等价(store.adoptEngineDeath):error 如实 + result 清(禁旧正文冒充
206
+ // 收养后产出)+ resumable=true(GUI waiting 判据)
207
+ expect(record.error).toBe("engine crashed: SIGKILL");
208
+ expect(record.result).toBeUndefined();
209
+ expect(record.resumable).toBe(true);
210
+ // 归口不改变 adopt 分支的产品语义:保持 running(不终态化、交监督器)
211
+ expect(record.status).toBe("running");
212
+ } finally {
213
+ h.service.dispose();
214
+ clearEngines();
215
+ fs.rmSync(h.agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
216
+ }
217
+ });
218
+
219
+ it("workflow 域(executeAndAwait → outcomeToAgentResult 回填点):sessionFile 回填 → `.alive` 存在且 pid=本进程", async () => {
220
+ const h = makeService();
221
+ try {
222
+ const sessionFile = path.join(h.agentDir, "wf-session.jsonl");
223
+ const execP = h.service.executeAndAwait({ task: "wf lease", slug: "wf" });
224
+ await vi.waitFor(() => expect(h.fake.runs.length).toBe(1));
225
+ h.fake.runs[0]!.settle({ content: "wf done", sessionFile });
226
+ const result = await execP;
227
+
228
+ // 成功应答映射(workflow 域 AgentResult.content 承载正文)
229
+ expect(result.content).toBe("wf done");
230
+ const actives = h.store.listAllActive();
231
+ expect(actives).toHaveLength(1);
232
+ const rec = actives[0]!;
233
+ expect(rec.sessionFile).toBe(sessionFile);
234
+ expect(readAliveMarker(sessionFile)).toMatchObject({ pid: process.pid, id: rec.id });
235
+ } finally {
236
+ h.service.dispose();
237
+ clearEngines();
238
+ fs.rmSync(h.agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
239
+ }
240
+ });
241
+ });
242
+
243
+ describe("settleOneShotOutcome SP-5 成功分支 → store.markRoundIdle 接线(U2b/C3)", () => {
244
+ it("markRoundIdle 簿记 + `.alive` 不删(D3a 跨轮保留)+ pending:unregister 发射点② store 簿记⑧单轨发射恰好一次", async () => {
245
+ const h = makeService();
246
+ try {
247
+ const sessionFile = path.join(h.agentDir, "sp5-session.jsonl");
248
+ fs.writeFileSync(sessionFile, "{}\n", "utf-8");
249
+ const record = createRecord("bg-sp5", {
250
+ agent: "general-purpose",
251
+ model: "prov/model-1",
252
+ mode: "background",
253
+ task: "t",
254
+ slug: "sp5",
255
+ startedAt: 1000,
256
+ rootSessionId: "root-session",
257
+ controller: new AbortController(),
258
+ });
259
+ record.round = 1;
260
+ record.sessionFile = sessionFile;
261
+ h.store.register(record);
262
+ // 在途轮的写权声明(模拟本轮回填点已 acquire 的形态)
263
+ h.store.acquireWriteLease(sessionFile, record.id);
264
+
265
+ await h.runOrchestration.settleOneShotOutcome(
266
+ record,
267
+ { text: "round done", turns: 1, durationMs: 100, success: true, sessionId: record.id, toolCalls: [] },
268
+ false,
269
+ );
270
+
271
+ // 簿记①-⑥:保持 running + result 写入 + round+1 + closedReason 清除 + resumable
272
+ expect(record.status).toBe("running");
273
+ expect(record.resumable).toBe(true);
274
+ expect(record.closedReason).toBeUndefined();
275
+ expect(record.round).toBe(2);
276
+ expect(record.result).toBe("round done");
277
+ // 簿记⑦:`.alive` 保留(旧 doFinalizeRoundToIdle 删点随归口移除——B5/D3a)
278
+ expect(fs.existsSync(`${sessionFile}.alive`)).toBe(true);
279
+ // 簿记⑧单轨:注销发射点②已收口 store 簿记⑧(SubagentService 构造点经
280
+ // setPendingUnregister 注入 notifyHost.emitPendingUnregister),本用例期间
281
+ // pi.events.emit 仅此一笔——调用方直发回归(双轨重复注销)会使计数 >1 而红
282
+ expect(h.pi.events.emit).toHaveBeenCalledTimes(1);
283
+ expect(h.pi.events.emit).toHaveBeenCalledWith("pending:unregister", {
284
+ id: "bg-sp5",
285
+ reason: "running",
286
+ });
287
+ } finally {
288
+ h.service.dispose();
289
+ clearEngines();
290
+ fs.rmSync(h.agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
291
+ }
292
+ });
293
+ });