@zhushanwen/subagent-core 0.2.0 → 0.4.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 (195) hide show
  1. package/README.md +45 -2
  2. package/agents/analyst.md +60 -0
  3. package/agents/coder.md +69 -0
  4. package/agents/debugger.md +66 -0
  5. package/agents/doc-reviewer.md +49 -0
  6. package/agents/explorer.md +63 -0
  7. package/agents/general-purpose.md +32 -0
  8. package/agents/orchestrator.md +61 -0
  9. package/agents/planner.md +53 -0
  10. package/agents/researcher.md +67 -0
  11. package/agents/reviewer.md +73 -0
  12. package/dist/chunk-4KN4TTG7.js +240 -0
  13. package/dist/chunk-APZY4IME.js +27 -0
  14. package/dist/chunk-X7SZ5HLQ.js +66 -0
  15. package/dist/execution/engine/engines/zcode/constants.cjs +65 -0
  16. package/dist/execution/engine/engines/zcode/constants.d.cts +93 -1
  17. package/dist/execution/engine/engines/zcode/constants.d.ts +93 -1
  18. package/dist/execution/engine/engines/zcode/constants.js +43 -1
  19. package/dist/execution/engine/engines/zcode/reader.d.cts +1 -1
  20. package/dist/execution/engine/engines/zcode/reader.d.ts +1 -1
  21. package/dist/execution/engine/engines/zcode/reader.js +4 -234
  22. package/dist/execution/engine/paths.js +7 -19
  23. package/dist/index.cjs +14709 -100
  24. package/dist/index.d.cts +4053 -200
  25. package/dist/index.d.ts +4053 -200
  26. package/dist/index.js +14358 -102
  27. package/dist/{types-BxyAidGf.d.cts → types-DpUO16pj.d.cts} +464 -1
  28. package/dist/{types-BxyAidGf.d.ts → types-DpUO16pj.d.ts} +464 -1
  29. package/package.json +6 -3
  30. package/src/__tests__/review-fix-loop-script.test.ts +90 -2
  31. package/src/__tests__/review-fix-loop-utils.test.ts +21 -0
  32. package/src/__tests__/smoke.test.ts +9 -2
  33. package/src/execution/__tests__/agent-profile.test.ts +232 -0
  34. package/src/execution/__tests__/agents-assembly.test.ts +219 -0
  35. package/src/execution/__tests__/chat-engine-routing.test.ts +71 -0
  36. package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +1 -0
  37. package/src/execution/__tests__/create-concurrency-pool.test.ts +194 -0
  38. package/src/execution/__tests__/delivery-methods.test.ts +2 -0
  39. package/src/execution/__tests__/descendant-sweep.test.ts +269 -0
  40. package/src/execution/__tests__/dialog-queue.test.ts +199 -1
  41. package/src/execution/__tests__/epipe-fallback.test.ts +2 -0
  42. package/src/execution/__tests__/execution-runtime-face.test.ts +272 -0
  43. package/src/execution/__tests__/finalize-record.test.ts +106 -0
  44. package/src/execution/__tests__/gc-timer.test.ts +2 -0
  45. package/src/execution/__tests__/get-record-for-action-restart.test.ts +2 -0
  46. package/src/execution/__tests__/get-state-handshake.test.ts +127 -0
  47. package/src/execution/__tests__/keep-alive-no-progress.test.ts +378 -0
  48. package/src/execution/__tests__/kill-all-escalation.test.ts +196 -0
  49. package/src/execution/__tests__/lifecycle-manager-idle-timer-identity.test.ts +117 -0
  50. package/src/execution/__tests__/lifecycle-manager.test.ts +39 -0
  51. package/src/execution/__tests__/manifest-store-tmp-recovery.test.ts +99 -0
  52. package/src/execution/__tests__/max-turns-to-watchdog-ms.test.ts +113 -0
  53. package/src/execution/__tests__/notify-ledger.test.ts +294 -0
  54. package/src/execution/__tests__/record-store-orphan-revive.test.ts +159 -0
  55. package/src/execution/__tests__/record-store.test.ts +83 -0
  56. package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +4 -0
  57. package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +1 -0
  58. package/src/execution/__tests__/run-spawn-edges.test.ts +407 -17
  59. package/src/execution/__tests__/run-spawn-integration.test.ts +1 -1
  60. package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +12 -4
  61. package/src/execution/__tests__/service-kill-escalation.test.ts +89 -0
  62. package/src/execution/__tests__/session-pending.test.ts +228 -0
  63. package/src/execution/__tests__/session-runner-branch-cache-lru.test.ts +110 -0
  64. package/src/execution/__tests__/session-runner-close-prune.test.ts +181 -0
  65. package/src/execution/__tests__/session-runner-epipe.test.ts +1 -0
  66. package/src/execution/__tests__/session-runner-heartbeat-idle-fallback.test.ts +226 -0
  67. package/src/execution/__tests__/settled-watchdog.test.ts +289 -0
  68. package/src/execution/__tests__/spawned-children.test.ts +153 -10
  69. package/src/execution/__tests__/subagent-actions-core.test.ts +997 -0
  70. package/src/execution/__tests__/subagent-service-message-close.test.ts +71 -10
  71. package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +174 -0
  72. package/src/execution/__tests__/subagent-service-notify-gate.test.ts +266 -0
  73. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +3 -0
  74. package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +316 -0
  75. package/src/execution/__tests__/timeout-integration.test.ts +7 -7
  76. package/src/execution/__tests__/ui-request-handler-factory.test.ts +95 -1
  77. package/src/execution/__tests__/ui-request-queue.test.ts +25 -0
  78. package/src/execution/__tests__/worktree-git-ops.test.ts +571 -0
  79. package/src/execution/__tests__/worktree-reconcile-aging.test.ts +204 -0
  80. package/src/execution/agent-registry.ts +225 -1
  81. package/src/execution/agents-assembly.ts +88 -0
  82. package/src/execution/concurrency-pool.ts +71 -9
  83. package/src/execution/dialog-queue.ts +101 -3
  84. package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +49 -2
  85. package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +87 -1
  86. package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +26 -0
  87. package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +83 -1
  88. package/src/execution/engine/__tests__/conformance/zcode-appserver-harness.ts +131 -0
  89. package/src/execution/engine/__tests__/registry.test.ts +90 -1
  90. package/src/execution/engine/engine-discovery.ts +12 -16
  91. package/src/execution/engine/engines/pi/task-spec-mapper.ts +3 -3
  92. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/fake-appserver.mjs +274 -0
  93. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-appserver.json +36 -0
  94. package/src/execution/engine/engines/zcode/__tests__/connection.test.ts +472 -0
  95. package/src/execution/engine/engines/zcode/__tests__/preparer-appserver.test.ts +387 -0
  96. package/src/execution/engine/engines/zcode/__tests__/session-channel.test.ts +780 -0
  97. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-appserver.test.ts +815 -0
  98. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-degrade.test.ts +462 -0
  99. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +119 -1
  100. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +136 -6
  101. package/src/execution/engine/engines/zcode/appserver-home.ts +442 -0
  102. package/src/execution/engine/engines/zcode/appserver-probe.ts +141 -0
  103. package/src/execution/engine/engines/zcode/connection.ts +585 -0
  104. package/src/execution/engine/engines/zcode/constants.ts +129 -0
  105. package/src/execution/engine/engines/zcode/golden-sample.ts +54 -8
  106. package/src/execution/engine/engines/zcode/preparer.ts +25 -22
  107. package/src/execution/engine/engines/zcode/session-channel.ts +656 -0
  108. package/src/execution/engine/engines/zcode/zcode-engine.ts +909 -41
  109. package/src/execution/engine/host-task-spec.ts +3 -3
  110. package/src/execution/engine/port.ts +41 -1
  111. package/src/execution/engine/registry.ts +53 -0
  112. package/src/execution/finalize-record.ts +63 -5
  113. package/src/execution/get-state-handshake.ts +85 -10
  114. package/src/execution/lifecycle-manager.ts +27 -3
  115. package/src/execution/manifest-store.ts +53 -62
  116. package/src/execution/notifier.ts +10 -10
  117. package/src/execution/notify-ledger.ts +117 -16
  118. package/src/execution/record-entry.ts +8 -2
  119. package/src/execution/record-store.ts +29 -15
  120. package/src/execution/session-pending.ts +213 -62
  121. package/src/execution/session-runner.ts +974 -128
  122. package/src/execution/sessions-index.ts +10 -55
  123. package/src/execution/settled-watchdog.ts +99 -0
  124. package/src/execution/subagent-actions-core.ts +686 -0
  125. package/src/execution/subagent-service.ts +396 -49
  126. package/src/execution/ui-request-handler-factory.ts +27 -4
  127. package/src/execution/worktree-git-ops.ts +397 -0
  128. package/src/execution/worktree-manager.ts +72 -10
  129. package/src/execution/worktree-registry.ts +6 -12
  130. package/src/index.ts +413 -6
  131. package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +42 -3
  132. package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +2 -4
  133. package/src/orchestration/__tests__/args-meta.test.ts +358 -0
  134. package/src/orchestration/__tests__/error-recovery-handlers.test.ts +1 -8
  135. package/src/orchestration/__tests__/error-recovery-rebuild-failure.test.ts +312 -0
  136. package/src/orchestration/__tests__/error-recovery-terminal-hardening.test.ts +382 -0
  137. package/src/orchestration/__tests__/file-run-store-prune.test.ts +168 -0
  138. package/src/orchestration/__tests__/file-run-store-throttle.test.ts +168 -0
  139. package/src/orchestration/__tests__/file-run-store.test.ts +389 -0
  140. package/src/orchestration/__tests__/helpers/flush-microtasks.ts +13 -0
  141. package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +34 -0
  142. package/src/orchestration/__tests__/lifecycle-abort-broadcast-signal.test.ts +357 -0
  143. package/src/orchestration/__tests__/lifecycle-recover-crashed.test.ts +289 -0
  144. package/src/orchestration/__tests__/lifecycle.test.ts +77 -0
  145. package/src/orchestration/__tests__/run-snapshot.test.ts +353 -0
  146. package/src/orchestration/__tests__/script-generate.test.ts +314 -0
  147. package/src/orchestration/__tests__/worker-pending-timeout-abort.test.ts +275 -0
  148. package/src/orchestration/__tests__/worker-script-builder-runtime.test.ts +78 -1
  149. package/src/orchestration/__tests__/workflow-files.test.ts +186 -0
  150. package/src/orchestration/__tests__/workflow-run-summary.test.ts +119 -0
  151. package/src/orchestration/__tests__/workflow-script-registry-impl.test.ts +124 -0
  152. package/src/orchestration/agent-opts-resolver.ts +7 -7
  153. package/src/orchestration/args-meta.ts +198 -0
  154. package/src/orchestration/error-recovery.ts +416 -146
  155. package/src/orchestration/execute-agent-call.ts +9 -9
  156. package/src/orchestration/file-run-store.ts +327 -0
  157. package/src/orchestration/launcher.ts +35 -26
  158. package/src/orchestration/lifecycle.ts +355 -79
  159. package/src/orchestration/models/agent-call.ts +7 -7
  160. package/src/orchestration/models/budget.ts +5 -5
  161. package/src/orchestration/models/run-runtime.ts +13 -13
  162. package/src/orchestration/models/trace.ts +8 -8
  163. package/src/orchestration/models/workflow-run.ts +27 -27
  164. package/src/orchestration/models/workflow-script.ts +4 -4
  165. package/src/orchestration/run-snapshot.ts +266 -0
  166. package/src/orchestration/script-generate.ts +154 -0
  167. package/src/orchestration/script-lint.ts +33 -33
  168. package/src/orchestration/worker-handle.ts +10 -10
  169. package/src/orchestration/worker-host.ts +10 -10
  170. package/src/orchestration/worker-script-builder.ts +401 -346
  171. package/src/orchestration/workflow-files.ts +37 -11
  172. package/src/orchestration/workflow-run-summary.ts +69 -0
  173. package/src/orchestration/workflow-script-registry-impl.ts +31 -9
  174. package/src/shared/__tests__/agent-ref.test.ts +209 -2
  175. package/src/shared/__tests__/atomic-write.test.ts +267 -0
  176. package/src/shared/__tests__/bounded-serialize.test.ts +236 -0
  177. package/src/shared/__tests__/injection-render.test.ts +518 -0
  178. package/src/shared/__tests__/resource-discovery-host-roots.test.ts +474 -0
  179. package/src/shared/__tests__/resource-discovery.test.ts +3 -2
  180. package/src/shared/agent-ref.ts +143 -2
  181. package/src/shared/atomic-write.ts +320 -0
  182. package/src/shared/bounded-serialize.ts +154 -0
  183. package/src/shared/injection-render.ts +279 -0
  184. package/src/shared/meta-parser.ts +41 -1
  185. package/src/shared/resource-discovery.ts +104 -37
  186. package/src/shared/resource-meta.ts +14 -0
  187. package/src/shared/xml-injection.ts +9 -9
  188. package/workflows/README.md +9 -9
  189. package/workflows/chain.js +4 -2
  190. package/workflows/map-reduce.js +5 -3
  191. package/workflows/parallel.js +5 -3
  192. package/workflows/review-fix-loop-utils.cjs +14 -3
  193. package/workflows/review-fix-loop.js +18 -7
  194. package/workflows/scatter-gather.js +4 -2
  195. package/dist/chunk-3VOERJPJ.js +0 -22
