wezard 2.7.4 → 2.8.0
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/dist/daemon/approval.js +7 -6
- package/dist/daemon/approval.js.map +1 -1
- package/dist/daemon/inbound.js +1 -1
- package/dist/daemon/inbound.js.map +1 -1
- package/dist/daemon/index.js +226 -67
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/jobs.js +7 -4
- package/dist/daemon/jobs.js.map +1 -1
- package/dist/daemon/message-events.js +220 -23
- package/dist/daemon/message-events.js.map +1 -1
- package/dist/daemon/mirror-bridge.js +439 -51
- package/dist/daemon/mirror-bridge.js.map +1 -1
- package/dist/daemon/peers.js +256 -0
- package/dist/daemon/peers.js.map +1 -1
- package/dist/daemon/route.js +274 -49
- package/dist/daemon/route.js.map +1 -1
- package/dist/daemon/router.js +93 -28
- package/dist/daemon/router.js.map +1 -1
- package/dist/daemon/spawn-tmux.js +6 -7
- package/dist/daemon/spawn-tmux.js.map +1 -1
- package/dist/daemon/wizard.js +11 -11
- package/dist/daemon/wizard.js.map +1 -1
- package/dist/mcp/server.js +8 -6
- package/dist/mcp/server.js.map +1 -1
- package/dist/shared/codex-hooks.js +60 -0
- package/dist/shared/codex-hooks.js.map +1 -1
- package/dist/shared/message-footer.js +16 -1
- package/dist/shared/message-footer.js.map +1 -1
- package/dist/shared/modal-pane.js +18 -2
- package/dist/shared/modal-pane.js.map +1 -1
- package/dist/shared/tool-hints.js +2 -2
- package/dist/shared/tool-hints.js.map +1 -1
- package/package.json +1 -1
package/dist/daemon/index.js
CHANGED
|
@@ -26,8 +26,8 @@ import { applyHot, cfgSeenBy, patchedTop, pinChatKeys, configGet, fromLegacy, le
|
|
|
26
26
|
import { computeUsage, costOf, fmtCost, fmtTokens, sumTotal } from "./usage.js";
|
|
27
27
|
import { installInboundRouter } from "./inbound.js";
|
|
28
28
|
import { loadMirrorStore } from "./mirror-store.js";
|
|
29
|
-
import { cancelOps, deadlineOf, eventRef, isEventRef, makeOutcome, openRouter, ops, pickPair } from "./router.js";
|
|
30
|
-
import { createMessageEvents, messageId, finalId } from "./message-events.js";
|
|
29
|
+
import { cancelOps, deadlineOf, eventRef, finalKind, isEventRef, makeOutcome, openRouter, ops, pickPair } from "./router.js";
|
|
30
|
+
import { createMessageEvents, messageId, finalId, reportSource, receiptCoverage, receiptFacts } from "./message-events.js";
|
|
31
31
|
import { startMirror, installMirrorEventListener } from "./mirror-bridge.js";
|
|
32
32
|
import { backendFor, runTmux, setTmuxTimeoutReporter, spawnTmuxClaude } from "./spawn-tmux.js";
|
|
33
33
|
import { bindCardChannel, installApprovalEventListener, makeApproveHandler, requestConfirm } from "./approval.js";
|
|
@@ -57,7 +57,7 @@ import { activeRows, bindNoticeBox, createNoticeBox, createSeenRing, gcSeen, cha
|
|
|
57
57
|
import { createLedger, digest, pendingGc, renderDigest, renderTable, renderAwaiting, renderStalled, renderShelved, ago as agoMs, HEARD_EVERY_MS } from "./pending-items.js";
|
|
58
58
|
import { checkDirection } from "./direction.js";
|
|
59
59
|
import { loadJobStore, dutyLine, awaitingAccept, stalledRoots, shelvedRoots, nudgeDue, dayClamp, childrenOf as jobChildren, jobEpisode, rejectReason, ACCEPTS, JOB_MEMBER_MAX, JOB_DEPTH_MAX, depthOf, ancestorsOf as jobAncestors, treeOrder, jobStage } from "./jobs.js";
|
|
60
|
-
import { cacheTouchMs, cacheTtlSec, clipMiddle,
|
|
60
|
+
import { cacheTouchMs, cacheTtlSec, clipMiddle, contextMaterials, firstStamp, parseClosing, stripClosingMark, stripForChat, lastContextTokens, lastExchange, lastModel, openingTurn, talkTurns, turnsSince, renderPeerEnvelope, renderReceiptEnvelope, renderTaskEnvelope } from "./peers.js";
|
|
61
61
|
import { expandHome } from "../shared/paths.js";
|
|
62
62
|
import { loadJsonMap } from "../shared/json-map-store.js";
|
|
63
63
|
import { createWorkflowNodes } from "./workflow-nodes.js";
|
|
@@ -68,7 +68,7 @@ import { isTerminal, renderInFlight, turnState } from "../shared/turn-state.js";
|
|
|
68
68
|
import { startIdleNudge } from "./idle-nudge.js";
|
|
69
69
|
import { askerFromEnvelope, renderReminder, withMsg } from "../shared/reminder.js";
|
|
70
70
|
import { createHandoffs, cuttingTurn, handedOff, handingOff } from "./handoff.js";
|
|
71
|
-
import { coldGateOf, decide, rankCandidates, renderCandidates, routeDutiesOf, tierFor, wakeCostOf } from "./route.js";
|
|
71
|
+
import { coldGateOf, decide, rankCandidates, routeDiagnostics, renderCandidates, routeDutiesOf, tierFor, wakeCostOf } from "./route.js";
|
|
72
72
|
import { parseWhen, renderChatLog, UNKNOWN_HUMAN } from "./chat-log.js";
|
|
73
73
|
import { audienceOf } from "../shared/role-view.js";
|
|
74
74
|
import { startGraph, stopRun, getRun, listRuns, validateSpec, waitForIdle, waitForQuorum, } from "./graph.js";
|
|
@@ -207,6 +207,13 @@ const main = async () => {
|
|
|
207
207
|
let deliverFinal;
|
|
208
208
|
let handoffAllowed = () => false;
|
|
209
209
|
let handoffFinalSuppressed = (e) => !!e.handoffCut && !e.handoffRestored;
|
|
210
|
+
const ownsReplyEvent = (e) => {
|
|
211
|
+
const row = e.serves ? router.get(e.serves) : undefined;
|
|
212
|
+
if (row && row.owner !== e.target)
|
|
213
|
+
return false;
|
|
214
|
+
const latest = e.serves ? events.latestTransfer(e.serves) : undefined;
|
|
215
|
+
return !latest || latest.id === e.id || latest.at < e.at;
|
|
216
|
+
};
|
|
210
217
|
const events = createMessageEvents(join(cfg.daemon.stateDir, "message-events.json"), {
|
|
211
218
|
resource: (target) => bridge.sessionInfo(target)?.tmuxPane || target,
|
|
212
219
|
transcript: (target) => bridge.inputSessionInfo(target)?.jsonlPath ?? bridge.sessionInfo(target)?.jsonlPath,
|
|
@@ -226,7 +233,7 @@ const main = async () => {
|
|
|
226
233
|
events.result(e.id, body, finalId(e.id, e.channel, body));
|
|
227
234
|
// A canceled, unexposed input has a terminal response owned by its original channel.
|
|
228
235
|
events.delivery({ id: finalId(e.id, e.channel, body), eventId: e.id, target: e.target, turn: e.turn || e.id,
|
|
229
|
-
channel: e.channel, revision: finalId(e.id, e.channel, body), body, chunks: bridge.formatFinal(e.target, e.turn || e.id, e.channel, body) });
|
|
236
|
+
channel: e.channel, revision: finalId(e.id, e.channel, body), body, purpose: "cancellation", reportKind: "canceled", chunks: bridge.formatFinal(e.target, e.turn || e.id, e.channel, body) });
|
|
230
237
|
}
|
|
231
238
|
},
|
|
232
239
|
// 人话 (非 peer) 不再问「pane 闲不闲」: 忙就贴进去, 顺序交给 backend CLI 自己的输入队列
|
|
@@ -281,8 +288,9 @@ const main = async () => {
|
|
|
281
288
|
// 一轮以 CLI 报错收尾: 那段报错不是定论, 但要作为中途态 (error) 报给 receiver ——
|
|
282
289
|
// 所以照旧不落成 result, 只把原文交给 final (见 §9.4)。
|
|
283
290
|
return { consumed: true, interjected: !!turns[start].queued,
|
|
284
|
-
...(next >= 0 ? { closedAt: turns[next].ms ?? Date.now() }
|
|
285
|
-
|
|
291
|
+
...(next >= 0 ? { closedAt: turns[next].ms ?? Date.now() }
|
|
292
|
+
: last?.end && !last.text ? { closedAt: last.ms ?? Date.now() } : {}),
|
|
293
|
+
...(ended && last?.text && !last.mid
|
|
286
294
|
? last.apiError ? { body: last.text, error: true } : { body: last.text, revision: finalId(e.id, e.channel, last.text) }
|
|
287
295
|
: {}) };
|
|
288
296
|
}
|
|
@@ -296,6 +304,18 @@ const main = async () => {
|
|
|
296
304
|
format: (d) => ({ ...d, chunks: d.chunks.map((c) => responseTracker.freeze(d.channel.replace(/^(user|chat|group):/, ""), c)) }),
|
|
297
305
|
send: async (d, n) => {
|
|
298
306
|
await bridge.settleFinalProgress(d.id);
|
|
307
|
+
const allowed = () => {
|
|
308
|
+
const source = d.eventId ? events.authorityOf(d.eventId) : undefined;
|
|
309
|
+
if (!source)
|
|
310
|
+
return !d.eventId || d.purpose === "task";
|
|
311
|
+
const row = source.serves ? router.get(source.serves) : undefined;
|
|
312
|
+
const cancellation = d.purpose === "cancellation" && source.canceled && source.unexposedHuman;
|
|
313
|
+
return source.target === d.target && !source.interjected && !handoffFinalSuppressed(source) && ownsReplyEvent(source)
|
|
314
|
+
&& (cancellation || !source.canceled && row?.outcome?.status !== "canceled" && row?.status !== "held")
|
|
315
|
+
&& (d.purpose !== "progress" || !!row && ["open", "need", "error"].includes(row.status));
|
|
316
|
+
};
|
|
317
|
+
if (!allowed())
|
|
318
|
+
return { failed: "delivery authority withdrawn before transport" };
|
|
299
319
|
const chat = d.channel.replace(/^(user|chat|group):/, "");
|
|
300
320
|
if (wx.owns(chat)) {
|
|
301
321
|
const result = wx.sendFinal(chat, wxMarkdown(d.chunks[n]), `${d.id}:${n}`);
|
|
@@ -304,7 +324,7 @@ const main = async () => {
|
|
|
304
324
|
failed: result.state === "failed" ? result.reason ?? "Weixin final rejected" : undefined };
|
|
305
325
|
}
|
|
306
326
|
const stream = d.stream && n === 0 && Date.now() < d.stream.expires ? d.stream : undefined;
|
|
307
|
-
const ack = await responseTracker.sendFrozen(chat, d.chunks[n], stream, events.running);
|
|
327
|
+
const ack = await responseTracker.sendFrozen(chat, d.chunks[n], stream, () => events.running() && allowed());
|
|
308
328
|
return ack && typeof ack === "object" && "errcode" in ack && ack.errcode === 0 ? { ack: JSON.stringify(ack) }
|
|
309
329
|
: { ack: JSON.stringify(ack), uncertain: "final transport returned no explicit success ACK" };
|
|
310
330
|
},
|
|
@@ -1237,13 +1257,6 @@ const main = async () => {
|
|
|
1237
1257
|
...(p.job ? { job: p.job } : {}), ...(p.asker ? { asker: p.asker } : {}), ...(p.after?.length ? { after: p.after } : {}),
|
|
1238
1258
|
});
|
|
1239
1259
|
};
|
|
1240
|
-
const ownsReplyEvent = (e) => {
|
|
1241
|
-
const row = e.serves ? router.get(e.serves) : undefined;
|
|
1242
|
-
if (row && row.owner !== e.target)
|
|
1243
|
-
return false;
|
|
1244
|
-
const latest = e.serves ? events.latestTransfer(e.serves) : undefined;
|
|
1245
|
-
return !latest || latest.id === e.id || latest.at < e.at;
|
|
1246
|
-
};
|
|
1247
1260
|
// Acceptance projects ownership only after the input is durable. A rejected
|
|
1248
1261
|
// acceptance retains the human's route and bubble for the original owner.
|
|
1249
1262
|
const acceptPeerDispatch = (input, transfer = false) => {
|
|
@@ -1335,14 +1348,15 @@ const main = async () => {
|
|
|
1335
1348
|
return;
|
|
1336
1349
|
// 挂起账 / 工单账本跟着定论走 (§9.5 / §9.6): 每一份定论都从这儿过 (onFinal 的 answer、
|
|
1337
1350
|
// 失败级联的 canceled、boot 的重投), 所以放这里而不是 commitRoute。收给群的根行不进账。
|
|
1338
|
-
|
|
1351
|
+
const noticeOnly = (out.status === "need" || out.status === "error") && !!row.outcome;
|
|
1352
|
+
if (row.receiver.kind === "wizard" && !noticeOnly) {
|
|
1339
1353
|
ledger.outcome(row.receiver.to, event, out.status, parseClosing(out.body).text || out.body);
|
|
1340
1354
|
// 带 workflow 追踪的那件活: 执行态/正文/摘要跟着定论走 (计划节点据此推进) ——
|
|
1341
1355
|
// 这一份原来只挂在旧回执 watcher 的 onOutcome 上, T10b 随它一起在这里重建。
|
|
1342
1356
|
workflow.outcome(row.receiver.to, event, out.status, out.body);
|
|
1343
1357
|
advancePlans();
|
|
1344
|
-
//
|
|
1345
|
-
if (row.job && isTerminal(out.status)
|
|
1358
|
+
// 追述更新最新结果, 首次落定计数由工单保持。
|
|
1359
|
+
if (row.job && isTerminal(out.status))
|
|
1346
1360
|
jobs.settle(row.job, row.owner, out.status, parseClosing(out.body).artifacts, { turn: event });
|
|
1347
1361
|
}
|
|
1348
1362
|
postRoute(row, out, router.escalate(event, (k) => !!wizards.get(k)), `rcpt:${event}:${out.rev}`);
|
|
@@ -1360,8 +1374,9 @@ const main = async () => {
|
|
|
1360
1374
|
// Persist it through the same final outbox before handing the route off.
|
|
1361
1375
|
const transferred = events.latestTransfer(event);
|
|
1362
1376
|
if (out.status !== "done" && transferred?.target === row.owner)
|
|
1363
|
-
m.deliverTransferredFinal(transferred, out.body);
|
|
1364
|
-
|
|
1377
|
+
m.deliverTransferredFinal(transferred, out.body, out.status);
|
|
1378
|
+
if (isTerminal(out.status) && row.outcome?.rev === out.rev)
|
|
1379
|
+
router.commit(ops.hand({ event, inbox: `chat:${receiver.message}:${out.rev}` }));
|
|
1365
1380
|
return;
|
|
1366
1381
|
}
|
|
1367
1382
|
// 回执那一轮服务的就是收方自己欠的那一笔 (`receiver.event`) —— 它的终句又一次落到那一笔上,
|
|
@@ -1381,6 +1396,13 @@ const main = async () => {
|
|
|
1381
1396
|
: undefined;
|
|
1382
1397
|
const job = row.job ? jobs.get(row.job) : undefined;
|
|
1383
1398
|
const tally = row.job ? jobs.tally(row.job) : undefined;
|
|
1399
|
+
const envelope = renderReceiptEnvelope(displayName(row.owner), channel ? chatNameOf(cfg, channel) : undefined, row.job ? { job: row.job, done: tally?.done ?? 0, total: tally?.total ?? 0, closed: job?.status !== "open", ...(job?.kind === "req" ? { req: true } : {}) } : undefined, out.status, event, { ...(row.asker ? { asker: row.asker } : {}),
|
|
1400
|
+
...(served?.kind === "wizard" ? { up: true } : {}),
|
|
1401
|
+
...(audienceName(channel, row.asker) ? { audience: audienceName(channel, row.asker) } : {}),
|
|
1402
|
+
...(via.length ? { via: via.join(", ") } : {}),
|
|
1403
|
+
...(late ? { late: true } : {}),
|
|
1404
|
+
...(retellTo ? { retellTo } : {}),
|
|
1405
|
+
...(row.late ? { retold: true } : {}) });
|
|
1384
1406
|
events.accept({
|
|
1385
1407
|
id: inbox, identity: JSON.stringify([to, event, out.status, out.rev]),
|
|
1386
1408
|
target: to, speaker: row.owner, channel, serves,
|
|
@@ -1388,26 +1410,35 @@ const main = async () => {
|
|
|
1388
1410
|
priority: "normal", deadline: Date.now() + 12 * 3600_000,
|
|
1389
1411
|
// 行首 RESULT:/NEED: 只是判定记号, 语义已由信封的 status 携带 —— 投递时剥掉
|
|
1390
1412
|
// (ARTIFACT 行保留: 路径对收方 wizard 有实际用途)。
|
|
1391
|
-
payload: { text: clipMiddle(stripClosingMark(out.body)),
|
|
1413
|
+
payload: { text: clipMiddle(stripClosingMark(out.body)), receipts: [{ event, version: out.rev, status: out.status,
|
|
1414
|
+
receiver, body: out.body, speaker: row.owner, envelope,
|
|
1415
|
+
source: row.reports?.[out.rev]?.source
|
|
1416
|
+
?? (row.outcome?.rev === out.rev ? row.outcome.report?.source : row.notice?.rev === out.rev ? row.notice.report?.source : undefined) }], peer: {
|
|
1392
1417
|
// 回执不是一件新活: `job` 留空 (否则 jobs.account 会把它当成工单的第二份派活数一遍)。
|
|
1393
1418
|
from: { kind: "peer", from: row.owner, receipt: true, status: out.status, event },
|
|
1394
1419
|
job: "", kind: "fyi",
|
|
1395
|
-
envelope
|
|
1396
|
-
...(served?.kind === "wizard" ? { up: true } : {}),
|
|
1397
|
-
...(audienceName(channel, row.asker) ? { audience: audienceName(channel, row.asker) } : {}),
|
|
1398
|
-
...(via.length ? { via: via.join(", ") } : {}),
|
|
1399
|
-
...(late ? { late: true } : {}),
|
|
1400
|
-
...(retellTo ? { retellTo } : {}),
|
|
1401
|
-
...(row.late ? { retold: true } : {}) }),
|
|
1420
|
+
envelope,
|
|
1402
1421
|
} },
|
|
1403
1422
|
}, retire);
|
|
1404
|
-
|
|
1423
|
+
if (isTerminal(out.status) && row.outcome?.rev === out.rev)
|
|
1424
|
+
router.commit(ops.hand({ event, inbox }));
|
|
1405
1425
|
};
|
|
1406
1426
|
/** 收件箱里那份回执投不到了 (`gone`: 收方没有绑定, 按 store 也恢复不了) —— 投进去时它还在
|
|
1407
1427
|
* 注册表上, 升级没跳过它。现在把它当不在, 沿 receiver 边重走一次升级: 能收的上层 wizard /
|
|
1408
1428
|
* 群根 (Dead Letter) 收下。链上无处可升 (根行的收方自己没了): canceled 是收掉它的那一方自己
|
|
1409
1429
|
* 发起的, 没人在等, 不再投; 别的定论退到收方的家群 (Dead Letter)。账本不再记第二遍。 */
|
|
1410
1430
|
const rerouteGone = (e) => {
|
|
1431
|
+
const facts = receiptFacts(e);
|
|
1432
|
+
if (facts.length <= 1) {
|
|
1433
|
+
rerouteReceipt(e, e.id);
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
facts.forEach((f, i) => rerouteReceipt({ ...e, id: `rcpt:${f.event}:${f.version}`,
|
|
1437
|
+
speaker: f.speaker, payload: { ...e.payload, text: f.body, receipts: [f],
|
|
1438
|
+
peer: { ...e.payload.peer, from: { kind: "peer", from: f.speaker, event: f.event,
|
|
1439
|
+
receipt: true, status: f.status }, envelope: f.envelope } } }, i === facts.length - 1 ? e.id : undefined));
|
|
1440
|
+
};
|
|
1441
|
+
const rerouteReceipt = (e, retire) => {
|
|
1411
1442
|
const from = e.payload.peer?.from;
|
|
1412
1443
|
const event = from?.receipt ? from.event : undefined;
|
|
1413
1444
|
const row = event ? router.get(event) : undefined;
|
|
@@ -1429,7 +1460,7 @@ const main = async () => {
|
|
|
1429
1460
|
// Replaying a torn reroute must target the same durable inbox entry.
|
|
1430
1461
|
const inbox = `${e.id}~${finalId(e.id, JSON.stringify(esc?.receiver ?? row.receiver), rev)}`;
|
|
1431
1462
|
log.info({ id: e.id, target: e.target, event, to: esc?.receiver ?? row.receiver, inbox }, "receipt reroute: 收方已不在, 改投");
|
|
1432
|
-
postRoute(row, { status: from.status, body: e.payload.text, rev }, esc, inbox,
|
|
1463
|
+
postRoute(row, { status: from.status, body: e.payload.text, rev }, esc, inbox, retire);
|
|
1433
1464
|
};
|
|
1434
1465
|
/** 一条路由 op: 先落盘, 再投该投的 (need / error 带正文入箱; answer 按 receiver 投递)。这里
|
|
1435
1466
|
* 只管 `re` (`mark open`) 把那一行拉回在飞 —— 账本与工单跟着 deliverRoute 走。 */
|
|
@@ -1471,7 +1502,7 @@ const main = async () => {
|
|
|
1471
1502
|
};
|
|
1472
1503
|
/** 一轮的终句落到它服务的那一行上 (定论 / 中途态), 验收不合规格就打回续问。 */
|
|
1473
1504
|
const settleFinal = (e, verdict) => {
|
|
1474
|
-
const plans = router.onFinal(e.target, e.serves, verdict);
|
|
1505
|
+
const plans = router.onFinal(e.target, e.serves, { ...verdict, source: reportSource(e) });
|
|
1475
1506
|
const answer = plans.flatMap((op) => (op.op === "answer" && op.outcome.status === "done" ? [op] : []))[0];
|
|
1476
1507
|
const row = answer ? router.get(answer.event) : undefined;
|
|
1477
1508
|
const why = answer && row ? acceptanceOf(row, answer.outcome.body) : undefined;
|
|
@@ -1537,9 +1568,12 @@ const main = async () => {
|
|
|
1537
1568
|
* 先留这个调用位。 */
|
|
1538
1569
|
const releaseHeld = (_row) => { };
|
|
1539
1570
|
/** §4.5 boot: fold 之后 `answered` 还没 `hand` 的行重投一次 —— rcpt 的 id 幂等, 重放不双投。 */
|
|
1540
|
-
const replayAnswered = () => router.rows()
|
|
1541
|
-
|
|
1542
|
-
|
|
1571
|
+
const replayAnswered = () => router.rows().forEach((r) => {
|
|
1572
|
+
if (r.status === "answered" && r.outcome)
|
|
1573
|
+
deliverRoute(r.event, r.outcome);
|
|
1574
|
+
if (r.notice && r.outcome?.status !== "canceled")
|
|
1575
|
+
deliverRoute(r.event, r.notice);
|
|
1576
|
+
});
|
|
1543
1577
|
const currentAsk = (target) => {
|
|
1544
1578
|
const env = openingAsk(target);
|
|
1545
1579
|
if (!env)
|
|
@@ -1801,8 +1835,43 @@ const main = async () => {
|
|
|
1801
1835
|
return;
|
|
1802
1836
|
}
|
|
1803
1837
|
const asker = askerOf(self);
|
|
1838
|
+
const activeId = openingOfSelf(self)?.env?.eventId;
|
|
1839
|
+
const active = activeId ? events.get(activeId) : undefined;
|
|
1840
|
+
const receipt = !!active?.payload.peer?.from.receipt;
|
|
1841
|
+
const leading = receipt ? /^(RESULT|Result|result|结论|NEED)[ \t]*[::]/.exec(content) : null;
|
|
1842
|
+
const closing = receipt ? parseClosing(content).kind : "none";
|
|
1843
|
+
if (receipt && active) {
|
|
1844
|
+
const row = active.serves ? router.get(active.serves) : undefined;
|
|
1845
|
+
if (active.canceled || active.interjected || handoffFinalSuppressed(active) || !ownsReplyEvent(active) || row?.outcome?.status === "canceled") {
|
|
1846
|
+
json(res, 409, { ok: false, reason: "这笔任务已取消或归属已变更,回执报告不再公开。" });
|
|
1847
|
+
return;
|
|
1848
|
+
}
|
|
1849
|
+
if (closing === "none" && (!row || !["open", "need", "error"].includes(row.status))) {
|
|
1850
|
+
json(res, 409, { ok: false, reason: "这笔任务已收口或无在途任务,回执确认不公开;新结果用 RESULT:,新问题用 NEED:。" });
|
|
1851
|
+
return;
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
const shown = receipt && (leading || closing !== "none") ? stripForChat(content) : content;
|
|
1855
|
+
if (!shown.trim()) {
|
|
1856
|
+
json(res, 400, { ok: false, reason: "markdown body required" });
|
|
1857
|
+
return;
|
|
1858
|
+
}
|
|
1859
|
+
const deliveries = [];
|
|
1804
1860
|
for (const dest of dests) {
|
|
1805
|
-
|
|
1861
|
+
if (active?.payload.peer?.from.receipt) {
|
|
1862
|
+
const purpose = closing === "none" ? "progress" : "report";
|
|
1863
|
+
const reportKind = closing === "need" ? "need" : "done";
|
|
1864
|
+
const coverage = purpose === "report" ? receiptCoverage(active, dest, closing === "need", content) : undefined;
|
|
1865
|
+
const revision = finalId(active.id, dest, content);
|
|
1866
|
+
const id = events.delivery({ id: finalId(active.id, dest, revision), eventId: active.id, target: self,
|
|
1867
|
+
turn: active.turn ?? active.id, channel: dest, revision, body: shown, purpose, reportKind,
|
|
1868
|
+
chunks: m.formatFinal(self, active.turn ?? active.id, dest, shown), ...(coverage ? { coverage } : {}) });
|
|
1869
|
+
const claims = events.deliveryClaims(coverage);
|
|
1870
|
+
deliveries.push({ channel: dest, id, states: claims.length ? claims.flatMap((c) => c.states) : events.deliveryStatus(id),
|
|
1871
|
+
...(claims.length > 1 ? { claims } : {}) });
|
|
1872
|
+
}
|
|
1873
|
+
else
|
|
1874
|
+
postAs(self, dest, content);
|
|
1806
1875
|
recordPost({ target: self, channel: dest, body: content, ...(asker ? { asker } : {}) });
|
|
1807
1876
|
}
|
|
1808
1877
|
// 微信群聊: 人超过 ctxTtlSec 没说话时微信会静默丢弃, 消息只能压着等他下次开口 —— 如实告诉调用方, 别回「已发送」。
|
|
@@ -1811,7 +1880,8 @@ const main = async () => {
|
|
|
1811
1880
|
const name = (d) => chatNameOf(cfg, d) || d;
|
|
1812
1881
|
json(res, 200, {
|
|
1813
1882
|
ok: true,
|
|
1814
|
-
sent: dests.filter((d) => !held.includes(d)).map(name),
|
|
1883
|
+
sent: dests.filter((d) => !held.includes(d) && !deliveries.some((x) => x.channel === d)).map(name),
|
|
1884
|
+
...(deliveries.length ? { deliveries, note: "已持久化;相同事实复用既有投递,实际状态见 states,不确定 ACK 不自动重发。" } : {}),
|
|
1815
1885
|
...(held.length ? { held: held.map(name), note: `微信群聊 ${held.map(name).join("、")} 那头超过 ${cfg.weixin.ctxTtlSec}s 没说话: 微信会静默丢弃主动推送, 这条已压着, 对方下次在微信里开口时送达 (定时提醒做不到准点)` } : {}),
|
|
1816
1886
|
});
|
|
1817
1887
|
});
|
|
@@ -2158,7 +2228,7 @@ const main = async () => {
|
|
|
2158
2228
|
guard: (phase, identity) => peerEvents.valid(e) && prereqsOf(e.speaker, p.after ?? []).every((r) => r.state === "done") && (!phase || ((!p.attemptId || workflow.boundary(p.attemptId)) && boundary(phase, identity))),
|
|
2159
2229
|
eventId: e.id,
|
|
2160
2230
|
});
|
|
2161
|
-
const r = !raw.ok && !raw.uncertain &&
|
|
2231
|
+
const r = !raw.ok && !raw.uncertain && raw.retryable === undefined && events.get(e.id)?.exposure === "none" ? { ...raw, retryable: true } : raw;
|
|
2162
2232
|
if (p.attemptId && (r.uncertain || (!r.ok && !r.retryable)))
|
|
2163
2233
|
workflow.dispatched(p.attemptId, false);
|
|
2164
2234
|
return r;
|
|
@@ -2170,12 +2240,21 @@ const main = async () => {
|
|
|
2170
2240
|
// 已落定的那一笔只放追述 (§4.3): 判据与 onFinal 的 retellOps 同源 (router.finalReleased)。
|
|
2171
2241
|
// 调用方可能拿着 result 落定前的快照, 正文按事件库现读。
|
|
2172
2242
|
m.setEventOwnerGate(ownsReplyEvent);
|
|
2243
|
+
m.setEventProgressGate((e) => {
|
|
2244
|
+
if (handoffFinalSuppressed(e) || e.quiet || e.canceled || e.interjected || !ownsReplyEvent(e))
|
|
2245
|
+
return false;
|
|
2246
|
+
const row = e.serves ? router.get(e.serves) : undefined;
|
|
2247
|
+
if (!row || row.owner !== e.target || row.status !== "open")
|
|
2248
|
+
return false;
|
|
2249
|
+
const kind = finalKind(router.table(), e.serves, e.result?.body ?? "");
|
|
2250
|
+
return !e.result || kind === "progress" || kind === "wait";
|
|
2251
|
+
});
|
|
2173
2252
|
m.setEventFinalGate((e) => {
|
|
2174
2253
|
if (handoffFinalSuppressed(e))
|
|
2175
2254
|
return false;
|
|
2176
|
-
if (e.canceled || e.interjected || !ownsReplyEvent(e))
|
|
2255
|
+
if (e.quiet || e.canceled || e.interjected || !ownsReplyEvent(e))
|
|
2177
2256
|
return false;
|
|
2178
|
-
return router.finalReleased(e.target, e.serves, (events.get(e.id) ?? e).result?.body ?? "");
|
|
2257
|
+
return router.finalReleased(e.target, e.serves, (events.get(e.id) ?? e).result?.body ?? "", reportSource(events.get(e.id) ?? e));
|
|
2179
2258
|
});
|
|
2180
2259
|
// boot 的路由表侧恢复 (T8): held 重查前置、answered 重投一次; 二者幂等, 与 events.resume
|
|
2181
2260
|
// 重放的注入/回执撞上无害。
|
|
@@ -2192,7 +2271,8 @@ const main = async () => {
|
|
|
2192
2271
|
// 自己就是计数器, 不另落一份。一次 tick 扫一遍收件箱就够 (这一轮不会再产新事件)。
|
|
2193
2272
|
const fruitless = new Map();
|
|
2194
2273
|
events.status().events.forEach((e) => {
|
|
2195
|
-
if (!e.serves || e.result || e.canceled || e.
|
|
2274
|
+
if (!e.serves || e.result || e.canceled || e.payload.peer?.from.receipt
|
|
2275
|
+
|| !(e.execution === "complete" || e.execution === "uncertain" && e.exposure === "consumed" && e.turnClosedAt))
|
|
2196
2276
|
return;
|
|
2197
2277
|
fruitless.set(e.serves, (fruitless.get(e.serves) ?? 0) + 1);
|
|
2198
2278
|
});
|
|
@@ -2425,6 +2505,12 @@ const main = async () => {
|
|
|
2425
2505
|
return { status: 409, body: { ok: false, gated: "cold", reason: gate + (force === undefined ? " (你的 MCP 是旧版、没有 force 参数: 10 分钟内原样再发一次即视为 force)" : "") } };
|
|
2426
2506
|
}
|
|
2427
2507
|
}
|
|
2508
|
+
if (actor?.reuseCheck) {
|
|
2509
|
+
const idle = await m.idleNow(target);
|
|
2510
|
+
const refusal = actor.reuseCheck(target) || (!idle ? "candidate became busy" : "");
|
|
2511
|
+
if (refusal)
|
|
2512
|
+
return { status: 409, body: { ok: false, gated: "reuse", reason: refusal } };
|
|
2513
|
+
}
|
|
2428
2514
|
// All caller-dependent facts are frozen here; workers never reread the caller's current turn.
|
|
2429
2515
|
const at = Date.now();
|
|
2430
2516
|
const deadlineAt = deadlineOf(at, body.deadline);
|
|
@@ -2459,6 +2545,13 @@ const main = async () => {
|
|
|
2459
2545
|
const canceledBeforeDispatch = () => !planAllowed() || (!!planned && !planned.alive()) || !!workflow.byTurn(self, planned ? planned.turn : serves)?.canceled || (!!jobId && jobs.get(jobId)?.status !== "open");
|
|
2460
2546
|
// 「忙」= 这一轮还没结束, 或停在审批上等人 —— 都不是能接新活的时候。
|
|
2461
2547
|
const wasBusy = !(await m.idleNow(target));
|
|
2548
|
+
if (actor?.reuseCheck) {
|
|
2549
|
+
const refusal = actor.reuseCheck(target, serves) || (wasBusy ? "candidate became busy" : "");
|
|
2550
|
+
if (refusal) {
|
|
2551
|
+
dropRow();
|
|
2552
|
+
return { status: 409, body: { ok: false, gated: "reuse", reason: refusal } };
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2462
2555
|
// urgent 只打断你自己派的那一轮 (或它自己的回执轮): 手上若是别的 wizard / 人 / 定时任务的活,
|
|
2463
2556
|
// 打断就误伤了那件 —— 降成 normal, 等它这一轮做完再投。
|
|
2464
2557
|
const others = asked === "urgent" && wasBusy ? await othersTurn(target, self) : undefined;
|
|
@@ -2645,23 +2738,33 @@ const main = async () => {
|
|
|
2645
2738
|
const results = uniq.map((h, i) => {
|
|
2646
2739
|
const wr = wrs[i];
|
|
2647
2740
|
// 同一对并发多笔是允许的 (§4.1): 先取**还在排队**的那份 (那份还没进过你的会话); 都取过了就
|
|
2648
|
-
//
|
|
2741
|
+
// 报最新那件的投递状态 —— 绝不把已经 paste 过的那份当成新答案交回来。
|
|
2649
2742
|
const mine = router.settledTo(self).filter((r) => r.owner === h.target).reverse() // byAt → 新在前
|
|
2650
|
-
.map((row) => {
|
|
2651
|
-
|
|
2743
|
+
.map((row) => {
|
|
2744
|
+
const box = receiptOf(row);
|
|
2745
|
+
return { row, box,
|
|
2746
|
+
execution: events.executionOf(box.id), exposure: events.exposureOf(box.id) };
|
|
2747
|
+
});
|
|
2748
|
+
const pick = mine.find((x) => x.execution === "queued" && x.exposure === "none") ?? mine[0];
|
|
2652
2749
|
if (!pick)
|
|
2653
2750
|
return { name: h.address, idle: wr.idle, ...(wr.reason ? { reason: wr.reason } : {}),
|
|
2654
2751
|
...(wr.idle ? { stale: true, reason: "它停下了, 但路由表上没有交回给你的定论 —— 多半停在一个等人点的弹窗上, 或那句话没被接住; peek_peer 看一眼" } : {}) };
|
|
2655
|
-
//
|
|
2656
|
-
|
|
2752
|
+
// Only an unexposed queued input can be claimed. Execution state alone
|
|
2753
|
+
// proves neither consumption nor non-delivery, including old tombstones.
|
|
2754
|
+
if (pick.execution !== "queued" || pick.exposure !== "none")
|
|
2657
2755
|
return { name: h.address, idle: wr.idle,
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
:
|
|
2756
|
+
eventId: pick.box.id, delivered: pick.exposure === "consumed", exposure: pick.exposure ?? "unknown",
|
|
2757
|
+
delivery: pick.exposure === "consumed" ? "consumed"
|
|
2758
|
+
: pick.execution === "uncertain" || !pick.exposure || pick.exposure === "possible" ? "uncertain" : pick.execution,
|
|
2759
|
+
reason: pick.exposure === "consumed" ? "回执已由会话消费,不重复领取。"
|
|
2760
|
+
: "结果已保存,尚未确认回执消费;按 eventId 对账,不自动重投或再次领取。" };
|
|
2661
2761
|
// 取走就撤单: 这一笔在收件箱里排着的**全部**回执一起作废 (need 之后又答完时排着两份, 取走
|
|
2662
2762
|
// 的那一份就是新结论, 旧的那份不再 paste 第二遍); `hand` 确认已交付 (已 handed 的是 no-op)。
|
|
2663
|
-
if (pick.
|
|
2664
|
-
|
|
2763
|
+
if (pick.execution === "queued") {
|
|
2764
|
+
const claim = events.claimReceipt(pick.box.id);
|
|
2765
|
+
if (!claim.ok)
|
|
2766
|
+
return { name: h.address, idle: wr.idle, eventId: pick.box.id,
|
|
2767
|
+
delivery: "pending-reconciliation", reason: claim.reason };
|
|
2665
2768
|
router.commit(ops.hand({ event: pick.row.event, inbox: pick.box.id }));
|
|
2666
2769
|
}
|
|
2667
2770
|
const closing = parseClosing(pick.box.text);
|
|
@@ -3431,24 +3534,36 @@ const main = async () => {
|
|
|
3431
3534
|
const roster = await rosterOf(self);
|
|
3432
3535
|
const openJobs = jobs.all();
|
|
3433
3536
|
const inflight = routeInFlight();
|
|
3537
|
+
const queued = events.status().events.filter((e) => e.execution === "queued" || e.execution === "dispatching");
|
|
3434
3538
|
return roster
|
|
3435
3539
|
.filter((r) => !r.self && keep(r))
|
|
3436
3540
|
.map((r) => {
|
|
3437
3541
|
const live = r.jsonlPath ? r.jsonlPath : "";
|
|
3438
3542
|
const ttl = live ? cacheTtlSec(live) : 0;
|
|
3543
|
+
const materials = live ? contextMaterials(live) : [];
|
|
3439
3544
|
return {
|
|
3440
3545
|
...r,
|
|
3441
3546
|
lead: wizards.get(r.target)?.lead === true,
|
|
3442
|
-
duties: routeDutiesOf(openJobs, r.target, inflight),
|
|
3547
|
+
duties: [...routeDutiesOf(openJobs, r.target, inflight), ...queued.filter((e) => e.target === r.target).map((e) => `${e.id} ${e.execution}`)],
|
|
3443
3548
|
contextTokens: live ? lastContextTokens(live) : 0,
|
|
3444
3549
|
...(ttl ? { cacheTtlMs: ttl * 1000 } : {}),
|
|
3445
3550
|
...(live ? { cacheAt: cacheTouchMs(live) } : {}),
|
|
3446
|
-
|
|
3551
|
+
sessionId: m.sessionInfo(r.target)?.sessionId,
|
|
3552
|
+
birthToken: wizards.get(r.target)?.birthToken,
|
|
3553
|
+
model: live ? lastModel(live) || r.model : r.model,
|
|
3554
|
+
materials,
|
|
3555
|
+
files: [...new Set(materials.filter((m) => m.confirmed && m.kind === "read").map((m) => m.path))],
|
|
3447
3556
|
asks: live ? talkTurns(live, 12).filter((t) => t.role === "user").map((t) => t.text) : [],
|
|
3448
3557
|
summary: live ? lastExchange(live, 80) : "",
|
|
3449
3558
|
};
|
|
3450
3559
|
});
|
|
3451
3560
|
};
|
|
3561
|
+
const autoRouteRowsOf = async (self, tier) => {
|
|
3562
|
+
const pick = pickTier(cfg, tier, {}, false);
|
|
3563
|
+
const budgetModel = (tier === "mini" || tier === "light") && pick.ok ? pick.model : undefined;
|
|
3564
|
+
return (await routeRowsOf(self, (r) => r.alive && !!tagOfKey(r.target)))
|
|
3565
|
+
.map((r) => ({ ...r, ...(budgetModel ? { budgetModel } : {}) }));
|
|
3566
|
+
};
|
|
3452
3567
|
http.register("POST /wizard/route", async (req, res) => {
|
|
3453
3568
|
const { self, body } = await readPeerBody(req);
|
|
3454
3569
|
if (!self) {
|
|
@@ -3462,8 +3577,18 @@ const main = async () => {
|
|
|
3462
3577
|
return;
|
|
3463
3578
|
}
|
|
3464
3579
|
const limit = Math.min(Math.max(Number(b.limit ?? 5) || 5, 1), 20);
|
|
3465
|
-
const
|
|
3466
|
-
|
|
3580
|
+
const tierArg = (b.tier ?? "").trim();
|
|
3581
|
+
if (tierArg && !TIERS.includes(tierArg)) {
|
|
3582
|
+
json(res, 400, { ok: false, reason: `unknown tier: ${tierArg}` });
|
|
3583
|
+
return;
|
|
3584
|
+
}
|
|
3585
|
+
const tier = (tierArg || tierFor(task, false));
|
|
3586
|
+
const force = b.force === true;
|
|
3587
|
+
const rows = await autoRouteRowsOf(self, tier);
|
|
3588
|
+
const now = Date.now();
|
|
3589
|
+
const cands = rankCandidates(task, (b.cwd ?? "").trim() || m.getCwd(self).runningCwd, rows, { now, ttlMs: CACHE_TTL_FALLBACK_SEC * 1000 });
|
|
3590
|
+
const decision = decide(task, cands, now, CACHE_TTL_FALLBACK_SEC * 1000, force, tier);
|
|
3591
|
+
json(res, 200, { ok: true, decision: decision.kind, reason: decision.why, tier, diagnostics: routeDiagnostics(task, cands, now, CACHE_TTL_FALLBACK_SEC * 1000, force, tier), text: `${cands.length > limit ? `共 ${cands.length} 个候选, 展示 ${limit} 个; diagnostics 保留全量。\n` : ""}${renderCandidates(task, cands.slice(0, limit), now, CACHE_TTL_FALLBACK_SEC * 1000, homedir(), { tier, force })}` });
|
|
3467
3592
|
});
|
|
3468
3593
|
// 派活收成一步 (见 route.decide): 查候选 → 过成本门控 → 转给已有的或按档白板 spawn →
|
|
3469
3594
|
// tell_peer 投出去, 回决定与理由。默认决定由守护进程算; 管家不同意就 `to` 点名、
|
|
@@ -3505,22 +3630,45 @@ const main = async () => {
|
|
|
3505
3630
|
const ttlMs = CACHE_TTL_FALLBACK_SEC * 1000;
|
|
3506
3631
|
// 占用保护由 decide 统一判断, 留在候选表里说明为什么不能自动复用。
|
|
3507
3632
|
// 显式 to 保留人为选定; force 只影响成本门, 不绕 lead/工单/连续性保护。
|
|
3508
|
-
const pool = to || b.spawn || lead ? [] : await routeRowsOf(self, (r) => r.alive && !!tagOfKey(r.target));
|
|
3509
|
-
if (!controlValid(self, dispatchOrigin.authority)) {
|
|
3510
|
-
json(res, 409, { ok: false, reason: "dispatch source turn/control changed while routing; original task remains with source" });
|
|
3511
|
-
return;
|
|
3512
|
-
}
|
|
3513
3633
|
const tierArg = (b.tier ?? "").toString().trim();
|
|
3514
3634
|
if (tierArg && !TIERS.includes(tierArg)) {
|
|
3515
3635
|
json(res, 400, { ok: false, reason: `unknown tier: ${tierArg}` });
|
|
3516
3636
|
return;
|
|
3517
3637
|
}
|
|
3518
3638
|
const taskTier = (tierArg || tierFor(task, lead));
|
|
3639
|
+
const pool = to || b.spawn || lead ? [] : await autoRouteRowsOf(self, taskTier);
|
|
3640
|
+
if (!controlValid(self, dispatchOrigin.authority)) {
|
|
3641
|
+
json(res, 409, { ok: false, reason: "dispatch source turn/control changed while routing; original task remains with source" });
|
|
3642
|
+
return;
|
|
3643
|
+
}
|
|
3519
3644
|
const cands = rankCandidates(task, m.getCwd(self).runningCwd, pool, { now, ttlMs });
|
|
3520
3645
|
let d = to ? { kind: "existing", why: `你点名了 ${to}` }
|
|
3521
3646
|
: b.spawn ? { kind: "spawn", why: "你要了新的" }
|
|
3522
3647
|
: lead ? { kind: "spawn", why: "复杂活: 白板起一个 hard 档的 lead, 由它组队" }
|
|
3523
3648
|
: decide(task, cands, now, ttlMs, force, taskTier);
|
|
3649
|
+
const diagnostics = { ...routeDiagnostics(task, cands, now, ttlMs, force, taskTier), deliveryFallback: "" };
|
|
3650
|
+
if (!to && d.kind === "existing" && d.row) {
|
|
3651
|
+
const selected = d.row;
|
|
3652
|
+
dispatchOrigin.reuseCheck = (target, ownEvent) => {
|
|
3653
|
+
const rec = wizards.get(target), info = m.sessionInfo(target);
|
|
3654
|
+
if (target !== selected.target || !selected.sessionId || info?.sessionId !== selected.sessionId
|
|
3655
|
+
|| rec?.name !== selected.name || rec?.birthToken !== selected.birthToken)
|
|
3656
|
+
return "candidate session/name/birthToken changed";
|
|
3657
|
+
if (selected.budgetModel) {
|
|
3658
|
+
const model = (info?.jsonlPath ? lastModel(info.jsonlPath) : "") || info?.model;
|
|
3659
|
+
if (model?.trim().toLowerCase() !== selected.budgetModel.trim().toLowerCase())
|
|
3660
|
+
return "candidate model no longer fits light task budget";
|
|
3661
|
+
}
|
|
3662
|
+
if (rec?.lead)
|
|
3663
|
+
return "candidate became lead";
|
|
3664
|
+
const duties = [
|
|
3665
|
+
...routeDutiesOf(jobs.all(), target, routeInFlight().filter((x) => x.turn !== ownEvent)),
|
|
3666
|
+
...events.status().events.filter((e) => e.target === target && (e.execution === "queued" || e.execution === "dispatching"))
|
|
3667
|
+
.map((e) => `${e.id} ${e.execution}`),
|
|
3668
|
+
];
|
|
3669
|
+
return duties.length ? `candidate acquired duties: ${duties.join(" · ")}` : "";
|
|
3670
|
+
};
|
|
3671
|
+
}
|
|
3524
3672
|
// 复杂活 (lead:true) 是一件「需求」: 先开需求根单 (账本, 不占 pane), 这一句带它派下去, lead 记成根单成员。
|
|
3525
3673
|
// 小活不开单。dryRun 不落账, 所以放在它后面开 (见下)。
|
|
3526
3674
|
const criteria = (b.criteria ?? "").toString().trim();
|
|
@@ -3532,9 +3680,9 @@ const main = async () => {
|
|
|
3532
3680
|
...(b.deadline !== undefined ? { deadline: b.deadline } : lead ? { deadline: 43200 } : {}),
|
|
3533
3681
|
...(rootId ? { job: rootId } : {}),
|
|
3534
3682
|
}, extra, undefined, dispatchOrigin);
|
|
3535
|
-
const shown = cands.
|
|
3683
|
+
const shown = cands.map((e) => `.${e.row.address || e.row.name}${e.row.busy ? " 忙" : ""}`);
|
|
3536
3684
|
if (b.dryRun) {
|
|
3537
|
-
json(res, 200, { ok: true, dryRun: true, decision: d.kind, reason: d.why, ...(d.kind === "existing" ? { name: to || (d.kind === "existing" && d.row ? d.row.address || d.row.name : "") } : { tier: taskTier }), ...(shown.length ? { candidates: shown } : {}) });
|
|
3685
|
+
json(res, 200, { ok: true, dryRun: true, diagnostics, decision: d.kind, reason: d.why, ...(d.kind === "existing" ? { name: to || (d.kind === "existing" && d.row ? d.row.address || d.row.name : "") } : { tier: taskTier }), ...(shown.length ? { candidates: shown } : {}) });
|
|
3538
3686
|
return;
|
|
3539
3687
|
}
|
|
3540
3688
|
if (lead) {
|
|
@@ -3548,8 +3696,17 @@ const main = async () => {
|
|
|
3548
3696
|
const settleRoot = (ok) => { if (rootId && !ok)
|
|
3549
3697
|
jobs.close(rootId, "派活没投成"); };
|
|
3550
3698
|
const withRoot = (body) => rootId
|
|
3551
|
-
? { ...body, job: rootId, hint: "需求根单已开, 对人**别提单号**。lead 交付后根单仍开着, 等人验收: 人认可 → close_job(job) 归档; 人说不对 → tell_peer({name, re, job}) 给 lead 返工 (同一张单, 不开新单); 人说不要了 → close_job({job, as:\"cancel\"}); 先放着 → close_job({job, as:\"shelve\"}) (停提醒, 不关单)。" }
|
|
3552
|
-
: body;
|
|
3699
|
+
? { ...body, diagnostics, job: rootId, hint: "需求根单已开, 对人**别提单号**。lead 交付后根单仍开着, 等人验收: 人认可 → close_job(job) 归档; 人说不对 → tell_peer({name, re, job}) 给 lead 返工 (同一张单, 不开新单); 人说不要了 → close_job({job, as:\"cancel\"}); 先放着 → close_job({job, as:\"shelve\"}) (停提醒, 不关单)。" }
|
|
3700
|
+
: { ...body, diagnostics };
|
|
3701
|
+
if (d.kind === "existing" && !to && d.row?.target) {
|
|
3702
|
+
const idle = await m.idleNow(d.row.target);
|
|
3703
|
+
const refusal = dispatchOrigin.reuseCheck?.(d.row.target) || (!idle ? "candidate became busy" : "");
|
|
3704
|
+
if (refusal) {
|
|
3705
|
+
diagnostics.deliveryFallback = refusal;
|
|
3706
|
+
delete dispatchOrigin.reuseCheck;
|
|
3707
|
+
d = { kind: "spawn", why: `${d.why}; 投递前复核退回 (${refusal}), 白板 spawn` };
|
|
3708
|
+
}
|
|
3709
|
+
}
|
|
3553
3710
|
if (d.kind === "existing") {
|
|
3554
3711
|
const name = to || (d.kind === "existing" && d.row ? d.row.address || d.row.name : "");
|
|
3555
3712
|
const r = resolvePeer(self, name);
|
|
@@ -3566,12 +3723,14 @@ const main = async () => {
|
|
|
3566
3723
|
if (rootId && t.status === 200)
|
|
3567
3724
|
jobs.attach(rootId, { target: r.target, task: "", spawned: false, role: "lead" });
|
|
3568
3725
|
// 自动选中的被冷门控 (decide 用名册的活动时刻, 门控用 transcript 的, 口径差一点) 或方向闸 (它归别人管 / 是借来的专家) 退回: 改走 spawn; 点名的不改。
|
|
3569
|
-
if (!(!to && (t.body.gated === "cold" || t.body.gated === "direction"))) {
|
|
3726
|
+
if (!(!to && (t.body.gated === "cold" || t.body.gated === "direction" || t.body.gated === "reuse"))) {
|
|
3570
3727
|
settleRoot(t.status === 200);
|
|
3571
3728
|
json(res, t.status, withRoot({ ...t.body, decision: "existing", reason: d.why, ...(shown.length ? { candidates: shown } : {}) }));
|
|
3572
3729
|
return;
|
|
3573
3730
|
}
|
|
3574
|
-
|
|
3731
|
+
diagnostics.deliveryFallback = String(t.body.reason);
|
|
3732
|
+
delete dispatchOrigin.reuseCheck;
|
|
3733
|
+
d = { kind: "spawn", why: `${d.why}; 但它被${t.body.gated === "reuse" ? "投递前复核" : t.body.gated === "direction" ? "方向闸" : "冷门控"}退回 (${String(t.body.reason)}) —— 改为白板 spawn` };
|
|
3575
3734
|
}
|
|
3576
3735
|
const name = (b.name ?? "").toString().trim();
|
|
3577
3736
|
const description = (b.description ?? "").toString().trim() || clipLine(task, 120);
|
|
@@ -3580,7 +3739,7 @@ const main = async () => {
|
|
|
3580
3739
|
const born = await bearAs(self, { inherit: false, name, description, tier, ...(lead ? { lead: true } : {}) }, undefined, dispatchOrigin?.authority);
|
|
3581
3740
|
if (born.status !== 200) {
|
|
3582
3741
|
settleRoot(false);
|
|
3583
|
-
json(res, born.status, { ...born.body, decision: "spawn", reason: d.why });
|
|
3742
|
+
json(res, born.status, { ...born.body, diagnostics, decision: "spawn", reason: d.why });
|
|
3584
3743
|
return;
|
|
3585
3744
|
}
|
|
3586
3745
|
if (!controlValid(self, dispatchOrigin.authority)) {
|
|
@@ -4863,7 +5022,7 @@ const main = async () => {
|
|
|
4863
5022
|
const eventId = `quiet:${t}:${turn}`;
|
|
4864
5023
|
const revision = finalId(eventId, base, text);
|
|
4865
5024
|
events.delivery({ id: finalId(eventId, base, revision), eventId, target: t, turn, channel: base,
|
|
4866
|
-
revision, body: text, chunks: m.formatFinal(t, turn, base, text) });
|
|
5025
|
+
revision, body: text, purpose: "task", chunks: m.formatFinal(t, turn, base, text) });
|
|
4867
5026
|
}
|
|
4868
5027
|
log.info({ target: t, quiet: isQuiet(reply), len: reply.length }, "quiet task: 一轮结束");
|
|
4869
5028
|
quietRelays.drop(turn);
|