@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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/execution/notifier.ts
|
|
2
2
|
//
|
|
3
|
-
// Background 完成回注主对话。sync
|
|
3
|
+
// Background 完成回注主对话。collect:"sync" 的批通知不经单条 notify() 生命周期,
|
|
4
|
+
// 由 collect-coordinator 合批后调本文件 notifyBatch 投递(成员结果不直接返回调用方)。
|
|
4
5
|
//
|
|
5
6
|
// 职责(U2 后):
|
|
6
7
|
// - buildLlmContent:格式化通知文案(本文件唯一逻辑职责)
|
|
@@ -9,6 +10,7 @@
|
|
|
9
10
|
// notify-ledger.ts);未装配时退回投递内核路径(createDelivery 经通知域窄端口
|
|
10
11
|
// NotifyDomainPorts 注入——core 依赖闭包不含 session-delivery,见 core/notify-ports.ts;
|
|
11
12
|
// 合并窗口 / 去重 / 退避 / flush 委托内核,旧装配 / 无 ledger 测试兼容)
|
|
13
|
+
// - notifyBatch / buildBatchLlmContent / computeBatchBudget:sync 批通知三件套(U3/U4)
|
|
12
14
|
//
|
|
13
15
|
// 投递通道(D5 单通道化):ledger 路径经 courier 在 settled 边沿直达
|
|
14
16
|
// pi.sendMessage({triggerTurn:true});内核路径的 port.send 同样只传
|
|
@@ -17,6 +19,8 @@
|
|
|
17
19
|
// 设计 D5 实测证伪);busy 场景由 ledger(settled 边沿 + isIdle 二次复查)或内核
|
|
18
20
|
// settled 订阅驱动在空闲边沿投递。
|
|
19
21
|
|
|
22
|
+
import { createHash } from "node:crypto";
|
|
23
|
+
|
|
20
24
|
import { getLogger } from "../core/logger.ts";
|
|
21
25
|
import { getNotifyDomainPorts, type DeliveryHandle, type DeliveryPort } from "../core/notify-ports.ts";
|
|
22
26
|
|
|
@@ -104,6 +108,169 @@ export interface NotifierHost {
|
|
|
104
108
|
onAgentSettled?(handler: () => void): void;
|
|
105
109
|
}
|
|
106
110
|
|
|
111
|
+
/** sync 批通知幂等键前缀(subagent-sync-collect 设计 §3.1.3 批身份)。 */
|
|
112
|
+
const BATCH_NOTIFY_ID_PREFIX = "sync-batch:";
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* sync 批通知 notifyId:`sync-batch:<sha1(sorted member ids)>`。
|
|
116
|
+
*
|
|
117
|
+
* 成员集身份(D2 隐式批):字典序排序后 sha1——同成员集不同登记顺序同 hash(重启
|
|
118
|
+
* 重建批 / E1 补发凭此与账本原条目对齐,幂等去重);不同成员集必异 hash(跨批不互吞)。
|
|
119
|
+
* 成员 id 为 sa-xxx 格式无分隔歧义,join(",") 仅作边界防御。
|
|
120
|
+
*/
|
|
121
|
+
export function buildBatchNotifyId(memberIds: readonly string[]): string {
|
|
122
|
+
const digest = createHash("sha1")
|
|
123
|
+
.update([...memberIds].sort().join(","))
|
|
124
|
+
.digest("hex");
|
|
125
|
+
return `${BATCH_NOTIFY_ID_PREFIX}${digest}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ─── U4 预算截断 + 指针(设计 §3.1.2 两段式预算,确定性统一收紧)───
|
|
129
|
+
|
|
130
|
+
/** sync 批预算参数(config collectSync 节语义)。U4 以设计默认值锁规格;
|
|
131
|
+
* config 热读(flush 时读值)需 service 传参接线,不在本函数内自行读 config。 */
|
|
132
|
+
export interface BatchBudgetParams {
|
|
133
|
+
/** 单条目正文上限(第一段截断)。 */
|
|
134
|
+
perItemChars: number;
|
|
135
|
+
/** 全批正文中量预算(第二段收紧触发阈值)。 */
|
|
136
|
+
totalChars: number;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** 设计默认预算(§3.1.3 config 示例值)。 */
|
|
140
|
+
const DEFAULT_BATCH_BUDGET: BatchBudgetParams = { perItemChars: 4000, totalChars: 24000 };
|
|
141
|
+
|
|
142
|
+
/** 预算计划(computeBatchBudget 输出,纯数据)。 */
|
|
143
|
+
export interface BatchBudgetPlan {
|
|
144
|
+
/** ② 总量再压缩是否触发(Σ per-item 截断后正文 > totalChars)。 */
|
|
145
|
+
tightened: boolean;
|
|
146
|
+
/** 每条目正文有效上限:未收紧 = perItemChars(兼作第一段截断上限);纯清单退化 = 0。 */
|
|
147
|
+
effectivePerItem: number;
|
|
148
|
+
/** 纯清单退化(floor(totalChars/n) < 200 → 条目只剩头行 + 指针行,正文 0 字符)。 */
|
|
149
|
+
listOnly: boolean;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** 绝对下限(设计 §3.1.2):预算折算低于它时放弃正文,条目退化为「头行 + 指针行」。 */
|
|
153
|
+
const LIST_ONLY_FLOOR = 200;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 两段式批预算计划(纯函数,规格测试 collect-budget.test.ts 锁定)。
|
|
157
|
+
*
|
|
158
|
+
* ① per-item:每条目正文 ≤ perItemChars;② 总量:Σ(截断后) > totalChars 时
|
|
159
|
+
* effectivePerItem = clamp(floor(totalChars / n), 200, perItemChars) 统一收紧
|
|
160
|
+
* (n = 成员数)。刻意放弃按剩余预算的瀑布分配(非确定、依赖条目顺序),
|
|
161
|
+
* 统一收紧换确定性;收紧后 Σ ≤ n × floor(totalChars/n) ≤ totalChars 恒回到预算内。
|
|
162
|
+
* 未触发收紧时 effectivePerItem = perItemChars(同时承担第一段截断上限)。
|
|
163
|
+
*/
|
|
164
|
+
export function computeBatchBudget(
|
|
165
|
+
bodyLengths: readonly number[],
|
|
166
|
+
perItemChars: number,
|
|
167
|
+
totalChars: number,
|
|
168
|
+
): BatchBudgetPlan {
|
|
169
|
+
const n = bodyLengths.length;
|
|
170
|
+
const sumAfterPerItem = bodyLengths.reduce((sum, len) => sum + Math.min(len, perItemChars), 0);
|
|
171
|
+
if (n === 0 || sumAfterPerItem <= totalChars) {
|
|
172
|
+
return { tightened: false, effectivePerItem: perItemChars, listOnly: false };
|
|
173
|
+
}
|
|
174
|
+
const tight = Math.floor(totalChars / n);
|
|
175
|
+
if (tight < LIST_ONLY_FLOOR) {
|
|
176
|
+
return { tightened: true, effectivePerItem: 0, listOnly: true };
|
|
177
|
+
}
|
|
178
|
+
return { tightened: true, effectivePerItem: Math.min(tight, perItemChars), listOnly: false };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** 千位分隔(对齐设计样例 11,234;手写正则零 ICU 依赖)。 */
|
|
182
|
+
function formatChars(n: number): string {
|
|
183
|
+
return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* session_read result action 的默认单条字符上限(adversarial-review-fixes §3.4 C3)。
|
|
188
|
+
* 对齐 extensions/universal/session-reader/src/result-action.ts 的 RESULT_DEFAULT_LIMIT
|
|
189
|
+
* (8000)——extension 与 runtime 包无共享常量处、反向 import 会建立 subagent-core 对
|
|
190
|
+
* extension 的依赖,故本地常量 + 注释锚定;两侧改动须同步。
|
|
191
|
+
*/
|
|
192
|
+
const SESSION_READ_DEFAULT_LIMIT = 8000;
|
|
193
|
+
|
|
194
|
+
/** 截断指针行(设计 §3.1.2 样例):主 agent 据此按需 session_read 取回全文。
|
|
195
|
+
* [S8 code-simplify 口径注] 本函数的 X(total - kept)= **丢弃**字符数;而
|
|
196
|
+
* session-reader result-action.ts formatResultTruncation 同模板的 X = **保留**
|
|
197
|
+
* 字符数(limit)——同模板异语义,统一字符串 = 行为变更,勿顺手改口径。
|
|
198
|
+
* [C3 limit 随附] perItemLimit = 该成员实际截断预算(effectivePerItem):预算超过
|
|
199
|
+
* session_read 默认 limit(8000) 时附 "limit":N——模型照抄指针行 JSON 调用才能取到
|
|
200
|
+
* ≥ 默认值的有效正文(否则默认 8000 反而少于通知里已见的 kept,触发第二层截断);
|
|
201
|
+
* ≤8000 不附(默认已覆盖,避免噪音)。 */
|
|
202
|
+
function buildTruncationPointer(id: string, kept: number, total: number, perItemLimit: number): string {
|
|
203
|
+
const limitPart = perItemLimit > SESSION_READ_DEFAULT_LIMIT ? `,"limit":${perItemLimit}` : "";
|
|
204
|
+
return `[truncated ${formatChars(total - kept)} of ${formatChars(total)} chars — full result: session_read {"action":"result","session":"${id}"${limitPart}}]`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** closed 成员 outcome 兑底物化(与下方 notify() 投影边界同款);running 原样透传。 */
|
|
208
|
+
function withMaterializedOutcome(record: BgNotifyRecord): BgNotifyRecord {
|
|
209
|
+
return record.status === "closed"
|
|
210
|
+
? { ...record, outcome: record.outcome ?? deriveOutcome(record.closedReason, record.error) }
|
|
211
|
+
: record;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* sync 批通知文案(U3 批形态 + U4 两段式预算截断)。
|
|
216
|
+
*
|
|
217
|
+
* 形态(设计 §3.1.1 交互样例):批头行 `Subagent batch completed: N finished,
|
|
218
|
+
* M failed, K cancelled.` + 各成员条目(buildLlmContent 同语义)以 "\n\n---\n\"
|
|
219
|
+
* \n" join(分隔符对齐 ledger mergeItems / 内核合批——TUI/LLM 两消费方同构)。
|
|
220
|
+
* 批头计数口径 = 成员 outcome 三态(completed→finished);closed 成员 outcome 缺省
|
|
221
|
+
* 时按单一权威 deriveOutcome 兑底(与 notify() 投影边界同款,幂等无害)。
|
|
222
|
+
*
|
|
223
|
+
* [U4 预算截断] 仅对展示结果正文的条目(closed+completed / running)生效;
|
|
224
|
+
* failed/cancelled 条目无 result 可取回(指针无意义)不计正文预算、原样输出。
|
|
225
|
+
* totalChars 口径(§3.1.2):仅计条目正文(截断后)之和,批头/头行/指针行/
|
|
226
|
+
* 分隔符等包装开销为有界常量不入预算。未触预算时输出与 U3 基线逐字节一致。
|
|
227
|
+
* budget 缺省 = 设计默认值;config 热读接线由 flush 调用方传入。
|
|
228
|
+
*/
|
|
229
|
+
export function buildBatchLlmContent(
|
|
230
|
+
records: readonly BgNotifyRecord[],
|
|
231
|
+
budget: BatchBudgetParams = DEFAULT_BATCH_BUDGET,
|
|
232
|
+
): string {
|
|
233
|
+
// 入口一次物化(S1,code-simplify):closed 成员 outcome 兑底仅此一处,bodies/计数/
|
|
234
|
+
// 条目全部读物化结果——「单一权威 deriveOutcome」从注释承诺变构造事实。幂等纯函数,
|
|
235
|
+
// 与原四处各自推导逐字节等价;running 原样透传零拷贝。
|
|
236
|
+
const mat = records.map(withMaterializedOutcome);
|
|
237
|
+
let finished = 0;
|
|
238
|
+
let failed = 0;
|
|
239
|
+
let cancelled = 0;
|
|
240
|
+
// 各条目参与预算的正文(§3.1.2 口径);null = 该条目不展示 result 正文。
|
|
241
|
+
const bodies = mat.map((record): string | null => {
|
|
242
|
+
if (record.status === "closed" && record.outcome !== "completed") return null;
|
|
243
|
+
return record.result ?? "(empty)";
|
|
244
|
+
});
|
|
245
|
+
const plan = computeBatchBudget(
|
|
246
|
+
bodies.map((body) => body?.length ?? 0),
|
|
247
|
+
budget.perItemChars,
|
|
248
|
+
budget.totalChars,
|
|
249
|
+
);
|
|
250
|
+
const items = mat.map((record, i) => {
|
|
251
|
+
if (record.status === "closed") {
|
|
252
|
+
const outcome = record.outcome;
|
|
253
|
+
if (outcome === "completed") finished += 1;
|
|
254
|
+
else if (outcome === "failed") failed += 1;
|
|
255
|
+
else if (outcome === "cancelled") cancelled += 1;
|
|
256
|
+
}
|
|
257
|
+
const body = bodies[i];
|
|
258
|
+
if (body === null || body.length <= plan.effectivePerItem) {
|
|
259
|
+
// 未触预算(含 failed/cancelled 条目):U3 基线字节不变。
|
|
260
|
+
return buildLlmContent(record);
|
|
261
|
+
}
|
|
262
|
+
// 截断:正文保留前 limit 字符 + 省略号,尾接指针行(kept=0 纯清单时省略号一并省略)。
|
|
263
|
+
const kept = body.slice(0, plan.effectivePerItem);
|
|
264
|
+
const truncated = buildLlmContent({ ...record, result: plan.listOnly ? "" : `${kept}…` });
|
|
265
|
+
// 纯清单时 result 置空会在 buildLlmContent 内残留头行尾换行("Result:\n" + ""),
|
|
266
|
+
// 收掉该空行让条目严格为「头行 + 指针行」两行形态(设计 §3.1.2 纯清单退化)。
|
|
267
|
+
const stripped = plan.listOnly ? truncated.replace(/\n$/, "") : truncated;
|
|
268
|
+
return `${stripped}\n${buildTruncationPointer(record.id, kept.length, body.length, plan.effectivePerItem)}`;
|
|
269
|
+
});
|
|
270
|
+
const header = `Subagent batch completed: ${finished} finished, ${failed} failed, ${cancelled} cancelled.`;
|
|
271
|
+
return [header, ...items].join("\n\n---\n\n");
|
|
272
|
+
}
|
|
273
|
+
|
|
107
274
|
/**
|
|
108
275
|
* 将 BgNotifyRecord 格式化为 LLM 可读的 notification content。
|
|
109
276
|
*
|
|
@@ -164,6 +331,13 @@ function buildLlmContent(record: BgNotifyRecord): string {
|
|
|
164
331
|
export interface BgNotifier {
|
|
165
332
|
/** 入队一条完成通知(去重 + 合批窗口合并)。dispose 后短路。 */
|
|
166
333
|
notify(record: BgNotifyRecord): void;
|
|
334
|
+
/**
|
|
335
|
+
* sync 批通知(subagent-sync-collect U3):成员集 → 单条批投递。幂等键
|
|
336
|
+
* `sync-batch:<sha1(sorted ids)>`,走与 notify 同一写账→settled 边沿投递链(单
|
|
337
|
+
* entry)。返回 false = 幂等拒绝(同成员集批已在账/已销账)或空批 / dispose 后——
|
|
338
|
+
* 调用方(service flushBatch 接线)据此决定 batchFinalized 落标。
|
|
339
|
+
*/
|
|
340
|
+
notifyBatch(records: readonly BgNotifyRecord[], budget?: BatchBudgetParams): boolean;
|
|
167
341
|
/** 立即 flush(session_shutdown 调用,防丢失)。 */
|
|
168
342
|
flushPendingNotifications(): void;
|
|
169
343
|
/** session 结束:清队列,dispose 内核 handle。 */
|
|
@@ -292,6 +466,37 @@ export function createNotifier(host: NotifierHost): BgNotifier {
|
|
|
292
466
|
};
|
|
293
467
|
let handle: DeliveryHandle = createHandle();
|
|
294
468
|
|
|
469
|
+
/** 四步投递链尾部共用段(S2,code-simplify):notify 单条与 notifyBatch 逐行同构
|
|
470
|
+
* 的收尾——原两份手写拷贝使注释宣称的「同一四步链」无结构强制,现单点实现。
|
|
471
|
+
* ledger 在 → ①写账(拒绝 = false,幂等去重:同 notifyId 已在账/已销账)→
|
|
472
|
+
* ②attemptDeliver(settled 边沿 + isIdle 二次复查,③④销账/重放在 ledger 内);
|
|
473
|
+
* ledger 缺(旧装配 / 部分测试,含 jiti 单例分裂的失效形态)→ 内核路径降级 +
|
|
474
|
+
* 降级留痕 warn(C-ext-06 配套,不改变向后兼容行为)。返回 false 仅 notifyBatch
|
|
475
|
+
* 消费(调用方跳过落标);notify 忽略(fire-and-forget)。闭包读 let handle:
|
|
476
|
+
* revive 重建后自然指向新 handle。 */
|
|
477
|
+
function deliverViaLedgerOrKernel(notifyId: string, content: string, details: object): boolean {
|
|
478
|
+
const ledger = getBoundNotifyLedger();
|
|
479
|
+
if (ledger) {
|
|
480
|
+
if (!ledger.record(notifyId, content, details)) return false;
|
|
481
|
+
ledger.attemptDeliver();
|
|
482
|
+
return true;
|
|
483
|
+
}
|
|
484
|
+
notifyLogger.warn("notify ledger not bound, falling back to delivery kernel path (at-most-once)", {
|
|
485
|
+
notifyId,
|
|
486
|
+
});
|
|
487
|
+
handle.send({
|
|
488
|
+
payload: {
|
|
489
|
+
kind: "custom",
|
|
490
|
+
customType: NOTIFY_CUSTOM_TYPE,
|
|
491
|
+
content,
|
|
492
|
+
display: true,
|
|
493
|
+
details,
|
|
494
|
+
},
|
|
495
|
+
dedupeKey: notifyId,
|
|
496
|
+
});
|
|
497
|
+
return true;
|
|
498
|
+
}
|
|
499
|
+
|
|
295
500
|
return {
|
|
296
501
|
notify(record: BgNotifyRecord): void {
|
|
297
502
|
if (disposed) return;
|
|
@@ -314,31 +519,35 @@ export function createNotifier(host: NotifierHost): BgNotifier {
|
|
|
314
519
|
// 投递尝试)→ ②courier 边沿投递(settled 边沿 + 120s 看门狗 + isIdle 二次
|
|
315
520
|
// 复查)→ ③回执销账 / ④重放幂等均在 ledger 内。record 返回 false = 同
|
|
316
521
|
// notifyId 已在账或已销账(幂等去重,含重启恢复后的已送达账号零重发)。
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
return;
|
|
322
|
-
}
|
|
522
|
+
// 无 ledger 装配时内核路径降级 + 降级留痕(C-ext-06 配套,见 helper 注释)。
|
|
523
|
+
// 单条通知为 fire-and-forget,返回值忽略。
|
|
524
|
+
deliverViaLedgerOrKernel(notifyId, content, payload);
|
|
525
|
+
},
|
|
323
526
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
//
|
|
328
|
-
//
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
527
|
+
notifyBatch(records: readonly BgNotifyRecord[], budget?: BatchBudgetParams): boolean {
|
|
528
|
+
if (disposed || records.length === 0) return false;
|
|
529
|
+
|
|
530
|
+
// 投影边界物化(与 notify 同款):closed 成员补 outcome——批头计数与成员条目
|
|
531
|
+
// 单一来源。消源自入参浅拷贝,不改写协调器缓冲持有的快照对象。
|
|
532
|
+
const payloads = records.map((record) =>
|
|
533
|
+
record.status === "closed"
|
|
534
|
+
? { ...record, outcome: record.outcome ?? deriveOutcome(record.closedReason, record.error) }
|
|
535
|
+
: { ...record },
|
|
536
|
+
);
|
|
537
|
+
const batchNotifyId = buildBatchNotifyId(payloads.map((p) => p.id));
|
|
538
|
+
// budget(可选)= service flushBatch/E1 补发接线传入的 config 热读值(U4 deviation #8
|
|
539
|
+
// 由 U5 接线);undefined → buildBatchLlmContent 参数缺省 = 设计默认值(4000/24000)。
|
|
540
|
+
// 账本重放走写入时已定格的 content,预算在写账时刻生效(“flush 时热读”语义)。
|
|
541
|
+
const content = buildBatchLlmContent(payloads, budget);
|
|
542
|
+
// details 形态 = ledger mergeItems 批量分支({batch:true, items}——bg-notify-render
|
|
543
|
+
// extractBatch 已支持,⛔1 核对)+ 顶层 notifyId:collectDeliveredNotifyIds 的回执
|
|
544
|
+
// 匹配键认 details.notifyId / details.items[].notifyId,批身份键必须顶层可达,
|
|
545
|
+
// 否则批 entry 永不销账、重投至放弃。顶层 notifyId 键对 extractBatch 透明(多键无感)。
|
|
546
|
+
const details = { batch: true as const, notifyId: batchNotifyId, items: payloads };
|
|
547
|
+
|
|
548
|
+
// 与 notify 同一四步链(deliverViaLedgerOrKernel,S2 提取):返回 false = 同成
|
|
549
|
+
// 员集批已在账(E1 重建重发 / 重复 flush)→ 调用方跳过落标。
|
|
550
|
+
return deliverViaLedgerOrKernel(batchNotifyId, content, details);
|
|
342
551
|
},
|
|
343
552
|
|
|
344
553
|
flushPendingNotifications(): void {
|
|
@@ -8,7 +8,7 @@ import { displayAgentName } from "../shared/agent-ref.ts";
|
|
|
8
8
|
import { snapshot } from "./execution-record.ts";
|
|
9
9
|
import { hasIdleTimer } from "./lifecycle-manager.ts";
|
|
10
10
|
import { hasLiveProcessHandle, isIdle, isResumable } from "./lifecycle-predicates.ts";
|
|
11
|
-
import type { BgNotifier, NotifierHost } from "./notifier.ts";
|
|
11
|
+
import type { BgNotifier, BatchBudgetParams, NotifierHost } from "./notifier.ts";
|
|
12
12
|
import { createNotifier } from "./notifier.ts";
|
|
13
13
|
import type { BgNotifyRecord } from "./notifier.ts";
|
|
14
14
|
import type { ExecutionRecord, RecordSnapshot } from "./types.ts";
|
|
@@ -50,6 +50,14 @@ export interface NotifyHost {
|
|
|
50
50
|
emitPendingRegister(id: string, name?: string): void;
|
|
51
51
|
/** pending-notifications 注销(原模块函数 emitPendingUnregister)。 */
|
|
52
52
|
emitPendingUnregister(id: string, reason: string): void;
|
|
53
|
+
/** [sync-collect U2 合并] record → BgNotifyRecord 映射——collectCoordinator 的
|
|
54
|
+
* toNotifyRecord/notifyAsync 依赖注入消费(守卫放行逻辑单一权威在本文件)。 */
|
|
55
|
+
toNotifyRecord(record: ExecutionRecord): BgNotifyRecord | undefined;
|
|
56
|
+
/** [sync-collect 合并] 单条直发——collectCoordinator notifyAsync 与 E9 dispose
|
|
57
|
+
* 转换路径消费(已越过 toNotifyRecord 守卫的成品通知)。 */
|
|
58
|
+
notify(record: BgNotifyRecord): void;
|
|
59
|
+
/** [sync-collect 合并] sync 批投递——collectCoordinator flushBatch 与 E1 补发消费。 */
|
|
60
|
+
notifyBatch(records: readonly BgNotifyRecord[], budget?: BatchBudgetParams): boolean;
|
|
53
61
|
/** dispose 的逆操作(initSession 复活,原 notifier.revive 委托)。 */
|
|
54
62
|
revive(): void;
|
|
55
63
|
/** dispose 前冲刷待发通知(原 notifier.flushPendingNotifications 委托)。 */
|
|
@@ -197,6 +205,18 @@ export function createNotifyHost(deps: NotifyHostDeps): NotifyHost {
|
|
|
197
205
|
emitPendingUnregister(deps.getPi(), id, reason);
|
|
198
206
|
},
|
|
199
207
|
|
|
208
|
+
toNotifyRecord(record: ExecutionRecord): BgNotifyRecord | undefined {
|
|
209
|
+
return toNotifyRecord(record);
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
notify(record: BgNotifyRecord): void {
|
|
213
|
+
notifier.notify(record);
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
notifyBatch(records: readonly BgNotifyRecord[], budget?: BatchBudgetParams): boolean {
|
|
217
|
+
return notifier.notifyBatch(records, budget);
|
|
218
|
+
},
|
|
219
|
+
|
|
200
220
|
revive(): void {
|
|
201
221
|
notifier.revive();
|
|
202
222
|
},
|
|
@@ -97,6 +97,18 @@ export interface SubagentRecordEntryData {
|
|
|
97
97
|
* 透传不枚举内部键(zcode = { sessionId, dbPath });缺省 = pi(存量 entry 零迁移)。
|
|
98
98
|
*/
|
|
99
99
|
engineHandle?: { sessionRef: Record<string, string>; journalPath?: string; poolKey: string };
|
|
100
|
+
/**
|
|
101
|
+
* 同步收集模式标记(subagent-sync-collect 设计 §3.1.3,U1 foundation)。
|
|
102
|
+
* undefined = async(缺省语义,旧 entry 零迁移——undefined 经 JSON.stringify 自然缺省)。
|
|
103
|
+
* 消费方:U5 rebuildEntryRecord 投影扩展 + E1 重建扫描。
|
|
104
|
+
*/
|
|
105
|
+
collectMode?: "sync";
|
|
106
|
+
/**
|
|
107
|
+
* 离开批终局标记(subagent-sync-collect 设计 §3.1.3,U1 foundation)。两出口统一
|
|
108
|
+
* 落标(批闭合 flush / E9 dispose 转换,均 appendEntry 持久化)。undefined =
|
|
109
|
+
* 未离开批 / 旧 entry 零迁移。消费方:U5 E1 重建扫描只收无标记成员(防双重通知)。
|
|
110
|
+
*/
|
|
111
|
+
batchFinalized?: boolean;
|
|
100
112
|
}
|
|
101
113
|
|
|
102
114
|
/** SubagentRecord → 自描述 entry data(快照投影,不 mutate 源)。
|
|
@@ -134,5 +146,9 @@ export function toSubagentRecordEntry(record: SubagentRecord): SubagentRecordEnt
|
|
|
134
146
|
engine: record.engine,
|
|
135
147
|
engineFallback: record.engineFallback,
|
|
136
148
|
engineHandle: record.engineHandle,
|
|
149
|
+
// 同步收集两字段(U1 foundation):undefined 经 JSON.stringify 自然缺省,
|
|
150
|
+
// 旧记录/旧 entry 序列化产物字节不变(零迁移)。
|
|
151
|
+
collectMode: record.collectMode,
|
|
152
|
+
batchFinalized: record.batchFinalized,
|
|
137
153
|
};
|
|
138
154
|
}
|
|
@@ -259,40 +259,103 @@ function collectLastRecordEntries(content: string): Map<string, Record<string, u
|
|
|
259
259
|
return lastById;
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
+
/** entry data 字段的安全 string 读取(非 string → undefined)。 */
|
|
263
|
+
function entryStr(d: Record<string, unknown>, k: string): string | undefined {
|
|
264
|
+
return typeof d[k] === "string" ? (d[k] as string) : undefined;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** entry data 字段的安全 number 读取(非 number → undefined)。 */
|
|
268
|
+
function entryNum(d: Record<string, unknown>, k: string): number | undefined {
|
|
269
|
+
return typeof d[k] === "number" ? (d[k] as number) : undefined;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** 终态域投影:status 只认 "closed" 字面量(其余含缺省 → "running",旧调用方行为不变);
|
|
273
|
+
* closedReason 经枚举守卫(非法/缺省 → undefined)。 */
|
|
274
|
+
function readEntryTerminalFields(
|
|
275
|
+
d: Record<string, unknown>,
|
|
276
|
+
): Pick<SubagentRecord, "status" | "closedReason"> {
|
|
277
|
+
const closedReason = entryStr(d, "closedReason");
|
|
278
|
+
return {
|
|
279
|
+
status: d.status === "closed" ? "closed" : "running",
|
|
280
|
+
closedReason: isValidClosedReason(closedReason) ? closedReason : undefined,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** 批收集域投影(U5 E1):仅显式字面量收敛,缺省 undefined(JSON 序列化自然缺省)。 */
|
|
285
|
+
function readEntryBatchFields(
|
|
286
|
+
d: Record<string, unknown>,
|
|
287
|
+
): Pick<SubagentRecord, "collectMode" | "batchFinalized" | "resumable"> {
|
|
288
|
+
return {
|
|
289
|
+
collectMode: d.collectMode === "sync" ? "sync" : undefined,
|
|
290
|
+
batchFinalized: d.batchFinalized === true ? true : undefined,
|
|
291
|
+
resumable: d.resumable === true ? true : undefined,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** engine 域投影:engineFallback/engineHandle 经运行时 guard(未知 JSON 不裸收)。 */
|
|
296
|
+
function readEntryEngineFields(
|
|
297
|
+
d: Record<string, unknown>,
|
|
298
|
+
): Pick<SubagentRecord, "engine" | "engineFallback" | "engineHandle"> {
|
|
299
|
+
return {
|
|
300
|
+
engine: entryStr(d, "engine"),
|
|
301
|
+
engineFallback: isEngineFallbackShape(d.engineFallback) ? d.engineFallback : undefined,
|
|
302
|
+
engineHandle: isEngineHandleShape(d.engineHandle) ? d.engineHandle : undefined,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
262
306
|
/** entry data 即 SubagentRecord v1 快照——带运行时 guard 重建(taste/no-unsafe-cast)。
|
|
263
|
-
* 损坏 entry(agent/task/startedAt 任一缺失)返回 null,由调用方跳过。
|
|
307
|
+
* 损坏 entry(agent/task/startedAt 任一缺失)返回 null,由调用方跳过。
|
|
308
|
+
* [U5 E1] 投影白名单扩展(设计 §3.1.3「标记读取通路」):collectMode/batchFinalized
|
|
309
|
+
* + 终态五字段 status/endedAt/closedReason/result/error——原实现硬编码
|
|
310
|
+
* status:"running" 且不投影终态,E1 重建成员恒被视为 running,「全员终态→补发」
|
|
311
|
+
* 判定永假、补发内容缺失,整条补发路径成死代码。status 守卫只认 "closed" 字面量
|
|
312
|
+
* (其余含缺省 → "running",旧调用方 recoverEntryOnlyOrphans 行为不变——其候选
|
|
313
|
+
* 守卫已滤非 running 末条);closedReason 经 isValidClosedReason 枚举守卫。
|
|
314
|
+
* [v2 D3] 再补 resumable/sessionFile 两投影:成功成员崩溃时末条恒为轮终
|
|
315
|
+
* running+resumable entry(SP-5 有意语义),不投影 resumable 则 E1 无法与协调器
|
|
316
|
+
* hasRunningSync 同构豁免(§2.3 断链 3);sessionFile 原硬编码 undefined,导致
|
|
317
|
+
* E1 落标路径重建快照丢失反查索引锚(断链 1 前置依赖)。recoverEntryOnlyOrphans
|
|
318
|
+
* 的候选判定(isEntryOrphanCandidate)只认 status==="running",两新字段不参与
|
|
319
|
+
* 判定(P-rebuild 探针守卫面)。
|
|
320
|
+
* [E1 恢复批语义修复] 再补 patchFile 投影:entry data 携带该字段
|
|
321
|
+
* (toSubagentRecordEntry 落盘含 patchFile)但原投影丢弃 → E1 补发记录丢失
|
|
322
|
+
* worktree patch 的 git-apply 回收指针(正常 flush 路径经 toNotifyRecord 特意
|
|
323
|
+
* 携带,notify-host.ts patchFile 透传)。undefined 经 JSON.stringify 自然缺省,
|
|
324
|
+
* 无 patchFile 的存量 entry 序列化字节不变(落标出口经 toSubagentRecordEntry
|
|
325
|
+
* 按名重投影,重建对象字段顺序不影响序列化字节形态)。
|
|
326
|
+
* 字段顺序 = 对象字面量原序(终态/批收集/engine 域以 spread 在原位置展开),
|
|
327
|
+
* entry 序列化字节形态不变。 */
|
|
264
328
|
function rebuildEntryRecord(id: string, d: Record<string, unknown>): SubagentRecord | null {
|
|
265
|
-
const
|
|
266
|
-
const
|
|
267
|
-
const
|
|
268
|
-
const task = str("task");
|
|
269
|
-
const startedAt = num("startedAt");
|
|
329
|
+
const agent = entryStr(d, "agent");
|
|
330
|
+
const task = entryStr(d, "task");
|
|
331
|
+
const startedAt = entryNum(d, "startedAt");
|
|
270
332
|
if (agent === undefined || task === undefined || startedAt === undefined) return null; // 损坏 entry:跳过
|
|
271
333
|
return {
|
|
272
334
|
id,
|
|
273
335
|
agent,
|
|
274
336
|
task,
|
|
275
|
-
slug:
|
|
276
|
-
|
|
337
|
+
slug: entryStr(d, "slug") ?? "",
|
|
338
|
+
...readEntryTerminalFields(d),
|
|
277
339
|
mode: "background",
|
|
278
340
|
startedAt,
|
|
279
|
-
rootSessionId:
|
|
280
|
-
parentRecordId:
|
|
281
|
-
depth:
|
|
282
|
-
endedAt:
|
|
283
|
-
turns:
|
|
284
|
-
totalTokens:
|
|
285
|
-
model:
|
|
286
|
-
thinkingLevel:
|
|
341
|
+
rootSessionId: entryStr(d, "rootSessionId"),
|
|
342
|
+
parentRecordId: entryStr(d, "parentRecordId"),
|
|
343
|
+
depth: entryNum(d, "depth") ?? 0,
|
|
344
|
+
endedAt: entryNum(d, "endedAt"),
|
|
345
|
+
turns: entryNum(d, "turns") ?? 0,
|
|
346
|
+
totalTokens: entryNum(d, "totalTokens") ?? 0,
|
|
347
|
+
model: entryStr(d, "model") ?? "",
|
|
348
|
+
thinkingLevel: entryStr(d, "thinkingLevel"),
|
|
287
349
|
eventLog: [],
|
|
288
350
|
displayItems: [],
|
|
289
|
-
|
|
351
|
+
result: entryStr(d, "result"),
|
|
352
|
+
error: entryStr(d, "error"),
|
|
353
|
+
sessionFile: entryStr(d, "sessionFile"),
|
|
354
|
+
patchFile: entryStr(d, "patchFile"),
|
|
290
355
|
chatMode: d.chatMode === true,
|
|
291
|
-
round:
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
isEngineFallbackShape(d.engineFallback) ? d.engineFallback : undefined,
|
|
295
|
-
engineHandle: isEngineHandleShape(d.engineHandle) ? d.engineHandle : undefined,
|
|
356
|
+
round: entryNum(d, "round"),
|
|
357
|
+
...readEntryEngineFields(d),
|
|
358
|
+
...readEntryBatchFields(d),
|
|
296
359
|
};
|
|
297
360
|
}
|
|
298
361
|
|
|
@@ -645,35 +708,53 @@ export class RecordStore {
|
|
|
645
708
|
* - 文件不可读(IO 错误,可能暂时)→ 不判终态,落 resumable entry(防御性路径,
|
|
646
709
|
* IO 恢复后重开可重判)。
|
|
647
710
|
*
|
|
711
|
+
* [v2 D3] mainSessionFile = 覆写 merge 数据源(主 session 每 id 末条 entry,与 E1
|
|
712
|
+
* 的 scanLastRecordEntries 同款通路):崩溃前的批域标记(collectMode/batchFinalized)
|
|
713
|
+
* 与轮终 result/model 只活在主文件 entry——重建矩阵(buildRecord)的数据源
|
|
714
|
+
* (sidecar/子文件 identity)不含它们,覆写前不 merge 就会被抹掉(v2 §2.2 断链 2:
|
|
715
|
+
* E1 候选集恒空的真根因)。缺省(undefined)时 merge 无源,行为与旧版一致。
|
|
716
|
+
* 参数为追加式第二参(rootSessionFilter 保持首参):既有调用面只传过滤参。
|
|
717
|
+
*
|
|
648
718
|
* 防重:orphanJudged 实例级缓存(resumable 形态无 sidecar 锚,同进程重复调用跳过;
|
|
649
719
|
* 终态形态双重防护 = sidecar + 缓存)。调用方:index.ts session_start 恢复段(一次)。
|
|
650
720
|
*/
|
|
651
|
-
recoverOrphanRecords(rootSessionFilter?: string): void {
|
|
721
|
+
recoverOrphanRecords(rootSessionFilter?: string, mainSessionFile?: string): void {
|
|
722
|
+
const lastById = new Map(this.scanLastRecordEntries(mainSessionFile).map((r) => [r.id, r]));
|
|
652
723
|
for (const rec of this.reconstructAll(rootSessionFilter)) {
|
|
653
724
|
// 分支 4 命中集 = running 且无活进程实例(分支 3 带 externalInstance,分支 1/2 已 closed)。
|
|
654
725
|
if (rec.status !== "running" || rec.externalInstance !== undefined) continue;
|
|
655
726
|
if (this.orphanJudged.has(rec.id)) continue;
|
|
656
727
|
this.orphanJudged.add(rec.id);
|
|
657
|
-
this.finalizeOrphanRecord(rec);
|
|
728
|
+
this.finalizeOrphanRecord(rec, lastById.get(rec.id));
|
|
658
729
|
}
|
|
659
730
|
}
|
|
660
731
|
|
|
661
732
|
/**
|
|
662
733
|
* 单孤儿 record 的终态判定与落 entry(residual-fixes §5.2 三判据 + chat 分流)。
|
|
663
734
|
* 防重锚(orphanJudged 标记)已由调用方完成。
|
|
735
|
+
*
|
|
736
|
+
* [v2 D3] 覆写前 merge(lastEntry = 主 session 同 id 末条 entry 重建,调用方构建):
|
|
737
|
+
* 覆写是状态迁移不是信息重建,迁移不应丢末条既有信息。三个落 entry 分支(chatMode
|
|
738
|
+
* 分流 / IO 保守 / 终态覆写)统一基于 merge 后的 rec,同款口径。
|
|
664
739
|
*/
|
|
665
|
-
private finalizeOrphanRecord(rec: SubagentRecord): void {
|
|
666
|
-
|
|
740
|
+
private finalizeOrphanRecord(rec: SubagentRecord, lastEntry?: SubagentRecord): void {
|
|
741
|
+
// 仅补 undefined/空值字段、不覆盖已有值——覆写自带的 status/closedReason/endedAt/
|
|
742
|
+
// error 不在 merge 字段集,天然不受影响。批域字段(collectMode/batchFinalized)对
|
|
743
|
+
// 非 sync 成员恒 no-op(末条 entry collectMode undefined → 无值可补);result/model
|
|
744
|
+
// 的修补对 async/chat 成员同样生效——拉齐 light 重建丢 result/model 与 full 重建的
|
|
745
|
+
// 既有形态不一致(v2 D3 影响面诚实口径),不触碰任何通知文案(golden 不锁 entry 字节)。
|
|
746
|
+
const rec0 = lastEntry === undefined ? rec : RecordStore.mergeOrphanLastEntry(rec, lastEntry);
|
|
747
|
+
if (rec0.chatMode === true) {
|
|
667
748
|
// chat 会话跨重启等续聊:保留 running(可续聊),仅落执行态信号。
|
|
668
|
-
this.reportSubagentRecord({ ...
|
|
749
|
+
this.reportSubagentRecord({ ...rec0, resumable: true });
|
|
669
750
|
return;
|
|
670
751
|
}
|
|
671
|
-
const sessionFile =
|
|
752
|
+
const sessionFile = rec0.sessionFile;
|
|
672
753
|
if (sessionFile === undefined) return; // 无子文件锚(目录扫描源不可达,防御)
|
|
673
754
|
const lastLine = readLastJsonlLine(sessionFile);
|
|
674
755
|
if (!lastLine.ok) {
|
|
675
756
|
// IO 错误可能暂时——判终态不可逆,保守落 resumable 等重开重判。
|
|
676
|
-
this.reportSubagentRecord({ ...
|
|
757
|
+
this.reportSubagentRecord({ ...rec0, resumable: true });
|
|
677
758
|
return;
|
|
678
759
|
}
|
|
679
760
|
let parseOk = false;
|
|
@@ -689,7 +770,7 @@ export class RecordStore {
|
|
|
689
770
|
// 把正常完成的记录误标成断联。
|
|
690
771
|
writeFinalized(sessionFile, "gc");
|
|
691
772
|
this.reportSubagentRecord({
|
|
692
|
-
...
|
|
773
|
+
...rec0,
|
|
693
774
|
status: "closed",
|
|
694
775
|
closedReason: "gc",
|
|
695
776
|
endedAt: Date.now(),
|
|
@@ -697,6 +778,25 @@ export class RecordStore {
|
|
|
697
778
|
});
|
|
698
779
|
}
|
|
699
780
|
|
|
781
|
+
/** [v2 D3] 孤儿覆写 merge 字段集:末条 entry 的批域标记 + 轮终正文/模型,仅补 rec 侧
|
|
782
|
+
* undefined/空值(model 的空值形态是 ""——light 重建无 model_change entry 时起步
|
|
783
|
+
* 空串),不覆盖已有值。merge 后写 entry 经 reportSubagentRecord →
|
|
784
|
+
* toSubagentRecordEntry 序列化,undefined 字段自然缺省(不引入显式 null)。 */
|
|
785
|
+
private static mergeOrphanLastEntry(rec: SubagentRecord, last: SubagentRecord): SubagentRecord {
|
|
786
|
+
const pickStr = (cur: string | undefined, src: string | undefined): string | undefined =>
|
|
787
|
+
cur !== undefined && cur !== "" ? cur : src;
|
|
788
|
+
return {
|
|
789
|
+
...rec,
|
|
790
|
+
collectMode: rec.collectMode ?? last.collectMode,
|
|
791
|
+
batchFinalized: rec.batchFinalized ?? last.batchFinalized,
|
|
792
|
+
result: pickStr(rec.result, last.result),
|
|
793
|
+
// 类型收尾:两侧实参恒 string(rec.model 类型非可选;last.model 重建投影自带
|
|
794
|
+
// `?? ""`),pickStr 签名宽返回 string|undefined —— `?? ""` 运行时不可达,
|
|
795
|
+
// 仅满足 model 非可选类型,空串回退语义不变(cur 空 → src,src 也空 → "")。
|
|
796
|
+
model: pickStr(rec.model, last.model) ?? "",
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
|
|
700
800
|
/**
|
|
701
801
|
* [E2E 实测缺口] entry-born 孤儿恢复:register entry 已落主 session、但子 session 文件
|
|
702
802
|
* 从未创建(父进程死在 spawn 窗口期——register 写点与子进程首笔写入之间的窗口;外部
|
|
@@ -762,6 +862,32 @@ export class RecordStore {
|
|
|
762
862
|
});
|
|
763
863
|
}
|
|
764
864
|
|
|
865
|
+
/**
|
|
866
|
+
* [E1/U5] sync 批崩溃恢复扫描:主 session 文件「每 id 末条 subagent-record entry」
|
|
867
|
+
* (collectLastRecordEntries + rebuildEntryRecord 组合通路,设计 §3.1.3「标记读取
|
|
868
|
+
* 通路」——batchFinalized 落标 entry 写主 session 文件,本扫描同文件域才可见;禁走
|
|
869
|
+
* collectRecords light 路径,它只读子文件 identity 头+sidecar,主 session 落标
|
|
870
|
+
* entry 不可见)。返回每 id 末条重建的完整 record(含 collectMode/batchFinalized /
|
|
871
|
+
* 终态五字段,损坏 entry 跳过);调用方(service.recoverSyncCollectBatch 的 E1 过滤、
|
|
872
|
+
* recoverOrphanRecords 的覆写 merge)自行取舍。主文件不可读(含新 session 未 flush
|
|
873
|
+
* 的 ENOENT)→ 空数组静默。
|
|
874
|
+
*/
|
|
875
|
+
scanLastRecordEntries(mainSessionFile: string | undefined): SubagentRecord[] {
|
|
876
|
+
if (mainSessionFile === undefined) return [];
|
|
877
|
+
let content: string;
|
|
878
|
+
try {
|
|
879
|
+
content = fs.readFileSync(mainSessionFile, "utf-8");
|
|
880
|
+
} catch {
|
|
881
|
+
return []; // 与 recoverEntryOnlyOrphans 同判:best-effort 恢复,不可读静默跳过
|
|
882
|
+
}
|
|
883
|
+
const out: SubagentRecord[] = [];
|
|
884
|
+
for (const [id, d] of collectLastRecordEntries(content)) {
|
|
885
|
+
const rec = rebuildEntryRecord(id, d);
|
|
886
|
+
if (rec !== null) out.push(rec);
|
|
887
|
+
}
|
|
888
|
+
return out;
|
|
889
|
+
}
|
|
890
|
+
|
|
765
891
|
/** 订阅变更。返回取消订阅函数。 */
|
|
766
892
|
onChange(listener: ChangeListener): () => void {
|
|
767
893
|
this.listeners.add(listener);
|
|
@@ -1320,6 +1446,11 @@ export class RecordStore {
|
|
|
1320
1446
|
engineFallback: r.engineFallback,
|
|
1321
1447
|
// U2:engineHandle 经 entry 持久化(register/archive 双写点均经本投影),无则 undefined 自然省略
|
|
1322
1448
|
engineHandle: r.engineHandle,
|
|
1449
|
+
// [U5 修复 U2 披露的投影缺口] 同步收集两字段随本投影持久化(register entry /
|
|
1450
|
+
// archive entry 双写点)——原缺失时闭合判定 flushBatch 重建、E1 重建扫描等消费方
|
|
1451
|
+
// 读不到原始值。undefined 经 JSON.stringify 自然缺省,旧 entry 零迁移。
|
|
1452
|
+
collectMode: r.collectMode,
|
|
1453
|
+
batchFinalized: r.batchFinalized,
|
|
1323
1454
|
};
|
|
1324
1455
|
}
|
|
1325
1456
|
}
|