@@ -0,0 +1,159 @@
1
+ // src/execution/__tests__/record-store-orphan-revive.test.ts
2
+ //
3
+ // [PS-10/T6④] revive() 复位 orphanJudged——落实 recoverOrphanRecords 注释承诺
4
+ // 「IO 恢复后重开可重判」。
5
+ //
6
+ // 缺陷(设计 §4.3 PS-10):resumable 形态(IO-error 保守分支)无 .finalized sidecar
7
+ // 锚,重判资格完全由 orphanJudged 实例级缓存承载;dispose 有 clear 但 /new 复活路径
8
+ // revive() 此前不复位 → 同进程内曾经的 IO 失败记录永久停留 resumable。
9
+ //
10
+ // 覆盖(验收④:IO 恢复记录 revive 后再次扫描会重新判定,不再停留 resumable):
11
+ // 阶段 1 IO 失败(openSync EACCES 注入)→ 判定落 resumable entry(保守分支真实触发)
12
+ // 阶段 2 IO 恢复 + revive() → 再次 recoverOrphanRecords 重新判定收敛 closed+gc
13
+ // (未修复时 orphanJudged 残留 → 605 行 continue 零 entry,本用例红)
14
+ //
15
+ // fs mock:只劫持 openSync 的读模式("r")按目标路径计数放行——扫描侧
16
+ // readIdentityHeader(session-reconstructor)与判定侧 readLastJsonlLine(record-store
17
+ // 私有)走同一原语,放行计数 = 让扫描成功、判定失败的最小注入面;写模式(sidecar)
18
+ // 与其余 fs 全部透传 actual。
19
+
20
+ import * as fs from "node:fs";
21
+ import * as os from "node:os";
22
+ import * as path from "node:path";
23
+
24
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
25
+
26
+ const { openSyncMock, fsActualHolder, loggerMock } = vi.hoisted(() => ({
27
+ openSyncMock: vi.fn(),
28
+ fsActualHolder: {} as { fs?: typeof import("node:fs") },
29
+ loggerMock: { debug: vi.fn(), warn: vi.fn(), error: vi.fn() },
30
+ }));
31
+ vi.mock("node:fs", async (importOriginal) => {
32
+ const actual = await importOriginal<typeof import("node:fs")>();
33
+ fsActualHolder.fs = actual;
34
+ return { ...actual, openSync: openSyncMock };
35
+ });
36
+ vi.mock("../../core/logger.ts", () => ({ getLogger: () => loggerMock }));
37
+
38
+ import { RecordStore } from "../record-store.ts";
39
+
40
+ let tmpDir = "";
41
+
42
+ beforeEach(() => {
43
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "rs-revive-"));
44
+ loggerMock.debug.mockClear();
45
+ loggerMock.warn.mockClear();
46
+ loggerMock.error.mockClear();
47
+ });
48
+
49
+ afterEach(() => {
50
+ fs.rmSync(tmpDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
51
+ openSyncMock.mockReset();
52
+ });
53
+
54
+ /** 最小合法 session.jsonl:session header + subagent-identity + 完整 assistant 末行。 */
55
+ function writeOrphanSession(filePath: string, id: string): void {
56
+ const header = JSON.stringify({
57
+ type: "session", version: 3, id: "sess-uuid", timestamp: "2026-01-01T00:00:00.000Z", cwd: "/tmp",
58
+ });
59
+ const identityEntry = JSON.stringify({
60
+ type: "custom", id: "id-1", parentId: null,
61
+ timestamp: "2026-01-01T00:00:01.000Z", customType: "subagent-identity",
62
+ data: { id, agent: "worker", mode: "background", task: "orphan revive", startedAt: 1000, rootSessionId: "sess-orphan" },
63
+ });
64
+ const assistantMsg = JSON.stringify({
65
+ type: "message", id: "msg-1", parentId: "id-1",
66
+ timestamp: "2026-01-01T00:00:02.000Z",
67
+ message: {
68
+ role: "assistant",
69
+ content: [{ type: "text", text: "result" }],
70
+ usage: { input: 10, output: 20, cacheRead: 0, cacheWrite: 0, totalTokens: 30, cost: { total: 0 } },
71
+ stopReason: "stop", timestamp: 2000,
72
+ },
73
+ });
74
+ fs.writeFileSync(filePath, `${header}\n${identityEntry}\n${assistantMsg}\n`, "utf-8");
75
+ }
76
+
77
+ function makeStore(): { store: RecordStore; appended: Array<{ customType: string; data: Record<string, unknown> }> } {
78
+ const appended: Array<{ customType: string; data: Record<string, unknown> }> = [];
79
+ const store = new RecordStore(tmpDir, undefined, {
80
+ appendEntry: (customType: string, data: unknown) => {
81
+ appended.push({ customType, data: data as Record<string, unknown> });
82
+ },
83
+ } as never);
84
+ return { store, appended };
85
+ }
86
+
87
+ describe("[PS-10] revive() 复位 orphanJudged(IO 恢复后重开可重判)", () => {
88
+ it("IO 失败落 resumable → revive 后 IO 恢复,再次扫描重新判定收敛 closed+gc", () => {
89
+ const sessionFile = path.join(tmpDir, "orphan-revive.jsonl");
90
+ writeOrphanSession(sessionFile, "sa-revive-1");
91
+ const realOpenSync = fsActualHolder.fs!.openSync;
92
+
93
+ // 目标路径读模式 open 的放行计数:阶段 1 只放行 1 次(扫描侧 readIdentityHeader),
94
+ // 判定侧 readLastJsonlLine 的 open 注入 EACCES(IO 暂时失败的保守形态)。
95
+ let allowReads = Infinity;
96
+ openSyncMock.mockImplementation((p: unknown, flags: unknown) => {
97
+ if (p === sessionFile && typeof flags === "string" && flags.includes("r")) {
98
+ if (allowReads <= 0) {
99
+ const err = new Error(`EACCES: permission denied, open '${sessionFile}'`) as NodeJS.ErrnoException;
100
+ err.code = "EACCES";
101
+ throw err;
102
+ }
103
+ allowReads--;
104
+ }
105
+ return realOpenSync(p as Parameters<typeof realOpenSync>[0], flags as Parameters<typeof realOpenSync>[1]);
106
+ });
107
+
108
+ // ── 阶段 1:IO 失败 → 保守落 resumable(注释承诺的形态)──
109
+ allowReads = 1;
110
+ const { store, appended } = makeStore();
111
+ store.recoverOrphanRecords("sess-orphan");
112
+
113
+ expect(appended).toHaveLength(1);
114
+ expect(appended[0]?.customType).toBe("subagent-record");
115
+ expect(appended[0]?.data.status).toBe("running");
116
+ expect(appended[0]?.data.resumable).toBe(true);
117
+ expect(fs.existsSync(`${sessionFile}.finalized`)).toBe(false); // 保守分支无 sidecar 锚
118
+
119
+ // ── 阶段 2:IO 恢复 + /new 复活(revive 复位 orphanJudged)→ 重判收敛终态 ──
120
+ allowReads = Infinity;
121
+ store.revive();
122
+ store.recoverOrphanRecords("sess-orphan");
123
+
124
+ expect(appended).toHaveLength(2); // 未修复时 orphanJudged 残留 → 零新 entry,此处红
125
+ const rejudged = appended[1];
126
+ expect(rejudged?.data.status).toBe("closed");
127
+ expect(rejudged?.data.closedReason).toBe("gc");
128
+ expect(rejudged?.data.resumable).toBeUndefined(); // 不再停留 resumable
129
+ expect(fs.existsSync(`${sessionFile}.finalized`)).toBe(true); // 终态防重锚落盘
130
+ });
131
+
132
+ it("未 revive 时重判资格保持(防重缓存语义不回归):重复 recover 零新 entry", () => {
133
+ const sessionFile = path.join(tmpDir, "orphan-norevive.jsonl");
134
+ writeOrphanSession(sessionFile, "sa-revive-2");
135
+ const realOpenSync = fsActualHolder.fs!.openSync;
136
+ let allowReads = Infinity;
137
+ openSyncMock.mockImplementation((p: unknown, flags: unknown) => {
138
+ if (p === sessionFile && typeof flags === "string" && flags.includes("r")) {
139
+ if (allowReads <= 0) {
140
+ const err = new Error(`EACCES: permission denied, open '${sessionFile}'`) as NodeJS.ErrnoException;
141
+ err.code = "EACCES";
142
+ throw err;
143
+ }
144
+ allowReads--;
145
+ }
146
+ return realOpenSync(p as Parameters<typeof realOpenSync>[0], flags as Parameters<typeof realOpenSync>[1]);
147
+ });
148
+
149
+ allowReads = 1;
150
+ const { store, appended } = makeStore();
151
+ store.recoverOrphanRecords("sess-orphan");
152
+ expect(appended).toHaveLength(1);
153
+
154
+ // 同进程内不经历 revive(未重开):IO 已恢复也不重判——同 session 防重语义保持
155
+ allowReads = Infinity;
156
+ store.recoverOrphanRecords("sess-orphan");
157
+ expect(appended).toHaveLength(1);
158
+ });
159
+ });
@@ -17,6 +17,24 @@ import * as path from "node:path";
17
17
 
18
18
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
19
19
 
20
+ // [PS-14/T7③ / LC-9 等 T7 可观测性] Mock 共享 logger:sessions-index 写失败的
21
+ // warn 留痕可被断言(对齐 channel-registry-handshake.test.ts 模式)。
22
+ const { loggerMock } = vi.hoisted(() => ({
23
+ loggerMock: { debug: vi.fn(), warn: vi.fn(), error: vi.fn() },
24
+ }));
25
+ vi.mock("../../core/logger.ts", () => ({
26
+ getLogger: () => loggerMock,
27
+ }));
28
+
29
+ // [PS-14] saveIndex 可注入失败(默认 resolve,零影响既有用例;loadIndex/常量保留 actual)。
30
+ const { saveIndexMock } = vi.hoisted(() => ({
31
+ saveIndexMock: vi.fn(() => Promise.resolve()),
32
+ }));
33
+ vi.mock("../sessions-index.ts", async (importOriginal) => {
34
+ const actual = await importOriginal<typeof import("../sessions-index.ts")>();
35
+ return { ...actual, saveIndex: saveIndexMock };
36
+ });
37
+
20
38
  import { writeAliveMarker } from "../alive-store.ts";
21
39
  import { completeRecord, createRecord, tryTransition } from "../execution-record.ts";
22
40
  import { writeFinalized } from "../finalized-marker.ts";
@@ -106,6 +124,12 @@ describe("RecordStore", () => {
106
124
  afterEach(() => {
107
125
  // maxRetries:fire-and-forget 的 sessions-index 写可能与删除并发(ENOTEMPTY 竞态)
108
126
  fs.rmSync(rootDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
127
+ // [PS-14] 恢复 saveIndex 默认 resolve 实现 + 清 logger 断言(用例间隔离)
128
+ saveIndexMock.mockReset();
129
+ saveIndexMock.mockImplementation(() => Promise.resolve());
130
+ loggerMock.debug.mockClear();
131
+ loggerMock.warn.mockClear();
132
+ loggerMock.error.mockClear();
109
133
  });
110
134
 
111
135
  // ============================================================
@@ -1054,4 +1078,63 @@ describe("RecordStore", () => {
1054
1078
  expect(() => store.reportRecordTransition(r)).not.toThrow();
1055
1079
  });
1056
1080
  });
1081
+
1082
+ // ============================================================
1083
+ // [PS-14/T7③] sessions-index 写失败可观测(warn 而非仅 debug)
1084
+ // ============================================================
1085
+ // flushIndexAfterScan 触发链:collectRecords 全量扫描(首扫 dirStamp===null)→
1086
+ // scanFile 探测置 indexDirty → 扫描尾 flush(lastIndexWriteAt=0 天然过 60s 节流窗)
1087
+ // → saveIndex(mock reject)→ .catch 恢复 dirty + warn。
1088
+ describe("PS-14: sessions-index 写失败可观测", () => {
1089
+ it("saveIndex reject → warn(不再仅 debug),detail 含 dir 与 error", async () => {
1090
+ saveIndexMock.mockRejectedValueOnce(new Error("EACCES: permission denied"));
1091
+ writeSessionJsonl(path.join(tmpDir, "sa-ps14.jsonl"), {
1092
+ id: "ps14",
1093
+ agent: "worker",
1094
+ mode: "sync",
1095
+ task: "t",
1096
+ startedAt: 1000,
1097
+ });
1098
+
1099
+ const store = new RecordStore(tmpDir);
1100
+ const records = store.collectRecords();
1101
+ expect(records).toHaveLength(1); // 扫描本身不受索引写失败影响
1102
+
1103
+ // fire-and-forget saveIndex:等 .catch 执行
1104
+ await vi.waitFor(() => {
1105
+ expect(loggerMock.warn).toHaveBeenCalledWith(
1106
+ expect.stringContaining("sessions-index write failed"),
1107
+ expect.objectContaining({
1108
+ detail: expect.objectContaining({
1109
+ error: expect.stringContaining("EACCES: permission denied"),
1110
+ }),
1111
+ }),
1112
+ );
1113
+ });
1114
+ // 升级语义:warn 而非 debug(debug 层不再承载该信号)
1115
+ const debugIndexMsgs = loggerMock.debug.mock.calls.filter((c) =>
1116
+ String(c[0]).includes("sessions-index write failed"),
1117
+ );
1118
+ expect(debugIndexMsgs).toHaveLength(0);
1119
+ });
1120
+
1121
+ it("saveIndex 成功 → 零 warn(正常路径不制造噪音)", async () => {
1122
+ writeSessionJsonl(path.join(tmpDir, "sa-ps14-ok.jsonl"), {
1123
+ id: "ps14ok",
1124
+ agent: "worker",
1125
+ mode: "sync",
1126
+ task: "t",
1127
+ startedAt: 1000,
1128
+ });
1129
+
1130
+ const store = new RecordStore(tmpDir);
1131
+ store.collectRecords();
1132
+ await vi.waitFor(() => expect(saveIndexMock).toHaveBeenCalled());
1133
+
1134
+ const warnIndexMsgs = loggerMock.warn.mock.calls.filter((c) =>
1135
+ String(c[0]).includes("sessions-index write failed"),
1136
+ );
1137
+ expect(warnIndexMsgs).toHaveLength(0);
1138
+ });
1139
+ });
1057
1140
  });
@@ -28,6 +28,10 @@ vi.mock("../../core/logger.ts", () => ({ getLogger: () => loggerMock }));
28
28
  vi.mock("../session-runner.ts", () => ({
29
29
  runSpawn: vi.fn(),
30
30
  killAllSpawnedChildren: vi.fn(),
31
+ getChildByRecord: vi.fn(() => undefined),
32
+ registerSpawnedChildForRecord: vi.fn(),
33
+ killRecordChildWithEscalation: vi.fn(),
34
+ spawnedChildren: new Map(),
31
35
  }));
32
36
 
33
37
  import { runSpawn } from "../session-runner.ts";
@@ -60,6 +60,7 @@ vi.mock("../alive-store.ts", () => ({
60
60
  // 非 chatMode 用例 ④ 的 agent_end 走正常 kill 分支(count=0 → SIGTERM)。
61
61
  vi.mock("../session-pending.ts", () => ({
62
62
  readActivePendingFromSessionFile: vi.fn(() => ({ count: 0 })),
63
+ prunePendingCursor: vi.fn(),
63
64
  }));
64
65
 
65
66
  vi.mock("../temp-prompt.ts", () => ({