wezard 2.1.0 → 2.1.2
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 +1 -1
- package/dist/daemon/detail.js +13 -2
- package/dist/daemon/detail.js.map +1 -1
- package/dist/daemon/index.js +1 -1
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/mirror-bridge.js +50 -27
- 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 +117 -87
- 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/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 +24 -15
- 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 +1 -1
- package/web/chat.css +23 -13
- package/web/chat.js +126 -21
|
@@ -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, roleUniq } 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, paneIsBusy,
|
|
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,16 +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
|
-
.map((p) => normAssistant(p).slice(0, 40))
|
|
1674
|
-
.filter((s) => s.length > 0);
|
|
1673
|
+
const pingSigs = keepalivePingSigs(cfg.wrc.mirror.keepalive.ping, cfg.wrc.mirror.keepalive.resumePing);
|
|
1675
1674
|
// The warmer alone — what the peer readers (peek / wait / roster summary) strip.
|
|
1676
1675
|
// `resumePing` is excluded on purpose: the turn it opens is real work resumed.
|
|
1677
|
-
const warmerSigs =
|
|
1678
|
-
const isKeepalivePing = (text) =>
|
|
1679
|
-
const n = normAssistant(text);
|
|
1680
|
-
return n.toLowerCase() === "ping" || keepalivePingSigs.some((sig) => n.includes(sig));
|
|
1681
|
-
};
|
|
1676
|
+
const warmerSigs = keepalivePingSigs(cfg.wrc.mirror.keepalive.ping);
|
|
1677
|
+
const isKeepalivePing = (text) => isKeepalivePingText(text, pingSigs);
|
|
1682
1678
|
// ── Typewriter stream lifecycle ────────────────────────────────────
|
|
1683
1679
|
// WeCom spec: server polls us for stream refreshes for up to 6 min from the
|
|
1684
1680
|
// original inbound. SDK queues replyStream calls per req_id, sends serially
|
|
@@ -1832,9 +1828,10 @@ export const startMirror = (deps) => {
|
|
|
1832
1828
|
// 头挂 chat 详情页的票据: 本轮那条 turn。没有活 turn 可挂 → undefined, 头退回裸
|
|
1833
1829
|
// `emoji #tag` (withLinkedTagHeader 自己兜)。
|
|
1834
1830
|
const chatTurnUrl = (target, turnId) => turnId
|
|
1835
|
-
? buildChatUrl(cfg.daemon.detailPublicBase, cfg.daemon.host, cfg.daemon.port, turnId,
|
|
1831
|
+
? buildChatUrl(cfg.daemon.detailPublicBase, cfg.daemon.host, cfg.daemon.port, turnId, roleUniq(target))
|
|
1836
1832
|
: undefined;
|
|
1837
|
-
|
|
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);
|
|
1838
1835
|
// 空正文一票否决 (近源拦截): 剥掉可能存在的路由头 (`🦊 #tag` / `[🧙](url)`)
|
|
1839
1836
|
// 后没有可见内容就不发。中央 chat-gate (last-response 的 SDK 包装) 是最后一道
|
|
1840
1837
|
// 防线; 这里拦在源头 —— 空内容不进 standalonePending FIFO、不占防抖 buf、
|
|
@@ -2188,8 +2185,8 @@ export const startMirror = (deps) => {
|
|
|
2188
2185
|
};
|
|
2189
2186
|
// 链接落到 chat 视图: 默认选中本 turn 所属的 #tag, 贴底显示整条会话。turnId 依旧
|
|
2190
2187
|
// 是凭据 (不可枚举), 只是页面从"一个 turn"扩成"这个 chat 的全部会话"。
|
|
2191
|
-
//
|
|
2192
|
-
const briefDetailLink = (turnId, target) => linkedTagHead(target, buildChatUrl(cfg.daemon.detailPublicBase, cfg.daemon.host, cfg.daemon.port, turnId,
|
|
2188
|
+
// wizard 名字作为 ww_uniq 传下去, 让同一个 wizard 的所有 turn 详情都复用一个 WeCom 窗口。
|
|
2189
|
+
const briefDetailLink = (turnId, target) => linkedTagHead(target, buildChatUrl(cfg.daemon.detailPublicBase, cfg.daemon.host, cfg.daemon.port, turnId, roleUniq(target)));
|
|
2193
2190
|
// ── 出处门 (chatOriginOnly) ───────────────────────────────────────────
|
|
2194
2191
|
// 人在 CLI 里手敲的一轮, 镜像只发生在两处: 他眼前的终端, 和 chat 详情页 (turn
|
|
2195
2192
|
// store 全量记录 + SSE 实时刷新)。聊天里不再下发它的气泡 —— 那份内容他本来就
|
|
@@ -2265,18 +2262,40 @@ export const startMirror = (deps) => {
|
|
|
2265
2262
|
a.pendingFrom = undefined;
|
|
2266
2263
|
return p && Date.now() - p.at <= ORIGIN_TTL_MS ? p.from : undefined;
|
|
2267
2264
|
};
|
|
2265
|
+
/** 公开派活的那个同伴 —— 回复的头要标给它 (见 AttachState.replyTo)。 */
|
|
2266
|
+
const publicPeer = (from) => from?.kind === "peer" && from.public ? from.from : undefined;
|
|
2268
2267
|
/** 频道印章: 有新印章就换频道; 没有则 `reset` (CLI 手敲 / 人从聊天发起) 回到
|
|
2269
|
-
* home, 其余续写 (收口后的补写)
|
|
2270
|
-
const consumeChannel = (a, reset) => {
|
|
2268
|
+
* home, 其余续写 (收口后的补写) 沿用上一轮。`from` = 本轮的出处, 答给谁跟着频道走。 */
|
|
2269
|
+
const consumeChannel = (a, reset, from) => {
|
|
2271
2270
|
const p = a.pendingChannel;
|
|
2272
2271
|
a.pendingChannel = undefined;
|
|
2272
|
+
const seeded = a.channelSeeded;
|
|
2273
|
+
a.channelSeeded = true;
|
|
2273
2274
|
if (p && Date.now() - p.at <= ORIGIN_TTL_MS) {
|
|
2274
2275
|
a.channel = p.channel === baseOfKey(a.target) ? undefined : p.channel;
|
|
2275
2276
|
a.speaker = p.speaker;
|
|
2277
|
+
a.replyTo = publicPeer(from);
|
|
2278
|
+
a.convFrom = from;
|
|
2276
2279
|
}
|
|
2277
2280
|
else if (reset) {
|
|
2278
2281
|
a.channel = undefined;
|
|
2279
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
|
+
}
|
|
2280
2299
|
}
|
|
2281
2300
|
};
|
|
2282
2301
|
/** 本轮落 turn 记录的频道字段。 */
|
|
@@ -2295,8 +2314,9 @@ export const startMirror = (deps) => {
|
|
|
2295
2314
|
a.queryEpoch = (a.queryEpoch ?? 0) + 1; // WeCom 侧的新一轮同样是 query 边界
|
|
2296
2315
|
a.turnFromChat = true; // 出处确凿: 这一轮有 frame, 群里就是它的主场
|
|
2297
2316
|
a.pendingFromCli = false; // 人改从聊天里说话了, 之前那条 CLI 输入不再是出处
|
|
2298
|
-
|
|
2299
|
-
|
|
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) });
|
|
2300
2320
|
// hardTimer 兜底: turn 若无终句 / turn_end 收口 (卡死/漏收), 到点仍收气泡。
|
|
2301
2321
|
const bubble = { frame, streamId, hardTimer: undefined, done: false };
|
|
2302
2322
|
const q = { turnId, bubble, isSlash };
|
|
@@ -2342,8 +2362,10 @@ export const startMirror = (deps) => {
|
|
|
2342
2362
|
const fromCli = a.pendingFromCli === true;
|
|
2343
2363
|
a.pendingFromCli = false;
|
|
2344
2364
|
a.turnFromChat = fromCli ? false : a.turnFromChat ?? true;
|
|
2345
|
-
|
|
2346
|
-
|
|
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) });
|
|
2347
2369
|
a.briefTurnId = turnId;
|
|
2348
2370
|
a.briefBubble = undefined;
|
|
2349
2371
|
a.briefIsSlash = false;
|
|
@@ -2899,7 +2921,7 @@ export const startMirror = (deps) => {
|
|
|
2899
2921
|
// transcript, not on the word alone: a real answer that mentions pong has a
|
|
2900
2922
|
// real question in front of it.
|
|
2901
2923
|
if (item.kind === "text" && !a.keepaliveQuiet && !a.keepaliveByContent && PONG_RE.test(item.body)) {
|
|
2902
|
-
const ping = openKeepalivePing(a.jsonlPath,
|
|
2924
|
+
const ping = openKeepalivePing(a.jsonlPath, pingSigs);
|
|
2903
2925
|
if (ping)
|
|
2904
2926
|
beginKeepaliveByContent(a, ping);
|
|
2905
2927
|
}
|
|
@@ -4807,11 +4829,12 @@ export const startMirror = (deps) => {
|
|
|
4807
4829
|
continue; // cache already cold — a ping would cold-rewrite for nothing
|
|
4808
4830
|
if (k.round >= kc.rounds)
|
|
4809
4831
|
continue; // budget spent — let it go cold
|
|
4810
|
-
// Stall recovery, decided by RULE only (no model self-judgment)
|
|
4811
|
-
// transcript
|
|
4812
|
-
//
|
|
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
|
|
4813
4836
|
// instruction instead of the plain warmer.
|
|
4814
|
-
const stalled = kc.resumeOnStall &&
|
|
4837
|
+
const stalled = kc.resumeOnStall && transcriptStalled(a.jsonlPath, keepalivePingSigs(kc.ping, kc.resumePing), idleTriggerMs, now);
|
|
4815
4838
|
k.pinging = true;
|
|
4816
4839
|
k.pingMtime = k.lastMs; // settles when a newer turn (the ping's own) appears
|
|
4817
4840
|
await fireKeepalive(a, stalled);
|