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.
@@ -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, undefined, { strict: true });
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 io.resource(e.target);
100
+ return resourceAt(e.target);
57
101
  if (e.sessionId)
58
- return e.sessionId === sessionId ? io.resource(e.target) : `session:${e.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 ? io.resource(e.target) : `transcript:${e.transcript}`;
61
- return io.resource(e.target);
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 = db.get(id);
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) => db.set(id, { ...db.get(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 = Object.values(db.all()).filter((e) => !["complete", "canceled"].includes(e.execution) && select(e));
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 old = db.get(input.id);
107
- if (old && old.fingerprint !== fingerprint)
154
+ const prior = db.get(input.id);
155
+ if (prior && prior.fingerprint !== fingerprint)
108
156
  throw new Error(`event identity conflict: ${input.id}`);
109
- let e = old;
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 = db.get(id);
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?.(db.get(id), { text: body });
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 = db.get(e.id);
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(db.get(e.id));
241
+ io.committed(live(e.id));
191
242
  if (evidence.body !== undefined && evidence.error) {
192
243
  // CLI 报错收尾: 那段报错不是定论 (§5), 交路由表落中途态 —— 事件本身不动, 它的执行态
193
244
  // 自有 deadline / 续跑那条路管 (与改动前一致)。
194
- io.final?.(db.get(e.id), { text: evidence.body, error: true });
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(db.get(e.id));
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 = db.get(id);
259
+ const e = live(id);
209
260
  if (!e || !isRcpt(e))
210
261
  return;
211
- const absorbed = Object.values(db.all())
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(db.get(id).target);
275
+ targets.add(target);
224
276
  try {
225
277
  absorb(id);
226
- const e = db.get(id);
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 = db.get(id);
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(db.get(id), boundary);
296
+ r = await io.inject(live(id), boundary);
245
297
  }
246
298
  catch (error) {
247
- r = { ok: false, uncertain: db.get(id).exposure !== "none", retryable: db.get(id).exposure === "none", reason: why(error) };
299
+ r = { ok: false, uncertain: live(id).exposure !== "none", retryable: live(id).exposure === "none", reason: why(error) };
248
300
  }
249
- const latest = db.get(id);
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(db.get(id).attempts) });
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(db.get(id).target);
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
- const pinOf = (e) => {
295
- const rows = Object.values(db.all()).sort((a, b) => a.at - b.at);
296
- const resource = io.resource(e.target);
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 && resourceOf(x) === resource
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(io.resource(e.target)) && (direct(e) || active.size < 4)
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 = db.get(id);
310
- if (!e || e.execution !== "queued")
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
- const queue = (id) => {
319
- const e = db.get(id);
320
- if (!e)
321
- return undefined;
322
- const resource = io.resource(e.target);
323
- const ahead = Object.values(db.all())
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 = db.get(id);
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
- // Scan the first unfinished part; later pending parts cannot jump an uncertain predecessor.
359
- Object.values(outbox.all()).forEach((d) => d.parts.forEach((p, n) => {
360
- if (!["waiting-ack", "uncertain"].includes(p.state))
361
- return;
362
- const evidence = io.deliveryEvidence?.(d, n);
363
- if (evidence)
364
- part(d.id, n, { state: evidence.state === "queued" ? "waiting-ack" : evidence.state, ack: evidence.ack, reason: evidence.reason });
365
- }));
366
- const finals = Object.values(outbox.all());
367
- const rotated = [...finals.slice(deliveryCursor), ...finals.slice(0, deliveryCursor)];
368
- rotated.filter((d) => {
369
- const p = d.parts.find((x) => x.state !== "delivered");
370
- return !delivering.has(d.id) && p?.state === "pending" && (p.retryAt ?? 0) <= Date.now();
371
- }).slice(0, Math.max(0, 4 - delivering.size)).forEach((d) => {
372
- deliveryCursor = (finals.findIndex((x) => x.id === d.id) + 1) % Math.max(1, finals.length);
373
- void send(d.id).catch(io.error);
374
- });
375
- const rows = Object.values(db.all()).sort((a, b) => a.at - b.at);
376
- [...holding.keys()].filter((id) => db.get(id)?.execution !== "queued").forEach((id) => holding.delete(id));
377
- const fair = [...rows.slice(probeCursor), ...rows.slice(0, probeCursor)];
378
- fair.forEach((e) => {
379
- if (stopped)
380
- return;
381
- // Expiration and permission withdrawal are ledger work; a busy or hung
382
- // probe on this pane must not prevent queued messages from expiring.
383
- if (e.execution === "queued" && (e.deadline <= Date.now() || !io.valid(e))) {
384
- cancel((x) => x.id === e.id, e.deadline <= Date.now() ? "deadline expired before submission" : "permission withdrawn");
385
- return;
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
- io.error(error);
419
- };
420
- if (["awaiting-result", "uncertain"].includes(e.execution)) {
421
- if (e.execution === "uncertain" && (e.turnClosedAt || e.releasedAt))
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
- if ((e.retryAt ?? 0) > Date.now())
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
- const currentSession = io.sessionId(e.target);
426
- const currentTranscript = io.transcript(e.target);
427
- const replaced = e.exposure !== "none" && (e.sessionId
428
- ? e.sessionId !== currentSession : !!e.transcript && e.transcript !== currentTranscript);
429
- if (replaced && !e.supersededBy)
430
- patch(e.id, { supersededBy: currentSession ?? (currentTranscript ? `transcript:${currentTranscript}` : "replacement-session-unavailable"),
431
- reason: `${e.reason ? `${e.reason}; ` : ""}session replaced; old execution remains uncertain pending transcript reconciliation` });
432
- void observe(e).then(() => {
433
- if (!stopped && e.deadline <= Date.now() && db.get(e.id).execution === "awaiting-result")
434
- patch(e.id, { execution: "uncertain", reason: "deadline exceeded; result/physical completion not confirmed" });
435
- }).catch(failed);
436
- return;
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
- patch(e.id, { finalized: true });
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
- return;
451
- }
452
- if (e.execution !== "queued")
453
- return;
454
- // 原因里不带重试时刻: 每次退避时刻都在变, 带上就成了「原因变了」, 日志每轮重报。
455
- if ((e.retryAt ?? 0) > Date.now()) {
456
- hold(e, `投递失败退避中${e.reason ? `(${e.reason})` : ""}`);
457
- return;
458
- }
459
- if (!injectable(e)) {
460
- hold(e, `injectable=false: ${queue(e.id)?.holds.join("; ") || "无效 / 过期"}`);
461
- return;
462
- }
463
- void bounded(e.id, () => io.ready(e)).then((ready) => {
464
- const current = db.get(e.id);
465
- if (ready && injectable(current))
466
- void inject(e.id, io.resource(current.target));
467
- else if (current.execution === "queued")
468
- hold(current, ready ? "injectable=false (ready 之后)" : "ready=false (前置未落定 / owner 未判闲)");
469
- }).catch(failed);
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
- Object.values(db.all()).filter((e) => e.execution === "dispatching").forEach((e) => patch(e.id, {
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 (db.get(id))
578
+ bind: (id, turn) => { if (live(id))
497
579
  patch(id, { turn }); },
498
- finished: (id) => { const e = db.get(id); if (e && !e.canceled && !e.result)
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 = db.get(id); if (e?.canceled)
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) => Object.values(db.all()).filter((e) => e.target === target && e.canceled).forEach((e) => patch(e.id, { execution: "canceled", 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 = db.get(id);
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 = db.get(id); return e && clone(e); },
519
- status: () => ({ events: Object.values(db.all()).map((e) => ({ ...clone(e),
520
- status: e.execution === "dispatching" ? "executing" : e.execution,
521
- ...(e.execution === "queued" && blocker(e.id) ? { blockedBy: blocker(e.id) } : {}),
522
- // 回执 / 派活排着时把扣住它的每一条都摆出来 (含 ready=false 这种 queue() 看不见的)。
523
- ...(e.execution === "queued" && !direct(e) ? { holds: [...queue(e.id).holds,
524
- ...(holding.get(e.id) ? [`${holding.get(e.id).why} · 已扣 ${Math.round((Date.now() - holding.get(e.id).since) / 1000)}s`] : [])] } : {}),
525
- delivery: (() => {
526
- const parts = Object.values(outbox.all()).filter((d) => d.eventId === e.id).flatMap((d) => d.parts);
527
- return parts.some((p) => p.state === "failed") ? "failed" : parts.some((p) => p.state === "uncertain") ? "uncertain" : parts.some((p) => p.state !== "delivered") ? "delivery-pending" : parts.length ? "delivered" : "none";
528
- })(), })), deliveries: Object.values(outbox.all()).map(clone) }),
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 = db.get(id); if (e)
531
- await observe(e); return db.get(id); },
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