@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.
- package/dist/{chunk-OL5BK4VN.js → chunk-LZJOEDGQ.js} +455 -203
- 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 +216 -155
- 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 +7686 -5894
- package/dist/index.d.cts +1876 -1360
- package/dist/index.d.ts +1876 -1360
- package/dist/index.js +7048 -5493
- package/dist.bundle/index.cjs +7747 -5957
- package/package.json +4 -4
- package/src/__tests__/manifest-store.test.ts +16 -15
- package/src/__tests__/robustness-medium-batch1.test.ts +10 -14
- package/src/__tests__/robustness-medium-batch2.test.ts +7 -2
- 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__/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__/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__/engine-discovery-scan.test.ts +0 -3
- package/src/execution/engine/__tests__/engine-discovery.test.ts +0 -3
- 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 +81 -25
- 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 +19 -41
- package/src/execution/engine/host/pi-host-binding.ts +5 -5
- package/src/execution/engine/port.ts +34 -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/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 -2980
- 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 +1 -1
- package/src/index.ts +6 -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-BXLA8y-z.d.cts +0 -1624
- package/dist/engine-discovery-scan-BXLA8y-z.d.ts +0 -1624
- 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
|
@@ -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 保守分支)无 .
|
|
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}.
|
|
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}.
|
|
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 {
|
|
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 + .
|
|
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
|
-
|
|
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("有 .
|
|
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
|
-
|
|
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(.
|
|
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
|
-
|
|
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:
|
|
409
|
-
it(".
|
|
406
|
+
// ── 分支 1: 终态 sidecar status=cancelled ──
|
|
407
|
+
it(".state sidecar(cancelled) → closed + closedReason=cancelled", () => {
|
|
410
408
|
const sessionFile = writeBaseSession();
|
|
411
|
-
|
|
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:
|
|
424
|
-
it(".
|
|
419
|
+
// ── 分支 2: 终态 sidecar status=finalized(done)──
|
|
420
|
+
it(".state sidecar(finalized) + stopReason=stop → done", () => {
|
|
425
421
|
const sessionFile = writeBaseSession();
|
|
426
|
-
|
|
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:
|
|
433
|
-
it(".
|
|
428
|
+
// ── 分支 2: 终态 sidecar status=finalized(failed)──
|
|
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
|
-
|
|
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
|
|
465
|
-
it(".alive + 存活 pid → running
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
496
|
-
it("
|
|
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
|
|
510
|
+
expect(found).not.toHaveProperty("externalInstance");
|
|
513
511
|
});
|
|
514
512
|
|
|
515
|
-
// ──
|
|
516
|
-
it(".cancelled
|
|
513
|
+
// ── 兼容读回归:存量旧名共存时 .cancelled 优先于 .finalized ──
|
|
514
|
+
it("兼容读:旧 .cancelled 优先于旧 .finalized(存量共存形态,优先级对齐合并前)", () => {
|
|
517
515
|
const sessionFile = writeBaseSession();
|
|
518
|
-
|
|
519
|
-
|
|
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
|
|
522
|
+
// v4 B-1:cancelled 优先级不变,但 status 折入 closed(closedReason='cancelled')
|
|
525
523
|
expect(found?.status).toBe("closed");
|
|
526
524
|
expect(found?.closedReason).toBe("cancelled");
|
|
527
525
|
});
|
|
528
526
|
|
|
529
|
-
// ──
|
|
530
|
-
it("
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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: 有 .
|
|
772
|
-
it("TC-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
|
-
|
|
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: .
|
|
788
|
-
it("TC-2b: .
|
|
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
|
-
|
|
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}.
|
|
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}.
|
|
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}.
|
|
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}.
|
|
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}.
|
|
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
|
-
|
|
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 /
|
|
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
|
-
|
|
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("../
|
|
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
|
-
|
|
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
|
-
|
|
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();
|