wezard 2.1.3 → 2.2.0
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/README.md +15 -8
- package/dist/daemon/cc-session.js +14 -1
- package/dist/daemon/cc-session.js.map +1 -1
- package/dist/daemon/detail.js +40 -7
- package/dist/daemon/detail.js.map +1 -1
- package/dist/daemon/handoff.js +132 -0
- package/dist/daemon/handoff.js.map +1 -0
- package/dist/daemon/inbound.js +13 -4
- package/dist/daemon/inbound.js.map +1 -1
- package/dist/daemon/index.js +191 -99
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/mirror-bridge.js +161 -12
- package/dist/daemon/mirror-bridge.js.map +1 -1
- package/dist/daemon/peers.js +114 -34
- package/dist/daemon/peers.js.map +1 -1
- package/dist/daemon/receipts.js +190 -0
- package/dist/daemon/receipts.js.map +1 -0
- package/dist/daemon/route.js +92 -0
- package/dist/daemon/route.js.map +1 -0
- package/dist/daemon/wizard.js +38 -19
- package/dist/daemon/wizard.js.map +1 -1
- package/dist/daemon/ws.js +3 -0
- package/dist/daemon/ws.js.map +1 -1
- package/dist/mcp/server.js +60 -25
- package/dist/mcp/server.js.map +1 -1
- package/dist/shared/chat-http.js +101 -21
- package/dist/shared/chat-http.js.map +1 -1
- package/dist/shared/chat-render.js +0 -1
- package/dist/shared/chat-render.js.map +1 -1
- package/dist/shared/chat-view.js +2 -9
- package/dist/shared/chat-view.js.map +1 -1
- package/dist/shared/config.js +6 -0
- package/dist/shared/config.js.map +1 -1
- package/dist/shared/detail-render.js +3 -2
- package/dist/shared/detail-render.js.map +1 -1
- package/dist/shared/detail-store.js +1 -1
- package/dist/shared/detail-store.js.map +1 -1
- package/dist/shared/keepalive.js +46 -0
- package/dist/shared/keepalive.js.map +1 -0
- package/dist/shared/reminder.js +13 -0
- package/dist/shared/reminder.js.map +1 -1
- package/dist/shared/role-render.js +2 -1
- package/dist/shared/role-render.js.map +1 -1
- package/dist/shared/role-search.js +100 -0
- package/dist/shared/role-search.js.map +1 -0
- package/dist/shared/role-view.js +85 -26
- package/dist/shared/role-view.js.map +1 -1
- package/dist/shared/world.js +38 -8
- package/dist/shared/world.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.css +420 -327
- package/web/chat.js +924 -728
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// Caveat: the user shouldn't be hammering the same session in their local TTY
|
|
14
14
|
// while a `--resume` injection is in flight; Claude Code locks aren't strict.
|
|
15
15
|
import { spawn } from "node:child_process";
|
|
16
|
-
import { existsSync, readdirSync, statSync, watch, openSync, readSync, closeSync } from "node:fs";
|
|
16
|
+
import { existsSync, readdirSync, readFileSync, statSync, watch, openSync, readSync, closeSync } from "node:fs";
|
|
17
17
|
import { join, dirname } from "node:path";
|
|
18
18
|
import { expandHome, sanitizeId } from "../shared/paths.js";
|
|
19
19
|
import { augmentedPath } from "../shared/exec-path.js";
|
|
@@ -26,15 +26,17 @@ import { noticeSuffixFor } from "./notices.js";
|
|
|
26
26
|
import { dangerOf } from "./danger.js";
|
|
27
27
|
import { runTmux, spawnTmuxClaude } from "./spawn-tmux.js";
|
|
28
28
|
import { selectModel } from "./model-select.js";
|
|
29
|
-
import { hasRegistry, markTranscript, probeOf, sessionOnPane, sessionPanes, submittedSince } from "./cc-session.js";
|
|
29
|
+
import { hasRegistry, markTranscript, probeOf, sessionOnPane, sessionPanes, submittedSince, watchRegistry } from "./cc-session.js";
|
|
30
|
+
import { waitForIdle } from "./graph.js";
|
|
30
31
|
import { wizardStore } from "./wizard.js";
|
|
31
32
|
import { startSubagentWatch } from "./subagent-tail.js";
|
|
32
|
-
import { recordTool, recordToolResult, recordMark, recordTurnStart, recordTurnItem, recordTurnUsage, recordTurnClose, recordCloseOpenTurns, lastChannelOf, buildDetailUrl, buildChatUrl, roleUniq } from "./detail.js";
|
|
33
|
+
import { knowsToolUse, recordTool, recordToolResult, recordMark, recordTurnStart, recordTurnQuery, recordTurnItem, recordTurnUsage, recordTurnClose, recordCloseOpenTurns, lastChannelOf, buildDetailUrl, buildChatUrl, roleUniq } from "./detail.js";
|
|
33
34
|
import { labelFor, tagOfKey, baseOfKey, keyOf, stripSigil, displayName, withTagHeader, withLinkedTagHeader, linkedTagHead, linkTags, parseTagHeader, MAX_BODY_LINKS } from "../shared/session-label.js";
|
|
34
35
|
import { splitMarkdown } from "../shared/md-chunk.js";
|
|
35
36
|
import { randomTip } from "./tips.js";
|
|
36
37
|
import { chatBaseOf, chatNameOf, listChatNames, parsePeerRef, peerAddress } from "./chat-name.js";
|
|
37
|
-
import { stripAnsi, paneIsBusy, transcriptStalled, summarizeTail, lastAssistantText, lastReply, unwrapPasted, lastContextTokens, keepaliveStamps, openKeepalivePing,
|
|
38
|
+
import { stripAnsi, paneIsBusy, transcriptStalled, summarizeTail, lastAssistantText, lastReply, replyToPeer as replyToPeerIn, unwrapPasted, lastContextTokens, keepaliveStamps, openKeepalivePing, talkRounds, openToolUses, renderDialog } from "./peers.js";
|
|
39
|
+
import { keepalivePingSigs, isKeepalivePingText } from "../shared/keepalive.js";
|
|
38
40
|
// PATH augmentation: launchd / systemd start the daemon
|
|
39
41
|
// with a stripped PATH that often lacks nvm / homebrew, breaking spawn(claudeBin).
|
|
40
42
|
// Backend resolution is per-transcript, not global: `backendForPath` recovers
|
|
@@ -241,6 +243,9 @@ const resolveSession = (cfg, log) => {
|
|
|
241
243
|
}
|
|
242
244
|
return top;
|
|
243
245
|
};
|
|
246
|
+
// TUI 给粘进来的图留的指代; 落盘文本里有它, 注入时记下的原文里没有。
|
|
247
|
+
const IMAGE_REF_RE = /\[Image #\d+\]\s*/g;
|
|
248
|
+
const hasImages = (c) => c.some((b) => b?.type === "image");
|
|
244
249
|
// Target keys are `user:xxx[#tag]` / `chat:xxx[#tag]`. Strip the principal
|
|
245
250
|
// prefix AND the `#tag` suffix so WeCom's sendMessage receives just the bare
|
|
246
251
|
// chatid/userid. Tag survives only inside the daemon's byTarget/store keys.
|
|
@@ -430,6 +435,43 @@ const renderToolUseGroupBody = (calls, deps) => {
|
|
|
430
435
|
});
|
|
431
436
|
return [header, ...lines].join("\n");
|
|
432
437
|
};
|
|
438
|
+
// 从尾往前, 逐个产出含 needle 的整行 —— 只切命中的那几行, 不把整份 transcript 拆成数组。
|
|
439
|
+
function* linesWithFromEnd(text, needle) {
|
|
440
|
+
let at = text.lastIndexOf(needle);
|
|
441
|
+
while (at >= 0) {
|
|
442
|
+
const s = text.lastIndexOf("\n", at) + 1;
|
|
443
|
+
const e = text.indexOf("\n", at);
|
|
444
|
+
yield text.slice(s, e < 0 ? undefined : e);
|
|
445
|
+
at = s > 0 ? text.lastIndexOf(needle, s - 1) : -1;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
// tail 漏看了某次 tool_use (daemon 停机窗口里落盘的行 —— 恢复后的 tail 从 EOF 起读;
|
|
449
|
+
// 一个挂了几小时的审批最常撞上), 结果却到了: 回 transcript 按 id 把调用本身捞回来。
|
|
450
|
+
const recoverToolUse = (toolUseId, deps) => {
|
|
451
|
+
let text;
|
|
452
|
+
try {
|
|
453
|
+
text = readFileSync(deps.jsonlPath, "utf8");
|
|
454
|
+
}
|
|
455
|
+
catch {
|
|
456
|
+
return undefined;
|
|
457
|
+
}
|
|
458
|
+
for (const raw of linesWithFromEnd(text, toolUseId)) {
|
|
459
|
+
let line;
|
|
460
|
+
try {
|
|
461
|
+
line = JSON.parse(raw);
|
|
462
|
+
}
|
|
463
|
+
catch {
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
if (deps.normalizeLine)
|
|
467
|
+
line = deps.normalizeLine(line);
|
|
468
|
+
const blocks = line?.type === "assistant" ? line.message?.content : undefined;
|
|
469
|
+
const b = Array.isArray(blocks) ? blocks.find((x) => x?.type === "tool_use" && x.id === toolUseId) : undefined;
|
|
470
|
+
if (b)
|
|
471
|
+
return { toolUseId, name: b.name ?? "tool", input: b.input };
|
|
472
|
+
}
|
|
473
|
+
return undefined;
|
|
474
|
+
};
|
|
433
475
|
// Render one transcript line into tagged items. Caller decides batching.
|
|
434
476
|
const renderLine = (raw, deps) => {
|
|
435
477
|
let line;
|
|
@@ -509,6 +551,18 @@ const renderLine = (raw, deps) => {
|
|
|
509
551
|
// onItem 消费完边界语义后自己丢掉。
|
|
510
552
|
out.push({ kind: "user_text", body: quoted, quiet: !deps.includeUser });
|
|
511
553
|
}
|
|
554
|
+
else if (Array.isArray(c) && hasImages(c) && !c.some((b) => b?.type === "tool_result")) {
|
|
555
|
+
// 带图的一句话: [text "[Image #1]…", image, …]。去掉占位符才与注入时记下的原文同形,
|
|
556
|
+
// 回显去重对得上; 记进详情页的则是带占位符的原样文本, 图本身不画。
|
|
557
|
+
const raw = c.filter((b) => b?.type === "text").map((b) => b.text ?? "").join("\n");
|
|
558
|
+
const text = cleanUserText(raw.replace(IMAGE_REF_RE, ""));
|
|
559
|
+
if (text && !deps.isOwnInject(text)) {
|
|
560
|
+
out.push({ kind: "user_text", body: cleanUserText(raw).split("\n").map((l) => `> ${l}`).join("\n"), quiet: !deps.includeUser });
|
|
561
|
+
}
|
|
562
|
+
else {
|
|
563
|
+
out.push({ kind: "user_query", body: unwrapPasted(raw).trim(), said: text });
|
|
564
|
+
}
|
|
565
|
+
}
|
|
512
566
|
else if (Array.isArray(c)) {
|
|
513
567
|
for (const b of c) {
|
|
514
568
|
if (b?.type !== "tool_result")
|
|
@@ -518,6 +572,12 @@ const renderLine = (raw, deps) => {
|
|
|
518
572
|
continue;
|
|
519
573
|
const toolUseId = b.tool_use_id ?? "";
|
|
520
574
|
const full = truncateWithCount(raw, DETAIL_RESULT_MAX);
|
|
575
|
+
// 孤儿 result: 先补发它的调用, 下游 (turn store / 气泡) 就和 tail 当场看到时一样配对。
|
|
576
|
+
const lost = toolUseId && deps.includeTools && !knowsToolUse(toolUseId) ? recoverToolUse(toolUseId, deps) : undefined;
|
|
577
|
+
if (lost) {
|
|
578
|
+
recordTool({ id: lost.toolUseId, toolName: lost.name, toolInput: lost.input, sessionId: deps.sessionId, target: deps.target });
|
|
579
|
+
out.push({ kind: "tool_use", calls: [lost], body: renderToolUseGroupBody([lost], deps) });
|
|
580
|
+
}
|
|
521
581
|
// 始终把完整 result 落 detail 库 + 作为 tool_result item 发出 — 与
|
|
522
582
|
// includeToolResults(气泡推送开关)彻底解耦: 关掉气泡时 detail 页与 brief
|
|
523
583
|
// turn 页(handleBriefItem 消费本 item 写 turn store)都仍要看到 result。
|
|
@@ -2934,6 +2994,16 @@ export const startMirror = (deps) => {
|
|
|
2934
2994
|
a.pendingBriefQuery = item.body;
|
|
2935
2995
|
a.pendingFromCli = true;
|
|
2936
2996
|
}
|
|
2997
|
+
// 还有一轮等着懒建 (peer / graph 注入先记了 pendingBriefQuery) 就换掉它; 否则这句话
|
|
2998
|
+
// 就是已开好的那一轮 (IM 侧说的, startBriefTurn 在注入前建好)。
|
|
2999
|
+
if (item.kind === "user_query") {
|
|
3000
|
+
if (a.pendingBriefQuery !== undefined || !a.briefTurnId)
|
|
3001
|
+
a.pendingBriefQuery = item.body;
|
|
3002
|
+
// 只认同一句话: 轮次中途 CLI 里又粘了一张图, 不能改写正在跑的这一轮的问话。
|
|
3003
|
+
else
|
|
3004
|
+
recordTurnQuery(a.briefTurnId, item.body, (prev) => cleanUserText(prev ?? "") === item.said);
|
|
3005
|
+
return;
|
|
3006
|
+
}
|
|
2937
3007
|
// 后台派发的结束信号 —— 它不是 function_call_result (那只是 spawn 句柄)。放在
|
|
2938
3008
|
// keepalive 门之前: 吞没窗口里到达也必须销账, 否则账本永久悬空。
|
|
2939
3009
|
if (item.kind === "skill_output" && item.agentDone)
|
|
@@ -3571,17 +3641,18 @@ export const startMirror = (deps) => {
|
|
|
3571
3641
|
const RESTORE_CONC = 8;
|
|
3572
3642
|
const persisted = deps.store.all();
|
|
3573
3643
|
const persistedKeys = Object.keys(persisted);
|
|
3644
|
+
let restored = Promise.resolve();
|
|
3574
3645
|
if (persistedKeys.length > 0) {
|
|
3575
3646
|
// 裸 fan-out 会把 2N 次 tmux 同时压下去 —— 几百个绑定就是七百个 client 一起
|
|
3576
3647
|
// 撞 10s 超时, spawn 风暴顺带把事件循环压住, :17890 要 25s 才 bind, 而 CLI 的
|
|
3577
3648
|
// reload 探测早放弃了 (「reload issued but /status not responding」的真身)。
|
|
3578
3649
|
// 一张快照 + 封顶并发; await 立刻让出, startHttp 先绑端口。
|
|
3579
|
-
|
|
3650
|
+
restored = (async () => {
|
|
3580
3651
|
const snap = await runTmux(["list-panes", "-a", "-F", "#{pane_id}\t#{pane_current_path}"]);
|
|
3581
3652
|
const panes = snap.ok ? parsePaneList(snap.stdout) : undefined;
|
|
3582
3653
|
await mapLimit(persistedKeys, RESTORE_CONC, (principal) => restoreFromStore(principal, panes));
|
|
3583
3654
|
log.info({ restored: persistedKeys.length, panes: panes?.size ?? -1 }, "mirror: boot restore done");
|
|
3584
|
-
})();
|
|
3655
|
+
})().catch((e) => { log.warn({ err: e.message }, "mirror: boot restore failed"); });
|
|
3585
3656
|
}
|
|
3586
3657
|
else if (cfg.wrc.mirror.sessionId.trim()) {
|
|
3587
3658
|
// Pinned-sessionId fallback: only honor when the store is empty (otherwise
|
|
@@ -4218,7 +4289,7 @@ export const startMirror = (deps) => {
|
|
|
4218
4289
|
// Pause keepalive like /stop; the first real turn (WeCom inbound, or the
|
|
4219
4290
|
// pane going busy after the resume grace) re-earns the budget. Mirrors
|
|
4220
4291
|
// interruptPane so /new can't strand a ping loop on an idle blank session.
|
|
4221
|
-
spawned.keepaliveOff =
|
|
4292
|
+
spawned.keepaliveOff = !opts?.warm;
|
|
4222
4293
|
spawned.keepaliveOffAt = Date.now();
|
|
4223
4294
|
spawned.keepalive = undefined; // re-anchors cleanly on resume
|
|
4224
4295
|
persistPause(spawned);
|
|
@@ -4548,6 +4619,57 @@ export const startMirror = (deps) => {
|
|
|
4548
4619
|
// and it keeps a graph step from blocking forever on a session that vanished.
|
|
4549
4620
|
const paneBusy = async (pane) => !!pane && (await tmuxPaneAlive(pane)) && paneIsBusy(await capturePaneTail(pane, 12));
|
|
4550
4621
|
const isBusy = (target) => paneBusy(paneOf(target));
|
|
4622
|
+
// 「它这一轮结束了、可以投了」。注册表认得这个 pane 时以它为准: status 在整轮里
|
|
4623
|
+
// (工具调用之间、PreToolUse 审批 hook 挂着时) 一直是 busy, 只在轮结束那一刻翻成
|
|
4624
|
+
// idle —— 所以不必抽样 spinner, 也不必连数几次安静来排除工具间隙。idle 却悬着工具
|
|
4625
|
+
// 调用 = 停在本地审批框上等人点, 不算闲。注册表不认得 (codebuddy / 旧版 CC /
|
|
4626
|
+
// 会话已退出) 返回 undefined, 交给 pane 裁决。
|
|
4627
|
+
const idleVerdict = (target) => {
|
|
4628
|
+
const pane = paneOf(target);
|
|
4629
|
+
const live = pane ? activeBackends().map((b) => sessionOnPane(b.homeDir, pane)).find(Boolean) : undefined;
|
|
4630
|
+
if (!live)
|
|
4631
|
+
return undefined;
|
|
4632
|
+
if (live.status === "busy")
|
|
4633
|
+
return false;
|
|
4634
|
+
if (live.status !== "idle")
|
|
4635
|
+
return undefined;
|
|
4636
|
+
const bound = jsonlOf(target);
|
|
4637
|
+
// resume / fork 会换 sid: 悬着的工具调用记在注册表报的当前那份 transcript 里。
|
|
4638
|
+
const jsonl = bound ? join(dirname(bound), `${live.sessionId}.jsonl`) : "";
|
|
4639
|
+
return !(jsonl && existsSync(jsonl) && openToolUses(jsonl).length > 0);
|
|
4640
|
+
};
|
|
4641
|
+
const idleNow = async (target) => idleVerdict(target) ?? !(await isBusy(target));
|
|
4642
|
+
// 等到它闲下来, 事件驱动: 注册表目录一有写入就重判, 翻成 idle 的那一刻即返回。
|
|
4643
|
+
// 兜底轮询只防 watch 漏事件 (以及悬着的审批被点掉后 status 没再动的边角)。
|
|
4644
|
+
const IDLE_SAFETY_POLL_MS = 1000;
|
|
4645
|
+
const untilIdle = (target, timeoutMs) => {
|
|
4646
|
+
if (idleVerdict(target) === undefined)
|
|
4647
|
+
return waitForIdle(target, isBusy, timeoutMs, () => false, { rampMs: 0, confirm: 2 });
|
|
4648
|
+
return new Promise((resolve) => {
|
|
4649
|
+
let done = false;
|
|
4650
|
+
const finish = (r) => {
|
|
4651
|
+
if (done)
|
|
4652
|
+
return;
|
|
4653
|
+
done = true;
|
|
4654
|
+
unwatch.forEach((f) => f());
|
|
4655
|
+
clearInterval(tick);
|
|
4656
|
+
clearTimeout(timer);
|
|
4657
|
+
resolve(r);
|
|
4658
|
+
};
|
|
4659
|
+
// 会话中途退出 (注册表不认得了): pane 也不忙就是「那里什么都没在跑」, 与 isBusy 同一口径。
|
|
4660
|
+
const check = async () => {
|
|
4661
|
+
if (done)
|
|
4662
|
+
return;
|
|
4663
|
+
const v = idleVerdict(target);
|
|
4664
|
+
if (v ?? !(await isBusy(target)))
|
|
4665
|
+
finish({ idle: true });
|
|
4666
|
+
};
|
|
4667
|
+
const unwatch = activeBackends().map((b) => watchRegistry(b.homeDir, () => void check()));
|
|
4668
|
+
const tick = setInterval(() => void check(), IDLE_SAFETY_POLL_MS);
|
|
4669
|
+
const timer = setTimeout(() => finish({ idle: false, reason: "idle wait timed out" }), timeoutMs);
|
|
4670
|
+
void check();
|
|
4671
|
+
});
|
|
4672
|
+
};
|
|
4551
4673
|
const lastText = (target) => {
|
|
4552
4674
|
const p = jsonlOf(target);
|
|
4553
4675
|
return p ? lastAssistantText(p, 4000, warmerSigs) : "";
|
|
@@ -4556,6 +4678,15 @@ export const startMirror = (deps) => {
|
|
|
4556
4678
|
const p = jsonlOf(target);
|
|
4557
4679
|
return p ? lastReply(p, sinceMs, warmerSigs) : "";
|
|
4558
4680
|
};
|
|
4681
|
+
const replyToPeerSince = (target, fromName, sinceMs = 0) => {
|
|
4682
|
+
const p = jsonlOf(target);
|
|
4683
|
+
return p ? replyToPeerIn(p, fromName, sinceMs, warmerSigs) : undefined;
|
|
4684
|
+
};
|
|
4685
|
+
/** 绑着的 pane 还能用 (存在、且不是别人的窗口) —— 不重生、不猜。 */
|
|
4686
|
+
const paneLiveOf = async (target) => {
|
|
4687
|
+
const a = byTarget.get(target);
|
|
4688
|
+
return a?.tmuxPane ? paneUsable(a.tmuxPane, a.jsonlPath) : false;
|
|
4689
|
+
};
|
|
4559
4690
|
// ── 未收口气泡的引用判定 ────────────────────────────────────────────
|
|
4560
4691
|
// tag 会话的 last stream 未收口时, 群里最新那条气泡是瞬态的: 详情链接 URL +
|
|
4561
4692
|
// 最新一条实时 CoT/工具行 (brief) 或累积中的 acc (非 brief)。引用它要的是路由,
|
|
@@ -5018,6 +5149,8 @@ export const startMirror = (deps) => {
|
|
|
5018
5149
|
chatRoster,
|
|
5019
5150
|
peekTurns,
|
|
5020
5151
|
isBusy,
|
|
5152
|
+
idleNow,
|
|
5153
|
+
untilIdle,
|
|
5021
5154
|
setModel: async (target, wanted, scope) => {
|
|
5022
5155
|
const a = byTarget.get(target);
|
|
5023
5156
|
if (!a?.tmuxPane || !(await tmuxPaneAlive(a.tmuxPane)))
|
|
@@ -5044,6 +5177,9 @@ export const startMirror = (deps) => {
|
|
|
5044
5177
|
}),
|
|
5045
5178
|
lastText,
|
|
5046
5179
|
lastReply: replySince,
|
|
5180
|
+
replyToPeer: replyToPeerSince,
|
|
5181
|
+
paneLive: paneLiveOf,
|
|
5182
|
+
restored,
|
|
5047
5183
|
isOpenBubbleQuote,
|
|
5048
5184
|
status: () => {
|
|
5049
5185
|
const list = Array.from(bySessionId.values()).map((a) => ({
|
|
@@ -5213,8 +5349,12 @@ export const startMirror = (deps) => {
|
|
|
5213
5349
|
if (!text.trim())
|
|
5214
5350
|
return { ok: false, reason: "empty text" };
|
|
5215
5351
|
// 出处改写要早于 inject: tail 可能在 promise resolve 之前就开出 turn。
|
|
5216
|
-
|
|
5217
|
-
|
|
5352
|
+
// 守护进程注入的一轮 (回执 / peer / 定时任务 / graph) 不是人在 CLI 手敲的: 可见性
|
|
5353
|
+
// 只由频道定 ("" 私聊才静默)。pendingFromCli 必须一并清 —— 它是粘性的, 不开轮的
|
|
5354
|
+
// CLI 行 (spawn 时 model-select 按下的 `/model`) 会把它一直挂到下一轮, 让紧跟着的
|
|
5355
|
+
// 注入轮被认成 CLI 发起, 公开频道里的回复因此被出处门吞掉。
|
|
5356
|
+
a.pendingFromCli = false;
|
|
5357
|
+
a.turnFromChat = true;
|
|
5218
5358
|
// 名册增量搭这趟车进去 —— 同伴派活 / 定时任务 / graph 步骤都走这里, 所以
|
|
5219
5359
|
// 一个从不被人直接说话的分身也能知道群里多了谁、少了谁。人说的话那一条
|
|
5220
5360
|
// 路径在 inbound.send 上挂 (dispatch 有自己的 inject)。
|
|
@@ -5237,9 +5377,6 @@ export const startMirror = (deps) => {
|
|
|
5237
5377
|
// 注入的那一行会被 recentInjects 当回显吞掉, 走不到 user_text —— 本轮的 query
|
|
5238
5378
|
// (rolepage 上「谁对它说了什么」那一半) 只能在这里记。
|
|
5239
5379
|
a.pendingBriefQuery = text;
|
|
5240
|
-
// 公开频道里说的话, 回复就该在群里看得见 —— 哪怕上一轮是人在 CLI 里敲的。
|
|
5241
|
-
if (opts?.channel)
|
|
5242
|
-
a.turnFromChat = true;
|
|
5243
5380
|
const sid = a.sessionId;
|
|
5244
5381
|
const paneAlive = a.tmuxPane ? await paneUsable(a.tmuxPane, a.jsonlPath) : false;
|
|
5245
5382
|
if (!paneAlive) {
|
|
@@ -5262,10 +5399,22 @@ export const startMirror = (deps) => {
|
|
|
5262
5399
|
// 同 dispatch: 在 inject 之前解除静默, 防止验证循环中 LLM 回复被永久吞掉。
|
|
5263
5400
|
a.muteUntilInject = false;
|
|
5264
5401
|
a.justSpawned = false;
|
|
5402
|
+
// 同 dispatch 的 /clear 与静默分叉跟随: self-handoff / 同伴 / 定时任务也走这里注入,
|
|
5403
|
+
// 漏了它 tail 就钉在旧 jsonl 上, 直到下一条人话经 dispatch 才把它拽回来。
|
|
5404
|
+
const clearing = isClearCommand(text);
|
|
5405
|
+
const preClearBaseline = clearing ? listJsonls(dirname(a.jsonlPath)) : undefined;
|
|
5265
5406
|
const r = await inject({
|
|
5266
5407
|
text: full, images: [], cfg, log: log.child({ principal: target, sessionId: sid, sub: "init-demo" }),
|
|
5267
5408
|
sessionId: sid, jsonlPath: a.jsonlPath, tmuxTarget: a.tmuxPane, freshSpawn: true,
|
|
5268
5409
|
});
|
|
5410
|
+
if (r.ok && a.tmuxPane) {
|
|
5411
|
+
if (preClearBaseline) {
|
|
5412
|
+
a.clearRebind = { baseline: preClearBaseline };
|
|
5413
|
+
startMigrationWatcher(a, preClearBaseline, jsonlIsPostClearChild, 0, true);
|
|
5414
|
+
}
|
|
5415
|
+
else
|
|
5416
|
+
armSilentForkRebind(a, full);
|
|
5417
|
+
}
|
|
5269
5418
|
return r;
|
|
5270
5419
|
},
|
|
5271
5420
|
interruptPane: async (target, opts) => {
|