@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
@@ -334,6 +334,21 @@ describe("chat 工具域引擎路由分叉(U0:D4/D5/D10)", () => {
334
334
  // 2. pi 缺省守护(D5 字节级)
335
335
  // ============================================================
336
336
 
337
+ it("[F6] pi 路由 kickOffChatRound:runCtx 带 sessionRootId(initSession 注入的根 id,relay 归属键权威源)", async () => {
338
+ // [S5 同根] initSession 读 env PI_SUBAGENT_ROOT_SESSION_ID 优先于 init.sessionId——
339
+ // 在 pi subagent 进程内跑测试(该 env 已注入)必红。显式 stub 为 undefined(空串
340
+ // 会被条件 spread 过滤为缺键,同样破坏断言),不依赖外层环境。
341
+ vi.stubEnv("PI_SUBAGENT_ROOT_SESSION_ID", undefined);
342
+ const { service, piEngine } = setup(agentDir);
343
+ const handle = await service.execute(baseOpts(agentDir));
344
+ await vi.waitFor(() => expect(piEngine.runs.length).toBe(1));
345
+ // initSession({sessionId: "test-session"}) 无 env → sessionRootId = "test-session";
346
+ // kickOffChatRound 是 pi 引擎 background 派发主路径(isPiRoute 恒路由至此,含
347
+ // workflow 域一次性 run),漏注 = GUI pi 派发 relay 拒绝 exit 13(Gate B F6 形态)。
348
+ expect(piEngine.runs[0]!.ctx.sessionRootId).toBe("test-session");
349
+ vi.unstubAllEnvs();
350
+ });
351
+
337
352
  it("[D5] 全缺省 pi record:engine===undefined 且 entry JSON 不含 engine 键", async () => {
338
353
  const { service, piEngine } = setup(agentDir);
339
354
  const handle = await service.execute(baseOpts(agentDir));
@@ -367,7 +382,7 @@ describe("chat 工具域引擎路由分叉(U0:D4/D5/D10)", () => {
367
382
  .execute(baseOpts(agentDir, { engine: "zcode", conversation: true }))
368
383
  .catch((e: unknown) => e);
369
384
  expect(err).toBeInstanceOf(Error);
370
- expect((err as Error).message).toContain("不支持 conversation");
385
+ expect((err as Error).message).toContain("不支持 resume 续聊");
371
386
  // 恢复指引(EngineError.recovery):W3 协议化口径——调参数 / 修 manifest / 升级
372
387
  // 引擎包(引擎包是能力声明载体;「改用 engine: pi」内置兜底指引已随协议化删除)
373
388
  expect((err as { recovery?: string }).recovery).toContain("去掉 conversation 参数");
@@ -519,6 +534,47 @@ describe("chat 工具域引擎路由分叉(U0:D4/D5/D10)", () => {
519
534
  // 按句移除断言(W10 注):inproc 双模不回灌 childStateChanged,镜像移除由
520
535
  // protocol-blackbox 承载(同 subprocess-agent-runner-routing D10 注)。
521
536
  });
537
+
538
+ // ============================================================
539
+ // 6. [F6] sessionRootId 注入(execute / executeAndAwait 两 runCtx 构造点)
540
+ // ============================================================
541
+
542
+ it("[F6] service.sessionRootId 注入 runCtx(根 session id 贯穿引擎派发——relay 归属键 SESSION_ID 权威源)", async () => {
543
+ // 根进程形态:env 无 PI_SUBAGENT_ROOT_SESSION_ID 时 sessionRootId = init.sessionId。
544
+ // 删除防外层 env 污染(该键存在时 initSession 走子进程形态,断言基准漂移)。
545
+ const prevRootEnv = process.env["PI_SUBAGENT_ROOT_SESSION_ID"];
546
+ delete process.env["PI_SUBAGENT_ROOT_SESSION_ID"];
547
+ try {
548
+ const { service, zcode, piEngine } = setup(agentDir);
549
+ zcode.runImpl = () => Promise.resolve({ handle: fakeHandle(), outcome: doneOutcome("ok") });
550
+ piEngine.runImpl = () => Promise.resolve({ handle: fakeHandle(), outcome: doneOutcome("ok") });
551
+
552
+ // chat 域 background 派发(runEngineTask 的 runCtx 构造点)
553
+ await service.execute(baseOpts(agentDir, { engine: "zcode" }));
554
+ await vi.waitFor(() => expect(zcode.runs.length).toBe(1));
555
+ expect(zcode.runs[0].ctx.sessionRootId).toBe("test-session");
556
+
557
+ // workflow 域 sync 派发(runAndFinalize 的 runCtx 构造点;其引擎解析恒走
558
+ // registry 'pi'——resolveChatEnginePort,不消费 opts.engine)。fire-and-forget:
559
+ // 本 harness 无 idle-notify 驱动面,收尾链(finalizeRoundToIdle)不收敛——
560
+ // 只断言构造点(runs 捕获在收尾之前),与既有用例的挂起 record 同等形态。
561
+ let awaitErr: unknown;
562
+ void service
563
+ .executeAndAwait(baseOpts(agentDir, { slug: "f6-await" }))
564
+ .catch((e: unknown) => {
565
+ awaitErr = e;
566
+ });
567
+ await vi.waitFor(() => {
568
+ if (awaitErr !== undefined) {
569
+ throw new Error(`executeAndAwait rejected before engine.run: ${String(awaitErr)}`);
570
+ }
571
+ expect(piEngine.runs.length).toBe(1);
572
+ });
573
+ expect(piEngine.runs[0].ctx.sessionRootId).toBe("test-session");
574
+ } finally {
575
+ if (prevRootEnv !== undefined) process.env["PI_SUBAGENT_ROOT_SESSION_ID"] = prevRootEnv;
576
+ }
577
+ }, 10_000);
522
578
  });
523
579
 
524
580
  // ============================================================
@@ -676,6 +732,66 @@ describe("chat 引擎分支 U2:probe 兜底 / journal / engineHandle", () => {
676
732
  await vi.waitFor(() => expect(service["findRecord"](handle.subagentId)).toBeUndefined());
677
733
  }, 10_000);
678
734
 
735
+ it("[onHandleReady F1] sessionId 先落 + 迟到只补 sessionFile → 按字段补缺落位;已有值不被迟到值覆盖(幂等)", async () => {
736
+ process.env.XYZ_AGENT_DATA_DIR = agentDir;
737
+ const { service, zcode, pi } = setup(agentDir);
738
+ const POOL = "zcode-appserver-home";
739
+ const LATE_SESSION_FILE = "/tmp/late/session-abc.jsonl";
740
+ let releaseRun!: (v: { handle: EngineHandle; outcome: AgentOutcome }) => void;
741
+ zcode.runImpl = (task, ctx) => {
742
+ ctx.onPoolResolved?.(POOL);
743
+ // ① create 应答:只带 sessionId(本 replay 批次新打通的可达面——close 期 LC-4
744
+ // 后缀反查在 sessionId 已知后才补发 sessionFile)。
745
+ ctx.onHandleReady?.({
746
+ sessionRef: { dbPath: ".zcode/cli/db/db.sqlite", sessionId: "sess-live-1" },
747
+ poolKey: POOL,
748
+ });
749
+ // ② 迟到 handleReady:只补 sessionFile——旧守卫「有 sessionId 即整条 return」
750
+ // 会把它整条吞掉,sessionFile 永不落位(消费点:冷续 resume 锚点 / interact 定位)。
751
+ ctx.onHandleReady?.({
752
+ sessionRef: { sessionId: "sess-live-1", sessionFile: LATE_SESSION_FILE },
753
+ poolKey: POOL,
754
+ });
755
+ // ③ 再迟到一次且带冲突值:已有值必须原样保留(幂等),poolKey 不被重置。
756
+ ctx.onHandleReady?.({
757
+ sessionRef: { sessionId: "sess-OVERWRITE", sessionFile: "/tmp/late/other.jsonl" },
758
+ poolKey: "hijacked-pool",
759
+ });
760
+ return new Promise((resolve) => {
761
+ releaseRun = resolve;
762
+ });
763
+ };
764
+ const handle = await service.execute(baseOpts(agentDir, { engine: "zcode" }));
765
+ await vi.waitFor(() => expect(zcode.runs.length).toBe(1));
766
+
767
+ const running = service["collectRecords"](10, "running").find((r) => r.id === handle.subagentId);
768
+ // 补缺落位:sessionFile 进 record.engineHandle(①级会话取证 / 冷续 resume 锚点钥匙)
769
+ expect(running?.engineHandle?.sessionRef["sessionFile"]).toBe(LATE_SESSION_FILE);
770
+ expect(running?.engineHandle).toEqual({
771
+ sessionRef: {
772
+ dbPath: ".zcode/cli/db/db.sqlite",
773
+ sessionId: "sess-live-1",
774
+ sessionFile: LATE_SESSION_FILE,
775
+ },
776
+ poolKey: POOL,
777
+ journalPath: resolveJournalPath(agentDir, "zcode", POOL, handle.subagentId),
778
+ });
779
+ // 补缺经 entry 持久化(运行中 GUI 经 entry 重建 record 即可见),且 ③ 的
780
+ // 「无新字段」重复回调不产生第二条写噪(同 sessionFile 恰好一条)。
781
+ const entries = pi.appendEntry.mock.calls.filter((c) => c[0] === "subagent-record");
782
+ const withLateSessionFile = entries.filter(
783
+ (c) =>
784
+ ((c[1] as Record<string, unknown>).engineHandle as
785
+ | { sessionRef?: Record<string, string> }
786
+ | undefined)?.sessionRef?.["sessionFile"] === LATE_SESSION_FILE,
787
+ );
788
+ expect(withLateSessionFile).toHaveLength(1);
789
+
790
+ // 终态收口(防 dangling)
791
+ releaseRun({ handle: fakeHandle(), outcome: doneOutcome("ok") });
792
+ await vi.waitFor(() => expect(service["findRecord"](handle.subagentId)).toBeUndefined());
793
+ }, 10_000);
794
+
679
795
  it("[onHandleReady] 引擎不回调(spawn 形态)时零回填——终态回填仍兜底(行为不变)", async () => {
680
796
  process.env.XYZ_AGENT_DATA_DIR = agentDir;
681
797
  const { service, zcode, pi } = setup(agentDir);
@@ -1,17 +1,21 @@
1
- // src/execution/__tests__/cold-resurrect.test.ts
1
+ // src/execution/__tests__/cold-lookup.test.ts
2
2
  //
3
3
  // [D4-③] coldLookupForAction 单元测试(依赖注入直测)。
4
4
  //
5
- // 背景:cold-resurrect.ts SubagentService 搬移后按 ColdResurrectDeps 依赖注入
6
- // 设计,此前仅有经 subagent-service 集成路径的 running 重建覆盖——closed 可重连
7
- // 候选的「恢复失败守卫 / 部分恢复回边」(assertReconnectAllowed / resurrectColdRecord
8
- // wasClosed 分支)零直测。本文件锁定这些路径的可观察行为:
5
+ // 背景:冷查链自 SubagentService 搬移后按 ColdLookupDeps 依赖注入设计,此前仅有经
6
+ // subagent-service 集成路径的 running 重建覆盖——closed 可重连候选的「恢复失败守卫 /
7
+ // 部分恢复回边」(assertReconnectAllowed / resurrectColdRecord 的 wasClosed 分支)
8
+ // 零直测。[H1 U6] 文件随 cold-resurrect.ts → cold-lookup.ts 改名(chatMode 无条件
9
+ // 置位语义迁 Continuation D4 revive 格 + D5 gate,重建只水合持久化 chatMode)。
10
+ // 本文件锁定这些路径的可观察行为:
9
11
  // - 恢复失败:worktree 绑定丢失 / 异进程活实例(closed 与 running 候选)→
10
12
  // ResurrectDeniedError 且内存无残留(register 不被调用);
11
13
  // - 部分恢复:closed 可重连记录 → resurrectClosed 回边翻回 running + 磁盘终态位
12
- // 同步翻转(.finalized 删除 + .alive 刷新当前进程)+ register/transition 上报;
13
- // - 恢复源缺失:sessionFile 缺失 / sidecar 翻转失败(目录不存在)均不阻断重生
14
- // (best-effort 语义)。
14
+ // 同步翻转(.state(L4 现行终态载体)与 legacy .finalized 删除 + .alive 刷新当前
15
+ // 进程)+ register/transition 上报——[U2a/B4] 回边三件套整体收编
16
+ // store.markResurrecteddeps 注入真实 RecordStore 实例承载);
17
+ // - [B4/D3c] 恢复源缺失(sessionFile 缺失 / marker 写失败)→ 响亮抛错中止重生主流程
18
+ // (旧「单 try 吞错续跑」形态消灭——acquire 失败 = 双写风险敞口,§3.4)。
15
19
  //
16
20
  // fixture 一律 mkdtempSync 自建自删(tmpdir),不触碰真实数据目录。
17
21
 
@@ -22,10 +26,16 @@ import * as path from "node:path";
22
26
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
23
27
 
24
28
  import { readAliveMarker, writeAliveMarker } from "../alive-store.ts";
25
- import { COLD_LOOKUP_SCAN_LIMIT, coldLookupForAction, type ColdResurrectDeps } from "../cold-resurrect.ts";
29
+ import { COLD_LOOKUP_SCAN_LIMIT, coldLookupForAction, type ColdLookupDeps } from "../cold-lookup.ts";
30
+ import { RecordStore } from "../record-store.ts";
26
31
  import type { SubagentRecord } from "../types.ts";
27
32
  import { ResurrectDeniedError } from "../types.ts";
28
33
 
34
+ /** [U1/A4] 「异进程且存活」的确定性模拟 pid:1 号进程(launchd/init)必然存在且非
35
+ * 本测试进程——kill(1, 0) 对普通用户返回 EPERM,isProcessAlive 按「存在但无权限」
36
+ * 保守判活(self-pid 排除后不能再以本进程 pid 模拟异进程实例)。 */
37
+ const FOREIGN_LIVE_PID = 1;
38
+
29
39
  /** 磁盘/索引侧候选记录(SubagentRecord 最小合法形状 + closed 可重连缺省)。 */
30
40
  function makeFound(overrides: Partial<SubagentRecord> = {}): SubagentRecord {
31
41
  return {
@@ -62,13 +72,20 @@ interface DepOverrides {
62
72
  baseline?: string;
63
73
  }
64
74
 
65
- /** 构造依赖注入桩(register / reportRecordTransition 可断言副作用)。 */
66
- function makeDeps(o: DepOverrides = {}): ColdResurrectDeps {
75
+ /** 构造依赖注入桩。markResurrected 经真实 RecordStore 承载([U2a/B4] 回边收编 store
76
+ * 原语——磁盘三件套/内存翻回/register 需真实落盘语义,stub 无法验证);register
77
+ * spy 包住 store.register 供「内存无残留」断言(markResurrected 内部 register 与
78
+ * spy 同一入口)。sessionsDir 经 beforeEach 刷新(describe 作用域变量模块级桥接)。 */
79
+ let currentSessionsDir = "";
80
+ function makeDeps(o: DepOverrides = {}): ColdLookupDeps {
81
+ const store = new RecordStore(currentSessionsDir);
82
+ const registerSpy = vi.spyOn(store, "register");
67
83
  return {
68
84
  findLightById: vi.fn(() => o.direct),
69
85
  collectRecords: vi.fn(() => o.disk ?? []),
70
- register: vi.fn(),
86
+ register: registerSpy,
71
87
  reportRecordTransition: vi.fn(),
88
+ markResurrected: (record, wasClosed) => store.markResurrected(record, wasClosed),
72
89
  getSessionRootId: vi.fn(() => o.rootId ?? "root-session"),
73
90
  getBaselineRecordId: vi.fn(() => o.baseline),
74
91
  };
@@ -78,17 +95,24 @@ describe("[D4-③] coldLookupForAction 冷查/复活链", () => {
78
95
  let dir: string;
79
96
 
80
97
  beforeEach(() => {
81
- dir = fs.mkdtempSync(path.join(os.tmpdir(), "cold-resurrect-"));
98
+ dir = fs.mkdtempSync(path.join(os.tmpdir(), "cold-lookup-"));
99
+ currentSessionsDir = dir;
82
100
  });
83
101
 
84
102
  afterEach(() => {
85
103
  fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
86
104
  });
87
105
 
88
- /** tmpdir 下建 session 文件 + 可选 .finalized sidecar,返回 sessionFile 路径。 */
89
- function writeSessionFixture(opts: { finalized?: string; aliveMarker?: string } = {}): string {
106
+ /** tmpdir 下建 session 文件 + 可选终态 sidecar(.state / legacy .finalized)+ 可选
107
+ * .alive sidecar,返回 sessionFile 路径。 */
108
+ function writeSessionFixture(
109
+ opts: { state?: string; finalized?: string; aliveMarker?: string } = {},
110
+ ): string {
90
111
  const sessionFile = path.join(dir, "20260901T000000-000_sa-cold-1.jsonl");
91
112
  fs.writeFileSync(sessionFile, "{}\n", "utf-8");
113
+ if (opts.state !== undefined) {
114
+ fs.writeFileSync(`${sessionFile}.state`, opts.state, "utf-8");
115
+ }
92
116
  if (opts.finalized !== undefined) {
93
117
  fs.writeFileSync(`${sessionFile}.finalized`, opts.finalized, "utf-8");
94
118
  }
@@ -111,8 +135,9 @@ describe("[D4-③] coldLookupForAction 冷查/复活链", () => {
111
135
  expect(record.status).toBe("running");
112
136
  expect(record.closedReason).toBeUndefined();
113
137
  expect(record.endedAt).toBeUndefined();
114
- // [v4 A-3] 跨重启恢复入口:chatMode 无条件 true
115
- expect(record.chatMode).toBe(true);
138
+ // [v4 A-3 → H1 U6 / D4-D5] 水合保留持久化 chatMode:候选未持久化 chatMode
139
+ //(undefined)→ 重建 false(升级置位归 Continuation revive 格 + gate,不在重建层)
140
+ expect(record.chatMode).toBe(false);
116
141
  // 身份/续聊字段从磁盘候选回填
117
142
  expect(record.sessionFile).toBe(sessionFile);
118
143
  expect(record.round).toBe(2);
@@ -130,6 +155,42 @@ describe("[D4-③] coldLookupForAction 冷查/复活链", () => {
130
155
  expect(vi.mocked(deps.collectRecords)).toHaveBeenCalledWith(COLD_LOOKUP_SCAN_LIMIT, "all", undefined);
131
156
  });
132
157
 
158
+ it("[L4] 磁盘 .state(现行终态载体)+ legacy .finalized 残留 → 重生回边两者同步删除(否则磁盘扫描翻回 closed)", () => {
159
+ // [review MF-8] 残留任一终态 sidecar 都会让 record-store buildRecord 终态分支
160
+ // (分支 1,.state 优先 / 旧名兼容归一)压过 .alive 活态分支——reload / 异进程 /
161
+ // session-reader 全部把 running record 报成 closed,并为跨进程二次 resurrect 开门。
162
+ const sessionFile = writeSessionFixture({
163
+ state: JSON.stringify({ status: "finalized", reason: "parent-shutdown" }),
164
+ finalized: JSON.stringify({ reason: "parent-shutdown" }),
165
+ });
166
+ const deps = makeDeps({ disk: [makeFound({ sessionFile, closedReason: "parent-shutdown" })] });
167
+
168
+ const record = coldLookupForAction(deps, "sa-cold-1", true)!;
169
+
170
+ expect(record.status).toBe("running");
171
+ expect(record.closedReason).toBeUndefined();
172
+ // 终态 sidecar 双双清除(现行 .state + legacy .finalized),磁盘扫描不再翻回 closed
173
+ expect(fs.existsSync(`${sessionFile}.state`)).toBe(false);
174
+ expect(fs.existsSync(`${sessionFile}.finalized`)).toBe(false);
175
+ expect(vi.mocked(deps.register)).toHaveBeenCalledTimes(1);
176
+ expect(vi.mocked(deps.reportRecordTransition)).toHaveBeenCalledTimes(1);
177
+ });
178
+
179
+ it("负向对照:重生被守卫拒绝(worktree 绑定丢失)→ 终态 sidecar 不被误删", () => {
180
+ // 守卫先于任何状态突变与副作用([v8.5 D] / [review MF-9]):拒绝路径不得触碰磁盘
181
+ const sessionFile = writeSessionFixture({
182
+ state: JSON.stringify({ status: "finalized", reason: "parent-shutdown" }),
183
+ finalized: JSON.stringify({ reason: "parent-shutdown" }),
184
+ });
185
+ const deps = makeDeps({ disk: [makeFound({ sessionFile, worktree: true })] });
186
+
187
+ expect(() => coldLookupForAction(deps, "sa-cold-1", true)).toThrow(ResurrectDeniedError);
188
+
189
+ expect(fs.existsSync(`${sessionFile}.state`)).toBe(true);
190
+ expect(fs.existsSync(`${sessionFile}.finalized`)).toBe(true);
191
+ expect(vi.mocked(deps.register)).not.toHaveBeenCalled();
192
+ });
193
+
133
194
  it("idToFile 索引直查返回非 running 态 → 回退磁盘全扫兜底定位(closed 候选仍可重连)", () => {
134
195
  const sessionFile = writeSessionFixture();
135
196
  const found = makeFound({ sessionFile });
@@ -143,6 +204,17 @@ describe("[D4-③] coldLookupForAction 冷查/复活链", () => {
143
204
  expect(vi.mocked(deps.register)).toHaveBeenCalledWith(record);
144
205
  });
145
206
 
207
+ it("候选持久化 chatMode=true(chat 容器)→ 水合保留 true(续聊直接走,无需升级格)", () => {
208
+ const sessionFile = writeSessionFixture();
209
+ const deps = makeDeps({ disk: [makeFound({ sessionFile, status: "running", chatMode: true })] });
210
+
211
+ const record = coldLookupForAction(deps, "sa-cold-1", true)!;
212
+
213
+ expect(record.status).toBe("running");
214
+ expect(record.chatMode).toBe(true);
215
+ expect(vi.mocked(deps.register)).toHaveBeenCalledTimes(1);
216
+ });
217
+
146
218
  it("closed disconnected(.finalized 空内容兜底死因)同样落在可重连集内", () => {
147
219
  const sessionFile = writeSessionFixture({ finalized: "" });
148
220
  const deps = makeDeps({ disk: [makeFound({ sessionFile, closedReason: "disconnected" })] });
@@ -165,9 +237,10 @@ describe("[D4-③] coldLookupForAction 冷查/复活链", () => {
165
237
  });
166
238
 
167
239
  it("closed 候选仍有异进程活实例(.alive 指向活 pid)→ 拒绝双写,register 不残留", () => {
168
- // 用本测试进程 pid 模拟「另一进程的活跃实例」——isProcessAlive 判活为真
240
+ // [U1/A4] 探针已补 self-pid 排除——「异进程」模拟不能再用本测试进程 pid,改用
241
+ // 恒活的外部 pid 1(launchd/init:kill(1,0) → EPERM → isProcessAlive 判活为真)。
169
242
  const sessionFile = writeSessionFixture();
170
- writeAliveMarker(sessionFile, { pid: process.pid, id: "sa-cold-1", startedAt: Date.now() });
243
+ writeAliveMarker(sessionFile, { pid: FOREIGN_LIVE_PID, id: "sa-cold-1", startedAt: Date.now() });
171
244
  const deps = makeDeps({ disk: [makeFound({ sessionFile })] });
172
245
 
173
246
  expect(() => coldLookupForAction(deps, "sa-cold-1", true)).toThrow(ResurrectDeniedError);
@@ -179,7 +252,7 @@ describe("[D4-③] coldLookupForAction 冷查/复活链", () => {
179
252
 
180
253
  it("running 候选仍有异进程活实例(父进程重启后旧子进程尚存窗口)→ 拒绝并给恢复指引", () => {
181
254
  const sessionFile = writeSessionFixture();
182
- writeAliveMarker(sessionFile, { pid: process.pid, id: "sa-cold-1", startedAt: Date.now() });
255
+ writeAliveMarker(sessionFile, { pid: FOREIGN_LIVE_PID, id: "sa-cold-1", startedAt: Date.now() });
183
256
  const deps = makeDeps({ disk: [makeFound({ sessionFile, status: "running" })] });
184
257
 
185
258
  expect(() => coldLookupForAction(deps, "sa-cold-1", true)).toThrow(ResurrectDeniedError);
@@ -253,31 +326,58 @@ describe("[D4-③] coldLookupForAction 冷查/复活链", () => {
253
326
  expect(vi.mocked(deps.register)).not.toHaveBeenCalled();
254
327
  });
255
328
 
256
- // ── 恢复源缺失(best-effort 不阻断重生主流程)──
329
+ // ── 恢复源缺失([B4/D3c] 响亮失败——吞错续跑形态消灭)──
257
330
 
258
- it("sessionFile 缺失的 closed 候选 → 跳过 sidecar 翻转,重生照常完成(register + transition 上报)", () => {
331
+ it("[B4] sessionFile 缺失的 closed 候选 → markResurrected 响亮抛错中止重生(无锚点无法声明写权)", () => {
259
332
  const deps = makeDeps({ disk: [makeFound({ sessionFile: undefined })] });
260
333
 
261
- const record = coldLookupForAction(deps, "sa-cold-1", true)!;
262
-
263
- expect(record.status).toBe("running");
264
- expect(record.sessionFile).toBeUndefined();
265
- expect(vi.mocked(deps.register)).toHaveBeenCalledTimes(1);
266
- expect(vi.mocked(deps.reportRecordTransition)).toHaveBeenCalledTimes(1);
334
+ // acquire 失败 = 双写风险敞口,禁止 best-effort 吞错续跑(§3.4/D3c——旧形态
335
+ // 「跳过 sidecar 翻转、重生照常完成」随回边收编消灭:无锚点的 running record
336
+ // 缺跨进程写权声明,正是 D3 要防的事故形态入口)。
337
+ expect(() => coldLookupForAction(deps, "sa-cold-1", true)).toThrow(/no sessionFile anchor/);
338
+ expect(vi.mocked(deps.register)).not.toHaveBeenCalled();
339
+ expect(vi.mocked(deps.reportRecordTransition)).not.toHaveBeenCalled();
267
340
  });
268
341
 
269
- it("sidecar 翻转失败(sessionFile 所在目录不存在,writeAliveMarker ENOENT)→ best-effort 吞错,重生不中断", () => {
342
+ it("[B4] sidecar 翻转失败(sessionFile 所在目录不存在,writeAliveMarker ENOENT)→ 响亮中止,终态位未被误删", () => {
270
343
  // 恢复源缺失形态:sessionFile 指向已消失的目录(推导路径过期 / 目录被清理)
271
344
  const sessionFile = path.join(dir, "vanished-dir", "20260901T000000-000_sa-cold-1.jsonl");
345
+ // 离线预置终态位(目录存在时才可写——改为断言 acquire-first 失败后磁盘保持旧形态
346
+ // 的推演基础:无终态位可删的目录缺失形态下,响亮失败即全部可观察行为)
272
347
  const deps = makeDeps({ disk: [makeFound({ sessionFile })] });
273
348
 
349
+ // acquire-first 写权声明失败 → 整体响亮抛错(单 try 域原子收敛,D3c)。
350
+ expect(() => coldLookupForAction(deps, "sa-cold-1", true)).toThrow(
351
+ /write-lease acquire\/terminal-position flip failed/,
352
+ );
353
+ expect(vi.mocked(deps.register)).not.toHaveBeenCalled();
354
+ expect(vi.mocked(deps.reportRecordTransition)).not.toHaveBeenCalled();
355
+ // marker 写入确实失败(目录不存在,未落盘)
356
+ expect(fs.existsSync(`${sessionFile}.alive`)).toBe(false);
357
+ });
358
+
359
+ it("[B4/D3c] acquire-first 失败后磁盘保持旧形态(终态位未删,重生可重试)", () => {
360
+ // acquire 成功但终态位删除失败不可构造(rmSync force 幂等)——验证 acquire-first
361
+ // 顺序的可观察等价面:目录存在 + 终态位在,marker 写成功路径下才执行删除。
362
+ // 本用例钉失败方向的可重试性:closed 候选 + 守卫通过 + 目录消失 → 响亮中止后
363
+ // .state 残留形态不变(无「半翻」状态)。
364
+ const sessionFile = path.join(dir, "gone", "20260901T000000-000_sa-cold-1.jsonl");
365
+ const deps = makeDeps({ disk: [makeFound({ sessionFile })] });
366
+ expect(() => coldLookupForAction(deps, "sa-cold-1", true)).toThrow();
367
+ // 磁盘面零触碰(目录本不存在——构造性「保持旧形态」)
368
+ expect(fs.existsSync(sessionFile)).toBe(false);
369
+ });
370
+
371
+ it("[B4/D3c] running 候选接管(wasClosed=false)→ 仍 acquire marker(接管即声明,B/C 双写窗闭合)", () => {
372
+ const sessionFile = writeSessionFixture();
373
+ const deps = makeDeps({ disk: [makeFound({ sessionFile, status: "running" })] });
374
+
274
375
  const record = coldLookupForAction(deps, "sa-cold-1", true)!;
275
376
 
276
- // best-effort:sidecar 翻转失败不阻断重生主流程(状态回边与注册照常)
277
377
  expect(record.status).toBe("running");
378
+ // 「接管即声明」:running 接管形态无终态位可删,但 acquire 照做——A 崩溃(marker
379
+ // pid 死)→ B 接管刷新 pid=B → C 再触达被拦(S8⑤ 验收锚点的磁盘面前置)。
380
+ expect(readAliveMarker(sessionFile)).toMatchObject({ pid: process.pid, id: "sa-cold-1" });
278
381
  expect(vi.mocked(deps.register)).toHaveBeenCalledTimes(1);
279
- expect(vi.mocked(deps.reportRecordTransition)).toHaveBeenCalledTimes(1);
280
- // marker 写入确实失败(目录不存在,未落盘)
281
- expect(fs.existsSync(`${sessionFile}.alive`)).toBe(false);
282
382
  });
283
383
  });
@@ -159,7 +159,7 @@ describe("collectCoordinator service integration (U2)", () => {
159
159
  expect(batches[0]!.map((m) => m.id)).toEqual([handle.subagentId]);
160
160
  });
161
161
 
162
- it("flushBatch accepted=false(幂等拒绝)时不落标:零带 batchFinalized 的 entry", async () => {
162
+ it("flushBatch accepted=false(幂等拒绝)时落标已前置完成(U3 归口:合一原语幂等覆写)", async () => {
163
163
  const spy = spyNotifier(service);
164
164
  spy.notifyBatch.mockReturnValue(false); // 模拟同成员集批已在账(E1 重建重发形态)
165
165
  const handle = await service.execute({ task: "collect me", slug: "collect-me", collect: "sync" });
@@ -167,14 +167,16 @@ describe("collectCoordinator service integration (U2)", () => {
167
167
  await settleLast();
168
168
  await until(() => spy.notifyBatch.mock.calls.length > 0);
169
169
  expect(handle.subagentId).toMatch(/^sa-/);
170
- // 落标出口①绑「写账成功」:accepted=false 无带 batchFinalized 标记的 subagent-record
171
- // entry(register/archive 常规 entry recordToSubagent 投影携带 batchFinalized=
172
- // undefined——未离场成员,非 true;U5 投影扩展后缺省键不落值,断言语义不变)
170
+ // [U3 批写归口] 落标随 markBatchFinalized 前置到写账前(合一原语形态下「屏障
171
+ // 写账 落标」三段不可兼得,构造性 barrier 优先):accepted=false(同成员集批
172
+ // 已在账 = 该批此前成功投递时已落标)时本次落标属幂等覆写(appendEntry
173
+ // last-writer-wins),E1 排除判据(collectMode=sync + batchFinalized)不受写账
174
+ // 拒绝影响——防该成员以无标记形态被 E1 重收(异成员集 hash 不触发账本幂等)。
173
175
  const recordEntries = pi.appendEntry.mock.calls
174
176
  .filter((c) => c[0] === "subagent-record")
175
177
  .map((c) => c[1] as Record<string, unknown>);
176
178
  expect(recordEntries.length).toBeGreaterThan(0); // execute 链自身的 register/archive 在场
177
- expect(recordEntries.some((d) => d["batchFinalized"] === true)).toBe(false);
179
+ expect(recordEntries.some((d) => d["id"] === handle.subagentId && d["batchFinalized"] === true)).toBe(true);
178
180
  });
179
181
 
180
182
  it("routes an async member through the same notifyAsync path (现状直通)", async () => {