wezard 1.2.9 → 1.2.10
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 +43 -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 +1 -1
|
@@ -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,
|
|
@@ -1647,7 +1646,8 @@ export const startMirror = (deps) => {
|
|
|
1647
1646
|
const BRIEF_TURN_OPENERS = new Set(["text", "tool_use", "tool_result", "skill_output"]);
|
|
1648
1647
|
// 链接落到 chat 视图: 默认选中本 turn 所属的 #tag, 贴底显示整条会话。turnId 依旧
|
|
1649
1648
|
// 是凭据 (不可枚举), 只是页面从"一个 turn"扩成"这个 chat 的全部会话"。
|
|
1650
|
-
|
|
1649
|
+
// target 作为 ww_uniq 传下去, 让同 chat 的所有 turn 详情都复用一个 WeCom 窗口。
|
|
1650
|
+
const briefDetailLink = (turnId, target) => `✴️ [View chat details](${buildChatUrl(cfg.daemon.detailPublicBase, cfg.daemon.host, cfg.daemon.port, turnId, stripPrincipalPrefix(target))})`;
|
|
1651
1651
|
// 收口一条 loading 气泡: finish=true 写入最终内容, 只生效一次。发送失败退回 standalone。
|
|
1652
1652
|
// 排队中的 turn 也持有气泡, 所以气泡是显式入参而不是从 a 上取。
|
|
1653
1653
|
const finishBubble = async (a, b, content) => {
|
|
@@ -1700,7 +1700,7 @@ export const startMirror = (deps) => {
|
|
|
1700
1700
|
log.warn({ sessionId: a.sessionId, turnId }, "brief: queued turn timed out — force-closing the stuck one");
|
|
1701
1701
|
closeBriefTurn(a);
|
|
1702
1702
|
}
|
|
1703
|
-
void finishBubble(a, bubble, briefDetailLink(turnId));
|
|
1703
|
+
void finishBubble(a, bubble, briefDetailLink(turnId, a.target));
|
|
1704
1704
|
}, HARD_TIMEOUT_MS);
|
|
1705
1705
|
if (a.briefTurnId) {
|
|
1706
1706
|
(a.briefQueue ??= []).push(q);
|
|
@@ -1733,7 +1733,7 @@ export const startMirror = (deps) => {
|
|
|
1733
1733
|
a.briefHadTool = false;
|
|
1734
1734
|
a.briefConcluded = false;
|
|
1735
1735
|
a.briefLastText = undefined;
|
|
1736
|
-
enqueueStandalone(a, briefDetailLink(turnId));
|
|
1736
|
+
enqueueStandalone(a, briefDetailLink(turnId, a.target));
|
|
1737
1737
|
log.info({ sessionId: a.sessionId, turnId }, "brief: turn started (CLI-side, no bubble)");
|
|
1738
1738
|
};
|
|
1739
1739
|
// 本轮一旦出现工具调用 / tool_result / 非 final 文本, 就说明这不是"一句话答完"的
|
|
@@ -1744,7 +1744,7 @@ export const startMirror = (deps) => {
|
|
|
1744
1744
|
if (!a.briefTurnId || !a.briefBubble || a.briefBubble.done)
|
|
1745
1745
|
return;
|
|
1746
1746
|
a.briefHadTool = true;
|
|
1747
|
-
void finishBriefBubble(a, briefDetailLink(a.briefTurnId));
|
|
1747
|
+
void finishBriefBubble(a, briefDetailLink(a.briefTurnId, a.target));
|
|
1748
1748
|
};
|
|
1749
1749
|
// 本轮结论落地, 只生效一次:
|
|
1750
1750
|
// • 无工具 → 结论 + 详情链接一并写进 loading 气泡 (仍是一条消息);
|
|
@@ -1758,7 +1758,7 @@ export const startMirror = (deps) => {
|
|
|
1758
1758
|
if (a.briefHadTool || !a.briefBubble)
|
|
1759
1759
|
sendStandalone(a, body);
|
|
1760
1760
|
else
|
|
1761
|
-
void finishBriefBubble(a, `${body}\n\n${briefDetailLink(turnId)}`);
|
|
1761
|
+
void finishBriefBubble(a, `${body}\n\n${briefDetailLink(turnId, a.target)}`);
|
|
1762
1762
|
// 排队中的 turn 已经建了记录但还没跑, 不能被这把扫帚扫成「已完成」。
|
|
1763
1763
|
const keep = [turnId, ...(a.briefQueue ?? []).map((q) => q.turnId)];
|
|
1764
1764
|
recordCloseOpenTurns({ target: a.target, sessionId: a.sessionId, exceptIds: keep });
|
|
@@ -1773,7 +1773,7 @@ export const startMirror = (deps) => {
|
|
|
1773
1773
|
// 气泡还开着 (有工具的 turn: 结论只发了 standalone, 气泡留到这里收链接; 或空 turn
|
|
1774
1774
|
// 没有结论) —— 收口: 统一写详情链接, 保证每个 turn 都有可点入口。
|
|
1775
1775
|
if (a.briefBubble && !a.briefBubble.done) {
|
|
1776
|
-
void finishBriefBubble(a, briefDetailLink(a.briefTurnId));
|
|
1776
|
+
void finishBriefBubble(a, briefDetailLink(a.briefTurnId, a.target));
|
|
1777
1777
|
}
|
|
1778
1778
|
recordTurnClose(a.briefTurnId);
|
|
1779
1779
|
log.info({ sessionId: a.sessionId, turnId: a.briefTurnId }, "brief: turn closed");
|
|
@@ -2216,8 +2216,10 @@ export const startMirror = (deps) => {
|
|
|
2216
2216
|
// Click-to-detail URL for a tool_use id. Cached on the bridge so both
|
|
2217
2217
|
// attach() and migrateAttachment() share the same closure. Returns ""
|
|
2218
2218
|
// when disabled in config — renderLine then drops the markdown wrapping.
|
|
2219
|
-
|
|
2220
|
-
|
|
2219
|
+
// `principal` flows through to ww_uniq so all detail links from the same
|
|
2220
|
+
// chat reuse one WeCom inner-browser window.
|
|
2221
|
+
const detailUrlFor = (id, principal) => cfg.daemon.detailLinksInMirror && id
|
|
2222
|
+
? buildDetailUrl(cfg.daemon.detailPublicBase, cfg.daemon.host, cfg.daemon.port, id, principal ? stripPrincipalPrefix(principal) : undefined)
|
|
2221
2223
|
: "";
|
|
2222
2224
|
const attach = ({ sessionId, jsonlPath, target: targetOverride, tmuxPane, tmuxSession, cwd, pendingCwd }) => {
|
|
2223
2225
|
const target = resolveTarget(targetOverride);
|
|
@@ -2601,8 +2603,16 @@ export const startMirror = (deps) => {
|
|
|
2601
2603
|
// 迁移是所有会话轮换的唯一漏斗 (注入的 /clear、TUI 里手打的 /clear、resume fork、
|
|
2602
2604
|
// worktree drift)。只有目标 jsonl 首条用户行是 /clear 的那种才是真清空 —— fork /
|
|
2603
2605
|
// drift 上下文是延续的, 让 store 去推它那条中性的 "switch"。
|
|
2604
|
-
if (jsonlIsPostClearChild(newJsonlPath))
|
|
2606
|
+
if (jsonlIsPostClearChild(newJsonlPath)) {
|
|
2605
2607
|
a.ctxCut = "clear";
|
|
2608
|
+
// 真清空 = 缓存里已无任何值得保温的内容。像 /stop 一样暂停保活 —— 与
|
|
2609
|
+
// dispatch 里 WeCom 注入 /clear 的暂停对齐;TUI 手打 /clear 只走这个漏斗,
|
|
2610
|
+
// 不在这里停下的话旧时钟会继续 ping 一个空上下文。busy-resume(过 grace)
|
|
2611
|
+
// 或真实 inbound 解除。下方 store.set 一并落盘。
|
|
2612
|
+
a.keepaliveOff = true;
|
|
2613
|
+
a.keepaliveOffAt = Date.now();
|
|
2614
|
+
a.keepalive = undefined;
|
|
2615
|
+
}
|
|
2606
2616
|
a.tail.stop();
|
|
2607
2617
|
bySessionId.delete(oldSessionId);
|
|
2608
2618
|
a.sessionId = newSessionId;
|
|
@@ -2634,6 +2644,10 @@ export const startMirror = (deps) => {
|
|
|
2634
2644
|
tmuxPane: a.tmuxPane || undefined,
|
|
2635
2645
|
cwd: a.runningCwd || undefined,
|
|
2636
2646
|
pendingCwd: a.pendingCwd || undefined,
|
|
2647
|
+
// store.set 是整记录替换 —— 必须带上暂停态,否则迁移会把 dispatch 刚落盘
|
|
2648
|
+
// 的 /clear 暂停(或之前的 /stop 暂停)从盘上抹掉,reload 后保活复活。
|
|
2649
|
+
keepaliveOff: a.keepaliveOff || undefined,
|
|
2650
|
+
keepaliveOffAt: a.keepaliveOffAt || undefined,
|
|
2637
2651
|
});
|
|
2638
2652
|
log.info({ target: a.target, oldSessionId, newSessionId, oldJsonlPath, newJsonlPath, startOffset }, "mirror migrated session");
|
|
2639
2653
|
};
|
|
@@ -3201,11 +3215,17 @@ export const startMirror = (deps) => {
|
|
|
3201
3215
|
};
|
|
3202
3216
|
const fireKeepalive = async (a) => {
|
|
3203
3217
|
const kc = cfg.wrc.mirror.keepalive;
|
|
3218
|
+
// Only the streak's FIRST ping carries the full instruction; consecutive
|
|
3219
|
+
// pings shrink to a bare "ping" — the instruction is already in context,
|
|
3220
|
+
// and the smaller delta means a smaller cache-write and less pane clutter.
|
|
3221
|
+
// keepaliveStamps matches both forms, so the bare one never reads as real
|
|
3222
|
+
// activity. (round is incremented below, after the inject lands.)
|
|
3223
|
+
const text = (a.keepalive?.round ?? 0) > 0 ? "ping" : kc.ping;
|
|
3204
3224
|
// Suppress the pane→WeCom echo of the ping user line, then swallow the whole
|
|
3205
3225
|
// ping turn (reply included). The 60s fail-safe clears the quiet window if
|
|
3206
3226
|
// the turn somehow never emits turn_end, so a later real turn is never muted;
|
|
3207
3227
|
// it also closes the detail turn so it can't hang open in the chat timeline.
|
|
3208
|
-
rememberInject(
|
|
3228
|
+
rememberInject(text);
|
|
3209
3229
|
a.keepaliveQuiet = setTimeout(() => {
|
|
3210
3230
|
a.keepaliveQuiet = undefined;
|
|
3211
3231
|
if (a.keepaliveTurnId) {
|
|
@@ -3214,7 +3234,7 @@ export const startMirror = (deps) => {
|
|
|
3214
3234
|
}
|
|
3215
3235
|
}, 60_000);
|
|
3216
3236
|
const r = await inject({
|
|
3217
|
-
text
|
|
3237
|
+
text, images: [], cfg,
|
|
3218
3238
|
log: log.child({ target: a.target, sessionId: a.sessionId, sub: "keepalive" }),
|
|
3219
3239
|
sessionId: a.sessionId, jsonlPath: a.jsonlPath, tmuxTarget: a.tmuxPane,
|
|
3220
3240
|
});
|
|
@@ -3227,11 +3247,7 @@ export const startMirror = (deps) => {
|
|
|
3227
3247
|
}
|
|
3228
3248
|
const tokens = lastContextTokens(a.jsonlPath);
|
|
3229
3249
|
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));
|
|
3250
|
+
const totalRounds = Math.max(1, kc.rounds);
|
|
3235
3251
|
const round = a.keepalive ? (a.keepalive.round += 1) : 1;
|
|
3236
3252
|
log.info({ target: a.target, tokens, round, totalRounds }, "keepalive: ping injected");
|
|
3237
3253
|
// Open a chat-detail turn for the real heartbeat exchange: userQuery is the
|
|
@@ -3240,7 +3256,7 @@ export const startMirror = (deps) => {
|
|
|
3240
3256
|
// closed on the ping's turn_end (or the fail-safe). Kept out of chat.
|
|
3241
3257
|
const turnId = newTurnId();
|
|
3242
3258
|
a.keepaliveTurnId = turnId;
|
|
3243
|
-
recordTurnStart({ id: turnId, target: a.target, sessionId: a.sessionId, userQuery:
|
|
3259
|
+
recordTurnStart({ id: turnId, target: a.target, sessionId: a.sessionId, userQuery: text });
|
|
3244
3260
|
if (kc.notify)
|
|
3245
3261
|
sendStandalone(a, `KeepAlive ${round}/${totalRounds} · ${size}`);
|
|
3246
3262
|
};
|
|
@@ -3253,7 +3269,6 @@ export const startMirror = (deps) => {
|
|
|
3253
3269
|
try {
|
|
3254
3270
|
const idleTriggerMs = Math.max(30, kc.ttlSec - kc.marginSec) * 1000;
|
|
3255
3271
|
const ttlMs = kc.ttlSec * 1000;
|
|
3256
|
-
const maxIdleMs = Math.max(kc.ttlSec, kc.maxIdleSec) * 1000;
|
|
3257
3272
|
const pingSig = normAssistant(kc.ping).slice(0, 40);
|
|
3258
3273
|
const now = Date.now();
|
|
3259
3274
|
for (const a of byTarget.values()) {
|
|
@@ -3322,13 +3337,12 @@ export const startMirror = (deps) => {
|
|
|
3322
3337
|
if (a.keepaliveOff)
|
|
3323
3338
|
continue; // paused by /stop until real activity returns
|
|
3324
3339
|
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
3340
|
if (idleSinceTouch < idleTriggerMs)
|
|
3327
3341
|
continue; // cache still comfortably warm
|
|
3328
3342
|
if (idleSinceTouch >= ttlMs)
|
|
3329
3343
|
continue; // cache already cold — a ping would cold-rewrite for nothing
|
|
3330
|
-
if (
|
|
3331
|
-
continue; //
|
|
3344
|
+
if (k.round >= kc.rounds)
|
|
3345
|
+
continue; // budget spent — let it go cold
|
|
3332
3346
|
k.pinging = true;
|
|
3333
3347
|
k.pingMtime = k.lastMs; // settles when a newer turn (the ping's own) appears
|
|
3334
3348
|
await fireKeepalive(a);
|