@zhushanwen/subagent-core 0.5.1 → 0.6.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/execution/engine/engines/zcode/reader.d.cts +1 -1
- package/dist/execution/engine/engines/zcode/reader.d.ts +1 -1
- package/dist/index.cjs +1122 -452
- package/dist/index.d.cts +188 -5
- package/dist/index.d.ts +188 -5
- package/dist/index.js +1133 -463
- package/dist/{types-Dv4QhSJ_.d.cts → types-C3fE3R9x.d.cts} +65 -0
- package/dist/{types-Dv4QhSJ_.d.ts → types-C3fE3R9x.d.ts} +65 -0
- package/dist.bundle/index.cjs +34933 -0
- package/package.json +2 -2
- package/src/execution/__tests__/agent-registry.test.ts +1 -1
- package/src/execution/__tests__/alive-store.test.ts +1 -1
- package/src/execution/__tests__/collect-budget.test.ts +291 -0
- package/src/execution/__tests__/collect-coordinator-service.test.ts +327 -0
- package/src/execution/__tests__/collect-coordinator.test.ts +371 -0
- package/src/execution/__tests__/collect-mixed-dispatch.test.ts +262 -0
- package/src/execution/__tests__/config-collect-sync.test.ts +120 -0
- package/src/execution/__tests__/config.test.ts +1 -1
- package/src/execution/__tests__/explicit-agent-ref-guard.test.ts +1 -1
- package/src/execution/__tests__/finalized-marker.test.ts +1 -1
- package/src/execution/__tests__/gc-timer.test.ts +1 -1
- package/src/execution/__tests__/manifest-store-tmp-recovery.test.ts +1 -1
- package/src/execution/__tests__/notify-batch.test.ts +500 -0
- package/src/execution/__tests__/notify-ledger.test.ts +27 -0
- package/src/execution/__tests__/pi-invocation.test.ts +34 -2
- package/src/execution/__tests__/record-entry-collect.test.ts +102 -0
- package/src/execution/__tests__/relay-env.test.ts +12 -1
- package/src/execution/__tests__/session-file-gc.test.ts +1 -1
- package/src/execution/__tests__/spawn-event-adapter.test.ts +2 -2
- package/src/execution/__tests__/start-collect-guard.test.ts +255 -0
- package/src/execution/__tests__/start-sync-model-guard.test.ts +1 -1
- package/src/execution/__tests__/subagent-actions-core.test.ts +6 -0
- package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +13 -0
- package/src/execution/__tests__/sync-collect-recovery.test.ts +1117 -0
- package/src/execution/__tests__/tombstone-store.test.ts +1 -1
- package/src/execution/__tests__/worktree-git-ops.test.ts +1 -1
- package/src/execution/__tests__/worktree-reconcile-aging.test.ts +2 -2
- package/src/execution/__tests__/worktree-reconcile.integration.test.ts +1 -1
- package/src/execution/__tests__/worktree-registry.test.ts +1 -1
- package/src/execution/collect-coordinator.ts +200 -0
- package/src/execution/config.ts +77 -5
- package/src/execution/engine/__tests__/common/event-journal.test.ts +1 -1
- package/src/execution/engine/__tests__/common/pool-manager.test.ts +2 -2
- package/src/execution/engine/__tests__/conformance/contract.read-degradation.test.ts +1 -1
- package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +1 -1
- package/src/execution/engine/engines/zcode/__tests__/appserver-launcher.test.ts +1 -1
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-degrade.test.ts +1 -1
- package/src/execution/execution-record.ts +7 -0
- package/src/execution/finalize-record.ts +15 -11
- package/src/execution/notifier.ts +234 -25
- package/src/execution/notify-host.ts +21 -1
- package/src/execution/record-entry.ts +16 -0
- package/src/execution/record-store.ts +161 -30
- package/src/execution/subagent-actions-core.ts +62 -6
- package/src/execution/subagent-service.ts +416 -7
- package/src/execution/sync-rebuild.ts +87 -0
- package/src/execution/types.ts +63 -0
- package/src/index.ts +1 -1
- package/src/orchestration/__tests__/config-loader.test.ts +1 -1
- package/src/orchestration/__tests__/file-run-store-prune.test.ts +1 -1
- package/src/orchestration/__tests__/file-run-store-throttle.test.ts +1 -1
- package/src/orchestration/__tests__/review-fix-loop-scriptpath-failfast.test.ts +1 -1
- package/src/orchestration/__tests__/script-generate.test.ts +5 -5
- package/src/orchestration/__tests__/skill-discovery.test.ts +2 -2
- package/src/orchestration/__tests__/workflow-files.test.ts +10 -10
- package/src/orchestration/__tests__/workflow-script-registry-impl.test.ts +1 -1
- package/src/shared/__tests__/atomic-write.test.ts +1 -1
- package/src/shared/__tests__/resource-discovery-host-roots.test.ts +5 -5
|
@@ -20,7 +20,7 @@ describe("tombstone-store", () => {
|
|
|
20
20
|
sessionFile = path.join(tmpDir, "2026-01-01_uuid.jsonl");
|
|
21
21
|
});
|
|
22
22
|
afterEach(() => {
|
|
23
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
23
|
+
fs.rmSync(tmpDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
24
24
|
});
|
|
25
25
|
|
|
26
26
|
describe("write → read 往返", () => {
|
|
@@ -84,7 +84,7 @@ describe("worktree-git-ops", { timeout: 30_000 }, () => {
|
|
|
84
84
|
|
|
85
85
|
afterEach(() => {
|
|
86
86
|
resetCoreForTests();
|
|
87
|
-
fs.rmSync(outerDir, { recursive: true, force: true });
|
|
87
|
+
fs.rmSync(outerDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
88
88
|
});
|
|
89
89
|
|
|
90
90
|
/**
|
|
@@ -76,7 +76,7 @@ describe("WorktreeManager 对账歧义跳过老化(PS-12 措施⑤)", { time
|
|
|
76
76
|
|
|
77
77
|
afterEach(() => {
|
|
78
78
|
process.env.TMPDIR = ORIG_TMPDIR;
|
|
79
|
-
fs.rmSync(outerDir, { recursive: true, force: true });
|
|
79
|
+
fs.rmSync(outerDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
80
80
|
});
|
|
81
81
|
|
|
82
82
|
/** 建一个物理 worktree(绕过 WorktreeManager.create 的脏树校验,路径布局同 create)。 */
|
|
@@ -174,7 +174,7 @@ describe("WorktreeManager 对账歧义跳过老化(PS-12 措施⑤)", { time
|
|
|
174
174
|
|
|
175
175
|
// 出现对应:s-r2 pid 死 + c2 删除 → 1 活 pid ↔ 1 残留 → 自愈补写 c1
|
|
176
176
|
await waitPidDead(sleeper.pid as number);
|
|
177
|
-
fs.rmSync(c2, { recursive: true, force: true });
|
|
177
|
+
fs.rmSync(c2, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
178
178
|
await mgr.scan();
|
|
179
179
|
expect(registry.load().map((e) => e.branch)).toEqual(["pi-sub-r1"]);
|
|
180
180
|
expect(escalationCalls()).toEqual([]);
|
|
@@ -66,7 +66,7 @@ describe("WorktreeManager.scan 物理面对账(D5b)", { timeout: 30_000 }, (
|
|
|
66
66
|
|
|
67
67
|
afterEach(() => {
|
|
68
68
|
process.env.TMPDIR = ORIG_TMPDIR;
|
|
69
|
-
fs.rmSync(outerDir, { recursive: true, force: true });
|
|
69
|
+
fs.rmSync(outerDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
70
70
|
});
|
|
71
71
|
|
|
72
72
|
/** 建一个物理 worktree(按 create() 的真实路径布局,绕过 WorktreeManager.create 的脏树校验)。 */
|
|
@@ -41,7 +41,7 @@ describe("WorktreeRegistry", () => {
|
|
|
41
41
|
});
|
|
42
42
|
|
|
43
43
|
afterEach(() => {
|
|
44
|
-
fs.rmSync(tmpAgentDir, { recursive: true, force: true });
|
|
44
|
+
fs.rmSync(tmpAgentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
describe("add + load", () => {
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// src/execution/collect-coordinator.ts
|
|
2
|
+
//
|
|
3
|
+
// collectCoordinator:notifyComplete 唯一路由入口——async 直通 / sync 批缓冲 / 闭合检测
|
|
4
|
+
// (subagent-sync-collect 设计 §3.1.4 终态数据流 / D2 隐式批 / D5 service 层缓冲)。
|
|
5
|
+
//
|
|
6
|
+
// 职责(U2):
|
|
7
|
+
// route(record):
|
|
8
|
+
// - record.collectMode !== "sync" → deps.notifyAsync(现状 notifier.notify 路径,
|
|
9
|
+
// 执行序列逐字节一致,G3/A5 零回归);
|
|
10
|
+
// - record.collectMode === "sync" → deps.toNotifyRecord 终态快照登记入缓冲(内存,
|
|
11
|
+
// 登记时定格——record 对象后续可变不影响快照)→ 闭合检测。
|
|
12
|
+
//
|
|
13
|
+
// 闭合判定(⛔3 已核实 U1,impl-plan §7):缓冲非空 && 无非终态 sync 成员。
|
|
14
|
+
// 「非终态」口径 = collectMode="sync" && 无 batchFinalized 标记 && status !== "closed"
|
|
15
|
+
// ——池排队成员在 store.register 时即 status="running",自动计入非终态(闭合等待它);
|
|
16
|
+
// batchFinalized=true 的已离场成员不阻止闭合。
|
|
17
|
+
//
|
|
18
|
+
// 跨轮续累(D2 隐式批):缓冲跨多次 route 累积不清空(分两轮派 2+1 sync → 闭合时
|
|
19
|
+
// 单批三成员);flush 后缓冲清空,后续 sync 成员自然开新批(语义 = "等所有 sync 待收")。
|
|
20
|
+
//
|
|
21
|
+
// [U8 拆批盲窗修复] 闭合满足 → 同宏任务去抖合批 flush(不再立即 flush):
|
|
22
|
+
// 盲窗机理:kickOffBackground 的 runAndFinalize(completeRecord+archive,record 从
|
|
23
|
+
// store.listAllActive 移除)先于 .then 链的 notifyComplete。同同步段背靠背终态时,
|
|
24
|
+
// 成员 A route 触发闭合检测(只扫 listAllActive 非终态)看不到「已 archive 未路由」
|
|
25
|
+
// 的成员 B → 立即 flush([A]);B route 再 flush([B]) → 拆两条单成员批(G1 弱化,
|
|
26
|
+
// 单唤醒变两次)。
|
|
27
|
+
// 修复:闭合首次满足改排程 flush(setTimeout(0),Node 1ms clamp——同一宏任务级窗口,
|
|
28
|
+
// 确定性、无可观察延迟);窗口内后续 route 落缓冲后一并 flush(背靠背成员合单批)。
|
|
29
|
+
// 触发时重验闭合条件:窗口内若有新 sync 成员注册 running(跨轮续累反转)→ 保留缓冲
|
|
30
|
+
// 不 flush,等其终态 route 重新排程(终态必经 notifyComplete,不丢、不悬挂)。
|
|
31
|
+
// 交互语义(授权修复轮决策):
|
|
32
|
+
// - E9 dispose:service.convertPendingSyncBufferToAsync 先 cancelScheduledFlush
|
|
33
|
+
// (取消而非同步 flush——放任触发会与逐条转 async 写账双通道并发 → 同成员双投递,
|
|
34
|
+
// 且触发点可能落在 notifier/store dispose 之后);取消后缓冲原样保留,E9 单通路
|
|
35
|
+
// 完整接管(选语义最简)。
|
|
36
|
+
// - E1 恢复补发:只在 session_start 编排处运行(此前 dispose 已取消挂起排程),
|
|
37
|
+
// 补发直走 notifier.notifyBatch 不经协调器;异常时序相撞由账本 sync-batch:<hash>
|
|
38
|
+
// 幂等拒绝兜底。
|
|
39
|
+
//
|
|
40
|
+
// flush 注入式(U3 已接线):deps.flushBatch 由宿主注入——U3 接 service 闭包的
|
|
41
|
+
//「manifest 屏障(await 落盘)→ notifier.notifyBatch 单条批投递(ledger sync-batch
|
|
42
|
+
// hash 写账)→ batchFinalized 落标」(见 subagent-service.ts flushBatch 闭包)。
|
|
43
|
+
// 测试注入 fake 断言闭合条件。flush 异步(Promise<void>)、协调器 void 调用不等待:
|
|
44
|
+
// 缓冲在调用前已整体移交清空,flush 内部时序(屏障 → 写账 → 落标)与失败语义归
|
|
45
|
+
// 实现方(与 notifier.notify 同风格)。
|
|
46
|
+
//
|
|
47
|
+
// 不变量:
|
|
48
|
+
// - 每个成员只登记一次:notifyComplete 调用点自带 CAS/gate 单次性(与既有 notify
|
|
49
|
+
// 单次性同源——cancelBackground CAS 抢锁 / kickOffBackground.then gate / watchdog
|
|
50
|
+
// CAS),协调器不重复去重,不改变 at-least-once 语义。
|
|
51
|
+
// - toNotifyRecord 返回 undefined(gate 未过/非终态)→ 静默跳过不入缓冲,与 async
|
|
52
|
+
// 路径的静默跳过对称——调用点 gate 语义不变,协调器不重复判定。
|
|
53
|
+
// - 闭合判定只敏感于「是否还有非终态 sync」:本条终态在 store 的可见性(archive 后
|
|
54
|
+
// 磁盘重建时序)不影响判定正确性。
|
|
55
|
+
// - [U8] 排程幂等:窗口内多条 route 至多一个挂起定时器;flush 只在触发时重验后执行,
|
|
56
|
+
// 缓冲在排程期内保持可枚举(pendingMembers 供 E9 dispose 转换读取)。
|
|
57
|
+
|
|
58
|
+
import type { BgNotifyRecord } from "./notifier.ts";
|
|
59
|
+
import type { ExecutionRecord } from "./types.ts";
|
|
60
|
+
|
|
61
|
+
/** 闭合判定的 record 最小视图(ExecutionRecord / SubagentRecord 的同构子集)。
|
|
62
|
+
*
|
|
63
|
+
* [U3 微调 / U5 已修] 原 deps 用 SubagentRecord——生产通路 collectRecords 经
|
|
64
|
+
* recordToSubagent 投影曾不含 collectMode/batchFinalized(U2 披露,U5 已补投影),
|
|
65
|
+
* 保留结构化最小接口 + service 接线走 store.listAllActive()(原始 ExecutionRecord
|
|
66
|
+
* 内存态,不经投影)——非终态 sync 成员必在内存(archive 只删终态),内存视图语义
|
|
67
|
+
* 完整;纯注入测试的 SubagentRecord stub 结构兼容零改动。 */
|
|
68
|
+
export interface CollectScanRecord {
|
|
69
|
+
status: string;
|
|
70
|
+
collectMode?: "sync";
|
|
71
|
+
batchFinalized?: boolean;
|
|
72
|
+
/** SP-5 执行态信号:true = 无活进程驱动的 running(one-shot 成功回退态)。
|
|
73
|
+
* [U3 补丁] ⛔3 非终态口径的必要补充——one-shot 成功完成后 record 不 archive,
|
|
74
|
+
* 以 running+resumable 留内存等 message 升级(真链 trace 实证):结果已定格、
|
|
75
|
+
* 进程已死 = 已完成待通知,不阻止闭合;否则 sync 批永不闭合。 */
|
|
76
|
+
resumable?: boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** 闭合判定的全 record 扫描上限(service 冷路径 COLD_LOOKUP_SCAN_LIMIT 同量级)。 */
|
|
80
|
+
export const COLLECT_SCAN_LIMIT = 1000;
|
|
81
|
+
|
|
82
|
+
/** 协调器宿主依赖(全部注入——协调器零 service/store 直依,可独立单测)。 */
|
|
83
|
+
export interface CollectCoordinatorDeps {
|
|
84
|
+
/** async 直通(现状 notifier.notify 路径,字节不变)。 */
|
|
85
|
+
notifyAsync(record: ExecutionRecord): void;
|
|
86
|
+
/** record → 通知快照(登记时定格)。undefined = gate 未过/非终态(静默跳过)。 */
|
|
87
|
+
toNotifyRecord(record: ExecutionRecord): BgNotifyRecord | undefined;
|
|
88
|
+
/** 闭合判定数据源(CollectScanRecord 最小视图,见接口注释——数据源须携带
|
|
89
|
+
* collectMode/batchFinalized 原始值,禁经 recordToSubagent 投影)。 */
|
|
90
|
+
listRecords(limit: number): CollectScanRecord[];
|
|
91
|
+
/** 批投递回调(合批窗口到期触发,缓冲整体移交后清空)。U3 接 service 闭包:
|
|
92
|
+
* manifest 屏障 → notifier.notifyBatch 写账 → batchFinalized 落标——屏障先于写账
|
|
93
|
+
* 是「通知可达 ⇒ 索引就位」的构造性保证,故异步;协调器 void 调用不等待其完成。 */
|
|
94
|
+
flushBatch(members: BgNotifyRecord[]): Promise<void>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** route 去向(测试/诊断断言用)。 */
|
|
98
|
+
export type CollectRouteResult =
|
|
99
|
+
/** 非 sync record:notifyAsync 直通(现状路径)。 */
|
|
100
|
+
| "async"
|
|
101
|
+
/** gate 未过(toNotifyRecord undefined):静默跳过,不入缓冲。 */
|
|
102
|
+
| "sync-skipped"
|
|
103
|
+
/** 入缓冲未闭合(仍有非终态 sync 成员)。 */
|
|
104
|
+
| "sync-buffered"
|
|
105
|
+
/** 闭合触发([U8] flush 已排程合批——同宏任务窗口内的后续 route 一并投递)。 */
|
|
106
|
+
| "sync-flushed";
|
|
107
|
+
|
|
108
|
+
export class CollectCoordinator {
|
|
109
|
+
/** 批缓冲(内存):已终态未 flush 的 sync 成员终态快照,跨 route 累积。
|
|
110
|
+
* [U8] 排程合批窗口期内保持可枚举(E9 dispose 转换依赖 pendingMembers 全量)。 */
|
|
111
|
+
private readonly buffer: BgNotifyRecord[] = [];
|
|
112
|
+
|
|
113
|
+
/** [U8] 挂起的合批排程定时器(undefined = 无排程)。 */
|
|
114
|
+
private flushTimer: ReturnType<typeof setTimeout> | undefined;
|
|
115
|
+
|
|
116
|
+
constructor(private readonly deps: CollectCoordinatorDeps) {}
|
|
117
|
+
|
|
118
|
+
/** notifyComplete 唯一路由入口。返回去向供测试/诊断断言。 */
|
|
119
|
+
route(record: ExecutionRecord): CollectRouteResult {
|
|
120
|
+
if (record.collectMode !== "sync") {
|
|
121
|
+
this.deps.notifyAsync(record);
|
|
122
|
+
return "async";
|
|
123
|
+
}
|
|
124
|
+
const snapshotRecord = this.deps.toNotifyRecord(record);
|
|
125
|
+
if (!snapshotRecord) return "sync-skipped";
|
|
126
|
+
this.buffer.push(snapshotRecord);
|
|
127
|
+
return this.armFlushIfClosed() ? "sync-flushed" : "sync-buffered";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** 诊断/测试:当前缓冲成员数(= 已终态未 flush 的 sync 成员数,含合批窗口内成员)。 */
|
|
131
|
+
get pendingCount(): number {
|
|
132
|
+
return this.buffer.length;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** 诊断/测试 + E9 dispose 转换数据源:缓冲成员快照(副本,非活引用)。 */
|
|
136
|
+
pendingMembers(): BgNotifyRecord[] {
|
|
137
|
+
return [...this.buffer];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** [U8·E9 交互] 取消挂起的合批排程(缓冲原样保留——dispose 转 async 通路仍读得到
|
|
141
|
+
* 全部成员)。无排程时 no-op;幂等。 */
|
|
142
|
+
cancelScheduledFlush(): void {
|
|
143
|
+
if (this.flushTimer !== undefined) {
|
|
144
|
+
clearTimeout(this.flushTimer);
|
|
145
|
+
this.flushTimer = undefined;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** 闭合判定 + 合批排程武装:缓冲非空 && 无非终态 sync 成员 → 排程 flush。
|
|
150
|
+
* 已有排程挂起时幂等(同窗口合批,不重复定时器)。(S6 改名:原名 closeDetected
|
|
151
|
+
* 读作纯检测,实带副作用——武装排程改状态,名实相符优先。) */
|
|
152
|
+
private armFlushIfClosed(): boolean {
|
|
153
|
+
if (this.buffer.length === 0) return false;
|
|
154
|
+
if (this.hasRunningSync()) return false;
|
|
155
|
+
this.scheduleFlush();
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** [U8] 同宏任务去抖合批排程:setTimeout(0)(Node 1ms clamp)——窗口跨度覆盖一个
|
|
160
|
+
* 完整事件循环轮次(当前 poll 段 I/O 回调 + 微任务链排空),足以收纳 finalize 链
|
|
161
|
+
* 间隙内的背靠背 route,同时无可观察延迟(毫秒级 < 任意 LLM/投递时标)。 */
|
|
162
|
+
private scheduleFlush(): void {
|
|
163
|
+
if (this.flushTimer !== undefined) return;
|
|
164
|
+
this.flushTimer = setTimeout(() => {
|
|
165
|
+
this.flushTimer = undefined;
|
|
166
|
+
this.flushIfClosed();
|
|
167
|
+
}, 0);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** 合批窗口到期:重验闭合条件后整体移交 flushBatch 并清空。
|
|
171
|
+
* 窗口内新 sync 成员注册 running(跨轮续累反转)→ 保留缓冲不 flush,等其终态
|
|
172
|
+
* route 重新排程(终态必经 notifyComplete,不丢、不悬挂)。
|
|
173
|
+
* void 不等待 flush 的 async 屏障序列(manifest → 写账 → 落标):缓冲已 splice
|
|
174
|
+
* 先行整体移交,屏障 await 期间窗口内新成员重新入空缓冲开新批,互不干扰。 */
|
|
175
|
+
private flushIfClosed(): void {
|
|
176
|
+
if (this.buffer.length === 0) return;
|
|
177
|
+
if (this.hasRunningSync()) return;
|
|
178
|
+
const members = this.buffer.splice(0, this.buffer.length);
|
|
179
|
+
void this.deps.flushBatch(members);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** 是否存在非终态 sync 成员(collectMode=sync && 无 batchFinalized && 非终态)。
|
|
183
|
+
* 非终态口径(⛔3 U1 已核实 + U3 resumable 补丁):status 非 closed 且非 resumable
|
|
184
|
+
* ——池排队/在跑成员在 store.register 时即 status="running",自动计入(闭合等待它);
|
|
185
|
+
* batchFinalized=true 的已离场成员不阻止闭合;running+resumable(SP-5 one-shot
|
|
186
|
+
* 成功回退态,进程已死结果已定格)视为已完成,不阻止闭合。 */
|
|
187
|
+
private hasRunningSync(): boolean {
|
|
188
|
+
for (const record of this.deps.listRecords(COLLECT_SCAN_LIMIT)) {
|
|
189
|
+
if (
|
|
190
|
+
record.collectMode === "sync" &&
|
|
191
|
+
record.batchFinalized !== true &&
|
|
192
|
+
record.resumable !== true &&
|
|
193
|
+
record.status !== "closed"
|
|
194
|
+
) {
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
}
|
package/src/execution/config.ts
CHANGED
|
@@ -9,7 +9,11 @@ import * as path from "node:path";
|
|
|
9
9
|
|
|
10
10
|
import { getLogger } from "../core/logger.ts";
|
|
11
11
|
|
|
12
|
-
import type { SubagentsGlobalConfig } from "./types.ts";
|
|
12
|
+
import type { CollectSyncConfig, SubagentsGlobalConfig } from "./types.ts";
|
|
13
|
+
|
|
14
|
+
// 同步收集配置节类型 re-export(类型权威定义在 types.ts,避免 config → types 反向依赖成环;
|
|
15
|
+
// 消费方从本模块 import 与从 types.ts import 等价,U1 foundation 契约)。
|
|
16
|
+
export type { CollectSyncConfig };
|
|
13
17
|
|
|
14
18
|
// 包内 execution 模块统一具名 logger(getLogger 缓存单例,见 notify-ledger.ts 同款)。
|
|
15
19
|
const logger = getLogger("subagents");
|
|
@@ -33,8 +37,25 @@ export const DEFAULT_CONFIG: SubagentsGlobalConfig = {
|
|
|
33
37
|
maxConcurrent: 6,
|
|
34
38
|
};
|
|
35
39
|
|
|
40
|
+
// 注意:DEFAULT_CONFIG 刻意不含 collectSync 键(与 defaultEngine/engineRouting 同风格)——
|
|
41
|
+
// SW 包 package.json 的 pi.startupConfig 声明守护(startup-config-declaration.test.ts)
|
|
42
|
+
// 断言与 DEFAULT_CONFIG 深相等,声明面只含既有键。collectSync 缺省语义由
|
|
43
|
+
// DEFAULT_COLLECT_SYNC 承载,消费方以 `config.collectSync ?? DEFAULT_COLLECT_SYNC` 兜底
|
|
44
|
+
// (loadGlobalConfig 文件缺失/坏 JSON 路径返回 {...DEFAULT_CONFIG} 同样无键)。
|
|
45
|
+
|
|
36
46
|
/** 默认 maxConcurrent(DEFAULT_CONFIG 的镜像,sanitize 用)。 */
|
|
37
47
|
const DEFAULT_MAX_CONCURRENT = 6;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 同步收集配置节权威默认值(subagent-sync-collect 设计 §3.1.3,U1 foundation)。
|
|
51
|
+
* 消费方:startHandler 缺省模式解析 / U3+U4 批通知预算(flush 时热读)。
|
|
52
|
+
* 供守护测试断言(E5 各坏值回默认)。展开拷贝防调用方 mutate。
|
|
53
|
+
*/
|
|
54
|
+
export const DEFAULT_COLLECT_SYNC: CollectSyncConfig = {
|
|
55
|
+
default: "async",
|
|
56
|
+
perItemChars: 4000,
|
|
57
|
+
totalChars: 24000,
|
|
58
|
+
};
|
|
38
59
|
// ============================================================
|
|
39
60
|
// 路径
|
|
40
61
|
// ============================================================
|
|
@@ -133,19 +154,70 @@ function errnoCodeOf(err: unknown): string | undefined {
|
|
|
133
154
|
function sanitizeParsedConfig(parsed: Partial<SubagentsGlobalConfig>): SubagentsGlobalConfig {
|
|
134
155
|
const defaultEngine = sanitizeDefaultEngine(parsed.defaultEngine);
|
|
135
156
|
const engineRouting = sanitizeEngineRouting(parsed.engineRouting);
|
|
157
|
+
const collectSync = sanitizeCollectSync(parsed.collectSync);
|
|
136
158
|
return {
|
|
137
159
|
version: parsed.version ?? DEFAULT_CONFIG.version,
|
|
138
160
|
maxConcurrent: sanitizeMaxConcurrent(parsed.maxConcurrent),
|
|
139
161
|
...(defaultEngine !== undefined ? { defaultEngine } : {}),
|
|
140
162
|
...(engineRouting !== undefined ? { engineRouting } : {}),
|
|
163
|
+
...(collectSync !== undefined ? { collectSync } : {}),
|
|
141
164
|
};
|
|
142
165
|
}
|
|
143
166
|
|
|
144
|
-
/** maxConcurrent
|
|
167
|
+
/** maxConcurrent 校验:正整数,否则默认(S5:委托 sanitizePositiveInt,正整数规则单点化)。 */
|
|
145
168
|
function sanitizeMaxConcurrent(value: unknown): number {
|
|
146
|
-
return
|
|
147
|
-
|
|
148
|
-
|
|
169
|
+
return sanitizePositiveInt(value, DEFAULT_MAX_CONCURRENT);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* collectSync 节校验(E5:负数/非枚举/坏类型回默认,不炸启动;与 maxConcurrent 同判。
|
|
174
|
+
* subagent-sync-collect 设计 §3.1.5 E5 + §3.1.3)。逐字段 sanitize:坏字段回该字段
|
|
175
|
+
* 默认,好字段透传(部分覆盖合法)。整节缺失返回 undefined(不落键,与 engineRouting
|
|
176
|
+
* 同风格——缺省语义由消费方读 DEFAULT_COLLECT_SYNC)。
|
|
177
|
+
*
|
|
178
|
+
* [D6 #8] 字段级坏值回默认时 warn 一条(缺省字段不 warn——部分覆盖合法):config.json
|
|
179
|
+
* 是用户手编文件,静默回落使「改了配置不生效」无从排查(sanitize 行为不变,只补可观测)。
|
|
180
|
+
*
|
|
181
|
+
* 消费时机:startHandler 缺省 collect 解析(config.default,新 session 生效)与
|
|
182
|
+
* U3/U4 批通知预算(perItemChars/totalChars,flush 时热读)。
|
|
183
|
+
*/
|
|
184
|
+
export function sanitizeCollectSync(value: unknown): CollectSyncConfig | undefined {
|
|
185
|
+
if (typeof value !== "object" || value === null) return undefined;
|
|
186
|
+
const v = value as Record<string, unknown>;
|
|
187
|
+
const fallback = DEFAULT_COLLECT_SYNC;
|
|
188
|
+
// [D6 #8] 坏值字段清单(仅「显式给了坏值」计——缺省是合法部分覆盖,不 warn)。
|
|
189
|
+
// 值格式化不用 JSON.stringify:Symbol/BigInt 等 stringify 返回 undefined,String() 全类型安全。
|
|
190
|
+
const fmt = (val: unknown): string => (typeof val === "string" ? JSON.stringify(val) : String(val));
|
|
191
|
+
const bad: string[] = [];
|
|
192
|
+
if (v.default !== undefined && v.default !== "sync" && v.default !== "async") {
|
|
193
|
+
bad.push(`default=${fmt(v.default)}`);
|
|
194
|
+
}
|
|
195
|
+
if (v.perItemChars !== undefined && !isPositiveInt(v.perItemChars)) {
|
|
196
|
+
bad.push(`perItemChars=${fmt(v.perItemChars)}`);
|
|
197
|
+
}
|
|
198
|
+
if (v.totalChars !== undefined && !isPositiveInt(v.totalChars)) {
|
|
199
|
+
bad.push(`totalChars=${fmt(v.totalChars)}`);
|
|
200
|
+
}
|
|
201
|
+
if (bad.length > 0) {
|
|
202
|
+
logger.warn(
|
|
203
|
+
`[subagents] config collectSync invalid field(s) reverted to defaults: ${bad.join(", ")}`,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
default: v.default === "sync" ? "sync" : v.default === "async" ? "async" : fallback.default,
|
|
208
|
+
perItemChars: sanitizePositiveInt(v.perItemChars, fallback.perItemChars),
|
|
209
|
+
totalChars: sanitizePositiveInt(v.totalChars, fallback.totalChars),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** 正整数校验(collectSync 预算字段用;与 sanitizeMaxConcurrent 同判)。 */
|
|
214
|
+
function sanitizePositiveInt(value: unknown, fallback: number): number {
|
|
215
|
+
return isPositiveInt(value) ? (value as number) : fallback;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** 正整数判定(sanitizePositiveInt 的探测面,坏值 warn 清单复用)。 */
|
|
219
|
+
function isPositiveInt(value: unknown): boolean {
|
|
220
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
149
221
|
}
|
|
150
222
|
|
|
151
223
|
/**
|
|
@@ -25,7 +25,7 @@ function tmpPath(name: string): string {
|
|
|
25
25
|
|
|
26
26
|
afterEach(() => {
|
|
27
27
|
if (tmpRoot !== undefined) {
|
|
28
|
-
rmSync(tmpRoot, { recursive: true, force: true });
|
|
28
|
+
rmSync(tmpRoot, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
29
29
|
tmpRoot = undefined;
|
|
30
30
|
}
|
|
31
31
|
});
|
|
@@ -29,7 +29,7 @@ beforeEach(() => {
|
|
|
29
29
|
});
|
|
30
30
|
|
|
31
31
|
afterEach(() => {
|
|
32
|
-
rmSync(tmpRoot, { recursive: true, force: true });
|
|
32
|
+
rmSync(tmpRoot, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
33
33
|
});
|
|
34
34
|
|
|
35
35
|
/** 读池 refs.json 的解析形态。 */
|
|
@@ -142,7 +142,7 @@ describe("releasePoolRef 引用计数与整池删除", () => {
|
|
|
142
142
|
it("池目录不存在时 release → 无原生状态可清,视为成功不抛", () => {
|
|
143
143
|
acquirePool(tmpRoot, "zcode", "ghost", "bg-1");
|
|
144
144
|
// 归零但目录已被外部删除
|
|
145
|
-
rmSync(resolvePoolDir(tmpRoot, "zcode", "ghost"), { recursive: true, force: true });
|
|
145
|
+
rmSync(resolvePoolDir(tmpRoot, "zcode", "ghost"), { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
146
146
|
expect(() => releasePoolRef(tmpRoot, "zcode", "ghost", "bg-1")).not.toThrow();
|
|
147
147
|
});
|
|
148
148
|
|
|
@@ -44,7 +44,7 @@ describe("conformance C5:read 降级链(三级都不 throw)", () => {
|
|
|
44
44
|
dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "c5-read-"));
|
|
45
45
|
});
|
|
46
46
|
afterEach(() => {
|
|
47
|
-
fs.rmSync(dataDir, { recursive: true, force: true });
|
|
47
|
+
fs.rmSync(dataDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
48
48
|
});
|
|
49
49
|
|
|
50
50
|
it("重放等价性:journal 重放 turns 与 live 累积一致(共用 updateFromEvent reducer)", () => {
|
|
@@ -45,7 +45,7 @@ describe("pi golden 回放(conformance C3/C5-journal,免 LLM)", () => {
|
|
|
45
45
|
// 深比较(含 undefined 键缺省语义——JSON 序列化后 undefined 字段自然消失,两侧同构)
|
|
46
46
|
expect(replayed).toEqual(golden.events);
|
|
47
47
|
} finally {
|
|
48
|
-
rmSync(dir, { recursive: true, force: true });
|
|
48
|
+
rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
49
49
|
}
|
|
50
50
|
});
|
|
51
51
|
|
|
@@ -123,7 +123,7 @@ beforeEach(() => {
|
|
|
123
123
|
});
|
|
124
124
|
|
|
125
125
|
afterEach(() => {
|
|
126
|
-
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
126
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
127
127
|
});
|
|
128
128
|
|
|
129
129
|
describe("wrapper 合并语义(v2 注入优先)", () => {
|
|
@@ -45,7 +45,7 @@ beforeEach(() => {
|
|
|
45
45
|
|
|
46
46
|
afterEach(async () => {
|
|
47
47
|
for (const engine of engines.splice(0)) await engine.dispose().catch(() => undefined);
|
|
48
|
-
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
48
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
49
49
|
});
|
|
50
50
|
|
|
51
51
|
interface ErrorScenario {
|
|
@@ -175,6 +175,12 @@ export function createRecord(
|
|
|
175
175
|
engine?: string;
|
|
176
176
|
/** 引擎 fallback 留痕(probe 失败路由回默认引擎)。GUI 警告条数据源。 */
|
|
177
177
|
engineFallback?: { from: string; reason: string };
|
|
178
|
+
/**
|
|
179
|
+
* 同步收集模式标记(subagent-sync-collect U1 foundation)。undefined = async
|
|
180
|
+
* (缺省语义,旧记录零迁移)。U2 接线点:service.createRecordForMode 从
|
|
181
|
+
* ExecuteOptions.collect 读入(opts.collect === "sync" ? "sync" : undefined)。
|
|
182
|
+
*/
|
|
183
|
+
collectMode?: "sync";
|
|
178
184
|
controller?: AbortController;
|
|
179
185
|
},
|
|
180
186
|
): ExecutionRecord {
|
|
@@ -194,6 +200,7 @@ export function createRecord(
|
|
|
194
200
|
idleTimeoutMs: identity.idleTimeoutMs,
|
|
195
201
|
engine: identity.engine,
|
|
196
202
|
engineFallback: identity.engineFallback,
|
|
203
|
+
collectMode: identity.collectMode,
|
|
197
204
|
|
|
198
205
|
// 状态(实时更新)
|
|
199
206
|
status: "running",
|
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
// D-017 时序:collectPatch → completeRecord → archive → cleanup(finalized+worktree+
|
|
6
6
|
// aliveMarker+pending注销) → manifest(最后 best-effort)。
|
|
7
7
|
//
|
|
8
|
-
// [Critical #1 / PR #85] cleanup 全部在 manifest 写之前执行——manifest
|
|
9
|
-
|
|
10
|
-
//
|
|
11
|
-
// pending 记账错乱。现 manifest 写移到 Step 4(最后),best-effort
|
|
12
|
-
|
|
8
|
+
// [Critical #1 / PR #85] cleanup 全部在 manifest 写之前执行——manifest 是 sa- id 反查
|
|
9
|
+
// 索引(最新已知快照),非正确性依赖,写失败仅记录不阻断。旧实现 Step 2.5 throw 会跳过
|
|
10
|
+
// Step 3 cleanup,导致磁盘满/权限错时 worktree 泄漏 + finalized marker 不写 + alive
|
|
11
|
+
// marker 残留 + pending 记账错乱。现 manifest 写移到 Step 4(最后),best-effort
|
|
12
|
+
//(console.error + appendEntry,不 throw)。
|
|
13
13
|
//
|
|
14
14
|
// B9 兜底:completeRecord/archive 抛错→后续 cleanup/manifest 仍执行。
|
|
15
15
|
|
|
@@ -177,11 +177,13 @@ function removeAliveMarkerIfPresent(record: ExecutionRecord): void {
|
|
|
177
177
|
/**
|
|
178
178
|
* Step 4 (last): manifest 持久化(best-effort,不阻断、不 throw)。
|
|
179
179
|
*
|
|
180
|
-
* [Critical #1] manifest
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
180
|
+
* [Critical #1] manifest 是 sa- id 反查索引(最新已知快照),不是正确性依赖。本步骤
|
|
181
|
+
* 写终态快照;sync 批成功成员不经此处——落标出口(subagent-service
|
|
182
|
+
* appendBatchFinalizedEntry)已先行补写,message upgrade 到终态时被本步骤原子
|
|
183
|
+
* 覆盖。写失败时仅记录(console.error + appendEntry),绝不让 manifest 写失败
|
|
184
|
+
* 跳过 Step 3 cleanup 或抛出打断 finalize 链。旧实现 Step 2.5 throw 会
|
|
185
|
+
* 跳过 Step 3 cleanup。task/slug/model 从 ExecutionRecord 抓取(配合
|
|
186
|
+
* ManifestRecord 补字段),manifestToSubagent 投影时用真实值而非硬编码空串。
|
|
185
187
|
*/
|
|
186
188
|
async function writeManifestBestEffort(deps: FinalizeDeps, record: ExecutionRecord): Promise<void> {
|
|
187
189
|
try {
|
|
@@ -265,7 +267,9 @@ export async function doFinalizeRecord(
|
|
|
265
267
|
* - 不调 completeRecord(record 不冻结,保留 turns[] 等运行时状态供续聊累积)
|
|
266
268
|
* - 不调 store.archive(record 留内存,getMutable 可查、list 可见)
|
|
267
269
|
* - 不 cleanup worktree(保留对话模式工作目录)
|
|
268
|
-
* - 不写 manifest
|
|
270
|
+
* - 不写 manifest:轮终回 running-resumable 非终态化,本方法无终态快照可写——
|
|
271
|
+
* manifest 是 sa- id 反查索引(最新已知快照),sync 批成员的先行快照由落标出口
|
|
272
|
+
* 补写(subagent-service appendBatchFinalizedEntry),此处不重复
|
|
269
273
|
* - 删 .alive marker(进程已 SIGTERM 回收,不再是活进程)
|
|
270
274
|
* - emitUnregister(进程已死,从 pending 活跃后代差集移除;record 留内存不 archive)
|
|
271
275
|
*
|