wezard 2.0.6 → 2.1.1

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.
Files changed (45) hide show
  1. package/CLAUDE.md +2 -2
  2. package/dist/daemon/chat-log.js +107 -0
  3. package/dist/daemon/chat-log.js.map +1 -0
  4. package/dist/daemon/chat-name.js +1 -1
  5. package/dist/daemon/detail.js +7 -0
  6. package/dist/daemon/detail.js.map +1 -1
  7. package/dist/daemon/inbound.js +10 -3
  8. package/dist/daemon/inbound.js.map +1 -1
  9. package/dist/daemon/index.js +125 -22
  10. package/dist/daemon/index.js.map +1 -1
  11. package/dist/daemon/mirror-bridge.js +81 -44
  12. package/dist/daemon/mirror-bridge.js.map +1 -1
  13. package/dist/daemon/notices.js +3 -5
  14. package/dist/daemon/notices.js.map +1 -1
  15. package/dist/daemon/peers.js +260 -125
  16. package/dist/daemon/peers.js.map +1 -1
  17. package/dist/daemon/session-name.js +1 -1
  18. package/dist/daemon/session-name.js.map +1 -1
  19. package/dist/daemon/wizard.js +33 -2
  20. package/dist/daemon/wizard.js.map +1 -1
  21. package/dist/mcp/server.js +34 -12
  22. package/dist/mcp/server.js.map +1 -1
  23. package/dist/shared/chat-http.js +25 -10
  24. package/dist/shared/chat-http.js.map +1 -1
  25. package/dist/shared/chat-render.js +17 -7
  26. package/dist/shared/chat-render.js.map +1 -1
  27. package/dist/shared/config.js +6 -6
  28. package/dist/shared/config.js.map +1 -1
  29. package/dist/shared/detail-render.js +22 -5
  30. package/dist/shared/detail-render.js.map +1 -1
  31. package/dist/shared/reminder.js +56 -0
  32. package/dist/shared/reminder.js.map +1 -0
  33. package/dist/shared/role-render.js +6 -3
  34. package/dist/shared/role-render.js.map +1 -1
  35. package/dist/shared/role-view.js +3 -3
  36. package/dist/shared/role-view.js.map +1 -1
  37. package/dist/shared/session-label.js +9 -3
  38. package/dist/shared/session-label.js.map +1 -1
  39. package/hooks/pre-tool-use.sh +1 -1
  40. package/package.json +4 -1
  41. package/web/chat.css +15 -11
  42. package/web/chat.js +38 -5
  43. package/web/highlight.min.js +1262 -0
  44. package/web/hljs-github.min.css +10 -0
  45. package/web/markdown-it.min.js +2 -0
@@ -1,4 +1,5 @@
1
1
  // Daemon entry. Resident process — exits only on signal or fatal WS auth failure.
2
+ import { homedir } from "node:os";
2
3
  import { loadConfig } from "../shared/config.js";
3
4
  import { makeLogger } from "../shared/log.js";
4
5
  import { bindCliBackends } from "../shared/cli-backends.js";
