@zhushanwen/subagent-core 0.8.0 → 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 (167) hide show
  1. package/dist/{chunk-OL5BK4VN.js → chunk-LZJOEDGQ.js} +455 -203
  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 +216 -155
  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 +7686 -5894
  10. package/dist/index.d.cts +1876 -1360
  11. package/dist/index.d.ts +1876 -1360
  12. package/dist/index.js +7048 -5493
  13. package/dist.bundle/index.cjs +7747 -5957
  14. package/package.json +4 -4
  15. package/src/__tests__/manifest-store.test.ts +16 -15
  16. package/src/__tests__/robustness-medium-batch1.test.ts +10 -14
  17. package/src/__tests__/robustness-medium-batch2.test.ts +7 -2
  18. package/src/execution/__tests__/alive-store.test.ts +117 -1
  19. package/src/execution/__tests__/batch-finalized.test.ts +206 -0
  20. package/src/execution/__tests__/chat-engine-routing.test.ts +117 -1
  21. package/src/execution/__tests__/{cold-resurrect.test.ts → cold-lookup.test.ts} +133 -33
  22. package/src/execution/__tests__/collect-coordinator-service.test.ts +7 -5
  23. package/src/execution/__tests__/conversation-continuation.test.ts +1452 -0
  24. package/src/execution/__tests__/delivery-methods.test.ts +90 -176
  25. package/src/execution/__tests__/dialog-queue.test.ts +70 -3
  26. package/src/execution/__tests__/dispose-manifest-recovery.test.ts +475 -0
  27. package/src/execution/__tests__/execute-and-await-worktree.test.ts +3 -3
  28. package/src/execution/__tests__/execute-nesting.test.ts +8 -5
  29. package/src/execution/__tests__/execution-record.test.ts +8 -148
  30. package/src/execution/__tests__/finalize-record.test.ts +295 -129
  31. package/src/execution/__tests__/get-record-for-action-restart.test.ts +21 -29
  32. package/src/execution/__tests__/helpers/fake-engine-port.ts +13 -68
  33. package/src/execution/__tests__/helpers/legacy-sidecar.ts +33 -0
  34. package/src/execution/__tests__/helpers/subagent-service-mocks.ts +9 -5
  35. package/src/execution/__tests__/idle-gc.test.ts +60 -0
  36. package/src/execution/__tests__/lifecycle-manager.test.ts +3 -254
  37. package/src/execution/__tests__/manifest-store-tmp-recovery.test.ts +43 -22
  38. package/src/execution/__tests__/model-config-service.test.ts +111 -0
  39. package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +10 -6
  40. package/src/execution/__tests__/notify-ledger.test.ts +42 -1
  41. package/src/execution/__tests__/rebuild-indexes.test.ts +329 -0
  42. package/src/execution/__tests__/record-binding.test.ts +613 -0
  43. package/src/execution/__tests__/record-origin.test.ts +318 -0
  44. package/src/execution/__tests__/record-store-intent-api.test.ts +600 -0
  45. package/src/execution/__tests__/record-store-orphan-revive.test.ts +71 -3
  46. package/src/execution/__tests__/record-store.test.ts +72 -54
  47. package/src/execution/__tests__/recursive-visibility-baseline.test.ts +8 -5
  48. package/src/execution/__tests__/round-supervisor-workflow-origin.test.ts +205 -0
  49. package/src/execution/__tests__/run-orchestration-write-lease.test.ts +293 -0
  50. package/src/execution/__tests__/run-orchestration.test.ts +199 -0
  51. package/src/execution/__tests__/session-file-gc.test.ts +35 -4
  52. package/src/execution/__tests__/session-reconstructor.test.ts +78 -0
  53. package/src/execution/__tests__/state-marker.test.ts +288 -0
  54. package/src/execution/__tests__/stream-sink-retirement.test.ts +8 -5
  55. package/src/execution/__tests__/subagent-actions-core.test.ts +67 -16
  56. package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +0 -1
  57. package/src/execution/__tests__/subagent-service-notify-gate.test.ts +27 -11
  58. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +17 -11
  59. package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +130 -53
  60. package/src/execution/__tests__/subagent-service.test.ts +17 -215
  61. package/src/execution/__tests__/subprocess-agent-runner-no-progress-killall.test.ts +138 -0
  62. package/src/execution/__tests__/subprocess-agent-runner.test.ts +105 -660
  63. package/src/execution/__tests__/sync-collect-recovery.test.ts +23 -16
  64. package/src/execution/__tests__/terminal-write-latency.test.ts +79 -0
  65. package/src/execution/__tests__/workflow-agent-dispatch.test.ts +782 -0
  66. package/src/execution/alive-store.ts +37 -28
  67. package/src/execution/{cold-resurrect.ts → cold-lookup.ts} +48 -45
  68. package/src/execution/config.ts +1 -1
  69. package/src/execution/conversation-continuation.ts +541 -0
  70. package/src/execution/dialog-queue.ts +60 -12
  71. package/src/execution/engine/__tests__/common/capability-gate.test.ts +1 -1
  72. package/src/execution/engine/__tests__/common/journal-wiring.test.ts +16 -1
  73. package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/fake-engine-protocol.mjs +17 -51
  74. package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/smoke-run.fixture.json +0 -5
  75. package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +1 -1
  76. package/src/execution/engine/__tests__/conformance/protocol-blackbox.test.ts +5 -9
  77. package/src/execution/engine/__tests__/engine-discovery-scan.test.ts +0 -3
  78. package/src/execution/engine/__tests__/engine-discovery.test.ts +0 -3
  79. package/src/execution/engine/__tests__/model-prompt.test.ts +0 -3
  80. package/src/execution/engine/__tests__/registry.test.ts +0 -1
  81. package/src/execution/engine/__tests__/routing.test.ts +0 -1
  82. package/src/execution/engine/__tests__/run-failure-worktree-cleanup.test.ts +0 -3
  83. package/src/execution/engine/client/__tests__/__fixtures__/fake-engine.mjs +0 -3
  84. package/src/execution/engine/client/__tests__/engine-client-reap.test.ts +212 -0
  85. package/src/execution/engine/client/__tests__/mirror.test.ts +73 -1
  86. package/src/execution/engine/client/__tests__/protocol-closure.test.ts +2 -9
  87. package/src/execution/engine/client/__tests__/remote-engine.test.ts +46 -12
  88. package/src/execution/engine/client/engine-client.ts +81 -25
  89. package/src/execution/engine/client/mirror.ts +9 -0
  90. package/src/execution/engine/client/remote-engine.ts +52 -34
  91. package/src/execution/engine/client/reverse-router.ts +6 -27
  92. package/src/execution/engine/common/__tests__/run-signals.test.ts +79 -0
  93. package/src/execution/engine/common/capability-gate.ts +27 -0
  94. package/src/execution/engine/common/data-dir.ts +3 -2
  95. package/src/execution/engine/common/errors.ts +1 -1
  96. package/src/execution/engine/common/journal-wiring.ts +18 -9
  97. package/src/execution/engine/common/run-signals.ts +83 -0
  98. package/src/execution/engine/d8-compat.ts +1 -1
  99. package/src/execution/engine/host/__tests__/host-bridge.test.ts +3 -18
  100. package/src/execution/engine/host/host-bridge.ts +19 -41
  101. package/src/execution/engine/host/pi-host-binding.ts +5 -5
  102. package/src/execution/engine/port.ts +34 -54
  103. package/src/execution/engine/types.ts +11 -14
  104. package/src/execution/execution-record.ts +15 -47
  105. package/src/execution/finalize-record.ts +94 -197
  106. package/src/execution/idle-gc.ts +9 -3
  107. package/src/execution/lifecycle-manager.ts +25 -361
  108. package/src/execution/lifecycle-predicates.ts +22 -19
  109. package/src/execution/manifest-store.ts +40 -42
  110. package/src/execution/model-config-service.ts +1 -1
  111. package/src/execution/notifier.ts +39 -1
  112. package/src/execution/notify-host.ts +13 -4
  113. package/src/execution/record-entry.ts +20 -2
  114. package/src/execution/record-store.ts +925 -160
  115. package/src/execution/relay-env.ts +8 -3
  116. package/src/execution/round-supervisor/domain.ts +17 -5
  117. package/src/execution/round-supervisor/service-binding.test.ts +89 -53
  118. package/src/execution/round-supervisor/service-binding.ts +71 -21
  119. package/src/execution/round-supervisor/supervisor.ts +23 -9
  120. package/src/execution/service/record-access.ts +547 -0
  121. package/src/execution/service/record-lifecycle.ts +542 -0
  122. package/src/execution/service/run-orchestration.ts +1647 -0
  123. package/src/execution/service/service-bootstrap.ts +111 -0
  124. package/src/execution/service/service-constants.ts +27 -0
  125. package/src/execution/service/session-baselines.ts +401 -0
  126. package/src/execution/service/sync-collect-domain.ts +442 -0
  127. package/src/execution/service/workflow-dispatch.ts +567 -0
  128. package/src/execution/session-file-gc.ts +11 -8
  129. package/src/execution/session-reconstructor.ts +44 -1
  130. package/src/execution/sessions-index.ts +20 -3
  131. package/src/execution/settled-watchdog.ts +86 -43
  132. package/src/execution/state-marker.ts +516 -0
  133. package/src/execution/stream-sink.ts +1 -1
  134. package/src/execution/subagent-actions-core.ts +53 -31
  135. package/src/execution/subagent-service.ts +643 -2980
  136. package/src/execution/subprocess-agent-runner.ts +46 -323
  137. package/src/execution/types.ts +60 -43
  138. package/src/execution/ui-request-handler-factory.ts +5 -0
  139. package/src/execution/worktree-manager.ts +1 -1
  140. package/src/execution/worktree-registry.ts +1 -1
  141. package/src/index.ts +6 -5
  142. package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +2 -3
  143. package/src/orchestration/__tests__/agent-call-stream.test.ts +24 -49
  144. package/src/orchestration/__tests__/error-recovery-terminal-hardening.test.ts +4 -4
  145. package/src/orchestration/__tests__/file-run-store.test.ts +5 -7
  146. package/src/orchestration/__tests__/lifecycle-abort-broadcast-signal.test.ts +2 -3
  147. package/src/orchestration/__tests__/run-snapshot.test.ts +27 -43
  148. package/src/orchestration/__tests__/worker-message-pump-workflow-dispatch.test.ts +274 -0
  149. package/src/orchestration/execute-agent-call.ts +1 -1
  150. package/src/orchestration/file-run-store.ts +1 -1
  151. package/src/orchestration/models/ports.ts +20 -13
  152. package/src/orchestration/models/types.ts +11 -13
  153. package/src/orchestration/run-snapshot.ts +18 -22
  154. package/src/orchestration/worker-message-pump.ts +33 -57
  155. package/src/shared/atomic-write.ts +10 -8
  156. package/dist/chunk-T2SYBW3I.js +0 -24
  157. package/dist/engine-discovery-scan-BXLA8y-z.d.cts +0 -1624
  158. package/dist/engine-discovery-scan-BXLA8y-z.d.ts +0 -1624
  159. package/src/execution/__tests__/chat-round-first-round-watchdog.test.ts +0 -255
  160. package/src/execution/__tests__/finalized-marker.test.ts +0 -104
  161. package/src/execution/__tests__/lifecycle-manager-lock.test.ts +0 -211
  162. package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +0 -488
  163. package/src/execution/__tests__/subprocess-agent-runner-timeout.test.ts +0 -75
  164. package/src/execution/__tests__/tombstone-store.test.ts +0 -73
  165. package/src/execution/engine/__tests__/conformance/chat-round-protocol.test.ts +0 -230
  166. package/src/execution/finalized-marker.ts +0 -70
  167. package/src/execution/tombstone-store.ts +0 -72
