@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.
- package/dist/{chunk-VURUAGMM.js → chunk-LZJOEDGQ.js} +603 -154
- package/dist/engine-discovery-scan-Da4Dkzsi.d.cts +818 -0
- package/dist/engine-discovery-scan-Da4Dkzsi.d.ts +818 -0
- package/dist/execution/engine/engine-discovery-scan.cjs +325 -154
- package/dist/execution/engine/engine-discovery-scan.d.cts +1 -2
- package/dist/execution/engine/engine-discovery-scan.d.ts +1 -2
- package/dist/execution/engine/engine-discovery-scan.js +1 -1
- package/dist/execution/relay-env.js +5 -4
- package/dist/index.cjs +9799 -7954
- package/dist/index.d.cts +1896 -1360
- package/dist/index.d.ts +1896 -1360
- package/dist/index.js +8952 -7513
- package/dist.bundle/index.cjs +7897 -6054
- package/package.json +5 -5
- package/src/__tests__/manifest-store.test.ts +16 -15
- package/src/__tests__/record-store-last-line.test.ts +12 -0
- package/src/__tests__/robustness-medium-batch1.test.ts +10 -14
- package/src/__tests__/robustness-medium-batch2.test.ts +7 -2
- package/src/core/logger.ts +1 -1
- package/src/execution/__tests__/alive-store.test.ts +117 -1
- package/src/execution/__tests__/batch-finalized.test.ts +206 -0
- package/src/execution/__tests__/chat-engine-routing.test.ts +117 -1
- package/src/execution/__tests__/{cold-resurrect.test.ts → cold-lookup.test.ts} +133 -33
- package/src/execution/__tests__/collect-coordinator-service.test.ts +7 -5
- package/src/execution/__tests__/conversation-continuation.test.ts +1452 -0
- package/src/execution/__tests__/delivery-methods.test.ts +90 -176
- package/src/execution/__tests__/dialog-queue.test.ts +70 -3
- package/src/execution/__tests__/dispose-manifest-recovery.test.ts +475 -0
- package/src/execution/__tests__/execute-and-await-worktree.test.ts +3 -3
- package/src/execution/__tests__/execute-nesting.test.ts +8 -5
- package/src/execution/__tests__/execution-record.test.ts +8 -148
- package/src/execution/__tests__/finalize-record.test.ts +295 -129
- package/src/execution/__tests__/get-record-for-action-restart.test.ts +21 -29
- package/src/execution/__tests__/helpers/fake-engine-port.ts +13 -68
- package/src/execution/__tests__/helpers/legacy-sidecar.ts +33 -0
- package/src/execution/__tests__/helpers/subagent-service-mocks.ts +9 -5
- package/src/execution/__tests__/idle-gc.test.ts +60 -0
- package/src/execution/__tests__/inflight-production-wiring.test.ts +223 -0
- package/src/execution/__tests__/lifecycle-manager.test.ts +3 -254
- package/src/execution/__tests__/manifest-store-tmp-recovery.test.ts +43 -22
- package/src/execution/__tests__/model-config-service.test.ts +111 -0
- package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +10 -6
- package/src/execution/__tests__/nested-visibility.test.ts +13 -1
- package/src/execution/__tests__/notify-ledger.test.ts +42 -1
- package/src/execution/__tests__/rebuild-indexes.test.ts +329 -0
- package/src/execution/__tests__/record-binding.test.ts +613 -0
- package/src/execution/__tests__/record-origin.test.ts +318 -0
- package/src/execution/__tests__/record-store-intent-api.test.ts +600 -0
- package/src/execution/__tests__/record-store-orphan-revive.test.ts +71 -3
- package/src/execution/__tests__/record-store.test.ts +72 -54
- package/src/execution/__tests__/recursive-visibility-baseline.test.ts +8 -5
- package/src/execution/__tests__/round-supervisor-workflow-origin.test.ts +205 -0
- package/src/execution/__tests__/run-orchestration-write-lease.test.ts +293 -0
- package/src/execution/__tests__/run-orchestration.test.ts +199 -0
- package/src/execution/__tests__/session-file-gc.test.ts +35 -4
- package/src/execution/__tests__/session-reconstructor.test.ts +78 -0
- package/src/execution/__tests__/state-marker.test.ts +288 -0
- package/src/execution/__tests__/stream-sink-retirement.test.ts +8 -5
- package/src/execution/__tests__/subagent-actions-core.test.ts +67 -16
- package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +0 -1
- package/src/execution/__tests__/subagent-service-notify-gate.test.ts +27 -11
- package/src/execution/__tests__/subagent-service-parent-guard.test.ts +17 -11
- package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +130 -53
- package/src/execution/__tests__/subagent-service.test.ts +17 -215
- package/src/execution/__tests__/subprocess-agent-runner-no-progress-killall.test.ts +138 -0
- package/src/execution/__tests__/subprocess-agent-runner.test.ts +105 -660
- package/src/execution/__tests__/sync-collect-recovery.test.ts +23 -16
- package/src/execution/__tests__/terminal-write-latency.test.ts +79 -0
- package/src/execution/__tests__/workflow-agent-dispatch.test.ts +782 -0
- package/src/execution/alive-store.ts +37 -28
- package/src/execution/{cold-resurrect.ts → cold-lookup.ts} +48 -45
- package/src/execution/config.ts +1 -1
- package/src/execution/conversation-continuation.ts +541 -0
- package/src/execution/dialog-queue.ts +60 -12
- package/src/execution/engine/__tests__/common/capability-gate.test.ts +1 -1
- package/src/execution/engine/__tests__/common/journal-wiring.test.ts +16 -1
- package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/fake-engine-protocol.mjs +17 -51
- package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/smoke-run.fixture.json +0 -5
- package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +1 -1
- package/src/execution/engine/__tests__/conformance/protocol-blackbox.test.ts +5 -9
- package/src/execution/engine/__tests__/conformance/registry-fork-filter.test.ts +55 -53
- package/src/execution/engine/__tests__/engine-discovery-scan.test.ts +0 -3
- package/src/execution/engine/__tests__/engine-discovery.test.ts +0 -3
- package/src/execution/engine/__tests__/inflight-snapshot.test.ts +180 -0
- package/src/execution/engine/__tests__/model-prompt.test.ts +0 -3
- package/src/execution/engine/__tests__/registry.test.ts +0 -1
- package/src/execution/engine/__tests__/routing.test.ts +0 -1
- package/src/execution/engine/__tests__/run-failure-worktree-cleanup.test.ts +0 -3
- package/src/execution/engine/client/__tests__/__fixtures__/fake-engine.mjs +0 -3
- package/src/execution/engine/client/__tests__/engine-client-reap.test.ts +212 -0
- package/src/execution/engine/client/__tests__/mirror.test.ts +73 -1
- package/src/execution/engine/client/__tests__/protocol-closure.test.ts +2 -9
- package/src/execution/engine/client/__tests__/remote-engine.test.ts +46 -12
- package/src/execution/engine/client/engine-client.ts +111 -26
- package/src/execution/engine/client/mirror.ts +9 -0
- package/src/execution/engine/client/remote-engine.ts +52 -34
- package/src/execution/engine/client/reverse-router.ts +6 -27
- package/src/execution/engine/common/__tests__/run-signals.test.ts +79 -0
- package/src/execution/engine/common/capability-gate.ts +27 -0
- package/src/execution/engine/common/data-dir.ts +3 -2
- package/src/execution/engine/common/errors.ts +1 -1
- package/src/execution/engine/common/journal-wiring.ts +18 -9
- package/src/execution/engine/common/run-signals.ts +83 -0
- package/src/execution/engine/d8-compat.ts +1 -1
- package/src/execution/engine/host/__tests__/host-bridge.test.ts +3 -18
- package/src/execution/engine/host/host-bridge.ts +26 -41
- package/src/execution/engine/host/pi-host-binding.ts +5 -5
- package/src/execution/engine/host/spawned-children.ts +1 -1
- package/src/execution/engine/inflight-snapshot.ts +92 -0
- package/src/execution/engine/port.ts +52 -54
- package/src/execution/engine/types.ts +11 -14
- package/src/execution/execution-record.ts +15 -47
- package/src/execution/finalize-record.ts +94 -197
- package/src/execution/idle-gc.ts +9 -3
- package/src/execution/lifecycle-manager.ts +25 -361
- package/src/execution/lifecycle-predicates.ts +22 -19
- package/src/execution/manifest-store.ts +40 -42
- package/src/execution/model-config-service.ts +1 -1
- package/src/execution/notifier.ts +39 -1
- package/src/execution/notify-host.ts +13 -4
- package/src/execution/path-encoding.ts +6 -0
- package/src/execution/record-entry.ts +20 -2
- package/src/execution/record-store.ts +925 -160
- package/src/execution/relay-env.ts +8 -3
- package/src/execution/round-supervisor/domain.ts +17 -5
- package/src/execution/round-supervisor/service-binding.test.ts +89 -53
- package/src/execution/round-supervisor/service-binding.ts +71 -21
- package/src/execution/round-supervisor/supervisor.ts +23 -9
- package/src/execution/service/record-access.ts +547 -0
- package/src/execution/service/record-lifecycle.ts +542 -0
- package/src/execution/service/run-orchestration.ts +1647 -0
- package/src/execution/service/service-bootstrap.ts +111 -0
- package/src/execution/service/service-constants.ts +27 -0
- package/src/execution/service/session-baselines.ts +401 -0
- package/src/execution/service/sync-collect-domain.ts +442 -0
- package/src/execution/service/workflow-dispatch.ts +567 -0
- package/src/execution/session-file-gc.ts +11 -8
- package/src/execution/session-reconstructor.ts +44 -1
- package/src/execution/sessions-index.ts +20 -3
- package/src/execution/settled-watchdog.ts +86 -43
- package/src/execution/state-marker.ts +516 -0
- package/src/execution/stream-sink.ts +1 -1
- package/src/execution/subagent-actions-core.ts +53 -31
- package/src/execution/subagent-service.ts +643 -2955
- package/src/execution/subprocess-agent-runner.ts +46 -323
- package/src/execution/types.ts +60 -43
- package/src/execution/ui-request-handler-factory.ts +5 -0
- package/src/execution/worktree-manager.ts +1 -1
- package/src/execution/worktree-registry.ts +12 -8
- package/src/index.ts +16 -5
- package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +2 -3
- package/src/orchestration/__tests__/agent-call-stream.test.ts +24 -49
- package/src/orchestration/__tests__/error-recovery-terminal-hardening.test.ts +4 -4
- package/src/orchestration/__tests__/file-run-store.test.ts +5 -7
- package/src/orchestration/__tests__/lifecycle-abort-broadcast-signal.test.ts +2 -3
- package/src/orchestration/__tests__/run-snapshot.test.ts +27 -43
- package/src/orchestration/__tests__/worker-message-pump-workflow-dispatch.test.ts +274 -0
- package/src/orchestration/execute-agent-call.ts +1 -1
- package/src/orchestration/file-run-store.ts +1 -1
- package/src/orchestration/models/ports.ts +20 -13
- package/src/orchestration/models/types.ts +11 -13
- package/src/orchestration/run-snapshot.ts +18 -22
- package/src/orchestration/worker-message-pump.ts +33 -57
- package/src/shared/atomic-write.ts +10 -8
- package/dist/chunk-T2SYBW3I.js +0 -24
- package/dist/engine-discovery-scan-ocpM8FmI.d.cts +0 -1608
- package/dist/engine-discovery-scan-ocpM8FmI.d.ts +0 -1608
- package/src/execution/__tests__/chat-round-first-round-watchdog.test.ts +0 -255
- package/src/execution/__tests__/finalized-marker.test.ts +0 -104
- package/src/execution/__tests__/lifecycle-manager-lock.test.ts +0 -211
- package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +0 -488
- package/src/execution/__tests__/subprocess-agent-runner-timeout.test.ts +0 -75
- package/src/execution/__tests__/tombstone-store.test.ts +0 -73
- package/src/execution/engine/__tests__/conformance/chat-round-protocol.test.ts +0 -230
- package/src/execution/finalized-marker.ts +0 -70
- 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("不支持
|
|
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-
|
|
1
|
+
// src/execution/__tests__/cold-lookup.test.ts
|
|
2
2
|
//
|
|
3
3
|
// [D4-③] coldLookupForAction 单元测试(依赖注入直测)。
|
|
4
4
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
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
|
|
13
|
-
//
|
|
14
|
-
// (
|
|
14
|
+
// 同步翻转(.state(L4 现行终态载体)与 legacy .finalized 删除 + .alive 刷新当前
|
|
15
|
+
// 进程)+ register/transition 上报——[U2a/B4] 回边三件套整体收编
|
|
16
|
+
// store.markResurrected(deps 注入真实 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
|
|
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
|
-
/**
|
|
66
|
-
|
|
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:
|
|
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-
|
|
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 文件 +
|
|
89
|
-
|
|
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]
|
|
115
|
-
|
|
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
|
-
//
|
|
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:
|
|
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:
|
|
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
|
-
// ── 恢复源缺失(
|
|
329
|
+
// ── 恢复源缺失([B4/D3c] 响亮失败——吞错续跑形态消灭)──
|
|
257
330
|
|
|
258
|
-
it("sessionFile 缺失的 closed 候选 →
|
|
331
|
+
it("[B4] sessionFile 缺失的 closed 候选 → markResurrected 响亮抛错中止重生(无锚点无法声明写权)", () => {
|
|
259
332
|
const deps = makeDeps({ disk: [makeFound({ sessionFile: undefined })] });
|
|
260
333
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
expect(
|
|
265
|
-
expect(vi.mocked(deps.register)).
|
|
266
|
-
expect(vi.mocked(deps.reportRecordTransition)).
|
|
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)→
|
|
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
|
|
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
|
-
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
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(
|
|
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 () => {
|