wezard 1.3.18 → 1.3.20
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/config.example.jsonc +3 -0
- package/dist/daemon/detail.js +10 -0
- package/dist/daemon/detail.js.map +1 -1
- package/dist/daemon/mirror-bridge.js +66 -126
- package/dist/daemon/mirror-bridge.js.map +1 -1
- package/dist/daemon/peers.js +17 -71
- package/dist/daemon/peers.js.map +1 -1
- package/dist/shared/chat-http.js +35 -16
- package/dist/shared/chat-http.js.map +1 -1
- package/dist/shared/chat-view.js +27 -2
- package/dist/shared/chat-view.js.map +1 -1
- package/dist/shared/config.js +7 -15
- package/dist/shared/config.js.map +1 -1
- package/dist/shared/detail-render.js +36 -4
- package/dist/shared/detail-render.js.map +1 -1
- package/dist/shared/detail-store.js +9 -4
- package/dist/shared/detail-store.js.map +1 -1
- package/dist/svr/index.js +1 -1
- package/dist/svr/index.js.map +1 -1
- package/package.json +1 -1
- package/web/chat.js +7 -7
package/CLAUDE.md
CHANGED
|
@@ -54,7 +54,7 @@ Three processes, all coordinating via 127.0.0.1:17890 and `~/.wezard/`:
|
|
|
54
54
|
- `approval.ts` builds the button card, parks an HTTP request via `pending.ts`, and resolves it on `event.template_card_event`. Three click outcomes: `allow` / `allow_window` (auto-allow N min for that session) / `deny`. `session-cache.ts` short-circuits repeats.
|
|
55
55
|
- `inbound.ts` routes `message.text` / `message.image` / `message.mixed` → either headless `cc-bridge.ts` or `mirror-bridge.ts`. Gates on `allowFrom`; one-shot exception is the bootstrap claim (see below).
|
|
56
56
|
- `cc-bridge.ts` (mode=headless) spawns `claude -p --output-format stream-json --verbose`, parses `assistant`/`stream_event` lines, and pumps text via `client.replyStream`. Per-principal queue serializes turns; session id is captured from the `system/init` line and persisted to `~/.wezard/sessions.json` for `--resume`.
|
|
57
|
-
- `mirror-bridge.ts` (mode=mirror) tails `<projectsDir>/<encoded(cwd)>/<sid>.jsonl` and pushes new `assistant` content to a chat. Inbound injection is either `claude --resume <sid> -p` (writes a turn into the same jsonl) or `tmux paste-buffer` against a live TTY (only path that makes WeCom messages visible in the user's CLI window). `recentInjects` ring buffer suppresses the WeCom→CLI echo when tailing.
|
|
57
|
+
- `mirror-bridge.ts` (mode=mirror) tails `<projectsDir>/<encoded(cwd)>/<sid>.jsonl` and pushes new `assistant` content to a chat — but only for turns the chat itself started: a turn opened by a human's CLI keystroke is recorded to the turn store (chat-detail page) and kept out of the chat (`wrc.mirror.chatOriginOnly`, brief mode only); approval/askq cards and `[mirror]` notices bypass that gate. Inbound injection is either `claude --resume <sid> -p` (writes a turn into the same jsonl) or `tmux paste-buffer` against a live TTY (only path that makes WeCom messages visible in the user's CLI window). `recentInjects` ring buffer suppresses the WeCom→CLI echo when tailing.
|
|
58
58
|
- `claim.ts` is a single-shot bootstrap: an exact-match magic phrase from any sender bypasses `allowFrom`, writes that principal as `defaultChat`, and appends to `allowFrom`. This is the **only** legitimate ingress for first-time setup.
|
|
59
59
|
- `peers.ts` is the read side of multi-agent awareness: sibling sessions of one chat are exactly the target keys sharing a base principal (`baseOfKey`), i.e. the untagged default plus every `#tag`. Pure parsing only — bounded transcript-tail reads (`tailTurns` / `lastAssistantText` / `summarizeTail`, shared with `session-scan.ts`) and pane classification (`paneIsBusy`). **Busy detection matches the TUI spinner footer** (`… (12m 31s · …)` on current versions, `esc to interrupt` on older ones) in the last 8 non-blank rows — transcript mtime is useless here, it goes quiet during long tool calls.
|
|
60
60
|
- `graph.ts` is the loop-graph runner: `nodes` = tagged sessions (own cli/model/cwd), `steps` = an ordered pipeline walked `rounds` times, each step injecting a prompt, waiting for that agent to go idle, then feeding its reply forward as `{{last}}` / `{{<tag>}}`. Stops early on the `until` sentinel. All effects go through injected deps backed by `mirror-bridge`, so a step is exactly what a human typing `#tag …` would do. Runs are **in-memory only** — a reload cancels them (panes survive).
|
package/config.example.jsonc
CHANGED
|
@@ -57,6 +57,9 @@
|
|
|
57
57
|
// 中间 tool_use / tool_result / thinking 只写进详情页 (可折叠, Claude 网页风格)。
|
|
58
58
|
// 授权卡照常发群。
|
|
59
59
|
"brief": false,
|
|
60
|
+
// 出处门: CLI 手敲的那一轮只进终端 + chat 详情页, 不下发气泡。WeCom 发起的
|
|
61
|
+
// 轮次、审批卡、[mirror] 提示不受影响。只在 brief=true 下生效。
|
|
62
|
+
"chatOriginOnly": true,
|
|
60
63
|
// 斜杠命令回执精简: /clear、/new 只回第一行 ack ("cleared"/"created"),
|
|
61
64
|
// 不再附 📂 项目信息与 💡 tip。默认 false = 完整回执。
|
|
62
65
|
"slashAckFirstLine": false
|
package/dist/daemon/detail.js
CHANGED
|
@@ -57,6 +57,16 @@ export const recordApprovalDecision = (reqId, decision, decidedBy) => {
|
|
|
57
57
|
if (rec)
|
|
58
58
|
forwardToRemote(rec);
|
|
59
59
|
};
|
|
60
|
+
// 上下文断点标记: 清空/轮换发生的那一刻就落一条独立记录, chat 线程据此画分隔线,
|
|
61
|
+
// 不必等下一轮开口 (那一轮还可能是个被丢弃的空壳)。
|
|
62
|
+
export const recordMark = (rec) => {
|
|
63
|
+
if (!store)
|
|
64
|
+
return;
|
|
65
|
+
store.recordMark(rec);
|
|
66
|
+
const full = store.get(rec.id);
|
|
67
|
+
if (full)
|
|
68
|
+
forwardToRemote(full);
|
|
69
|
+
};
|
|
60
70
|
// Brief 模式聚合详情: 一个 turn 的时间线。startTurn 建空壳,item 增量 append,close 收尾。
|
|
61
71
|
// 每次变更都完整重推整条 record 给 remote svr — svr 侧存的是 last-write-wins 快照。
|
|
62
72
|
export const recordTurnStart = (rec) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"detail.js","sourceRoot":"","sources":["../../daemon/detail.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EACL,iBAAiB,
|
|
1
|
+
{"version":3,"file":"detail.js","sourceRoot":"","sources":["../../daemon/detail.ts"],"names":[],"mappings":"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"}
|
|
@@ -23,12 +23,12 @@ import { isAutoWindowActive } from "./session-cache.js";
|
|
|
23
23
|
import { dangerOf } from "./danger.js";
|
|
24
24
|
import { runTmux as runTmuxCmd, spawnTmuxClaude } from "./spawn-tmux.js";
|
|
25
25
|
import { startSubagentWatch } from "./subagent-tail.js";
|
|
26
|
-
import { recordTool, recordToolResult, recordTurnStart, recordTurnItem, recordTurnUsage, recordTurnClose, recordCloseOpenTurns, buildDetailUrl, buildChatUrl } from "./detail.js";
|
|
26
|
+
import { recordTool, recordToolResult, recordMark, recordTurnStart, recordTurnItem, recordTurnUsage, recordTurnClose, recordCloseOpenTurns, buildDetailUrl, buildChatUrl } from "./detail.js";
|
|
27
27
|
import { labelFor, tagOfKey, baseOfKey, keyOf, withTagHeader, parseTagHeader } from "../shared/session-label.js";
|
|
28
28
|
import { splitMarkdown } from "../shared/md-chunk.js";
|
|
29
29
|
import { randomTip } from "./tips.js";
|
|
30
30
|
import { chatBaseOf, chatNameOf, listChatNames, normChatName, parsePeerRef, peerAddress } from "./chat-name.js";
|
|
31
|
-
import { stripAnsi, compactPane, paneIsBusy, paneIsStalled, transcriptStalled,
|
|
31
|
+
import { stripAnsi, compactPane, paneIsBusy, paneIsStalled, transcriptStalled, summarizeTail, lastAssistantText, lastContextTokens, keepaliveStamps, tailTurns, renderDialog } from "./peers.js";
|
|
32
32
|
// Same PATH augmentation logic as cc-bridge: launchd / systemd start the daemon
|
|
33
33
|
// with a stripped PATH that often lacks nvm / homebrew, breaking spawn(claudeBin).
|
|
34
34
|
const NODE_BIN_DIR = dirname(process.execPath);
|
|
@@ -1555,6 +1555,7 @@ export const startMirror = (deps) => {
|
|
|
1555
1555
|
};
|
|
1556
1556
|
const TOOL_DETAIL_PREFIX = "TOOL_DETAIL|";
|
|
1557
1557
|
const newTurnId = () => `t${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|
|
1558
|
+
const newMarkId = () => `m${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|
|
1558
1559
|
const detailCardFor = (s, target) => {
|
|
1559
1560
|
if (s.tools.length === 0)
|
|
1560
1561
|
return undefined;
|
|
@@ -2027,6 +2028,21 @@ export const startMirror = (deps) => {
|
|
|
2027
2028
|
const tag = tagOfKey(target);
|
|
2028
2029
|
return tag ? `[${labelFor(tag)} #${tag}](${url})` : `[🧙](${url})`;
|
|
2029
2030
|
};
|
|
2031
|
+
// ── 出处门 (chatOriginOnly) ───────────────────────────────────────────
|
|
2032
|
+
// 人在 CLI 里手敲的一轮, 镜像只发生在两处: 他眼前的终端, 和 chat 详情页 (turn
|
|
2033
|
+
// store 全量记录 + SSE 实时刷新)。聊天里不再下发它的气泡 —— 那份内容他本来就
|
|
2034
|
+
// 在看。群里保留的是需要在群里发生的事: WeCom 发起的轮次、审批/提问卡、
|
|
2035
|
+
// `[mirror]` 系统提示 —— 它们都不走这道门。
|
|
2036
|
+
// 只在 brief 下生效: 抑制的前提是详情页兜得住内容, 而 turn store 只有 brief 在写。
|
|
2037
|
+
const turnSilent = (a) => cfg.wrc.mirror.brief && cfg.wrc.mirror.chatOriginOnly && a.turnFromChat === false;
|
|
2038
|
+
// 详情页入口一次性告知。CLI 轮全程静默后, 一个从未收到过气泡的会话在群里将没有
|
|
2039
|
+
// 任何链接可点 —— 每个 attachment 发一条 (且仅一条) 带链接的提示解决这个死角。
|
|
2040
|
+
const noticeCliSilent = (a, turnId) => {
|
|
2041
|
+
if (a.cliSilentNoticed)
|
|
2042
|
+
return;
|
|
2043
|
+
a.cliSilentNoticed = true;
|
|
2044
|
+
sendRaw(a, `${briefDetailLink(turnId, a.target)} CLI 侧对话不再下发到群里,点标签看实时详情。`);
|
|
2045
|
+
};
|
|
2030
2046
|
// 收口一条 loading 气泡: finish=true 写入最终内容, 只生效一次。发送失败退回 standalone。
|
|
2031
2047
|
// raw=true skips withSessionTag (used when content already contains the linked tag header).
|
|
2032
2048
|
// WeCom 客户端收到 finish=true 后仍有打字机动画要播放, 如果紧接着就下发
|
|
@@ -2071,11 +2087,13 @@ export const startMirror = (deps) => {
|
|
|
2071
2087
|
clearCot(a); // turn 级状态 —— 进度行跟着气泡走, 不能跨 turn 残留
|
|
2072
2088
|
log.info({ sessionId: a.sessionId, turnId: q.turnId, isSlash: q.isSlash }, "brief: turn started");
|
|
2073
2089
|
};
|
|
2074
|
-
//
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2090
|
+
// 上下文断点落一条独立记录 —— 清空/轮换发生在两轮之间, 分隔线就画在两轮之间。
|
|
2091
|
+
// 挂在"下一轮"上等于要等人再开口才显形, 而那一轮还可能被当成空壳 turn 丢掉。
|
|
2092
|
+
// sessionId 记断点之后的那个: detail-store 的轮换判定以它为基准, 免得紧随其后的
|
|
2093
|
+
// 第一轮又被判成一次轮换、多画一条线。
|
|
2094
|
+
const markCut = (target, sessionId, cut) => {
|
|
2095
|
+
recordMark({ id: newMarkId(), target, sessionId, cut });
|
|
2096
|
+
log.info({ target, sessionId, cut }, "detail: context cut marked");
|
|
2079
2097
|
};
|
|
2080
2098
|
// graph 归因同样一次性: 一次注入只解释它开出的那一轮。超过 ORIGIN_TTL_MS 还没被
|
|
2081
2099
|
// 认领 = 那次注入没能开出 turn, 印章作废 —— 宁可少标一轮, 也不能把人打的字冒认成
|
|
@@ -2089,7 +2107,9 @@ export const startMirror = (deps) => {
|
|
|
2089
2107
|
const startBriefTurn = async (a, frame, streamId, isSlash = false, userQuery = "") => {
|
|
2090
2108
|
const turnId = newTurnId();
|
|
2091
2109
|
a.queryEpoch = (a.queryEpoch ?? 0) + 1; // WeCom 侧的新一轮同样是 query 边界
|
|
2092
|
-
|
|
2110
|
+
a.turnFromChat = true; // 出处确凿: 这一轮有 frame, 群里就是它的主场
|
|
2111
|
+
a.pendingFromCli = false; // 人改从聊天里说话了, 之前那条 CLI 输入不再是出处
|
|
2112
|
+
recordTurnStart({ id: turnId, target: a.target, sessionId: a.sessionId, cwd: a.runningCwd || undefined, userQuery: userQuery.trim() || undefined, origin: consumeOrigin(a) });
|
|
2093
2113
|
// hardTimer 兜底: turn 若无终句 / turn_end 收口 (卡死/漏收), 到点仍收气泡。
|
|
2094
2114
|
const bubble = { frame, streamId, hardTimer: undefined, done: false };
|
|
2095
2115
|
const q = { turnId, bubble, isSlash };
|
|
@@ -2130,7 +2150,12 @@ export const startMirror = (deps) => {
|
|
|
2130
2150
|
const turnId = newTurnId();
|
|
2131
2151
|
const query = a.pendingBriefQuery?.replace(/^> ?/gm, "").trim();
|
|
2132
2152
|
a.pendingBriefQuery = undefined;
|
|
2133
|
-
|
|
2153
|
+
// 出处继承: 有真人 CLI 输入行开头 → CLI 轮; 否则 (收口后的补写、peer/graph 注入
|
|
2154
|
+
// 开出的轮) 沿用上一轮 —— 那些续写属于同一场对话, 不该改变可见性。
|
|
2155
|
+
const fromCli = a.pendingFromCli === true;
|
|
2156
|
+
a.pendingFromCli = false;
|
|
2157
|
+
a.turnFromChat = fromCli ? false : a.turnFromChat ?? true;
|
|
2158
|
+
recordTurnStart({ id: turnId, target: a.target, sessionId: a.sessionId, cwd: a.runningCwd || undefined, userQuery: query || undefined, origin: consumeOrigin(a) });
|
|
2134
2159
|
a.briefTurnId = turnId;
|
|
2135
2160
|
a.briefBubble = undefined;
|
|
2136
2161
|
a.briefIsSlash = false;
|
|
@@ -2138,10 +2163,12 @@ export const startMirror = (deps) => {
|
|
|
2138
2163
|
a.briefConcluded = false;
|
|
2139
2164
|
a.briefLastText = undefined;
|
|
2140
2165
|
clearCot(a);
|
|
2141
|
-
//
|
|
2142
|
-
//
|
|
2143
|
-
|
|
2144
|
-
|
|
2166
|
+
// 详情链接不在这里单独发 — 本轮的每条 standalone 都由 withLinkedTag 自带
|
|
2167
|
+
// (linkedTagPrefix 读的正是 a.briefTurnId), 再暂存一份只会在正文前拼出第二条
|
|
2168
|
+
// 一模一样的链接。静默轮没有 standalone 可搭车, 单独提示一次。
|
|
2169
|
+
if (turnSilent(a))
|
|
2170
|
+
noticeCliSilent(a, turnId);
|
|
2171
|
+
log.info({ sessionId: a.sessionId, turnId, fromCli, silent: turnSilent(a) }, "brief: turn started (CLI-side, no bubble)");
|
|
2145
2172
|
};
|
|
2146
2173
|
// 本轮出现过工具调用 / tool_result / 非 final 文本 —— 只记状态: 详情链接从 ack 起
|
|
2147
2174
|
// 就挂在气泡里, 无需再推一次。气泡仍然不关, 等正文到来后以 `链接 正文` 覆盖收口。
|
|
@@ -2172,13 +2199,10 @@ export const startMirror = (deps) => {
|
|
|
2172
2199
|
const turnId = a.briefTurnId;
|
|
2173
2200
|
if (!turnId || a.briefConcluded)
|
|
2174
2201
|
return;
|
|
2175
|
-
if (!body.trim())
|
|
2176
|
-
a.pendingBriefHeader = undefined;
|
|
2202
|
+
if (!body.trim())
|
|
2177
2203
|
return;
|
|
2178
|
-
}
|
|
2179
2204
|
if (isDuplicateConclusion(a, body)) {
|
|
2180
2205
|
a.briefConcluded = true; // 本轮已"定稿", 只是不投递
|
|
2181
|
-
a.pendingBriefHeader = undefined;
|
|
2182
2206
|
log.info({ sessionId: a.sessionId, turnId, epoch: a.queryEpoch ?? 0 }, "brief: duplicate conclusion suppressed");
|
|
2183
2207
|
return;
|
|
2184
2208
|
}
|
|
@@ -2188,12 +2212,13 @@ export const startMirror = (deps) => {
|
|
|
2188
2212
|
a.lastConcludedAt = Date.now();
|
|
2189
2213
|
if (a.briefBubble && !a.briefBubble.done) {
|
|
2190
2214
|
void finishBriefBubble(a, `${briefDetailLink(turnId, a.target)} ${body}`, true);
|
|
2191
|
-
|
|
2215
|
+
}
|
|
2216
|
+
else if (turnSilent(a)) {
|
|
2217
|
+
// CLI 手敲的一轮: 终稿只留在 turn store, 详情页照常实时可见。
|
|
2218
|
+
log.info({ sessionId: a.sessionId, turnId }, "brief: CLI-origin conclusion kept out of chat");
|
|
2192
2219
|
}
|
|
2193
2220
|
else {
|
|
2194
|
-
|
|
2195
|
-
a.pendingBriefHeader = undefined;
|
|
2196
|
-
sendStandalone(a, header ? `${header} ${body}` : body);
|
|
2221
|
+
sendStandalone(a, body); // 详情链接由 withLinkedTag 统一前缀, 不在这里拼
|
|
2197
2222
|
}
|
|
2198
2223
|
// 只保留当前 turn: 其余仍开着的 turn 记录是漏收的 close, 一并扫掉。
|
|
2199
2224
|
recordCloseOpenTurns({ target: a.target, sessionId: a.sessionId, exceptIds: [turnId] });
|
|
@@ -2215,7 +2240,6 @@ export const startMirror = (deps) => {
|
|
|
2215
2240
|
a.briefIsSlash = false;
|
|
2216
2241
|
a.briefConcluded = false;
|
|
2217
2242
|
a.briefLastText = undefined;
|
|
2218
|
-
a.pendingBriefHeader = undefined;
|
|
2219
2243
|
// 收口 = 欠账两清。前台派发的 result 不会再落回这一轮; 后台派发跨轮存活, 由完成
|
|
2220
2244
|
// 通知 / TTL 销账 (voidTurnAgents)。挂着的软收口计时器一并撤掉 —— 它到点会去收
|
|
2221
2245
|
// 「当前 turn」, 那时的当前 turn 已经是下一轮了。
|
|
@@ -2286,7 +2310,7 @@ export const startMirror = (deps) => {
|
|
|
2286
2310
|
sessionId: a.sessionId,
|
|
2287
2311
|
cwd: a.runningCwd || undefined,
|
|
2288
2312
|
userQuery: task,
|
|
2289
|
-
agent: { id: agentId, type: resolved?.type, description: resolved?.description },
|
|
2313
|
+
agent: { id: agentId, type: resolved?.type, description: resolved?.description, parentTurnId: a.briefTurnId },
|
|
2290
2314
|
});
|
|
2291
2315
|
log.info({ agentId, turnId, label: run.label }, "subagent: turn started");
|
|
2292
2316
|
if (item.kind === "task")
|
|
@@ -2474,14 +2498,12 @@ export const startMirror = (deps) => {
|
|
|
2474
2498
|
// loading 气泡。非 slash 场景的 skill_output 是用户可见的中间反馈, 走 standalone。
|
|
2475
2499
|
if (a.briefIsSlash && !a.briefHadTool && a.briefBubble && !a.briefBubble.done) {
|
|
2476
2500
|
void finishBriefBubble(a, item.body);
|
|
2477
|
-
|
|
2501
|
+
}
|
|
2502
|
+
else if (turnSilent(a)) {
|
|
2503
|
+
// CLI 里手敲的 /clear、/model 之类, 回执给敲的人看就够了 —— 只进详情页。
|
|
2478
2504
|
}
|
|
2479
2505
|
else {
|
|
2480
|
-
|
|
2481
|
-
// "只发了一条 link 渲染成空文本" 的前置消息。
|
|
2482
|
-
const header = a.pendingBriefHeader;
|
|
2483
|
-
a.pendingBriefHeader = undefined;
|
|
2484
|
-
sendStandalone(a, header ? `${header} ${item.body}` : item.body);
|
|
2506
|
+
sendStandalone(a, item.body); // 链接前缀同上, 走 withLinkedTag
|
|
2485
2507
|
}
|
|
2486
2508
|
return;
|
|
2487
2509
|
}
|
|
@@ -2670,6 +2692,11 @@ export const startMirror = (deps) => {
|
|
|
2670
2692
|
// 清掉防残留阻塞新 turn 的软收口 (guard 的 defer 判断依据)。
|
|
2671
2693
|
voidTurnAgents(a);
|
|
2672
2694
|
a.queryEpoch = (a.queryEpoch ?? 0) + 1;
|
|
2695
|
+
// 这一行是人在 CLI 里敲的 —— 它既是下一轮的 query, 也是下一轮的出处。
|
|
2696
|
+
// quiet (includeUser=false, 默认) 同样记: includeUser 只决定气泡渲不渲染,
|
|
2697
|
+
// 不决定详情页记不记 —— 聊天不再下发 CLI 轮之后, 详情页是唯一的落点。
|
|
2698
|
+
a.pendingBriefQuery = item.body;
|
|
2699
|
+
a.pendingFromCli = true;
|
|
2673
2700
|
}
|
|
2674
2701
|
// 后台派发的结束信号 —— 它不是 function_call_result (那只是 spawn 句柄)。放在
|
|
2675
2702
|
// keepalive 门之前: 吞没窗口里到达也必须销账, 否则账本永久悬空。
|
|
@@ -2877,13 +2904,9 @@ export const startMirror = (deps) => {
|
|
|
2877
2904
|
}
|
|
2878
2905
|
// Brief 模式下没有活跃 turn 时, 任何 assistant 侧产出都补开一个无气泡 turn ——
|
|
2879
2906
|
// 覆盖 CLI 侧直接开的新一轮 (WeCom 没参与, 拿不到 frame) 与收口后的零星补写。
|
|
2880
|
-
//
|
|
2881
|
-
if (cfg.wrc.mirror.brief && !a.briefTurnId)
|
|
2882
|
-
|
|
2883
|
-
a.pendingBriefQuery = item.body;
|
|
2884
|
-
else if (BRIEF_TURN_OPENERS.has(item.kind))
|
|
2885
|
-
ensureBriefTurn(a);
|
|
2886
|
-
}
|
|
2907
|
+
// user 行不开 turn (CLI 敲字 ≠ 一定有回复), 它的 query/出处已在上面记下。
|
|
2908
|
+
if (cfg.wrc.mirror.brief && !a.briefTurnId && BRIEF_TURN_OPENERS.has(item.kind))
|
|
2909
|
+
ensureBriefTurn(a);
|
|
2887
2910
|
// Usage snapshots never flow into WeCom bubbles — brief 分支下 handleBriefItem
|
|
2888
2911
|
// 会把它写进 turn store, 非 brief 下直接吞掉, 保持 onItem 主流程只处理有渲染
|
|
2889
2912
|
// 输出的 item 类型。
|
|
@@ -3437,9 +3460,10 @@ export const startMirror = (deps) => {
|
|
|
3437
3460
|
const oldJsonlPath = a.jsonlPath;
|
|
3438
3461
|
// 迁移是所有会话轮换的唯一漏斗 (注入的 /clear、TUI 里手打的 /clear、resume fork、
|
|
3439
3462
|
// worktree drift)。只有目标 jsonl 首条用户行是 /clear 的那种才是真清空 —— fork /
|
|
3440
|
-
// drift 上下文是延续的,
|
|
3441
|
-
|
|
3442
|
-
|
|
3463
|
+
// drift 上下文是延续的, 画中性的 "switch"。
|
|
3464
|
+
const cleared = jsonlIsPostClearChild(newJsonlPath);
|
|
3465
|
+
markCut(a.target, newSessionId, cleared ? "clear" : "switch");
|
|
3466
|
+
if (cleared) {
|
|
3443
3467
|
// 真清空 = 缓存里已无任何值得保温的内容。像 /stop 一样暂停保活 —— 与
|
|
3444
3468
|
// dispatch 里 WeCom 注入 /clear 的暂停对齐;TUI 手打 /clear 只走这个漏斗,
|
|
3445
3469
|
// 不在这里停下的话旧时钟会继续 ping 一个空上下文。busy-resume(过 grace)
|
|
@@ -3867,7 +3891,7 @@ export const startMirror = (deps) => {
|
|
|
3867
3891
|
spawned.muteUntilInject = true;
|
|
3868
3892
|
// 只有换过 pane 才是断点 —— 首次 /wrc 建会话时 prev 为空, 那不是"不连续", 是开局。
|
|
3869
3893
|
if (prev)
|
|
3870
|
-
spawned.
|
|
3894
|
+
markCut(target, spawned.sessionId, "new");
|
|
3871
3895
|
// A freshly spawned session is empty — nothing in the cache to keep warm.
|
|
3872
3896
|
// Pause keepalive like /stop; the first real turn (WeCom inbound, or the
|
|
3873
3897
|
// pane going busy after the resume grace) re-earns the budget. Mirrors
|
|
@@ -4352,10 +4376,8 @@ export const startMirror = (deps) => {
|
|
|
4352
4376
|
// Stall recovery, decided by RULE only (no model self-judgment): the last
|
|
4353
4377
|
// transcript turn is a synthetic API-error/limit line, or the idle pane
|
|
4354
4378
|
// still shows an error banner ⇒ a turn died mid-work. Send the resume
|
|
4355
|
-
// instruction instead of the plain warmer.
|
|
4356
|
-
|
|
4357
|
-
// limitResumeTick owns that recovery, anchored on the reset moment.
|
|
4358
|
-
const stalled = kc.resumeOnStall && !a.limitResume && (transcriptStalled(a.jsonlPath) || paneIsStalled(paneTail));
|
|
4379
|
+
// instruction instead of the plain warmer.
|
|
4380
|
+
const stalled = kc.resumeOnStall && (transcriptStalled(a.jsonlPath) || paneIsStalled(paneTail));
|
|
4359
4381
|
k.pinging = true;
|
|
4360
4382
|
k.pingMtime = k.lastMs; // settles when a newer turn (the ping's own) appears
|
|
4361
4383
|
await fireKeepalive(a, stalled);
|
|
@@ -4369,83 +4391,6 @@ export const startMirror = (deps) => {
|
|
|
4369
4391
|
}
|
|
4370
4392
|
};
|
|
4371
4393
|
const keepaliveTimer = setInterval(() => void keepaliveTick(), 15_000);
|
|
4372
|
-
// ── Rate-limit auto-resume ────────────────────────────────────────────
|
|
4373
|
-
// Detection is poll-derived from the transcript tail every tick (limitResetAt:
|
|
4374
|
-
// "last message turn IS a 429 line"), so there is nothing to persist — a
|
|
4375
|
-
// daemon reload, a human retry in the TTY, or our own inject all converge
|
|
4376
|
-
// naturally: any newer turn makes detection stop firing and drops the state.
|
|
4377
|
-
const fmtClock = (ms) => {
|
|
4378
|
-
const d = new Date(ms);
|
|
4379
|
-
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
4380
|
-
};
|
|
4381
|
-
let limitResumeTicking = false;
|
|
4382
|
-
const limitResumeTick = async () => {
|
|
4383
|
-
const lr = cfg.wrc.mirror.limitResume;
|
|
4384
|
-
if (!lr.enabled || limitResumeTicking)
|
|
4385
|
-
return;
|
|
4386
|
-
limitResumeTicking = true;
|
|
4387
|
-
try {
|
|
4388
|
-
const now = Date.now();
|
|
4389
|
-
for (const a of byTarget.values()) {
|
|
4390
|
-
const resetsAt = limitResetAt(a.jsonlPath, now);
|
|
4391
|
-
if (!resetsAt) {
|
|
4392
|
-
a.limitResume = undefined;
|
|
4393
|
-
continue;
|
|
4394
|
-
}
|
|
4395
|
-
if (a.migrationWatcher)
|
|
4396
|
-
continue; // session rotating — judge after it settles
|
|
4397
|
-
if (a.keepaliveOff)
|
|
4398
|
-
continue; // /stop = deliberately quieted; honor it
|
|
4399
|
-
if (a.limitResume?.resetsAt !== resetsAt)
|
|
4400
|
-
a.limitResume = { resetsAt, notified: false, retried: false };
|
|
4401
|
-
const st = a.limitResume;
|
|
4402
|
-
const dueAt = resetsAt + lr.delaySec * 1000;
|
|
4403
|
-
if (!st.notified) {
|
|
4404
|
-
st.notified = true;
|
|
4405
|
-
log.info({ target: a.target, resetsAt, dueAt }, "limit-resume: parked, retry scheduled");
|
|
4406
|
-
sendStandalone(a, `[mirror] ⏳ 已触限额 (reset ${fmtClock(resetsAt)}) — ${fmtClock(dueAt)} 自动注入 \`${lr.text}\` 续跑`);
|
|
4407
|
-
}
|
|
4408
|
-
if (st.retried || now < dueAt)
|
|
4409
|
-
continue;
|
|
4410
|
-
if (a.liveStream && !a.liveStream.closed)
|
|
4411
|
-
continue; // mid typewriter — don't inject
|
|
4412
|
-
if (a.tmuxPane) {
|
|
4413
|
-
// Pane mode: a dead pane means the human closed shop — auto-respawning
|
|
4414
|
-
// a TUI at 3am would be a surprise, not a service. Busy means someone
|
|
4415
|
-
// (human, or another turn) is already driving; detection clears itself
|
|
4416
|
-
// once their turn lands. Spawn-mode has no pane and no such hazards —
|
|
4417
|
-
// inject's `claude --resume -p` fallback IS its normal operation.
|
|
4418
|
-
if (!(await tmuxPaneAlive(a.tmuxPane)))
|
|
4419
|
-
continue;
|
|
4420
|
-
if (paneIsBusy(await capturePaneTail(a.tmuxPane, 16)))
|
|
4421
|
-
continue;
|
|
4422
|
-
}
|
|
4423
|
-
st.retried = true;
|
|
4424
|
-
// Unlike keepalive there is no swallow: the resumed work is real work —
|
|
4425
|
-
// the tail mirrors its output to chat normally. rememberInject only
|
|
4426
|
-
// suppresses the echo of the injected user line itself.
|
|
4427
|
-
rememberInject(lr.text);
|
|
4428
|
-
const r = await inject({
|
|
4429
|
-
text: lr.text, images: [], cfg,
|
|
4430
|
-
log: log.child({ target: a.target, sessionId: a.sessionId, sub: "limit-resume" }),
|
|
4431
|
-
sessionId: a.sessionId, jsonlPath: a.jsonlPath, tmuxTarget: a.tmuxPane,
|
|
4432
|
-
});
|
|
4433
|
-
if (!r.ok) {
|
|
4434
|
-
st.retried = false; // roll back so the next tick retries the inject
|
|
4435
|
-
log.warn({ target: a.target, reason: r.reason }, "limit-resume: inject failed");
|
|
4436
|
-
continue;
|
|
4437
|
-
}
|
|
4438
|
-
log.info({ target: a.target, resetsAt }, "limit-resume: resume injected");
|
|
4439
|
-
}
|
|
4440
|
-
}
|
|
4441
|
-
catch (e) {
|
|
4442
|
-
log.warn({ err: e.message }, "limit-resume tick failed");
|
|
4443
|
-
}
|
|
4444
|
-
finally {
|
|
4445
|
-
limitResumeTicking = false;
|
|
4446
|
-
}
|
|
4447
|
-
};
|
|
4448
|
-
const limitResumeTimer = setInterval(() => void limitResumeTick(), 30_000);
|
|
4449
4394
|
// ── 原生权限 picker 哨兵 ──────────────────────────────────────────────
|
|
4450
4395
|
// CodeBuddy 对一部分调用先弹自家权限 picker、PreToolUse hook 在人点掉之后才
|
|
4451
4396
|
// 触发 / 根本不触发 (与 AskUserQuestion 的「本地面板先行」同源; 实测命中: 子代理
|
|
@@ -4961,7 +4906,6 @@ export const startMirror = (deps) => {
|
|
|
4961
4906
|
shutdown: () => {
|
|
4962
4907
|
clearInterval(paneDriftTimer);
|
|
4963
4908
|
clearInterval(keepaliveTimer);
|
|
4964
|
-
clearInterval(limitResumeTimer);
|
|
4965
4909
|
clearInterval(pickerTimer);
|
|
4966
4910
|
for (const a of bySessionId.values()) {
|
|
4967
4911
|
if (a.outbound?.kind === "deferred")
|
|
@@ -5053,10 +4997,6 @@ export const startMirror = (deps) => {
|
|
|
5053
4997
|
// see this clear as an overlap by the time it evaluates a candidate.
|
|
5054
4998
|
if (armMigration)
|
|
5055
4999
|
noteClearInject(a);
|
|
5056
|
-
// 记下断点, 由下一轮 (第一轮读不到上文的轮次) 领走。/clear 自己不建 turn,
|
|
5057
|
-
// 否则这次清空在 chat 详情里毫无痕迹, 前后两轮看着还是连续的。
|
|
5058
|
-
if (armMigration)
|
|
5059
|
-
a.ctxCut = "clear";
|
|
5060
5000
|
// 新消息 = 对话边界: 上一 turn 的出站状态在此清算。softEnd 是旧 turn 的
|
|
5061
5001
|
// 静默期收口判决, 残留到新 turn 会在中途开火、提前收掉新气泡/新流;
|
|
5062
5002
|
// deferred 缓冲里是已从 tail 消费但还没下发的 item, 收入旧 streamId 收口
|