wezard 1.3.11 → 1.3.13
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.
|
@@ -328,6 +328,43 @@ const cleanUserText = (raw) => {
|
|
|
328
328
|
return stripped;
|
|
329
329
|
};
|
|
330
330
|
const truncate = (s, max) => s.length <= max ? s : `${s.slice(0, max)}…(+${s.length - max})`;
|
|
331
|
+
// 后台子agent 完成通知的无标签形态 —— codebuddy 平台在子agent 跑完后注入
|
|
332
|
+
// `[Framework Auto-Notification] <agent> completed` 的纯文本 user 行 (不套
|
|
333
|
+
// <task-notification>)。只认"整条消息以它开头", 免得把正文里提到它的消息吞掉。
|
|
334
|
+
const AGENT_NOTICE_RE = /^\s*(\[(?:Framework )?Auto-Notification\][^\n]*)/;
|
|
335
|
+
// Agent/Task 派发是不是后台的。后台派发的 result 是 spawn 句柄 (300ms 就回),
|
|
336
|
+
// 不代表子agent 结束 —— in-flight 记账必须按这个标志分流。
|
|
337
|
+
const isBackgroundAgentCall = (input) => {
|
|
338
|
+
const i = input;
|
|
339
|
+
return i?.run_in_background === true || i?.runInBackground === true;
|
|
340
|
+
};
|
|
341
|
+
// 派发的可辨识名 —— 完成通知里没有 callId, 只有 agent 名/描述, 用它对账。
|
|
342
|
+
const agentCallLabel = (input) => {
|
|
343
|
+
const i = input;
|
|
344
|
+
const v = [i?.name, i?.subagent_type, i?.description].find((x) => typeof x === "string" && x.trim());
|
|
345
|
+
return typeof v === "string" ? v.trim() : "";
|
|
346
|
+
};
|
|
347
|
+
// 4-gram Jaccard。中文按字符切窗即可, 无需分词; 只服务一个判断: 这两份终稿是不是
|
|
348
|
+
// 同一份答案 (平台把"子agent 完成"展开成结果 + 通知两条注入时, 模型会被唤醒两次,
|
|
349
|
+
// 各写一遍内容几乎相同的终稿 —— 逐字相等的比较拦不住)。
|
|
350
|
+
const shinglesOf = (s) => {
|
|
351
|
+
const t = s.replace(/\s+/g, "");
|
|
352
|
+
const out = new Set();
|
|
353
|
+
for (let i = 0; i + 4 <= t.length; i += 1)
|
|
354
|
+
out.add(t.slice(i, i + 4));
|
|
355
|
+
return out;
|
|
356
|
+
};
|
|
357
|
+
const textSimilarity = (x, y) => {
|
|
358
|
+
const a = shinglesOf(x);
|
|
359
|
+
const b = shinglesOf(y);
|
|
360
|
+
if (a.size === 0 || b.size === 0)
|
|
361
|
+
return x.replace(/\s+/g, "") === y.replace(/\s+/g, "") ? 1 : 0;
|
|
362
|
+
const ratio = a.size / b.size;
|
|
363
|
+
if (ratio > 2 || ratio < 0.5)
|
|
364
|
+
return 0; // 长度差一倍以上不可能是同一份答案
|
|
365
|
+
const inter = [...a].reduce((n, g) => (b.has(g) ? n + 1 : n), 0);
|
|
366
|
+
return inter / (a.size + b.size - inter);
|
|
367
|
+
};
|
|
331
368
|
const renderToolInput = (input) => {
|
|
332
369
|
try {
|
|
333
370
|
const json = JSON.stringify(input ?? {}, null, 0);
|
|
@@ -458,7 +495,14 @@ const renderLine = (raw, deps) => {
|
|
|
458
495
|
// a single styled line. quiet: 这是 subagent 的回执, 不单独推给聊天。
|
|
459
496
|
const taskMatch = c.match(TASK_NOTIF_RE);
|
|
460
497
|
if (taskMatch && taskMatch[1]) {
|
|
461
|
-
out.push({ kind: "skill_output", body: renderTaskNotification(taskMatch[1]), quiet: true });
|
|
498
|
+
out.push({ kind: "skill_output", body: renderTaskNotification(taskMatch[1]), quiet: true, agentDone: taskMatch[1] });
|
|
499
|
+
return out;
|
|
500
|
+
}
|
|
501
|
+
// 同一语义的无标签形态 (codebuddy 平台注入)。必须绕开下面的 includeUser 门 ——
|
|
502
|
+
// 它是后台派发唯一的结束信号, 被丢掉 openAgents 就永远等不到销账。
|
|
503
|
+
const notice = c.match(AGENT_NOTICE_RE)?.[1];
|
|
504
|
+
if (notice) {
|
|
505
|
+
out.push({ kind: "skill_output", body: `🏁 ${notice.trim()}`, quiet: true, agentDone: notice });
|
|
462
506
|
return out;
|
|
463
507
|
}
|
|
464
508
|
// /goal activation — non-isMeta 形态兜底 (marker 格式漂移防御), emit
|
|
@@ -475,12 +519,12 @@ const renderLine = (raw, deps) => {
|
|
|
475
519
|
// cover reloads or replays; the user message itself always can.
|
|
476
520
|
if (deps.isKeepalivePing?.(text))
|
|
477
521
|
return [{ kind: "keepalive_start", body: text }];
|
|
478
|
-
if (!deps.includeUser)
|
|
479
|
-
return [];
|
|
480
522
|
if (deps.isOwnInject(text))
|
|
481
523
|
return []; // dedupe WeCom→CLI echo
|
|
482
524
|
const quoted = text.split("\n").map((l) => `> ${l}`).join("\n");
|
|
483
|
-
|
|
525
|
+
// includeUser 只决定"渲不渲染", 不决定"算不算边界" —— quiet 的这一条照样发出,
|
|
526
|
+
// onItem 消费完边界语义后自己丢掉。
|
|
527
|
+
out.push({ kind: "user_text", body: quoted, quiet: !deps.includeUser });
|
|
484
528
|
}
|
|
485
529
|
else if (Array.isArray(c)) {
|
|
486
530
|
for (const b of c) {
|
|
@@ -1455,6 +1499,17 @@ export const startMirror = (deps) => {
|
|
|
1455
1499
|
const SOFT_TURN_END_DEFAULT_CLAUDE = 4_000;
|
|
1456
1500
|
const SOFT_TURN_END_DEFAULT_CB = 1_000;
|
|
1457
1501
|
const softTurnEndMsFor = (a) => cfg.wrc.mirror.softTurnEndMs ?? (backendForPath(a.jsonlPath).name === "codebuddy" ? SOFT_TURN_END_DEFAULT_CB : SOFT_TURN_END_DEFAULT_CLAUDE);
|
|
1502
|
+
/** 未返回派发的挂账上限。到点即销账 —— 结束信号可能永远不来 (后台 agent 挂了 /
|
|
1503
|
+
* 平台没发通知 / 前台 result 因中断丢失), 而 CLI 侧起的 turn 没有 hardTimer,
|
|
1504
|
+
* 没有这个上限就是永久静默。 */
|
|
1505
|
+
const OPEN_AGENT_TTL_MS = 15 * 60_000;
|
|
1506
|
+
/** 子agent 刚返回后的宽限窗口。此刻立刻收口投出去的还是派发前那句独白 —— 父侧
|
|
1507
|
+
* 正要写真正的答复。宽限期内父侧一有产出就撤销重排, 真的没有下文才以独白兜底。 */
|
|
1508
|
+
const AGENT_RETURN_GRACE_MS = 15_000;
|
|
1509
|
+
/** 同一 query 轮次内, 与上一份终稿的近似度超过此值即判为重复投递。 */
|
|
1510
|
+
const DUP_CONCLUSION_RATIO = 0.8;
|
|
1511
|
+
/** 重复终稿抑制的时间上限 —— 轮次相同但隔了很久的答复仍算新答复。 */
|
|
1512
|
+
const DUP_CONCLUSION_WINDOW_MS = 10 * 60_000;
|
|
1458
1513
|
const STREAM_SOFT_CAP = 18_000;
|
|
1459
1514
|
const TOOL_DETAIL_TTL_MS = 24 * 60 * 60 * 1000;
|
|
1460
1515
|
const turnRegistry = new Map();
|
|
@@ -1862,6 +1917,12 @@ export const startMirror = (deps) => {
|
|
|
1862
1917
|
// 其它所有 item 只写入 turn detail store。
|
|
1863
1918
|
// 能证明"assistant 已经在产出"的 item —— 见到它们才补开无气泡 turn。
|
|
1864
1919
|
const BRIEF_TURN_OPENERS = new Set(["text", "tool_use", "tool_result", "skill_output"]);
|
|
1920
|
+
// 父 agent 自己的发言 —— 只有这些能推翻"上一条消息说完了"的收口意图。其余 item
|
|
1921
|
+
// (tool_result / 完成通知 / 平台注入的 user 行) 都不是父侧产出, 只把欠着的那次
|
|
1922
|
+
// 软收口往后顺延, 不销欠账。见 onItem 的 softOwed 分支。
|
|
1923
|
+
// turn_end / goal_start 同样销欠账 —— 一个是权威收口 (软形态会在紧随其后的分支里
|
|
1924
|
+
// 重新记上欠账), 一个是整个 turn 交给 goal 模式接管。
|
|
1925
|
+
const SOFT_END_CANCELLERS = new Set(["text", "thinking", "tool_use", "turn_end", "goal_start"]);
|
|
1865
1926
|
// ── CoT 进度行 (brief 气泡) ─────────────────────────────────────────
|
|
1866
1927
|
// 一轮里正文要等 final text 才落地, 中间几十秒到几分钟气泡只有一个 "…"。这里把最新的
|
|
1867
1928
|
// thinking / 工具调用覆盖进这条还没收口的气泡, 让群里看得到进展。三条硬约束:
|
|
@@ -1986,6 +2047,7 @@ export const startMirror = (deps) => {
|
|
|
1986
2047
|
};
|
|
1987
2048
|
const startBriefTurn = async (a, frame, streamId, isSlash = false, userQuery = "") => {
|
|
1988
2049
|
const turnId = newTurnId();
|
|
2050
|
+
a.queryEpoch = (a.queryEpoch ?? 0) + 1; // WeCom 侧的新一轮同样是 query 边界
|
|
1989
2051
|
recordTurnStart({ id: turnId, target: a.target, sessionId: a.sessionId, cwd: a.runningCwd || undefined, userQuery: userQuery.trim() || undefined, cut: consumeCut(a), origin: consumeOrigin(a) });
|
|
1990
2052
|
// hardTimer 兜底: turn 若无终句 / turn_end 收口 (卡死/漏收), 到点仍收气泡。
|
|
1991
2053
|
const bubble = { frame, streamId, hardTimer: undefined, done: false };
|
|
@@ -2053,6 +2115,18 @@ export const startMirror = (deps) => {
|
|
|
2053
2115
|
// • 气泡已收 (过 6min 窗口 / 已收口) 或无气泡 turn (CLI 侧发起) → 把暂存 header
|
|
2054
2116
|
// 拼到 body 前缀一并 standalone, 一条消息含详情入口 + 正文。
|
|
2055
2117
|
// • body 为空 → 一个字不发, 顺手清掉 header, 不留"只有链接"的空消息。
|
|
2118
|
+
// • 同一 query 轮次内的第二份近似终稿 → 只记进 turn/detail, 不再投递 (见下)。
|
|
2119
|
+
//
|
|
2120
|
+
// 重复终稿: 平台把"一个后台子agent 完成"展开成结果 + 完成通知两条独立 user 注入,
|
|
2121
|
+
// 模型被唤醒两次、各写一遍完整终稿。briefConcluded 只在 turn 内幂等 —— turn 一收,
|
|
2122
|
+
// 下一条 text 就是 BRIEF_TURN_OPENER, 补开新 turn 再投一次。判据只能跨 turn:
|
|
2123
|
+
// 同一 query 轮次 (queryEpoch 未推进 = 中间没有真实用户输入) + 与上一份终稿高度
|
|
2124
|
+
// 近似 = 重复。气泡仍开的 turn 不在此列 —— 那是 WeCom 自己发起的一轮, 必须收口。
|
|
2125
|
+
const isDuplicateConclusion = (a, body) => a.lastConcludedBody !== undefined
|
|
2126
|
+
&& a.lastConcludedEpoch === (a.queryEpoch ?? 0)
|
|
2127
|
+
&& Date.now() - (a.lastConcludedAt ?? 0) < DUP_CONCLUSION_WINDOW_MS
|
|
2128
|
+
&& (!a.briefBubble || a.briefBubble.done)
|
|
2129
|
+
&& textSimilarity(a.lastConcludedBody, body) >= DUP_CONCLUSION_RATIO;
|
|
2056
2130
|
const concludeBriefTurn = (a, body) => {
|
|
2057
2131
|
const turnId = a.briefTurnId;
|
|
2058
2132
|
if (!turnId || a.briefConcluded)
|
|
@@ -2061,7 +2135,16 @@ export const startMirror = (deps) => {
|
|
|
2061
2135
|
a.pendingBriefHeader = undefined;
|
|
2062
2136
|
return;
|
|
2063
2137
|
}
|
|
2138
|
+
if (isDuplicateConclusion(a, body)) {
|
|
2139
|
+
a.briefConcluded = true; // 本轮已"定稿", 只是不投递
|
|
2140
|
+
a.pendingBriefHeader = undefined;
|
|
2141
|
+
log.info({ sessionId: a.sessionId, turnId, epoch: a.queryEpoch ?? 0 }, "brief: duplicate conclusion suppressed");
|
|
2142
|
+
return;
|
|
2143
|
+
}
|
|
2064
2144
|
a.briefConcluded = true;
|
|
2145
|
+
a.lastConcludedBody = body;
|
|
2146
|
+
a.lastConcludedEpoch = a.queryEpoch ?? 0;
|
|
2147
|
+
a.lastConcludedAt = Date.now();
|
|
2065
2148
|
if (a.briefBubble && !a.briefBubble.done) {
|
|
2066
2149
|
void finishBriefBubble(a, `${briefDetailLink(turnId, a.target)} ${body}`, true);
|
|
2067
2150
|
a.pendingBriefHeader = undefined;
|
|
@@ -2092,6 +2175,16 @@ export const startMirror = (deps) => {
|
|
|
2092
2175
|
a.briefConcluded = false;
|
|
2093
2176
|
a.briefLastText = undefined;
|
|
2094
2177
|
a.pendingBriefHeader = undefined;
|
|
2178
|
+
// 收口 = 欠账两清。前台派发的 result 不会再落回这一轮; 后台派发跨轮存活, 由完成
|
|
2179
|
+
// 通知 / TTL 销账 (voidTurnAgents)。挂着的软收口计时器一并撤掉 —— 它到点会去收
|
|
2180
|
+
// 「当前 turn」, 那时的当前 turn 已经是下一轮了。
|
|
2181
|
+
if (a.softEnd) {
|
|
2182
|
+
clearTimeout(a.softEnd);
|
|
2183
|
+
a.softEnd = undefined;
|
|
2184
|
+
}
|
|
2185
|
+
a.softOwed = false;
|
|
2186
|
+
a.softDeferred = false;
|
|
2187
|
+
voidTurnAgents(a);
|
|
2095
2188
|
closeSubagentTurns(a); // 父 turn 收口 = 它派出的 subagent 都已结束
|
|
2096
2189
|
clearCot(a);
|
|
2097
2190
|
};
|
|
@@ -2393,17 +2486,70 @@ export const startMirror = (deps) => {
|
|
|
2393
2486
|
}
|
|
2394
2487
|
// tool_use / tool_result / user_text / turn_usage: recorded to detail store; no bubble.
|
|
2395
2488
|
};
|
|
2489
|
+
// ── 未返回派发的账本 (openAgents) ───────────────────────────────────
|
|
2490
|
+
// 记账/销账/挂账上限。软收口的"独白 vs 正文"判据全靠它 —— 判据本身只是一个静默
|
|
2491
|
+
// 计时器, 分不出"我在等子agent"和"我说完了"。
|
|
2492
|
+
// TTL 到期的挂账作废。结束信号可能永远不来, 上限是唯一的兜底。
|
|
2493
|
+
const pruneOpenAgents = (a) => {
|
|
2494
|
+
const now = Date.now();
|
|
2495
|
+
for (const [id, v] of a.openAgents ?? []) {
|
|
2496
|
+
if (now - v.at < OPEN_AGENT_TTL_MS)
|
|
2497
|
+
continue;
|
|
2498
|
+
a.openAgents.delete(id);
|
|
2499
|
+
log.info({ sessionId: a.sessionId, callId: id, label: v.label }, "open agent expired — TTL reached");
|
|
2500
|
+
}
|
|
2501
|
+
};
|
|
2502
|
+
// 轮次作废: 已经过去的那一轮派发的前台 Agent/Task, result 不可能再落回, 直接销账。
|
|
2503
|
+
// 两条边界都要按 turnId 认人 —— 平台注入的 user 行 (子agent 结果回灌) 与本轮的完成
|
|
2504
|
+
// 通知都会走到这里, 一刀切会把**本轮正在跑**的前台派发也销掉, guard 当场失效。
|
|
2505
|
+
// 后台派发与轮次无关: 只有完成通知 / TTL 有资格销它。
|
|
2506
|
+
const voidTurnAgents = (a) => {
|
|
2507
|
+
for (const [id, v] of a.openAgents ?? []) {
|
|
2508
|
+
if (!v.background && v.turnId !== a.briefTurnId)
|
|
2509
|
+
a.openAgents.delete(id);
|
|
2510
|
+
}
|
|
2511
|
+
};
|
|
2512
|
+
// 后台派发销账。通知里没有 callId, 只有 agent 名/摘要: 先按派发时留的 label 命中,
|
|
2513
|
+
// 命中不了就销最早的一笔 —— N 次派发对 N 条通知, 账本终会归零。
|
|
2514
|
+
const clearBackgroundAgent = (a, notice) => {
|
|
2515
|
+
const open = [...(a.openAgents ?? [])].filter(([, v]) => v.background);
|
|
2516
|
+
if (open.length === 0)
|
|
2517
|
+
return;
|
|
2518
|
+
const hit = open.find(([, v]) => v.label && notice.includes(v.label))
|
|
2519
|
+
?? open.reduce((oldest, e) => (e[1].at < oldest[1].at ? e : oldest));
|
|
2520
|
+
a.openAgents.delete(hit[0]);
|
|
2521
|
+
log.info({ sessionId: a.sessionId, callId: hit[0], label: hit[1].label, left: a.openAgents.size }, "background agent done — cleared by completion notice");
|
|
2522
|
+
};
|
|
2523
|
+
// 下一次软收口尝试的等待时长: 账上还有未返回的派发 → 等到最早那笔 TTL 到期
|
|
2524
|
+
// (期间任何信号都会重排); 账已清零 → 宽限窗口 (子agent 刚回, 父侧马上要说话)。
|
|
2525
|
+
const deferredSoftDelay = (a) => {
|
|
2526
|
+
const oldest = [...(a.openAgents?.values() ?? [])]
|
|
2527
|
+
.reduce((m, v) => (m === undefined || v.at < m ? v.at : m), undefined);
|
|
2528
|
+
return oldest === undefined
|
|
2529
|
+
? Math.max(softTurnEndMsFor(a), AGENT_RETURN_GRACE_MS)
|
|
2530
|
+
: Math.max(1_000, OPEN_AGENT_TTL_MS - (Date.now() - oldest));
|
|
2531
|
+
};
|
|
2532
|
+
const armSoftEnd = (a, ms) => {
|
|
2533
|
+
if (a.softEnd)
|
|
2534
|
+
clearTimeout(a.softEnd);
|
|
2535
|
+
a.softEnd = setTimeout(() => fireSoftTurnEnd(a), ms);
|
|
2536
|
+
};
|
|
2396
2537
|
// 软收口到点: 静默期内没有新 item, 确认这一轮真的结束了。
|
|
2397
2538
|
const fireSoftTurnEnd = (a) => {
|
|
2398
2539
|
a.softEnd = undefined;
|
|
2399
|
-
|
|
2400
|
-
//
|
|
2401
|
-
//
|
|
2402
|
-
//
|
|
2540
|
+
pruneOpenAgents(a);
|
|
2541
|
+
// 子agent 调用中 (已派发、结束信号未到): 上一条文本只是普通独白, 不能当 final
|
|
2542
|
+
// 正文收口 —— 静默期往往正是父 turn 阻塞在子agent 上的表象。
|
|
2543
|
+
// 必须重挂: 后台派发的结束信号是平台注入的 user 行/通知, 它们不会让父转录再写
|
|
2544
|
+
// 一段文本, 光靠"下一次软收口"等不来。重挂的到点时刻见 deferredSoftDelay。
|
|
2403
2545
|
if (a.openAgents && a.openAgents.size > 0) {
|
|
2546
|
+
a.softDeferred = true;
|
|
2547
|
+
armSoftEnd(a, deferredSoftDelay(a));
|
|
2404
2548
|
log.info({ sessionId: a.sessionId, turnId: a.briefTurnId, open: [...a.openAgents.keys()] }, "soft turn_end deferred — subagent in flight, keeping monologue");
|
|
2405
2549
|
return;
|
|
2406
2550
|
}
|
|
2551
|
+
a.softOwed = false;
|
|
2552
|
+
a.softDeferred = false;
|
|
2407
2553
|
log.debug({ sessionId: a.sessionId, turnId: a.briefTurnId }, "soft turn_end confirmed");
|
|
2408
2554
|
if (a.goalActive) {
|
|
2409
2555
|
handleGoalItem(a, { kind: "turn_end" });
|
|
@@ -2475,10 +2621,15 @@ export const startMirror = (deps) => {
|
|
|
2475
2621
|
// its reply) mirror normally instead of being eaten by the stale swallow.
|
|
2476
2622
|
if (item.kind === "user_text") {
|
|
2477
2623
|
endKeepaliveSwallow(a);
|
|
2478
|
-
// 对话边界 = 旧 turn 清算。上一 turn
|
|
2624
|
+
// 对话边界 = 旧 turn 清算。上一 turn 派发而未返回的前台 Agent/Task 到此作废,
|
|
2479
2625
|
// 清掉防残留阻塞新 turn 的软收口 (guard 的 defer 判断依据)。
|
|
2480
|
-
a
|
|
2626
|
+
voidTurnAgents(a);
|
|
2627
|
+
a.queryEpoch = (a.queryEpoch ?? 0) + 1;
|
|
2481
2628
|
}
|
|
2629
|
+
// 后台派发的结束信号 —— 它不是 function_call_result (那只是 spawn 句柄)。放在
|
|
2630
|
+
// keepalive 门之前: 吞没窗口里到达也必须销账, 否则账本永久悬空。
|
|
2631
|
+
if (item.kind === "skill_output" && item.agentDone)
|
|
2632
|
+
clearBackgroundAgent(a, item.agentDone);
|
|
2482
2633
|
if (a.keepaliveQuiet || a.keepaliveByContent) {
|
|
2483
2634
|
const id = a.keepaliveTurnId;
|
|
2484
2635
|
if (id) {
|
|
@@ -2502,12 +2653,26 @@ export const startMirror = (deps) => {
|
|
|
2502
2653
|
}
|
|
2503
2654
|
return;
|
|
2504
2655
|
}
|
|
2505
|
-
//
|
|
2506
|
-
//
|
|
2656
|
+
// 父 agent 自己的新产出到达 = 上一条"消息写完了"并不代表这一轮结束 → 撤销待确认
|
|
2657
|
+
// 的软收口。硬信号 (end_turn / turn_duration) 的后端永远不会走到这里。
|
|
2658
|
+
//
|
|
2659
|
+
// 只有父侧发言 (SOFT_END_CANCELLERS) 才有资格推翻收口意图。tool_result / 后台完成
|
|
2660
|
+
// 通知 / 平台注入的 user 行都不是父侧发言 —— 它们只让欠着的那次收口往后顺延。
|
|
2661
|
+
// 一视同仁地抹掉欠账, 子agent 一返回独白就永远等不到收口 (CLI 侧 turn 没有
|
|
2662
|
+
// hardTimer 兜底 —— 那只挂在 WeCom 起的气泡上)。
|
|
2507
2663
|
if (a.softEnd) {
|
|
2508
2664
|
clearTimeout(a.softEnd);
|
|
2509
2665
|
a.softEnd = undefined;
|
|
2510
2666
|
}
|
|
2667
|
+
if (SOFT_END_CANCELLERS.has(item.kind)) {
|
|
2668
|
+
a.softOwed = false;
|
|
2669
|
+
a.softDeferred = false;
|
|
2670
|
+
}
|
|
2671
|
+
else if (a.softOwed)
|
|
2672
|
+
armSoftEnd(a, a.softDeferred ? deferredSoftDelay(a) : softTurnEndMsFor(a));
|
|
2673
|
+
// includeUser=false 下的 user 行只作边界信号 (上面已清账/推进轮次), 不渲染。
|
|
2674
|
+
if (item.kind === "user_text" && item.quiet)
|
|
2675
|
+
return;
|
|
2511
2676
|
// thinking 唯一的出口是 brief 气泡的 CoT 进度行。必须在这里就吃掉 —— 漏到下面任何
|
|
2512
2677
|
// 一条通道 (deferred buf → renderBuf / standalone / stream append) 都会把它写进正文,
|
|
2513
2678
|
// 那就变回已下线的 thinkStyle 了。
|
|
@@ -2517,17 +2682,21 @@ export const startMirror = (deps) => {
|
|
|
2517
2682
|
return;
|
|
2518
2683
|
}
|
|
2519
2684
|
if (item.kind === "turn_end" && item.soft === true) {
|
|
2520
|
-
a.
|
|
2685
|
+
a.softOwed = true;
|
|
2686
|
+
a.softDeferred = false;
|
|
2687
|
+
armSoftEnd(a, softTurnEndMsFor(a));
|
|
2521
2688
|
return;
|
|
2522
2689
|
}
|
|
2523
2690
|
// codebuddy: ExitPlanMode 若被本地先答, result 落盘即作废挂着的计划审批卡
|
|
2524
2691
|
// (卡 pending 期间 model 阻塞在本地对话框, 不可能有其它 tool_result)。
|
|
2525
2692
|
if (item.kind === "tool_result") {
|
|
2526
2693
|
mootMirrorPlan(a.sessionId);
|
|
2527
|
-
// Agent/Task 的 result 落回主转录 = 子agent 会话结束、控制权返回主会话。
|
|
2694
|
+
// 前台 Agent/Task 的 result 落回主转录 = 子agent 会话结束、控制权返回主会话。
|
|
2528
2695
|
// 任何结果 (含非 Agent/Task 工具) 都可能恰好顶着某 callId, 只删命中项即可。
|
|
2529
|
-
|
|
2530
|
-
|
|
2696
|
+
// 后台派发例外: 它的 result 是 300ms 就回的 spawn 句柄, 销账等于把 guard 关掉。
|
|
2697
|
+
const open = item.toolUseId ? a.openAgents?.get(item.toolUseId) : undefined;
|
|
2698
|
+
if (open && !open.background)
|
|
2699
|
+
a.openAgents.delete(item.toolUseId);
|
|
2531
2700
|
}
|
|
2532
2701
|
// Record tool_use signatures unconditionally (before any state branching),
|
|
2533
2702
|
// so flushBeforeCard's poll-drain can detect that the to-be-approved tool
|
|
@@ -2553,9 +2722,14 @@ export const startMirror = (deps) => {
|
|
|
2553
2722
|
description: typeof inp?.description === "string" && inp.description ? inp.description : undefined,
|
|
2554
2723
|
at: Date.now(),
|
|
2555
2724
|
}];
|
|
2556
|
-
// 派发即记 in-flight (key=callId)
|
|
2725
|
+
// 派发即记 in-flight (key=callId)。销账路径按前后台分流, 见 openAgents 注释。
|
|
2557
2726
|
if (c.toolUseId) {
|
|
2558
|
-
(a.openAgents ??= new Map()).set(c.toolUseId,
|
|
2727
|
+
(a.openAgents ??= new Map()).set(c.toolUseId, {
|
|
2728
|
+
at: Date.now(),
|
|
2729
|
+
background: isBackgroundAgentCall(c.input),
|
|
2730
|
+
label: agentCallLabel(c.input),
|
|
2731
|
+
turnId: a.briefTurnId,
|
|
2732
|
+
});
|
|
2559
2733
|
}
|
|
2560
2734
|
}
|
|
2561
2735
|
// codebuddy 的 ExitPlanMode 完全不过 PreToolUse hook (实测: 由
|