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
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { loadJsonMap } from "../shared/json-map-store.js";
|
|
3
|
+
const TOMB_AFTER_MS = 24 * 3600_000;
|
|
4
|
+
const TOMB_TTL_MS = 7 * 24 * 3600_000;
|
|
5
|
+
const isTomb = (x) => x.tomb === true;
|
|
6
|
+
const terminal = (x) => x === "complete" || x === "canceled";
|
|
7
|
+
/** complete 要等 finalized (路由判定补完) 才算了结; canceled 本身就是了结。 */
|
|
8
|
+
const settled = (e) => e.execution === "canceled" || (e.execution === "complete" && !!e.finalized);
|
|
9
|
+
const endedAt = (e) => e.endedAt ?? Math.max(e.at, e.result?.at ?? 0, e.turnClosedAt ?? 0, e.releasedAt ?? 0);
|
|
10
|
+
const entomb = (e) => ({ id: e.id, fingerprint: e.fingerprint, execution: e.execution, endedAt: endedAt(e), tomb: true });
|
|
11
|
+
/** 每次落盘前跑: 了结满一天的行裁成墓碑, 墓碑满七天删掉。 */
|
|
12
|
+
export const gcEvents = (now) => (rows) => Object.fromEntries(Object.entries(rows).flatMap(([id, x]) => isTomb(x) ? (now - x.endedAt >= TOMB_TTL_MS ? [] : [[id, x]])
|
|
13
|
+
: settled(x) && terminal(x.execution) && now - endedAt(x) >= TOMB_AFTER_MS ? [[id, entomb({ ...x, execution: x.execution })]]
|
|
14
|
+
: [[id, x]]));
|
|
3
15
|
const HOLD_LOG_MS = 30_000;
|
|
4
16
|
const HOLD_RELOG_MS = 5 * 60_000;
|
|
5
17
|
const canonical = (x) => Array.isArray(x) ? x.map(canonical)
|
|
@@ -15,9 +27,14 @@ export const rejectedAck = (e) => !!e && typeof e === "object"
|
|
|
15
27
|
&& typeof e.errcode === "number" && e.errcode !== 0
|
|
16
28
|
&& typeof e.errmsg === "string" && !!e.headers;
|
|
17
29
|
export const createMessageEvents = (file, io) => {
|
|
18
|
-
const db = loadJsonMap(file,
|
|
30
|
+
const db = loadJsonMap(file, (rows) => gcEvents(Date.now())(rows), { strict: true });
|
|
19
31
|
const outbox = loadJsonMap(`${file}.finals`, undefined, { strict: true });
|
|
20
32
|
Object.entries(db.all()).forEach(([id, e]) => {
|
|
33
|
+
if (e && isTomb(e)) {
|
|
34
|
+
if (e.id !== id || typeof e.fingerprint !== "string" || !terminal(e.execution) || !Number.isFinite(e.endedAt))
|
|
35
|
+
throw new Error(`invalid message event tombstone ${id}`);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
21
38
|
if (!e || e.id !== id || typeof e.fingerprint !== "string" || !e.target || typeof e.speaker !== "string" || typeof e.channel !== "string" || (e.serves !== undefined && typeof e.serves !== "string") || !e.payload || typeof e.payload.text !== "string" || (!e.payload.human === !e.payload.peer) || !Number.isFinite(e.at) || !Number.isFinite(e.attempts) || !Number.isFinite(e.deadline) || !["queued", "dispatching", "awaiting-result", "complete", "canceled", "uncertain"].includes(e.execution))
|
|
22
39
|
throw new Error(`invalid message event ${id}`);
|
|
23
40
|
});
|
|
@@ -25,8 +42,12 @@ export const createMessageEvents = (file, io) => {
|
|
|
25
42
|
if (!d || d.id !== id || !Array.isArray(d.parts) || d.parts.length !== d.chunks?.length || !d.chunks.every((c) => typeof c === "string") || !d.parts.every((p) => ["pending", "sending", "waiting-ack", "delivered", "uncertain", "failed"].includes(p.state)) || !d.channel)
|
|
26
43
|
throw new Error(`invalid final delivery ${id}`);
|
|
27
44
|
});
|
|
45
|
+
/** 调度只看活行; 墓碑只给 accept 判重 (与 `known`)。 */
|
|
46
|
+
const live = (id) => { const x = db.get(id); return x && !isTomb(x) ? x : undefined; };
|
|
47
|
+
const liveRows = () => Object.values(db.all()).filter((x) => !isTomb(x));
|
|
28
48
|
let stopped = true;
|
|
29
49
|
let accepting = true;
|
|
50
|
+
let draining = false;
|
|
30
51
|
let pumping = false;
|
|
31
52
|
let timer;
|
|
32
53
|
const active = new Set();
|
|
@@ -47,21 +68,44 @@ export const createMessageEvents = (file, io) => {
|
|
|
47
68
|
if (due)
|
|
48
69
|
io.held?.(e, why, now - h.since);
|
|
49
70
|
};
|
|
71
|
+
/** 一次同步扫描 (pump 的一轮 / status 的一次) 内, 三个适配器按 target 只问一次: 它们读注册表、
|
|
72
|
+
* 读绑定, 每行现问就是 O(行数) 次 I/O, 而 pinOf / queue 又对每行扫全表 —— 那就是 O(R²)。
|
|
73
|
+
* 同一次同步调用里 pane / 会话本来不会变, 快照反而让一次扫描内各行口径一致。扫描外 (异步
|
|
74
|
+
* 回调、注入边界) 不在 scope 里, 照旧现问。 */
|
|
75
|
+
let pass;
|
|
76
|
+
const scan = (run) => {
|
|
77
|
+
if (pass)
|
|
78
|
+
return run();
|
|
79
|
+
pass = new Map();
|
|
80
|
+
try {
|
|
81
|
+
return run();
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
pass = undefined;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
const ask = (kind, target) => {
|
|
88
|
+
const key = `${kind}\0${target}`;
|
|
89
|
+
if (pass?.has(key))
|
|
90
|
+
return pass.get(key);
|
|
91
|
+
const v = io[kind](target);
|
|
92
|
+
pass?.set(key, v);
|
|
93
|
+
return v;
|
|
94
|
+
};
|
|
95
|
+
const resourceAt = (target) => ask("resource", target);
|
|
50
96
|
// An exposed event belongs to the session that received it, not a replacement
|
|
51
97
|
// pane now bound to the same wizard. Keep its uncertain history for reconciliation.
|
|
52
98
|
const resourceOf = (e) => {
|
|
53
|
-
const transcript = io.transcript(e.target);
|
|
54
|
-
const sessionId = io.sessionId(e.target);
|
|
55
99
|
if (e.exposure === "none")
|
|
56
|
-
return
|
|
100
|
+
return resourceAt(e.target);
|
|
57
101
|
if (e.sessionId)
|
|
58
|
-
return e.sessionId === sessionId ?
|
|
102
|
+
return e.sessionId === ask("sessionId", e.target) ? resourceAt(e.target) : `session:${e.sessionId}`;
|
|
59
103
|
if (e.transcript)
|
|
60
|
-
return e.transcript === transcript ?
|
|
61
|
-
return
|
|
104
|
+
return e.transcript === ask("transcript", e.target) ? resourceAt(e.target) : `transcript:${e.transcript}`;
|
|
105
|
+
return resourceAt(e.target);
|
|
62
106
|
};
|
|
63
107
|
const bounded = (id, read) => {
|
|
64
|
-
const e =
|
|
108
|
+
const e = live(id);
|
|
65
109
|
const target = e ? resourceOf(e) : id;
|
|
66
110
|
if (probing.has(id) || probeTargets.has(target) || probeCount >= 16)
|
|
67
111
|
return Promise.reject(new Error("probe already running or probe capacity occupied"));
|
|
@@ -82,13 +126,17 @@ export const createMessageEvents = (file, io) => {
|
|
|
82
126
|
timer = setTimeout(() => reject(new Error("pane probe exceeded 5s; no transport attempted")), 5000);
|
|
83
127
|
})]).finally(() => { clearTimeout(timer); probeCount--; release(); });
|
|
84
128
|
};
|
|
85
|
-
const patch = (id, p) =>
|
|
129
|
+
const patch = (id, p) => {
|
|
130
|
+
const prev = live(id);
|
|
131
|
+
const next = { ...prev, ...p };
|
|
132
|
+
return db.set(id, terminal(next.execution) && !terminal(prev.execution) ? { ...next, endedAt: Date.now() } : next);
|
|
133
|
+
};
|
|
86
134
|
const part = (id, n, p) => {
|
|
87
135
|
const d = outbox.get(id);
|
|
88
136
|
outbox.set(id, { ...d, parts: d.parts.map((x, i) => i === n ? { ...x, ...p } : x) });
|
|
89
137
|
};
|
|
90
138
|
const cancel = (select, reason = "canceled by owner") => {
|
|
91
|
-
const rows =
|
|
139
|
+
const rows = liveRows().filter((e) => !["complete", "canceled"].includes(e.execution) && select(e));
|
|
92
140
|
rows.forEach((e) => {
|
|
93
141
|
const next = patch(e.id, { canceled: true, reason, execution: e.exposure === "none" ? "canceled" : "uncertain" });
|
|
94
142
|
io.canceled(next); // Cancellation after exposure never asserts the pane stopped.
|
|
@@ -103,10 +151,13 @@ export const createMessageEvents = (file, io) => {
|
|
|
103
151
|
// 内容就该是同一条消息, 哪怕 serves 的来历变了 —— 它不算内容的一部分。
|
|
104
152
|
const { serves: _routing, ...identified } = clean;
|
|
105
153
|
const fingerprint = digest(identified.identity ? [JSON.parse(identified.identity), identified.speaker, identified.channel] : identified);
|
|
106
|
-
const
|
|
107
|
-
if (
|
|
154
|
+
const prior = db.get(input.id);
|
|
155
|
+
if (prior && prior.fingerprint !== fingerprint)
|
|
108
156
|
throw new Error(`event identity conflict: ${input.id}`);
|
|
109
|
-
|
|
157
|
+
// 墓碑 = 早已投过并了结: 重放 / 重入箱到此为止, 不复活、不再注入。
|
|
158
|
+
if (prior && isTomb(prior))
|
|
159
|
+
return { ok: true, queued: false, eventId: prior.id };
|
|
160
|
+
let e = prior;
|
|
110
161
|
if (!e) {
|
|
111
162
|
e = { ...clean, fingerprint, at: Date.now(), execution: "queued", exposure: "none", attempts: 0 };
|
|
112
163
|
db.merge({ [e.id]: e });
|
|
@@ -124,11 +175,11 @@ export const createMessageEvents = (file, io) => {
|
|
|
124
175
|
return { ok: true, queued: e.execution === "queued", eventId: e.id };
|
|
125
176
|
};
|
|
126
177
|
const result = (id, body, revision, turn) => {
|
|
127
|
-
const e =
|
|
178
|
+
const e = live(id);
|
|
128
179
|
if (!e || e.result?.revision === revision)
|
|
129
180
|
return;
|
|
130
181
|
patch(id, { finalized: false, result: { body, revision, at: Date.now() }, execution: "complete", exposure: "consumed", ...(turn ? { turn } : {}) });
|
|
131
|
-
io.final?.(
|
|
182
|
+
io.final?.(live(id), { text: body });
|
|
132
183
|
};
|
|
133
184
|
const delivery = (input) => {
|
|
134
185
|
const old = outbox.get(input.id);
|
|
@@ -175,7 +226,7 @@ export const createMessageEvents = (file, io) => {
|
|
|
175
226
|
return;
|
|
176
227
|
if (!evidence.consumed)
|
|
177
228
|
return;
|
|
178
|
-
const current =
|
|
229
|
+
const current = live(e.id);
|
|
179
230
|
if (["complete", "canceled"].includes(current.execution))
|
|
180
231
|
return;
|
|
181
232
|
if (current.exposure !== "consumed" || (evidence.closedAt && !current.turnClosedAt))
|
|
@@ -187,15 +238,15 @@ export const createMessageEvents = (file, io) => {
|
|
|
187
238
|
...(evidence.closedAt ? { turnClosedAt: evidence.closedAt } : {}), turn: evidence.turn ?? current.turn,
|
|
188
239
|
});
|
|
189
240
|
if (!current.canceled)
|
|
190
|
-
io.committed(
|
|
241
|
+
io.committed(live(e.id));
|
|
191
242
|
if (evidence.body !== undefined && evidence.error) {
|
|
192
243
|
// CLI 报错收尾: 那段报错不是定论 (§5), 交路由表落中途态 —— 事件本身不动, 它的执行态
|
|
193
244
|
// 自有 deadline / 续跑那条路管 (与改动前一致)。
|
|
194
|
-
io.final?.(
|
|
245
|
+
io.final?.(live(e.id), { text: evidence.body, error: true });
|
|
195
246
|
}
|
|
196
247
|
else if (evidence.body !== undefined && evidence.revision) {
|
|
197
248
|
result(e.id, evidence.body, evidence.revision, evidence.turn);
|
|
198
|
-
io.recovered(
|
|
249
|
+
io.recovered(live(e.id));
|
|
199
250
|
}
|
|
200
251
|
};
|
|
201
252
|
/** 收件箱里的回执事件 (`rcpt:<event>:<rev>`): 只有它带路由归属 (serves), 也只有它该合并。 */
|
|
@@ -205,10 +256,10 @@ export const createMessageEvents = (file, io) => {
|
|
|
205
256
|
* (`execution: complete`, 它们的 id 已终态, 重放不再注入); 结论由这一轮的终句统一交代。
|
|
206
257
|
* 回执的正文与信封是分开的两件 (`payload.text` 不含信封), 所以并的只是正文, 信封留注入那一份。 */
|
|
207
258
|
const absorb = (id) => {
|
|
208
|
-
const e =
|
|
259
|
+
const e = live(id);
|
|
209
260
|
if (!e || !isRcpt(e))
|
|
210
261
|
return;
|
|
211
|
-
const absorbed =
|
|
262
|
+
const absorbed = liveRows()
|
|
212
263
|
.filter((x) => x.id !== e.id && isRcpt(x) && x.target === e.target && x.serves === e.serves
|
|
213
264
|
&& x.execution === "queued" && !x.canceled)
|
|
214
265
|
.sort((a, b) => a.at - b.at || (a.id < b.id ? -1 : 1));
|
|
@@ -219,17 +270,18 @@ export const createMessageEvents = (file, io) => {
|
|
|
219
270
|
absorbed.forEach((x) => patch(x.id, { execution: "complete", exposure: "consumed", finalized: true, reason: `merged into ${e.id}` }));
|
|
220
271
|
};
|
|
221
272
|
const inject = async (id, resource) => {
|
|
273
|
+
const target = live(id).target;
|
|
222
274
|
active.add(resource);
|
|
223
|
-
targets.add(
|
|
275
|
+
targets.add(target);
|
|
224
276
|
try {
|
|
225
277
|
absorb(id);
|
|
226
|
-
const e =
|
|
278
|
+
const e = live(id);
|
|
227
279
|
if (stopped || e.execution !== "queued" || !io.valid(e))
|
|
228
280
|
return;
|
|
229
281
|
io.accepted(e);
|
|
230
282
|
patch(id, { execution: "dispatching", attempts: e.attempts + 1, transcript: io.transcript(e.target) ?? e.transcript });
|
|
231
283
|
const boundary = (phase, identity) => {
|
|
232
|
-
const current =
|
|
284
|
+
const current = live(id);
|
|
233
285
|
if (stopped || current.canceled || !io.valid(current))
|
|
234
286
|
return false;
|
|
235
287
|
if (current.exposure !== "none" && current.transcript && current.transcript !== io.transcript(e.target))
|
|
@@ -241,12 +293,12 @@ export const createMessageEvents = (file, io) => {
|
|
|
241
293
|
};
|
|
242
294
|
let r;
|
|
243
295
|
try {
|
|
244
|
-
r = await io.inject(
|
|
296
|
+
r = await io.inject(live(id), boundary);
|
|
245
297
|
}
|
|
246
298
|
catch (error) {
|
|
247
|
-
r = { ok: false, uncertain:
|
|
299
|
+
r = { ok: false, uncertain: live(id).exposure !== "none", retryable: live(id).exposure === "none", reason: why(error) };
|
|
248
300
|
}
|
|
249
|
-
const latest =
|
|
301
|
+
const latest = live(id);
|
|
250
302
|
if (latest.result || latest.execution === "complete") {
|
|
251
303
|
if (!latest.canceled)
|
|
252
304
|
io.committed(latest);
|
|
@@ -257,6 +309,10 @@ export const createMessageEvents = (file, io) => {
|
|
|
257
309
|
if (!latest.canceled)
|
|
258
310
|
io.committed(committed);
|
|
259
311
|
}
|
|
312
|
+
else if (r.gone && !r.uncertain && latest.exposure === "none" && !latest.canceled) {
|
|
313
|
+
// 收方真没了 (绑定按 store 也恢复不了): 退避重投只会空转到期限, 收成 canceled 交给路由层上浮。
|
|
314
|
+
io.gone?.(patch(id, { execution: "canceled", reason: r.reason || "target gone" }));
|
|
315
|
+
}
|
|
260
316
|
else if (r.retryable && !r.uncertain && !latest.canceled) {
|
|
261
317
|
patch(id, { execution: "queued", exposure: "none", retryAt: backoff(latest.attempts), reason: r.reason });
|
|
262
318
|
}
|
|
@@ -265,7 +321,7 @@ export const createMessageEvents = (file, io) => {
|
|
|
265
321
|
}
|
|
266
322
|
catch (e) {
|
|
267
323
|
try {
|
|
268
|
-
patch(id, { reason: why(e), retryAt: backoff(
|
|
324
|
+
patch(id, { reason: why(e), retryAt: backoff(live(id).attempts) });
|
|
269
325
|
}
|
|
270
326
|
catch {
|
|
271
327
|
accepting = false;
|
|
@@ -277,7 +333,7 @@ export const createMessageEvents = (file, io) => {
|
|
|
277
333
|
}
|
|
278
334
|
finally {
|
|
279
335
|
active.delete(resource);
|
|
280
|
-
targets.delete(
|
|
336
|
+
targets.delete(target);
|
|
281
337
|
void pump();
|
|
282
338
|
}
|
|
283
339
|
};
|
|
@@ -291,38 +347,36 @@ export const createMessageEvents = (file, io) => {
|
|
|
291
347
|
* `retryAt` 未到的行不算挡住 —— 它在退避, 不该把整条 pane 的后来者一起钉住: 队头一次
|
|
292
348
|
* 探测超时 (5s) 就能把一个 target 静音十几分钟, 人话排在后面时是纯损失。
|
|
293
349
|
* injectable 与 blocker 共用这一条 —— 判据分家过一次, 表现是「对外说没人挡着, 实际谁也进不去」。 */
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
const
|
|
350
|
+
// 先判执行态 (纯字段), 再问 resourceOf: 表里九成多是终态行, 不该替它们付 I/O。
|
|
351
|
+
const pinOf = (e) => scan(() => {
|
|
352
|
+
const rows = liveRows().sort((a, b) => a.at - b.at);
|
|
353
|
+
const resource = resourceAt(e.target);
|
|
297
354
|
const older = new Set(rows.slice(0, rows.findIndex((x) => x.id === e.id)).map((x) => x.id));
|
|
298
|
-
return rows.find((x) => x.id !== e.id
|
|
355
|
+
return rows.find((x) => x.id !== e.id
|
|
299
356
|
&& (x.execution === "dispatching"
|
|
300
357
|
|| (!direct(e) && ((x.execution === "uncertain" && !x.releasedAt)
|
|
301
358
|
|| (e.priority === "normal" && (x.execution === "awaiting-result"
|
|
302
|
-
|| (x.execution === "queued" && (x.retryAt ?? 0) <= Date.now() && older.has(x.id)))))))
|
|
303
|
-
|
|
359
|
+
|| (x.execution === "queued" && (x.retryAt ?? 0) <= Date.now() && older.has(x.id)))))))
|
|
360
|
+
&& resourceOf(x) === resource);
|
|
361
|
+
});
|
|
304
362
|
/** 全局 4 路并发只限回执 / 派活: 人话之间、人话与别的 pane 都互不相干, 只认同 pane 物理互斥。 */
|
|
305
363
|
const injectable = (e) => !stopped && e.execution === "queued" && !e.canceled && e.deadline > Date.now() && io.valid(e)
|
|
306
|
-
&& !targets.has(e.target) && !active.has(
|
|
364
|
+
&& !targets.has(e.target) && !active.has(resourceAt(e.target)) && (direct(e) || active.size < 4)
|
|
307
365
|
&& pinOf(e) === undefined;
|
|
366
|
+
const blockerOf = (hit) => hit && { id: hit.id, execution: hit.execution,
|
|
367
|
+
reason: hit.reason ?? (hit.execution === "uncertain" ? "旧事件投递结果不确定,需对账" : `等待较早事件(${hit.execution})`) };
|
|
308
368
|
const blocker = (id) => {
|
|
309
|
-
const e =
|
|
310
|
-
|
|
311
|
-
return undefined;
|
|
312
|
-
const hit = pinOf(e);
|
|
313
|
-
return hit ? { id: hit.id, execution: hit.execution,
|
|
314
|
-
reason: hit.reason ?? (hit.execution === "uncertain" ? "旧事件投递结果不确定,需对账" : `等待较早事件(${hit.execution})`) } : undefined;
|
|
369
|
+
const e = live(id);
|
|
370
|
+
return e?.execution === "queued" ? blockerOf(pinOf(e)) : undefined;
|
|
315
371
|
};
|
|
316
372
|
/** 一条还没贴出去的 event 此刻的排队全貌: 同 pane 上排在它前面的未了结行, 以及 injectable
|
|
317
|
-
* 的每一条否决项各自落在哪 —— 给人看「卡在谁后面、为什么、几时再试」, 不是一句「已排队」。
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
.filter((x) => x.id !== e.id && x.at <= e.at && resourceOf(x) === resource
|
|
325
|
-
&& ["queued", "dispatching", "awaiting-result"].includes(x.execution) && !x.canceled)
|
|
373
|
+
* 的每一条否决项各自落在哪 —— 给人看「卡在谁后面、为什么、几时再试」, 不是一句「已排队」。
|
|
374
|
+
* `pin` 一并交回, 让 status 不必为 blocker 再扫一遍全表。 */
|
|
375
|
+
const queueOf = (e) => scan(() => {
|
|
376
|
+
const resource = resourceAt(e.target);
|
|
377
|
+
const ahead = liveRows()
|
|
378
|
+
.filter((x) => x.id !== e.id && x.at <= e.at && ["queued", "dispatching", "awaiting-result"].includes(x.execution) && !x.canceled
|
|
379
|
+
&& resourceOf(x) === resource)
|
|
326
380
|
.sort((a, b) => a.at - b.at);
|
|
327
381
|
const pin = pinOf(e);
|
|
328
382
|
const holds = [
|
|
@@ -332,10 +386,33 @@ export const createMessageEvents = (file, io) => {
|
|
|
332
386
|
!direct(e) && active.size >= 4 ? `全局同时投递已满 ${active.size} 路` : "",
|
|
333
387
|
pin ? `挡在前面: ${pin.id.slice(0, 10)}(${pin.execution}${pin.reason ? ` · ${pin.reason}` : ""})` : "",
|
|
334
388
|
].filter(Boolean);
|
|
335
|
-
return { execution: e.execution, ahead: ahead.map(clone), holds };
|
|
389
|
+
return { execution: e.execution, ahead: ahead.map(clone), holds, pin };
|
|
390
|
+
});
|
|
391
|
+
const queue = (id) => {
|
|
392
|
+
const e = live(id);
|
|
393
|
+
if (!e)
|
|
394
|
+
return undefined;
|
|
395
|
+
const { pin: _pin, ...q } = queueOf(e);
|
|
396
|
+
return q;
|
|
397
|
+
};
|
|
398
|
+
const partsByEvent = () => Object.values(outbox.all()).reduce((m, d) => d.eventId ? m.set(d.eventId, [...(m.get(d.eventId) ?? []), ...d.parts]) : m, new Map());
|
|
399
|
+
const deliveryOf = (parts) => parts.some((p) => p.state === "failed") ? "failed"
|
|
400
|
+
: parts.some((p) => p.state === "uncertain") ? "uncertain" : parts.some((p) => p.state !== "delivered") ? "delivery-pending" : parts.length ? "delivered" : "none";
|
|
401
|
+
/** status 里的一行: 排着的行只扫一次队 (blocker 取 queueOf 交回的 pin)。 */
|
|
402
|
+
const view = (e, parts) => {
|
|
403
|
+
const q = e.execution === "queued" ? queueOf(e) : undefined;
|
|
404
|
+
const blockedBy = blockerOf(q?.pin);
|
|
405
|
+
const held = holding.get(e.id);
|
|
406
|
+
return { ...clone(e),
|
|
407
|
+
status: e.execution === "dispatching" ? "executing" : e.execution,
|
|
408
|
+
...(blockedBy ? { blockedBy } : {}),
|
|
409
|
+
// 回执 / 派活排着时把扣住它的每一条都摆出来 (含 ready=false 这种 queue() 看不见的)。
|
|
410
|
+
...(q && !direct(e) ? { holds: [...q.holds, ...(held ? [`${held.why} · 已扣 ${Math.round((Date.now() - held.since) / 1000)}s`] : [])] } : {}),
|
|
411
|
+
delivery: deliveryOf(parts),
|
|
412
|
+
};
|
|
336
413
|
};
|
|
337
414
|
const release = (id, reason) => {
|
|
338
|
-
const e =
|
|
415
|
+
const e = live(id);
|
|
339
416
|
const note = reason.trim().slice(0, 500);
|
|
340
417
|
if (!e)
|
|
341
418
|
return { ok: false, reason: "unknown event" };
|
|
@@ -355,118 +432,120 @@ export const createMessageEvents = (file, io) => {
|
|
|
355
432
|
return;
|
|
356
433
|
pumping = true;
|
|
357
434
|
try {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
}
|
|
387
|
-
if (["awaiting-result", "uncertain"].includes(e.execution) && e.deadline <= Date.now() && !e.releasedAt) {
|
|
388
|
-
patch(e.id, { execution: "uncertain", releasedAt: Date.now(),
|
|
389
|
-
releaseSource: "deadline",
|
|
390
|
-
releaseReason: "event deadline elapsed; uncertainty retained and automatic retry withheld",
|
|
391
|
-
reason: `${e.reason ? `${e.reason}; ` : ""}event deadline elapsed; queue released with uncertain delivery` });
|
|
392
|
-
return;
|
|
393
|
-
}
|
|
394
|
-
// 人话的 ready 不碰 pane (只看交接闸), 不必占探测位: 否则同 pane 上一条 awaiting-result
|
|
395
|
-
// 每秒一次的 evidence 探测 (最长 5s) 几乎一直占着 probeTargets, 人话被反复跳过或误判失败退避。
|
|
396
|
-
if (e.execution === "queued" && direct(e)) {
|
|
397
|
-
if ((e.retryAt ?? 0) > Date.now() || !injectable(e))
|
|
435
|
+
scan(() => {
|
|
436
|
+
// Scan the first unfinished part; later pending parts cannot jump an uncertain predecessor.
|
|
437
|
+
Object.values(outbox.all()).forEach((d) => d.parts.forEach((p, n) => {
|
|
438
|
+
if (!["waiting-ack", "uncertain"].includes(p.state))
|
|
439
|
+
return;
|
|
440
|
+
const evidence = io.deliveryEvidence?.(d, n);
|
|
441
|
+
if (evidence)
|
|
442
|
+
part(d.id, n, { state: evidence.state === "queued" ? "waiting-ack" : evidence.state, ack: evidence.ack, reason: evidence.reason });
|
|
443
|
+
}));
|
|
444
|
+
const finals = Object.values(outbox.all());
|
|
445
|
+
const rotated = [...finals.slice(deliveryCursor), ...finals.slice(0, deliveryCursor)];
|
|
446
|
+
rotated.filter((d) => {
|
|
447
|
+
const p = d.parts.find((x) => x.state !== "delivered");
|
|
448
|
+
return !delivering.has(d.id) && p?.state === "pending" && (p.retryAt ?? 0) <= Date.now();
|
|
449
|
+
}).slice(0, Math.max(0, 4 - delivering.size)).forEach((d) => {
|
|
450
|
+
deliveryCursor = (finals.findIndex((x) => x.id === d.id) + 1) % Math.max(1, finals.length);
|
|
451
|
+
void send(d.id).catch(io.error);
|
|
452
|
+
});
|
|
453
|
+
const rows = liveRows().sort((a, b) => a.at - b.at);
|
|
454
|
+
[...holding.keys()].filter((id) => live(id)?.execution !== "queued").forEach((id) => holding.delete(id));
|
|
455
|
+
const fair = [...rows.slice(probeCursor), ...rows.slice(0, probeCursor)];
|
|
456
|
+
fair.forEach((e) => {
|
|
457
|
+
if (stopped)
|
|
458
|
+
return;
|
|
459
|
+
// Expiration and permission withdrawal are ledger work; a busy or hung
|
|
460
|
+
// probe on this pane must not prevent queued messages from expiring.
|
|
461
|
+
if (e.execution === "queued" && (e.deadline <= Date.now() || !io.valid(e))) {
|
|
462
|
+
cancel((x) => x.id === e.id, e.deadline <= Date.now() ? "deadline expired before submission" : "permission withdrawn");
|
|
398
463
|
return;
|
|
399
|
-
void io.ready(e).then((ready) => {
|
|
400
|
-
const current = db.get(e.id);
|
|
401
|
-
if (ready && injectable(current))
|
|
402
|
-
void inject(e.id, io.resource(current.target));
|
|
403
|
-
}).catch(io.error);
|
|
404
|
-
return;
|
|
405
|
-
}
|
|
406
|
-
if (probing.has(e.id) || probeTargets.has(resourceOf(e)) || probeCount >= 16) {
|
|
407
|
-
if (e.execution === "queued")
|
|
408
|
-
hold(e, "探测位被占 (同 pane 上另一条正在探测)");
|
|
409
|
-
return;
|
|
410
|
-
}
|
|
411
|
-
probeCursor = (rows.findIndex((x) => x.id === e.id) + 1) % Math.max(1, rows.length);
|
|
412
|
-
const failed = (error) => {
|
|
413
|
-
const current = db.get(e.id);
|
|
414
|
-
if (!stopped && current && !["complete", "canceled"].includes(current.execution)) {
|
|
415
|
-
const probeAttempts = (current.probeAttempts ?? 0) + 1;
|
|
416
|
-
patch(e.id, { probeAttempts, retryAt: backoff(probeAttempts), reason: `${current.reason ? `${current.reason}; ` : ""}${why(error)}` });
|
|
417
464
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
465
|
+
if (["awaiting-result", "uncertain"].includes(e.execution) && e.deadline <= Date.now() && !e.releasedAt) {
|
|
466
|
+
patch(e.id, { execution: "uncertain", releasedAt: Date.now(),
|
|
467
|
+
releaseSource: "deadline",
|
|
468
|
+
releaseReason: "event deadline elapsed; uncertainty retained and automatic retry withheld",
|
|
469
|
+
reason: `${e.reason ? `${e.reason}; ` : ""}event deadline elapsed; queue released with uncertain delivery` });
|
|
422
470
|
return;
|
|
423
|
-
|
|
471
|
+
}
|
|
472
|
+
// 人话的 ready 不碰 pane (只看交接闸), 不必占探测位: 否则同 pane 上一条 awaiting-result
|
|
473
|
+
// 每秒一次的 evidence 探测 (最长 5s) 几乎一直占着 probeTargets, 人话被反复跳过或误判失败退避。
|
|
474
|
+
if (e.execution === "queued" && direct(e)) {
|
|
475
|
+
if ((e.retryAt ?? 0) > Date.now() || !injectable(e))
|
|
476
|
+
return;
|
|
477
|
+
void io.ready(e).then((ready) => {
|
|
478
|
+
const current = live(e.id);
|
|
479
|
+
if (ready && injectable(current))
|
|
480
|
+
void inject(e.id, io.resource(current.target));
|
|
481
|
+
}).catch(io.error);
|
|
424
482
|
return;
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
}
|
|
438
|
-
if (e.execution === "complete") {
|
|
439
|
-
if (!e.finalized) {
|
|
440
|
-
if (!e.canceled)
|
|
441
|
-
io.committed(e);
|
|
442
|
-
if (e.result) {
|
|
443
|
-
io.recovered(e);
|
|
444
|
-
// reload 落在 result 与 finalized 之间: 重放这一刻补上路由判定 (幂等 —— 同一条
|
|
445
|
-
// 定论重放只会在表上再写一次同样的 op)。
|
|
446
|
-
io.final?.(e, { text: e.result.body });
|
|
483
|
+
}
|
|
484
|
+
if (probing.has(e.id) || probeTargets.has(resourceOf(e)) || probeCount >= 16) {
|
|
485
|
+
if (e.execution === "queued")
|
|
486
|
+
hold(e, "探测位被占 (同 pane 上另一条正在探测)");
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
probeCursor = (rows.findIndex((x) => x.id === e.id) + 1) % Math.max(1, rows.length);
|
|
490
|
+
const failed = (error) => {
|
|
491
|
+
const current = live(e.id);
|
|
492
|
+
if (!stopped && current && !["complete", "canceled"].includes(current.execution)) {
|
|
493
|
+
const probeAttempts = (current.probeAttempts ?? 0) + 1;
|
|
494
|
+
patch(e.id, { probeAttempts, retryAt: backoff(probeAttempts), reason: `${current.reason ? `${current.reason}; ` : ""}${why(error)}` });
|
|
447
495
|
}
|
|
448
|
-
|
|
496
|
+
io.error(error);
|
|
497
|
+
};
|
|
498
|
+
if (["awaiting-result", "uncertain"].includes(e.execution)) {
|
|
499
|
+
if (e.execution === "uncertain" && (e.turnClosedAt || e.releasedAt))
|
|
500
|
+
return;
|
|
501
|
+
if ((e.retryAt ?? 0) > Date.now())
|
|
502
|
+
return;
|
|
503
|
+
const currentSession = ask("sessionId", e.target);
|
|
504
|
+
const currentTranscript = ask("transcript", e.target);
|
|
505
|
+
const replaced = e.exposure !== "none" && (e.sessionId
|
|
506
|
+
? e.sessionId !== currentSession : !!e.transcript && e.transcript !== currentTranscript);
|
|
507
|
+
if (replaced && !e.supersededBy)
|
|
508
|
+
patch(e.id, { supersededBy: currentSession ?? (currentTranscript ? `transcript:${currentTranscript}` : "replacement-session-unavailable"),
|
|
509
|
+
reason: `${e.reason ? `${e.reason}; ` : ""}session replaced; old execution remains uncertain pending transcript reconciliation` });
|
|
510
|
+
void observe(e).then(() => {
|
|
511
|
+
if (!stopped && e.deadline <= Date.now() && live(e.id).execution === "awaiting-result")
|
|
512
|
+
patch(e.id, { execution: "uncertain", reason: "deadline exceeded; result/physical completion not confirmed" });
|
|
513
|
+
}).catch(failed);
|
|
514
|
+
return;
|
|
449
515
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
hold(
|
|
469
|
-
|
|
516
|
+
if (e.execution === "complete") {
|
|
517
|
+
if (!e.finalized) {
|
|
518
|
+
if (!e.canceled)
|
|
519
|
+
io.committed(e);
|
|
520
|
+
if (e.result) {
|
|
521
|
+
io.recovered(e);
|
|
522
|
+
// reload 落在 result 与 finalized 之间: 重放这一刻补上路由判定 (幂等 —— 同一条
|
|
523
|
+
// 定论重放只会在表上再写一次同样的 op)。
|
|
524
|
+
io.final?.(e, { text: e.result.body });
|
|
525
|
+
}
|
|
526
|
+
patch(e.id, { finalized: true });
|
|
527
|
+
}
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
if (e.execution !== "queued")
|
|
531
|
+
return;
|
|
532
|
+
// 原因里不带重试时刻: 每次退避时刻都在变, 带上就成了「原因变了」, 日志每轮重报。
|
|
533
|
+
if ((e.retryAt ?? 0) > Date.now()) {
|
|
534
|
+
hold(e, `投递失败退避中${e.reason ? `(${e.reason})` : ""}`);
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
if (!injectable(e)) {
|
|
538
|
+
hold(e, `injectable=false: ${queue(e.id)?.holds.join("; ") || "无效 / 过期"}`);
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
void bounded(e.id, () => io.ready(e)).then((ready) => {
|
|
542
|
+
const current = live(e.id);
|
|
543
|
+
if (ready && injectable(current))
|
|
544
|
+
void inject(e.id, io.resource(current.target));
|
|
545
|
+
else if (current.execution === "queued")
|
|
546
|
+
hold(current, ready ? "injectable=false (ready 之后)" : "ready=false (前置未落定 / owner 未判闲)");
|
|
547
|
+
}).catch(failed);
|
|
548
|
+
});
|
|
470
549
|
});
|
|
471
550
|
}
|
|
472
551
|
catch (e) {
|
|
@@ -477,7 +556,10 @@ export const createMessageEvents = (file, io) => {
|
|
|
477
556
|
}
|
|
478
557
|
}
|
|
479
558
|
const resume = () => {
|
|
480
|
-
|
|
559
|
+
// Boot restoration may finish after shutdown has begun; never restart delivery then.
|
|
560
|
+
if (draining)
|
|
561
|
+
return;
|
|
562
|
+
liveRows().filter((e) => e.execution === "dispatching").forEach((e) => patch(e.id, {
|
|
481
563
|
execution: e.exposure === "none" ? "queued" : "uncertain", reason: "daemon restarted at input boundary; transcript evidence required",
|
|
482
564
|
}));
|
|
483
565
|
Object.values(outbox.all()).forEach((d) => d.parts.forEach((p, n) => {
|
|
@@ -493,16 +575,16 @@ export const createMessageEvents = (file, io) => {
|
|
|
493
575
|
return {
|
|
494
576
|
accept, pump, resume, result, delivery, cancel, blocker, queue, release,
|
|
495
577
|
running: () => !stopped,
|
|
496
|
-
bind: (id, turn) => { if (
|
|
578
|
+
bind: (id, turn) => { if (live(id))
|
|
497
579
|
patch(id, { turn }); },
|
|
498
|
-
finished: (id) => { const e =
|
|
580
|
+
finished: (id) => { const e = live(id); if (e && !e.canceled && !e.result)
|
|
499
581
|
patch(id, { execution: "complete", exposure: "consumed" }); },
|
|
500
|
-
released: (id) => { const e =
|
|
582
|
+
released: (id) => { const e = live(id); if (e?.canceled)
|
|
501
583
|
patch(id, { execution: "canceled", reason: `${e.reason ?? "canceled"}; pane idle confirmed` }); },
|
|
502
|
-
stopped: (target, reason) =>
|
|
584
|
+
stopped: (target, reason) => liveRows().filter((e) => e.target === target && e.canceled).forEach((e) => patch(e.id, { execution: "canceled", reason })),
|
|
503
585
|
submission: async (id) => {
|
|
504
586
|
for (;;) {
|
|
505
|
-
const e =
|
|
587
|
+
const e = live(id);
|
|
506
588
|
if (!e)
|
|
507
589
|
return { ok: false, retryable: true, reason: "event missing" };
|
|
508
590
|
if (["submitted", "consumed"].includes(e.exposure))
|
|
@@ -514,21 +596,24 @@ export const createMessageEvents = (file, io) => {
|
|
|
514
596
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
515
597
|
}
|
|
516
598
|
},
|
|
599
|
+
// Keep ingress durable until WS/HTTP and their in-flight handlers have closed.
|
|
600
|
+
drain: () => { draining = true; stopped = true; clearInterval(timer); timer = undefined; },
|
|
517
601
|
stop: () => { accepting = false; stopped = true; clearInterval(timer); timer = undefined; },
|
|
518
|
-
get: (id) => { const e =
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
602
|
+
get: (id) => { const e = live(id); return e && clone(e); },
|
|
603
|
+
/** 这个 id 用过没有 (含墓碑): 自己配 id 的调用方 (续问序号 / 交接续答) 据此避开撞号。 */
|
|
604
|
+
known: (id) => db.get(id) !== undefined,
|
|
605
|
+
ids: () => Object.keys(db.all()),
|
|
606
|
+
/** 只问执行态 (含墓碑): 判「这份投过没有」的调用方不必在乎它是不是已被裁掉。 */
|
|
607
|
+
executionOf: (id) => db.get(id)?.execution,
|
|
608
|
+
status: () => scan(() => {
|
|
609
|
+
const parts = partsByEvent();
|
|
610
|
+
return { events: liveRows().map((e) => view(e, parts.get(e.id) ?? [])), deliveries: Object.values(outbox.all()).map(clone) };
|
|
611
|
+
}),
|
|
612
|
+
/** 一条 event 的 status 视图 —— 轮询单条的调用方 (回归脚本) 不必为它把全表算一遍。 */
|
|
613
|
+
event: (id) => { const e = live(id); return e && scan(() => view(e, partsByEvent().get(id) ?? [])); },
|
|
529
614
|
deliveryStatus: (id) => outbox.get(id)?.parts.map((p) => p.state),
|
|
530
|
-
reconcile: async (id) => { const e =
|
|
531
|
-
await observe(e); return
|
|
615
|
+
reconcile: async (id) => { const e = live(id); if (e)
|
|
616
|
+
await observe(e); return live(id); },
|
|
532
617
|
};
|
|
533
618
|
};
|
|
534
619
|
//# sourceMappingURL=message-events.js.map
|