wezard 2.0.1 → 2.0.3
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 +14 -25
- package/dist/daemon/detail.js +1 -2
- package/dist/daemon/detail.js.map +1 -1
- package/dist/daemon/inbound.js +35 -12
- package/dist/daemon/inbound.js.map +1 -1
- package/dist/daemon/index.js +33 -26
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/mirror-bridge.js +69 -37
- package/dist/daemon/mirror-bridge.js.map +1 -1
- package/dist/daemon/wizard.js +24 -1
- package/dist/daemon/wizard.js.map +1 -1
- package/dist/mcp/server.js +2 -2
- package/dist/mcp/server.js.map +1 -1
- package/dist/shared/chat-http.js +41 -15
- package/dist/shared/chat-http.js.map +1 -1
- package/dist/shared/chat-render.js +11 -8
- 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 +35 -16
- package/dist/shared/detail-render.js.map +1 -1
- package/dist/shared/detail-store.js +13 -2
- package/dist/shared/detail-store.js.map +1 -1
- package/dist/shared/role-view.js +27 -7
- package/dist/shared/role-view.js.map +1 -1
- package/package.json +1 -1
- package/web/chat.css +83 -44
- package/web/chat.js +141 -90
|
@@ -1655,6 +1655,9 @@ export const startMirror = (deps) => {
|
|
|
1655
1655
|
// for >60s mid-turn and we don't want to drop the bubble while it's chewing.
|
|
1656
1656
|
const FLUSH_MS = 250;
|
|
1657
1657
|
const HARD_TIMEOUT_MS = 350_000;
|
|
1658
|
+
/** hard cap 到点时的中间结束语: 气泡必须 finish=true, 但 turn 还在跑 —— 明说
|
|
1659
|
+
* 「没完, 去链接看」, 而不是让气泡停在一句看似说完的话 / 一行过期的进度上。 */
|
|
1660
|
+
const STILL_WORKING = "⏳ 仍在处理中, 点击链接查看详情…";
|
|
1658
1661
|
/** 软收口静默期。后端只能说"这条消息写完了"(codebuddy) 时, 等这么久没有新 item
|
|
1659
1662
|
* 才认定一轮结束。取值只需盖住"叙述消息落盘 → 紧随其后的 function_call 落盘"
|
|
1660
1663
|
* 这一段, 与模型思考/工具执行时长无关。
|
|
@@ -1721,7 +1724,7 @@ export const startMirror = (deps) => {
|
|
|
1721
1724
|
return;
|
|
1722
1725
|
s.flushTimer = setTimeout(() => void flushStream(s), FLUSH_MS);
|
|
1723
1726
|
};
|
|
1724
|
-
const finalizeStream = async (a, s) => {
|
|
1727
|
+
const finalizeStream = async (a, s, timedOut = false) => {
|
|
1725
1728
|
if (s.closed)
|
|
1726
1729
|
return;
|
|
1727
1730
|
s.closed = true;
|
|
@@ -1743,17 +1746,20 @@ export const startMirror = (deps) => {
|
|
|
1743
1746
|
// 没正文不写: finish=true 是整条覆盖, 空正文只会把 "…" ack 覆盖成光秃秃的
|
|
1744
1747
|
// 链接头, 结束处理自己制造错发。气泡保持现有内容, 由 WeCom 6min 窗口自然
|
|
1745
1748
|
// 到期。dead 流同理只做本地清理。
|
|
1746
|
-
|
|
1749
|
+
// 例外是 hard cap 超时: turn 还在跑, 收口时缀上中间结束语 —— 这时即使没正文
|
|
1750
|
+
// 也有话可说, 不算空覆盖。
|
|
1751
|
+
const body = timedOut ? [s.acc, STILL_WORKING].filter((x) => x.trim()).join("\n\n") : s.acc;
|
|
1752
|
+
if (!s.dead && body.trim()) {
|
|
1747
1753
|
const card = detailCardFor(s, a.target);
|
|
1748
1754
|
try {
|
|
1749
1755
|
if (card) {
|
|
1750
1756
|
s.cardSent = true;
|
|
1751
|
-
await client.replyStreamWithCard(s.frame, s.streamId, withLinkedTag(a,
|
|
1757
|
+
await client.replyStreamWithCard(s.frame, s.streamId, withLinkedTag(a, body, undefined, s.turnId), true, { templateCard: card });
|
|
1752
1758
|
}
|
|
1753
1759
|
else {
|
|
1754
|
-
await client.replyStream(s.frame, s.streamId, withLinkedTag(a,
|
|
1760
|
+
await client.replyStream(s.frame, s.streamId, withLinkedTag(a, body, undefined, s.turnId), true);
|
|
1755
1761
|
}
|
|
1756
|
-
log.info({ sessionId: a.sessionId, turnId: s.turnId, accLen: s.acc.length, tools: s.tools.length, withCard: !!card }, "stream finalize");
|
|
1762
|
+
log.info({ sessionId: a.sessionId, turnId: s.turnId, accLen: s.acc.length, tools: s.tools.length, withCard: !!card, timedOut }, "stream finalize");
|
|
1757
1763
|
}
|
|
1758
1764
|
catch (e) {
|
|
1759
1765
|
log.warn({ sessionId: a.sessionId, turnId: s.turnId, err: e.message }, "stream finalize failed");
|
|
@@ -1783,7 +1789,7 @@ export const startMirror = (deps) => {
|
|
|
1783
1789
|
capped: false, closed: false, dead: false, cardSent: false,
|
|
1784
1790
|
tools: [], sawTool: false,
|
|
1785
1791
|
};
|
|
1786
|
-
s.hardTimer = setTimeout(() => void finalizeStream(a, s), HARD_TIMEOUT_MS);
|
|
1792
|
+
s.hardTimer = setTimeout(() => void finalizeStream(a, s, true), HARD_TIMEOUT_MS);
|
|
1787
1793
|
log.info({ sessionId: a.sessionId, turnId: s.turnId, streamId }, "stream open");
|
|
1788
1794
|
return s;
|
|
1789
1795
|
};
|
|
@@ -2261,11 +2267,11 @@ export const startMirror = (deps) => {
|
|
|
2261
2267
|
const bubble = { frame, streamId, hardTimer: undefined, done: false };
|
|
2262
2268
|
const q = { turnId, bubble, isSlash };
|
|
2263
2269
|
bubble.hardTimer = setTimeout(() => {
|
|
2264
|
-
// WeCom ~6min stream 窗口快到, 必须 finish=true
|
|
2265
|
-
//
|
|
2266
|
-
//
|
|
2267
|
-
const
|
|
2268
|
-
void finishBubble(a, bubble, `${briefDetailLink(turnId, a.target)}${
|
|
2270
|
+
// WeCom ~6min stream 窗口快到, 必须 finish=true 收口。finish 是整条替换 ——
|
|
2271
|
+
// 定格在一行过期的 CoT 进度上看着像卡死, 光链接又像说完了; 气泡仍是本轮活跃
|
|
2272
|
+
// 气泡 (turn 还在跑) 就明说「仍在处理中」, 已被顶替的旧轮只留链接。
|
|
2273
|
+
const live = a.briefBubble === bubble && !a.briefConcluded;
|
|
2274
|
+
void finishBubble(a, bubble, `${briefDetailLink(turnId, a.target)}${live ? ` ${STILL_WORKING}` : ""}`, true);
|
|
2269
2275
|
}, HARD_TIMEOUT_MS);
|
|
2270
2276
|
// 新消息 = 对话边界: 立刻收掉上一 turn, 新 turn 直接激活、不排队。收口语义见
|
|
2271
2277
|
// closeBriefTurn: 有正文收入旧气泡, 没正文不写一个字 (绝不因边界结束凭空新发/
|
|
@@ -4023,6 +4029,9 @@ export const startMirror = (deps) => {
|
|
|
4023
4029
|
// 由 index.ts 在启动时装上 (注册表活在那边); 没装 = 退回无身份行为, 全链路无回归。
|
|
4024
4030
|
let charterOf;
|
|
4025
4031
|
const setCharterProvider = (fn) => { charterOf = fn; };
|
|
4032
|
+
// 新 pane 落地后的钩子 —— pane 上限在这里数 (由 index.ts 装上; 没装 = 不数)。
|
|
4033
|
+
let afterSpawn = () => { };
|
|
4034
|
+
const onSpawn = (fn) => { afterSpawn = fn; };
|
|
4026
4035
|
// `cwd` = 这个 pane 正要启动的目录。attach 在 spawn 之后才发生, 所以此刻 getCwd
|
|
4027
4036
|
// 给的还是上一个 pane 的目录 —— 换目录重开时照它渲染, 新 wizard 会以为自己还在
|
|
4028
4037
|
// 旧工作区里。
|
|
@@ -4143,6 +4152,7 @@ export const startMirror = (deps) => {
|
|
|
4143
4152
|
}
|
|
4144
4153
|
if (!opts?.silent)
|
|
4145
4154
|
pushProjectInfo(target, "created");
|
|
4155
|
+
afterSpawn();
|
|
4146
4156
|
return { ok: true, sessionId: r.sessionId, cwd: r.cwd, model: r.model, modelWarning: r.modelWarning };
|
|
4147
4157
|
};
|
|
4148
4158
|
// ── Clone ───────────────────────────────────────────────────────────
|
|
@@ -4253,6 +4263,7 @@ export const startMirror = (deps) => {
|
|
|
4253
4263
|
persistPause(spawned);
|
|
4254
4264
|
}
|
|
4255
4265
|
lg.info({ parent: args.parent, sessionId: fork.sessionId }, "clone: forked");
|
|
4266
|
+
afterSpawn();
|
|
4256
4267
|
return { ok: true, sessionId: fork.sessionId, cwd: r.cwd, inherited: true, model: r.model, modelWarning: r.modelWarning };
|
|
4257
4268
|
};
|
|
4258
4269
|
/** 一个会话此刻的硬事实 —— whoami / 交接判断要用的那几个数。 */
|
|
@@ -5209,45 +5220,64 @@ export const startMirror = (deps) => {
|
|
|
5209
5220
|
log.info({ target, sessionId: a?.sessionId, pane }, "mirror /kill — pane killed, binding dropped");
|
|
5210
5221
|
return { ok: true };
|
|
5211
5222
|
},
|
|
5212
|
-
|
|
5223
|
+
lastActivity: (target) => {
|
|
5224
|
+
const jsonl = jsonlOf(target);
|
|
5225
|
+
try {
|
|
5226
|
+
return jsonl ? statSync(jsonl).mtimeMs : 0;
|
|
5227
|
+
}
|
|
5228
|
+
catch {
|
|
5229
|
+
return 0;
|
|
5230
|
+
}
|
|
5231
|
+
},
|
|
5232
|
+
reapOverflow: async (max, skip) => {
|
|
5213
5233
|
const now = Date.now();
|
|
5214
5234
|
const reaped = [];
|
|
5215
5235
|
const skipped = {};
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
const
|
|
5224
|
-
|
|
5236
|
+
// 一次 list-panes 同时回答「活着吗」和「有人盯着吗」—— 跑了几个月的机器上
|
|
5237
|
+
// allTargets 是几百个 key, 见 target 就问 tmux 会打穿 fd (同 worldPeers 的
|
|
5238
|
+
// WORLD_PROBE 取舍)。
|
|
5239
|
+
const snap = await runTmux(["list-panes", "-a", "-F", "#{pane_id}\t#{window_active}:#{session_attached}"]);
|
|
5240
|
+
if (snap.code !== 0)
|
|
5241
|
+
return { reaped, skipped };
|
|
5242
|
+
const watchedBy = new Map(snap.stdout.split("\n").filter(Boolean).map((l) => {
|
|
5243
|
+
const [id, w = ""] = l.split("\t");
|
|
5244
|
+
const [wActive, sAttached] = w.split(":");
|
|
5245
|
+
return [id, wActive === "1" && sAttached !== "0"];
|
|
5246
|
+
}));
|
|
5247
|
+
const live = allTargets().filter((t) => watchedBy.has(paneOf(t)));
|
|
5248
|
+
let over = live.length - max;
|
|
5249
|
+
if (over <= 0)
|
|
5250
|
+
return { reaped, skipped };
|
|
5251
|
+
// 超了才 stat: 最久没动的先收。还没写过 transcript 的是刚起的 pane, 按"此刻"算。
|
|
5252
|
+
const mtimeOf = (target) => {
|
|
5225
5253
|
try {
|
|
5226
|
-
|
|
5227
|
-
mtime = statSync(jsonl).mtimeMs;
|
|
5254
|
+
return statSync(jsonlOf(target)).mtimeMs;
|
|
5228
5255
|
}
|
|
5229
|
-
catch {
|
|
5230
|
-
|
|
5231
|
-
|
|
5256
|
+
catch {
|
|
5257
|
+
return now;
|
|
5258
|
+
}
|
|
5259
|
+
};
|
|
5260
|
+
const oldestFirst = live.map((target) => ({ target, mtime: mtimeOf(target) })).sort((x, y) => x.mtime - y.mtime);
|
|
5261
|
+
for (const { target, mtime } of oldestFirst) {
|
|
5262
|
+
if (over <= 0)
|
|
5263
|
+
break;
|
|
5264
|
+
const a = byTarget.get(target);
|
|
5265
|
+
const sessionId = a?.sessionId || deps.store.get(target)?.sessionId || "";
|
|
5232
5266
|
const veto = skip?.(target, sessionId);
|
|
5233
5267
|
if (veto) {
|
|
5234
5268
|
skipped[target] = veto;
|
|
5235
5269
|
continue;
|
|
5236
5270
|
}
|
|
5237
5271
|
const pane = paneOf(target);
|
|
5238
|
-
if (!pane || !(await tmuxPaneAlive(pane)))
|
|
5239
|
-
continue; // pane 已死, 没什么可收的
|
|
5240
|
-
if (paneIsBusy(await capturePaneTail(pane, 12))) {
|
|
5241
|
-
skipped[target] = "busy";
|
|
5242
|
-
continue;
|
|
5243
|
-
}
|
|
5244
5272
|
// 人正盯着这个 window —— 收掉他的视野比省下那点内存更讨嫌。
|
|
5245
|
-
|
|
5246
|
-
const [wActive, sAttached] = w.stdout.trim().split(":");
|
|
5247
|
-
if (wActive === "1" && sAttached !== "0") {
|
|
5273
|
+
if (watchedBy.get(pane)) {
|
|
5248
5274
|
skipped[target] = "watched";
|
|
5249
5275
|
continue;
|
|
5250
5276
|
}
|
|
5277
|
+
if (paneIsBusy(await capturePaneTail(pane, 12))) {
|
|
5278
|
+
skipped[target] = "busy";
|
|
5279
|
+
continue;
|
|
5280
|
+
}
|
|
5251
5281
|
// 与 killPane 同款前戏: Esc 给生成中的 CLI 一个收束落盘的机会。
|
|
5252
5282
|
await runTmux(["send-keys", "-t", pane, "Escape"]);
|
|
5253
5283
|
await sleep(250);
|
|
@@ -5257,10 +5287,11 @@ export const startMirror = (deps) => {
|
|
|
5257
5287
|
continue;
|
|
5258
5288
|
}
|
|
5259
5289
|
if (a)
|
|
5260
|
-
detach(a, "
|
|
5290
|
+
detach(a, "pane cap");
|
|
5261
5291
|
// 不碰 store —— 绑定留着, 下一条 inbound 走 dead-pane `--resume` 复活。
|
|
5262
5292
|
reaped.push(target);
|
|
5263
|
-
|
|
5293
|
+
over -= 1;
|
|
5294
|
+
log.info({ target, sessionId, pane, idleHours: Math.round((now - mtime) / 3.6e6), max }, "mirror pane cap — pane killed, binding kept");
|
|
5264
5295
|
}
|
|
5265
5296
|
return { reaped, skipped };
|
|
5266
5297
|
},
|
|
@@ -5351,6 +5382,7 @@ export const startMirror = (deps) => {
|
|
|
5351
5382
|
cloneSession,
|
|
5352
5383
|
sessionInfo,
|
|
5353
5384
|
setCharterProvider,
|
|
5385
|
+
onSpawn,
|
|
5354
5386
|
shutdown: () => {
|
|
5355
5387
|
clearInterval(paneDriftTimer);
|
|
5356
5388
|
clearInterval(keepaliveTimer);
|