@@ -29,10 +30,11 @@ import { baseOfKey, bindTagLinker, keyOf, linkTags, normalizeTag, tagFromCwd, ta
29
30
  import { clipForCharter, inboxPath, memoryPath, memoryRoot, proposeMemory, readMemory } from "./wizard-memory.js";
30
31
  import { ensureStewardTask } from "./memory-steward.js";
31
32
  import { applyChatNames, chatBaseOf, chatNameOf, clearChatName, listChatNames, normChatName, peerAddress, planChatNames, setChatName } from "./chat-name.js";
32
- import { bindWizardStore, loadWizardStore, wizardStore, settleName, settleAll, evictStaleName, childrenOf, ancestorsOf, renderCharter, } from "./wizard.js";
33
+ import { bindWizardStore, loadWizardStore, wizardStore, settleName, settleAll, evictStaleName, childrenOf, ancestorsOf, renderCharter, renderRoster, } from "./wizard.js";
33
34
  import { bindNoticeBox, createNoticeBox, chatAudience } from "./notices.js";
34
35
  import { loadJobStore, renderJobOpen, renderJobClose, JOB_MEMBER_MAX } from "./jobs.js";
35
- import { clipMiddle, extractResult, renderPeerEnvelope } from "./peers.js";
36
+ import { clipMiddle, extractResult, keepalivePingSigs, lastExchange, lastModel, renderPeerEnvelope, renderTaskEnvelope } from "./peers.js";
37
+ import { parseWhen, renderChatLog } from "./chat-log.js";
36
38
  import { startGraph, stopRun, getRun, listRuns, validateSpec, waitForIdle, waitForQuorum, } from "./graph.js";
37
39
  // pino's file transport is async; without flush, log.fatal before process.exit
38
40
  // vanishes. Mirror anything fatal to stderr too so launchd's stderr.log captures it.
@@ -527,6 +529,8 @@ const main = async () => {
527
529
  const body = (await readBody(req));
528
530
  return { self: resolveSelf(body), body };
529
531
  };
532
+ // list_peers 已并进 wizard_roster ({chat}); 路由留着, 是因为正在跑的 wizard 的
533
+ // MCP 进程还是旧代码, 仍会打到这里。
530
534
  http.register("POST /peers/list", async (req, res) => {
531
535
  const { self } = await readPeerBody(req);
532
536
  if (!self) {
@@ -685,19 +689,104 @@ const main = async () => {
685
689
  }
686
690
  const { target, foreign } = r;
687
691
  const turns = Math.min(Math.max(Number(body.turns ?? 6) || 6, 1), 40);
688
- // 先读 transcript —— 那才是对话本身。只有当这个 wizard 还没有可读的 jsonl
689
- // (从没 attach 过, 或者刚 `/clear` 完) 才退回去刮它的终端。
692
+ // 对话只从 transcript 读 (正文轮次, 去掉保温 ping/pong), 不刮终端: pane 是
693
+ // 截断的视口加一层 TUI 装饰, 而「它卡在哪」transcript 答得更准 —— 不在转圈
694
+ // 却悬着工具调用, 就是停在审批卡 / 本地弹窗上等人点。
690
695
  const peek = await m.peekTurns(target, turns);
691
- const pane = peek.ok ? undefined : await m.peekPane(target, 24);
696
+ const waiting = peek.waiting ?? [];
697
+ const state = peek.busy
698
+ ? "正在生成"
699
+ : waiting.length
700
+ ? `停在 ${waiting.join(" / ")} 上 —— 工具调用发出去了还没结果, 多半在等人点审批卡或本地弹窗`
701
+ : "空闲";
692
702
  json(res, 200, {
693
703
  ok: true,
694
- target,
695
- foreign,
696
- dialog: peek.dialog ?? "",
697
- pane: pane?.pane ?? "",
698
- error: peek.ok ? undefined : (pane?.reason ?? peek.reason),
704
+ name: peerAddress(cfg, self, target),
699
705
  busy: peek.busy ?? false,
700
- lastText: m.lastText(target),
706
+ ...(waiting.length ? { waiting } : {}),
707
+ text: [`${displayName(target)} · ${state}${foreign ? ` · 住在群 ${chatNameOf(cfg, target) || "(未命名)"}` : ""}`, peek.dialog ?? `(${peek.reason})`].join("\n"),
708
+ });
709
+ });
710
+ // ── 聊天记录 ────────────────────────────────────────────────────────
711
+ // peek_peer 读的是**一个会话**听到和说过的一切; 这条读的是**往来**: 三级收窄
712
+ // role (谁的视角) → chat (哪个群) → target (和谁), 再按时间窗与条数裁 —— 与
713
+ // rolepage 同一条轴。全部从各会话的 transcript 现算 (见 chat-log.ts), 不经 turn
714
+ // store。名字全局可达, 一句话可能落在任何一个会话的 transcript 里 (A 对 B 说的
715
+ // 在 B 那份), 所以候选是全体; 按 mtime 只留最近动过的那一批 (一次 stat, 不
716
+ // spawn), 点了名的 role / target 与调用方自己无条件入选。
717
+ const LOG_RECENT_MS = 7 * 24 * 3600_000;
718
+ const LOG_SESSIONS = 40;
719
+ const logSession = (t) => {
720
+ const home = baseOfKey(t.target);
721
+ const rec = wizards.get(t.target);
722
+ return {
723
+ name: displayName(t.target),
724
+ jsonlPath: t.jsonlPath,
725
+ homeChat: chatNameOf(cfg, home),
726
+ homeHuman: home.startsWith("user:") ? home.slice(5) : "人",
727
+ since: rec?.clonedFrom ? rec.bornAt : 0,
728
+ };
729
+ };
730
+ /** role / target 的写法: wizard 的名字解析成它的称呼; 解析不出的原样当作人的
731
+ * userid (或 `定时 <id>`) —— 记录里人就是按这个字面出现的。 */
732
+ const logRole = (self, ref) => {
733
+ const r = resolvePeer(self, ref);
734
+ return r.ok ? { name: displayName(r.target), target: r.target } : { name: ref };
735
+ };
736
+ http.register("POST /chats/read", async (req, res) => {
737
+ const { self, body } = await readPeerBody(req);
738
+ if (!self) {
739
+ json(res, 400, { ok: false, reason: "cannot resolve caller session" });
740
+ return;
741
+ }
742
+ // 线上字段是 `with`: 请求体里的 `target` 已经是「调用方是谁」的覆盖 (resolveSelf)。
743
+ const b = body;
744
+ ensureChatNames(self);
745
+ const now = Date.now();
746
+ const [roleRef, chatRef, targetRef, sinceRef, untilRef] = [b.role, b.chat, b.with, b.since, b.until].map((x) => (x ?? "").toString().trim());
747
+ // 什么都没给 = 调用方这一轮所在的群; 只给 target = 调用方与它的往来。
748
+ const base = chatRef ? chatBaseOf(cfg, chatRef) : roleRef || targetRef ? "" : channelOf(self);
749
+ if (chatRef && !base) {
750
+ json(res, 404, { ok: false, reason: `认不出聊天 '${chatRef}' (list_chats 看有哪些)` });
751
+ return;
752
+ }
753
+ const role = roleRef ? logRole(self, roleRef) : targetRef ? { name: displayName(self), target: self } : undefined;
754
+ const target = targetRef ? logRole(self, targetRef) : undefined;
755
+ const since = sinceRef ? parseWhen(sinceRef, now) : undefined;
756
+ const until = untilRef ? parseWhen(untilRef, now) : undefined;
757
+ if ((sinceRef && since === undefined) || (untilRef && until === undefined)) {
758
+ json(res, 400, { ok: false, reason: "时间认不出 —— 写 `2h` / `30m` / `3d` (多久以前)、`14:30` (今天)、`09-30 14:30` 或 ISO 时间" });
759
+ return;
760
+ }
761
+ const limit = Math.min(Math.max(Number(b.limit ?? 30) || 30, 1), 200);
762
+ const per = Math.min(Math.max(Number(b.per ?? 500) || 500, 40), 4000);
763
+ const pinned = new Set([self, role?.target, target?.target]);
764
+ const all = m.transcripts();
765
+ // 两方之间的话只可能落在这两方里的 wizard 自己的 transcript 里 (人没有 transcript)
766
+ // —— 不必去翻别的会话, 「往回还有没有」也因此答得准。
767
+ const between = [role?.target, target?.target].filter((t) => !!t);
768
+ const sessions = role && target && between.length
769
+ ? all.filter((t) => between.includes(t.target))
770
+ : [
771
+ ...all.filter((t) => pinned.has(t.target)),
772
+ ...all.filter((t) => !pinned.has(t.target) && now - t.mtime < LOG_RECENT_MS).sort((x, y) => y.mtime - x.mtime).slice(0, LOG_SESSIONS),
773
+ ];
774
+ const chat = base ? chatNameOf(cfg, base) : undefined;
775
+ const log_ = renderChatLog(sessions.map(logSession), { role: role?.name, chat, target: target?.name, since, until, limit, per }, keepalivePingSigs(cfg.wrc.mirror.keepalive.ping), now);
776
+ const scope = [
777
+ chat !== undefined ? `${base.startsWith("user:") ? "单聊" : "群"} ${chat || base}` : "",
778
+ role && target ? `${role.name} ⇄ ${target.name}` : role ? `${role.name} 的往来` : "",
779
+ ].filter(Boolean).join(" · ");
780
+ json(res, 200, {
781
+ ok: true,
782
+ shown: log_.shown,
783
+ total: log_.total,
784
+ text: [
785
+ `${scope} · ${log_.shown} 条`,
786
+ log_.text || "(没有读到对话)",
787
+ ...(log_.earlier ? [`更早的: until="${log_.earlier}"`] : log_.later || !log_.shown ? [] : ["(往回已经到头)"]),
788
+ ...(log_.later ? [`往后还有 ${log_.total - log_.shown} 条: since="${log_.later}"`] : []),
789
+ ].join("\n"),
701
790
  });
702
791
  });
703
792
  http.register("POST /peers/send", async (req, res) => {
@@ -1162,8 +1251,8 @@ const main = async () => {
1162
1251
  ...b,
1163
1252
  target: p.target,
1164
1253
  chat: chatNameOf(cfg, p.target) || p.chat,
1165
- label: p.label,
1166
- cli: p.cli,
1254
+ solo: baseOfKey(p.target).startsWith("user:"),
1255
+ jsonlPath: p.jsonlPath,
1167
1256
  model: p.model,
1168
1257
  busy: p.busy,
1169
1258
  alive: p.paneAlive,
@@ -1180,13 +1269,16 @@ const main = async () => {
1180
1269
  ...briefOf(self, w.target),
1181
1270
  target: w.target,
1182
1271
  chat: chatNameOf(cfg, w.target),
1272
+ solo: baseOfKey(w.target).startsWith("user:"),
1273
+ jsonlPath: "",
1274
+ model: "",
1183
1275
  busy: false,
1184
1276
  alive: false,
1185
1277
  self: false,
1186
1278
  // 没跑过就没有活动时刻。排序拿它当最旧 —— 冷会话排在活着的后面, 正是
1187
1279
  // 截断时该被留下的顺序。
1188
1280
  lastActivity: 0,
1189
- summary: "(未运行 —— 发消息或 send_peer 会把它唤醒)",
1281
+ summary: "",
1190
1282
  ...kinOf(self, all, w.target),
1191
1283
  }));
1192
1284
  return [...live, ...cold];
@@ -1258,16 +1350,27 @@ const main = async () => {
1258
1350
  // 自己在最前, 然后活着的, 然后最近动过的 —— 截断时留下的正是最可能有用的。
1259
1351
  const ranked = hit.slice().sort((x, y) => Number(y.self) - Number(x.self) || Number(y.alive) - Number(x.alive) || (y.lastActivity ?? 0) - (x.lastActivity ?? 0));
1260
1352
  const limit = Math.min(Math.max(Number(b.limit ?? 40) || 40, 1), 300);
1261
- const shown = ranked.slice(0, limit);
1353
+ // 「最近」与模型都从 transcript 读, 而且只为列出来的这一页读: 摘要用不指代任何人
1354
+ // 的箭头 (读名册的是另一个 wizard), 模型取它真正跑的那个 —— 绑定里的 model 只在
1355
+ // 有人显式选过时才有。
1356
+ const warm = keepalivePingSigs(cfg.wrc.mirror.keepalive.ping);
1357
+ const shown = ranked.slice(0, limit).map((r) => ({
1358
+ ...r,
1359
+ model: r.model || (r.jsonlPath ? lastModel(r.jsonlPath) : ""),
1360
+ summary: r.jsonlPath ? lastExchange(r.jsonlPath, 80, warm) : "",
1361
+ }));
1262
1362
  json(res, 200, {
1263
1363
  ok: true,
1264
- self: identityOf(self, self),
1265
1364
  total: all.length,
1266
1365
  matched: hit.length,
1267
- wizards: shown,
1268
- ...(hit.length > shown.length
1269
- ? { more: `还有 ${hit.length - shown.length} 个没列出来 —— 用 query (名字/职责) / cwd (工作区) / chat 收窄, 或者调大 limit` }
1270
- : {}),
1366
+ shown: shown.length,
1367
+ text: [
1368
+ `名册 · 列出 ${shown.length}/${hit.length} 个 (全机 ${all.length} 个) · 忙 = 正在生成, 闲 = 活着没在跑, 冷 = 没有 pane (发消息会唤醒)`,
1369
+ renderRoster(shown, Date.now(), homedir()),
1370
+ ...(hit.length > shown.length
1371
+ ? [`还有 ${hit.length - shown.length} 个没列出来 —— 用 query (名字/职责) / cwd (工作区) / chat 收窄, 或者调大 limit`]
1372
+ : []),
1373
+ ].join("\n"),
1271
1374
  });
1272
1375
  });
1273
1376
  // 记忆三种作用域: self 跟着自己 (wizards.json), 直写; chat / workspace 是共享的 md,
@@ -1837,7 +1940,7 @@ const main = async () => {
1837
1940
  scheduledTaskInject = async (target, text, { taskId, fresh }) => {
1838
1941
  const busy = fresh ? false : await m.isBusy(target);
1839
1942
  if (!fresh && !busy)
1840
- return m.injectText(target, text, undefined, { fromChat: true, from: { kind: "task", taskId } });
1943
+ return m.injectText(target, text, undefined, { fromChat: true, from: { kind: "task", taskId }, envelope: renderTaskEnvelope(taskId) });
1841
1944
  // 执行体挂在日程的主人名下: 名字 `<主人>-task-xxxx`, parent = 主人 —— 它的
1842
1945
  // rolepage 里看得见这一枪是谁的日程放的。落在执行目标的群/目录里。
1843
1946
  const base = baseOfKey(target);
@@ -1864,7 +1967,7 @@ const main = async () => {
1864
1967
  notifyChat(base, withTagHeader(target, busy
1865
1968
  ? `⏰ 定时任务到点时正忙, 已起白板 wizard .${runnerName} 单独执行, 完成后自动收掉`
1866
1969
  : `⏰ 定时任务已起白板 wizard .${runnerName} 执行, 完成后自动收掉`));
1867
- const inj = await m.injectText(runner, text, undefined, { fromChat: true, from: { kind: "task", taskId } });
1970
+ const inj = await m.injectText(runner, text, undefined, { fromChat: true, from: { kind: "task", taskId }, envelope: renderTaskEnvelope(taskId) });
1868
1971
  if (!inj.ok) {
1869
1972
  wizards.drop(runner);
1870
1973
  return inj;