@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
@@ -3,7 +3,7 @@
3
3
  // [PS-10/T6④] revive() 复位 orphanJudged——落实 recoverOrphanRecords 注释承诺
4
4
  // 「IO 恢复后重开可重判」。
5
5
  //
6
- // 缺陷(设计 §4.3 PS-10):resumable 形态(IO-error 保守分支)无 .finalized sidecar
6
+ // 缺陷(设计 §4.3 PS-10):resumable 形态(IO-error 保守分支)无 .state sidecar
7
7
  // 锚,重判资格完全由 orphanJudged 实例级缓存承载;dispose 有 clear 但 /new 复活路径
8
8
  // revive() 此前不复位 → 同进程内曾经的 IO 失败记录永久停留 resumable。
9
9
  //
@@ -36,6 +36,7 @@ vi.mock("node:fs", async (importOriginal) => {
36
36
  vi.mock("../../core/logger.ts", () => ({ getLogger: () => loggerMock }));
37
37
 
38
38
  import { RecordStore } from "../record-store.ts";
39
+ import { writeAliveMarker } from "../alive-store.ts";
39
40
 
40
41
  let tmpDir = "";
41
42
 
@@ -114,7 +115,7 @@ describe("[PS-10] revive() 复位 orphanJudged(IO 恢复后重开可重判)"
114
115
  expect(appended[0]?.customType).toBe("subagent-record");
115
116
  expect(appended[0]?.data.status).toBe("running");
116
117
  expect(appended[0]?.data.resumable).toBe(true);
117
- expect(fs.existsSync(`${sessionFile}.finalized`)).toBe(false); // 保守分支无 sidecar 锚
118
+ expect(fs.existsSync(`${sessionFile}.state`)).toBe(false); // 保守分支无 sidecar 锚
118
119
 
119
120
  // ── 阶段 2:IO 恢复 + /new 复活(revive 复位 orphanJudged)→ 重判收敛终态 ──
120
121
  allowReads = Infinity;
@@ -126,7 +127,7 @@ describe("[PS-10] revive() 复位 orphanJudged(IO 恢复后重开可重判)"
126
127
  expect(rejudged?.data.status).toBe("closed");
127
128
  expect(rejudged?.data.closedReason).toBe("gc");
128
129
  expect(rejudged?.data.resumable).toBeUndefined(); // 不再停留 resumable
129
- expect(fs.existsSync(`${sessionFile}.finalized`)).toBe(true); // 终态防重锚落盘
130
+ expect(fs.existsSync(`${sessionFile}.state`)).toBe(true); // 终态防重锚落盘
130
131
  });
131
132
 
132
133
  it("未 revive 时重判资格保持(防重缓存语义不回归):重复 recover 零新 entry", () => {
@@ -157,3 +158,70 @@ describe("[PS-10] revive() 复位 orphanJudged(IO 恢复后重开可重判)"
157
158
  expect(appended).toHaveLength(1);
158
159
  });
159
160
  });
161
+
162
+ // ── [U4a / D3b (a″)] 孤儿恢复活实例跳过:findForeignLiveInstance 现查探针 ──
163
+ //
164
+ // 原判据 = 重建时缓存的 rec.externalInstance(分支 3 填充);externalInstance 字段链
165
+ // 删除后换直接探针(pid 单判据 + self-pid 排除)。验收(F2):
166
+ // - 探针活(异宿主 pid 在持声明)→ 跳过终态化——boot 不得误杀异宿主持有中 record
167
+ // (同 root 双宿主形态下,宿主 A 的 boot 把宿主 B 持有中的 record 直断 gc 会
168
+ // 击穿跨进程写权防御,设计 D3b (a″));
169
+ // - pid 死(无在持声明)→ 正常终态化(closed+gc + .state 防重锚)。
170
+ describe("[U4a / D3b (a″)] 孤儿恢复活实例跳过:现查探针(pid 单判据)", () => {
171
+ /** openSync 透传真实实现(本 describe 的判定路径需要真实 IO;mockReset 后默认
172
+ * 实现返回 undefined,readLastJsonlLine 会拿到非法 fd)。 */
173
+ function passthroughOpenSync(): void {
174
+ const realOpenSync = fsActualHolder.fs!.openSync;
175
+ openSyncMock.mockImplementation(
176
+ (p: Parameters<typeof realOpenSync>[0], flags: Parameters<typeof realOpenSync>[1]) =>
177
+ realOpenSync(p, flags),
178
+ );
179
+ }
180
+
181
+ it("探针活(异宿主 pid 在持声明)→ 跳过终态化:零 entry + 无 .state 防重锚", () => {
182
+ const sessionFile = path.join(tmpDir, "orphan-foreign-live.jsonl");
183
+ writeOrphanSession(sessionFile, "sa-probe-1");
184
+ // pid 1(launchd)必然存活且非本测试进程——异宿主「在持声明」的确定性形态
185
+ writeAliveMarker(sessionFile, { pid: 1, id: "sa-probe-1", startedAt: Date.now() });
186
+ passthroughOpenSync();
187
+
188
+ const { store, appended } = makeStore();
189
+ store.recoverOrphanRecords("sess-orphan");
190
+
191
+ expect(appended).toHaveLength(0); // 跳过:不落任何终态/resumable entry
192
+ expect(fs.existsSync(`${sessionFile}.state`)).toBe(false); // 未终态化 = 无防重锚
193
+ });
194
+
195
+ it("pid 死(marker 残留但持有者已退)→ 正常终态化:closed+gc entry + .state 防重锚", () => {
196
+ const sessionFile = path.join(tmpDir, "orphan-dead-pid.jsonl");
197
+ writeOrphanSession(sessionFile, "sa-probe-2");
198
+ // 大 pid 用户空间必然不存在(ESRCH 判死)——原持有宿主已退出的残留 marker 形态
199
+ writeAliveMarker(sessionFile, { pid: 9999999, id: "sa-probe-2", startedAt: Date.now() });
200
+ passthroughOpenSync();
201
+
202
+ const { store, appended } = makeStore();
203
+ store.recoverOrphanRecords("sess-orphan");
204
+
205
+ expect(appended).toHaveLength(1);
206
+ expect(appended[0]?.customType).toBe("subagent-record");
207
+ expect(appended[0]?.data.status).toBe("closed");
208
+ expect(appended[0]?.data.closedReason).toBe("gc");
209
+ expect(fs.existsSync(`${sessionFile}.state`)).toBe(true); // 终态防重锚落盘
210
+ });
211
+
212
+ it("self-pid marker(pid 复用到本进程的残留声明)→ 放行清理:原持有者已死,record 确为孤儿", () => {
213
+ const sessionFile = path.join(tmpDir, "orphan-self-pid.jsonl");
214
+ writeOrphanSession(sessionFile, "sa-probe-3");
215
+ // self-pid 排除:findForeignLiveInstance 视同无 foreign——复用窗口内残留 marker
216
+ // 不构成「异宿主在持」,record 是真孤儿应照常终态化
217
+ writeAliveMarker(sessionFile, { pid: process.pid, id: "sa-probe-3", startedAt: Date.now() });
218
+ passthroughOpenSync();
219
+
220
+ const { store, appended } = makeStore();
221
+ store.recoverOrphanRecords("sess-orphan");
222
+
223
+ expect(appended).toHaveLength(1);
224
+ expect(appended[0]?.data.status).toBe("closed");
225
+ expect(appended[0]?.data.closedReason).toBe("gc");
226
+ });
227
+ });
@@ -37,15 +37,15 @@ vi.mock("../sessions-index.ts", async (importOriginal) => {
37
37
 
38
38
  import { writeAliveMarker } from "../alive-store.ts";
39
39
  import { completeRecord, createRecord, projectOutcome, tryTransition } from "../execution-record.ts";
40
- import { writeFinalized } from "../finalized-marker.ts";
40
+ import { writeCancelledState, writeFinalizedState } from "../state-marker.ts";
41
41
  import type { ManifestRecord } from "../manifest-store.ts";
42
42
  import { ManifestStore } from "../manifest-store.ts";
43
43
  import { getSubagentRecordsDir, getSubagentSessionDir } from "../path-encoding.ts";
44
44
  import { SUBAGENT_RECORD_CUSTOM_TYPE } from "../record-entry.ts";
45
45
  import type { StatusFilter } from "../record-store.ts";
46
46
  import { RecordStore } from "../record-store.ts";
47
- import { writeCancelledTombstone } from "../tombstone-store.ts";
48
47
  import type { AliveMarker, ExecutionRecord } from "../types.ts";
48
+ import { writeLegacyCancelledSidecar, writeLegacyFinalizedSidecar } from "./helpers/legacy-sidecar.ts";
49
49
 
50
50
  /** 构造 ExecutionRecord(base 默认 running,over 覆盖任意字段)。 */
51
51
  function makeRecord(over: Partial<ExecutionRecord> = {}): ExecutionRecord {
@@ -187,12 +187,12 @@ describe("RecordStore", () => {
187
187
  expect(full?.status).toBe("running"); // 状态矩阵在全量路径同样套用
188
188
  });
189
189
 
190
- it("磁盘 session.jsonl + .finalized sidecar → done", () => {
190
+ it("磁盘 session.jsonl + .state sidecar(finalized) → done", () => {
191
191
  const sessionFile = path.join(tmpDir, "2026-01-01-uuid-b.jsonl");
192
192
  writeSessionJsonl(sessionFile, {
193
193
  id: "bg-2", agent: "worker", mode: "background", task: "do it", startedAt: 5000,
194
194
  });
195
- writeFinalized(sessionFile);
195
+ writeFinalizedState(sessionFile);
196
196
  const store = new RecordStore(tmpDir);
197
197
  const found = store.collectRecords(100).find((r) => r.id === "bg-2");
198
198
  expect(found).toBeDefined();
@@ -276,14 +276,12 @@ describe("RecordStore", () => {
276
276
  // cancelled tombstone override
277
277
  // ============================================================
278
278
  describe("cancelled tombstone", () => {
279
- it("有 .cancelled sidecar → status override 为 closed + closedReason=cancelled", () => {
279
+ it("有 .state sidecar(cancelled) → status override 为 closed + closedReason=cancelled", () => {
280
280
  const sessionFile = path.join(tmpDir, "2026-01-01-uuid-a.jsonl");
281
281
  writeSessionJsonl(sessionFile, {
282
282
  id: "bg-1", agent: "worker", mode: "background", task: "do it", startedAt: 5000,
283
283
  });
284
- writeCancelledTombstone(sessionFile, {
285
- id: "bg-1", status: "cancelled", agent: "worker", startedAt: 5000, endedAt: 6000,
286
- });
284
+ writeCancelledState(sessionFile, 6000);
287
285
  const store = new RecordStore(tmpDir);
288
286
  const found = store.collectRecords(100).find((r) => r.id === "bg-1");
289
287
  // v4 B-1:cancelled 折入 closed,closedReason='cancelled' 保留用户取消语义
@@ -302,12 +300,12 @@ describe("RecordStore", () => {
302
300
  // 内存 running record
303
301
  const running = makeRecord({ id: "run-1", mode: "background", startedAt: 3000, status: "running" });
304
302
  store.register(running);
305
- // 磁盘 closed record(.finalized sidecar → closed,v4 B-1 统一终态)
303
+ // 磁盘 closed record(.state sidecar(finalized) → closed,v4 B-1 统一终态)
306
304
  const doneFile = path.join(tmpDir, "a.jsonl");
307
305
  writeSessionJsonl(doneFile, {
308
306
  id: "done-1", agent: "w", mode: "background", task: "t", startedAt: 5000,
309
307
  });
310
- writeFinalized(doneFile);
308
+ writeFinalizedState(doneFile);
311
309
  const ids = store.collectRecords(100).map((r) => r.id);
312
310
  expect(ids[0]).toBe("run-1"); // running(0) 排在 closed(3) 前
313
311
  });
@@ -405,12 +403,10 @@ describe("RecordStore", () => {
405
403
  return sessionFile;
406
404
  }
407
405
 
408
- // ── 分支 1: .cancelled ──
409
- it(".cancelled sidecar → closed + closedReason=cancelled", () => {
406
+ // ── 分支 1: 终态 sidecar status=cancelled ──
407
+ it(".state sidecar(cancelled) → closed + closedReason=cancelled", () => {
410
408
  const sessionFile = writeBaseSession();
411
- writeCancelledTombstone(sessionFile, {
412
- id: SESSION_ID, status: "cancelled", agent: "worker", startedAt: STARTED_AT, endedAt: 6000,
413
- });
409
+ writeCancelledState(sessionFile, 6000);
414
410
  const store = new RecordStore(tmpDir);
415
411
  const found = store.collectRecords(100).find((r) => r.id === SESSION_ID);
416
412
  // v4 B-1:cancelled 折入 closed(closedReason='cancelled' 区分用户取消)
@@ -420,17 +416,17 @@ describe("RecordStore", () => {
420
416
  expect(found?.endedAt).toBe(6000);
421
417
  });
422
418
 
423
- // ── 分支 2: .finalized done ──
424
- it(".finalized sidecar + stopReason=stop → done", () => {
419
+ // ── 分支 2: 终态 sidecar status=finalizeddone)──
420
+ it(".state sidecar(finalized) + stopReason=stop → done", () => {
425
421
  const sessionFile = writeBaseSession();
426
- writeFinalized(sessionFile);
422
+ writeFinalizedState(sessionFile);
427
423
  const store = new RecordStore(tmpDir);
428
424
  const found = store.collectRecords(100).find((r) => r.id === SESSION_ID);
429
425
  expect(found?.status).toBe("closed");
430
426
  });
431
427
 
432
- // ── 分支 2: .finalized failed ──
433
- it(".finalized sidecar + stopReason=error → failed", () => {
428
+ // ── 分支 2: 终态 sidecar status=finalizedfailed)──
429
+ it(".state sidecar(finalized) + stopReason=error → failed", () => {
434
430
  // 写一个 stopReason=error 的 session.jsonl
435
431
  const sessionFile = path.join(tmpDir, "2026-01-01-uuid-fail.jsonl");
436
432
  const header = JSON.stringify({
@@ -455,22 +451,24 @@ describe("RecordStore", () => {
455
451
  });
456
452
  fs.writeFileSync(sessionFile, `${header}\n${identityEntry}\n${assistantMsg}\n`, "utf-8");
457
453
 
458
- writeFinalized(sessionFile);
454
+ writeFinalizedState(sessionFile);
459
455
  const store = new RecordStore(tmpDir);
460
456
  const found = store.collectRecords(100).find((r) => r.id === SESSION_ID);
461
457
  expect(found?.status).toBe("closed");
462
458
  });
463
459
 
464
- // ── 分支 3: .alive + 活 pid → running + externalInstance ──
465
- it(".alive + 存活 pid → running + externalInstance=true", () => {
460
+ // ── 分支 3→4: .alive + 活 pid → running 兜底(U4a / D3b (a):投影退役)──
461
+ it(".alive + 存活 pid → running 兜底,无 externalInstance 投影(探活读面走现查探针)", () => {
466
462
  const sessionFile = writeBaseSession();
467
- const recentStartedAt = Date.now() - 1000; // 1 秒前,确保未超 24h
463
+ const recentStartedAt = Date.now() - 1000; // 1 秒前
468
464
  const marker: AliveMarker = { pid: process.pid, id: SESSION_ID, startedAt: recentStartedAt };
469
465
  writeAliveMarker(sessionFile, marker);
470
466
  const store = new RecordStore(tmpDir);
471
467
  const found = store.collectRecords(100).find((r) => r.id === SESSION_ID);
472
468
  expect(found?.status).toBe("running");
473
- expect(found?.externalInstance).toEqual(marker);
469
+ // externalInstance 字段链已删(D3b (a):重建投影不再携带探活缓存,
470
+ // fork-from 守卫/孤儿恢复改 findForeignLiveInstance 现查)。
471
+ expect(found).not.toHaveProperty("externalInstance");
474
472
  });
475
473
 
476
474
  // ── 分支 3→4: .alive + 死 pid → running(SP-2:跨重启可恢复)──
@@ -481,7 +479,7 @@ describe("RecordStore", () => {
481
479
  const store = new RecordStore(tmpDir);
482
480
  const found = store.collectRecords(100).find((r) => r.id === SESSION_ID);
483
481
  expect(found?.status).toBe("running");
484
- expect(found?.externalInstance).toBeUndefined();
482
+ expect(found).not.toHaveProperty("externalInstance");
485
483
  });
486
484
 
487
485
  // ── 分支 4: 都无 sidecar → running(SP-2:跨重启可恢复)──
@@ -492,10 +490,10 @@ describe("RecordStore", () => {
492
490
  expect(found?.status).toBe("running");
493
491
  });
494
492
 
495
- // ── 分支 4: >24h 软超时 running(SP-2:跨重启可恢复,无视探活)──
496
- it(">24h 软超时 → running(SP-2,即使 pid 存活)", () => {
493
+ // ── 分支 4: 老旧 .alive(原 >24h 软超时形态)→ running(status 与 .alive 解耦)──
494
+ it(".alive 存在即不影响 status 判定 → running 兜底(软超时/探活读面已退役)", () => {
497
495
  const sessionFile = writeBaseSession();
498
- // startedAt 设为 25 小时前
496
+ // startedAt 设为 25 小时前(旧三判据形态下的软超时区——现已无判定参与)
499
497
  const oldStartedAt = Date.now() - 25 * 60 * 60 * 1000;
500
498
  const marker: AliveMarker = { pid: process.pid, id: SESSION_ID, startedAt: oldStartedAt };
501
499
  writeAliveMarker(sessionFile, marker);
@@ -509,27 +507,49 @@ describe("RecordStore", () => {
509
507
  const store = new RecordStore(tmpDir);
510
508
  const found = store.collectRecords(100).find((r) => r.id === SESSION_ID);
511
509
  expect(found?.status).toBe("running");
512
- expect(found?.externalInstance).toBeUndefined();
510
+ expect(found).not.toHaveProperty("externalInstance");
513
511
  });
514
512
 
515
- // ── 回归:.cancelled 优先于 .finalized ──
516
- it(".cancelled 优先于 .finalized(即使两者共存)", () => {
513
+ // ── 兼容读回归:存量旧名共存时 .cancelled 优先于 .finalized ──
514
+ it("兼容读:旧 .cancelled 优先于旧 .finalized(存量共存形态,优先级对齐合并前)", () => {
517
515
  const sessionFile = writeBaseSession();
518
- writeFinalized(sessionFile);
519
- writeCancelledTombstone(sessionFile, {
516
+ writeLegacyFinalizedSidecar(sessionFile);
517
+ writeLegacyCancelledSidecar(sessionFile, {
520
518
  id: SESSION_ID, status: "cancelled", agent: "worker", startedAt: STARTED_AT, endedAt: 6000,
521
519
  });
522
520
  const store = new RecordStore(tmpDir);
523
521
  const found = store.collectRecords(100).find((r) => r.id === SESSION_ID);
524
- // v4 B-1:.cancelled 优先级不变,但 status 折入 closed(closedReason='cancelled')
522
+ // v4 B-1cancelled 优先级不变,但 status 折入 closed(closedReason='cancelled')
525
523
  expect(found?.status).toBe("closed");
526
524
  expect(found?.closedReason).toBe("cancelled");
527
525
  });
528
526
 
529
- // ── 回归:旧 .cancelled 单分支行为不变 ──
530
- it(".cancelled 单分支行为不变(回归,status 折入 closed)", () => {
527
+ // ── 新名权威:.state 与存量旧名共存时 .state 胜出 ──
528
+ it(".state 优先于存量旧名(新写侧权威,旧文件残留不覆盖新终态)", () => {
529
+ const sessionFile = writeBaseSession();
530
+ writeLegacyCancelledSidecar(sessionFile, {
531
+ id: SESSION_ID, status: "cancelled", agent: "worker", startedAt: STARTED_AT, endedAt: 6000,
532
+ });
533
+ writeFinalizedState(sessionFile, "user-close");
534
+ const store = new RecordStore(tmpDir);
535
+ const found = store.collectRecords(100).find((r) => r.id === SESSION_ID);
536
+ expect(found?.status).toBe("closed");
537
+ expect(found?.closedReason).toBe("user-close");
538
+ });
539
+
540
+ // ── 回归:旧名单分支行为不变(存量 .finalized / .cancelled 仍被读出)──
541
+ it("兼容读:旧 .finalized 单分支行为不变(存量空文件 → disconnected)", () => {
531
542
  const sessionFile = writeBaseSession();
532
- writeCancelledTombstone(sessionFile, {
543
+ writeLegacyFinalizedSidecar(sessionFile);
544
+ const store = new RecordStore(tmpDir);
545
+ const found = store.collectRecords(100).find((r) => r.id === SESSION_ID);
546
+ expect(found?.status).toBe("closed");
547
+ expect(found?.closedReason).toBe("disconnected");
548
+ });
549
+
550
+ it("兼容读:旧 .cancelled 单分支行为不变(回归,status 折入 closed)", () => {
551
+ const sessionFile = writeBaseSession();
552
+ writeLegacyCancelledSidecar(sessionFile, {
533
553
  id: SESSION_ID, status: "cancelled", agent: "worker", startedAt: STARTED_AT, endedAt: 7000,
534
554
  });
535
555
  const store = new RecordStore(tmpDir);
@@ -621,13 +641,13 @@ describe("RecordStore", () => {
621
641
  // endedAt 重建(问题 2 修复:终态耗时不再随墙钟增长)
622
642
  // ============================================================
623
643
  describe("endedAt 重建(耗时不再无限增长)", () => {
624
- it(".finalized light endedAt 用 mtime 近似,全量 endedAt 为最后 entry 时间戳", () => {
644
+ it(".state(finalized)→ light endedAt 用 mtime 近似,全量 endedAt 为最后 entry 时间戳", () => {
625
645
  const sessionFile = path.join(tmpDir, "fin.jsonl");
626
646
  writeSessionJsonl(sessionFile, {
627
647
  id: "bg-1", agent: "w", mode: "background", task: "t",
628
648
  startedAt: 5000, lastTs: 9000, rootSessionId: "sess-A",
629
649
  });
630
- writeFinalized(sessionFile);
650
+ writeFinalizedState(sessionFile);
631
651
  const store = new RecordStore(tmpDir);
632
652
  const found = store.collectRecords(100, "all", "sess-A").find((r) => r.id === "bg-1");
633
653
  // [perf] light 分支 2:endedAt 用 jsonl mtime 近似(finalize 后文件不再变化,
@@ -768,14 +788,14 @@ describe("RecordStore", () => {
768
788
  expect(found?.endedAt).toBeUndefined();
769
789
  });
770
790
 
771
- // TC-2: 有 .finalized marker 的 record 仍归档为 done(不回归)
772
- it("TC-2: .finalized marker → done(分支 1/2 不回归)", () => {
791
+ // TC-2: 有 .state marker(finalized) 的 record 仍归档为 done(不回归)
792
+ it("TC-2: .state marker(finalized) → done(分支 1/2 不回归)", () => {
773
793
  const sessionFile = path.join(tmpDir, "sp2-tc2.jsonl");
774
794
  writeSessionJsonl(sessionFile, {
775
795
  id: "sa-sp2-2", agent: "worker", mode: "background", task: "finalized task",
776
796
  startedAt: 2000, rootSessionId: "sess-sp2",
777
797
  });
778
- writeFinalized(sessionFile);
798
+ writeFinalizedState(sessionFile);
779
799
  const store = new RecordStore(tmpDir);
780
800
  const found = store.collectRecords(100, "all", "sess-sp2").find((r) => r.id === "sa-sp2-2");
781
801
  expect(found).toBeDefined();
@@ -784,16 +804,14 @@ describe("RecordStore", () => {
784
804
  expect(found?.endedAt).toBeDefined();
785
805
  });
786
806
 
787
- // TC-2b: .cancelled marker → closed+cancelled(分支 1 不回归)
788
- it("TC-2b: .cancelled marker → closed+cancelled reason(分支 1 不回归)", () => {
807
+ // TC-2b: .state marker(cancelled) → closed+cancelled(分支 1 不回归)
808
+ it("TC-2b: .state marker(cancelled) → closed+cancelled reason(分支 1 不回归)", () => {
789
809
  const sessionFile = path.join(tmpDir, "sp2-tc2b.jsonl");
790
810
  writeSessionJsonl(sessionFile, {
791
811
  id: "sa-sp2-2b", agent: "worker", mode: "background", task: "cancelled task",
792
812
  startedAt: 3000, rootSessionId: "sess-sp2",
793
813
  });
794
- writeCancelledTombstone(sessionFile, {
795
- id: "sa-sp2-2b", status: "cancelled", agent: "worker", startedAt: 3000, endedAt: 4000,
796
- });
814
+ writeCancelledState(sessionFile, 4000);
797
815
  const store = new RecordStore(tmpDir);
798
816
  const found = store.collectRecords(100, "all", "sess-sp2").find((r) => r.id === "sa-sp2-2b");
799
817
  expect(found).toBeDefined();
@@ -839,7 +857,7 @@ describe("RecordStore", () => {
839
857
  closedReason: entry?.data.closedReason as never,
840
858
  error: entry?.data.error as string | undefined,
841
859
  })).toBe("failed");
842
- expect(fs.existsSync(`${sessionFile}.finalized`)).toBe(true);
860
+ expect(fs.existsSync(`${sessionFile}.state`)).toBe(true);
843
861
  // 防重:sidecar 使下次重建走分支 2(closed),不再进判定
844
862
  const again = [...appended];
845
863
  store.recoverOrphanRecords("sess-orphan");
@@ -889,7 +907,7 @@ describe("RecordStore", () => {
889
907
  expect(entry?.data.status).toBe("running");
890
908
  expect(entry?.data.resumable).toBe(true);
891
909
  // 保留分支不写终态 sidecar
892
- expect(fs.existsSync(`${sessionFile}.finalized`)).toBe(false);
910
+ expect(fs.existsSync(`${sessionFile}.state`)).toBe(false);
893
911
  });
894
912
 
895
913
  it("末行截断 → closed + error(保守方向;[F3] in-flight 同时携重启中断语义)", () => {
@@ -907,7 +925,7 @@ describe("RecordStore", () => {
907
925
  expect(entry?.data.status).toBe("closed");
908
926
  expect(entry?.data.error).toContain("truncated");
909
927
  expect(entry?.data.error).toContain("host restart");
910
- expect(fs.existsSync(`${sessionFile}.finalized`)).toBe(true);
928
+ expect(fs.existsSync(`${sessionFile}.state`)).toBe(true);
911
929
  });
912
930
 
913
931
  it("末行 >64KB 完整 JSON → 不误判截断,[F3] in-flight 携重启中断 error(无 truncated)", () => {
@@ -932,7 +950,7 @@ describe("RecordStore", () => {
932
950
  // [F3] in-flight 重启中断 error 必有;「不误判截断」= error 不含 truncated 标记
933
951
  expect(entry?.data.error).toContain("host restart");
934
952
  expect(entry?.data.error).not.toContain("truncated");
935
- expect(fs.existsSync(`${sessionFile}.finalized`)).toBe(true);
953
+ expect(fs.existsSync(`${sessionFile}.state`)).toBe(true);
936
954
  });
937
955
 
938
956
  it("chatMode 孤儿 → 不终态化,落 resumable entry(可续聊产品语义)", () => {
@@ -949,7 +967,7 @@ describe("RecordStore", () => {
949
967
  expect(entry?.data.resumable).toBe(true);
950
968
  expect(entry?.data.chatMode).toBe(true);
951
969
  // chat 分流不写终态 sidecar
952
- expect(fs.existsSync(`${sessionFile}.finalized`)).toBe(false);
970
+ expect(fs.existsSync(`${sessionFile}.state`)).toBe(false);
953
971
  // orphanJudged 缓存:同 store 重复调用不重复 append
954
972
  const count = appended.length;
955
973
  store.recoverOrphanRecords("sess-orphan");
@@ -962,7 +980,7 @@ describe("RecordStore", () => {
962
980
  id: "sa-orphan-4", agent: "worker", mode: "background", task: "already closed",
963
981
  startedAt: 4000, rootSessionId: "sess-orphan",
964
982
  });
965
- writeFinalized(closedFile);
983
+ writeFinalizedState(closedFile);
966
984
  const { store, appended } = makeRecoveryStore();
967
985
  store.recoverOrphanRecords("sess-orphan");
968
986
  expect(appended.find((c) => c.data.id === "sa-orphan-4")).toBeUndefined();
@@ -16,7 +16,7 @@
16
16
  // 4. forkDepth 基线:env PI_SUBAGENT_FORK_DEPTH → fork spawn env 递增
17
17
  //
18
18
  // mock 策略与 execute-nesting.test.ts 一致(spawn → FakeChild,fs 同步方法 mock,
19
- // temp-prompt / alive-store / finalized-marker / manifest-store mock;已收敛
19
+ // temp-prompt / alive-store / state-marker / manifest-store mock;已收敛
20
20
  // ./helpers/subagent-service-mocks.ts 四文件共享单源)。
21
21
 
22
22
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
@@ -24,7 +24,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
24
24
  import {
25
25
  aliveStoreModule,
26
26
  childProcessModule,
27
- finalizedMarkerModule,
27
+ stateMarkerModule,
28
28
  fsSyncModule,
29
29
  manifestStoreModule,
30
30
  } from "./helpers/subagent-service-mocks.ts";
@@ -34,7 +34,7 @@ import {
34
34
  vi.mock("node:child_process", () => childProcessModule());
35
35
  vi.mock("node:fs", async (importOriginal) => fsSyncModule(await importOriginal<typeof import("node:fs")>()));
36
36
  vi.mock("../alive-store.ts", async (importOriginal) => aliveStoreModule(await importOriginal<typeof import("../alive-store.ts")>()));
37
- vi.mock("../finalized-marker.ts", () => finalizedMarkerModule());
37
+ vi.mock("../state-marker.ts", () => stateMarkerModule());
38
38
  vi.mock("../manifest-store.ts", () => manifestStoreModule());
39
39
 
40
40
  import { spawn } from "node:child_process";
@@ -167,7 +167,9 @@ describe("进程级基线兜底(ALS 断裂修复,pi 事件回调模型)",
167
167
 
168
168
  const { service, store } = setup({});
169
169
  registerHangingEngine();
170
- const execNesting = Reflect.get(service, "execNesting") as ExecCtxAls;
170
+ // [R1 深绑改写] execNesting 随域 #2 聚合迁入 SessionBaselines,深绑路径改为
171
+ // service → baselines 聚合实例(断言对象与强度不变,路径对齐终态结构)。
172
+ const execNesting = (Reflect.get(service, "baselines") as { execNesting: ExecCtxAls }).execNesting;
171
173
 
172
174
  const handle = await execNesting.run({ recordId: "sa-inline-parent", depth: 3 }, () =>
173
175
  service.execute({ task: "inline nested", slug: "test", ctxModel }),
@@ -191,7 +193,8 @@ describe("进程级基线兜底(ALS 断裂修复,pi 事件回调模型)",
191
193
  const { service } = setup({});
192
194
  registerHangingEngine();
193
195
 
194
- expect(Reflect.get(service, "forkDepthBaseline")).toBe(1);
196
+ // [R1 深绑改写] forkDepthBaseline 经 baselines 聚合路径读取(同一权威字段,等价强度)。
197
+ expect((Reflect.get(service, "baselines") as { forkDepthBaseline: number }).forkDepthBaseline).toBe(1);
195
198
 
196
199
  // fork 请求照常派发(引擎侧承载深度传递)——不因基线读取抛错
197
200
  await expect(service.execute({ task: "fork child", slug: "test", ctxModel, fork: true })).resolves.toBeDefined();