wezard 2.0.4 → 2.0.6
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/cc-session.js +2 -0
- package/dist/daemon/cc-session.js.map +1 -1
- package/dist/daemon/inbound.js +25 -34
- package/dist/daemon/inbound.js.map +1 -1
- package/dist/daemon/index.js +2 -2
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/mirror-bridge.js +66 -24
- package/dist/daemon/mirror-bridge.js.map +1 -1
- package/dist/daemon/peers.js +11 -0
- package/dist/daemon/peers.js.map +1 -1
- package/dist/shared/detail-render.js +147 -69
- package/dist/shared/detail-render.js.map +1 -1
- package/dist/shared/role-render.js +11 -6
- package/dist/shared/role-render.js.map +1 -1
- package/dist/shared/role-view.js +9 -4
- package/dist/shared/role-view.js.map +1 -1
- package/package.json +1 -1
- package/web/chat.css +59 -68
- package/web/chat.js +27 -29
|
@@ -26,7 +26,7 @@ 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, submittedSince } from "./cc-session.js";
|
|
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
32
|
import { recordTool, recordToolResult, recordMark, recordTurnStart, recordTurnItem, recordTurnUsage, recordTurnClose, recordCloseOpenTurns, buildDetailUrl, buildChatUrl } from "./detail.js";
|
|
@@ -34,7 +34,7 @@ import { labelFor, tagOfKey, baseOfKey, keyOf, stripSigil, displayName, withTagH
|
|
|
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, compactPane, paneIsBusy, paneIsStalled, transcriptStalled, summarizeTail, lastAssistantText, lastReply, unwrapPasted, lastContextTokens, keepaliveStamps, tailTurns, renderDialog } from "./peers.js";
|
|
37
|
+
import { stripAnsi, compactPane, paneIsBusy, paneIsStalled, transcriptStalled, summarizeTail, lastAssistantText, lastReply, unwrapPasted, lastContextTokens, keepaliveStamps, openKeepalivePing, tailTurns, 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
|
|
@@ -1565,6 +1565,9 @@ const queues = new Map();
|
|
|
1565
1565
|
* behind it forever. Well under brief's 350s bubble ceiling so the user still
|
|
1566
1566
|
* gets an error bubble rather than a silent stream timeout. */
|
|
1567
1567
|
const INJECT_JOB_TIMEOUT_MS = 90_000;
|
|
1568
|
+
/** 没有绑定的 pane 静默多久才算孤儿。刚 spawn、还没 attach 的 pane (并发 fan-out 时
|
|
1569
|
+
* 别人的 sweep 会撞见它) 在这段时间里一直有输出, 远大于一次 spawn → attach 的耗时。 */
|
|
1570
|
+
const ORPHAN_GRACE_MS = 10 * 60_000;
|
|
1568
1571
|
/** Drop a session's queue chain. A hung job can't be cancelled (it's parked in a
|
|
1569
1572
|
* tmux read), but detaching the chain means the NEXT message doesn't inherit
|
|
1570
1573
|
* its deadlock. Pair with an injectGen bump so the zombie can't act when it
|
|
@@ -2830,6 +2833,7 @@ export const startMirror = (deps) => {
|
|
|
2830
2833
|
// slower than the 60s fail-safe. Reuses the detail turn the timer flow may
|
|
2831
2834
|
// have opened (fireKeepalive skips opening when this got there first).
|
|
2832
2835
|
const KEEPALIVE_CONTENT_FAILSAFE_MS = 5 * 60_000;
|
|
2836
|
+
const PONG_RE = /pong/i;
|
|
2833
2837
|
const beginKeepaliveByContent = (a, text) => {
|
|
2834
2838
|
a.keepaliveByContent = true;
|
|
2835
2839
|
if (a.keepaliveContentTimer)
|
|
@@ -2883,6 +2887,19 @@ export const startMirror = (deps) => {
|
|
|
2883
2887
|
// keepalive 门之前: 吞没窗口里到达也必须销账, 否则账本永久悬空。
|
|
2884
2888
|
if (item.kind === "skill_output" && item.agentDone)
|
|
2885
2889
|
clearBackgroundAgent(a, item.agentDone);
|
|
2890
|
+
// Reply-side belt: a "pong" arriving with NO swallow holding. Both triggers
|
|
2891
|
+
// above live in memory, and the tail re-attaches at EOF — a reload between
|
|
2892
|
+
// the ping and its reply forgets the ping was ever sent, and the pong opens
|
|
2893
|
+
// a chat turn of its own. So ask the transcript: if the last thing said to
|
|
2894
|
+
// the model is a keepalive ping, this text is its pong — swallow from here
|
|
2895
|
+
// to the turn_end, recorded as the same marked detail turn. Gated on the
|
|
2896
|
+
// transcript, not on the word alone: a real answer that mentions pong has a
|
|
2897
|
+
// real question in front of it.
|
|
2898
|
+
if (item.kind === "text" && !a.keepaliveQuiet && !a.keepaliveByContent && PONG_RE.test(item.body)) {
|
|
2899
|
+
const ping = openKeepalivePing(a.jsonlPath, keepalivePingSigs);
|
|
2900
|
+
if (ping)
|
|
2901
|
+
beginKeepaliveByContent(a, ping);
|
|
2902
|
+
}
|
|
2886
2903
|
if (a.keepaliveQuiet || a.keepaliveByContent) {
|
|
2887
2904
|
const id = a.keepaliveTurnId;
|
|
2888
2905
|
if (id) {
|
|
@@ -5285,18 +5302,55 @@ export const startMirror = (deps) => {
|
|
|
5285
5302
|
// 一次 list-panes 同时回答「活着吗」和「有人盯着吗」—— 跑了几个月的机器上
|
|
5286
5303
|
// allTargets 是几百个 key, 见 target 就问 tmux 会打穿 fd (同 worldPeers 的
|
|
5287
5304
|
// WORLD_PROBE 取舍)。
|
|
5288
|
-
const
|
|
5305
|
+
const orphans = [];
|
|
5306
|
+
const snap = await runTmux(["list-panes", "-a", "-F", "#{pane_id}\t#{window_active}:#{session_attached}\t#{session_name}\t#{window_activity}"]);
|
|
5289
5307
|
if (snap.code !== 0)
|
|
5290
|
-
return { reaped, skipped };
|
|
5291
|
-
const
|
|
5292
|
-
|
|
5308
|
+
return { reaped, orphans, skipped };
|
|
5309
|
+
const rows = snap.stdout.split("\n").filter(Boolean).map((l) => l.split("\t"));
|
|
5310
|
+
const watchedBy = new Map(rows.map(([id = "", w = ""]) => {
|
|
5293
5311
|
const [wActive, sAttached] = w.split(":");
|
|
5294
5312
|
return [id, wActive === "1" && sAttached !== "0"];
|
|
5295
5313
|
}));
|
|
5296
|
-
|
|
5314
|
+
// 收一个 pane; 返回没收成的原因, 收掉了返回 undefined。
|
|
5315
|
+
const putDown = async (pane) => {
|
|
5316
|
+
// 人正盯着这个 window —— 收掉他的视野比省下那点内存更讨嫌。
|
|
5317
|
+
if (watchedBy.get(pane))
|
|
5318
|
+
return "watched";
|
|
5319
|
+
if (paneIsBusy(await capturePaneTail(pane, 12)))
|
|
5320
|
+
return "busy";
|
|
5321
|
+
// 与 killPane 同款前戏: Esc 给生成中的 CLI 一个收束落盘的机会。
|
|
5322
|
+
await runTmux(["send-keys", "-t", pane, "Escape"]);
|
|
5323
|
+
await sleep(250);
|
|
5324
|
+
const r = await runTmux(["kill-pane", "-t", pane]);
|
|
5325
|
+
return r.code === 0 ? undefined : `kill-pane failed: ${r.stdout.slice(-120) || r.code}`;
|
|
5326
|
+
};
|
|
5327
|
+
const targets = allTargets();
|
|
5328
|
+
// 孤儿: 住在 daemon 自己的 tmux session 里、却没有任何绑定指着它的 pane —— 绑定
|
|
5329
|
+
// 丢了 pane id (restore 认不出 → 下一条消息另起新 pane, 旧 pane 不杀) 之后留下的。
|
|
5330
|
+
// 没有地址能再叫到它, 却不在下面 `live` 的账上, 攒多少都触不到上限; 所以不占
|
|
5331
|
+
// 名额、不等超限, 见到就收。刚起的 pane 在 attach 之前也长这样 (并发 spawn),
|
|
5332
|
+
// 用 window_activity 留一段宽限。只收会话 pane: 注册表认得里面住着活的 AI 会话
|
|
5333
|
+
// 才算; 普通 pane (人自己开的 shell / 编辑器, 会话已退回 shell 的, 没有注册表的
|
|
5334
|
+
// 后端) 一律跳过 —— 认不出就不动。
|
|
5335
|
+
const bound = new Set(targets.map(paneOf).filter(Boolean));
|
|
5336
|
+
const hosting = new Set(activeBackends().flatMap((b) => [...sessionPanes(b.homeDir)]));
|
|
5337
|
+
for (const [pane = "", , session, activity] of rows) {
|
|
5338
|
+
if (session !== cfg.wrc.tmuxPrefix || bound.has(pane) || !hosting.has(pane))
|
|
5339
|
+
continue;
|
|
5340
|
+
if (now - Number(activity) * 1000 < ORPHAN_GRACE_MS)
|
|
5341
|
+
continue;
|
|
5342
|
+
const why = await putDown(pane);
|
|
5343
|
+
if (why) {
|
|
5344
|
+
skipped[pane] = why;
|
|
5345
|
+
continue;
|
|
5346
|
+
}
|
|
5347
|
+
orphans.push(pane);
|
|
5348
|
+
log.info({ pane, idleHours: Math.round((now - Number(activity) * 1000) / 3.6e6) }, "mirror pane cap — orphan pane killed (no binding)");
|
|
5349
|
+
}
|
|
5350
|
+
const live = targets.filter((t) => watchedBy.has(paneOf(t)));
|
|
5297
5351
|
let over = live.length - max;
|
|
5298
5352
|
if (over <= 0)
|
|
5299
|
-
return { reaped, skipped };
|
|
5353
|
+
return { reaped, orphans, skipped };
|
|
5300
5354
|
// 超了才 stat: 最久没动的先收。还没写过 transcript 的是刚起的 pane, 按"此刻"算。
|
|
5301
5355
|
const mtimeOf = (target) => {
|
|
5302
5356
|
try {
|
|
@@ -5318,21 +5372,9 @@ export const startMirror = (deps) => {
|
|
|
5318
5372
|
continue;
|
|
5319
5373
|
}
|
|
5320
5374
|
const pane = paneOf(target);
|
|
5321
|
-
|
|
5322
|
-
if (
|
|
5323
|
-
skipped[target] =
|
|
5324
|
-
continue;
|
|
5325
|
-
}
|
|
5326
|
-
if (paneIsBusy(await capturePaneTail(pane, 12))) {
|
|
5327
|
-
skipped[target] = "busy";
|
|
5328
|
-
continue;
|
|
5329
|
-
}
|
|
5330
|
-
// 与 killPane 同款前戏: Esc 给生成中的 CLI 一个收束落盘的机会。
|
|
5331
|
-
await runTmux(["send-keys", "-t", pane, "Escape"]);
|
|
5332
|
-
await sleep(250);
|
|
5333
|
-
const r = await runTmux(["kill-pane", "-t", pane]);
|
|
5334
|
-
if (r.code !== 0) {
|
|
5335
|
-
skipped[target] = `kill-pane failed: ${r.stdout.slice(-120) || r.code}`;
|
|
5375
|
+
const why = await putDown(pane);
|
|
5376
|
+
if (why) {
|
|
5377
|
+
skipped[target] = why;
|
|
5336
5378
|
continue;
|
|
5337
5379
|
}
|
|
5338
5380
|
if (a)
|
|
@@ -5342,7 +5384,7 @@ export const startMirror = (deps) => {
|
|
|
5342
5384
|
over -= 1;
|
|
5343
5385
|
log.info({ target, sessionId, pane, idleHours: Math.round((now - mtime) / 3.6e6), max }, "mirror pane cap — pane killed, binding kept");
|
|
5344
5386
|
}
|
|
5345
|
-
return { reaped, skipped };
|
|
5387
|
+
return { reaped, orphans, skipped };
|
|
5346
5388
|
},
|
|
5347
5389
|
submitPane: async (target) => {
|
|
5348
5390
|
const a = byTarget.get(target);
|