wezard 1.2.9 → 1.2.11
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/daemon/approval.js +6 -6
- package/dist/daemon/approval.js.map +1 -1
- package/dist/daemon/detail.js +5 -3
- package/dist/daemon/detail.js.map +1 -1
- package/dist/daemon/mirror-bridge.js +69 -29
- package/dist/daemon/mirror-bridge.js.map +1 -1
- package/dist/daemon/peers.js +12 -7
- package/dist/daemon/peers.js.map +1 -1
- package/dist/daemon/usage.js +10 -9
- package/dist/daemon/usage.js.map +1 -1
- package/dist/shared/config.js +9 -7
- package/dist/shared/config.js.map +1 -1
- package/package.json +7 -7
|
@@ -393,7 +393,7 @@ const renderToolInputCompact = (input, max) => {
|
|
|
393
393
|
const renderToolUseGroupBody = (calls, deps) => {
|
|
394
394
|
const renderOne = (c) => ({
|
|
395
395
|
compact: safeForMarkdown(renderToolInputCompact(c.input, deps.toolUseInlineMaxChars)),
|
|
396
|
-
url: deps.detailUrlFor(c.toolUseId),
|
|
396
|
+
url: deps.detailUrlFor(c.toolUseId, deps.target),
|
|
397
397
|
});
|
|
398
398
|
if (calls.length === 1) {
|
|
399
399
|
const c = calls[0];
|
|
@@ -489,7 +489,7 @@ const renderLine = (raw, deps) => {
|
|
|
489
489
|
if (toolUseId)
|
|
490
490
|
recordToolResult(toolUseId, full);
|
|
491
491
|
const compact = safeForMarkdown(oneLineSummary(full, 40));
|
|
492
|
-
const url = deps.detailUrlFor(toolUseId);
|
|
492
|
+
const url = deps.detailUrlFor(toolUseId, deps.target);
|
|
493
493
|
out.push({
|
|
494
494
|
kind: "tool_result",
|
|
495
495
|
toolUseId,
|
|
@@ -574,13 +574,12 @@ const renderLine = (raw, deps) => {
|
|
|
574
574
|
const rawIn = u.input_tokens ?? 0;
|
|
575
575
|
const cr = u.cache_read_input_tokens ?? 0;
|
|
576
576
|
const cw = u.cache_creation_input_tokens ?? 0;
|
|
577
|
-
//
|
|
577
|
+
// 网关把 input_tokens 报成 cr+cw+fresh 的总和 (claude-4.7-opus, deepseek-v4-flash),
|
|
578
578
|
// 而 Anthropic 官方 input_tokens 只含 fresh (与 cache_creation/cache_read disjoint)。
|
|
579
|
-
//
|
|
580
|
-
//
|
|
581
|
-
|
|
582
|
-
const
|
|
583
|
-
const input = isGatewayTotalized && rawIn >= cr + cw ? rawIn - cr - cw : rawIn;
|
|
579
|
+
// 判据用数据本身: input_tokens 是否已覆盖两个 cache 档 —— 覆盖即网关口径, 反推 fresh;
|
|
580
|
+
// 否则按原生口径 (input 即 fresh) 原样保留。不按模型名风格猜, deepseek 无点号版本。
|
|
581
|
+
const isTotalized = rawIn >= cr + cw;
|
|
582
|
+
const input = isTotalized ? rawIn - cr - cw : rawIn;
|
|
584
583
|
out.push({
|
|
585
584
|
kind: "turn_usage",
|
|
586
585
|
model,
|
|
@@ -1289,6 +1288,8 @@ export const startMirror = (deps) => {
|
|
|
1289
1288
|
// for >60s mid-turn and we don't want to drop the bubble while it's chewing.
|
|
1290
1289
|
const FLUSH_MS = 250;
|
|
1291
1290
|
const HARD_TIMEOUT_MS = 350_000;
|
|
1291
|
+
/** 首条 CLI 产出到达前的早期超时: 3s 无响应则先收气泡为详情链接。 */
|
|
1292
|
+
const EARLY_LINK_MS = 3_000;
|
|
1292
1293
|
/** 软收口静默期。后端只能说"这条消息写完了"(codebuddy) 时, 等这么久没有新 item
|
|
1293
1294
|
* 才认定一轮结束。取值只需盖住"叙述消息落盘 → 紧随其后的 function_call 落盘"
|
|
1294
1295
|
* 这一段, 与模型思考/工具执行时长无关。 */
|
|
@@ -1647,7 +1648,8 @@ export const startMirror = (deps) => {
|
|
|
1647
1648
|
const BRIEF_TURN_OPENERS = new Set(["text", "tool_use", "tool_result", "skill_output"]);
|
|
1648
1649
|
// 链接落到 chat 视图: 默认选中本 turn 所属的 #tag, 贴底显示整条会话。turnId 依旧
|
|
1649
1650
|
// 是凭据 (不可枚举), 只是页面从"一个 turn"扩成"这个 chat 的全部会话"。
|
|
1650
|
-
|
|
1651
|
+
// target 作为 ww_uniq 传下去, 让同 chat 的所有 turn 详情都复用一个 WeCom 窗口。
|
|
1652
|
+
const briefDetailLink = (turnId, target) => `✴️ [View chat details](${buildChatUrl(cfg.daemon.detailPublicBase, cfg.daemon.host, cfg.daemon.port, turnId, stripPrincipalPrefix(target))})`;
|
|
1651
1653
|
// 收口一条 loading 气泡: finish=true 写入最终内容, 只生效一次。发送失败退回 standalone。
|
|
1652
1654
|
// 排队中的 turn 也持有气泡, 所以气泡是显式入参而不是从 a 上取。
|
|
1653
1655
|
const finishBubble = async (a, b, content) => {
|
|
@@ -1655,6 +1657,10 @@ export const startMirror = (deps) => {
|
|
|
1655
1657
|
return;
|
|
1656
1658
|
b.done = true;
|
|
1657
1659
|
clearTimeout(b.hardTimer);
|
|
1660
|
+
if (b.earlyTimer) {
|
|
1661
|
+
clearTimeout(b.earlyTimer);
|
|
1662
|
+
b.earlyTimer = undefined;
|
|
1663
|
+
}
|
|
1658
1664
|
if (a.briefBubble === b)
|
|
1659
1665
|
a.briefBubble = undefined;
|
|
1660
1666
|
try {
|
|
@@ -1700,8 +1706,16 @@ export const startMirror = (deps) => {
|
|
|
1700
1706
|
log.warn({ sessionId: a.sessionId, turnId }, "brief: queued turn timed out — force-closing the stuck one");
|
|
1701
1707
|
closeBriefTurn(a);
|
|
1702
1708
|
}
|
|
1703
|
-
void finishBubble(a, bubble, briefDetailLink(turnId));
|
|
1709
|
+
void finishBubble(a, bubble, briefDetailLink(turnId, a.target));
|
|
1704
1710
|
}, HARD_TIMEOUT_MS);
|
|
1711
|
+
// earlyTimer: 3s 无 CLI 产出 → 先把 loading 气泡收成详情链接, 用户可即时点入。
|
|
1712
|
+
// 首条 item 到达时清除 (handleBriefItem 路径)。用显式 bubble ref 而非 a.briefBubble,
|
|
1713
|
+
// 因为排队中的 turn 还没成为活跃 turn, a.briefBubble 指向的是前一个。
|
|
1714
|
+
bubble.earlyTimer = setTimeout(() => {
|
|
1715
|
+
if (bubble.done)
|
|
1716
|
+
return;
|
|
1717
|
+
void finishBubble(a, bubble, briefDetailLink(turnId, a.target));
|
|
1718
|
+
}, EARLY_LINK_MS);
|
|
1705
1719
|
if (a.briefTurnId) {
|
|
1706
1720
|
(a.briefQueue ??= []).push(q);
|
|
1707
1721
|
log.info({ sessionId: a.sessionId, turnId, queued: a.briefQueue.length }, "brief: turn queued (previous still running)");
|
|
@@ -1733,7 +1747,7 @@ export const startMirror = (deps) => {
|
|
|
1733
1747
|
a.briefHadTool = false;
|
|
1734
1748
|
a.briefConcluded = false;
|
|
1735
1749
|
a.briefLastText = undefined;
|
|
1736
|
-
enqueueStandalone(a, briefDetailLink(turnId));
|
|
1750
|
+
enqueueStandalone(a, briefDetailLink(turnId, a.target));
|
|
1737
1751
|
log.info({ sessionId: a.sessionId, turnId }, "brief: turn started (CLI-side, no bubble)");
|
|
1738
1752
|
};
|
|
1739
1753
|
// 本轮一旦出现工具调用 / tool_result / 非 final 文本, 就说明这不是"一句话答完"的
|
|
@@ -1744,7 +1758,7 @@ export const startMirror = (deps) => {
|
|
|
1744
1758
|
if (!a.briefTurnId || !a.briefBubble || a.briefBubble.done)
|
|
1745
1759
|
return;
|
|
1746
1760
|
a.briefHadTool = true;
|
|
1747
|
-
void finishBriefBubble(a, briefDetailLink(a.briefTurnId));
|
|
1761
|
+
void finishBriefBubble(a, briefDetailLink(a.briefTurnId, a.target));
|
|
1748
1762
|
};
|
|
1749
1763
|
// 本轮结论落地, 只生效一次:
|
|
1750
1764
|
// • 无工具 → 结论 + 详情链接一并写进 loading 气泡 (仍是一条消息);
|
|
@@ -1758,7 +1772,7 @@ export const startMirror = (deps) => {
|
|
|
1758
1772
|
if (a.briefHadTool || !a.briefBubble)
|
|
1759
1773
|
sendStandalone(a, body);
|
|
1760
1774
|
else
|
|
1761
|
-
void finishBriefBubble(a, `${body}\n\n${briefDetailLink(turnId)}`);
|
|
1775
|
+
void finishBriefBubble(a, `${body}\n\n${briefDetailLink(turnId, a.target)}`);
|
|
1762
1776
|
// 排队中的 turn 已经建了记录但还没跑, 不能被这把扫帚扫成「已完成」。
|
|
1763
1777
|
const keep = [turnId, ...(a.briefQueue ?? []).map((q) => q.turnId)];
|
|
1764
1778
|
recordCloseOpenTurns({ target: a.target, sessionId: a.sessionId, exceptIds: keep });
|
|
@@ -1773,7 +1787,7 @@ export const startMirror = (deps) => {
|
|
|
1773
1787
|
// 气泡还开着 (有工具的 turn: 结论只发了 standalone, 气泡留到这里收链接; 或空 turn
|
|
1774
1788
|
// 没有结论) —— 收口: 统一写详情链接, 保证每个 turn 都有可点入口。
|
|
1775
1789
|
if (a.briefBubble && !a.briefBubble.done) {
|
|
1776
|
-
void finishBriefBubble(a, briefDetailLink(a.briefTurnId));
|
|
1790
|
+
void finishBriefBubble(a, briefDetailLink(a.briefTurnId, a.target));
|
|
1777
1791
|
}
|
|
1778
1792
|
recordTurnClose(a.briefTurnId);
|
|
1779
1793
|
log.info({ sessionId: a.sessionId, turnId: a.briefTurnId }, "brief: turn closed");
|
|
@@ -1793,6 +1807,11 @@ export const startMirror = (deps) => {
|
|
|
1793
1807
|
const turnId = a.briefTurnId;
|
|
1794
1808
|
if (!turnId)
|
|
1795
1809
|
return;
|
|
1810
|
+
// 首条 item 到达 → 清除 earlyTimer (CLI 已有响应, 不需要超时收链接了)。
|
|
1811
|
+
if (a.briefBubble?.earlyTimer) {
|
|
1812
|
+
clearTimeout(a.briefBubble.earlyTimer);
|
|
1813
|
+
a.briefBubble.earlyTimer = undefined;
|
|
1814
|
+
}
|
|
1796
1815
|
const now = Date.now();
|
|
1797
1816
|
if (item.kind === "tool_use") {
|
|
1798
1817
|
a.briefHadTool = true; // 有工具 → 气泡收口写详情链接, 结论另发 standalone
|
|
@@ -1922,6 +1941,9 @@ export const startMirror = (deps) => {
|
|
|
1922
1941
|
a.keepaliveQuiet = undefined;
|
|
1923
1942
|
};
|
|
1924
1943
|
const onItem = (a, item) => {
|
|
1944
|
+
// 新 spawn 的 pane 在首次 inject 落地前吞掉所有初始输出 (greeting/system)。
|
|
1945
|
+
if (a.muteUntilInject)
|
|
1946
|
+
return;
|
|
1925
1947
|
// Keepalive ping turns are cache-warmers: swallow every item from the WeCom
|
|
1926
1948
|
// paths so the ping/pong never reaches chat. But record the REAL exchange
|
|
1927
1949
|
// into its chat-detail turn — the actual assistant reply (expected: just
|
|
@@ -2216,8 +2238,10 @@ export const startMirror = (deps) => {
|
|
|
2216
2238
|
// Click-to-detail URL for a tool_use id. Cached on the bridge so both
|
|
2217
2239
|
// attach() and migrateAttachment() share the same closure. Returns ""
|
|
2218
2240
|
// when disabled in config — renderLine then drops the markdown wrapping.
|
|
2219
|
-
|
|
2220
|
-
|
|
2241
|
+
// `principal` flows through to ww_uniq so all detail links from the same
|
|
2242
|
+
// chat reuse one WeCom inner-browser window.
|
|
2243
|
+
const detailUrlFor = (id, principal) => cfg.daemon.detailLinksInMirror && id
|
|
2244
|
+
? buildDetailUrl(cfg.daemon.detailPublicBase, cfg.daemon.host, cfg.daemon.port, id, principal ? stripPrincipalPrefix(principal) : undefined)
|
|
2221
2245
|
: "";
|
|
2222
2246
|
const attach = ({ sessionId, jsonlPath, target: targetOverride, tmuxPane, tmuxSession, cwd, pendingCwd }) => {
|
|
2223
2247
|
const target = resolveTarget(targetOverride);
|
|
@@ -2601,8 +2625,16 @@ export const startMirror = (deps) => {
|
|
|
2601
2625
|
// 迁移是所有会话轮换的唯一漏斗 (注入的 /clear、TUI 里手打的 /clear、resume fork、
|
|
2602
2626
|
// worktree drift)。只有目标 jsonl 首条用户行是 /clear 的那种才是真清空 —— fork /
|
|
2603
2627
|
// drift 上下文是延续的, 让 store 去推它那条中性的 "switch"。
|
|
2604
|
-
if (jsonlIsPostClearChild(newJsonlPath))
|
|
2628
|
+
if (jsonlIsPostClearChild(newJsonlPath)) {
|
|
2605
2629
|
a.ctxCut = "clear";
|
|
2630
|
+
// 真清空 = 缓存里已无任何值得保温的内容。像 /stop 一样暂停保活 —— 与
|
|
2631
|
+
// dispatch 里 WeCom 注入 /clear 的暂停对齐;TUI 手打 /clear 只走这个漏斗,
|
|
2632
|
+
// 不在这里停下的话旧时钟会继续 ping 一个空上下文。busy-resume(过 grace)
|
|
2633
|
+
// 或真实 inbound 解除。下方 store.set 一并落盘。
|
|
2634
|
+
a.keepaliveOff = true;
|
|
2635
|
+
a.keepaliveOffAt = Date.now();
|
|
2636
|
+
a.keepalive = undefined;
|
|
2637
|
+
}
|
|
2606
2638
|
a.tail.stop();
|
|
2607
2639
|
bySessionId.delete(oldSessionId);
|
|
2608
2640
|
a.sessionId = newSessionId;
|
|
@@ -2634,6 +2666,10 @@ export const startMirror = (deps) => {
|
|
|
2634
2666
|
tmuxPane: a.tmuxPane || undefined,
|
|
2635
2667
|
cwd: a.runningCwd || undefined,
|
|
2636
2668
|
pendingCwd: a.pendingCwd || undefined,
|
|
2669
|
+
// store.set 是整记录替换 —— 必须带上暂停态,否则迁移会把 dispatch 刚落盘
|
|
2670
|
+
// 的 /clear 暂停(或之前的 /stop 暂停)从盘上抹掉,reload 后保活复活。
|
|
2671
|
+
keepaliveOff: a.keepaliveOff || undefined,
|
|
2672
|
+
keepaliveOffAt: a.keepaliveOffAt || undefined,
|
|
2637
2673
|
});
|
|
2638
2674
|
log.info({ target: a.target, oldSessionId, newSessionId, oldJsonlPath, newJsonlPath, startOffset }, "mirror migrated session");
|
|
2639
2675
|
};
|
|
@@ -2994,6 +3030,7 @@ export const startMirror = (deps) => {
|
|
|
2994
3030
|
const spawned = byTarget.get(target);
|
|
2995
3031
|
if (spawned) {
|
|
2996
3032
|
spawned.justSpawned = true;
|
|
3033
|
+
spawned.muteUntilInject = true;
|
|
2997
3034
|
// 只有换过 pane 才是断点 —— 首次 /wrc 建会话时 prev 为空, 那不是"不连续", 是开局。
|
|
2998
3035
|
if (prev)
|
|
2999
3036
|
spawned.ctxCut = "new";
|
|
@@ -3201,11 +3238,17 @@ export const startMirror = (deps) => {
|
|
|
3201
3238
|
};
|
|
3202
3239
|
const fireKeepalive = async (a) => {
|
|
3203
3240
|
const kc = cfg.wrc.mirror.keepalive;
|
|
3241
|
+
// Only the streak's FIRST ping carries the full instruction; consecutive
|
|
3242
|
+
// pings shrink to a bare "ping" — the instruction is already in context,
|
|
3243
|
+
// and the smaller delta means a smaller cache-write and less pane clutter.
|
|
3244
|
+
// keepaliveStamps matches both forms, so the bare one never reads as real
|
|
3245
|
+
// activity. (round is incremented below, after the inject lands.)
|
|
3246
|
+
const text = (a.keepalive?.round ?? 0) > 0 ? "ping" : kc.ping;
|
|
3204
3247
|
// Suppress the pane→WeCom echo of the ping user line, then swallow the whole
|
|
3205
3248
|
// ping turn (reply included). The 60s fail-safe clears the quiet window if
|
|
3206
3249
|
// the turn somehow never emits turn_end, so a later real turn is never muted;
|
|
3207
3250
|
// it also closes the detail turn so it can't hang open in the chat timeline.
|
|
3208
|
-
rememberInject(
|
|
3251
|
+
rememberInject(text);
|
|
3209
3252
|
a.keepaliveQuiet = setTimeout(() => {
|
|
3210
3253
|
a.keepaliveQuiet = undefined;
|
|
3211
3254
|
if (a.keepaliveTurnId) {
|
|
@@ -3214,7 +3257,7 @@ export const startMirror = (deps) => {
|
|
|
3214
3257
|
}
|
|
3215
3258
|
}, 60_000);
|
|
3216
3259
|
const r = await inject({
|
|
3217
|
-
text
|
|
3260
|
+
text, images: [], cfg,
|
|
3218
3261
|
log: log.child({ target: a.target, sessionId: a.sessionId, sub: "keepalive" }),
|
|
3219
3262
|
sessionId: a.sessionId, jsonlPath: a.jsonlPath, tmuxTarget: a.tmuxPane,
|
|
3220
3263
|
});
|
|
@@ -3227,11 +3270,7 @@ export const startMirror = (deps) => {
|
|
|
3227
3270
|
}
|
|
3228
3271
|
const tokens = lastContextTokens(a.jsonlPath);
|
|
3229
3272
|
const size = tokens > 0 ? `~${fmtTokens(tokens)} tokens` : "未知";
|
|
3230
|
-
|
|
3231
|
-
// not the nominal ttlSec — pings fire marginSec early, so ttlSec would undercount
|
|
3232
|
-
// and let `n` exceed `N` (a 9/8). floor keeps the last shown round the last one that fires.
|
|
3233
|
-
const cadenceSec = Math.max(30, kc.ttlSec - kc.marginSec);
|
|
3234
|
-
const totalRounds = Math.max(1, Math.floor(kc.maxIdleSec / cadenceSec));
|
|
3273
|
+
const totalRounds = Math.max(1, kc.rounds);
|
|
3235
3274
|
const round = a.keepalive ? (a.keepalive.round += 1) : 1;
|
|
3236
3275
|
log.info({ target: a.target, tokens, round, totalRounds }, "keepalive: ping injected");
|
|
3237
3276
|
// Open a chat-detail turn for the real heartbeat exchange: userQuery is the
|
|
@@ -3240,7 +3279,7 @@ export const startMirror = (deps) => {
|
|
|
3240
3279
|
// closed on the ping's turn_end (or the fail-safe). Kept out of chat.
|
|
3241
3280
|
const turnId = newTurnId();
|
|
3242
3281
|
a.keepaliveTurnId = turnId;
|
|
3243
|
-
recordTurnStart({ id: turnId, target: a.target, sessionId: a.sessionId, userQuery:
|
|
3282
|
+
recordTurnStart({ id: turnId, target: a.target, sessionId: a.sessionId, userQuery: text });
|
|
3244
3283
|
if (kc.notify)
|
|
3245
3284
|
sendStandalone(a, `KeepAlive ${round}/${totalRounds} · ${size}`);
|
|
3246
3285
|
};
|
|
@@ -3253,7 +3292,6 @@ export const startMirror = (deps) => {
|
|
|
3253
3292
|
try {
|
|
3254
3293
|
const idleTriggerMs = Math.max(30, kc.ttlSec - kc.marginSec) * 1000;
|
|
3255
3294
|
const ttlMs = kc.ttlSec * 1000;
|
|
3256
|
-
const maxIdleMs = Math.max(kc.ttlSec, kc.maxIdleSec) * 1000;
|
|
3257
3295
|
const pingSig = normAssistant(kc.ping).slice(0, 40);
|
|
3258
3296
|
const now = Date.now();
|
|
3259
3297
|
for (const a of byTarget.values()) {
|
|
@@ -3322,13 +3360,12 @@ export const startMirror = (deps) => {
|
|
|
3322
3360
|
if (a.keepaliveOff)
|
|
3323
3361
|
continue; // paused by /stop until real activity returns
|
|
3324
3362
|
const idleSinceTouch = k.lastMs ? now - k.lastMs : now - mtime; // last model turn = cache touch
|
|
3325
|
-
const realIdle = k.lastRealMs ? now - k.lastRealMs : Infinity; // no real turn in window ⇒ dead
|
|
3326
3363
|
if (idleSinceTouch < idleTriggerMs)
|
|
3327
3364
|
continue; // cache still comfortably warm
|
|
3328
3365
|
if (idleSinceTouch >= ttlMs)
|
|
3329
3366
|
continue; // cache already cold — a ping would cold-rewrite for nothing
|
|
3330
|
-
if (
|
|
3331
|
-
continue; //
|
|
3367
|
+
if (k.round >= kc.rounds)
|
|
3368
|
+
continue; // budget spent — let it go cold
|
|
3332
3369
|
k.pinging = true;
|
|
3333
3370
|
k.pingMtime = k.lastMs; // settles when a newer turn (the ping's own) appears
|
|
3334
3371
|
await fireKeepalive(a);
|
|
@@ -3823,6 +3860,7 @@ export const startMirror = (deps) => {
|
|
|
3823
3860
|
sessionId: sid, jsonlPath: a.jsonlPath, tmuxTarget: a.tmuxPane, freshSpawn,
|
|
3824
3861
|
});
|
|
3825
3862
|
if (!r.ok) {
|
|
3863
|
+
a.muteUntilInject = false;
|
|
3826
3864
|
if (s) {
|
|
3827
3865
|
s.acc = s.acc ? `${s.acc}\n\n[mirror] ✗ ${r.reason ?? "failed"}` : `[mirror] ✗ ${r.reason ?? "failed"}`;
|
|
3828
3866
|
await finalizeStream(a, s);
|
|
@@ -3846,6 +3884,8 @@ export const startMirror = (deps) => {
|
|
|
3846
3884
|
}
|
|
3847
3885
|
return;
|
|
3848
3886
|
}
|
|
3887
|
+
// inject 成功 → 解除初始输出静默, 后续 onItem 正常流转。
|
|
3888
|
+
a.muteUntilInject = false;
|
|
3849
3889
|
// Inject "succeeded" but the input box never cleared — the target
|
|
3850
3890
|
// session may be busy / not consuming input (long task, full context).
|
|
3851
3891
|
// Hint the user once so a silently-dropped message isn't mistaken for a
|