wezard 2.6.3 → 2.6.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-plugin/plugin.json +1 -1
- package/dist/daemon/bot-mux.js +4 -0
- package/dist/daemon/bot-mux.js.map +1 -1
- package/dist/daemon/image-input.js +25 -5
- package/dist/daemon/image-input.js.map +1 -1
- package/dist/daemon/inbound.js +3 -2
- package/dist/daemon/inbound.js.map +1 -1
- package/dist/daemon/index.js +312 -109
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/jobs.js +8 -3
- package/dist/daemon/jobs.js.map +1 -1
- package/dist/daemon/last-response.js +22 -2
- package/dist/daemon/last-response.js.map +1 -1
- package/dist/daemon/message-events.js +361 -0
- package/dist/daemon/message-events.js.map +1 -0
- package/dist/daemon/mirror-bridge.js +189 -63
- package/dist/daemon/mirror-bridge.js.map +1 -1
- package/dist/daemon/peers.js +2 -2
- package/dist/daemon/peers.js.map +1 -1
- package/dist/daemon/receipts.js +87 -25
- package/dist/daemon/receipts.js.map +1 -1
- package/dist/daemon/weixin.js +111 -7
- package/dist/daemon/weixin.js.map +1 -1
- package/dist/shared/cli-backends.js.map +1 -1
- package/dist/shared/codex.js +1 -1
- package/dist/shared/codex.js.map +1 -1
- package/dist/shared/detail-store.js.map +1 -1
- package/dist/shared/reminder.js +6 -1
- package/dist/shared/reminder.js.map +1 -1
- package/package.json +1 -1
package/dist/daemon/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { wxMarkdown } from "../shared/wx-text.js";
|
|
1
2
|
// Daemon entry. Resident process — exits only on signal or fatal WS auth failure.
|
|
2
3
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
3
4
|
import { join } from "node:path";
|
|
@@ -12,7 +13,7 @@ import { patchJsonc } from "../shared/config-writer.js";
|
|
|
12
13
|
import { EFFORTS, parseEffort } from "../shared/effort.js";
|
|
13
14
|
import { bindCliBackends, projectDirsFor } from "../shared/cli-backends.js";
|
|
14
15
|
import { bindCodexStateDir } from "../shared/codex.js";
|
|
15
|
-
import { startMux } from "./bot-mux.js";
|
|
16
|
+
import { startMux, pinFrameBot } from "./bot-mux.js";
|
|
16
17
|
import { startWeixin } from "./weixin.js";
|
|
17
18
|
import { installWeixinPort } from "./weixin-port.js";
|
|
18
19
|
import { bindWxCommands, makeOnBound, makeWxCommands, routeSendMedia, wecomBindSink, weixinSendMedia, wxAdmins, wxRoutes } from "./weixin-cmd.js";
|
|
@@ -25,6 +26,7 @@ import { applyHot, cfgSeenBy, patchedTop, pinChatKeys, configGet, fromLegacy, le
|
|
|
25
26
|
import { computeUsage, costOf, fmtCost, fmtTokens, sumTotal } from "./usage.js";
|
|
26
27
|
import { installInboundRouter } from "./inbound.js";
|
|
27
28
|
import { loadMirrorStore } from "./mirror-store.js";
|
|
29
|
+
import { createMessageEvents, messageId, finalId } from "./message-events.js";
|
|
28
30
|
import { startMirror, installMirrorEventListener } from "./mirror-bridge.js";
|
|
29
31
|
import { backendFor, runTmux, setTmuxTimeoutReporter, spawnTmuxClaude } from "./spawn-tmux.js";
|
|
30
32
|
import { bindCardChannel, installApprovalEventListener, makeApproveHandler, requestConfirm } from "./approval.js";
|
|
@@ -54,7 +56,7 @@ import { bindNoticeBox, createNoticeBox, createSeenRing, gcSeen, chatAudience, h
|
|
|
54
56
|
import { createLedger, digest, pendingGc, renderDigest, renderTable, renderAwaiting, renderStalled, renderShelved, ago as agoMs, HEARD_EVERY_MS } from "./pending-items.js";
|
|
55
57
|
import { checkDirection } from "./direction.js";
|
|
56
58
|
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";
|
|
57
|
-
import { cacheTouchMs, cacheTtlSec, clipMiddle, contextFiles, firstStamp, parseClosing, lastContextTokens, lastExchange, lastModel, openingTurn, replyClosedBefore, talkTurns, renderPeerEnvelope, renderReceiptEnvelope, renderTaskEnvelope } from "./peers.js";
|
|
59
|
+
import { cacheTouchMs, cacheTtlSec, clipMiddle, contextFiles, firstStamp, parseClosing, lastContextTokens, lastExchange, lastModel, openingTurn, replyClosedBefore, talkTurns, turnsSince, renderPeerEnvelope, renderReceiptEnvelope, renderTaskEnvelope } from "./peers.js";
|
|
58
60
|
import { keepalivePingSigs } from "../shared/keepalive.js";
|
|
59
61
|
import { expandHome } from "../shared/paths.js";
|
|
60
62
|
import { loadJsonMap } from "../shared/json-map-store.js";
|
|
@@ -156,7 +158,7 @@ const main = async () => {
|
|
|
156
158
|
// last-response tracker enables inbound's `quote` dedup of bot self-replies,
|
|
157
159
|
// and its chat-gate drops header-only pushes (empty messages) daemon-wide.
|
|
158
160
|
// 微信出站不走企微长连接, 不必等它连上。
|
|
159
|
-
installResponseTracker(ws.client, log.child({ mod: "chat-gate" }), (c) => wx.owns(c), (context) => responseFormatPolicy(cfg, context, (target) => chatUrlFor(cfg.daemon, target)));
|
|
161
|
+
const responseTracker = installResponseTracker(ws.client, log.child({ mod: "chat-gate" }), (c) => wx.owns(c), (context) => responseFormatPolicy(cfg, context, (target) => chatUrlFor(cfg.daemon, target)));
|
|
160
162
|
const mirrorStore = loadMirrorStore(cfg.wrc.mirror.attachmentsFile);
|
|
161
163
|
// 定时任务表 —— 每条任务是 ~/.wezard/tasks/<id>.task.mjs 一份可注入代码的配置
|
|
162
164
|
// (见 shared/task-file.ts)。目录是热加载的: wizard 改完文件不用 reload 守护进程。
|
|
@@ -171,7 +173,81 @@ const main = async () => {
|
|
|
171
173
|
log.warn({ err: e.message }, "memory steward task not retired");
|
|
172
174
|
}
|
|
173
175
|
const tasks = await openTaskRegistry(log.child({ mod: "tasks" }), (added, removed) => log.info({ added, removed }, "task files changed"));
|
|
174
|
-
|
|
176
|
+
let peerEvents;
|
|
177
|
+
const events = createMessageEvents(join(cfg.daemon.stateDir, "message-events.json"), {
|
|
178
|
+
resource: (target) => bridge.sessionInfo(target)?.tmuxPane || target,
|
|
179
|
+
transcript: (target) => bridge.sessionInfo(target)?.jsonlPath,
|
|
180
|
+
valid: (e) => e.payload.peer ? !!peerEvents?.valid(e) : true,
|
|
181
|
+
accepted: (e) => { if (e.payload.peer)
|
|
182
|
+
peerEvents?.accepted(e); },
|
|
183
|
+
committed: (e) => { if (e.payload.peer)
|
|
184
|
+
peerEvents?.committed(e); },
|
|
185
|
+
canceled: (e) => {
|
|
186
|
+
if (e.payload.peer)
|
|
187
|
+
peerEvents?.canceled(e);
|
|
188
|
+
else if (e.payload.human && e.exposure === "none") {
|
|
189
|
+
const body = `这条消息未执行:${e.reason ?? "已取消"}。`;
|
|
190
|
+
events.result(e.id, body, finalId(e.id, e.channel, body));
|
|
191
|
+
// A canceled, unexposed input has a terminal response owned by its original channel.
|
|
192
|
+
events.delivery({ id: finalId(e.id, e.channel, body), eventId: e.id, target: e.target, turn: e.turn || e.id,
|
|
193
|
+
channel: e.channel, revision: finalId(e.id, e.channel, body), body, chunks: bridge.formatFinal(e.target, e.turn || e.id, e.channel, body) });
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
ready: async (e) => !handingOff(e.target) && (e.payload.peer ? !!(await peerEvents?.ready(e)) : !bridge.hasMirrorTarget(e.target) || await bridge.idleNow(e.target) || !(await bridge.paneLive(e.target))),
|
|
197
|
+
inject: async (e, boundary) => {
|
|
198
|
+
if (e.payload.peer)
|
|
199
|
+
return peerEvents.inject(e, boundary);
|
|
200
|
+
if (!bridge.hasMirrorTarget(e.target)) {
|
|
201
|
+
const spawned = await bridge.newSession(e.target, wizardStore()?.get(e.target)?.name || e.target, undefined, { silent: true });
|
|
202
|
+
if (!spawned.ok)
|
|
203
|
+
return { ok: false, retryable: true, reason: spawned.reason };
|
|
204
|
+
}
|
|
205
|
+
let outcome;
|
|
206
|
+
pinFrameBot(e.payload.human.frame, botOfKey(e.channel));
|
|
207
|
+
await bridge.dispatch({ ...e.payload.human, eventId: e.id,
|
|
208
|
+
text: e.payload.text + renderReminder({ wezard: "message-event", event: e.id }, []),
|
|
209
|
+
guard: boundary, onSubmitted: (r) => { outcome = r; } });
|
|
210
|
+
return outcome ?? { ok: false, retryable: events.get(e.id)?.exposure === "none", uncertain: events.get(e.id)?.exposure !== "none", reason: "dispatch ended without a submission acknowledgement" };
|
|
211
|
+
},
|
|
212
|
+
evidence: async (e) => {
|
|
213
|
+
if (e.canceled && await bridge.idleNow(e.target))
|
|
214
|
+
events.released(e.id);
|
|
215
|
+
const paths = [...new Set([bridge.sessionInfo(e.target)?.jsonlPath, e.transcript].filter((p) => !!p))];
|
|
216
|
+
for (const path of paths) {
|
|
217
|
+
const turns = turnsSince(expandHome(path), e.at - 60_000);
|
|
218
|
+
const start = turns.findIndex((t) => t.role === "user" && t.env?.eventId === e.id);
|
|
219
|
+
if (start < 0)
|
|
220
|
+
continue;
|
|
221
|
+
const next = turns.findIndex((t, i) => i > start && t.role === "user" && !t.queued);
|
|
222
|
+
const replies = turns.slice(start + 1, next < 0 ? undefined : next).filter((t) => t.role === "assistant");
|
|
223
|
+
const last = replies.at(-1);
|
|
224
|
+
const ended = next >= 0 || await bridge.idleNow(e.target);
|
|
225
|
+
return { consumed: true, interjected: !!turns[start].queued, ...(ended && last && !last.mid && !last.apiError ? { body: last.text, revision: finalId(e.id, e.channel, last.text) } : {}) };
|
|
226
|
+
}
|
|
227
|
+
return { consumed: false };
|
|
228
|
+
},
|
|
229
|
+
recovered: (e) => bridge.recoverEventFinal(e),
|
|
230
|
+
format: (d) => ({ ...d, chunks: d.chunks.map((c) => responseTracker.freeze(d.channel.replace(/^(user|chat|group):/, ""), c)) }),
|
|
231
|
+
send: async (d, n) => {
|
|
232
|
+
await bridge.settleFinalProgress(d.id);
|
|
233
|
+
const chat = d.channel.replace(/^(user|chat|group):/, "");
|
|
234
|
+
if (wx.owns(chat)) {
|
|
235
|
+
const result = wx.sendFinal(chat, wxMarkdown(d.chunks[n]), `${d.id}:${n}`);
|
|
236
|
+
return { ack: result.ack, queued: result.state === "queued" || result.state === "sending",
|
|
237
|
+
uncertain: result.state === "uncertain" ? result.reason ?? "Weixin send ACK unknown" : undefined,
|
|
238
|
+
failed: result.state === "failed" ? result.reason ?? "Weixin final rejected" : undefined };
|
|
239
|
+
}
|
|
240
|
+
const stream = d.stream && n === 0 && d.chunks.length === 1 && Date.now() < d.stream.expires ? d.stream : undefined;
|
|
241
|
+
const ack = d.transfer
|
|
242
|
+
? await bridge.deliverAnswer(d.transfer.id, d.target, d.turn, d.channel, "", d.chunks[n], d.transfer.standalone || n > 0 || d.chunks.length > 1, true)
|
|
243
|
+
: await responseTracker.sendFrozen(chat, d.chunks[n], stream, events.running);
|
|
244
|
+
return ack && typeof ack === "object" && "errcode" in ack && ack.errcode === 0 ? { ack: JSON.stringify(ack) }
|
|
245
|
+
: { ack: JSON.stringify(ack), uncertain: "final transport returned no explicit success ACK" };
|
|
246
|
+
},
|
|
247
|
+
deliveryEvidence: (d, n) => wx.finalStatus(`${d.id}:${n}`),
|
|
248
|
+
error: (e) => log.error({ err: errText(e) }, "message event scheduler"),
|
|
249
|
+
});
|
|
250
|
+
const bridge = startMirror({ cfg, log: log.child({ mod: "mirror" }), client: ws.client, store: mirrorStore, events, sendFrozen: (channel, content, stream) => responseTracker.sendFrozen(channel.replace(/^(user|chat|group):/, ""), content, stream, events.running) });
|
|
175
251
|
// 群里每个 `.name` 都挂它的 rolepage (见 session-label 的 TagLinker)。正文提及
|
|
176
252
|
// 只认真有其人的 —— 随手写的 `.gitignore` 不该变成一个开在空栏上的链接。
|
|
177
253
|
const known = (t) => bridge.chatTargets(t).includes(t);
|
|
@@ -190,7 +266,18 @@ const main = async () => {
|
|
|
190
266
|
// 重开的空档里 resume 回旧 sid。
|
|
191
267
|
// 宪章守卫 (见「改配置会不会动到宪章」): 要等下面宪章渲染那一块立起来才有; 之前 (启动时那次自动补名) 只写不比对。
|
|
192
268
|
let guardCharter = (write) => ({ r: write(), note: Promise.resolve("") });
|
|
193
|
-
installInboundRouter(ws.client, cfg, log, { ...bridge, dispatch: async (a) =>
|
|
269
|
+
installInboundRouter(ws.client, cfg, log, { ...bridge, dispatch: async (a) => {
|
|
270
|
+
if (a.text.trimStart().startsWith("/")) {
|
|
271
|
+
await handedOff(a.principal);
|
|
272
|
+
await bridge.dispatch(a);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const id = a.eventId ?? messageId(botOfKey(a.principal), a.channel, a.streamId);
|
|
276
|
+
events.accept({ id, identity: a.identity, originalAt: a.originalAt, target: a.principal, speaker: a.speaker, channel: a.channel,
|
|
277
|
+
asker: { who: a.speaker, chat: a.channel }, priority: "normal", deadline: Date.now() + 12 * 3600_000,
|
|
278
|
+
payload: { text: a.text, human: a } });
|
|
279
|
+
await ws.client.replyStream(a.frame, a.streamId, withTagHeader(a.principal, "已排队"), false).catch(() => undefined);
|
|
280
|
+
} }, sourcePath, (w, c) => guardCharter(w, c));
|
|
194
281
|
// approval click → finalize 当前 liveStream, 后续 tool/text 落到 standalone。
|
|
195
282
|
// 规则 2: 用户点击授权那一刻就是"上一段对话"的边界, 截断 stream 让授权后的
|
|
196
283
|
// 工作单独成块, 比让 stream 一直长到下一个 inbound / hardTimer 更清晰。
|
|
@@ -333,6 +420,11 @@ const main = async () => {
|
|
|
333
420
|
{
|
|
334
421
|
const m = bridge;
|
|
335
422
|
installMirrorEventListener(ws.client, m, log.child({ mod: "mirror" }));
|
|
423
|
+
http.register("GET /message-events/status", (_req, res) => json(res, 200, events.status()));
|
|
424
|
+
http.register("POST /message-events/reconcile", async (req, res) => {
|
|
425
|
+
const body = await readBody(req);
|
|
426
|
+
json(res, 200, await events.reconcile(String(body.id ?? "")) ?? { error: "unknown event" });
|
|
427
|
+
});
|
|
336
428
|
http.register("GET /mirror/status", (_req, res) => json(res, 200, m.status()));
|
|
337
429
|
// Manual auto-spawn trigger — used by `wezard init` to materialize a
|
|
338
430
|
// tmux+claude pane immediately after claim, instead of waiting for the
|
|
@@ -778,7 +870,20 @@ const main = async () => {
|
|
|
778
870
|
if (!answers.delivering(id))
|
|
779
871
|
return false;
|
|
780
872
|
try {
|
|
781
|
-
|
|
873
|
+
const eventId = `answer:${id}`;
|
|
874
|
+
const revision = finalId(eventId, r.channel, r.outcome.body);
|
|
875
|
+
const deliveryId = events.delivery({ id: finalId(eventId, r.channel, revision), eventId, target: r.target,
|
|
876
|
+
turn: r.sourceTurn, channel: r.channel, revision, body: r.outcome.body,
|
|
877
|
+
chunks: m.formatFinal(r.target, r.sourceTurn, r.channel, r.outcome.body),
|
|
878
|
+
transfer: { id, standalone: !["done", "need"].includes(r.outcome.status) } });
|
|
879
|
+
for (;;) {
|
|
880
|
+
const states = events.deliveryStatus(deliveryId);
|
|
881
|
+
if (states.every((s) => s === "delivered"))
|
|
882
|
+
break;
|
|
883
|
+
if (states.some((s) => s === "uncertain" || s === "failed"))
|
|
884
|
+
throw new Error(`final delivery uncertain: ${deliveryId}`);
|
|
885
|
+
await sleep(250);
|
|
886
|
+
}
|
|
782
887
|
answers.delivered(id);
|
|
783
888
|
recordPost({ target: r.target, channel: r.channel, body: r.outcome.body, ...(r.tell.asker ? { asker: r.tell.asker } : {}) });
|
|
784
889
|
return true;
|
|
@@ -802,6 +907,18 @@ const main = async () => {
|
|
|
802
907
|
&& !a.canceled && a.acceptance !== "rejected" && jobs.get(r.plan.job)?.status === "open" && ["running", "needs-input", "errored"].includes(a.execution)
|
|
803
908
|
&& (r.plan.nodes.find((x) => x.id === n.id)?.after ?? []).every((id) => workflow.prereq(r.owner, r.nodes.find((x) => x.id === id).turn)?.state === "done");
|
|
804
909
|
};
|
|
910
|
+
const acceptContinuation = (tell, text) => {
|
|
911
|
+
if (workflow.byTurn(tell.from, tell.turn) || !planStore.allowExternal(tell.to, wizards.get(tell.to)?.name))
|
|
912
|
+
return false;
|
|
913
|
+
const id = `p:${tell.from}:${tell.to}:${tell.turn}:${tell.legs}`;
|
|
914
|
+
events.accept({ id, target: tell.to, speaker: tell.from, channel: tell.channel, asker: tell.asker, k: tell.k,
|
|
915
|
+
priority: "normal", deadline: tell.deadlineAt ?? deadlineOf(tell.at ?? Date.now()), payload: { text, peer: {
|
|
916
|
+
from: { kind: "peer", from: tell.from, turn: tell.turn, ...(tell.asker ? { asker: tell.asker } : {}) },
|
|
917
|
+
envelope: envelopeFor(tell.from, tell.channel, { turn: tell.turn, legs: tell.legs, re: true, asker: tell.asker }),
|
|
918
|
+
tell: { ...tell, dispatchKey: id }, job: tell.job ?? "", kind: "task", held: true,
|
|
919
|
+
} } });
|
|
920
|
+
return true;
|
|
921
|
+
};
|
|
805
922
|
const receipts = createReceipts({
|
|
806
923
|
deliverAnswer: deliverTransferredAnswer,
|
|
807
924
|
workflowPrereq: workflow.prereq,
|
|
@@ -819,14 +936,10 @@ const main = async () => {
|
|
|
819
936
|
},
|
|
820
937
|
jobTally: (id) => jobs.tally(id),
|
|
821
938
|
parkedNow: m.parkedNow,
|
|
822
|
-
settleJob: (id, target, outcome, artifacts) => { jobs.settle(id, target, outcome, artifacts); },
|
|
939
|
+
settleJob: (id, target, outcome, artifacts, identity) => { jobs.settle(id, target, outcome, artifacts, identity); },
|
|
823
940
|
accept: (id, body) => { const j = jobs.get(id); return j?.status === "open" ? rejectReason(j.accept ?? "result", parseClosing(body)) : undefined; },
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
from: { kind: "peer", from: x.from, turn: x.turn, ...(x.asker ? { asker: x.asker } : {}) },
|
|
827
|
-
channel: x.channel,
|
|
828
|
-
envelope: envelopeFor(x.from, x.channel, { turn: x.turn, legs: x.legs, re: true, asker: x.asker }),
|
|
829
|
-
})).ok,
|
|
941
|
+
durableRebound: true,
|
|
942
|
+
rebound: async (x, why, tell) => !!tell && acceptContinuation(tell, `(守护进程代 ${displayName(x.from)} 打回一次: 这件活是工单里的一份, 你的回复${why}。请按约定收口:RESULT: <结论>;交付文件逐项 ARTIFACT: <路径> — 一句话;真缺信息用 NEED: <问题>。)`),
|
|
830
943
|
untilIdle: m.untilIdle,
|
|
831
944
|
paneLive: m.paneLive,
|
|
832
945
|
nameOf: displayName,
|
|
@@ -835,28 +948,32 @@ const main = async () => {
|
|
|
835
948
|
holdStore: loadJsonMap(cfg.wrc.mirror.holdsFile, undefined, { strict: true }),
|
|
836
949
|
// 放行停放着的 after 件: 走 tell_peer 同一条注入路径, 沿用停放时的件号 / 频道 / 父 k。
|
|
837
950
|
release: async (h, seen, alive) => {
|
|
838
|
-
const r = await tellPeerAs(h.from, h.body, afterNote(h.after, seen, h.from), { turn: h.turn, channel: h.channel, after: h.after, seen: h.seen, alive, ...(h.k ? { k: h.k } : {}), ...(h.asker ? { asker: h.asker } : {}) });
|
|
951
|
+
const r = await tellPeerAs(h.from, h.body, afterNote(h.after, seen, h.from), { at: h.at, turn: h.turn, channel: h.channel, after: h.after, seen: h.seen, alive, ...(h.k ? { k: h.k } : {}), ...(h.asker ? { asker: h.asker } : {}) });
|
|
839
952
|
return r.status === 200 ? { ok: true } : { ok: false, reason: String(r.body.reason ?? `HTTP ${r.status}`) };
|
|
840
953
|
},
|
|
841
954
|
handingOff,
|
|
842
955
|
handedOff,
|
|
843
956
|
replyFor: (to, fromName, since, turn) => m.replyToPeer(to, fromName, since, turn),
|
|
844
957
|
lastWords: m.lastReply,
|
|
845
|
-
deliver: (to, bodyText, meta, alive) =>
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
958
|
+
deliver: async (to, bodyText, meta, alive) => {
|
|
959
|
+
if ((alive && !alive()) || !planContinuationAllowed(to))
|
|
960
|
+
return { ok: false, retryable: true, reason: "receipt permission withdrawn" };
|
|
961
|
+
const id = meta.eventId;
|
|
962
|
+
const text = clipMiddle(bodyText);
|
|
963
|
+
events.accept({ id, identity: JSON.stringify([to, text, meta]), target: to, speaker: meta.from, channel: meta.channel,
|
|
964
|
+
asker: meta.asker, k: meta.k, replyTo: meta.replyTo, priority: "normal", deadline: Date.now() + 12 * 3600_000,
|
|
965
|
+
payload: { text, peer: { job: "", kind: "fyi",
|
|
966
|
+
from: { kind: "peer", from: meta.from, receipt: true, status: meta.status, ...(meta.turn ? { turn: meta.turn } : {}), ...(meta.job ? { job: meta.job, done: meta.done, total: meta.total } : {}), ...(meta.asker ? { asker: meta.asker } : {}) },
|
|
967
|
+
envelope: renderReceiptEnvelope(displayName(meta.from), meta.channel ? chatNameOf(cfg, meta.channel) : undefined, meta.job ? { job: meta.job, done: meta.done, total: meta.total, closed: jobs.get(meta.job)?.status !== "open", ...(jobs.get(meta.job)?.kind === "req" ? { req: true } : {}) } : undefined, meta.status, meta.turn || undefined, {
|
|
968
|
+
...(meta.replyTo ? { replyTo: meta.replyTo.kind === "chat" ? chatNameOf(cfg, meta.replyTo.channel) : displayName(meta.replyTo.from) } : {}),
|
|
969
|
+
...(meta.k ? { k: kAttr(meta.k) } : {}),
|
|
970
|
+
...(meta.asker ? { asker: meta.asker } : {}),
|
|
971
|
+
...(meta.pending ? { pending: meta.pending } : {}),
|
|
972
|
+
...(audienceName(meta.channel, meta.asker) ? { audience: audienceName(meta.channel, meta.asker) } : {}),
|
|
973
|
+
}) + (meta.eventId ? renderReminder({ wezard: "receipt-delivery", event: meta.eventId }, []) : "")
|
|
974
|
+
} } });
|
|
975
|
+
return events.submission(id);
|
|
976
|
+
},
|
|
860
977
|
});
|
|
861
978
|
stopReceipts = receipts.stop;
|
|
862
979
|
const turnsLine = (xs, target) => renderInFlight(xs, target, displayName);
|
|
@@ -968,7 +1085,9 @@ const main = async () => {
|
|
|
968
1085
|
};
|
|
969
1086
|
const controlSnapshot = (self) => ({ opening: openingOfSelf(self)?.ms, turn: m.answerContext(self)?.turn });
|
|
970
1087
|
const controlValid = (self, snapshot) => openingOfSelf(self)?.ms === snapshot.opening && m.answerContext(self)?.turn === snapshot.turn && !lostAnswer(self) && !terminalAnswerTurn(self);
|
|
971
|
-
const hasAnswerChildren = (self, k) => !!k && receipts.states().some((s) => s.from === self && !!s.k && kAttr(s.k) === kAttr(k))
|
|
1088
|
+
const hasAnswerChildren = (self, k) => !!k && (receipts.states().some((s) => s.from === self && !!s.k && kAttr(s.k) === kAttr(k))
|
|
1089
|
+
|| events.status().events.some((e) => e.speaker === self && e.k && kAttr(e.k) === kAttr(k) && e.payload.peer?.tell && !["complete", "canceled"].includes(e.execution))
|
|
1090
|
+
|| workflow.all().some((a) => a.owner === self && a.tell?.k && kAttr(a.tell.k) === kAttr(k) && !a.canceled && a.execution !== "terminal"));
|
|
972
1091
|
m.setAnswerSilencer((target, turn) => answers.source(target, turn) || lostAnswer(target));
|
|
973
1092
|
const parentKOf = (self) => {
|
|
974
1093
|
const opening = openingOfSelf(self);
|
|
@@ -992,6 +1111,11 @@ const main = async () => {
|
|
|
992
1111
|
void m.restored.then(() => {
|
|
993
1112
|
handoffs.resume();
|
|
994
1113
|
answers.all().filter((r) => r.state !== "delivered").forEach((r) => {
|
|
1114
|
+
const savedFinal = events.status().deliveries.find((d) => d.transfer?.id === r.id);
|
|
1115
|
+
if (savedFinal?.parts.every((p) => p.state === "delivered")) {
|
|
1116
|
+
answers.delivered(r.id);
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
995
1119
|
const action = answerRecoveryAction(r);
|
|
996
1120
|
if (action === "cancel-before-send") {
|
|
997
1121
|
answers.activate(r.id);
|
|
@@ -1028,16 +1152,8 @@ const main = async () => {
|
|
|
1028
1152
|
ask: async ({ from, to, turn, text }) => {
|
|
1029
1153
|
if (workflow.byTurn(from, turn) || !planStore.allowExternal(to, wizards.get(to)?.name))
|
|
1030
1154
|
return false;
|
|
1031
|
-
const
|
|
1032
|
-
|
|
1033
|
-
return false;
|
|
1034
|
-
ledger.touch(from, turn);
|
|
1035
|
-
return (await m.injectText(to, text, undefined, {
|
|
1036
|
-
guard: () => planStore.allowExternal(to, wizards.get(to)?.name),
|
|
1037
|
-
from: { kind: "peer", from, turn, ...(x.asker ? { asker: x.asker } : {}) },
|
|
1038
|
-
channel: x.channel,
|
|
1039
|
-
envelope: envelopeFor(from, x.channel, { turn, legs: x.legs, re: true, asker: x.asker }),
|
|
1040
|
-
})).ok;
|
|
1155
|
+
const tell = receipts.continuation(from, to, turn);
|
|
1156
|
+
return !!tell && acceptContinuation(tell, text);
|
|
1041
1157
|
},
|
|
1042
1158
|
nameOf: displayName,
|
|
1043
1159
|
store: loadJsonMap(join(cfg.daemon.stateDir, "idle-nudges.json")),
|
|
@@ -1393,7 +1509,7 @@ const main = async () => {
|
|
|
1393
1509
|
if (!a.dispatches?.length)
|
|
1394
1510
|
throw new Error("historical tracked attempt lacks dispatch accounting evidence; reconcile stopped before retry (no blind budget import)");
|
|
1395
1511
|
for (const d of a.dispatches) {
|
|
1396
|
-
jobs.account(a.job, d.key, { target: a.to, task: d.continuation ? "" : d.text, spawned: false, ...(d.role ? { role: d.role } : {}) }, d.kind, d.free, recovery, !!d.free && !!d.continuation);
|
|
1512
|
+
jobs.account(a.job, d.key, { target: a.to, task: d.continuation ? "" : d.text, spawned: false, turn: (d.tell ?? a.tell)?.turn, ...(d.role ? { role: d.role } : {}) }, d.kind, d.free, recovery, !!d.free && !!d.continuation);
|
|
1397
1513
|
const t = d.tell ?? a.tell;
|
|
1398
1514
|
ledger.dispatch(d.key, { turn: a.turn, owner: a.owner, to: a.to, text: d.text, at: d.at, job: a.job, ...(t?.k ? { for: t.k } : {}), ...(t?.asker ? { asker: t.asker } : {}) });
|
|
1399
1515
|
}
|
|
@@ -1410,10 +1526,86 @@ const main = async () => {
|
|
|
1410
1526
|
...workflow.all().filter((a) => a.to === target && a.execution !== "terminal" && !a.canceled && only({ from: a.owner, turn: a.turn })).map((a) => `${a.owner}\0${a.turn}`),
|
|
1411
1527
|
...receipts.states().filter((s) => s.to === target && only(s)).map((s) => `${s.from}\0${s.turn}`),
|
|
1412
1528
|
]);
|
|
1529
|
+
events.cancel((e) => e.target === target && !!e.payload.peer && only({ from: e.speaker, turn: e.payload.peer.from.turn ?? "" }));
|
|
1413
1530
|
workflow.cancel(target, undefined, undefined, undefined, only);
|
|
1414
1531
|
receipts.cancel(target, self, only);
|
|
1415
1532
|
return keys.size;
|
|
1416
1533
|
};
|
|
1534
|
+
peerEvents = {
|
|
1535
|
+
valid: (e) => {
|
|
1536
|
+
const p = e.payload.peer;
|
|
1537
|
+
const attempt = p.attemptId ? workflow.byTurn(e.speaker, p.from.turn ?? "") : undefined;
|
|
1538
|
+
return planStore.healthy() && !e.canceled && (!p.controlTransfer || !answers.get(p.controlTransfer)?.outcome) && (!p.from.receipt || planContinuationAllowed(e.target)) && (!p.job || jobs.get(p.job)?.status === "open") && !attempt?.canceled
|
|
1539
|
+
&& (!p.after?.length || receipts.prereqs(e.speaker, p.after, p.afterSeen).every((r) => r.state === "done" || r.state === "pending"))
|
|
1540
|
+
&& !answers.all().some((r) => r.target === e.target && answerTargetOccupied(r));
|
|
1541
|
+
},
|
|
1542
|
+
accepted: (e) => {
|
|
1543
|
+
if (e.execution !== "queued")
|
|
1544
|
+
return;
|
|
1545
|
+
const p = e.payload.peer;
|
|
1546
|
+
if (p.tell && e.k?.kind === "chat")
|
|
1547
|
+
events.defer(e.k.turn, true);
|
|
1548
|
+
if (p.attemptId)
|
|
1549
|
+
accountWorkflow(workflow.byTurn(e.speaker, p.from.turn ?? ""));
|
|
1550
|
+
else if (p.job && !p.held)
|
|
1551
|
+
jobs.account(p.job, e.id, { target: e.target, turn: p.from.turn, task: (p.tell?.legs ?? 1) > 1 ? "" : e.payload.text, spawned: !!p.spawned, ...(p.role ? { role: p.role } : {}) }, p.kind === "ask" ? "ask" : "task", !!p.free || !!p.held, false, !!p.free);
|
|
1552
|
+
if (p.tell) {
|
|
1553
|
+
if (!p.attemptId)
|
|
1554
|
+
ledger.dispatch(e.id, { turn: p.tell.turn, owner: e.speaker, to: e.target, text: e.payload.text, at: e.at, ...(p.job ? { job: p.job } : {}), ...(e.k ? { for: e.k } : {}), ...(e.asker ? { asker: e.asker } : {}) });
|
|
1555
|
+
if (!receipts.watching(p.tell))
|
|
1556
|
+
receipts.register({ ...p.tell, queued: true });
|
|
1557
|
+
}
|
|
1558
|
+
},
|
|
1559
|
+
committed: (e) => {
|
|
1560
|
+
const p = e.payload.peer;
|
|
1561
|
+
if (p.attemptId) {
|
|
1562
|
+
const a = workflow.byTurn(e.speaker, p.from.turn ?? "");
|
|
1563
|
+
if (a?.execution === "dispatch-unknown" && !a.canceled && !a.status && e.exposure === "consumed")
|
|
1564
|
+
workflow.verdict(e.speaker, p.attemptId, "sent", undefined, `transcript confirms message event ${e.id}`);
|
|
1565
|
+
else
|
|
1566
|
+
workflow.dispatched(p.attemptId, true);
|
|
1567
|
+
}
|
|
1568
|
+
if (p.tell)
|
|
1569
|
+
receipts.commit(p.tell);
|
|
1570
|
+
if (p.from.receipt)
|
|
1571
|
+
receipts.confirmDelivery(e.id);
|
|
1572
|
+
},
|
|
1573
|
+
canceled: (e) => {
|
|
1574
|
+
const p = e.payload.peer;
|
|
1575
|
+
if (p.attemptId)
|
|
1576
|
+
workflow.cancel(e.target, e.speaker, p.job, p.from.turn);
|
|
1577
|
+
if (p.tell)
|
|
1578
|
+
receipts.cancel(e.target, "scheduler", (s) => s.from === e.speaker && s.turn === p.tell.turn);
|
|
1579
|
+
},
|
|
1580
|
+
ready: async (e) => receipts.prereqs(e.speaker, e.payload.peer.after ?? [], e.payload.peer.afterSeen).every((r) => r.state === "done")
|
|
1581
|
+
&& (e.priority === "now" || await m.idleNow(e.target) || !(await m.paneLive(e.target)) || (e.priority === "urgent" && !(await othersTurn(e.target, e.speaker)))),
|
|
1582
|
+
inject: async (e, boundary) => {
|
|
1583
|
+
const p = e.payload.peer;
|
|
1584
|
+
if (e.priority === "urgent" && !(await m.idleNow(e.target))) {
|
|
1585
|
+
if (await othersTurn(e.target, e.speaker))
|
|
1586
|
+
return { ok: false, retryable: true, reason: "urgent target now belongs to another actor" };
|
|
1587
|
+
const path = m.sessionInfo(e.target)?.jsonlPath;
|
|
1588
|
+
const opening = path ? openingTurn(expandHome(path), pingSigs)?.ms : undefined;
|
|
1589
|
+
const r = await m.interruptPane(e.target, { guard: () => peerEvents.valid(e)
|
|
1590
|
+
&& m.sessionInfo(e.target)?.jsonlPath === path && (!path || openingTurn(expandHome(path), pingSigs)?.ms === opening) });
|
|
1591
|
+
if (!r.ok)
|
|
1592
|
+
return { ok: false, retryable: true, reason: r.reason };
|
|
1593
|
+
}
|
|
1594
|
+
if (p.attemptId && p.tell)
|
|
1595
|
+
workflow.intent(p.attemptId, p.tell);
|
|
1596
|
+
const raw = await m.injectText(e.target, e.payload.text, undefined, {
|
|
1597
|
+
from: p.from, channel: e.channel, requireIdle: e.priority === "normal",
|
|
1598
|
+
envelope: p.envelope + (p.after?.length ? afterNote(p.after, Object.fromEntries(p.after.map((t, n) => [t, receipts.prereqs(e.speaker, p.after, p.afterSeen)[n].seen])), e.speaker) : "") + renderReminder({ wezard: "message-event", event: e.id }, []),
|
|
1599
|
+
guard: (phase) => peerEvents.valid(e) && receipts.prereqs(e.speaker, p.after ?? [], p.afterSeen).every((r) => r.state === "done") && (!phase || ((!p.attemptId || workflow.boundary(p.attemptId)) && boundary(phase))),
|
|
1600
|
+
});
|
|
1601
|
+
const r = !raw.ok && !raw.uncertain && !raw.retryable && events.get(e.id)?.exposure === "none" ? { ...raw, retryable: true } : raw;
|
|
1602
|
+
if (p.attemptId && (r.uncertain || (!r.ok && !r.retryable)))
|
|
1603
|
+
workflow.dispatched(p.attemptId, false);
|
|
1604
|
+
return r;
|
|
1605
|
+
},
|
|
1606
|
+
};
|
|
1607
|
+
m.setEventFinalGate((e) => !e.awaitingContinuation && !e.canceled && !e.interjected && !receipts.pendingDelivery(e.target, { kind: "chat", channel: e.channel, turn: e.id }, e.currentContinuation) && !hasAnswerChildren(e.target, { kind: "chat", channel: e.channel, turn: e.id }) && !answers.all().some((r) => r.source === e.target && (r.sourceTurn === e.turn || r.origin === e.id)));
|
|
1608
|
+
void m.restored.then(() => events.resume());
|
|
1417
1609
|
/** tell_peer 的本体: 以 `self` 的名义说一句 —— HTTP 路由与 dispatch 共用。`extra` = 挂在信封后面的
|
|
1418
1610
|
* 一段 (只进对方输入框, 不进 `text`: 公开时 text 会成群气泡)。 */
|
|
1419
1611
|
const peerDispatching = new Map();
|
|
@@ -1533,10 +1725,6 @@ const main = async () => {
|
|
|
1533
1725
|
return { status: 409, body: { ok: false, reason: `你已有一件活 (${cur.turn}) 在 ${displayName(target)} 手上没落定, 同一对只有一个回执槽 —— 等它交差, 或把它也列进 after` } };
|
|
1534
1726
|
}
|
|
1535
1727
|
}
|
|
1536
|
-
const parkIt = prereqs.some((p) => p.state === "pending");
|
|
1537
|
-
// 前置都已落定: 不停放, 结论附在信封后面照常投。
|
|
1538
|
-
if (afters.length && !parkIt)
|
|
1539
|
-
extra += afterNote(afters, Object.fromEntries(afters.map((t, i) => [t, prereqs[i].seen])), self);
|
|
1540
1728
|
// 公开与否由发话方 (LLM) 判断: 公开 = 在它这一轮的公开频道里说, 气泡进群、对方
|
|
1541
1729
|
// 那一轮的回复也发进这个群; 私聊 (默认) = 只落双方的 rolepage, 回复靠 wait_peer 取。
|
|
1542
1730
|
// 只有两端都是登记在册的 wizard 才能当着人说: 裸 target 冒充的发话方、forget
|
|
@@ -1558,7 +1746,8 @@ const main = async () => {
|
|
|
1558
1746
|
// 空 pane 会 resume 回旧 sid (见 handoff.ts)。发话时刻取在这之后。
|
|
1559
1747
|
if (answer === "transfer" && handingOff(target))
|
|
1560
1748
|
return { status: 409, body: { ok: false, reason: "answer target is handing off; source retains answer and control" } };
|
|
1561
|
-
|
|
1749
|
+
if (answer === "transfer")
|
|
1750
|
+
await handedOff(target);
|
|
1562
1751
|
if (!actorStillValid())
|
|
1563
1752
|
return { status: 409, body: { ok: false, reason: "caller turn/control changed while waiting; dispatch refused" } };
|
|
1564
1753
|
// 投递策略。对方闲着三者一样 —— 立刻投; 只在它正忙时分岔:
|
|
@@ -1600,36 +1789,6 @@ const main = async () => {
|
|
|
1600
1789
|
throw new Error("plan dispatch canceled or job reserved");
|
|
1601
1790
|
return node ? workflow.begin(self, jobId, node, turn.turn, target, queued).id : undefined;
|
|
1602
1791
|
};
|
|
1603
|
-
if (parkIt) {
|
|
1604
|
-
try {
|
|
1605
|
-
attemptId = beginNode(true);
|
|
1606
|
-
}
|
|
1607
|
-
catch (e) {
|
|
1608
|
-
return { status: 409, body: { ok: false, reason: e.message } };
|
|
1609
|
-
}
|
|
1610
|
-
const bp = body;
|
|
1611
|
-
if (attemptId) {
|
|
1612
|
-
try {
|
|
1613
|
-
workflow.recordLeg(attemptId, { text, at: Date.now(), tell: { from: self, to: target, channel, job: jobId, turn: turn.turn, at: Date.now(), legs: 1, ...(k ? { k } : {}), ...(asker ? { asker } : {}) }, kind: "task", ...(roleParam(bp.role) ? { role: roleParam(bp.role) } : {}) });
|
|
1614
|
-
accountWorkflow(workflow.byTurn(self, turn.turn));
|
|
1615
|
-
}
|
|
1616
|
-
catch (e) {
|
|
1617
|
-
return { status: 409, body: { ok: false, attemptId, reason: e.message } };
|
|
1618
|
-
}
|
|
1619
|
-
}
|
|
1620
|
-
receipts.hold({
|
|
1621
|
-
from: self, to: target, turn: turn.turn, at: Date.now(), after: afters, channel, job: jobId,
|
|
1622
|
-
...(k ? { k } : {}), ...(asker ? { asker } : {}),
|
|
1623
|
-
// force: 冷门控上面已过一遍, 放行时缓存又凉了也不再退回。
|
|
1624
|
-
body: { name: peerAddress(cfg, self, target), text, kind, force: true, ...(node ? { node } : {}), ...(jobId ? { job: jobId } : {}), ...(bp.role ? { role: bp.role } : {}), ...(bp.priority ? { priority: bp.priority } : {}), ...(bp.waitSec ? { waitSec: bp.waitSec } : {}), ...(bp.deadline !== undefined ? { deadline: bp.deadline } : {}), ...(bp.receipt === false ? { receipt: false } : {}) },
|
|
1625
|
-
});
|
|
1626
|
-
if (jobId && !attemptId) {
|
|
1627
|
-
const role = roleParam(bp.role);
|
|
1628
|
-
jobs.attach(jobId, { target, task: text, spawned: false, ...(role ? { role } : {}) }, kind === "ask" ? "ask" : "task");
|
|
1629
|
-
jobs.spend(jobId);
|
|
1630
|
-
}
|
|
1631
|
-
return { status: 200, body: { ok: true, parked: true, after: afters, turn: turn.turn, ...(attemptId ? { node, attemptId } : {}), name: peerAddress(cfg, self, target), ...(jobId ? { job: jobId } : {}), receipt: `排在 ${afters.join(" ")} 之后: 前置满足放行条件才投(tracked 要 accepted,legacy 要 done;带上结论), 任一失败这件不投、按 canceled 回给你; 不要挂在 wait_peer 上等, 接着干你自己的事` } };
|
|
1632
|
-
}
|
|
1633
1792
|
try {
|
|
1634
1793
|
attemptId = beginNode(true);
|
|
1635
1794
|
}
|
|
@@ -1645,6 +1804,44 @@ const main = async () => {
|
|
|
1645
1804
|
// 打断就误伤了那件 —— 降成 normal, 等它这一轮做完再投。
|
|
1646
1805
|
const others = asked === "urgent" && wasBusy ? await othersTurn(target, self) : undefined;
|
|
1647
1806
|
const policy = others ? "normal" : asked;
|
|
1807
|
+
if (answer !== "transfer") {
|
|
1808
|
+
if (canceledBeforeDispatch() || !actorStillValid())
|
|
1809
|
+
return { status: 409, body: { ok: false, reason: "caller authority or dispatch permit changed before acceptance" } };
|
|
1810
|
+
if (jobId) {
|
|
1811
|
+
const budget = checkJob(jobId, target, (!!open?.need && open.turn === turn.turn) || !!held);
|
|
1812
|
+
if (!budget.ok)
|
|
1813
|
+
return { status: budget.status, body: { ok: false, reason: budget.reason } };
|
|
1814
|
+
}
|
|
1815
|
+
// All caller-dependent facts are frozen here; workers never reread the caller's current turn.
|
|
1816
|
+
const at = held?.at ?? Date.now();
|
|
1817
|
+
const deadlineAt = deadlineOf(at, body.deadline);
|
|
1818
|
+
const wantReceipt = kind !== "fyi" && body.receipt !== false;
|
|
1819
|
+
const eventId = `p:${self}:${target}:${turn.turn}:${turn.legs}`;
|
|
1820
|
+
let tell = { from: self, to: target, channel, job: jobId, at, turn: turn.turn, legs: turn.legs, deadlineAt, k, asker, dispatchKey: eventId };
|
|
1821
|
+
if (attemptId) {
|
|
1822
|
+
const leg = workflow.recordLeg(attemptId, { text, tell, continuation: turn.legs > 1, at, kind: "task", role: roleParam(body.role), free: !!open?.need && open.turn === turn.turn }, !!held);
|
|
1823
|
+
tell = { ...tell, dispatchKey: leg.key };
|
|
1824
|
+
}
|
|
1825
|
+
let accepted;
|
|
1826
|
+
try {
|
|
1827
|
+
accepted = events.accept({ id: eventId, target, speaker: self, channel, asker, k, priority: policy, deadline: deadlineAt,
|
|
1828
|
+
payload: { text, peer: {
|
|
1829
|
+
from: { kind: "peer", from: self, turn: turn.turn, ...(jobId ? { job: jobId } : {}), ...(isPublic ? { public: true } : {}), ...(asker ? { asker } : {}) },
|
|
1830
|
+
envelope: envelopeFor(self, channel, { turn: turn.turn, asker, legs: turn.legs, ...(turn.legs > 1 ? { re: true } : {}),
|
|
1831
|
+
...(body.deadline !== undefined ? { deadline: deadlineAt } : {}), ...(kind !== "task" ? { act: kind } : {}), ...(wantReceipt ? {} : { quiet: true }) }) + extra,
|
|
1832
|
+
...(wantReceipt ? { tell } : {}), job: jobId, kind, attemptId, after: held?.after ?? afters, afterSeen: held?.seen,
|
|
1833
|
+
controlTransfer: answers.all().find((r) => r.target === self && r.tell.turn === openingOfSelf(self)?.env?.turn && !r.outcome)?.id,
|
|
1834
|
+
role: roleParam(body.role), free: !!open?.need && open.turn === turn.turn, held: !!held,
|
|
1835
|
+
} } });
|
|
1836
|
+
}
|
|
1837
|
+
catch (error) {
|
|
1838
|
+
return { status: 409, body: { ok: false, eventId, reason: errText(error) } };
|
|
1839
|
+
}
|
|
1840
|
+
return { status: 200, body: { ...accepted, queued: true, ...(prereqs.some((p) => p.state === "pending") ? { parked: true, after: afters } : {}), turn: turn.turn, name: peerAddress(cfg, self, target), priority: policy, wasBusy,
|
|
1841
|
+
...(jobId ? { job: jobId } : {}), ...(attemptId ? { node, attemptId, acceptance: "pending" } : {}),
|
|
1842
|
+
...(wantReceipt ? { receipt: "已持久化排队;提交后观察原件回执,结论自动返回" } : {}),
|
|
1843
|
+
...(others ? { urgentDowngraded: "目标正在处理他人的轮次,按 normal 排队" } : {}) } };
|
|
1844
|
+
}
|
|
1648
1845
|
let waitedMs = 0;
|
|
1649
1846
|
let interrupted;
|
|
1650
1847
|
if (policy === "normal" && wasBusy) {
|
|
@@ -1733,7 +1930,7 @@ const main = async () => {
|
|
|
1733
1930
|
}
|
|
1734
1931
|
if (answerContext && k?.kind === "chat") {
|
|
1735
1932
|
try {
|
|
1736
|
-
answers.reserve({ id: turn.turn, source: self, target, sourceTurn: answerContext.turn, sourceOpening: answerSourceOpening, origin: answerTransferOrigin(self, answerContext.turn), channel: k.channel, prefix: answerContext.prefix, tell: trackedTell, state: "reserved" });
|
|
1933
|
+
answers.reserve({ id: turn.turn, source: self, target, sourceTurn: answerContext.turn, sourceOpening: answerSourceOpening, origin: openingOfSelf(self)?.env?.eventId ?? answerTransferOrigin(self, answerContext.turn), channel: k.channel, prefix: answerContext.prefix, tell: trackedTell, state: "reserved" });
|
|
1737
1934
|
}
|
|
1738
1935
|
catch (e) {
|
|
1739
1936
|
return { status: 409, body: { ok: false, reason: e.message } };
|
|
@@ -1948,7 +2145,7 @@ const main = async () => {
|
|
|
1948
2145
|
return;
|
|
1949
2146
|
}
|
|
1950
2147
|
// 同一个 wizard 写两遍不该顶掉两个名额。
|
|
1951
|
-
const uniq = hits.filter((h, i) => hits.findIndex((o) => o.target === h.target) === i);
|
|
2148
|
+
const uniq = hits.filter((h, i) => hits.findIndex((o) => o.target === h.target) === i).map((h) => ({ ...h, turn: receipts.sentTurn(self, h.target), at: receipts.sentAt(self, h.target) }));
|
|
1952
2149
|
const need = Math.min(Math.max(Number(b.need ?? uniq.length) || uniq.length, 1), uniq.length);
|
|
1953
2150
|
const timeoutMs = Math.min(Math.max(Number(b.timeoutSec ?? 900) || 900, 10), 7200) * 1000;
|
|
1954
2151
|
const wrs = await waitForQuorum(uniq.map((h) => h.target), m.isBusy, timeoutMs, need);
|
|
@@ -1963,8 +2160,8 @@ const main = async () => {
|
|
|
1963
2160
|
const results = uniq.map((h, i) => {
|
|
1964
2161
|
const wr = wrs[i];
|
|
1965
2162
|
// 取走就占位: 同一段话不会再作为回执 paste 进来一遍 (见 receipts.claim)。
|
|
1966
|
-
const full = wr.idle ? m.
|
|
1967
|
-
const already = full ? receipts.claim(self, h.target) : false;
|
|
2163
|
+
const full = wr.idle && h.turn ? m.replyToPeer(h.target, displayName(self), h.at, h.turn)?.text ?? "" : "";
|
|
2164
|
+
const already = full ? receipts.claim(self, h.target, h.turn) : false;
|
|
1968
2165
|
const closing = parseClosing(already === "uncertain" ? "" : full);
|
|
1969
2166
|
const result = closing.kind === "result" ? closing.text : "";
|
|
1970
2167
|
return {
|
|
@@ -3140,11 +3337,7 @@ const main = async () => {
|
|
|
3140
3337
|
cwd: b.cwd,
|
|
3141
3338
|
systemPrompt: charter,
|
|
3142
3339
|
inherit,
|
|
3143
|
-
//
|
|
3144
|
-
// 也少一次"就位了但没事干"的空转。
|
|
3145
|
-
// 子 lead 的宪章只有层级规矩, 打法跟第一句的信封走 (同 dispatch 对已有 wizard 的做法)。
|
|
3146
|
-
bootstrap: task ? task + envelopeFor(self, "", { turn: taskTurn, asker }) + (b.lead === true ? renderReminder({ wezard: "lead" }, renderLeadPlaybook()) : "") : undefined,
|
|
3147
|
-
...(task ? { bootstrapFrom: { from: { kind: "peer", from: self, turn: taskTurn, ...(jobId ? { job: jobId } : {}), ...(asker ? { asker } : {}) }, query: task } } : {}),
|
|
3340
|
+
// Fork initialization carries no task; actual work is durably accepted below.
|
|
3148
3341
|
keepalive,
|
|
3149
3342
|
}).catch((e) => ({ ok: false, reason: `spawn threw: ${String(e)}`, inherited: false }));
|
|
3150
3343
|
// 生不出来就回滚身份 —— 否则名字被一个永远没有会话的记录占住。
|
|
@@ -3167,26 +3360,22 @@ const main = async () => {
|
|
|
3167
3360
|
...(kid.description ? { job: kid.description } : {}), cwd: r.cwd,
|
|
3168
3361
|
}, subjectOf(wizards.get(target)));
|
|
3169
3362
|
}
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
const
|
|
3174
|
-
|
|
3363
|
+
const dispatched = !!task && birthGuard();
|
|
3364
|
+
if (dispatched) {
|
|
3365
|
+
const eventId = `p:${self}:${target}:${taskTurn}:1`;
|
|
3366
|
+
const deadlineAt = deadlineOf(taskAt);
|
|
3367
|
+
events.accept({ id: eventId, target, speaker: self, channel: "", asker, k, priority: "normal", deadline: deadlineAt,
|
|
3368
|
+
payload: { text: task, peer: {
|
|
3369
|
+
from: { kind: "peer", from: self, turn: taskTurn, ...(jobId ? { job: jobId } : {}), ...(asker ? { asker } : {}) },
|
|
3370
|
+
envelope: envelopeFor(self, "", { turn: taskTurn, asker }) + (b.lead === true ? renderReminder({ wezard: "lead" }, renderLeadPlaybook()) : ""),
|
|
3371
|
+
tell: { from: self, to: target, channel: "", job: jobId, at: taskAt, turn: taskTurn, legs: 1, deadlineAt, k, asker, dispatchKey: eventId },
|
|
3372
|
+
job: jobId, kind: "task", spawned: true, role: roleParam(body.role),
|
|
3373
|
+
} } });
|
|
3175
3374
|
}
|
|
3176
|
-
if (jobId && birthGuard()) {
|
|
3177
|
-
|
|
3178
|
-
const from = inherit ? source : undefined;
|
|
3179
|
-
jobs.attach(jobId, { target, task, spawned: true, ...(role ? { role } : {}), ...(from ? { forkOf: from } : {}) });
|
|
3375
|
+
else if (jobId && birthGuard()) {
|
|
3376
|
+
jobs.attach(jobId, { target, task: "", spawned: true, ...(roleParam(body.role) ? { role: roleParam(body.role) } : {}) });
|
|
3180
3377
|
}
|
|
3181
|
-
|
|
3182
|
-
jobs.spend(jobId);
|
|
3183
|
-
// 分身的第一件活也守回执: fan-out 最常见的形状就是 clone_wizard({task}) × N,
|
|
3184
|
-
// 让它们干完自己把结论送回来, 发起方不必挂在 wait_peer 上。
|
|
3185
|
-
if (dispatched)
|
|
3186
|
-
receipts.register({ from: self, to: target, channel: "", job: jobId, at: taskAt, turn: taskTurn, ...(k ? { k } : {}), ...(asker ? { asker } : {}) });
|
|
3187
|
-
if (dispatched)
|
|
3188
|
-
ledger.open({ turn: taskTurn, owner: self, to: target, text: task, at: taskAt, ...(jobId ? { job: jobId } : {}), ...(k ? { for: k } : {}), ...(asker ? { asker } : {}) });
|
|
3189
|
-
return { status: 200, body: { ok: true, target, name, address: name, inherited: r.inherited, sessionId: r.sessionId, cwd: r.cwd, dispatched, ...(dispatched ? { turn: taskTurn } : {}), keepalive, ...(r.model ? { model: r.model } : {}), ...(r.modelWarning ? { modelWarning: r.modelWarning } : {}), ...(r.effort ? { effort: r.effort } : {}), ...(pick.tier ? { tier: pick.tier } : {}), via: pick.via, ...(pick.note ? { tierNote: pick.note } : {}), ...(jobId ? { job: jobId } : {}), ...(detached ? { detached: true } : {}) } };
|
|
3378
|
+
return { status: 200, body: { ok: true, target, name, address: name, inherited: r.inherited, sessionId: r.sessionId, cwd: r.cwd, dispatched, queued: dispatched, ...(dispatched ? { turn: taskTurn } : {}), keepalive, ...(r.model ? { model: r.model } : {}), ...(r.modelWarning ? { modelWarning: r.modelWarning } : {}), ...(r.effort ? { effort: r.effort } : {}), ...(pick.tier ? { tier: pick.tier } : {}), via: pick.via, ...(pick.note ? { tierNote: pick.note } : {}), ...(jobId ? { job: jobId } : {}), ...(detached ? { detached: true } : {}) } };
|
|
3190
3379
|
}
|
|
3191
3380
|
finally {
|
|
3192
3381
|
if (birthJob) {
|
|
@@ -3211,6 +3400,7 @@ const main = async () => {
|
|
|
3211
3400
|
cancel: (run) => {
|
|
3212
3401
|
workflow.cancel(undefined, run.owner, run.plan.job);
|
|
3213
3402
|
run.nodes.filter((n) => n.to).forEach((n) => receipts.cancel(n.to, run.owner, (x) => x.from === run.owner && x.turn === n.turn));
|
|
3403
|
+
events.cancel((e) => e.speaker === run.owner && e.payload.peer?.job === run.plan.job);
|
|
3214
3404
|
receipts.cancelHeld(run.plan.job, run.owner);
|
|
3215
3405
|
},
|
|
3216
3406
|
error: (e) => log.error({ err: errText(e) }, "workflow plan advancement paused"),
|
|
@@ -3514,6 +3704,7 @@ const main = async () => {
|
|
|
3514
3704
|
if (how === "cancel")
|
|
3515
3705
|
j.members.filter((mm) => !mm.outcome).forEach((mm) => receipts.cancel(mm.target, j.owner, (x) => x.from === j.owner));
|
|
3516
3706
|
// 这张单名下还排着没投的 (after 停放) 也不投了。
|
|
3707
|
+
events.cancel((e) => e.speaker === j.owner && e.payload.peer?.job === j.id);
|
|
3517
3708
|
receipts.cancelHeld(j.id, j.owner);
|
|
3518
3709
|
const closed = jobs.close(j.id, summary, mark);
|
|
3519
3710
|
ledger.closeJob(j.id);
|
|
@@ -4086,6 +4277,8 @@ const main = async () => {
|
|
|
4086
4277
|
const turn = newTurn();
|
|
4087
4278
|
// Confirm idle inside the physical pane transaction; a race is unsent
|
|
4088
4279
|
// and returns to the target consumer, without releasing task dedup.
|
|
4280
|
+
if (quiet)
|
|
4281
|
+
quietRelays.set(turn, { target, at, base: baseOfKey(target) });
|
|
4089
4282
|
const inj = await m.injectText(target, text, undefined, quiet
|
|
4090
4283
|
? { guard, requireIdle: true, from: { kind: "task", taskId }, channel: "", envelope: renderTaskEnvelope(taskId, { turn }) }
|
|
4091
4284
|
: { guard, requireIdle: true, fromChat: true, from: { kind: "task", taskId }, envelope: renderTaskEnvelope(taskId) });
|
|
@@ -4106,12 +4299,19 @@ const main = async () => {
|
|
|
4106
4299
|
const end = at + TASK_QUIET_WAIT_MS;
|
|
4107
4300
|
// 先等它动起来: 刚注入时注册表还报 idle。
|
|
4108
4301
|
await sleep(5000);
|
|
4109
|
-
|
|
4110
|
-
|
|
4302
|
+
let firstObservation = true;
|
|
4303
|
+
while (firstObservation || Date.now() < end) {
|
|
4304
|
+
firstObservation = false;
|
|
4305
|
+
if (Date.now() < end)
|
|
4306
|
+
await m.untilIdle(t, end - Date.now());
|
|
4111
4307
|
const reply = (m.replyToPeer(t, "", at, turn)?.text ?? "").trim();
|
|
4112
4308
|
if (reply && (await m.idleNow(t))) {
|
|
4113
|
-
if (!isQuiet(reply))
|
|
4114
|
-
|
|
4309
|
+
if (!isQuiet(reply)) {
|
|
4310
|
+
const eventId = `quiet:${t}:${turn}`;
|
|
4311
|
+
const revision = finalId(eventId, base, reply);
|
|
4312
|
+
events.delivery({ id: finalId(eventId, base, revision), eventId, target: t, turn, channel: base,
|
|
4313
|
+
revision, body: reply, chunks: m.formatFinal(t, turn, base, reply) });
|
|
4314
|
+
}
|
|
4115
4315
|
log.info({ target: t, quiet: isQuiet(reply), len: reply.length }, "quiet task: 一轮结束");
|
|
4116
4316
|
quietRelays.drop(turn);
|
|
4117
4317
|
return;
|
|
@@ -4119,10 +4319,10 @@ const main = async () => {
|
|
|
4119
4319
|
await sleep(5000);
|
|
4120
4320
|
}
|
|
4121
4321
|
log.warn({ target: t }, "quiet task: 等满仍没收口, 不转发");
|
|
4122
|
-
quietRelays.
|
|
4322
|
+
quietRelays.set(turn, { target: t, at, base, reason: "deadline passed; final not observed, retained for reconciliation" });
|
|
4123
4323
|
};
|
|
4124
4324
|
// 落盘: 这个仓库里 reload 是常态, 守在内存里的那一轮一 reload 就既不转发也不说 QUIET 了。
|
|
4125
|
-
const quietRelays = loadJsonMap(join(cfg.daemon.stateDir, "quiet-relays.json"),
|
|
4325
|
+
const quietRelays = loadJsonMap(join(cfg.daemon.stateDir, "quiet-relays.json"), undefined, { strict: true });
|
|
4126
4326
|
void m.restored.then(() => Object.entries(quietRelays.all()).forEach(([turn, r]) => void relayUnlessQuiet(r.target, r.at, turn, r.base)));
|
|
4127
4327
|
/** 到点起一个白板 wizard 执行这一枪, 跑完自动收掉。 */
|
|
4128
4328
|
const runTaskInRunner = async (target, text, taskId, quiet, busy, guard) => {
|
|
@@ -4171,6 +4371,8 @@ const main = async () => {
|
|
|
4171
4371
|
}
|
|
4172
4372
|
const at = Date.now();
|
|
4173
4373
|
const turn = newTurn();
|
|
4374
|
+
if (quiet)
|
|
4375
|
+
quietRelays.set(turn, { target: runner, at, base });
|
|
4174
4376
|
const inj = await m.injectText(runner, text, undefined, quiet
|
|
4175
4377
|
? { guard: allowed, from: { kind: "task", taskId }, channel: "", envelope: renderTaskEnvelope(taskId, { turn }) }
|
|
4176
4378
|
: { guard: allowed, fromChat: true, from: { kind: "task", taskId }, envelope: renderTaskEnvelope(taskId) });
|
|
@@ -4304,6 +4506,7 @@ const main = async () => {
|
|
|
4304
4506
|
}
|
|
4305
4507
|
const shutdown = async (signal) => {
|
|
4306
4508
|
log.info({ signal }, "shutdown signal");
|
|
4509
|
+
events.stop();
|
|
4307
4510
|
stopReceipts();
|
|
4308
4511
|
scheduler.stop();
|
|
4309
4512
|
steward?.stop();
|