wezard 1.3.2 → 1.3.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/CLAUDE.md +2 -2
- package/README.md +34 -25
- package/cli/wezard.sh +101 -4
- package/commands/update.md +6 -0
- package/commands/wrc.md +1 -1
- package/dist/cli/init.js +1 -1
- package/dist/cli/init.js.map +1 -1
- package/dist/daemon/approval.js +2 -2
- package/dist/daemon/approval.js.map +1 -1
- package/dist/daemon/inbound.js +14 -8
- package/dist/daemon/inbound.js.map +1 -1
- package/dist/daemon/index.js +22 -10
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/mirror-bridge.js +135 -57
- package/dist/daemon/mirror-bridge.js.map +1 -1
- package/dist/daemon/tips.js +1 -1
- package/dist/daemon/tips.js.map +1 -1
- package/dist/mcp/server.js +23 -32
- package/dist/mcp/server.js.map +1 -1
- package/dist/shared/config.js +2 -2
- package/dist/shared/config.js.map +1 -1
- package/dist/shared/detail-render.js +2 -2
- package/dist/shared/detail-render.js.map +1 -1
- package/package.json +2 -1
|
@@ -465,11 +465,17 @@ const renderLine = (raw, deps) => {
|
|
|
465
465
|
const goal = goalStartOf(c);
|
|
466
466
|
if (goal)
|
|
467
467
|
return [goal];
|
|
468
|
-
if (!deps.includeUser)
|
|
469
|
-
return [];
|
|
470
468
|
const text = cleanUserText(c);
|
|
471
469
|
if (!text)
|
|
472
470
|
return []; // pure slash-command meta / stdout — drop
|
|
471
|
+
// Keepalive ping user line: judged by CONTENT, before every other gate
|
|
472
|
+
// (includeUser / isOwnInject's 60s TTL) — the ping never echoes, and
|
|
473
|
+
// the reply turn is swallowed by content in onItem. Timer windows can't
|
|
474
|
+
// cover reloads or replays; the user message itself always can.
|
|
475
|
+
if (deps.isKeepalivePing?.(text))
|
|
476
|
+
return [{ kind: "keepalive_start", body: text }];
|
|
477
|
+
if (!deps.includeUser)
|
|
478
|
+
return [];
|
|
473
479
|
if (deps.isOwnInject(text))
|
|
474
480
|
return []; // dedupe WeCom→CLI echo
|
|
475
481
|
const quoted = text.split("\n").map((l) => `> ${l}`).join("\n");
|
|
@@ -1414,6 +1420,19 @@ export const startMirror = (deps) => {
|
|
|
1414
1420
|
recentAssistantSends.splice(i, 1); // one-shot: a genuine later re-say still streams
|
|
1415
1421
|
return true;
|
|
1416
1422
|
};
|
|
1423
|
+
// Keepalive ping detector for the tail's user-line judgment — the SAME
|
|
1424
|
+
// normalized-prefix signature set keepaliveTick feeds keepaliveStamps
|
|
1425
|
+
// (every configured form + the bare "ping" legacy streak form), so
|
|
1426
|
+
// renderLine, the tick and the transcript parser all agree on what counts
|
|
1427
|
+
// as a ping. Content is the source of truth for the swallow: timers and
|
|
1428
|
+
// echo-TTLs lose it across reloads/replays.
|
|
1429
|
+
const keepalivePingSigs = [cfg.wrc.mirror.keepalive.ping, cfg.wrc.mirror.keepalive.resumePing]
|
|
1430
|
+
.map((p) => normAssistant(p).slice(0, 40))
|
|
1431
|
+
.filter((s) => s.length > 0);
|
|
1432
|
+
const isKeepalivePing = (text) => {
|
|
1433
|
+
const n = normAssistant(text);
|
|
1434
|
+
return n.toLowerCase() === "ping" || keepalivePingSigs.some((sig) => n.includes(sig));
|
|
1435
|
+
};
|
|
1417
1436
|
// ── Typewriter stream lifecycle ────────────────────────────────────
|
|
1418
1437
|
// WeCom spec: server polls us for stream refreshes for up to 6 min from the
|
|
1419
1438
|
// original inbound. SDK queues replyStream calls per req_id, sends serially
|
|
@@ -1818,7 +1837,7 @@ export const startMirror = (deps) => {
|
|
|
1818
1837
|
// 每个 turn 挂一条气泡: ack 时就写进 `tag 详情链接 …` (finish=false, 不立刻关掉),
|
|
1819
1838
|
// 让群里从收到消息那一刻起就有详情页入口。收口时:
|
|
1820
1839
|
// • 正文到位 → 以 `链接 正文` 覆盖这条气泡。
|
|
1821
|
-
// • 始终没正文 / 过 6min 窗口 →
|
|
1840
|
+
// • 始终没正文 / 过 6min 窗口 → 以最新 CoT 进度行 (没有则纯 `链接`) 收口。
|
|
1822
1841
|
// • 气泡已收口或无气泡 turn (CLI 侧发起) → 正文另发一条 standalone。
|
|
1823
1842
|
// 其它所有 item 只写入 turn detail store。
|
|
1824
1843
|
// 能证明"assistant 已经在产出"的 item —— 见到它们才补开无气泡 turn。
|
|
@@ -1857,6 +1876,7 @@ export const startMirror = (deps) => {
|
|
|
1857
1876
|
// 收口后这一路全部失效: 气泡要么已 done, 要么 turn 已经换人/清空。
|
|
1858
1877
|
if (!text || !b || b.done || !turnId || a.briefConcluded)
|
|
1859
1878
|
return;
|
|
1879
|
+
a.cotLastSent = text;
|
|
1860
1880
|
try {
|
|
1861
1881
|
await client.replyStream(b.frame, b.streamId, `${briefDetailLink(turnId, a.target)} \`${text}\``, false);
|
|
1862
1882
|
}
|
|
@@ -1951,8 +1971,11 @@ export const startMirror = (deps) => {
|
|
|
1951
1971
|
const bubble = { frame, streamId, hardTimer: undefined, done: false };
|
|
1952
1972
|
const q = { turnId, bubble, isSlash };
|
|
1953
1973
|
bubble.hardTimer = setTimeout(() => {
|
|
1954
|
-
// WeCom ~6min stream 窗口快到, 必须 finish=true
|
|
1955
|
-
|
|
1974
|
+
// WeCom ~6min stream 窗口快到, 必须 finish=true 收口。没正文时不能拿光链接
|
|
1975
|
+
// 覆盖 —— finish 是整条替换, 会把屏幕上的 CoT 进度行抹成光秃秃的链接; 改用
|
|
1976
|
+
// 最新进度行定格 (a 上的 CoT 只在气泡仍是本轮活跃气泡时才可信, 换轮后归新轮)。
|
|
1977
|
+
const cot = a.briefBubble === bubble && !a.briefConcluded ? a.cotText ?? a.cotLastSent : undefined;
|
|
1978
|
+
void finishBubble(a, bubble, `${briefDetailLink(turnId, a.target)}${cot ? ` \`${cot}\`` : ""}`, true);
|
|
1956
1979
|
}, HARD_TIMEOUT_MS);
|
|
1957
1980
|
// 新消息 = 对话边界: 立刻收掉上一 turn, 新 turn 直接激活、不排队。收口语义见
|
|
1958
1981
|
// closeBriefTurn: 有正文收入旧气泡, 没正文不写一个字 (绝不因边界结束凭空新发/
|
|
@@ -2232,13 +2255,41 @@ export const startMirror = (deps) => {
|
|
|
2232
2255
|
if (a.liveStream && !a.liveStream.closed)
|
|
2233
2256
|
void finalizeStream(a, a.liveStream);
|
|
2234
2257
|
};
|
|
2235
|
-
//
|
|
2236
|
-
//
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2258
|
+
// End the keepalive swallow — the ping turn is over (turn_end / genuine
|
|
2259
|
+
// user line / inject failure), so real items flow through onItem again.
|
|
2260
|
+
// Clears BOTH the timer window and the content flag.
|
|
2261
|
+
const endKeepaliveSwallow = (a) => {
|
|
2262
|
+
if (a.keepaliveQuiet) {
|
|
2263
|
+
clearTimeout(a.keepaliveQuiet);
|
|
2264
|
+
a.keepaliveQuiet = undefined;
|
|
2265
|
+
}
|
|
2266
|
+
if (a.keepaliveContentTimer) {
|
|
2267
|
+
clearTimeout(a.keepaliveContentTimer);
|
|
2268
|
+
a.keepaliveContentTimer = undefined;
|
|
2269
|
+
}
|
|
2270
|
+
a.keepaliveByContent = undefined;
|
|
2271
|
+
};
|
|
2272
|
+
// Content-based swallow entry: the tailed user line IS a configured ping.
|
|
2273
|
+
// Belt to the inject-time timer window — this one keys on the user message
|
|
2274
|
+
// itself, so it holds across daemon reloads, tail replays and replies
|
|
2275
|
+
// slower than the 60s fail-safe. Reuses the detail turn the timer flow may
|
|
2276
|
+
// have opened (fireKeepalive skips opening when this got there first).
|
|
2277
|
+
const KEEPALIVE_CONTENT_FAILSAFE_MS = 5 * 60_000;
|
|
2278
|
+
const beginKeepaliveByContent = (a, text) => {
|
|
2279
|
+
a.keepaliveByContent = true;
|
|
2280
|
+
if (a.keepaliveContentTimer)
|
|
2281
|
+
clearTimeout(a.keepaliveContentTimer);
|
|
2282
|
+
// Fail-safe: a ping turn whose reply never emits turn_end (crash/hang)
|
|
2283
|
+
// must not mute a later real turn forever.
|
|
2284
|
+
a.keepaliveContentTimer = setTimeout(() => {
|
|
2285
|
+
a.keepaliveContentTimer = undefined;
|
|
2286
|
+
a.keepaliveByContent = undefined;
|
|
2287
|
+
}, KEEPALIVE_CONTENT_FAILSAFE_MS);
|
|
2288
|
+
if (!a.keepaliveTurnId) {
|
|
2289
|
+
const turnId = newTurnId();
|
|
2290
|
+
a.keepaliveTurnId = turnId;
|
|
2291
|
+
recordTurnStart({ id: turnId, target: a.target, sessionId: a.sessionId, userQuery: text });
|
|
2292
|
+
}
|
|
2242
2293
|
};
|
|
2243
2294
|
// A keepalive ping turn is swallowed wholesale — the reply content doesn't
|
|
2244
2295
|
// matter (model may add extra commentary beyond "pong", that's fine).
|
|
@@ -2246,12 +2297,24 @@ export const startMirror = (deps) => {
|
|
|
2246
2297
|
// 新 spawn 的 pane 在首次 inject 落地前吞掉所有初始输出 (greeting/system)。
|
|
2247
2298
|
if (a.muteUntilInject)
|
|
2248
2299
|
return;
|
|
2249
|
-
// Keepalive ping turns are cache-warmers: swallow every item from
|
|
2250
|
-
//
|
|
2251
|
-
//
|
|
2252
|
-
//
|
|
2253
|
-
//
|
|
2254
|
-
|
|
2300
|
+
// Keepalive ping turns are cache-warmers: swallow every item from every
|
|
2301
|
+
// WeCom path so the ping/pong never reaches chat. Two triggers, either
|
|
2302
|
+
// suffices — the timer window opened at inject (keepaliveQuiet) OR the
|
|
2303
|
+
// user line itself matching a configured ping (keepaliveByContent; the
|
|
2304
|
+
// reload/replay/slow-reply-proof one). The REAL exchange is recorded
|
|
2305
|
+
// into its chat-detail turn — the actual assistant reply, the tool calls
|
|
2306
|
+
// if any, and the usage — so the detail page shows the genuine heartbeat.
|
|
2307
|
+
if (item.kind === "keepalive_start") {
|
|
2308
|
+
beginKeepaliveByContent(a, item.body);
|
|
2309
|
+
return;
|
|
2310
|
+
}
|
|
2311
|
+
// A genuine user line (not our inject, not a ping) is a hard turn
|
|
2312
|
+
// boundary — any keepalive swallow still holding never got its turn_end
|
|
2313
|
+
// (crashed reply). Release BEFORE the swallow gate so this real line (and
|
|
2314
|
+
// its reply) mirror normally instead of being eaten by the stale swallow.
|
|
2315
|
+
if (item.kind === "user_text")
|
|
2316
|
+
endKeepaliveSwallow(a);
|
|
2317
|
+
if (a.keepaliveQuiet || a.keepaliveByContent) {
|
|
2255
2318
|
const id = a.keepaliveTurnId;
|
|
2256
2319
|
if (id) {
|
|
2257
2320
|
const now = Date.now();
|
|
@@ -2270,7 +2333,7 @@ export const startMirror = (deps) => {
|
|
|
2270
2333
|
}
|
|
2271
2334
|
}
|
|
2272
2335
|
if (item.kind === "turn_end") {
|
|
2273
|
-
|
|
2336
|
+
endKeepaliveSwallow(a);
|
|
2274
2337
|
}
|
|
2275
2338
|
return;
|
|
2276
2339
|
}
|
|
@@ -2610,6 +2673,7 @@ export const startMirror = (deps) => {
|
|
|
2610
2673
|
toolUseInlineMaxChars: cfg.wrc.mirror.toolUseInlineMaxChars,
|
|
2611
2674
|
isOwnInject,
|
|
2612
2675
|
isOwnAssistantSend,
|
|
2676
|
+
isKeepalivePing,
|
|
2613
2677
|
onItem: (item) => onItem(a, item),
|
|
2614
2678
|
detailUrlFor,
|
|
2615
2679
|
sessionId,
|
|
@@ -2991,6 +3055,7 @@ export const startMirror = (deps) => {
|
|
|
2991
3055
|
toolUseInlineMaxChars: cfg.wrc.mirror.toolUseInlineMaxChars,
|
|
2992
3056
|
isOwnInject,
|
|
2993
3057
|
isOwnAssistantSend,
|
|
3058
|
+
isKeepalivePing,
|
|
2994
3059
|
onItem: (item) => onItem(a, item),
|
|
2995
3060
|
detailUrlFor,
|
|
2996
3061
|
sessionId: newSessionId,
|
|
@@ -3289,33 +3354,13 @@ export const startMirror = (deps) => {
|
|
|
3289
3354
|
// from the running cwd). Decoupling means /pwd can show truth even after
|
|
3290
3355
|
// the AI sets a new path but the user hasn't /new'd yet.
|
|
3291
3356
|
const expandedDefaultCwd = expandHome(cfg.wrc.cwd);
|
|
3292
|
-
// Long absolute paths wrap in the WeCom bubble — show only the trailing
|
|
3293
|
-
// three segments, which is enough to identify the project.
|
|
3294
|
-
const shortCwd = (p) => p.split("/").filter(Boolean).slice(-3).join("/");
|
|
3295
|
-
const renderProjectInfo = (target) => {
|
|
3296
|
-
const a = byTarget.get(target);
|
|
3297
|
-
const rec = a ? undefined : deps.store.get(target);
|
|
3298
|
-
const running = (a?.runningCwd?.trim()) || rec?.cwd?.trim() || expandedDefaultCwd;
|
|
3299
|
-
// pendingCwd is chat-scoped — the queued switch applies to every session
|
|
3300
|
-
// in this chat, so read it from the shared base slot rather than the
|
|
3301
|
-
// caller's own (now-empty) attachment record.
|
|
3302
|
-
const pending = chatCwdFallback(target).pending;
|
|
3303
|
-
const lines = [`📂 cwd: \`${shortCwd(running)}\``];
|
|
3304
|
-
if (pending && pending !== running) {
|
|
3305
|
-
lines.push(`下次切换: \`${shortCwd(pending)}\` (使用 /new 或 /clear 生效)`);
|
|
3306
|
-
}
|
|
3307
|
-
// Session-boundary footer — `/new` and `/clear` are the only two callers,
|
|
3308
|
-
// so the tip lands exactly once per fresh context, never mid-conversation.
|
|
3309
|
-
lines.push(randomTip());
|
|
3310
|
-
return lines.join("\n");
|
|
3311
|
-
};
|
|
3312
3357
|
// `header` folds the caller's ack ("created") into this same bubble — /new
|
|
3313
3358
|
// must land as exactly ONE WeCom message, not card + separate reply.
|
|
3314
|
-
//
|
|
3359
|
+
// 会话边界回执一律不携带 cwd 信息 (📂 项目行 / 下次切换), 需要时 `/pwd` 可查。
|
|
3360
|
+
// slashAckFirstLine 进一步省去 💡 tip, 只剩首行 ack。
|
|
3315
3361
|
const pushProjectInfo = (target, header) => {
|
|
3316
3362
|
const concise = cfg.wrc.mirror.slashAckFirstLine && !!header;
|
|
3317
|
-
const
|
|
3318
|
-
const md = header ? (info ? `${header}\n\n${info}` : header) : info;
|
|
3363
|
+
const md = header ? (concise ? header : `${header}\n\n${randomTip()}`) : randomTip();
|
|
3319
3364
|
const a = byTarget.get(target);
|
|
3320
3365
|
if (a) {
|
|
3321
3366
|
sendStandalone(a, md);
|
|
@@ -3332,7 +3377,7 @@ export const startMirror = (deps) => {
|
|
|
3332
3377
|
// (default + any `#tag` siblings) share one cwd/pendingCwd, tracked on the
|
|
3333
3378
|
// BASE principal's byTarget/store record. Tagged sessions still have their
|
|
3334
3379
|
// own `runningCwd` (the tmux pane's actual working dir at spawn time), but
|
|
3335
|
-
// cwd fallbacks and `
|
|
3380
|
+
// cwd fallbacks and `set_workspace` pendingCwd writes always resolve against the base.
|
|
3336
3381
|
const chatCwdFallback = (target) => {
|
|
3337
3382
|
const base = basePrincipalOf(target);
|
|
3338
3383
|
const baseA = byTarget.get(base);
|
|
@@ -3351,7 +3396,7 @@ export const startMirror = (deps) => {
|
|
|
3351
3396
|
// base.pending > caller.pending > target.running > base.running > default
|
|
3352
3397
|
// A fresh tagged session inherits the chat's current cwd; re-`/new`ing a
|
|
3353
3398
|
// live tagged session keeps its pane cwd unless the base session queued a
|
|
3354
|
-
//
|
|
3399
|
+
// cwd switch. This keeps siblings aligned by default without forcibly clobbering
|
|
3355
3400
|
// an already-spawned tagged pane on every base-cwd change.
|
|
3356
3401
|
const chat = chatCwdFallback(target);
|
|
3357
3402
|
const rec = !prev ? deps.store.get(target) : undefined;
|
|
@@ -3449,9 +3494,9 @@ export const startMirror = (deps) => {
|
|
|
3449
3494
|
return { ok: true, sessionId: r.sessionId, cwd: r.cwd };
|
|
3450
3495
|
};
|
|
3451
3496
|
const getCwd = (target) => {
|
|
3452
|
-
// pendingCwd is chat-scoped — a `
|
|
3453
|
-
// switch for the whole chat. runningCwd stays per-session (each
|
|
3454
|
-
// its own spawn dir).
|
|
3497
|
+
// pendingCwd is chat-scoped — a `set_workspace` from any sibling session
|
|
3498
|
+
// queues the switch for the whole chat. runningCwd stays per-session (each
|
|
3499
|
+
// pane has its own spawn dir).
|
|
3455
3500
|
const chat = chatCwdFallback(target);
|
|
3456
3501
|
const pending = chat.pending;
|
|
3457
3502
|
const a = byTarget.get(target);
|
|
@@ -3463,9 +3508,9 @@ export const startMirror = (deps) => {
|
|
|
3463
3508
|
return { runningCwd: chat.running || expandedDefaultCwd, pendingCwd: pending, defaultCwd: expandedDefaultCwd };
|
|
3464
3509
|
};
|
|
3465
3510
|
// Write `pendingCwd` to the BASE principal so the switch applies chat-wide —
|
|
3466
|
-
// the next /new in any tagged/untagged session picks it up. `
|
|
3467
|
-
// tagged session still writes to the shared slot, not the tagged
|
|
3468
|
-
// own record, matching "sessions share the chat's cwd".
|
|
3511
|
+
// the next /new in any tagged/untagged session picks it up. `set_workspace`
|
|
3512
|
+
// from a tagged session still writes to the shared slot, not the tagged
|
|
3513
|
+
// session's own record, matching "sessions share the chat's cwd".
|
|
3469
3514
|
const setPendingCwd = (target, cwd) => {
|
|
3470
3515
|
const trimmed = (cwd ?? "").trim();
|
|
3471
3516
|
if (!trimmed)
|
|
@@ -3627,6 +3672,34 @@ export const startMirror = (deps) => {
|
|
|
3627
3672
|
const p = jsonlOf(target);
|
|
3628
3673
|
return p ? lastAssistantText(p) : "";
|
|
3629
3674
|
};
|
|
3675
|
+
// ── 未收口气泡的引用判定 ────────────────────────────────────────────
|
|
3676
|
+
// tag 会话的 last stream 未收口时, 群里最新那条气泡是瞬态的: 详情链接 URL +
|
|
3677
|
+
// 最新一条实时 CoT/工具行 (brief) 或累积中的 acc (非 brief)。引用它要的是路由,
|
|
3678
|
+
// 不是把这些中间态贴回 prompt —— 判定命中 ⇒ 调用方只保留 tag, 正文丢弃。
|
|
3679
|
+
// URL 必须先于 canon 剥掉: canonForCompare 只剩字母数字, host/path 的字符会
|
|
3680
|
+
// 永远卡住比对。两侧同剥, 才在同一层级上比。
|
|
3681
|
+
const OPEN_QUOTE_URL_RE = /https?:\/\/\S+/g;
|
|
3682
|
+
const quoteCanon = (s) => s.replace(OPEN_QUOTE_URL_RE, " ").replace(/[^\p{L}\p{N}]/gu, "");
|
|
3683
|
+
const openBubbleBodies = (a) => {
|
|
3684
|
+
if (a.briefBubble && !a.briefBubble.done && !a.briefConcluded) {
|
|
3685
|
+
return [a.cotLastSent, a.cotText].filter((s) => !!s);
|
|
3686
|
+
}
|
|
3687
|
+
const s = a.liveStream;
|
|
3688
|
+
return s && !s.closed && !s.dead ? [s.acc, s.lastSent] : [];
|
|
3689
|
+
};
|
|
3690
|
+
const isOpenBubbleQuote = (target, quoted) => {
|
|
3691
|
+
const a = byTarget.get(target);
|
|
3692
|
+
if (!a)
|
|
3693
|
+
return false;
|
|
3694
|
+
const bodies = openBubbleBodies(a);
|
|
3695
|
+
if (bodies.length === 0)
|
|
3696
|
+
return false;
|
|
3697
|
+
const qc = quoteCanon(quoted);
|
|
3698
|
+
// 剥掉 URL 后什么都不剩 ⇒ 引用的只是链接头 + "…" 这类纯 chrome, 直接命中。
|
|
3699
|
+
if (!qc)
|
|
3700
|
+
return true;
|
|
3701
|
+
return bodies.some((b) => quoteCanon(b).includes(qc));
|
|
3702
|
+
};
|
|
3630
3703
|
// 一个 target 的完整画像。本 chat 的兄弟和外 chat 的 peer 走同一条,只有
|
|
3631
3704
|
// `self` / `address` 因观察者而异 —— 地址本来就是相对调用方说的话。
|
|
3632
3705
|
const peerInfoOf = async (t, self) => {
|
|
@@ -3756,7 +3829,7 @@ export const startMirror = (deps) => {
|
|
|
3756
3829
|
sessionId: a.sessionId, jsonlPath: a.jsonlPath, tmuxTarget: a.tmuxPane,
|
|
3757
3830
|
});
|
|
3758
3831
|
if (!r.ok) {
|
|
3759
|
-
|
|
3832
|
+
endKeepaliveSwallow(a);
|
|
3760
3833
|
if (a.keepalive)
|
|
3761
3834
|
a.keepalive.pinging = false; // roll back so the next tick can retry
|
|
3762
3835
|
log.warn({ target: a.target, reason: r.reason }, "keepalive: inject failed");
|
|
@@ -3769,9 +3842,13 @@ export const startMirror = (deps) => {
|
|
|
3769
3842
|
// actual ping we injected; the assistant reply (expected: just "pong"), any
|
|
3770
3843
|
// tool calls, and usage are grafted on from the swallowed items in onItem;
|
|
3771
3844
|
// closed on the ping's turn_end (or the fail-safe). Kept out of chat.
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3845
|
+
// The tail's keepalive_start (content match on the user line) may have
|
|
3846
|
+
// opened this turn already — only open when it hasn't.
|
|
3847
|
+
if (!a.keepaliveTurnId) {
|
|
3848
|
+
const turnId = newTurnId();
|
|
3849
|
+
a.keepaliveTurnId = turnId;
|
|
3850
|
+
recordTurnStart({ id: turnId, target: a.target, sessionId: a.sessionId, userQuery: text });
|
|
3851
|
+
}
|
|
3775
3852
|
};
|
|
3776
3853
|
let keepaliveTicking = false;
|
|
3777
3854
|
const keepaliveTick = async () => {
|
|
@@ -3891,6 +3968,7 @@ export const startMirror = (deps) => {
|
|
|
3891
3968
|
peekTurns,
|
|
3892
3969
|
isBusy,
|
|
3893
3970
|
lastText,
|
|
3971
|
+
isOpenBubbleQuote,
|
|
3894
3972
|
status: () => {
|
|
3895
3973
|
const list = Array.from(bySessionId.values()).map((a) => ({
|
|
3896
3974
|
sessionId: a.sessionId,
|
|
@@ -3917,7 +3995,7 @@ export const startMirror = (deps) => {
|
|
|
3917
3995
|
},
|
|
3918
3996
|
targetForSession: (sessionId) => {
|
|
3919
3997
|
// Live attach is the fast path. Fall back to scanning persisted store:
|
|
3920
|
-
// an MCP tool (e.g. `
|
|
3998
|
+
// an MCP tool (e.g. `set_workspace`) called from a claude that isn't mirror-attached
|
|
3921
3999
|
// would otherwise miss here and silently retarget to defaultChat — that
|
|
3922
4000
|
// bug stranded pendingCwd on the wrong principal. Store keeps each
|
|
3923
4001
|
// target's sessionId in sync via migrate/attach/setPendingCwd writes.
|
|
@@ -4572,10 +4650,10 @@ export const startMirror = (deps) => {
|
|
|
4572
4650
|
// and surface a standalone "cleared" so the user gets explicit
|
|
4573
4651
|
// feedback (the skip-stream path otherwise leaves WeCom silent).
|
|
4574
4652
|
if (armMigration) {
|
|
4575
|
-
// slashAckFirstLine
|
|
4653
|
+
// cwd 不随回执下发 (/pwd 可查); slashAckFirstLine 连 tip 也省去 (与 /new 一致)。
|
|
4576
4654
|
sendStandalone(a, cfg.wrc.mirror.slashAckFirstLine
|
|
4577
4655
|
? "cleared"
|
|
4578
|
-
: `cleared\n\n${
|
|
4656
|
+
: `cleared\n\n${randomTip()}`);
|
|
4579
4657
|
a.clearRebind = { baseline: preClearBaseline };
|
|
4580
4658
|
startMigrationWatcher(a, preClearBaseline, jsonlIsPostClearChild, 0, true);
|
|
4581
4659
|
}
|