wezard 2.7.1 → 2.7.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/CLAUDE.md +1 -1
- package/README.md +4 -4
- package/cli/wezard.sh +9 -2
- package/dist/daemon/chat-name.js +6 -4
- package/dist/daemon/chat-name.js.map +1 -1
- package/dist/daemon/handoff.js +34 -6
- package/dist/daemon/handoff.js.map +1 -1
- package/dist/daemon/index.js +124 -47
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/jobs.js +3 -2
- package/dist/daemon/jobs.js.map +1 -1
- package/dist/daemon/message-events.js +263 -178
- package/dist/daemon/message-events.js.map +1 -1
- package/dist/daemon/mirror-bridge.js +253 -55
- package/dist/daemon/mirror-bridge.js.map +1 -1
- package/dist/daemon/peers.js +9 -3
- package/dist/daemon/peers.js.map +1 -1
- package/dist/daemon/router.js +3 -2
- package/dist/daemon/router.js.map +1 -1
- package/dist/daemon/wezard-session.js +24 -2
- package/dist/daemon/wezard-session.js.map +1 -1
- package/dist/daemon/wizard.js +4 -3
- package/dist/daemon/wizard.js.map +1 -1
- package/dist/daemon/workflow-nodes.js +19 -0
- package/dist/daemon/workflow-nodes.js.map +1 -1
- package/dist/mcp/server.js +4 -4
- package/dist/mcp/server.js.map +1 -1
- package/dist/shared/config.js +2 -12
- package/dist/shared/config.js.map +1 -1
- package/dist/shared/modal-pane.js +6 -0
- package/dist/shared/modal-pane.js.map +1 -1
- package/package.json +1 -1
- package/dist/daemon/top-only-nudge.js +0 -29
- package/dist/daemon/top-only-nudge.js.map +0 -1
package/dist/daemon/index.js
CHANGED
|
@@ -51,7 +51,6 @@ import { baseOfKey, bindMultiBot, bindTagLinker, botOfKey, headed, ownHead, isIn
|
|
|
51
51
|
import { appendEpisode, clipForCharter, cwdOfMd, episodePath, inboxPath, mdsOf, proposedCwds, memoryPath, memoryRoot, proposeMemory, readMemory } from "./wizard-memory.js";
|
|
52
52
|
import { retireStewardTask, startSteward, stewardEnvelope, STEWARD_ID, STEWARD_RUN_MS, STEWARD_TARGET } from "./memory-steward.js";
|
|
53
53
|
import { applyChatNames, chatBaseOf, chatNameOf, chatPolicyOf, clearChatName, listChatNames, normChatName, peerAddress, planChatNames, setChatName, stewardTierOf } from "./chat-name.js";
|
|
54
|
-
import { createTopOnlyNudge, gcNudge, policyKeyOf, renderNudge } from "./top-only-nudge.js";
|
|
55
54
|
import { bindWizardStore, loadWizardStore, wizardStore, settleName, settleAll, backfillBorn, sweepUnborn, BOOT_ID, evictStaleName, childrenOf, ancestorsOf, renderCharter, renderLead, renderLeadPlaybook, ROSTER_FRESH_MS, renderRoster, handoffAt, memoryNudgeAt, MEMORY_NUDGE_FLOOR, MEMORY_MAX, NOTE_MAX, } from "./wizard.js";
|
|
56
55
|
import { activeRows, bindNoticeBox, createNoticeBox, createSeenRing, gcSeen, chatAudience, hears } from "./notices.js";
|
|
57
56
|
import { createLedger, digest, pendingGc, renderDigest, renderTable, renderAwaiting, renderStalled, renderShelved, ago as agoMs, HEARD_EVERY_MS } from "./pending-items.js";
|
|
@@ -67,7 +66,7 @@ import { returnOpOf } from "./dispatch.js";
|
|
|
67
66
|
import { isTerminal, renderInFlight, turnState } from "../shared/turn-state.js";
|
|
68
67
|
import { startIdleNudge } from "./idle-nudge.js";
|
|
69
68
|
import { askerFromEnvelope, renderReminder, withMsg } from "../shared/reminder.js";
|
|
70
|
-
import { createHandoffs, handedOff, handingOff } from "./handoff.js";
|
|
69
|
+
import { createHandoffs, cuttingTurn, handedOff, handingOff } from "./handoff.js";
|
|
71
70
|
import { coldGateOf, decide, rankCandidates, renderCandidates, routeDutiesOf, tierFor, wakeCostOf } from "./route.js";
|
|
72
71
|
import { parseWhen, renderChatLog, UNKNOWN_HUMAN } from "./chat-log.js";
|
|
73
72
|
import { audienceOf } from "../shared/role-view.js";
|
|
@@ -214,6 +213,8 @@ const main = async () => {
|
|
|
214
213
|
peerEvents?.accepted(e); },
|
|
215
214
|
committed: (e) => { if (e.payload.peer)
|
|
216
215
|
peerEvents?.committed(e); },
|
|
216
|
+
gone: (e) => { if (e.payload.peer)
|
|
217
|
+
peerEvents?.gone?.(e); },
|
|
217
218
|
canceled: (e) => {
|
|
218
219
|
if (e.payload.peer)
|
|
219
220
|
peerEvents?.canceled(e);
|
|
@@ -521,6 +522,11 @@ const main = async () => {
|
|
|
521
522
|
const m = bridge;
|
|
522
523
|
installMirrorEventListener(ws.client, m, log.child({ mod: "mirror" }));
|
|
523
524
|
http.register("GET /message-events/status", (_req, res) => json(res, 200, events.status()));
|
|
525
|
+
// 单条 event 的 status 视图 (`?id=`; id 里带冒号, 不走路径段) —— 轮询单条不必把全表算一遍。
|
|
526
|
+
http.register("GET /message-events/event", (_req, res, url) => {
|
|
527
|
+
const e = events.event(url.searchParams.get("id") ?? "");
|
|
528
|
+
json(res, e ? 200 : 404, e ?? { error: "unknown event" });
|
|
529
|
+
});
|
|
524
530
|
// 路由表的诊断视图 (`?owner=` = 那个 wizard 名下的行, 缺省 = 全表)。
|
|
525
531
|
http.register("GET /routes", (_req, res, url) => {
|
|
526
532
|
const owner = url.searchParams.get("owner")?.trim();
|
|
@@ -924,17 +930,6 @@ const main = async () => {
|
|
|
924
930
|
const relayLabel = (t) => ((h) => (h ? tagLink(t, h) : ""))(tagHead(t));
|
|
925
931
|
/** 以 `self` 的名义往 `dest` 群发一条: 头独占一行; 头怎么写 (群默认 wizard 报不报名) 归 ownHead。 */
|
|
926
932
|
const postAs = (self, dest, text) => notifyChat(dest, headed(ownHead(self, dest), text, "\n\n"));
|
|
927
|
-
// 没开顶层模式的群里公开气泡多了 → 往那个群的管家 (群默认会话, target 就是 base principal) 信箱挂一行,
|
|
928
|
-
// 让它问人要不要开顶层模式 (见 top-only-nudge.ts)。路径里写聊天名: 管家照抄就能 config_set。
|
|
929
|
-
const topOnlyNudge = createTopOnlyNudge({
|
|
930
|
-
store: loadJsonMap("~/.wezard/top-only-nudge.json", gcNudge),
|
|
931
|
-
policy: () => ((p) => ({ windowMs: p.windowMin * 60_000, threshold: p.threshold, cooldownMs: p.cooldownH * 3_600_000 }))(cfg.wrc.mirror.topOnlyNudge),
|
|
932
|
-
nudge: (chat, n) => {
|
|
933
|
-
const p = cfg.wrc.mirror.topOnlyNudge;
|
|
934
|
-
notices.post([chat], renderNudge(policyKeyOf(chatNameOf(cfg, chat), chat), n, p.windowMin, p.cooldownH));
|
|
935
|
-
log.info({ chat, n }, "top-only nudge: 已提醒管家问人要不要开顶层模式");
|
|
936
|
-
},
|
|
937
|
-
});
|
|
938
933
|
/** 公开的 wizard 间对话: 落在 `channel` 这个群。 */
|
|
939
934
|
const relayPeer = (from, to, body, channel) => {
|
|
940
935
|
const text = body.trim();
|
|
@@ -945,8 +940,6 @@ const main = async () => {
|
|
|
945
940
|
// 头独占一行, 正文自成一个块 —— 只隔一个换行的话, markdown 会把正文首行
|
|
946
941
|
// 当成头那一段的续行; 表格因此整张塌成一行带竖线的文字 (表格不能打断段落)。
|
|
947
942
|
notifyChat(channel, `${head}\n\n${clipped}`);
|
|
948
|
-
if (!chatPolicyOf(cfg, channel).topOnly)
|
|
949
|
-
topOnlyNudge.count(baseOfKey(channel));
|
|
950
943
|
};
|
|
951
944
|
/** 调用方这一轮所在的公开频道: 人从哪个群叫的它 / 公开 peer 轮的那个群;
|
|
952
945
|
* 私聊轮或无记录 → 它的 home 群。notify 与 public send_peer 默认发到这里。 */
|
|
@@ -986,8 +979,8 @@ const main = async () => {
|
|
|
986
979
|
};
|
|
987
980
|
/** 一笔活第几个来回 (§3.2): 同一行上已经注入过的续问数 + 1 —— 现算, 不落第二份数据。
|
|
988
981
|
* 续问的注入事件 id 是 `route:<event>:r<legs>`, 所以从它自己就能数回来 (reload 也在盘上)。 */
|
|
989
|
-
const legsOf = (event) => events.
|
|
990
|
-
const m =
|
|
982
|
+
const legsOf = (event) => events.ids().reduce((n, id) => {
|
|
983
|
+
const m = id.startsWith(`route:${event}:r`) ? Number(id.slice(`route:${event}:r`.length)) : NaN;
|
|
991
984
|
return Number.isInteger(m) && m > n ? m : n;
|
|
992
985
|
}, 1);
|
|
993
986
|
/** 同一笔活上的一句续问 (idle-nudge 追问 / 工单打回): 不新开行, 注入事件 id
|
|
@@ -1008,8 +1001,9 @@ const main = async () => {
|
|
|
1008
1001
|
} } });
|
|
1009
1002
|
return true;
|
|
1010
1003
|
};
|
|
1011
|
-
/** 已经因为不合工单验收被打回过的那些笔 (行状态会随打回留在 `open`, 光看它认不出「打回过」)
|
|
1012
|
-
|
|
1004
|
+
/** 已经因为不合工单验收被打回过的那些笔 (行状态会随打回留在 `open`, 光看它认不出「打回过」)
|
|
1005
|
+
* → 被打回的那一轮的注入事件 id: 同一轮的终句重放 (见 deliverFinal) 认得出不是补交。 */
|
|
1006
|
+
const bounced = new Map();
|
|
1013
1007
|
/** 工单验收 (§9.6, 从 receipts 的 watcher 重建): 交的活不合规格 → 这一份不算交差。
|
|
1014
1008
|
* 计划节点有自己的验收 (workflow), 不经这里。 */
|
|
1015
1009
|
const acceptanceOf = (row, body) => {
|
|
@@ -1022,8 +1016,8 @@ const main = async () => {
|
|
|
1022
1016
|
return j?.status === "open" ? rejectReason(j.accept ?? "result", parseClosing(body)) : undefined;
|
|
1023
1017
|
};
|
|
1024
1018
|
/** 以工单 owner 的名义在同一笔上打回一次: 不新开行, 行留在 `open` 等它按约定再交一份。 */
|
|
1025
|
-
const rebound = (row, why) => {
|
|
1026
|
-
bounced.
|
|
1019
|
+
const rebound = (row, why, eventId) => {
|
|
1020
|
+
bounced.set(row.event, eventId);
|
|
1027
1021
|
const from = ledgerOwnerOf(row.event) || row.owner;
|
|
1028
1022
|
acceptContinuation({
|
|
1029
1023
|
from, to: row.owner, channel: "", event: row.event, legs: legsOf(row.event) + 1,
|
|
@@ -1059,6 +1053,16 @@ const main = async () => {
|
|
|
1059
1053
|
// 交接是生命周期事件, 不进群; 失败只捎给它自己 (它调完 handoff_self 拿到的是
|
|
1060
1054
|
// scheduled:true, 不说的话它不知道上下文其实没换)。
|
|
1061
1055
|
notify: (t, text) => notices.post([t], text),
|
|
1056
|
+
// 交接没做成: 切断那一轮扣下的终句按原行补投 —— 走与轮终同一条 onFinal (行已被合成失败落定的, 照追述规则处理)。
|
|
1057
|
+
uncut: (t, ids) => ids.forEach((id) => {
|
|
1058
|
+
const e = events.get(id);
|
|
1059
|
+
if (!e?.result) {
|
|
1060
|
+
log.warn({ target: t, event: id }, "handoff: 扣下的终句已不在事件库, 补投不了");
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
log.info({ target: t, event: id, serves: e.serves }, "handoff: 交接没做成, 补投切断那一轮的终句");
|
|
1064
|
+
settleFinal(e, { text: e.result.body });
|
|
1065
|
+
}),
|
|
1062
1066
|
log,
|
|
1063
1067
|
store: loadJsonMap(cfg.wrc.mirror.handoffsFile),
|
|
1064
1068
|
archive: (e) => {
|
|
@@ -1082,7 +1086,7 @@ const main = async () => {
|
|
|
1082
1086
|
const waiting = r.kind === "wizard" ? r.to : baseOfKey(r.channel); // 等着它交代的那一方 (信封挂在它名下)
|
|
1083
1087
|
const channel = r.kind === "chat" ? r.channel : ""; // 结论要落回群里时才带频道 (§9.4)
|
|
1084
1088
|
const base = `route:${event}:resume`;
|
|
1085
|
-
const id = events.
|
|
1089
|
+
const id = events.known(base) ? `${base}:${at}` : base;
|
|
1086
1090
|
try {
|
|
1087
1091
|
events.accept({
|
|
1088
1092
|
id, identity: JSON.stringify([t, event, "resume", at]),
|
|
@@ -1250,7 +1254,12 @@ const main = async () => {
|
|
|
1250
1254
|
if (row.job && isTerminal(out.status) && !row.late)
|
|
1251
1255
|
jobs.settle(row.job, row.owner, out.status, parseClosing(out.body).artifacts, { turn: event });
|
|
1252
1256
|
}
|
|
1253
|
-
|
|
1257
|
+
postRoute(row, out, router.escalate(event, (k) => !!wizards.get(k)), `rcpt:${event}:${out.rev}`);
|
|
1258
|
+
};
|
|
1259
|
+
/** 一份定论的投递 (账本已记过): 投给升级后的收方 `esc` (没有 = 行上的 receiver)。群收方只落
|
|
1260
|
+
* `hand`; wizard 收方 (含 Dead Letter 退到的群默认 wizard) 进收件箱, id = `inbox`。 */
|
|
1261
|
+
const postRoute = (row, out, esc, inbox) => {
|
|
1262
|
+
const event = row.event;
|
|
1254
1263
|
const receiver = esc?.receiver ?? row.receiver;
|
|
1255
1264
|
const via = (esc?.via ?? []).map(displayName);
|
|
1256
1265
|
// 升级落在 chat 根 = Dead Letter: 退到那个群, 由它的默认 wizard 收下这份结论。
|
|
@@ -1274,7 +1283,6 @@ const main = async () => {
|
|
|
1274
1283
|
const retellTo = late && servedRow && servedRow.owner === to && servedRow.outcome?.status !== "canceled"
|
|
1275
1284
|
? servedRow.receiver.kind === "wizard" ? displayName(servedRow.receiver.to) : chatNameOf(cfg, servedRow.receiver.channel) || servedRow.receiver.channel
|
|
1276
1285
|
: undefined;
|
|
1277
|
-
const inbox = `rcpt:${event}:${out.rev}`;
|
|
1278
1286
|
const job = row.job ? jobs.get(row.job) : undefined;
|
|
1279
1287
|
const tally = row.job ? jobs.tally(row.job) : undefined;
|
|
1280
1288
|
events.accept({
|
|
@@ -1299,6 +1307,34 @@ const main = async () => {
|
|
|
1299
1307
|
});
|
|
1300
1308
|
router.commit(ops.hand({ event, inbox }));
|
|
1301
1309
|
};
|
|
1310
|
+
/** 收件箱里那份回执投不到了 (`gone`: 收方没有绑定, 按 store 也恢复不了) —— 投进去时它还在
|
|
1311
|
+
* 注册表上, 升级没跳过它。现在把它当不在, 沿 receiver 边重走一次升级: 能收的上层 wizard /
|
|
1312
|
+
* 群根 (Dead Letter) 收下。链上无处可升 (根行的收方自己没了): canceled 是收掉它的那一方自己
|
|
1313
|
+
* 发起的, 没人在等, 不再投; 别的定论退到收方的家群 (Dead Letter)。账本不再记第二遍。 */
|
|
1314
|
+
const rerouteGone = (e) => {
|
|
1315
|
+
const from = e.payload.peer?.from;
|
|
1316
|
+
const event = from?.receipt ? from.event : undefined;
|
|
1317
|
+
const row = event ? router.get(event) : undefined;
|
|
1318
|
+
if (!event || !row || !from?.status) {
|
|
1319
|
+
log.warn({ id: e.id, target: e.target }, "receipt reroute: 认不出回执的那一行, 投不出去");
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
const reachable = (k) => k !== e.target && !!wizards.get(k) && !!mirrorStore.get(k);
|
|
1323
|
+
const home = baseOfKey(e.target);
|
|
1324
|
+
const esc = router.escalate(event, reachable)
|
|
1325
|
+
?? (row.receiver.kind === "wizard" && reachable(row.receiver.to) ? undefined
|
|
1326
|
+
: from.status === "canceled" || home === e.target ? null
|
|
1327
|
+
: { receiver: { kind: "chat", channel: home, message: "" }, serves: "", via: [e.target] });
|
|
1328
|
+
if (esc === null) {
|
|
1329
|
+
log.info({ id: e.id, target: e.target, event, status: from.status }, "receipt reroute: 收方已不在且无处可升, 不再投");
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
const rev = e.id.slice(`rcpt:${event}:`.length).split("~")[0];
|
|
1333
|
+
const prefix = `rcpt:${event}:${rev}~`;
|
|
1334
|
+
const inbox = `${prefix}${events.ids().filter((id) => id.startsWith(prefix)).length + 1}`;
|
|
1335
|
+
log.info({ id: e.id, target: e.target, event, to: esc?.receiver ?? row.receiver, inbox }, "receipt reroute: 收方已不在, 改投");
|
|
1336
|
+
postRoute(row, { status: from.status, body: e.payload.text, rev }, esc, inbox);
|
|
1337
|
+
};
|
|
1302
1338
|
/** 一条路由 op: 先落盘, 再投该投的 (need / error 带正文入箱; answer 按 receiver 投递)。这里
|
|
1303
1339
|
* 只管 `re` (`mark open`) 把那一行拉回在飞 —— 账本与工单跟着 deliverRoute 走。 */
|
|
1304
1340
|
const commitRoute = (op) => {
|
|
@@ -1320,6 +1356,23 @@ const main = async () => {
|
|
|
1320
1356
|
* 也最多再多打回一次)。**这是「轮终 evidence → onFinal」那条主触发链的落点**: 没有它,
|
|
1321
1357
|
* answer 就只能靠 sweeper / 级联取消 / boot 重放合成, 谁答完都不会有回执。 */
|
|
1322
1358
|
deliverFinal = (e, verdict) => {
|
|
1359
|
+
// 同一轮的终句会递进来两次: result() 当场递一次, 泵见 complete 未 finalized 再补判一次
|
|
1360
|
+
// (message-events)。被打回那一轮的重放不是补交 —— 丢掉; 否则 bounced 已记着这一笔,
|
|
1361
|
+
// 第二次就把被打回的原文照收成 done (打回的续问还没注入, 定论先落了)。
|
|
1362
|
+
if (e.serves && bounced.get(e.serves) === e.id)
|
|
1363
|
+
return;
|
|
1364
|
+
// 自我交接切断的那一轮: 收尾那句不是欠着的那几笔的答案 —— 行留 open, 交给新会话的「继续 <event>」。
|
|
1365
|
+
// 扣下记在这场交接上: 交接没做成 (新会话没起来), 它就是那几笔仅有的交代, 由 uncut 补投。
|
|
1366
|
+
if (cuttingTurn(e.target)) {
|
|
1367
|
+
log.info({ event: e.id, target: e.target, serves: e.serves }, "handoff: 交接切断的那一轮终句不落定");
|
|
1368
|
+
if (!verdict.error)
|
|
1369
|
+
handoffs.cut(e.target, e.id);
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
settleFinal(e, verdict);
|
|
1373
|
+
};
|
|
1374
|
+
/** 一轮的终句落到它服务的那一行上 (定论 / 中途态), 验收不合规格就打回续问。 */
|
|
1375
|
+
const settleFinal = (e, verdict) => {
|
|
1323
1376
|
const plans = router.onFinal(e.target, e.serves, verdict);
|
|
1324
1377
|
const answer = plans.flatMap((op) => (op.op === "answer" && op.outcome.status === "done" ? [op] : []))[0];
|
|
1325
1378
|
const row = answer ? router.get(answer.event) : undefined;
|
|
@@ -1327,7 +1380,7 @@ const main = async () => {
|
|
|
1327
1380
|
if (row && why) {
|
|
1328
1381
|
if (row.status === "need" || row.status === "error")
|
|
1329
1382
|
commitRoute(ops.mark({ event: row.event, status: "open" }));
|
|
1330
|
-
rebound(row, why);
|
|
1383
|
+
rebound(row, why, e.id);
|
|
1331
1384
|
return;
|
|
1332
1385
|
}
|
|
1333
1386
|
plans.forEach(commitRoute);
|
|
@@ -1879,7 +1932,9 @@ const main = async () => {
|
|
|
1879
1932
|
valid: (e) => {
|
|
1880
1933
|
const p = e.payload.peer;
|
|
1881
1934
|
const attempt = p.attemptId ? workflow.byTurn(e.speaker, p.from.event ?? "") : undefined;
|
|
1882
|
-
|
|
1935
|
+
// 单已收但这一笔的行还 open = 收工前就派出去的活, 照投 (见 close_job)。
|
|
1936
|
+
return planStore.healthy() && !e.canceled && (!p.from.receipt || planContinuationAllowed(e.target))
|
|
1937
|
+
&& (!p.job || jobs.get(p.job)?.status === "open" || (!!e.serves && router.get(e.serves)?.status === "open")) && !attempt?.canceled
|
|
1883
1938
|
&& (!p.after?.length || prereqsOf(e.speaker, p.after).every((r) => r.state === "done" || r.state === "pending"));
|
|
1884
1939
|
},
|
|
1885
1940
|
accepted: (e) => {
|
|
@@ -1918,6 +1973,15 @@ const main = async () => {
|
|
|
1918
1973
|
if (p.attemptId)
|
|
1919
1974
|
workflow.cancel(e.target, e.speaker, p.job, p.from.event);
|
|
1920
1975
|
},
|
|
1976
|
+
// 收方没了: 回执沿链改投; 派活 / 续问不另做 —— 那一行的 owner 就是它, sweeper 见 pane 不在判 dead 上浮。
|
|
1977
|
+
gone: (e) => {
|
|
1978
|
+
const p = e.payload.peer;
|
|
1979
|
+
log.warn({ id: e.id, target: e.target, receipt: !!p.from.receipt }, "message event: 收方已不在 (无绑定, 恢复失败), 不再重投");
|
|
1980
|
+
if (p.attemptId)
|
|
1981
|
+
workflow.dispatched(p.attemptId, false);
|
|
1982
|
+
if (p.from.receipt)
|
|
1983
|
+
rerouteGone(e);
|
|
1984
|
+
},
|
|
1921
1985
|
ready: async (e) => prereqsOf(e.speaker, e.payload.peer.after ?? []).every((r) => r.state === "done")
|
|
1922
1986
|
&& (e.priority === "now" || await m.idleNow(e.target) || !(await m.paneLive(e.target)) || (e.priority === "urgent" && !(await othersTurn(e.target, e.speaker)))),
|
|
1923
1987
|
inject: async (e, boundary) => {
|
|
@@ -1956,6 +2020,7 @@ const main = async () => {
|
|
|
1956
2020
|
return false;
|
|
1957
2021
|
return router.finalReleased(e.target, e.serves, (events.get(e.id) ?? e).result?.body ?? "");
|
|
1958
2022
|
});
|
|
2023
|
+
m.setEventHandedAway((e) => { const row = e.serves ? router.get(e.serves) : undefined; return !!row && row.owner !== e.target; });
|
|
1959
2024
|
// boot 的路由表侧恢复 (T8): held 重查前置、answered 重投一次; 二者幂等, 与 events.resume
|
|
1960
2025
|
// 重放的注入/回执撞上无害。
|
|
1961
2026
|
void m.restored.then(() => { events.resume(); settleHeld(); replayAnswered(); });
|
|
@@ -2273,6 +2338,9 @@ const main = async () => {
|
|
|
2273
2338
|
catch (error) {
|
|
2274
2339
|
return { status: 409, body: { ok: false, eventId, reason: errText(error) } };
|
|
2275
2340
|
}
|
|
2341
|
+
// 交出的是人问的那一笔: 那句话的气泡跟着行走, 接手方开轮时接过去续写 (不另开新气泡)。
|
|
2342
|
+
if (handedChat && serves)
|
|
2343
|
+
m.handBubble(self, serves, eventId, target);
|
|
2276
2344
|
// `re` 把那一笔从 need / error 拉回 open (§3.2 的 need→open / error→open): 行状态是「这份中途态
|
|
2277
2345
|
// 已经报过了」的去重记号 —— 不拉回去, owner 下一轮交同一个中途态就被静默吞掉 (往返断在这里)。
|
|
2278
2346
|
if (reTarget && (reTarget.status === "need" || reTarget.status === "error"))
|
|
@@ -2424,7 +2492,7 @@ const main = async () => {
|
|
|
2424
2492
|
// 同一对并发多笔是允许的 (§4.1): 先取**还在排队**的那份 (那份还没进过你的会话); 都取过了就
|
|
2425
2493
|
// 报最新那件已交付 —— 绝不把已经 paste 过的那份当成新答案交回来。
|
|
2426
2494
|
const mine = router.settledTo(self).filter((r) => r.owner === h.target).reverse() // byAt → 新在前
|
|
2427
|
-
.map((row) => { const box = receiptOf(row); return { row, box, ev:
|
|
2495
|
+
.map((row) => { const box = receiptOf(row); const execution = events.executionOf(box.id); return { row, box, ev: execution && { execution } }; });
|
|
2428
2496
|
const pick = mine.find((x) => x.ev?.execution === "queued") ?? mine[0];
|
|
2429
2497
|
if (!pick)
|
|
2430
2498
|
return { name: h.address, idle: wr.idle, ...(wr.reason ? { reason: wr.reason } : {}),
|
|
@@ -3284,10 +3352,9 @@ const main = async () => {
|
|
|
3284
3352
|
json(res, 409, { ok: false, reason: "transfer hands over the row this turn owes; a lead dispatch cannot transfer" });
|
|
3285
3353
|
return;
|
|
3286
3354
|
}
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
}
|
|
3355
|
+
// 交出人问的那一笔同样合法 (同 tell_peer): 那一行归 chat receiver, 与同伴派的件走同一条
|
|
3356
|
+
// move —— 接手方终句就是那句人话的答复。没得交 (`serves` 落空 / 已不在手上) 由
|
|
3357
|
+
// transferRefusal 在投递前拦, 不在这里按 receiver 种类预判。
|
|
3291
3358
|
const force = b.force === true;
|
|
3292
3359
|
const to = (b.to ?? "").toString().trim();
|
|
3293
3360
|
const now = Date.now();
|
|
@@ -3363,12 +3430,8 @@ const main = async () => {
|
|
|
3363
3430
|
d = { kind: "spawn", why: `${d.why}; 但它被${t.body.gated === "direction" ? "方向闸" : "冷门控"}退回 (${String(t.body.reason)}) —— 改为白板 spawn` };
|
|
3364
3431
|
}
|
|
3365
3432
|
const name = (b.name ?? "").toString().trim();
|
|
3366
|
-
const description = (b.description ?? "").toString().trim();
|
|
3367
|
-
|
|
3368
|
-
settleRoot(false);
|
|
3369
|
-
json(res, 400, { ok: false, decision: "spawn", reason: `${d.why} —— 要新生一个, 带上 name (这件事的短名) 与 description (它的职责) 重发`, ...(shown.length ? { candidates: shown } : {}) });
|
|
3370
|
-
return;
|
|
3371
|
-
}
|
|
3433
|
+
const description = (b.description ?? "").toString().trim() || clipLine(task, 120);
|
|
3434
|
+
// 空 name 交给 bearAs / claimSlot 按职责自动取名避重, 不当作显式点名复用旧记录。
|
|
3372
3435
|
const tier = taskTier;
|
|
3373
3436
|
const born = await bearAs(self, { inherit: false, name, description, tier, ...(lead ? { lead: true } : {}) }, undefined, dispatchOrigin?.authority);
|
|
3374
3437
|
if (born.status !== 200) {
|
|
@@ -3478,9 +3541,8 @@ const main = async () => {
|
|
|
3478
3541
|
const plan = b.task?.trim() && !planned ? planReturn(self, body) : undefined;
|
|
3479
3542
|
if (plan && !plan.ok)
|
|
3480
3543
|
return { status: plan.status, body: { ok: false, reason: plan.reason } };
|
|
3481
|
-
// transfer
|
|
3482
|
-
|
|
3483
|
-
return { status: 409, body: { ok: false, reason: "这一轮没有欠着的同伴活可以交出去 (transfer 只用于同伴派给你的那一笔)" } };
|
|
3544
|
+
// transfer 交出人问的那一笔同样合法 (同 tell_peer, 那一行归 chat receiver) —— 没得交 /
|
|
3545
|
+
// 已不在手上由下面的 transferRefusal 拦, 不在这里按 receiver 种类预判。
|
|
3484
3546
|
const askedEffort = parseEffort(b.effort);
|
|
3485
3547
|
if (b.effort !== undefined && !askedEffort) {
|
|
3486
3548
|
return { status: 400, body: { ok: false, reason: `effort 只有这几档: ${EFFORTS.join(" / ")}` } };
|
|
@@ -3654,21 +3716,28 @@ const main = async () => {
|
|
|
3654
3716
|
if (dispatched) {
|
|
3655
3717
|
const deadlineAt = deadlineOf(taskAt);
|
|
3656
3718
|
const quiet = !!plan?.ok && plan.op === "none";
|
|
3719
|
+
// transfer 交出的是人问的那一笔 (同 tell_peer): 注入事件得带上那个群, 否则频道为空被当
|
|
3720
|
+
// 私聊轮扣下, 刚出生的接手方那句终句人永远收不到 (见 mirror-bridge 的 final 通道)。
|
|
3721
|
+
// 同伴派的件 (receiver 是 wizard) 仍旧私聊 —— handedChat 为空, 行为不变。
|
|
3722
|
+
const handed = plan?.ok && plan.op === "transfer" && plan.serves ? router.get(plan.serves)?.receiver : undefined;
|
|
3723
|
+
const handedChat = handed?.kind === "chat" ? handed.channel : "";
|
|
3657
3724
|
// 路由行 (§3.1): 生出来的那个领活, 结论回生它的这位。
|
|
3658
3725
|
const serves = plan?.ok
|
|
3659
3726
|
? openDispatchRow({ self, target, op: plan.op, deadlineAt, job: jobId, ...(asker ? { asker } : {}), ...(plan.serves ? { serves: plan.serves } : {}) })
|
|
3660
3727
|
: "";
|
|
3661
3728
|
dispatchedEvent = serves;
|
|
3662
3729
|
const eventId = serves ? `route:${serves}` : `p:${self}:${target}:${taskAt}:1`;
|
|
3663
|
-
const birthTell = { from: self, to: target, channel:
|
|
3664
|
-
events.accept({ id: eventId, target, speaker: self, channel:
|
|
3730
|
+
const birthTell = { from: self, to: target, channel: handedChat, job: jobId, at: taskAt, event: serves, legs: 1, deadlineAt, asker, dispatchKey: eventId };
|
|
3731
|
+
events.accept({ id: eventId, target, speaker: self, channel: handedChat, serves, asker, priority: "normal", deadline: deadlineAt,
|
|
3665
3732
|
payload: { text: task, peer: {
|
|
3666
|
-
from: { kind: "peer", from: self, event: serves, ...(jobId ? { job: jobId } : {}), ...(asker ? { asker } : {}) },
|
|
3667
|
-
envelope: envelopeFor(self,
|
|
3733
|
+
from: { kind: "peer", from: self, event: serves, ...(jobId ? { job: jobId } : {}), ...(handedChat ? { transferred: true } : {}), ...(asker ? { asker } : {}) },
|
|
3734
|
+
envelope: envelopeFor(self, handedChat, { asker, ...(quiet ? { quiet: true } : {}) }) + (b.lead === true ? renderReminder({ wezard: "lead" }, renderLeadPlaybook()) : ""),
|
|
3668
3735
|
...(quiet ? {} : { tell: birthTell }),
|
|
3669
3736
|
job: jobId, kind: "task", spawned: true, role: roleParam(body.role),
|
|
3670
3737
|
...(plan?.ok && plan.op === "transfer" ? { transferred: true } : {}),
|
|
3671
3738
|
} } });
|
|
3739
|
+
if (handedChat && serves)
|
|
3740
|
+
m.handBubble(self, serves, eventId, target);
|
|
3672
3741
|
}
|
|
3673
3742
|
else if (jobId && birthGuard()) {
|
|
3674
3743
|
jobs.attach(jobId, { target, task: "", spawned: true, ...(roleParam(body.role) ? { role: roleParam(body.role) } : {}) });
|
|
@@ -4018,8 +4087,10 @@ const main = async () => {
|
|
|
4018
4087
|
// 这张单名下还排着没投的 (held: 等前置 / 没轮上) 也不投了 —— 单都关了, 它再也轮不到
|
|
4019
4088
|
// (注入的闸要求单还开着)。receipts.cancelHeld 同一条, 那边只做账 (T6)。
|
|
4020
4089
|
cancelRoutes(router.rows().filter((r) => r.status === "held" && r.job === j.id).map((r) => r.event), () => `随工单 ${j.id} 收了, 这件排着的活没投。`, j.owner);
|
|
4021
|
-
// 这张单名下还排着没投的 (after 停放)
|
|
4022
|
-
|
|
4090
|
+
// 这张单名下还排着没投的 (after 停放) 也不投了。行还 open 的那笔是已经派出去的活
|
|
4091
|
+
// (出生事件常在冷 pane 上排着没注入): 收工 (accept) 不收活 —— 照投、照答, 回执标「已经收工」;
|
|
4092
|
+
// 行被上面级联落定 (cancel / held) 的、没开行的才撤。
|
|
4093
|
+
events.cancel((e) => e.speaker === j.owner && e.payload.peer?.job === j.id && router.get(e.serves ?? "")?.status !== "open");
|
|
4023
4094
|
const closed = jobs.close(j.id, summary, mark);
|
|
4024
4095
|
ledger.closeJob(j.id);
|
|
4025
4096
|
// 收工结论留档到开单者名下 (与交接简报同一份 jsonl); 空工单 (没人、没结论) 不记。
|
|
@@ -4862,9 +4933,14 @@ const main = async () => {
|
|
|
4862
4933
|
}
|
|
4863
4934
|
setTimeout(() => void sweep().catch((e) => log.warn({ err: e.message }, "pane cap sweep failed")), 15_000).unref();
|
|
4864
4935
|
}
|
|
4936
|
+
let shuttingDown = false;
|
|
4865
4937
|
const shutdown = async (signal) => {
|
|
4938
|
+
if (shuttingDown)
|
|
4939
|
+
return;
|
|
4940
|
+
shuttingDown = true;
|
|
4866
4941
|
log.info({ signal }, "shutdown signal");
|
|
4867
|
-
|
|
4942
|
+
// Pause delivery, but persist arrivals while transports/in-flight handlers drain.
|
|
4943
|
+
events.drain();
|
|
4868
4944
|
stopSweep();
|
|
4869
4945
|
scheduler.stop();
|
|
4870
4946
|
steward?.stop();
|
|
@@ -4879,6 +4955,7 @@ const main = async () => {
|
|
|
4879
4955
|
setTimeout(() => process.exit(0), 1500).unref();
|
|
4880
4956
|
await new Promise((r) => setTimeout(r, 200));
|
|
4881
4957
|
await Promise.allSettled([ws.shutdown(), http.close()]);
|
|
4958
|
+
events.stop();
|
|
4882
4959
|
process.exit(0);
|
|
4883
4960
|
};
|
|
4884
4961
|
http.register("POST /shutdown", (_req, res) => {
|