@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhushanwen/subagent-core",
3
- "version": "0.7.1",
3
+ "version": "0.9.0",
4
4
  "description": "Cross-host shared subagent execution layer and workflow orchestration core (dual-form npm package: TS source for workspace, dist ESM+CJS for npm)",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -73,8 +73,8 @@
73
73
  "ajv": "^8.20.0",
74
74
  "proper-lockfile": "^4.1.2",
75
75
  "yaml": "^2.9.0",
76
- "@xyz-agent/extension-protocol": "0.8.2",
77
- "@zhushanwen/subagent-engine-sdk": "0.2.1"
76
+ "@xyz-agent/extension-protocol": "0.9.0",
77
+ "@zhushanwen/subagent-engine-sdk": "0.3.0"
78
78
  },
79
79
  "devDependencies": {
80
80
  "@types/node": "^24.0.0",
@@ -82,8 +82,8 @@
82
82
  "tsup": "^8.5.1",
83
83
  "typescript": "^5.7.0",
84
84
  "vitest": "^4.1.8",
85
- "@zhushanwen/pi-subagent-cli": "0.1.2",
86
- "@zhushanwen/zcode-subagent-cli": "0.1.2"
85
+ "@zhushanwen/pi-subagent-cli": "0.2.0",
86
+ "@zhushanwen/zcode-subagent-cli": "0.2.1"
87
87
  },
88
88
  "scripts": {
89
89
  "build": "tsup",
@@ -114,7 +114,10 @@ describe("ManifestStore", () => {
114
114
  });
115
115
  });
116
116
 
