@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,329 @@
1
+ // src/execution/__tests__/rebuild-indexes.test.ts
2
+ //
3
+ // [U4c / G1+G2+G4-S5] 缓存降级三锚点:
4
+ // - S5① 手动删 manifest/sessions-index → 重建照常无错误静默吞(boot 全量通道
5
+ // rebuildIndexes + 查询面惰性通道 mergedRecords 双通道各自可独立触发);
6
+ // - 失败降级:无 identity 的损坏/异构文件不在扫描集 → 不重建不抛(sessions-index
7
+ // 「损坏静默回退全扫」同款先例);幸存 manifest 不覆写(幂等补缺);
8
+ // - G2 词汇双写:四 manifest 写面(markFinalized/markCancelled/markBatchFinalized/
9
+ // markIdleArchived)旧 status 三态投影 + executionStatus/closedReason 并存。
10
+
11
+ import * as fs from "node:fs";
12
+ import * as os from "node:os";
13
+ import * as path from "node:path";
14
+
15
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
16
+
17
+ import { createRecord } from "../execution-record.ts";
18
+ import { RecordStore } from "../record-store.ts";
19
+ import { INDEX_FILENAME } from "../sessions-index.ts";
20
+ import { writeCancelledState, writeFinalizedState } from "../state-marker.ts";
21
+ import type { ExecutionRecord, SubagentRecord } from "../types.ts";
22
+
23
+ /** 最小合法子 session 文件(session header + identity custom entry + assistant msg)。 */
24
+ function writeSessionJsonl(
25
+ filePath: string,
26
+ identity: { id: string; task: string; startedAt: number; rootSessionId?: string },
27
+ ): void {
28
+ const header = JSON.stringify({
29
+ type: "session", version: 3, id: `sess-${identity.id}`, timestamp: new Date(identity.startedAt).toISOString(), cwd: "/tmp",
30
+ });
31
+ const identityData: Record<string, unknown> = {
32
+ id: identity.id,
33
+ agent: "worker",
34
+ mode: "background",
35
+ task: identity.task,
36
+ slug: "rebuild",
37
+ startedAt: identity.startedAt,
38
+ };
39
+ if (identity.rootSessionId !== undefined) identityData.rootSessionId = identity.rootSessionId;
40
+ const identityEntry = JSON.stringify({
41
+ type: "custom",
42
+ id: "id-1",
43
+ parentId: null,
44
+ timestamp: new Date(identity.startedAt).toISOString(),
45
+ customType: "subagent-identity",
46
+ data: identityData,
47
+ });
48
+ const assistantMsg = JSON.stringify({
49
+ type: "message",
50
+ id: "msg-1",
51
+ parentId: "id-1",
52
+ timestamp: new Date(identity.startedAt + 1000).toISOString(),
53
+ message: {
54
+ role: "assistant",
55
+ content: [{ type: "text", text: "result text" }],
56
+ usage: { input: 10, output: 20, cacheRead: 0, cacheWrite: 0, totalTokens: 30, cost: { total: 0 } },
57
+ stopReason: "stop",
58
+ timestamp: identity.startedAt + 1000,
59
+ },
60
+ });
61
+ fs.writeFileSync(filePath, `${header}\n${identityEntry}\n${assistantMsg}\n`, "utf-8");
62
+ }
63
+
64
+ function makeRecord(id: string, overrides: Partial<ExecutionRecord> = {}): ExecutionRecord {
65
+ const r = createRecord(id, {
66
+ agent: "worker",
67
+ model: "test/model",
68
+ mode: "background",
69
+ task: "rebuild task",
70
+ slug: "rebuild",
71
+ startedAt: 1000,
72
+ rootSessionId: "root-session",
73
+ chatMode: false,
74
+ controller: new AbortController(),
75
+ });
76
+ Object.assign(r, overrides);
77
+ return r;
78
+ }
79
+
80
+ /** markBatchFinalized 入参的最小 SubagentRecord。 */
81
+ function makeSubagentRecord(id: string, sessionFile?: string): SubagentRecord {
82
+ return {
83
+ id,
84
+ agent: "worker",
85
+ task: "batch task",
86
+ slug: "batch",
87
+ status: "running",
88
+ mode: "background",
89
+ startedAt: 1000,
90
+ rootSessionId: "root-session",
91
+ parentRecordId: undefined,
92
+ depth: 0,
93
+ endedAt: undefined,
94
+ turns: 0,
95
+ totalTokens: 0,
96
+ model: "test/model",
97
+ thinkingLevel: undefined,
98
+ eventLog: [],
99
+ displayItems: [],
100
+ result: undefined,
101
+ error: undefined,
102
+ sessionFile,
103
+ };
104
+ }
105
+
106
+ describe("[U4c/G1] rebuildIndexes 双通道 + S5 缓存可丢锚点", () => {
107
+ let rootDir: string;
108
+ let sessionsDir: string;
109
+ let recordsDir: string;
110
+
111
+ beforeEach(() => {
112
+ rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "rebuild-indexes-"));
113
+ sessionsDir = path.join(rootDir, "sessions");
114
+ recordsDir = path.join(rootDir, "records");
115
+ fs.mkdirSync(sessionsDir);
116
+ fs.mkdirSync(recordsDir);
117
+ });
118
+
119
+ afterEach(() => {
120
+ fs.rmSync(rootDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
121
+ });
122
+
123
+ it("S5①: 手动删 manifest/sessions-index → boot 全量重建照常,无错误静默吞", async () => {
124
+ const fileA = path.join(sessionsDir, "20260912T000000_a.jsonl");
125
+ const fileB = path.join(sessionsDir, "20260912T000001_b.jsonl");
126
+ writeSessionJsonl(fileA, { id: "sa-closed", task: "closed task", startedAt: 1000, rootSessionId: "root-session" });
127
+ writeSessionJsonl(fileB, { id: "sa-running", task: "running task", startedAt: 2000, rootSessionId: "root-session" });
128
+ // 权威终态位(重建源 = `.state` 优先)
129
+ writeFinalizedState(fileA, "gc");
130
+ // 损坏/异构文件(无 identity 无绑定):不在扫描集,重建面必须静默跳过
131
+ fs.writeFileSync(path.join(sessionsDir, "junk.jsonl"), "not a session at all\n", "utf-8");
132
+ // 「缓存曾存在」的现场:manifest + sessions-index 各留一份后人为删除(S5 场景步骤)
133
+ fs.writeFileSync(path.join(recordsDir, "sa-closed.json"), "{}", "utf-8");
134
+ fs.writeFileSync(path.join(rootDir, INDEX_FILENAME), "{corrupted", "utf-8");
135
+ fs.rmSync(path.join(recordsDir, "sa-closed.json"));
136
+ fs.rmSync(path.join(rootDir, INDEX_FILENAME));
137
+
138
+ const store = new RecordStore(sessionsDir, undefined, undefined, recordsDir);
139
+ let rebuilt = -1;
140
+ expect(() => {
141
+ rebuilt = store.rebuildIndexes();
142
+ }).not.toThrow();
143
+ // 幂等补缺面 = 扫描集内全部 record(closed + running 两种形态都补)
144
+ expect(rebuilt).toBe(2);
145
+
146
+ // manifest 重建产物:词汇双写(旧三态 + executionStatus)+ identity 富字段
147
+ const closed = JSON.parse(fs.readFileSync(path.join(recordsDir, "sa-closed.json"), "utf-8")) as Record<string, unknown>;
148
+ expect(closed.status).toBe("closed");
149
+ expect(closed.executionStatus).toBe("closed");
150
+ expect(closed.closedReason).toBe("gc");
151
+ expect(closed.agentName).toBe("worker");
152
+ expect(closed.task).toBe("closed task");
153
+ expect(closed.sessionFile).toBe(fileA);
154
+ const running = JSON.parse(fs.readFileSync(path.join(recordsDir, "sa-running.json"), "utf-8")) as Record<string, unknown>;
155
+ expect(running.status).toBe("running");
156
+ expect(running.executionStatus).toBe("running");
157
+ expect(running.completedAt).toBeUndefined();
158
+
159
+ // junk 文件不产生 manifest(扫描集外静默跳过)
160
+ expect(fs.readdirSync(recordsDir).sort()).toEqual(["sa-closed.json", "sa-running.json"]);
161
+
162
+ // sessions-index 经首扫自愈重写(fire-and-forget,等待落盘)
163
+ await vi.waitFor(() => expect(fs.existsSync(path.join(rootDir, INDEX_FILENAME))).toBe(true));
164
+ const index = JSON.parse(fs.readFileSync(path.join(rootDir, INDEX_FILENAME), "utf-8")) as {
165
+ version: number;
166
+ entries: Record<string, unknown>;
167
+ };
168
+ expect(index.version).toBe(1);
169
+ expect(Object.keys(index.entries)).toContain("20260912T000000_a.jsonl");
170
+ expect(Object.keys(index.entries)).toContain("20260912T000001_b.jsonl");
171
+
172
+ // 列表/详情照常(S5 通过标准)
173
+ const ids = store.collectRecords(100).map((r) => r.id);
174
+ expect(ids).toContain("sa-closed");
175
+ expect(ids).toContain("sa-running");
176
+ store.dispose();
177
+ });
178
+
179
+ it("幸存 manifest 不覆写(重建是补缺不是刷新——防用尽力数据降级幸存快照)", () => {
180
+ const fileA = path.join(sessionsDir, "20260912T000002_survivor.jsonl");
181
+ writeSessionJsonl(fileA, { id: "sa-survivor", task: "disk task", startedAt: 1000, rootSessionId: "root-session" });
182
+ // 幸存 manifest 的 task 比 disk 重建源「新鲜」(终态写点真实值)
183
+ fs.writeFileSync(
184
+ path.join(recordsDir, "sa-survivor.json"),
185
+ JSON.stringify({ id: "sa-survivor", rootSessionId: "root-session", agentName: "worker", status: "closed", task: "survivor task", createdAt: 1000 }),
186
+ "utf-8",
187
+ );
188
+
189
+ const store = new RecordStore(sessionsDir, undefined, undefined, recordsDir);
190
+ expect(store.rebuildIndexes()).toBe(0);
191
+ const manifest = JSON.parse(fs.readFileSync(path.join(recordsDir, "sa-survivor.json"), "utf-8")) as Record<string, unknown>;
192
+ expect(manifest.task).toBe("survivor task");
193
+ store.dispose();
194
+ });
195
+
196
+ it("惰性通道: 查询面(collectRecords)补建缺员 manifest;在途 record 不建(创建时不写契约保持)", () => {
197
+ const fileA = path.join(sessionsDir, "20260912T000003_lazy.jsonl");
198
+ const fileLive = path.join(sessionsDir, "20260912T000004_live.jsonl");
199
+ writeSessionJsonl(fileA, { id: "sa-lazy", task: "lazy task", startedAt: 1000, rootSessionId: "root-session" });
200
+ writeSessionJsonl(fileLive, { id: "sa-live", task: "live task", startedAt: 2000, rootSessionId: "root-session" });
201
+
202
+ const store = new RecordStore(sessionsDir, undefined, undefined, recordsDir);
203
+ // 在途 record(内存持有)——终态写点会落 manifest,惰性通道不得提前建
204
+ store.register(makeRecord("sa-live", { sessionFile: fileLive }));
205
+
206
+ const ids = store.collectRecords(100).map((r) => r.id);
207
+ expect(ids).toContain("sa-lazy");
208
+ expect(ids).toContain("sa-live");
209
+
210
+ // 惰性补建只发生在外源 record
211
+ expect(fs.existsSync(path.join(recordsDir, "sa-lazy.json"))).toBe(true);
212
+ expect(fs.existsSync(path.join(recordsDir, "sa-live.json"))).toBe(false);
213
+ store.dispose();
214
+ });
215
+
216
+ it("每 id 每进程一次守卫 + revive 复位(防高频查询放大磁盘写)", () => {
217
+ const fileA = path.join(sessionsDir, "20260912T000005_guard.jsonl");
218
+ writeSessionJsonl(fileA, { id: "sa-guard", task: "guard task", startedAt: 1000, rootSessionId: "root-session" });
219
+
220
+ const store = new RecordStore(sessionsDir, undefined, undefined, recordsDir);
221
+ store.collectRecords(100);
222
+ expect(fs.existsSync(path.join(recordsDir, "sa-guard.json"))).toBe(true);
223
+
224
+ // 再次删除 + 查询:本进程已尝试过,不重复补写(boot 全量轮/下次进程承接)
225
+ fs.rmSync(path.join(recordsDir, "sa-guard.json"));
226
+ store.collectRecords(100);
227
+ expect(fs.existsSync(path.join(recordsDir, "sa-guard.json"))).toBe(false);
228
+
229
+ // /new /resume 复活(revive)复位守卫——「重开重判」语义
230
+ store.revive();
231
+ store.collectRecords(100);
232
+ expect(fs.existsSync(path.join(recordsDir, "sa-guard.json"))).toBe(true);
233
+ store.dispose();
234
+ });
235
+ });
236
+
237
+ describe("[U4c/G2] manifest 词汇双写——四写面旧三态投影 + executionStatus/closedReason 并存", () => {
238
+ let rootDir: string;
239
+ let sessionsDir: string;
240
+ let recordsDir: string;
241
+
242
+ beforeEach(() => {
243
+ rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "rebuild-dualwrite-"));
244
+ sessionsDir = path.join(rootDir, "sessions");
245
+ recordsDir = path.join(rootDir, "records");
246
+ fs.mkdirSync(sessionsDir);
247
+ fs.mkdirSync(recordsDir);
248
+ });
249
+
250
+ afterEach(() => {
251
+ fs.rmSync(rootDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
252
+ });
253
+
254
+ function readManifest(id: string): Record<string, unknown> {
255
+ return JSON.parse(fs.readFileSync(path.join(recordsDir, `${id}.json`), "utf-8")) as Record<string, unknown>;
256
+ }
257
+
258
+ it("markFinalized:status closed(旧三态)+ executionStatus closed + closedReason 随写", () => {
259
+ const store = new RecordStore(sessionsDir, undefined, undefined, recordsDir);
260
+ const sessionFile = path.join(sessionsDir, "20260912T000006_fin.jsonl");
261
+ fs.writeFileSync(sessionFile, "{}\n", "utf-8");
262
+ const record = makeRecord("sa-fin", { sessionFile, status: "closed", closedReason: "parent-shutdown", endedAt: 2000 });
263
+
264
+ expect(store.markFinalized(record, "parent-shutdown")).toBe(true);
265
+ const manifest = readManifest("sa-fin");
266
+ expect(manifest.status).toBe("closed");
267
+ expect(manifest.executionStatus).toBe("closed");
268
+ expect(manifest.closedReason).toBe("parent-shutdown");
269
+ store.dispose();
270
+ });
271
+
272
+ it("markCancelled:终态投影同 markFinalized(双写字段齐)", () => {
273
+ const store = new RecordStore(sessionsDir, undefined, undefined, recordsDir);
274
+ const sessionFile = path.join(sessionsDir, "20260912T000007_cx.jsonl");
275
+ fs.writeFileSync(sessionFile, "{}\n", "utf-8");
276
+ const record = makeRecord("sa-cx2", { sessionFile, endedAt: 2000 });
277
+
278
+ expect(store.markCancelled(record)).toBe(true);
279
+ const manifest = readManifest("sa-cx2");
280
+ expect(manifest.status).toBe("closed");
281
+ expect(manifest.executionStatus).toBe("closed");
282
+ store.dispose();
283
+ });
284
+
285
+ it("markIdleArchived:归档点补写 running 投影(非终态化语义——磁盘仍 running 可接管)", () => {
286
+ const store = new RecordStore(sessionsDir, undefined, undefined, recordsDir);
287
+ const sessionFile = path.join(sessionsDir, "20260912T000008_idle.jsonl");
288
+ fs.writeFileSync(sessionFile, "{}\n", "utf-8");
289
+ const record = makeRecord("sa-idle", { sessionFile, resumable: true });
290
+
291
+ store.markIdleArchived(record);
292
+ const manifest = readManifest("sa-idle");
293
+ expect(manifest.status).toBe("running");
294
+ expect(manifest.executionStatus).toBe("running");
295
+ expect(manifest.completedAt).toBeUndefined();
296
+ // 非终态化:不写 .state(归档 ≠ 放弃可重连性)
297
+ expect(fs.existsSync(`${sessionFile}.state`)).toBe(false);
298
+ store.dispose();
299
+ });
300
+
301
+ it("markBatchFinalized:批成员 running 投影双写(barrier 面词汇一致)", async () => {
302
+ const store = new RecordStore(sessionsDir, undefined, undefined, recordsDir);
303
+ const sessionFile = path.join(sessionsDir, "20260912T000009_batch.jsonl");
304
+ fs.writeFileSync(sessionFile, "{}\n", "utf-8");
305
+
306
+ await store.markBatchFinalized([makeSubagentRecord("sa-batch", sessionFile)]);
307
+ const manifest = readManifest("sa-batch");
308
+ expect(manifest.status).toBe("running");
309
+ expect(manifest.executionStatus).toBe("running");
310
+ expect(manifest.closedReason).toBeUndefined();
311
+ store.dispose();
312
+ });
313
+
314
+ it("重建投影的旧三态 cancelled 派生:`.state` cancelled → status cancelled + closedReason cancelled", () => {
315
+ // markCancelled 终态位的磁盘重建形态(buildRecord 分支 1):重建 manifest 的旧
316
+ // 词汇须能区分 cancelled(session-reader 投影消费三态)。
317
+ const fileA = path.join(sessionsDir, "20260912T000010_cxl.jsonl");
318
+ writeSessionJsonl(fileA, { id: "sa-cxl", task: "cancelled task", startedAt: 1000, rootSessionId: "root-session" });
319
+ writeCancelledState(fileA, 3000);
320
+
321
+ const store = new RecordStore(sessionsDir, undefined, undefined, recordsDir);
322
+ store.rebuildIndexes();
323
+ const manifest = readManifest("sa-cxl");
324
+ expect(manifest.status).toBe("cancelled");
325
+ expect(manifest.executionStatus).toBe("closed");
326
+ expect(manifest.closedReason).toBe("cancelled");
327
+ store.dispose();
328
+ });
329
+ });