wezard 2.0.3 → 2.0.4
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 +1 -1
- package/README.md +119 -291
- package/dist/daemon/index.js +73 -19
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/mirror-bridge.js +57 -8
- package/dist/daemon/mirror-bridge.js.map +1 -1
- package/dist/daemon/model-select.js +173 -61
- package/dist/daemon/model-select.js.map +1 -1
- package/dist/daemon/peers.js +54 -8
- package/dist/daemon/peers.js.map +1 -1
- package/dist/daemon/spawn-tmux.js +16 -4
- package/dist/daemon/spawn-tmux.js.map +1 -1
- package/dist/daemon/wizard.js +16 -4
- package/dist/daemon/wizard.js.map +1 -1
- package/dist/mcp/server.js +21 -7
- package/dist/mcp/server.js.map +1 -1
- package/dist/shared/detail-render.js +6 -230
- package/dist/shared/detail-render.js.map +1 -1
- package/package.json +1 -1
- package/web/chat.css +13 -13
- package/web/chat.js +13 -4
package/dist/daemon/index.js
CHANGED
|
@@ -32,7 +32,7 @@ import { applyChatNames, chatBaseOf, chatNameOf, clearChatName, listChatNames, n
|
|
|
32
32
|
import { bindWizardStore, loadWizardStore, wizardStore, settleName, settleAll, evictStaleName, childrenOf, ancestorsOf, renderCharter, } from "./wizard.js";
|
|
33
33
|
import { bindNoticeBox, createNoticeBox, chatAudience } from "./notices.js";
|
|
34
34
|
import { loadJobStore, renderJobOpen, renderJobClose, JOB_MEMBER_MAX } from "./jobs.js";
|
|
35
|
-
import { extractResult } from "./peers.js";
|
|
35
|
+
import { clipMiddle, extractResult, renderPeerEnvelope } from "./peers.js";
|
|
36
36
|
import { startGraph, stopRun, getRun, listRuns, validateSpec, waitForIdle, waitForQuorum, } from "./graph.js";
|
|
37
37
|
// pino's file transport is async; without flush, log.fatal before process.exit
|
|
38
38
|
// vanishes. Mirror anything fatal to stderr too so launchd's stderr.log captures it.
|
|
@@ -575,6 +575,13 @@ const main = async () => {
|
|
|
575
575
|
/** 调用方这一轮所在的公开频道: 人从哪个群叫的它 / 公开 peer 轮的那个群;
|
|
576
576
|
* 私聊轮或无记录 → 它的 home 群。notify 与 public send_peer 默认发到这里。 */
|
|
577
577
|
const channelOf = (self) => m.currentChannel(self) || baseOfKey(self);
|
|
578
|
+
/** 同伴注入的信封: 公开轮写上那个群的名字, 私聊不写 (见 peers.renderPeerEnvelope)。 */
|
|
579
|
+
const envelopeFor = (from, channel) => renderPeerEnvelope(displayName(from), channel ? chatNameOf(cfg, channel) : undefined);
|
|
580
|
+
// 发话时刻 —— wait_peer 拿它把「回复」和「这一次发话」对上 (peers.lastReply)。
|
|
581
|
+
// 纯内存: reload 之后退化成不带关联的旧行为, 不值得为它落盘。
|
|
582
|
+
const sentAt = new Map();
|
|
583
|
+
const pairOf = (from, to) => `${from}\u0000${to}`;
|
|
584
|
+
const stampSend = (from, to) => void sentAt.set(pairOf(from, to), Date.now());
|
|
578
585
|
/** 入参里的地址: 新字段 `name`, 老 MCP 进程 (正在跑的 wizard) 仍在传 `tag`。 */
|
|
579
586
|
const addrOf = (b) => String(b.name ?? b.tag ?? "");
|
|
580
587
|
/** 撞上的名字属于一个静默超过一天的 wizard → 顶掉它 (见 evictStaleName), 同群的
|
|
@@ -747,9 +754,11 @@ const main = async () => {
|
|
|
747
754
|
return;
|
|
748
755
|
}
|
|
749
756
|
}
|
|
757
|
+
stampSend(self, target);
|
|
750
758
|
const inj = await m.injectText(target, text, undefined, {
|
|
751
759
|
from: { kind: "peer", from: self, ...(jobId ? { job: jobId } : {}), ...(isPublic ? { public: true } : {}) },
|
|
752
760
|
channel,
|
|
761
|
+
envelope: envelopeFor(self, channel),
|
|
753
762
|
});
|
|
754
763
|
// 工单成员照旧记账 (收工那一条会列出各自那段活); 公开的那一句在群里成气泡。
|
|
755
764
|
if (inj.ok && jobId)
|
|
@@ -758,7 +767,7 @@ const main = async () => {
|
|
|
758
767
|
relayPeer(self, target, text, channel);
|
|
759
768
|
// `wasBusy` 是给调用方的判断依据: 立刻投给一个正在生成的会话, 这句话会排在
|
|
760
769
|
// 它这一轮后面, 而不是马上被读到。
|
|
761
|
-
json(res, inj.ok ? 200 : 502, { ...inj,
|
|
770
|
+
json(res, inj.ok ? 200 : 502, { ...inj, name: peerAddress(cfg, self, target), public: isPublic, wasBusy, ...(waitedMs ? { waitedMs } : {}), ...(jobId ? { job: jobId } : {}) });
|
|
762
771
|
});
|
|
763
772
|
// 等一个 wizard, 或者等一**组**。fan-out 之后 join 必须是并行的: 串行地等五个
|
|
764
773
|
// 分身, 墙钟是五个之和, 而它们本来就在同时干活。`need` 把 all / any / 过半收进
|
|
@@ -793,24 +802,36 @@ const main = async () => {
|
|
|
793
802
|
const need = Math.min(Math.max(Number(b.need ?? uniq.length) || uniq.length, 1), uniq.length);
|
|
794
803
|
const timeoutMs = Math.min(Math.max(Number(b.timeoutSec ?? 900) || 900, 10), 7200) * 1000;
|
|
795
804
|
const wrs = await waitForQuorum(uniq.map((h) => h.target), m.isBusy, timeoutMs, need);
|
|
805
|
+
// 回程不再进群: 私聊的结论由调用方自己收口给人; 公开轮的回复 mirror 早已发进那个群。
|
|
806
|
+
// 载荷只给一份, 且只给该给的那一份 —— 这段 json 是原样进调用方上下文的:
|
|
807
|
+
// - 只认**这一次发话之后**的回复 (lastReply); 停下了却没有新回复 = `stale`,
|
|
808
|
+
// 绝不把上一件事的答案当成这一件的交回去;
|
|
809
|
+
// - 它收口了 `RESULT:` → 只回 `result` (从全文里摘, 不受截断影响), 正文不再
|
|
810
|
+
// 重复一遍, `omitted` 说还有多少字没给 (要读就 peek_peer);
|
|
811
|
+
// - 没收口 → 回 `lastText`, 超长掐中间保住头尾;
|
|
812
|
+
// - 内部 key (`target`) 与 `foreign` 不回: 模型用不上, 还会被诱导去拼 key。
|
|
796
813
|
const results = uniq.map((h, i) => {
|
|
797
814
|
const wr = wrs[i];
|
|
798
|
-
const
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
target: first.target, foreign: first.foreign, idle: first.idle, reason: first.reason, result: first.result, lastText: first.lastText,
|
|
815
|
+
const full = wr.idle ? m.lastReply(h.target, sentAt.get(pairOf(self, h.target)) ?? 0) : "";
|
|
816
|
+
const result = extractResult(full);
|
|
817
|
+
return {
|
|
818
|
+
name: h.address,
|
|
819
|
+
idle: wr.idle,
|
|
820
|
+
...(wr.reason ? { reason: wr.reason } : {}),
|
|
821
|
+
...(result
|
|
822
|
+
? { result, ...(full.length > result.length + 200 ? { omitted: full.length - result.length } : {}) }
|
|
823
|
+
: full
|
|
824
|
+
? { lastText: clipMiddle(full) }
|
|
825
|
+
: wr.idle
|
|
826
|
+
? { stale: true, reason: "它停下了, 但自你上次 send_peer 以来没有新回复 —— 多半停在一个等人点的弹窗上, 或那句话没被接住; peek_peer 看一眼" }
|
|
827
|
+
: {}),
|
|
828
|
+
};
|
|
813
829
|
});
|
|
830
|
+
const done = results.filter((x) => x.idle).length;
|
|
831
|
+
// 只等一个就摊平, 等一组才给数组 —— 同一份载荷不出现两遍。
|
|
832
|
+
json(res, 200, results.length === 1
|
|
833
|
+
? { ok: true, ...results[0] }
|
|
834
|
+
: { ok: true, need, done, satisfied: done >= need, results });
|
|
814
835
|
});
|
|
815
836
|
// POST /handoff — 交接一个 pane 的会话给一个全新会话,原地完成。先让目标
|
|
816
837
|
// 会话把当前工作压成一份"零上下文也能接手"的交接简报,等它写完并抓取,
|
|
@@ -1387,6 +1408,10 @@ const main = async () => {
|
|
|
1387
1408
|
const task = (b.task ?? "").toString().trim();
|
|
1388
1409
|
// 没点名就按配置的 keepalive.spawnDefault 来 —— 调用方明说的永远优先。
|
|
1389
1410
|
const keepalive = typeof b.keepalive === "boolean" ? b.keepalive : cfg.wrc.mirror.keepalive.spawnDefault;
|
|
1411
|
+
// 先盖章再生: 分叉出来的 transcript 里躺着被克隆者的旧回复, 时刻早于这一枚章,
|
|
1412
|
+
// wait_peer 才不会把它们当成分身对这件活的答复。
|
|
1413
|
+
if (task)
|
|
1414
|
+
stampSend(self, target);
|
|
1390
1415
|
const r = await m.cloneSession({
|
|
1391
1416
|
parent: source,
|
|
1392
1417
|
target,
|
|
@@ -1398,7 +1423,7 @@ const main = async () => {
|
|
|
1398
1423
|
inherit,
|
|
1399
1424
|
// 继承路径上第一句话是分叉的触发器, 所以直接把活当开场白 —— 少一次往返,
|
|
1400
1425
|
// 也少一次"就位了但没事干"的空转。
|
|
1401
|
-
bootstrap: task
|
|
1426
|
+
bootstrap: task ? task + envelopeFor(self, "") : undefined,
|
|
1402
1427
|
keepalive,
|
|
1403
1428
|
});
|
|
1404
1429
|
if (!r.ok) {
|
|
@@ -1421,7 +1446,7 @@ const main = async () => {
|
|
|
1421
1446
|
// 继承路径上活已经随开场白进去了, 空白分身才需要在这里补一次注入 (私聊)。
|
|
1422
1447
|
let dispatched = r.inherited && !!task;
|
|
1423
1448
|
if (task && !r.inherited) {
|
|
1424
|
-
const inj = await m.injectText(target, task, undefined, { from: { kind: "peer", from: self, ...(jobId ? { job: jobId } : {}) }, channel: "" });
|
|
1449
|
+
const inj = await m.injectText(target, task, undefined, { from: { kind: "peer", from: self, ...(jobId ? { job: jobId } : {}) }, channel: "", envelope: envelopeFor(self, "") });
|
|
1425
1450
|
dispatched = inj.ok;
|
|
1426
1451
|
}
|
|
1427
1452
|
if (jobId)
|
|
@@ -1463,6 +1488,35 @@ const main = async () => {
|
|
|
1463
1488
|
postRoster(baseOfKey(target), [target, self], `**${victim.name || target}** 已收工 · 由 ${displayName(self)} 结束${b.forget ? " (记录一并抹掉)" : ""}`);
|
|
1464
1489
|
json(res, 200, { ok: true, target, name: victim.name, mode: end ? "end" : "interrupt", forgotten: !!(end && b.forget) });
|
|
1465
1490
|
});
|
|
1491
|
+
// 换模型: 省略名字 = 换自己 (选择器盖在正在跑的这一轮上面照样能开), 点名 = 换
|
|
1492
|
+
// 那个 wizard。与 spawn 时的 `model` 是同一条路 (model-select.ts)。
|
|
1493
|
+
http.register("POST /wizard/model", async (req, res) => {
|
|
1494
|
+
const { self, body } = await readPeerBody(req);
|
|
1495
|
+
if (!self) {
|
|
1496
|
+
json(res, 400, { ok: false, reason: "cannot resolve caller session" });
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
const b = body;
|
|
1500
|
+
const model = (b.model ?? "").toString().trim();
|
|
1501
|
+
if (!model) {
|
|
1502
|
+
json(res, 400, { ok: false, reason: "model 必填 —— 口语化写就行 ('opus' / 'haiku' / 'sonnet 5')" });
|
|
1503
|
+
return;
|
|
1504
|
+
}
|
|
1505
|
+
const scope = (b.scope ?? "session").toString().trim();
|
|
1506
|
+
if (scope !== "session" && scope !== "default") {
|
|
1507
|
+
json(res, 400, { ok: false, reason: "scope 只有两档: 'session' (只换这一个会话) / 'default' (同时设为新会话的默认模型)" });
|
|
1508
|
+
return;
|
|
1509
|
+
}
|
|
1510
|
+
// 空串不能交给 resolvePeer —— 那是「本聊天的默认 wizard」, 不是「我自己」。
|
|
1511
|
+
const addr = addrOf(body).trim();
|
|
1512
|
+
const r = addr ? resolvePeer(self, addr) : { ok: true, target: self };
|
|
1513
|
+
if (!r.ok) {
|
|
1514
|
+
json(res, r.status, { ok: false, reason: r.reason, candidates: r.candidates });
|
|
1515
|
+
return;
|
|
1516
|
+
}
|
|
1517
|
+
const done = await m.setModel(r.target, model, scope);
|
|
1518
|
+
json(res, done.ok ? 200 : 502, { ...done, target: r.target, name: briefOf(self, r.target).name, ...(done.ok ? { model: done.applied } : {}) });
|
|
1519
|
+
});
|
|
1466
1520
|
// 自我交接: 上下文撑不住了, 自己把工作压成简报, 原地 /clear 重开, 再把简报贴
|
|
1467
1521
|
// 回去。和 /handoff 的区别是**简报由调用方自己写在入参里** —— 它没法在自己
|
|
1468
1522
|
// 生成的当口再被问一次 (那正是 /handoff 拒绝对自身操作的原因)。所以这里先应答,
|