117
- describe("recoverTmpFiles", () => {
117
+ // [U4c / G3] tmp 恢复退役(D6):manifest 已是可丢可重建缓存(权威 = `.state`,
118
+ // 重建 = RecordStore.rebuildIndexes),promote 半写 tmp 的恢复语义失效——[H4/U5
119
+ // 更名收口] recoverTmpFiles → sweepTmpFiles(名实对齐静默删除),返回删除计数。
120
+ describe("sweepTmpFiles", () => {
118
121
  it("should delete tmp when manifest exists", async () => {
119
122
  const id = "test-recovery-1";
120
123
  const manifestPath = path.join(tmpDir, `${id}.json`);
@@ -123,20 +126,20 @@ describe("ManifestStore", () => {
123
126
  fs.writeFileSync(manifestPath, '{"id":"test-recovery-1"}');
124
127
  fs.writeFileSync(tmpPath, '{"id":"test-recovery-1"}');
125
128
 
126
- const result = await store.recoverTmpFiles();
129
+ const result = await store.sweepTmpFiles();
127
130
 
128
- expect(result.deleted).toBe(1);
131
+ expect(result).toBe(1);
129
132
  expect(fs.existsSync(tmpPath)).toBe(false);
130
133
  expect(fs.existsSync(manifestPath)).toBe(true);
131
134
  });
132
135
 
133
- it("should rename valid tmp when manifest missing", async () => {
136
+ it("should delete valid tmp when manifest missing (promote retired)", async () => {
134
137
  const id = "test-recovery-2";
135
138
  const tmpPath = path.join(tmpDir, `${id}.json.tmp.12345`);
136
139
  const manifestPath = path.join(tmpDir, `${id}.json`);
137
140
 
138
- // F2 recoverTmpFiles isValidManifest 严格校验——tmp 必须含全部必填字段
139
- // (id/rootSessionId/agentName/createdAt/status)才会 promote,否则走删除分支 3b。
141
+ // promote 分支形态(合法完整 manifest tmp + 正式文件缺失)也删——
142
+ // 半写 tmp 不再被复活成「看似权威」的索引,缺员由 rebuildIndexes 重建。
140
143
  fs.writeFileSync(tmpPath, JSON.stringify({
141
144
  id,
142
145
  rootSessionId: "session-123",
@@ -145,10 +148,10 @@ describe("ManifestStore", () => {
145
148
  createdAt: Date.now(),
146
149
  }));
147
150
 
148
- const result = await store.recoverTmpFiles();
151
+ const result = await store.sweepTmpFiles();
149
152
 
150
- expect(result.recovered).toBe(1);
151
- expect(fs.existsSync(manifestPath)).toBe(true);
153
+ expect(result).toBe(1);
154
+ expect(fs.existsSync(manifestPath)).toBe(false);
152
155
  expect(fs.existsSync(tmpPath)).toBe(false);
153
156
  });
154
157
 
@@ -158,20 +161,18 @@ describe("ManifestStore", () => {
158
161
 
159
162
  fs.writeFileSync(tmpPath, "invalid json {{{");
160
163
 
161
- const result = await store.recoverTmpFiles();
164
+ const result = await store.sweepTmpFiles();
162
165
 
163
- expect(result.deleted).toBe(1);
166
+ expect(result).toBe(1);
164
167
  expect(fs.existsSync(tmpPath)).toBe(false);
165
168
  });
166
169
 
167
- // F2:合法 JSON 但缺必填字段(非合法 manifest)应走删除(分支 3b),不能 promote
168
170
  it("should delete tmp when JSON valid but not a valid manifest (missing required fields)", async () => {
169
171
  const id = "test-recovery-4";
170
172
  const tmpPath = path.join(tmpDir, `${id}.json.tmp.12345`);
171
173
  fs.writeFileSync(tmpPath, JSON.stringify({ foo: "bar" })); // 合法 JSON,非 manifest
172
- const result = await store.recoverTmpFiles();
173
- expect(result.deleted).toBe(1);
174
- expect(result.recovered).toBe(0);
174
+ const result = await store.sweepTmpFiles();
175
+ expect(result).toBe(1);
175
176
  expect(fs.existsSync(tmpPath)).toBe(false);
176
177
  });
177
178
  });
@@ -21,6 +21,18 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
21
21
  import { ManifestStore } from "../execution/manifest-store";
22
22
  import { RecordStore } from "../execution/record-store";
23
23
 
24
+ // [Gate A teardown 稳定性] no-op 掉索引落盘。根因:recoverOrphanRecords 走的
25
+ // reconstructAll(record-store.ts)扫描尾 fire-and-forget saveIndex(tmp+fsync+rename
26
+ // 异步 fs),满载下该 promise 可能在本文件 teardown 之后才 settle,其失败分支经
27
+ // logger.warn → console.warn 上报 vitest(rpc onUserConsoleLog 在途)——worker 关闭
28
+ // rpc 时在途调用被 reject 为 EnvironmentTeardownError(0 断言失败,纯 teardown 时序)。
29
+ // 本文件用一次性 tmpdir、断言只观察 appendEntry 捕获的判定结果,落盘与否无观察者——
30
+ // no-op 消除在途 IO 链,任何负载下确定;loadIndex 等其余导出保留原实现。
31
+ vi.mock("../execution/sessions-index.ts", async (importOriginal) => ({
32
+ ...(await importOriginal<typeof import("../execution/sessions-index.ts")>()),
33
+ saveIndex: async () => {},
34
+ }));
35
+
24
36
  interface CapturedEntry {
25
37
  type: string;
26
38
  data: Record<string, unknown>;
@@ -1,6 +1,9 @@
1
1
  // Medium batch 1 robustness fixes verification
2
2
  //
3
- // M4: dispatchAgentCall .then() clears node.live BEFORE stale guard (not after)
3
+ // M4: [H2 W3 改写] dispatchAgentCall node.live 清理已随 trace.live 字段退役
4
+ // (设计 subagent-workflow-record-unification.md D2——旁路 progress record 族
5
+ // 整体删除,节点无运行期附属对象)。原「live 清理先于 stale guard」的顺序
6
+ // 断言改为退役回归:pump 无 node.live 写点。
4
7
  // M7: handleWorkerMessage validates msg shape before dereferencing msg.opts
5
8
  // M8: session-reconstructor guards Array.isArray(msg.content) before for...of
6
9
 
@@ -17,24 +20,17 @@ function readSrc(relPath: string): string {
17
20
  return readFileSync(join(PKG_ROOT, relPath), "utf-8");
18
21
  }
19
22
 
20
- // ── M4: node.live cleared before stale guard ─────────────────
23
+ // ── M4: node.live 写点已退役([H2 W3] 回归锁定) ─────────────
21
24
 
22
- describe("M4: dispatchAgentCall .then() clears node.live before stale guard", () => {
25
+ describe("M4: [H2 W3] dispatchAgentCall 不再写 node.live", () => {
23
26
  const src = readSrc(join("src", "orchestration", "worker-message-pump.ts"));
24
27
 
25
- it("node.live = undefined appears before the stale guard return", () => {
26
- // 找到 .then 回调中的 stale guard 和 node.live 清理的相对顺序
28
+ it("整个 pump 源码无 .live 写点(旁路 record 族退役),stale guard 仍在", () => {
29
+ expect(src).not.toMatch(/\.live\s*=/);
30
+ // .then 的 stale guard 仍在(原顺序断言的对象消失,守卫本身保留)
27
31
  const thenMatch = src.match(/\.then\(\(\)\s*=>\s*\{[\s\S]*?\}\)/);
28
32
  expect(thenMatch).toBeTruthy();
29
- const thenBlock = thenMatch![0];
30
-
31
- const liveIdx = thenBlock.indexOf("node.live = undefined");
32
- const guardIdx = thenBlock.indexOf('run.state.status !== "running"');
33
-
34
- expect(liveIdx).toBeGreaterThan(-1);
35
- expect(guardIdx).toBeGreaterThan(-1);
36
- // node.live 清理必须在 stale guard 之前(代码行号更小)
37
- expect(liveIdx).toBeLessThan(guardIdx);
33
+ expect(thenMatch![0]).toContain('run.state.status !== "running"');
38
34
  });
39
35
  });
40
36
 
@@ -24,11 +24,16 @@ function readSrc(relPath: string): string {
24
24
  // ── M6: worktree cleanup decoupled from patchOk ──────────────
25
25
 
26
26
  describe("M6: worktree cleanup not gated by patchOk", () => {
27
- const src = readSrc(join("src", "execution", "subagent-service.ts"));
27
+ // [H3/R3] worktree cleanup 调用点(disposeAllRecords / cancelBackground)已随终态
28
+ // 写面迁 execution/service/record-lifecycle.ts(strangler 抽取,壳只留单行转发)——
29
+ // 源文本守卫随被守卫本体改读聚合文件,断言强度不变(patchOk 解耦锁)。
30
+ const src = readSrc(join("src", "execution", "service", "record-lifecycle.ts"));
28
31
 
29
32
  it("worktree cleanup condition does not reference patchOk", () => {
30
33
  // 找到 worktree cleanup 调用前的条件判断
31
- const cleanupMatch = src.match(/if\s*\([^)]*worktreeHandle[^)]*\)\s*\{[\s\S]*?worktreeManager\.cleanup/);
34
+ // [H3/R3] cleanup 通道字面随依赖注入改造为 deps.getWorktreeManager().cleanup
35
+ //(this.worktreeManager → this.deps.getWorktreeManager(),方法体其余逐字节保留)。
36
+ const cleanupMatch = src.match(/if\s*\([^)]*worktreeHandle[^)]*\)\s*\{[\s\S]*?getWorktreeManager\(\)\.cleanup/);
32
37
  expect(cleanupMatch).toBeTruthy();
33
38
  const condition = cleanupMatch![0];
34
39
  // 条件中不应包含 patchOk(解耦后 worktree cleanup 只依赖 worktreeHandle 存在)
@@ -13,7 +13,7 @@
13
13
 
14
14
  import { getHostServices } from "./host-services.ts";
15
15
 
16
- /** 日志级别。对齐 @zhushanwen/pi-extension-logger LogLevel(三值,无 info)。 */
16
+ /** 日志级别。级别集合对齐 pi-extension-logger(其 LogLevel 为包内部类型、含 info 四值;实例 API 仅 debug/warn/error 三方法,core facade 据此收窄为三值)。 */
17
17
  export type LogLevel = "debug" | "warn" | "error";
18
18
 
19
19
  /** core logger 接口。与 pi-extension-logger 的 ExtensionLogger 结构兼容——
@@ -4,9 +4,33 @@ import * as fs from "node:fs";
4
4
  import * as os from "node:os";
5
5
  import * as path from "node:path";
6
6
 
7
- import { afterEach,describe, expect, it } from "vitest";
7
+ import { afterEach, describe, expect, it, vi } from "vitest";
8
+
9
+ // [U1 A4] removeAliveMarker 失败留痕断言需要 warn 可观察。
10
+ const { loggerMock } = vi.hoisted(() => ({
11
+ loggerMock: { debug: vi.fn(), warn: vi.fn(), error: vi.fn() },
12
+ }));
13
+ vi.mock("../../core/logger.ts", () => ({ getLogger: () => loggerMock }));
14
+
15
+ // fs partial mock:unlinkSync 可注错(release 失败留痕用例),其余转发真实实现。
16
+ // ESM namespace 不可直接赋值/spyOn,注错必须走模块 mock(state-marker.test.ts 同款)。
17
+ type UnlinkSyncFn = typeof import("node:fs").unlinkSync;
18
+ const { unlinkSyncMock, actualUnlinkRef } = vi.hoisted(() => ({
19
+ unlinkSyncMock: vi.fn(),
20
+ actualUnlinkRef: { current: undefined as UnlinkSyncFn | undefined },
21
+ }));
22
+ vi.mock("node:fs", async (importOriginal) => {
23
+ const actual = await importOriginal<typeof import("node:fs")>();
24
+ actualUnlinkRef.current = actual.unlinkSync;
25
+ return {
26
+ ...actual,
27
+ unlinkSync: unlinkSyncMock,
28
+ default: { ...actual, unlinkSync: unlinkSyncMock },
29
+ };
30
+ });
8
31
 
9
32
  import {
33
+ findForeignLiveInstance,
10
34
  isProcessAlive,
11
35
  readAliveMarker,
12
36
  removeAliveMarker,
@@ -18,6 +42,13 @@ function makeTmpDir(): string {
18
42
  return fs.mkdtempSync(path.join(os.tmpdir(), "alive-store-test-"));
19
43
  }
20
44
 
45
+ /**
46
+ * [U1 A4] 「异进程且存活」的确定性模拟 pid:1 号进程(launchd/init)必然存在且非
47
+ * 本测试进程——kill(1, 0) 对普通用户返回 EPERM,isProcessAlive 按「存在但无权限」
48
+ * 保守判活。self-pid 排除后不能再以本进程 pid 模拟异进程实例。
49
+ */
50
+ const FOREIGN_LIVE_PID = 1;
51
+
21
52
  describe("alive-store", () => {
22
53
  let tmpDir: string;
23
54
 
@@ -25,6 +56,11 @@ describe("alive-store", () => {
25
56
  if (tmpDir) {
26
57
  fs.rmSync(tmpDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
27
58
  }
59
+ // unlinkSync mock 基线复位 = 真实实现(个别用例注错覆盖后恢复)。
60
+ if (actualUnlinkRef.current !== undefined) {
61
+ unlinkSyncMock.mockReset().mockImplementation(actualUnlinkRef.current);
62
+ }
63
+ loggerMock.warn.mockClear();
28
64
  });
29
65
 
30
66
  // ── writeAliveMarker + readAliveMarker 往返 ──
@@ -144,4 +180,84 @@ describe("alive-store", () => {
144
180
  process.kill = originalKill;
145
181
  }
146
182
  });
183
+
184
+ // ── [U1 A4] findForeignLiveInstance:self-pid 排除 + pid 单判据三态 ──
185
+
186
+ describe("findForeignLiveInstance(D3c/D3b v7:self-pid 排除 + pid 单判据)", () => {
187
+ it("本进程 pid(marker.pid === process.pid)→ 放行(视同无 foreign,D3c 排除)", () => {
188
+ tmpDir = makeTmpDir();
189
+ const sessionFile = path.join(tmpDir, "session.jsonl");
190
+ writeAliveMarker(sessionFile, { pid: process.pid, id: "bg-1", startedAt: Date.now() });
191
+
192
+ expect(findForeignLiveInstance(sessionFile)).toBeUndefined();
193
+ });
194
+
195
+ it("异进程 pid 活 → 拦截(返回 marker,在持声明)", () => {
196
+ tmpDir = makeTmpDir();
197
+ const sessionFile = path.join(tmpDir, "session.jsonl");
198
+ const marker: AliveMarker = { pid: FOREIGN_LIVE_PID, id: "bg-1", startedAt: Date.now() };
199
+ writeAliveMarker(sessionFile, marker);
200
+
201
+ expect(findForeignLiveInstance(sessionFile)).toEqual(marker);
202
+ });
203
+
204
+ it("pid 死 → 放行(确死,允许透明重生)", () => {
205
+ tmpDir = makeTmpDir();
206
+ const sessionFile = path.join(tmpDir, "session.jsonl");
207
+ // 大 pid 用户空间必然不存在(isProcessAlive ESRCH 判死,与既有用例同款手法)
208
+ writeAliveMarker(sessionFile, { pid: 9999999, id: "bg-1", startedAt: Date.now() });
209
+
210
+ expect(findForeignLiveInstance(sessionFile)).toBeUndefined();
211
+ });
212
+
213
+ it("软超时不再参与判定:异进程 pid 活 + startedAt 超 1h 仍拦截(防御不随 idle 时长衰减)", () => {
214
+ tmpDir = makeTmpDir();
215
+ const sessionFile = path.join(tmpDir, "session.jsonl");
216
+ const stale: AliveMarker = {
217
+ pid: FOREIGN_LIVE_PID,
218
+ id: "bg-1",
219
+ startedAt: Date.now() - 2 * 3_600_000, // 2h 前——旧三判据形态下会超软超时放行
220
+ };
221
+ writeAliveMarker(sessionFile, stale);
222
+
223
+ expect(findForeignLiveInstance(sessionFile)).toEqual(stale);
224
+ });
225
+
226
+ it("marker 缺失/损坏 → 放行(无声明)", () => {
227
+ tmpDir = makeTmpDir();
228
+ const sessionFile = path.join(tmpDir, "session.jsonl");
229
+ expect(findForeignLiveInstance(sessionFile)).toBeUndefined();
230
+ fs.writeFileSync(`${sessionFile}.alive`, "not-json", "utf-8");
231
+ expect(findForeignLiveInstance(sessionFile)).toBeUndefined();
232
+ });
233
+ });
234
+
235
+ // ── [U1] removeAliveMarker 失败留痕(D3a:release 失败 bestEffort 留痕)──
236
+
237
+ describe("removeAliveMarker 失败留痕", () => {
238
+ it("删除失败(非 ENOENT)→ warn 留痕不抛出", () => {
239
+ tmpDir = makeTmpDir();
240
+ const sessionFile = path.join(tmpDir, "session.jsonl");
241
+ writeAliveMarker(sessionFile, { pid: 1, id: "x", startedAt: 0 });
242
+ unlinkSyncMock.mockImplementation(() => {
243
+ const err = new Error("EACCES") as NodeJS.ErrnoException;
244
+ err.code = "EACCES";
245
+ throw err;
246
+ });
247
+ expect(() => removeAliveMarker(sessionFile)).not.toThrow();
248
+ expect(loggerMock.warn).toHaveBeenCalledTimes(1);
249
+ });
250
+
251
+ it("ENOENT(marker 不存在)→ 静默(幂等终态,无 warn)", () => {
252
+ tmpDir = makeTmpDir();
253
+ const sessionFile = path.join(tmpDir, "session.jsonl");
254
+ unlinkSyncMock.mockImplementation(() => {
255
+ const err = new Error("ENOENT") as NodeJS.ErrnoException;
256
+ err.code = "ENOENT";
257
+ throw err;
258
+ });
259
+ expect(() => removeAliveMarker(sessionFile)).not.toThrow();
260
+ expect(loggerMock.warn).not.toHaveBeenCalled();
261
+ });
262
+ });
147
263
  });
@@ -0,0 +1,206 @@
1
+ // src/execution/__tests__/batch-finalized.test.ts
2
+ //
3
+ // [U3 / D1] 批写归口后的 barrier 语义测试(设计 docs/design/subagent-record-
4
+ // persistence-consolidation.md §3.1 markBatchFinalized 行 + §3.3 D4②/D5):
5
+ //
6
+ // 1. E1 调用面(SyncCollectDomain 直构 + 真 RecordStore):notifyBatch(写账入口)
7
+ // 被调用的时刻,全部成员 manifest 必已在磁盘——「manifest 落盘完成先于批通知
8
+ // 写账」的构造性保证(违反即红;迁移前 fire-and-forget 形态该时点只大概率成立);
9
+ // 2. 原语(manifestDir 接线,构造点同步分支):markBatchFinalized await 返回即
10
+ // manifest 文件可见(无 fire-and-forget)+ 落标 entry 携带 collectMode/batchFinalized
11
+ // 显式覆写 + 落标写出的时点 manifest 已在(原语内部序);
12
+ // 3. 原语(降级分支,manifestDir 缺省——双轨期形态):allSettled 屏障后落标,
13
+ // barrier 语义与同步分支等价。
14
+ //
15
+ // 与 record-store-intent-api.test.ts(U1,store 单体语义)的分工:本文件断言
16
+ // **调用面时序**——批通知写账(notifyBatch)/ 落标 entry 写出的时刻 manifest 必已
17
+ // 就位,即 D1 验收条款的字面断言面。
18
+ //
19
+ // 通路保真:RecordStore / SyncCollectDomain / ManifestStore 走真实实现(tmpdir
20
+ // 自建自删,红线),仅 pi.appendEntry / notifyHost 为 mock(写账与落标的观察点)。
21
+
22
+ import * as fs from "node:fs";
23
+ import * as os from "node:os";
24
+ import * as path from "node:path";
25
+
26
+ import { afterEach, beforeEach, describe, expect, it, vi, type Mock } from "vitest";
27
+
28
+ import { ManifestStore } from "../manifest-store.ts";
29
+ import { getSubagentRecordsDir, getSubagentSessionDir } from "../path-encoding.ts";
30
+ import { RecordStore } from "../record-store.ts";
31
+ import { SyncCollectDomain } from "../service/sync-collect-domain.ts";
32
+ import type { SubagentRecord } from "../types.ts";
33
+
34
+ const ROOT_SESSION = "root-batch-finalized";
35
+
36
+ /** 种子成员 record(对齐 sync-collect-recovery.test.ts memberRecord 形态——
37
+ * rebuildEntryRecord 解析门槛字段齐备)。 */
38
+ function memberRecord(overrides: Partial<SubagentRecord> & { id: string }): SubagentRecord {
39
+ return {
40
+ agent: "/agents/worker.md",
41
+ task: "batch task",
42
+ slug: "batch",
43
+ status: "running",
44
+ mode: "background",
45
+ startedAt: 1000,
46
+ rootSessionId: ROOT_SESSION,
47
+ parentRecordId: undefined,
48
+ depth: 0,
49
+ endedAt: undefined,
50
+ turns: 0,
51
+ totalTokens: 0,
52
+ model: "prov/m1",
53
+ thinkingLevel: undefined,
54
+ eventLog: [],
55
+ displayItems: [],
56
+ result: undefined,
57
+ error: undefined,
58
+ sessionFile: undefined,
59
+ chatMode: false,
60
+ collectMode: "sync",
61
+ ...overrides,
62
+ };
63
+ }
64
+
65
+ describe("batch-finalized 归口(U3/D1)——barrier:manifest 落盘完成先于批通知写账", () => {
66
+ let tmpDir: string;
67
+ let sessionsDir: string;
68
+ let manifestDir: string;
69
+ let mainFile: string;
70
+ let appendEntryMock: Mock<(customType: string, data: unknown) => void>;
71
+
72
+ beforeEach(() => {
73
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "core-batch-finalized-"));
74
+ sessionsDir = getSubagentSessionDir(tmpDir, tmpDir);
75
+ manifestDir = getSubagentRecordsDir(tmpDir, tmpDir);
76
+ fs.mkdirSync(sessionsDir, { recursive: true });
77
+ fs.mkdirSync(manifestDir, { recursive: true });
78
+ mainFile = path.join(tmpDir, "main-session.jsonl");
79
+ appendEntryMock = vi.fn();
80
+ });
81
+
82
+ afterEach(() => {
83
+ fs.rmSync(tmpDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
84
+ });
85
+
86
+ /** 写文件 pi(种子用):appendEntry 真写主 session JSONL(pi 落盘形态)。 */
87
+ function makeWritingPi() {
88
+ return {
89
+ appendEntry: (customType: string, data: unknown) => {
90
+ fs.appendFileSync(
91
+ mainFile,
92
+ `${JSON.stringify({
93
+ type: "custom",
94
+ id: `seed-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
95
+ parentId: null,
96
+ timestamp: new Date().toISOString(),
97
+ customType,
98
+ data,
99
+ })}\n`,
100
+ "utf-8",
101
+ );
102
+ },
103
+ };
104
+ }
105
+
106
+ it("E1 调用面:notifyBatch 写账时点全成员 manifest 已落盘(构造性 barrier,违反即红)", async () => {
107
+ // 种子:两成员(register running+sync → 终态 closed)经真实 reportSubagentRecord
108
+ // 序列化落主文件——崩溃残留(全员终态 + 无标记)形态。
109
+ const seedStore = new RecordStore(sessionsDir, undefined, makeWritingPi());
110
+ for (const id of ["sa-bf-a", "sa-bf-b"]) {
111
+ seedStore.reportSubagentRecord(memberRecord({ id }));
112
+ seedStore.reportSubagentRecord(
113
+ memberRecord({ id, status: "closed", closedReason: "gc", endedAt: 2000, result: `done-${id}` }),
114
+ );
115
+ }
116
+
117
+ // 被测面:真 RecordStore(manifestDir 接线——构造点形态)+ 直构 SyncCollectDomain。
118
+ const store = new RecordStore(sessionsDir, undefined, { appendEntry: appendEntryMock }, manifestDir);
119
+ const notifyBatch = vi.fn(() => true);
120
+ const domain = new SyncCollectDomain({
121
+ getStore: () => store,
122
+ getNotifyHost: () => ({
123
+ toNotifyRecord: () => undefined,
124
+ notify: () => {},
125
+ notifyBatch,
126
+ }),
127
+ getPi: () => null,
128
+ getSessionRootId: () => ROOT_SESSION,
129
+ getMainSessionFile: () => mainFile,
130
+ getCollectSyncSection: () => undefined,
131
+ });
132
+
133
+ // 写账时点钩子(严格时序门):notifyBatch 被调用的时刻,两成员 manifest 必已
134
+ // 在磁盘——这是 D1 barrier 的字面断言面(「通知可达 ⇒ 索引就位」)。
135
+ const manifestsAtLedgerWrite: boolean[] = [];
136
+ notifyBatch.mockImplementation(() => {
137
+ manifestsAtLedgerWrite.push(
138
+ fs.existsSync(path.join(manifestDir, "sa-bf-a.json")),
139
+ fs.existsSync(path.join(manifestDir, "sa-bf-b.json")),
140
+ );
141
+ return true;
142
+ });
143
+
144
+ await domain.recoverSyncCollectBatch();
145
+
146
+ expect(notifyBatch).toHaveBeenCalledTimes(1);
147
+ expect(manifestsAtLedgerWrite).toEqual([true, true]);
148
+ // 落标随 markBatchFinalized 前置完成(U3 归口):两成员各一笔带标记 entry。
149
+ const marks = appendEntryMock.mock.calls
150
+ .filter((c) => c[0] === "subagent-record")
151
+ .map((c) => c[1] as Record<string, unknown>)
152
+ .filter((d) => d["batchFinalized"] === true);
153
+ expect(marks.map((m) => m["id"]).sort()).toEqual(["sa-bf-a", "sa-bf-b"]);
154
+ for (const mark of marks) expect(mark["collectMode"]).toBe("sync");
155
+ });
156
+
157
+ it("原语(manifestDir 接线 / 同步分支):await 返回即 manifest 可见;落标 entry 写出时点 manifest 已在", async () => {
158
+ const store = new RecordStore(sessionsDir, undefined, { appendEntry: appendEntryMock }, manifestDir);
159
+ const members = [memberRecord({ id: "sa-sync-1" }), memberRecord({ id: "sa-sync-2" })];
160
+
161
+ // 落标时点钩子:appendEntry(落标写面)触发的时刻,该成员 manifest 必已存在
162
+ // (原语内部序 = manifest 落盘完成先于批通知写账/落标 entry)。
163
+ appendEntryMock.mockImplementation((_type: string, data: unknown) => {
164
+ const d = data as { id?: string };
165
+ if (typeof d?.id === "string") {
166
+ expect(fs.existsSync(path.join(manifestDir, `${d.id}.json`)), `manifest of ${d.id} at entry time`).toBe(true);
167
+ }
168
+ });
169
+
170
+ await store.markBatchFinalized(members);
171
+
172
+ // 写后即刻可见(无 fire-and-forget——D8 writeSync 语义)。
173
+ expect(fs.existsSync(path.join(manifestDir, "sa-sync-1.json"))).toBe(true);
174
+ expect(fs.existsSync(path.join(manifestDir, "sa-sync-2.json"))).toBe(true);
175
+ // 落标 entry:collectMode/batchFinalized 显式覆写(防非 entry 源重建丢标记)。
176
+ expect(appendEntryMock).toHaveBeenCalledTimes(2);
177
+ expect(appendEntryMock).toHaveBeenCalledWith(
178
+ "subagent-record",
179
+ expect.objectContaining({ id: "sa-sync-1", collectMode: "sync", batchFinalized: true }),
180
+ );
181
+ });
182
+
183
+ it("原语(降级分支 / manifestDir 缺省——双轨期形态):allSettled 屏障后落标,barrier 语义等价", async () => {
184
+ // 缺省 manifestDir:store 降级走 manifestStore.writeManifest 异步屏障(双轨期
185
+ // 现行语义,D7)——await 完成后 manifest 照常落盘 + 落标在其后(顺序不变)。
186
+ const store = new RecordStore(
187
+ sessionsDir,
188
+ new ManifestStore(manifestDir),
189
+ { appendEntry: appendEntryMock },
190
+ );
191
+ appendEntryMock.mockImplementation((_type: string, data: unknown) => {
192
+ const d = data as { id?: string };
193
+ if (typeof d?.id === "string") {
194
+ expect(fs.existsSync(path.join(manifestDir, `${d.id}.json`)), `manifest of ${d.id} at entry time`).toBe(true);
195
+ }
196
+ });
197
+
198
+ await store.markBatchFinalized([memberRecord({ id: "sa-degraded" })]);
199
+
200
+ expect(fs.existsSync(path.join(manifestDir, "sa-degraded.json"))).toBe(true);
201
+ expect(appendEntryMock).toHaveBeenCalledWith(
202
+ "subagent-record",
203
+ expect.objectContaining({ id: "sa-degraded", collectMode: "sync", batchFinalized: true }),
204
+ );
205
+ });
206
+ });