@@ -0,0 +1,600 @@
1
+ // src/__tests__/record-store-intent-api.test.ts
2
+ //
3
+ // [U1 / record 持久化收敛 §3.1] RecordStore 意图级操作 API 立面专属测试。
4
+ //
5
+ // 覆盖(验收条款 A1/A2/A5/A6):
6
+ // - A1 十意图原语齐备(register/appendEvent/markRoundStarted/markRoundIdle/
7
+ // markFinalized/markCancelled/markBatchFinalized/adoptEngineDeath/
8
+ // markResurrected/markIdleArchived)+ acquireWriteLease(A6);
9
+ // - A2 终态写序(D8 v7):`.state` writeSync 先 → entry/archive → manifest
10
+ // writeSync → `.alive` 删除;markBatchFinalized barrier(manifest 落盘完成
11
+ // 先于批通知写账);markRoundIdle 簿记⑦ `.alive` 保留;markIdleArchived
12
+ // archive 先 release 后(archive 抛错 marker 必未删);
13
+ // - A5 markResurrected D3c 三中间形态((ii) acquire 后中断 / (iii) 全成 /
14
+ // acquire 失败)+ running 候选接管形态(跳删终态位仍 acquire);
15
+ // - §3.4 失败语义:`.state` 写失败 → 零持久化副作用、record 留 running 形态。
16
+ //
17
+ // 手法:state-marker / alive-store partial mock(包装真实实现并记录调用序)+
18
+ // manifestDir 真实落盘断言「写后即刻可见」(无 fire-and-forget)。fixture 一律
19
+ // mkdtempSync 自建自删(tmpdir),不触碰真实数据目录。
20
+
21
+ import * as fs from "node:fs";
22
+ import * as os from "node:os";
23
+ import * as path from "node:path";
24
+
25
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
26
+
27
+ // 跨模块调用序记录(A2 写序断言锚)+ release 时点探针。
28
+ const { order, probe } = vi.hoisted(() => ({
29
+ order: [] as string[],
30
+ probe: {
31
+ manifestPath: undefined as string | undefined,
32
+ manifestExistedAtRelease: undefined as boolean | undefined,
33
+ },
34
+ }));
35
+
36
+ const { loggerMock } = vi.hoisted(() => ({
37
+ loggerMock: { debug: vi.fn(), warn: vi.fn(), error: vi.fn() },
38
+ }));
39
+ vi.mock("../../core/logger.ts", () => ({
40
+ getLogger: () => loggerMock,
41
+ }));
42
+
43
+ // state-marker partial mock:写函数包装真实实现并记录调用序。
44
+ vi.mock("../state-marker.ts", async (importOriginal) => {
45
+ const actual = await importOriginal<typeof import("../state-marker.ts")>();
46
+ return {
47
+ ...actual,
48
+ writeFinalizedState: vi.fn((sessionFile: string, reason?: string) => {
49
+ order.push("state-finalized");
50
+ return actual.writeFinalizedState(sessionFile, reason);
51
+ }),
52
+ writeCancelledState: vi.fn((sessionFile: string, endedAt: number) => {
53
+ order.push("state-cancelled");
54
+ return actual.writeCancelledState(sessionFile, endedAt);
55
+ }),
56
+ };
57
+ });
58
+
59
+ // alive-store partial mock:acquire/release 包装真实实现并记录调用序;release
60
+ // 时点探测 manifest 是否已落盘(A2「.state 先 → manifest 后 → .alive 删」断言)。
61
+ vi.mock("../alive-store.ts", async (importOriginal) => {
62
+ const actual = await importOriginal<typeof import("../alive-store.ts")>();
63
+ const nodeFs = await import("node:fs");
64
+ return {
65
+ ...actual,
66
+ writeAliveMarker: vi.fn((sessionFile: string, marker: Parameters<typeof actual.writeAliveMarker>[1]) => {
67
+ order.push("alive-acquire");
68
+ return actual.writeAliveMarker(sessionFile, marker);
69
+ }),
70
+ removeAliveMarker: vi.fn((sessionFile: string) => {
71
+ order.push("alive-release");
72
+ if (probe.manifestPath !== undefined) {
73
+ probe.manifestExistedAtRelease = nodeFs.existsSync(probe.manifestPath);
74
+ }
75
+ return actual.removeAliveMarker(sessionFile);
76
+ }),
77
+ };
78
+ });
79
+
80
+ // node:fs partial mock:rmSync 可注错(D3c (ii) 删终态位失败注入),其余真实。
81
+ type RmSyncFn = typeof import("node:fs").rmSync;
82
+ const { rmSyncMock, actualRmRef } = vi.hoisted(() => ({
83
+ rmSyncMock: vi.fn(),
84
+ actualRmRef: { current: undefined as RmSyncFn | undefined },
85
+ }));
86
+ vi.mock("node:fs", async (importOriginal) => {
87
+ const actual = await importOriginal<typeof import("node:fs")>();
88
+ actualRmRef.current = actual.rmSync;
89
+ return {
90
+ ...actual,
91
+ rmSync: rmSyncMock,
92
+ default: { ...actual, rmSync: rmSyncMock },
93
+ };
94
+ });
95
+
96
+ import { writeAliveMarker, readAliveMarker } from "../alive-store.ts";
97
+ import * as stateMarker from "../state-marker.ts";
98
+ import { createRecord, tryTransition } from "../execution-record.ts";
99
+ import { RecordStore } from "../record-store.ts";
100
+ import type { ExecutionRecord, SubagentRecord } from "../types.ts";
101
+
102
+ /** 构造 ExecutionRecord(running 基线,over 覆盖)。 */
103
+ function makeRecord(id: string, over: Partial<ExecutionRecord> = {}): ExecutionRecord {
104
+ const base = createRecord(id, {
105
+ agent: "worker",
106
+ model: "m",
107
+ mode: "background",
108
+ task: "t",
109
+ slug: "intent-api",
110
+ startedAt: 1000,
111
+ rootSessionId: "sess-current",
112
+ chatMode: false,
113
+ });
114
+ return { ...base, ...over };
115
+ }
116
+
117
+ /** 批成员 SubagentRecord 最小合法形状。 */
118
+ function makeSubagentRecord(id: string, over: Partial<SubagentRecord> = {}): SubagentRecord {
119
+ return {
120
+ id,
121
+ agent: "worker",
122
+ task: "batch task",
123
+ slug: "batch",
124
+ status: "running",
125
+ mode: "background",
126
+ startedAt: 1_700_000_000_000,
127
+ rootSessionId: "sess-current",
128
+ parentRecordId: undefined,
129
+ depth: 0,
130
+ endedAt: undefined,
131
+ turns: 0,
132
+ totalTokens: 0,
133
+ model: "test/model-a",
134
+ thinkingLevel: undefined,
135
+ eventLog: [],
136
+ displayItems: [],
137
+ ...over,
138
+ };
139
+ }
140
+
141
+ describe("RecordStore 意图 API 立面(U1 A1/A2/A5/A6)", () => {
142
+ let tmpDir: string;
143
+ let sessionsDir: string;
144
+ let manifestDir: string;
145
+ let sessionFile: string;
146
+ /** Pi appendEntry mock(显式签名:可作 RecordStorePi 直传 + 断言面可用)。 */
147
+ let appendEntryMock: ReturnType<typeof vi.fn<(customType: string, data: unknown) => void>>;
148
+ let store: RecordStore;
149
+
150
+ beforeEach(() => {
151
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "record-store-intent-api-"));
152
+ sessionsDir = path.join(tmpDir, "sessions");
153
+ manifestDir = path.join(tmpDir, "records");
154
+ fs.mkdirSync(sessionsDir, { recursive: true });
155
+ fs.mkdirSync(manifestDir, { recursive: true });
156
+ sessionFile = path.join(sessionsDir, "2026-01-01_uuid.jsonl");
157
+ appendEntryMock = vi.fn<(customType: string, data: unknown) => void>();
158
+ store = new RecordStore(sessionsDir, undefined, { appendEntry: appendEntryMock }, manifestDir);
159
+ order.length = 0;
160
+ probe.manifestPath = undefined;
161
+ probe.manifestExistedAtRelease = undefined;
162
+ loggerMock.error.mockClear();
163
+ loggerMock.warn.mockClear();
164
+ // rmSync 基线 = 真实实现(个别用例注错覆盖)。
165
+ if (actualRmRef.current !== undefined) {
166
+ rmSyncMock.mockReset().mockImplementation(actualRmRef.current);
167
+ }
168
+ });
169
+
170
+ afterEach(() => {
171
+ fs.rmSync(tmpDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
172
+ vi.restoreAllMocks();
173
+ });
174
+
175
+ const manifestPathOf = (id: string): string => path.join(manifestDir, `${id}.json`);
176
+
177
+ const readManifestJson = (id: string): Record<string, unknown> =>
178
+ JSON.parse(fs.readFileSync(manifestPathOf(id), "utf-8")) as Record<string, unknown>;
179
+
180
+ // ============================================================
181
+ // A1 十原语齐备
182
+ // ============================================================
183
+ describe("A1 意图原语立面", () => {
184
+ it("十意图原语 + store 内部 acquire 动作齐备", () => {
185
+ const fns = [
186
+ "register",
187
+ "appendEvent",
188
+ "markRoundStarted",
189
+ "markRoundIdle",
190
+ "markFinalized",
191
+ "markCancelled",
192
+ "markBatchFinalized",
193
+ "adoptEngineDeath",
194
+ "markResurrected",
195
+ "markIdleArchived",
196
+ "acquireWriteLease", // A6
197
+ ] as const;
198
+ for (const name of fns) {
199
+ expect(typeof store[name], `primitive ${name}`).toBe("function");
200
+ }
201
+ });
202
+ });
203
+
204
+ // ============================================================
205
+ // A2 markFinalized / markCancelled 写序(D8 v7)
206
+ // ============================================================
207
+ describe("markFinalized(A2 写序 + §3.4 失败语义)", () => {
208
+ it("写序 = .state 先 → entry/archive → manifest writeSync → .alive 删;写后即刻可见", () => {
209
+ const record = makeRecord("bg-1");
210
+ record.sessionFile = sessionFile;
211
+ store.acquireWriteLease(sessionFile, "bg-1"); // 持有期写权声明(release 前置形态)
212
+ store.register(record);
213
+ tryTransition(record, "closed", "user-close");
214
+ record.endedAt = 5000;
215
+ probe.manifestPath = manifestPathOf("bg-1");
216
+
217
+ expect(store.markFinalized(record, "user-close")).toBe(true);
218
+
219
+ // 写序:.state → .alive release(顺序数组中两者先后可证)。
220
+ expect(order).toEqual(["alive-acquire", "state-finalized", "alive-release"]);
221
+ // release 时点 manifest 已落盘(.state 先 → manifest 后 → .alive 删)。
222
+ expect(probe.manifestExistedAtRelease).toBe(true);
223
+ // 写后即刻可见(同步写,无 fire-and-forget)。
224
+ expect(JSON.parse(fs.readFileSync(`${sessionFile}.state`, "utf-8"))).toEqual({
225
+ status: "finalized",
226
+ reason: "user-close",
227
+ });
228
+ expect(fs.existsSync(`${sessionFile}.alive`)).toBe(false);
229
+ // manifest 投影(同步落盘)。
230
+ expect(readManifestJson("bg-1")).toMatchObject({
231
+ id: "bg-1",
232
+ status: "closed",
233
+ closedReason: "user-close",
234
+ agentName: "worker",
235
+ completedAt: 5000,
236
+ });
237
+ // archive:内存移除 + 终态 entry 落盘。
238
+ expect(store.getMutable("bg-1")).toBeUndefined();
239
+ expect(appendEntryMock).toHaveBeenCalledWith(
240
+ "subagent-record",
241
+ expect.objectContaining({ id: "bg-1", status: "closed" }),
242
+ );
243
+ });
244
+
245
+ it(".state 写失败(重试耗尽)→ 返回 false、零持久化副作用、record 留 running 形态", () => {
246
+ const record = makeRecord("bg-2");
247
+ record.sessionFile = sessionFile;
248
+ store.acquireWriteLease(sessionFile, "bg-2");
249
+ store.register(record);
250
+ tryTransition(record, "closed", "user-close");
251
+ appendEntryMock.mockClear();
252
+
253
+ vi.mocked(stateMarker.writeFinalizedState).mockReturnValueOnce(false);
254
+
255
+ expect(store.markFinalized(record, "user-close")).toBe(false);
256
+ // 零持久化副作用:不归档(内存仍在)、无 manifest、写权声明未 release、无终态 entry。
257
+ expect(store.getMutable("bg-2")).toBeDefined();
258
+ expect(fs.existsSync(manifestPathOf("bg-2"))).toBe(false);
259
+ expect(fs.existsSync(`${sessionFile}.alive`)).toBe(true);
260
+ // `.state` 写尝试确实发生(mockReturnValueOnce 短路包装层,顺序 token 不入 order,
261
+ // 以调用事实为准);且其后无任何后续写动作(alive-release 未入 order)。
262
+ expect(stateMarker.writeFinalizedState).toHaveBeenCalledWith(sessionFile, "user-close");
263
+ expect(order).toEqual(["alive-acquire"]);
264
+ expect(appendEntryMock).not.toHaveBeenCalled();
265
+ });
266
+
267
+ it("sessionFile 缺失 → .state 面跳过(warn 留痕),manifest/entry 照常", () => {
268
+ const record = makeRecord("bg-3");
269
+ store.register(record);
270
+ tryTransition(record, "closed", "gc");
271
+
272
+ expect(store.markFinalized(record, "gc")).toBe(true);
273
+ expect(loggerMock.warn).toHaveBeenCalled();
274
+ expect(fs.existsSync(manifestPathOf("bg-3"))).toBe(true);
275
+ expect(store.getMutable("bg-3")).toBeUndefined();
276
+ });
277
+ });
278
+
279
+ describe("markCancelled(A2 写序 + tombstone)", () => {
280
+ it("tombstone endedAt 落 .state;写序/manifest 同 markFinalized", () => {
281
+ const record = makeRecord("bg-c1");
282
+ record.sessionFile = sessionFile;
283
+ store.register(record);
284
+ store.acquireWriteLease(sessionFile, "bg-c1");
285
+ tryTransition(record, "closed", "cancelled");
286
+ record.endedAt = 7777;
287
+ probe.manifestPath = manifestPathOf("bg-c1");
288
+
289
+ expect(store.markCancelled(record)).toBe(true);
290
+
291
+ expect(order).toEqual(["alive-acquire", "state-cancelled", "alive-release"]);
292
+ expect(probe.manifestExistedAtRelease).toBe(true);
293
+ expect(JSON.parse(fs.readFileSync(`${sessionFile}.state`, "utf-8"))).toEqual({
294
+ status: "cancelled",
295
+ endedAt: 7777,
296
+ });
297
+ expect(readManifestJson("bg-c1")).toMatchObject({ id: "bg-c1", status: "closed", closedReason: "cancelled" });
298
+ expect(fs.existsSync(`${sessionFile}.alive`)).toBe(false);
299
+ });
300
+
301
+ it(".state 写失败 → 返回 false、tombstone 未落、record 不归档", () => {
302
+ const record = makeRecord("bg-c2");
303
+ record.sessionFile = sessionFile;
304
+ store.register(record);
305
+ tryTransition(record, "closed", "cancelled");
306
+
307
+ vi.mocked(stateMarker.writeCancelledState).mockReturnValueOnce(false);
308
+
309
+ expect(store.markCancelled(record)).toBe(false);
310
+ expect(fs.existsSync(`${sessionFile}.state`)).toBe(false);
311
+ expect(store.getMutable("bg-c2")).toBeDefined();
312
+ });
313
+ });
314
+
315
+ // ============================================================
316
+ // A2 markBatchFinalized barrier
317
+ // ============================================================
318
+ describe("markBatchFinalized(barrier:manifest 落盘先于批通知写账)", () => {
319
+ it("entry 写账时点全部成员 manifest 均已落盘;落标 entry 携带 collectMode/batchFinalized", async () => {
320
+ const members = [makeSubagentRecord("sa-m1"), makeSubagentRecord("sa-m2")];
321
+ // 写账时点断言 barrier:appendEntry 被调时该成员 manifest 必已存在。
322
+ appendEntryMock.mockImplementation((_type: string, data: unknown) => {
323
+ const d = data as { id?: string };
324
+ if (typeof d?.id === "string") {
325
+ expect(fs.existsSync(manifestPathOf(d.id)), `manifest of ${d.id} at entry time`).toBe(true);
326
+ }
327
+ });
328
+
329
+ await store.markBatchFinalized(members);
330
+
331
+ expect(appendEntryMock).toHaveBeenCalledTimes(2);
332
+ expect(appendEntryMock).toHaveBeenCalledWith(
333
+ "subagent-record",
334
+ expect.objectContaining({ id: "sa-m1", collectMode: "sync", batchFinalized: true }),
335
+ );
336
+ // manifest status 如实投影(成功成员此刻 running+resumable)。
337
+ expect(readManifestJson("sa-m1")).toMatchObject({ id: "sa-m1", status: "running" });
338
+ expect(readManifestJson("sa-m2")).toMatchObject({ id: "sa-m2", status: "running" });
339
+ });
340
+ });
341
+
342
+ // ============================================================
343
+ // A2 markRoundStarted / markRoundIdle 簿记
344
+ // ============================================================
345
+ describe("markRoundStarted(轮始重置:字段①②⑤)", () => {
346
+ it("status=running + result/resumable 清除 + entry 上报", () => {
347
+ const record = makeRecord("chat-1", { chatMode: true });
348
+ record.result = "prev round";
349
+ record.resumable = true;
350
+ store.register(record);
351
+ appendEntryMock.mockClear();
352
+
353
+ expect(store.markRoundStarted("chat-1")).toBe(true);
354
+ expect(record.status).toBe("running");
355
+ expect(record.result).toBeUndefined();
356
+ expect(record.resumable).toBeUndefined();
357
+ expect(appendEntryMock).toHaveBeenCalledWith(
358
+ "subagent-record",
359
+ expect.objectContaining({ id: "chat-1" }),
360
+ );
361
+ });
362
+
363
+ it("id 不在内存 → false 无副作用", () => {
364
+ expect(store.markRoundStarted("nope")).toBe(false);
365
+ });
366
+ });
367
+
368
+ describe("markRoundIdle(簿记全集①-⑨;簿记⑦ .alive 保留)", () => {
369
+ it("成功轮:result=content、round+1、closedReason 清、resumable=true、idleSince 刷新、注销②、entry 携带新 round", () => {
370
+ const record = makeRecord("chat-2", { chatMode: true, round: 1 });
371
+ record.closedReason = "gc"; // [S10]:前置残留不清则泄漏进 list 投影
372
+ record.sessionFile = sessionFile;
373
+ store.register(record);
374
+ store.acquireWriteLease(sessionFile, "chat-2");
375
+ const unregister = vi.fn();
376
+ store.setPendingUnregister(unregister);
377
+ appendEntryMock.mockClear();
378
+ order.length = 0;
379
+
380
+ expect(store.markRoundIdle("chat-2", { kind: "success", content: "round done" })).toBe(true);
381
+
382
+ expect(record.status).toBe("running"); // ① 保持 running(非 idle)
383
+ expect(record.result).toBe("round done"); // ②
384
+ expect(record.round).toBe(2); // ③
385
+ expect(record.closedReason).toBeUndefined(); // ④
386
+ expect(record.resumable).toBe(true); // ⑤
387
+ expect(record.idleSince).toBeGreaterThan(0); // ⑥
388
+ expect(order).toEqual([]); // ⑦ `.alive` 保留——无 release 动作
389
+ expect(fs.existsSync(`${sessionFile}.alive`)).toBe(true); // ⑦ 落盘面仍持声明
390
+ expect(unregister).toHaveBeenCalledWith("chat-2", "running"); // ⑧ 发射点②
391
+ expect(appendEntryMock).toHaveBeenCalledWith(
392
+ "subagent-record",
393
+ expect.objectContaining({ id: "chat-2", round: 2, result: "round done" }), // ⑨
394
+ );
395
+ });
396
+
397
+ it("失败轮:lastError 写原因、result=前值??失败摘要", () => {
398
+ const record = makeRecord("chat-3", { chatMode: true, round: 0 });
399
+ store.register(record);
400
+
401
+ expect(store.markRoundIdle("chat-3", { kind: "failed", reason: "engine boom" })).toBe(true);
402
+ expect(record.lastError).toBe("engine boom"); // ⑨ lastError 归口
403
+ expect(record.result).toBe("round did not complete: engine boom"); // ② 首轮失败无前值
404
+ });
405
+
406
+ it("终态簿记已冻结(endedAt 已设)→ fail-fast 抛错", () => {
407
+ const record = makeRecord("chat-4");
408
+ record.endedAt = 123; // completeRecord 已跑的冻结判据
409
+ store.register(record);
410
+
411
+ expect(() => store.markRoundIdle("chat-4", { kind: "success", content: "x" })).toThrow(
412
+ /terminal bookkeeping already frozen/,
413
+ );
414
+ });
415
+
416
+ it("id 不在内存 → false 无副作用", () => {
417
+ expect(store.markRoundIdle("nope", { kind: "success", content: "x" })).toBe(false);
418
+ });
419
+ });
420
+
421
+ // ============================================================
422
+ // appendEvent / adoptEngineDeath(过程原语)
423
+ // ============================================================
424
+ describe("appendEvent / adoptEngineDeath", () => {
425
+ it("appendEvent:事件归约进 turns + entry 变迁上报;id 不在内存 → false", () => {
426
+ const record = makeRecord("ev-1");
427
+ store.register(record);
428
+ appendEntryMock.mockClear();
429
+
430
+ expect(store.appendEvent("ev-1", { type: "text_delta", delta: "hi" })).toBe(true);
431
+ expect(record.turns[0]?.text).toBe("hi"); // ⑧ turns 归约
432
+ expect(appendEntryMock).toHaveBeenCalledTimes(1);
433
+ expect(store.appendEvent("nope", { type: "text_delta", delta: "x" })).toBe(false);
434
+ });
435
+
436
+ it("adoptEngineDeath:error/result/resumable 三写 + entry;id 不在内存 → false", () => {
437
+ const record = makeRecord("adopt-1");
438
+ record.result = "partial";
439
+ store.register(record);
440
+ appendEntryMock.mockClear();
441
+
442
+ expect(store.adoptEngineDeath("adopt-1", { error: "engine crashed" })).toBe(true);
443
+ expect(record.error).toBe("engine crashed"); // ⑩
444
+ expect(record.result).toBeUndefined();
445
+ expect(record.resumable).toBe(true); // ⑤ 收养
446
+ expect(appendEntryMock).toHaveBeenCalledWith(
447
+ "subagent-record",
448
+ expect.objectContaining({ id: "adopt-1", resumable: true }),
449
+ );
450
+ expect(store.adoptEngineDeath("nope", { error: "x" })).toBe(false);
451
+ });
452
+ });
453
+
454
+ // ============================================================
455
+ // A5 markResurrected(D3c:acquire-first + 单 try 域 + 三中间形态)
456
+ // ============================================================
457
+ describe("markResurrected(A5 D3c)", () => {
458
+ const makeClosedCandidate = (id: string): ExecutionRecord => {
459
+ const record = makeRecord(id);
460
+ record.sessionFile = sessionFile;
461
+ tryTransition(record, "closed", "parent-shutdown");
462
+ record.endedAt = 4000;
463
+ return record;
464
+ };
465
+
466
+ it("(iii) 全成:acquire marker(pid=本进程) → .state/.finalized 删 → 内存翻回 + register", () => {
467
+ // 磁盘预置终态位(现行载体 + legacy)。
468
+ fs.writeFileSync(`${sessionFile}.state`, JSON.stringify({ status: "finalized", reason: "parent-shutdown" }));
469
+ fs.writeFileSync(`${sessionFile}.finalized`, "parent-shutdown");
470
+ const record = makeClosedCandidate("rs-1");
471
+ order.length = 0;
472
+
473
+ store.markResurrected(record, true);
474
+
475
+ expect(order).toEqual(["alive-acquire"]); // acquire-first:声明先于终态位删除
476
+ expect(readAliveMarker(sessionFile)).toMatchObject({ pid: process.pid, id: "rs-1" });
477
+ expect(fs.existsSync(`${sessionFile}.state`)).toBe(false);
478
+ expect(fs.existsSync(`${sessionFile}.finalized`)).toBe(false);
479
+ expect(record.status).toBe("running"); // resurrectClosed 内存翻回
480
+ expect(record.closedReason).toBeUndefined();
481
+ expect(store.getMutable("rs-1")).toBe(record); // register
482
+ });
483
+
484
+ it("(iii) pre-L4 legacy:仅 .cancelled 终态(无 .state/.finalized)→ resurrect 后 readStateMarker undefined(live ≡ reload)", () => {
485
+ // 存量形态:L4 合并前 writeCancelledTombstone 的旧名 tombstone(单行 JSON +
486
+ // 换行),readStateMarker 在 .state 缺失时回退认领——resurrect 必须一并删除,
487
+ // 否则磁盘终态位未真正翻转(重建 cancelled 与内存 running 不一致)。
488
+ fs.writeFileSync(
489
+ `${sessionFile}.cancelled`,
490
+ `${JSON.stringify({ id: "rs-legacy", status: "cancelled", agent: "worker", startedAt: 1000, endedAt: 4000 })}\n`,
491
+ );
492
+ expect(stateMarker.readStateMarker(sessionFile)).toMatchObject({ status: "cancelled", endedAt: 4000 }); // 前置:旧名回退可读
493
+ const record = makeClosedCandidate("rs-legacy");
494
+
495
+ store.markResurrected(record, true);
496
+
497
+ expect(stateMarker.readStateMarker(sessionFile)).toBeUndefined(); // 磁盘终态位真正翻转
498
+ expect(record.status).toBe("running"); // 内存翻回
499
+ expect(store.getMutable("rs-legacy")).toBe(record); // register
500
+ });
501
+
502
+ it("(ii) acquire 后删终态位失败 → 响亮抛错:marker 已写、.state 仍在、内存无半态", () => {
503
+ fs.writeFileSync(`${sessionFile}.state`, JSON.stringify({ status: "finalized", reason: "parent-shutdown" }));
504
+ const record = makeClosedCandidate("rs-2");
505
+ rmSyncMock.mockImplementationOnce(() => {
506
+ throw new Error("simulated EACCES");
507
+ });
508
+
509
+ expect(() => store.markResurrected(record, true)).toThrow(/write-lease acquire\/terminal-position flip failed/);
510
+ expect(readAliveMarker(sessionFile)).toMatchObject({ pid: process.pid }); // acquire 已成
511
+ expect(fs.existsSync(`${sessionFile}.state`)).toBe(true); // 终态位未删(旧形态保持)
512
+ expect(store.getMutable("rs-2")).toBeUndefined(); // 内存无半态(未 register)
513
+ expect(loggerMock.error).toHaveBeenCalled();
514
+ });
515
+
516
+ it("acquire 失败(写 .alive 抛错)→ 响亮抛错:终态位未动、未注册(禁止吞错续跑)", () => {
517
+ fs.writeFileSync(`${sessionFile}.state`, JSON.stringify({ status: "finalized", reason: "parent-shutdown" }));
518
+ const record = makeClosedCandidate("rs-3");
519
+ const aliveMock = vi.mocked(writeAliveMarker);
520
+ aliveMock.mockImplementationOnce(() => {
521
+ throw new Error("simulated ENOSPC");
522
+ });
523
+
524
+ expect(() => store.markResurrected(record, true)).toThrow(/acquire\/terminal-position flip failed/);
525
+ expect(fs.existsSync(`${sessionFile}.state`)).toBe(true); // acquire-first:失败时终态位未删
526
+ expect(fs.existsSync(`${sessionFile}.alive`)).toBe(false);
527
+ expect(store.getMutable("rs-3")).toBeUndefined();
528
+ expect(loggerMock.error).toHaveBeenCalled();
529
+ });
530
+
531
+ it("running 候选接管(wasClosed=false)→ 跳过删终态位、仍 acquire 声明", () => {
532
+ fs.writeFileSync(`${sessionFile}.state`, JSON.stringify({ status: "finalized", reason: "parent-shutdown" }));
533
+ const record = makeRecord("rs-4");
534
+ record.sessionFile = sessionFile;
535
+ order.length = 0;
536
+
537
+ store.markResurrected(record, false);
538
+
539
+ expect(order).toEqual(["alive-acquire"]);
540
+ expect(fs.existsSync(`${sessionFile}.state`)).toBe(true); // 不删终态位(running 形态无 .state 可删)
541
+ expect(readAliveMarker(sessionFile)).toMatchObject({ pid: process.pid, id: "rs-4" }); // 接管即声明
542
+ expect(store.getMutable("rs-4")).toBe(record);
543
+ });
544
+
545
+ it("sessionFile 缺失 → 响亮抛错(无锚点无法声明写权)", () => {
546
+ const record = makeRecord("rs-5"); // 无 sessionFile
547
+ expect(() => store.markResurrected(record, true)).toThrow(/no sessionFile anchor/);
548
+ expect(store.getMutable("rs-5")).toBeUndefined();
549
+ });
550
+ });
551
+
552
+ // ============================================================
553
+ // A2 markIdleArchived(archive 先、release 后)
554
+ // ============================================================
555
+ describe("markIdleArchived(A2 写序)", () => {
556
+ it("归档成功 → 内存移除 + .alive release(磁盘仍 running 可接管,不写 .state)", () => {
557
+ const record = makeRecord("idle-1");
558
+ record.sessionFile = sessionFile;
559
+ store.register(record);
560
+ store.acquireWriteLease(sessionFile, "idle-1");
561
+ order.length = 0;
562
+
563
+ store.markIdleArchived(record);
564
+
565
+ expect(store.getMutable("idle-1")).toBeUndefined(); // archive 先
566
+ expect(order).toEqual(["alive-release"]); // release 后
567
+ expect(fs.existsSync(`${sessionFile}.alive`)).toBe(false);
568
+ expect(fs.existsSync(`${sessionFile}.state`)).toBe(false); // 非终态化
569
+ });
570
+
571
+ it("archive 抛错 → 原语整体失败、marker 必未删(持有与声明一致)", () => {
572
+ const record = makeRecord("idle-2");
573
+ record.sessionFile = sessionFile;
574
+ store.register(record);
575
+ store.acquireWriteLease(sessionFile, "idle-2");
576
+ const archiveSpy = vi.spyOn(store, "archive").mockImplementationOnce(() => {
577
+ throw new Error("archive boom");
578
+ });
579
+
580
+ expect(() => store.markIdleArchived(record)).toThrow(/archive boom/);
581
+ expect(fs.existsSync(`${sessionFile}.alive`)).toBe(true); // marker 未删
582
+ expect(archiveSpy).toHaveBeenCalledTimes(1);
583
+ });
584
+ });
585
+
586
+ // ============================================================
587
+ // A6 acquireWriteLease(store 内部 acquire 动作)
588
+ // ============================================================
589
+ describe("acquireWriteLease(A6)", () => {
590
+ it("写 .alive 声明(pid=本进程)——spawn 侧 sessionFile 回填挂钩锚", () => {
591
+ store.acquireWriteLease(sessionFile, "bg-lease");
592
+ expect(readAliveMarker(sessionFile)).toMatchObject({ pid: process.pid, id: "bg-lease" });
593
+ });
594
+
595
+ it("写失败原样上抛(响亮,禁止 best-effort 吞错)", () => {
596
+ const badPath = path.join(tmpDir, "nonexistent-sub", "s.jsonl");
597
+ expect(() => store.acquireWriteLease(badPath, "bg-lease2")).toThrow();
598
+ });
599
+ });
600
+ });