wezard 2.0.6 → 2.1.1
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/CLAUDE.md +2 -2
- package/dist/daemon/chat-log.js +107 -0
- package/dist/daemon/chat-log.js.map +1 -0
- package/dist/daemon/chat-name.js +1 -1
- package/dist/daemon/detail.js +7 -0
- package/dist/daemon/detail.js.map +1 -1
- package/dist/daemon/inbound.js +10 -3
- package/dist/daemon/inbound.js.map +1 -1
- package/dist/daemon/index.js +125 -22
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/mirror-bridge.js +81 -44
- package/dist/daemon/mirror-bridge.js.map +1 -1
- package/dist/daemon/notices.js +3 -5
- package/dist/daemon/notices.js.map +1 -1
- package/dist/daemon/peers.js +260 -125
- package/dist/daemon/peers.js.map +1 -1
- package/dist/daemon/session-name.js +1 -1
- package/dist/daemon/session-name.js.map +1 -1
- package/dist/daemon/wizard.js +33 -2
- package/dist/daemon/wizard.js.map +1 -1
- package/dist/mcp/server.js +34 -12
- package/dist/mcp/server.js.map +1 -1
- package/dist/shared/chat-http.js +25 -10
- package/dist/shared/chat-http.js.map +1 -1
- package/dist/shared/chat-render.js +17 -7
- package/dist/shared/chat-render.js.map +1 -1
- package/dist/shared/config.js +6 -6
- package/dist/shared/config.js.map +1 -1
- package/dist/shared/detail-render.js +22 -5
- package/dist/shared/detail-render.js.map +1 -1
- package/dist/shared/reminder.js +56 -0
- package/dist/shared/reminder.js.map +1 -0
- package/dist/shared/role-render.js +6 -3
- package/dist/shared/role-render.js.map +1 -1
- package/dist/shared/role-view.js +3 -3
- package/dist/shared/role-view.js.map +1 -1
- package/dist/shared/session-label.js +9 -3
- package/dist/shared/session-label.js.map +1 -1
- package/hooks/pre-tool-use.sh +1 -1
- package/package.json +4 -1
- package/web/chat.css +15 -11
- package/web/chat.js +38 -5
- package/web/highlight.min.js +1262 -0
- package/web/hljs-github.min.css +10 -0
- package/web/markdown-it.min.js +2 -0
|
@@ -29,12 +29,12 @@ import { selectModel } from "./model-select.js";
|
|
|
29
29
|
import { hasRegistry, markTranscript, probeOf, sessionOnPane, sessionPanes, submittedSince } from "./cc-session.js";
|
|
30
30
|
import { wizardStore } from "./wizard.js";
|
|
31
31
|
import { startSubagentWatch } from "./subagent-tail.js";
|
|
32
|
-
import { recordTool, recordToolResult, recordMark, recordTurnStart, recordTurnItem, recordTurnUsage, recordTurnClose, recordCloseOpenTurns, buildDetailUrl, buildChatUrl } from "./detail.js";
|
|
32
|
+
import { recordTool, recordToolResult, recordMark, recordTurnStart, recordTurnItem, recordTurnUsage, recordTurnClose, recordCloseOpenTurns, lastChannelOf, buildDetailUrl, buildChatUrl } from "./detail.js";
|
|
33
33
|
import { labelFor, tagOfKey, baseOfKey, keyOf, stripSigil, displayName, withTagHeader, withLinkedTagHeader, linkedTagHead, linkTags, parseTagHeader, MAX_BODY_LINKS } from "../shared/session-label.js";
|
|
34
34
|
import { splitMarkdown } from "../shared/md-chunk.js";
|
|
35
35
|
import { randomTip } from "./tips.js";
|
|
36
36
|
import { chatBaseOf, chatNameOf, listChatNames, parsePeerRef, peerAddress } from "./chat-name.js";
|
|
37
|
-
import { stripAnsi,
|
|
37
|
+
import { stripAnsi, paneIsBusy, transcriptStalled, summarizeTail, lastAssistantText, lastReply, unwrapPasted, lastContextTokens, keepaliveStamps, openKeepalivePing, keepalivePingSigs, isKeepalivePingText, talkRounds, openToolUses, renderDialog } from "./peers.js";
|
|
38
38
|
// PATH augmentation: launchd / systemd start the daemon
|
|
39
39
|
// with a stripped PATH that often lacks nvm / homebrew, breaking spawn(claudeBin).
|
|
40
40
|
// Backend resolution is per-transcript, not global: `backendForPath` recovers
|
|
@@ -686,8 +686,9 @@ const renderLine = (raw, deps) => {
|
|
|
686
686
|
// fenced block or table in a way that breaks rendering — see splitMarkdown.
|
|
687
687
|
// Room reserved in every chunk for the `emoji \`#tag\` \`2/5\`` header line and
|
|
688
688
|
// the chat-detail links linkTags hangs on it and on up to MAX_BODY_LINKS body
|
|
689
|
-
// mentions — each `(url)` runs ~200-250 bytes.
|
|
690
|
-
|
|
689
|
+
// mentions — each `(url)` runs ~200-250 bytes. 头里最多两条: 自己的, 以及公开 peer 轮
|
|
690
|
+
// 的 `→ .对方`。
|
|
691
|
+
const TAG_HEADER_BUDGET = 256 * (2 + MAX_BODY_LINKS);
|
|
691
692
|
export const startMirrorTail = (deps) => {
|
|
692
693
|
const { log } = deps;
|
|
693
694
|
// A single logical tail follows ONE sessionId whose transcript file may move
|
|
@@ -1669,13 +1670,11 @@ export const startMirror = (deps) => {
|
|
|
1669
1670
|
// renderLine, the tick and the transcript parser all agree on what counts
|
|
1670
1671
|
// as a ping. Content is the source of truth for the swallow: timers and
|
|
1671
1672
|
// echo-TTLs lose it across reloads/replays.
|
|
1672
|
-
const
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
const
|
|
1676
|
-
|
|
1677
|
-
return n.toLowerCase() === "ping" || keepalivePingSigs.some((sig) => n.includes(sig));
|
|
1678
|
-
};
|
|
1673
|
+
const pingSigs = keepalivePingSigs(cfg.wrc.mirror.keepalive.ping, cfg.wrc.mirror.keepalive.resumePing);
|
|
1674
|
+
// The warmer alone — what the peer readers (peek / wait / roster summary) strip.
|
|
1675
|
+
// `resumePing` is excluded on purpose: the turn it opens is real work resumed.
|
|
1676
|
+
const warmerSigs = keepalivePingSigs(cfg.wrc.mirror.keepalive.ping);
|
|
1677
|
+
const isKeepalivePing = (text) => isKeepalivePingText(text, pingSigs);
|
|
1679
1678
|
// ── Typewriter stream lifecycle ────────────────────────────────────
|
|
1680
1679
|
// WeCom spec: server polls us for stream refreshes for up to 6 min from the
|
|
1681
1680
|
// original inbound. SDK queues replyStream calls per req_id, sends serially
|
|
@@ -1831,7 +1830,8 @@ export const startMirror = (deps) => {
|
|
|
1831
1830
|
const chatTurnUrl = (target, turnId) => turnId
|
|
1832
1831
|
? buildChatUrl(cfg.daemon.detailPublicBase, cfg.daemon.host, cfg.daemon.port, turnId, stripPrincipalPrefix(target))
|
|
1833
1832
|
: undefined;
|
|
1834
|
-
|
|
1833
|
+
// `[mirror]` / `[wezard]` 系统提示不是答给谁的话, 不标方向。
|
|
1834
|
+
const withLinkedTag = (a, content, seq, turnId) => withLinkedTagHeader(a.target, content, chatTurnUrl(a.target, turnId ?? a.briefTurnId), seq, /^\[(mirror|wezard)\]/.test(content.trim()) ? undefined : a.replyTo);
|
|
1835
1835
|
// 空正文一票否决 (近源拦截): 剥掉可能存在的路由头 (`🦊 #tag` / `[🧙](url)`)
|
|
1836
1836
|
// 后没有可见内容就不发。中央 chat-gate (last-response 的 SDK 包装) 是最后一道
|
|
1837
1837
|
// 防线; 这里拦在源头 —— 空内容不进 standalonePending FIFO、不占防抖 buf、
|
|
@@ -2262,18 +2262,40 @@ export const startMirror = (deps) => {
|
|
|
2262
2262
|
a.pendingFrom = undefined;
|
|
2263
2263
|
return p && Date.now() - p.at <= ORIGIN_TTL_MS ? p.from : undefined;
|
|
2264
2264
|
};
|
|
2265
|
+
/** 公开派活的那个同伴 —— 回复的头要标给它 (见 AttachState.replyTo)。 */
|
|
2266
|
+
const publicPeer = (from) => from?.kind === "peer" && from.public ? from.from : undefined;
|
|
2265
2267
|
/** 频道印章: 有新印章就换频道; 没有则 `reset` (CLI 手敲 / 人从聊天发起) 回到
|
|
2266
|
-
* home, 其余续写 (收口后的补写)
|
|
2267
|
-
const consumeChannel = (a, reset) => {
|
|
2268
|
+
* home, 其余续写 (收口后的补写) 沿用上一轮。`from` = 本轮的出处, 答给谁跟着频道走。 */
|
|
2269
|
+
const consumeChannel = (a, reset, from) => {
|
|
2268
2270
|
const p = a.pendingChannel;
|
|
2269
2271
|
a.pendingChannel = undefined;
|
|
2272
|
+
const seeded = a.channelSeeded;
|
|
2273
|
+
a.channelSeeded = true;
|
|
2270
2274
|
if (p && Date.now() - p.at <= ORIGIN_TTL_MS) {
|
|
2271
2275
|
a.channel = p.channel === baseOfKey(a.target) ? undefined : p.channel;
|
|
2272
2276
|
a.speaker = p.speaker;
|
|
2277
|
+
a.replyTo = publicPeer(from);
|
|
2278
|
+
a.convFrom = from;
|
|
2273
2279
|
}
|
|
2274
2280
|
else if (reset) {
|
|
2275
2281
|
a.channel = undefined;
|
|
2276
2282
|
a.speaker = undefined;
|
|
2283
|
+
a.replyTo = undefined;
|
|
2284
|
+
a.convFrom = undefined;
|
|
2285
|
+
}
|
|
2286
|
+
else if (from) {
|
|
2287
|
+
a.convFrom = from;
|
|
2288
|
+
}
|
|
2289
|
+
else if (!seeded) {
|
|
2290
|
+
// reload 后的第一轮续写: 内存里的频道随进程没了, 缺省 = home 会把一段私聊的
|
|
2291
|
+
// 后续记成群里公开说的 (rolepage 上私聊窗口从此不再更新)。接回最近一轮的印章。
|
|
2292
|
+
const last = lastChannelOf(a.target);
|
|
2293
|
+
if (last) {
|
|
2294
|
+
a.channel = last.channel === baseOfKey(a.target) ? undefined : last.channel;
|
|
2295
|
+
a.speaker = last.speaker;
|
|
2296
|
+
a.replyTo = publicPeer(last.from);
|
|
2297
|
+
a.convFrom = last.from;
|
|
2298
|
+
}
|
|
2277
2299
|
}
|
|
2278
2300
|
};
|
|
2279
2301
|
/** 本轮落 turn 记录的频道字段。 */
|
|
@@ -2292,8 +2314,9 @@ export const startMirror = (deps) => {
|
|
|
2292
2314
|
a.queryEpoch = (a.queryEpoch ?? 0) + 1; // WeCom 侧的新一轮同样是 query 边界
|
|
2293
2315
|
a.turnFromChat = true; // 出处确凿: 这一轮有 frame, 群里就是它的主场
|
|
2294
2316
|
a.pendingFromCli = false; // 人改从聊天里说话了, 之前那条 CLI 输入不再是出处
|
|
2295
|
-
|
|
2296
|
-
|
|
2317
|
+
const from = consumeFrom(a);
|
|
2318
|
+
consumeChannel(a, true, from);
|
|
2319
|
+
recordTurnStart({ id: turnId, target: a.target, sessionId: a.sessionId, cli: cliOf(a), cwd: a.runningCwd || undefined, userQuery: userQuery.trim() || undefined, origin: consumeOrigin(a), from, ...channelFields(a) });
|
|
2297
2320
|
// hardTimer 兜底: turn 若无终句 / turn_end 收口 (卡死/漏收), 到点仍收气泡。
|
|
2298
2321
|
const bubble = { frame, streamId, hardTimer: undefined, done: false };
|
|
2299
2322
|
const q = { turnId, bubble, isSlash };
|
|
@@ -2339,8 +2362,10 @@ export const startMirror = (deps) => {
|
|
|
2339
2362
|
const fromCli = a.pendingFromCli === true;
|
|
2340
2363
|
a.pendingFromCli = false;
|
|
2341
2364
|
a.turnFromChat = fromCli ? false : a.turnFromChat ?? true;
|
|
2342
|
-
|
|
2343
|
-
|
|
2365
|
+
const fresh = consumeFrom(a);
|
|
2366
|
+
consumeChannel(a, fromCli, fresh);
|
|
2367
|
+
const from = fresh ?? a.convFrom;
|
|
2368
|
+
recordTurnStart({ id: turnId, target: a.target, sessionId: a.sessionId, cli: cliOf(a), cwd: a.runningCwd || undefined, userQuery: query || undefined, origin: consumeOrigin(a), from, ...channelFields(a) });
|
|
2344
2369
|
a.briefTurnId = turnId;
|
|
2345
2370
|
a.briefBubble = undefined;
|
|
2346
2371
|
a.briefIsSlash = false;
|
|
@@ -2896,7 +2921,7 @@ export const startMirror = (deps) => {
|
|
|
2896
2921
|
// transcript, not on the word alone: a real answer that mentions pong has a
|
|
2897
2922
|
// real question in front of it.
|
|
2898
2923
|
if (item.kind === "text" && !a.keepaliveQuiet && !a.keepaliveByContent && PONG_RE.test(item.body)) {
|
|
2899
|
-
const ping = openKeepalivePing(a.jsonlPath,
|
|
2924
|
+
const ping = openKeepalivePing(a.jsonlPath, pingSigs);
|
|
2900
2925
|
if (ping)
|
|
2901
2926
|
beginKeepaliveByContent(a, ping);
|
|
2902
2927
|
}
|
|
@@ -4499,11 +4524,11 @@ export const startMirror = (deps) => {
|
|
|
4499
4524
|
const isBusy = (target) => paneBusy(paneOf(target));
|
|
4500
4525
|
const lastText = (target) => {
|
|
4501
4526
|
const p = jsonlOf(target);
|
|
4502
|
-
return p ? lastAssistantText(p) : "";
|
|
4527
|
+
return p ? lastAssistantText(p, 4000, warmerSigs) : "";
|
|
4503
4528
|
};
|
|
4504
4529
|
const replySince = (target, sinceMs = 0) => {
|
|
4505
4530
|
const p = jsonlOf(target);
|
|
4506
|
-
return p ? lastReply(p, sinceMs) : "";
|
|
4531
|
+
return p ? lastReply(p, sinceMs, warmerSigs) : "";
|
|
4507
4532
|
};
|
|
4508
4533
|
// ── 未收口气泡的引用判定 ────────────────────────────────────────────
|
|
4509
4534
|
// tag 会话的 last stream 未收口时, 群里最新那条气泡是瞬态的: 详情链接 URL +
|
|
@@ -4565,13 +4590,13 @@ export const startMirror = (deps) => {
|
|
|
4565
4590
|
paneAlive,
|
|
4566
4591
|
busy: paneAlive ? paneIsBusy(await capturePaneTail(pane, 12)) : false,
|
|
4567
4592
|
lastActivity,
|
|
4568
|
-
summary: jsonlPath ? summarizeTail(jsonlPath) : "(未绑定会话)",
|
|
4593
|
+
summary: jsonlPath ? summarizeTail(jsonlPath, 3, 80, warmerSigs) : "(未绑定会话)",
|
|
4569
4594
|
self: t === self,
|
|
4570
4595
|
};
|
|
4571
4596
|
};
|
|
4572
4597
|
const byRecent = (x, y) => y.lastActivity - x.lastActivity;
|
|
4573
4598
|
const peers = async (target) => (await Promise.all(chatTargets(target).map((t) => peerInfoOf(t, target)))).sort(byRecent);
|
|
4574
|
-
//
|
|
4599
|
+
// 名册的跨 chat 补充: 其他 chat 里的 wizard。名字全局唯一, 所以它们
|
|
4575
4600
|
// 全都叫得动 —— 地址就是 `.name`, 与住在哪个聊天无关。
|
|
4576
4601
|
const foreignPeers = async (self) => {
|
|
4577
4602
|
const selfBase = baseOfKey(self);
|
|
@@ -4609,27 +4634,23 @@ export const startMirror = (deps) => {
|
|
|
4609
4634
|
const hot = ranked.slice(0, WORLD_PROBE).map((x) => x.t);
|
|
4610
4635
|
return (await mapLimit(hot, WORLD_CONC, (t) => peerInfoOf(t, self))).sort(byRecent);
|
|
4611
4636
|
};
|
|
4612
|
-
// Capture a few extra rows then compact away the TUI's blank padding, so
|
|
4613
|
-
// `rows` counts lines the user actually cares about.
|
|
4614
|
-
const peekPane = async (target, rows = 24) => {
|
|
4615
|
-
const pane = paneOf(target);
|
|
4616
|
-
if (!pane)
|
|
4617
|
-
return { ok: false, reason: "no tmux pane bound for target" };
|
|
4618
|
-
if (!(await tmuxPaneAlive(pane)))
|
|
4619
|
-
return { ok: false, reason: "tmux pane no longer alive — the session needs /new or a respawn" };
|
|
4620
|
-
const raw = await capturePaneTail(pane, Math.max(8, rows) + 12);
|
|
4621
|
-
return { ok: true, pane: compactPane(raw, rows), busy: paneIsBusy(raw) };
|
|
4622
|
-
};
|
|
4623
4637
|
// Reading a peer's conversation is a transcript job, not a terminal job: the
|
|
4624
4638
|
// jsonl holds whole role-tagged messages, while a pane capture is ANSI-laden
|
|
4625
|
-
// and clipped at the viewport edge.
|
|
4639
|
+
// and clipped at the viewport edge. Text turns only, keepalive stripped. Only
|
|
4640
|
+
// `busy` still comes from the pane (the spinner is the one thing a transcript
|
|
4641
|
+
// cannot say during a long tool call); "where is it stuck" is answered by the
|
|
4642
|
+
// transcript too — `waiting` = tool calls still hanging on an idle session.
|
|
4626
4643
|
const peekTurns = async (target, n = 6) => {
|
|
4627
4644
|
const jsonl = jsonlOf(target);
|
|
4628
4645
|
const busy = await isBusy(target);
|
|
4629
4646
|
if (!jsonl || !existsSync(jsonl))
|
|
4630
|
-
return { ok: false, reason: "
|
|
4631
|
-
|
|
4632
|
-
|
|
4647
|
+
return { ok: false, reason: "它还没有 transcript (没接过第一句话) —— send_peer 跟它说一句就有了", busy };
|
|
4648
|
+
// `n` 数的是来回: 每个来回给问话和它最新的一句 (跑完的是终句, 还在跑的是途中
|
|
4649
|
+
// 最近说的那句) —— 途中的其余叙述不给, 那是过程不是结论。
|
|
4650
|
+
const rounds = talkRounds(jsonl, n, warmerSigs).slice(-n);
|
|
4651
|
+
const dialog = renderDialog(rounds.flatMap((r) => (r.length > 1 ? [r[0], r.at(-1)] : r)), 800, 4000);
|
|
4652
|
+
const waiting = busy ? [] : openToolUses(jsonl);
|
|
4653
|
+
return dialog ? { ok: true, dialog, busy, waiting } : { ok: false, reason: "transcript 里还没有对话", busy, waiting };
|
|
4633
4654
|
};
|
|
4634
4655
|
// ── Prompt-cache keepalive ──────────────────────────────────────────
|
|
4635
4656
|
// Anthropic's prompt cache expires ~5min after the last request. A pane that
|
|
@@ -4652,13 +4673,18 @@ export const startMirror = (deps) => {
|
|
|
4652
4673
|
const initKeepalive = (jsonlPath, mtime, pingSigs) => {
|
|
4653
4674
|
let lastMs = 0;
|
|
4654
4675
|
let lastRealMs = 0;
|
|
4676
|
+
// round is seeded from the transcript too: starting at 0 would hand every
|
|
4677
|
+
// daemon reload a fresh budget, and a session idle across N reloads gets
|
|
4678
|
+
// N × rounds pings instead of rounds.
|
|
4679
|
+
let round = 0;
|
|
4655
4680
|
try {
|
|
4656
4681
|
const s = keepaliveStamps(jsonlPath, pingSigs);
|
|
4657
4682
|
lastMs = s.lastMs;
|
|
4658
4683
|
lastRealMs = s.lastRealMs;
|
|
4684
|
+
round = s.streak;
|
|
4659
4685
|
}
|
|
4660
4686
|
catch { /* unreadable tail */ }
|
|
4661
|
-
return { lastMs, lastRealMs, seenMtime: mtime, pinging: false, pingMtime: 0, round
|
|
4687
|
+
return { lastMs, lastRealMs, seenMtime: mtime, pinging: false, pingMtime: 0, round, settledAt: 0 };
|
|
4662
4688
|
};
|
|
4663
4689
|
const fireKeepalive = async (a, stalled) => {
|
|
4664
4690
|
const kc = cfg.wrc.mirror.keepalive;
|
|
@@ -4803,11 +4829,12 @@ export const startMirror = (deps) => {
|
|
|
4803
4829
|
continue; // cache already cold — a ping would cold-rewrite for nothing
|
|
4804
4830
|
if (k.round >= kc.rounds)
|
|
4805
4831
|
continue; // budget spent — let it go cold
|
|
4806
|
-
// Stall recovery, decided by RULE only (no model self-judgment)
|
|
4807
|
-
// transcript
|
|
4808
|
-
//
|
|
4832
|
+
// Stall recovery, decided by RULE only (no model self-judgment) and from
|
|
4833
|
+
// the transcript's structure only — never the screen: the turn is still
|
|
4834
|
+
// unfinished (died on a synthetic API error, or a tool result nobody
|
|
4835
|
+
// answered) and has been quiet for the whole idle window. Send the resume
|
|
4809
4836
|
// instruction instead of the plain warmer.
|
|
4810
|
-
const stalled = kc.resumeOnStall &&
|
|
4837
|
+
const stalled = kc.resumeOnStall && transcriptStalled(a.jsonlPath, keepalivePingSigs(kc.ping, kc.resumePing), idleTriggerMs, now);
|
|
4811
4838
|
k.pinging = true;
|
|
4812
4839
|
k.pingMtime = k.lastMs; // settles when a newer turn (the ping's own) appears
|
|
4813
4840
|
await fireKeepalive(a, stalled);
|
|
@@ -4963,7 +4990,6 @@ export const startMirror = (deps) => {
|
|
|
4963
4990
|
worldPeers,
|
|
4964
4991
|
resolvePeerTag,
|
|
4965
4992
|
chatRoster,
|
|
4966
|
-
peekPane,
|
|
4967
4993
|
peekTurns,
|
|
4968
4994
|
isBusy,
|
|
4969
4995
|
setModel: async (target, wanted, scope) => {
|
|
@@ -4979,6 +5005,17 @@ export const startMirror = (deps) => {
|
|
|
4979
5005
|
}
|
|
4980
5006
|
return r;
|
|
4981
5007
|
},
|
|
5008
|
+
transcripts: () => allTargets().flatMap((target) => {
|
|
5009
|
+
const jsonlPath = jsonlOf(target);
|
|
5010
|
+
if (!jsonlPath)
|
|
5011
|
+
return [];
|
|
5012
|
+
let mtime = 0;
|
|
5013
|
+
try {
|
|
5014
|
+
mtime = statSync(jsonlPath).mtimeMs;
|
|
5015
|
+
}
|
|
5016
|
+
catch { /* not written yet */ }
|
|
5017
|
+
return [{ target, jsonlPath, mtime }];
|
|
5018
|
+
}),
|
|
4982
5019
|
lastText,
|
|
4983
5020
|
lastReply: replySince,
|
|
4984
5021
|
isOpenBubbleQuote,
|