@llblab/pi-telegram 0.51.6 → 0.52.1

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.
Files changed (178) hide show
  1. package/AGENTS.md +15 -12
  2. package/BACKLOG.md +16 -9
  3. package/CHANGELOG.md +19 -0
  4. package/LICENSE +21 -0
  5. package/README.md +17 -6
  6. package/dist/lib/activity-verbosity.d.ts +1 -5
  7. package/dist/lib/activity-verbosity.js +5 -7
  8. package/dist/lib/bindings.d.ts +5 -1
  9. package/dist/lib/bindings.js +7 -1
  10. package/dist/lib/bus-api.js +8 -5
  11. package/dist/lib/bus-follower.d.ts +54 -56
  12. package/dist/lib/bus-follower.js +206 -139
  13. package/dist/lib/bus-leader.d.ts +21 -2
  14. package/dist/lib/bus-leader.js +181 -25
  15. package/dist/lib/bus-transport.d.ts +4 -1
  16. package/dist/lib/bus-transport.js +28 -4
  17. package/dist/lib/bus.d.ts +52 -56
  18. package/dist/lib/bus.js +78 -246
  19. package/dist/lib/channel-posts.d.ts +1 -3
  20. package/dist/lib/channel-posts.js +23 -26
  21. package/dist/lib/command-templates.d.ts +0 -5
  22. package/dist/lib/command-templates.js +5 -5
  23. package/dist/lib/commands.d.ts +0 -23
  24. package/dist/lib/commands.js +15 -15
  25. package/dist/lib/config.d.ts +0 -17
  26. package/dist/lib/config.js +12 -14
  27. package/dist/lib/delivery.d.ts +0 -4
  28. package/dist/lib/delivery.js +2 -5
  29. package/dist/lib/extension.js +258 -61
  30. package/dist/lib/generative-apps.d.ts +0 -19
  31. package/dist/lib/inbound.d.ts +0 -2
  32. package/dist/lib/inbound.js +2 -2
  33. package/dist/lib/journal.d.ts +183 -16
  34. package/dist/lib/journal.js +1175 -248
  35. package/dist/lib/keyboard.d.ts +0 -2
  36. package/dist/lib/keyboard.js +2 -2
  37. package/dist/lib/lifecycle.js +1 -1
  38. package/dist/lib/locks.d.ts +100 -12
  39. package/dist/lib/locks.js +380 -14
  40. package/dist/lib/logging.d.ts +30 -12
  41. package/dist/lib/logging.js +129 -72
  42. package/dist/lib/media.d.ts +28 -5
  43. package/dist/lib/media.js +26 -8
  44. package/dist/lib/menu-model.d.ts +0 -11
  45. package/dist/lib/menu-model.js +8 -8
  46. package/dist/lib/menu-settings.d.ts +0 -18
  47. package/dist/lib/menu-settings.js +18 -18
  48. package/dist/lib/menu-status.d.ts +4 -4
  49. package/dist/lib/menu-status.js +12 -7
  50. package/dist/lib/menu-thinking.d.ts +1 -1
  51. package/dist/lib/menu-thinking.js +1 -1
  52. package/dist/lib/menu.d.ts +1 -0
  53. package/dist/lib/menu.js +4 -3
  54. package/dist/lib/outbound-attachments.d.ts +0 -1
  55. package/dist/lib/outbound-attachments.js +1 -1
  56. package/dist/lib/outbound-buttons.js +1 -8
  57. package/dist/lib/outbound-markup.d.ts +1 -6
  58. package/dist/lib/outbound-markup.js +4 -21
  59. package/dist/lib/outbound.d.ts +0 -8
  60. package/dist/lib/outbound.js +5 -5
  61. package/dist/lib/paths.d.ts +50 -7
  62. package/dist/lib/paths.js +155 -17
  63. package/dist/lib/pi.d.ts +1 -0
  64. package/dist/lib/pi.js +2 -1
  65. package/dist/lib/polling.d.ts +0 -5
  66. package/dist/lib/polling.js +5 -5
  67. package/dist/lib/preview.d.ts +0 -3
  68. package/dist/lib/preview.js +3 -3
  69. package/dist/lib/prompt-templates.d.ts +0 -1
  70. package/dist/lib/prompt-templates.js +1 -1
  71. package/dist/lib/queue.d.ts +5 -15
  72. package/dist/lib/queue.js +94 -11
  73. package/dist/lib/recovery.d.ts +29 -9
  74. package/dist/lib/recovery.js +104 -33
  75. package/dist/lib/replies.d.ts +0 -4
  76. package/dist/lib/replies.js +1 -1
  77. package/dist/lib/routing.d.ts +74 -8
  78. package/dist/lib/routing.js +2280 -307
  79. package/dist/lib/sections.d.ts +0 -2
  80. package/dist/lib/sections.js +1 -1
  81. package/dist/lib/status.d.ts +44 -9
  82. package/dist/lib/status.js +149 -27
  83. package/dist/lib/sync.d.ts +0 -5
  84. package/dist/lib/sync.js +5 -4
  85. package/dist/lib/telegram-api.d.ts +13 -3
  86. package/dist/lib/telegram-api.js +38 -23
  87. package/dist/lib/thread-cleanup-manager.d.ts +1 -0
  88. package/dist/lib/thread-cleanup-manager.js +18 -21
  89. package/dist/lib/thread-naming.d.ts +32 -5
  90. package/dist/lib/thread-naming.js +191 -7
  91. package/dist/lib/thread-reconciler.d.ts +2 -0
  92. package/dist/lib/thread-reconciler.js +1 -1
  93. package/dist/lib/threads.d.ts +293 -59
  94. package/dist/lib/threads.js +1689 -369
  95. package/dist/lib/turns.d.ts +0 -1
  96. package/dist/lib/turns.js +1 -1
  97. package/dist/lib/updates.d.ts +191 -28
  98. package/dist/lib/updates.js +1048 -114
  99. package/dist/lib/wire.d.ts +12 -0
  100. package/dist/lib/wire.js +21 -0
  101. package/dist/lib/workspace-admission.d.ts +25 -17
  102. package/dist/lib/workspace-admission.js +95 -20
  103. package/dist/lib/workspace-identity.d.ts +20 -0
  104. package/dist/lib/workspace-identity.js +103 -0
  105. package/dist/lib/workspace-retirement.d.ts +43 -8
  106. package/dist/lib/workspace-retirement.js +148 -52
  107. package/dist/package.json +6 -6
  108. package/dist/skills/generated-control-surface/SKILL.md +3 -3
  109. package/dist/skills/telegram-bridge/references/diagnosis.md +3 -3
  110. package/docs/README.md +1 -1
  111. package/docs/activity.md +1 -1
  112. package/docs/architecture.md +156 -36
  113. package/docs/callback-namespaces.md +1 -1
  114. package/docs/delivery.md +1 -1
  115. package/docs/generative-apps.md +2 -2
  116. package/docs/inbound.md +1 -1
  117. package/docs/multi-instance-bus.md +125 -19
  118. package/docs/public-api.md +3 -1
  119. package/docs/ui-style.md +23 -9
  120. package/docs/updates.md +1 -1
  121. package/lib/activity-verbosity.ts +5 -9
  122. package/lib/bindings.ts +7 -0
  123. package/lib/bus-api.ts +9 -4
  124. package/lib/bus-follower.ts +243 -224
  125. package/lib/bus-leader.ts +178 -31
  126. package/lib/bus-transport.ts +30 -8
  127. package/lib/bus.ts +110 -334
  128. package/lib/channel-posts.ts +29 -29
  129. package/lib/command-templates.ts +5 -5
  130. package/lib/commands.ts +15 -15
  131. package/lib/config.ts +12 -17
  132. package/lib/delivery.ts +2 -8
  133. package/lib/extension.ts +261 -72
  134. package/lib/generative-apps.ts +0 -22
  135. package/lib/inbound.ts +2 -2
  136. package/lib/journal.ts +1250 -325
  137. package/lib/keyboard.ts +2 -2
  138. package/lib/lifecycle.ts +1 -1
  139. package/lib/locks.ts +402 -23
  140. package/lib/logging.ts +151 -101
  141. package/lib/media.ts +51 -9
  142. package/lib/menu-model.ts +8 -8
  143. package/lib/menu-settings.ts +18 -18
  144. package/lib/menu-status.ts +11 -0
  145. package/lib/menu-thinking.ts +2 -2
  146. package/lib/menu.ts +5 -1
  147. package/lib/outbound-attachments.ts +1 -1
  148. package/lib/outbound-buttons.ts +1 -10
  149. package/lib/outbound-markup.ts +5 -24
  150. package/lib/outbound.ts +5 -5
  151. package/lib/paths.ts +177 -17
  152. package/lib/pi.ts +5 -2
  153. package/lib/polling.ts +5 -5
  154. package/lib/preview.ts +3 -3
  155. package/lib/prompt-templates.ts +1 -1
  156. package/lib/queue.ts +93 -12
  157. package/lib/recovery.ts +104 -44
  158. package/lib/replies.ts +1 -1
  159. package/lib/routing.ts +2010 -375
  160. package/lib/sections.ts +1 -1
  161. package/lib/status.ts +156 -36
  162. package/lib/sync.ts +4 -4
  163. package/lib/telegram-api.ts +53 -33
  164. package/lib/thread-cleanup-manager.ts +24 -21
  165. package/lib/thread-naming.ts +267 -9
  166. package/lib/thread-reconciler.ts +3 -1
  167. package/lib/threads.ts +1736 -488
  168. package/lib/turns.ts +1 -1
  169. package/lib/updates.ts +1094 -151
  170. package/lib/wire.ts +28 -0
  171. package/lib/workspace-admission.ts +97 -47
  172. package/lib/workspace-identity.ts +147 -0
  173. package/lib/workspace-retirement.ts +188 -89
  174. package/package.json +6 -6
  175. package/scripts/audit-exports.mjs +100 -0
  176. package/scripts/check-downgrade.mjs +80 -43
  177. package/skills/generated-control-surface/SKILL.md +3 -3
  178. package/skills/telegram-bridge/references/diagnosis.md +3 -3
@@ -4,8 +4,10 @@
4
4
  * Owns update extraction, authorization, execution planning, generation-fenced journal draining, and the public update-handler registry
5
5
  */
6
6
  import { randomUUID } from "node:crypto";
7
+ import { isDeepStrictEqual } from "node:util";
8
+ import { isWireRecord as isTelegramUpdateAdmissionRecord, isNonEmptyWireString as isTelegramUpdateAdmissionString, } from "./wire.js";
7
9
  import { createTelegramPrivateTarget, createTelegramThreadTarget, } from "./target.js";
8
- import { TELEGRAM_UPDATE_JOURNAL_VERSION, TELEGRAM_UPDATE_JOURNAL_EXCLUSION_VERSION, TELEGRAM_UPDATE_JOURNAL_CUSTODY_VERSION, TELEGRAM_UPDATE_JOURNAL_FAILURE_CLASS_MAX_LENGTH, TELEGRAM_UPDATE_JOURNAL_FAILURE_SUMMARY_MAX_LENGTH, TELEGRAM_UPDATE_JOURNAL_QUEUE_OWNER_ID_MAX_LENGTH, areTelegramUpdateJournalQueueOwnersEqual, getTelegramUpdateJournalBindingPath, isTelegramUpdateJournalQueueOwnerProcess, parseTelegramUpdateJournalQueueOwner, } from "./journal.js";
10
+ import { TELEGRAM_UPDATE_JOURNAL_VERSION, TELEGRAM_UPDATE_JOURNAL_EXCLUSION_VERSION, TELEGRAM_UPDATE_JOURNAL_CUSTODY_VERSION, isTelegramUpdateJournalLegacyFamilyVersion, TELEGRAM_UPDATE_JOURNAL_FAILURE_CLASS_MAX_LENGTH, TELEGRAM_UPDATE_JOURNAL_FAILURE_SUMMARY_MAX_LENGTH, TELEGRAM_UPDATE_JOURNAL_QUEUE_OWNER_ID_MAX_LENGTH, areTelegramUpdateJournalQueueOwnersEqual, createTelegramUpdateJournalEntryDigest, getTelegramUpdateJournalBindingPath, isTelegramUpdateJournalQueueOwnerProcess, parseTelegramUpdateJournalQueueOwner, } from "./journal.js";
9
11
  import { areTelegramQueueAdmissionReceiptsEqual, createTelegramQueueHandoff, removeTelegramQueueItemByReceipt, } from "./queue.js";
10
12
  import { createTelegramUserPairingRuntime, getTelegramAuthorizationState, } from "./config.js";
11
13
  export const TELEGRAM_PRIORITY_REACTIONS = [
@@ -43,23 +45,6 @@ export function collectTelegramReactionEmojis(reactions) {
43
45
  function getTelegramReactionEmoji(emojis, candidates) {
44
46
  return candidates.find((emoji) => emojis.has(emoji));
45
47
  }
46
- export function getTelegramQueueReactionDisposition(reactions) {
47
- const emojis = collectTelegramReactionEmojis(reactions);
48
- const suppressionEmoji = getTelegramReactionEmoji(emojis, TELEGRAM_REMOVAL_REACTION_EMOJIS);
49
- const priorityEmoji = getTelegramReactionEmoji(emojis, TELEGRAM_PRIORITY_REACTION_EMOJIS);
50
- if (suppressionEmoji && priorityEmoji) {
51
- return {
52
- kind: "priority-suppressed",
53
- priorityEmoji,
54
- suppressionEmoji,
55
- };
56
- }
57
- if (suppressionEmoji)
58
- return { kind: "suppressed", emoji: suppressionEmoji };
59
- if (priorityEmoji)
60
- return { kind: "priority", emoji: priorityEmoji };
61
- return { kind: "default" };
62
- }
63
48
  function getTelegramQueueReactionTransition(oldReactions, newReactions) {
64
49
  const oldEmojis = collectTelegramReactionEmojis(oldReactions);
65
50
  const newEmojis = collectTelegramReactionEmojis(newReactions);
@@ -149,7 +134,7 @@ export function getAuthorizedTelegramGuestMessage(update) {
149
134
  }
150
135
  return guestMessage;
151
136
  }
152
- export class TelegramForeignUpdateSettlementError extends Error {
137
+ class TelegramForeignUpdateSettlementError extends Error {
153
138
  settlement;
154
139
  constructor(operation, settlement) {
155
140
  super(`Telegram ${operation} forwarding did not settle: ${settlement.failureClass}.`);
@@ -175,6 +160,7 @@ function rejectTelegramForeignUpdateSettlement(settlement, operation, source) {
175
160
  }
176
161
  export const TELEGRAM_INTERNAL_AGENT_MESSAGE = Symbol("telegram.internalAgentMessage");
177
162
  const TELEGRAM_UPDATE_ADMISSION_BINDING = Symbol("telegram.update-admission.binding");
163
+ const TELEGRAM_HELD_SOURCE_INSPECTION_LIMIT = 20;
178
164
  function bindTelegramUpdateAdmissionCarrier(value, binding) {
179
165
  if (!value || typeof value !== "object")
180
166
  return value;
@@ -195,13 +181,14 @@ function getTelegramUpdateAdmissionBinding(value) {
195
181
  ? binding
196
182
  : undefined;
197
183
  }
198
- export function bindTelegramUpdateAdmissionSource(update, report) {
184
+ export function bindTelegramUpdateAdmissionSource(update, report, controls) {
199
185
  if (!Number.isSafeInteger(update.update_id) || update.update_id < 0) {
200
186
  throw new TelegramUpdateAdmissionOutcomeError("Telegram update admission requires a safe update_id.");
201
187
  }
202
188
  const binding = {
203
189
  sourceUpdateId: update.update_id,
204
190
  report,
191
+ ...controls,
205
192
  };
206
193
  const callbackQuery = update.callback_query
207
194
  ? bindTelegramUpdateAdmissionCarrier({
@@ -232,6 +219,24 @@ export function bindTelegramUpdateAdmissionSource(update, report) {
232
219
  : {}),
233
220
  };
234
221
  }
222
+ /**
223
+ * Conservative read-only census: any retained entry naming this Thread, in any nested Telegram object or state,
224
+ * is unresolved custody. It cannot see updates still in transit before journal append.
225
+ */
226
+ export function collectTelegramJournalThreadUpdateIds(entries, target) {
227
+ const matches = (value, depth) => {
228
+ if (depth > 8 || !value || typeof value !== "object")
229
+ return false;
230
+ const record = value;
231
+ if (record.message_thread_id === target.threadId) {
232
+ const chat = record.chat;
233
+ if (!chat || typeof chat !== "object" || chat.id === target.chatId)
234
+ return true;
235
+ }
236
+ return Object.values(record).some(child => matches(child, depth + 1));
237
+ };
238
+ return entries.filter(entry => matches(entry.update, 0)).map(entry => entry.updateId).sort((left, right) => left - right);
239
+ }
235
240
  export function collectTelegramAdmissionSourceUpdateIds(values) {
236
241
  const sourceUpdateIds = new Set();
237
242
  for (const value of values) {
@@ -241,27 +246,122 @@ export function collectTelegramAdmissionSourceUpdateIds(values) {
241
246
  }
242
247
  return [...sourceUpdateIds].sort((left, right) => left - right);
243
248
  }
249
+ /** Observe the original journal entry, never the mutable routed carrier, through current worker authority. */
250
+ export function inspectTelegramDeferredSource(value) {
251
+ const execution = getTelegramUpdateExecutionFence(value);
252
+ if (execution?.isCurrent() !== true)
253
+ return undefined;
254
+ return getTelegramUpdateAdmissionBinding(value)?.inspectSource?.();
255
+ }
256
+ /** A route-owned pre-disposition acceptance publisher; wraps only this carrier, never the shared worker binding. */
257
+ export function bindTelegramUpdateCompletionAcceptance(value, publish) {
258
+ const binding = getTelegramUpdateAdmissionBinding(value);
259
+ if (!binding || getTelegramUpdateExecutionFence(value)?.isCurrent() !== true) {
260
+ throw new TelegramUpdateAdmissionOutcomeError("Telegram completion acceptance requires a live source carrier.");
261
+ }
262
+ let retained;
263
+ let queued = false;
264
+ const scoped = { ...binding, report(outcome) {
265
+ if (outcome.kind !== "complete") {
266
+ if (outcome.kind === "queued")
267
+ queued = true;
268
+ binding.report(outcome);
269
+ return;
270
+ }
271
+ assertTelegramUpdateExecutionCurrent(value);
272
+ // A command's trailing implicit completion cannot dispose receipt-owned or uncertain queued work.
273
+ if (queued) {
274
+ if (outcome.expectedSource)
275
+ throw new TelegramUpdateAdmissionOutcomeError("Telegram completion acceptance cannot dispose a queued source.");
276
+ return;
277
+ }
278
+ if (!retained) {
279
+ const published = validateTelegramUpdateAdmissionOutcome({ kind: "complete", expectedSource: publish() }, binding.sourceUpdateId, new Set([binding.sourceUpdateId]));
280
+ if (published.kind !== "complete" || !published.expectedSource?.completionSha256) {
281
+ throw new TelegramUpdateAdmissionOutcomeError("Telegram completion acceptance lacks its scoped journal proof.");
282
+ }
283
+ retained = published.expectedSource;
284
+ }
285
+ assertTelegramUpdateExecutionCurrent(value);
286
+ if (outcome.expectedSource && (outcome.expectedSource.updateId !== retained.updateId ||
287
+ outcome.expectedSource.journalBindingKey !== retained.journalBindingKey || outcome.expectedSource.sourceSha256 !== retained.sourceSha256 ||
288
+ outcome.expectedSource.completionSha256 !== undefined && outcome.expectedSource.completionSha256 !== retained.completionSha256)) {
289
+ throw new TelegramUpdateAdmissionOutcomeError("Telegram completion acceptance conflicts with its reported source.");
290
+ }
291
+ binding.report({ kind: "complete", expectedSource: { ...retained } });
292
+ } };
293
+ return carryTelegramUpdateExecutionFence(value, bindTelegramUpdateAdmissionCarrier(value, scoped));
294
+ }
244
295
  /** Report source completion; true means reported, not a durable settlement acknowledgement. */
245
- export function reportTelegramUpdateCompleted(value) {
296
+ export function reportTelegramUpdateCompleted(value, expectedSource) {
246
297
  const binding = getTelegramUpdateAdmissionBinding(value);
247
298
  if (!binding)
248
299
  return false;
249
300
  const execution = getTelegramUpdateExecutionFence(value);
250
301
  if (execution && !execution.isCurrent())
251
302
  return false;
252
- binding.report({ kind: "complete" });
303
+ binding.report(validateTelegramUpdateAdmissionOutcome({ kind: "complete", ...(expectedSource !== undefined ? { expectedSource } : {}) }, binding.sourceUpdateId, new Set([binding.sourceUpdateId])));
253
304
  return true;
254
305
  }
255
306
  export function reportTelegramUpdateDeferred(value) {
256
307
  const binding = getTelegramUpdateAdmissionBinding(value);
257
308
  if (!binding)
258
309
  return false;
310
+ const execution = getTelegramUpdateExecutionFence(value);
311
+ if (execution && !execution.isCurrent())
312
+ return false;
259
313
  binding.report({ kind: "deferred" });
260
314
  return true;
261
315
  }
316
+ /** Exact source-bound cancellation; undefined means no eligible attempt was made. */
317
+ export function abandonTelegramDeferredUpdate(value, authority) {
318
+ return getTelegramUpdateAdmissionBinding(value)?.abandon?.(authority);
319
+ }
320
+ /** Capability for chooser publication; the eventual action still requires exact deferred authority. */
321
+ export function supportsTelegramDeferredAbandonment(value, journalBindingKey) {
322
+ return getTelegramUpdateAdmissionBinding(value)?.supportsAbandonment?.(journalBindingKey) === true;
323
+ }
324
+ /** Observe protected attempts through a current carrier; the UI still owns human authorization. */
325
+ export function inspectTelegramAbandoningUpdates(value, request) {
326
+ return getTelegramUpdateAdmissionBinding(value)?.inspectAbandoning?.(request);
327
+ }
328
+ /** Historical inspection is not human confirmation or permission to stop delivery. */
329
+ export function inspectTelegramHistoricalInputs(value, request) {
330
+ return getTelegramUpdateAdmissionBinding(value)?.inspectHistorical?.(request);
331
+ }
332
+ export function isTelegramHistoricalInput(value, matchesOriginal) {
333
+ return getTelegramUpdateAdmissionBinding(value)?.isHistorical?.(matchesOriginal) === true;
334
+ }
335
+ /** Routing's last-boundary hold when ownership changed after early classification. */
336
+ export function reportTelegramHistoricalRoutingReview(value) {
337
+ if (!isTelegramHistoricalInput(value) || getTelegramUpdateExecutionFence(value)?.isCurrent() !== true)
338
+ return false;
339
+ getTelegramUpdateAdmissionBinding(value).report({ kind: "deferred", routingReview: true });
340
+ return true;
341
+ }
342
+ /** Arm only a positively published chooser; the journal owns the immutable hour deadline. */
343
+ export function armTelegramRoutingInputs(values, operatorUserId) {
344
+ const first = values[0], execution = getTelegramUpdateExecutionFence(first);
345
+ if (!execution || values.some(value => getTelegramUpdateExecutionFence(value)?.signal !== execution.signal))
346
+ return undefined;
347
+ for (const value of values)
348
+ assertTelegramUpdateExecutionCurrent(value);
349
+ const ids = collectTelegramAdmissionSourceUpdateIds(values);
350
+ if (!ids.length)
351
+ return undefined;
352
+ return getTelegramUpdateAdmissionBinding(first)?.armRoutingInput?.(operatorUserId, ids);
353
+ }
354
+ /** Reserve source execution; an actual choice must also freeze its durable TTL before any effect. */
355
+ export function acquireTelegramUpdateRouting(value, select = false, sourceUpdateIds) {
356
+ assertTelegramUpdateExecutionCurrent(value);
357
+ return getTelegramUpdateAdmissionBinding(value)?.acquireRouting?.(select, sourceUpdateIds) ?? (() => { });
358
+ }
262
359
  export function reportTelegramQueueAdmission(values, receipts) {
263
360
  const bindings = new Map();
264
361
  for (const value of values) {
362
+ const execution = getTelegramUpdateExecutionFence(value);
363
+ if (execution && !execution.isCurrent())
364
+ return false;
265
365
  const binding = getTelegramUpdateAdmissionBinding(value);
266
366
  if (!binding)
267
367
  continue;
@@ -427,7 +527,13 @@ function getForeignTelegramMessageOwnership(target, deps) {
427
527
  : undefined;
428
528
  }
429
529
  function getForeignTelegramCallbackOwnership(query, deps) {
430
- const messageOwnership = getForeignTelegramMessageOwnership(getTelegramCallbackMessageTarget(query), deps);
530
+ const messageTarget = getTelegramCallbackMessageTarget(query);
531
+ const currentInstanceId = deps.getCurrentInstanceId?.();
532
+ const knownOwner = messageTarget && deps.getMessageOwnership?.(messageTarget.chatId, messageTarget.messageId);
533
+ // A locally published chooser stays local when Restore moves its Thread to a follower.
534
+ if (currentInstanceId && knownOwner?.instanceId === currentInstanceId)
535
+ return undefined;
536
+ const messageOwnership = getForeignTelegramMessageOwnership(messageTarget, deps);
431
537
  const targetOwnership = getForeignTelegramTargetOwnership(query.message ? getTelegramMessageTarget(query.message) : undefined, deps);
432
538
  if (!messageOwnership)
433
539
  return targetOwnership;
@@ -727,7 +833,11 @@ export async function executeTelegramUpdatePlan(plan, deps) {
727
833
  deps.handleUnboundTelegramTopicMessage) {
728
834
  assertExecutionCurrent();
729
835
  await deps.handleUnboundTelegramTopicMessage(plan.message, deps.ctx);
730
- assertExecutionCurrent();
836
+ // A terminal historical hold intentionally suspends this exact carrier.
837
+ // Do not turn its acknowledged deferral into a retryable execution failure.
838
+ if (getTelegramUpdateExecutionFence(plan.message) !== deps.execution ||
839
+ getTelegramUpdateAdmissionBinding(plan.message)?.isHistoricalReviewHeld?.() !== true)
840
+ assertExecutionCurrent();
731
841
  return;
732
842
  }
733
843
  if (plan.kind === "edited-message") {
@@ -746,10 +856,28 @@ export async function executeTelegramUpdatePlan(plan, deps) {
746
856
  }
747
857
  }
748
858
  // --- Durable update worker ---
749
- export const TELEGRAM_UPDATE_RETRY_BASE_DELAY_MS = 1_000;
750
- export const TELEGRAM_UPDATE_RETRY_MAX_DELAY_MS = 60_000;
751
- export const TELEGRAM_UPDATE_WORKER_BATCH_SIZE = 64;
752
- export class TelegramUpdateAdmissionOutcomeError extends Error {
859
+ const TELEGRAM_UPDATE_RETRY_BASE_DELAY_MS = 1_000;
860
+ const TELEGRAM_UPDATE_RETRY_MAX_DELAY_MS = 60_000;
861
+ const TELEGRAM_UPDATE_WORKER_BATCH_SIZE = 64;
862
+ function normalizeTelegramQueueSourceCompletions(value, sourceUpdateIds, journalBindingKey, full = true) {
863
+ if (!Array.isArray(value) || (full ? value.length !== sourceUpdateIds.size : value.length > sourceUpdateIds.size) || value.length === 0)
864
+ throw new Error("Telegram scoped queue completion requires full source coverage.");
865
+ const seen = new Set(), scopes = new Set();
866
+ return value.map(source => {
867
+ if (!Number.isSafeInteger(source?.updateId) || !sourceUpdateIds.has(source.updateId) || seen.has(source.updateId)) {
868
+ throw new Error("Telegram scoped queue completion has invalid source membership.");
869
+ }
870
+ const normalized = validateTelegramUpdateAdmissionOutcome({ kind: "complete", expectedSource: source }, source.updateId, sourceUpdateIds);
871
+ if (normalized.kind !== "complete" || !normalized.expectedSource?.completionSha256 ||
872
+ normalized.expectedSource.journalBindingKey !== journalBindingKey || scopes.has(normalized.expectedSource.completionSha256)) {
873
+ throw new Error("Telegram scoped queue completion has invalid source scope.");
874
+ }
875
+ seen.add(source.updateId);
876
+ scopes.add(normalized.expectedSource.completionSha256);
877
+ return { ...normalized.expectedSource, completionSha256: normalized.expectedSource.completionSha256 };
878
+ }).sort((a, b) => a.updateId - b.updateId);
879
+ }
880
+ class TelegramUpdateAdmissionOutcomeError extends Error {
753
881
  constructor(message) {
754
882
  super(message);
755
883
  this.name = "TelegramUpdateAdmissionOutcomeError";
@@ -759,20 +887,29 @@ const TELEGRAM_UPDATE_WORKER_EXECUTION_ABORTED = Symbol("telegram.update-worker.
759
887
  function getTelegramUpdateWorkerStateSnapshot(state) {
760
888
  return { ...state };
761
889
  }
762
- function isTelegramUpdateAdmissionRecord(value) {
763
- return !!value && typeof value === "object" && !Array.isArray(value);
764
- }
765
- function isTelegramUpdateAdmissionString(value) {
766
- return typeof value === "string" && value.length > 0;
767
- }
768
890
  function validateTelegramUpdateAdmissionOutcome(value, currentUpdateId, claimableUpdateIds) {
769
891
  if (!isTelegramUpdateAdmissionRecord(value)) {
770
892
  throw new TelegramUpdateAdmissionOutcomeError(`Telegram update ${currentUpdateId} returned no admission outcome.`);
771
893
  }
772
- if (value.kind === "complete")
773
- return { kind: "complete" };
774
- if (value.kind === "deferred")
775
- return { kind: "deferred" };
894
+ if (value.kind === "complete") {
895
+ if (value.expectedSource === undefined)
896
+ return { kind: "complete" };
897
+ const source = value.expectedSource;
898
+ if (!isTelegramUpdateAdmissionRecord(source) || Object.keys(source).some(key => !["journalBindingKey", "updateId", "sourceSha256", "completionSha256"].includes(key)) ||
899
+ source.updateId !== currentUpdateId || !isTelegramUpdateAdmissionString(source.journalBindingKey) ||
900
+ typeof source.sourceSha256 !== "string" || !/^[a-f0-9]{64}$/u.test(source.sourceSha256) ||
901
+ source.completionSha256 !== undefined && (typeof source.completionSha256 !== "string" || !/^[a-f0-9]{64}$/u.test(source.completionSha256))) {
902
+ throw new TelegramUpdateAdmissionOutcomeError(`Telegram update ${currentUpdateId} returned an invalid completion source.`);
903
+ }
904
+ return { kind: "complete", expectedSource: { journalBindingKey: source.journalBindingKey,
905
+ updateId: currentUpdateId, sourceSha256: source.sourceSha256,
906
+ ...(source.completionSha256 !== undefined ? { completionSha256: source.completionSha256 } : {}) } };
907
+ }
908
+ if (value.kind === "deferred") {
909
+ if (value.routingReview !== undefined && value.routingReview !== true)
910
+ throw new TelegramUpdateAdmissionOutcomeError("Invalid historical review outcome.");
911
+ return { kind: "deferred", ...(value.routingReview === true ? { routingReview: true } : {}) };
912
+ }
776
913
  if (value.kind === "queued") {
777
914
  if ((value.queueKind !== "prompt" && value.queueKind !== "control") ||
778
915
  !isTelegramUpdateAdmissionString(value.receiptId) ||
@@ -871,12 +1008,23 @@ export function createTelegramUpdateWorkerRuntime(deps) {
871
1008
  throw new Error("Telegram update retry scheduling requires matching schedule and cancel ports.");
872
1009
  }
873
1010
  const getNowMs = deps.getNowMs ?? Date.now;
1011
+ const onHeldSourcesPrepared = deps.onHeldSourcesPrepared;
1012
+ const spendHistorical = deps.spendHistoricalInput === true;
1013
+ const routingJournal = deps.journal.routingInputs;
1014
+ const armRouting = routingJournal?.arm.bind(routingJournal);
1015
+ const selectRouting = routingJournal?.select.bind(routingJournal);
1016
+ const expireRouting = routingJournal?.expire.bind(routingJournal);
1017
+ const expireRoutingObserver = deps.expireRoutingInput;
874
1018
  const batchSize = deps.batchSize ?? TELEGRAM_UPDATE_WORKER_BATCH_SIZE;
875
1019
  if (!Number.isSafeInteger(batchSize) || batchSize <= 0) {
876
1020
  throw new Error("Telegram update worker batch size must be positive.");
877
1021
  }
878
1022
  const yieldToEventLoop = deps.yieldToEventLoop ??
879
1023
  (() => new Promise((resolve) => setTimeout(resolve, 0)));
1024
+ const completeQueuedExact = deps.journal.completeQueuedExact?.bind(deps.journal);
1025
+ const completeExact = deps.journal.removeCompletedExact?.bind(deps.journal);
1026
+ const inspectCompletion = deps.journal.inspectSourceCompletion?.bind(deps.journal);
1027
+ const observeQueuedCompletion = deps.onQueueReceiptCompleted;
880
1028
  const fallbackQueueOwnerInstanceId = `worker-${randomUUID()}`;
881
1029
  const fallbackQueueOwnerProcessId = process.pid > 0 ? process.pid : 1;
882
1030
  const createAbortController = deps.createAbortController ?? (() => new AbortController());
@@ -890,6 +1038,13 @@ export function createTelegramUpdateWorkerRuntime(deps) {
890
1038
  processBirthId: `${fallbackQueueOwnerProcessId}:${fallbackQueueOwnerInstanceId}`,
891
1039
  sessionGeneration: generation,
892
1040
  });
1041
+ const isQueueOwnerIdentityCurrent = (expected) => {
1042
+ if (!deps.getQueueOwnerIdentity)
1043
+ return true;
1044
+ const live = deps.getQueueOwnerIdentity(expected.ctx), captured = expected.queueOwnerIdentity;
1045
+ return !!live && live.instanceId === captured.instanceId && live.processId === captured.processId &&
1046
+ live.processBirthId === captured.processBirthId && live.sessionGeneration === captured.sessionGeneration;
1047
+ };
893
1048
  const state = {
894
1049
  phase: "stopped",
895
1050
  generation: 0,
@@ -903,8 +1058,11 @@ export function createTelegramUpdateWorkerRuntime(deps) {
903
1058
  unsettledExecutionCount: 0,
904
1059
  };
905
1060
  const claims = new Map();
1061
+ const deferredSources = new Map();
906
1062
  const unsettledExecutionsByUpdateId = new Map();
907
1063
  const committedQueueReceipts = new Map();
1064
+ const requiredScopedQueueReceipts = new Set();
1065
+ const scopedQueueCompletionAttempts = new Map();
908
1066
  const unsettledExecutions = new Set();
909
1067
  let owner;
910
1068
  let drainPromise;
@@ -914,6 +1072,8 @@ export function createTelegramUpdateWorkerRuntime(deps) {
914
1072
  let retryTimer;
915
1073
  let retryTimerAtMs;
916
1074
  let retryTimerToken;
1075
+ let routingTimer;
1076
+ let routingTimerToken;
917
1077
  let launchDrain = () => { };
918
1078
  const recordRuntimeEvent = (error, details) => {
919
1079
  try {
@@ -934,14 +1094,28 @@ export function createTelegramUpdateWorkerRuntime(deps) {
934
1094
  const updateClaimCounts = () => {
935
1095
  let deferredClaimCount = 0;
936
1096
  let queuedClaimCount = 0;
1097
+ let abandoningClaimCount = 0;
1098
+ let historicalClaimCount = 0;
937
1099
  for (const claim of claims.values()) {
938
1100
  if (claim === "queued")
939
1101
  queuedClaimCount += 1;
940
1102
  else
941
1103
  deferredClaimCount += 1;
1104
+ if (claim === "abandoning")
1105
+ abandoningClaimCount += 1;
1106
+ if (claim === "historical" || claim === "retained")
1107
+ historicalClaimCount += 1;
942
1108
  }
943
1109
  state.deferredClaimCount = deferredClaimCount;
944
1110
  state.queuedClaimCount = queuedClaimCount;
1111
+ if (abandoningClaimCount > 0)
1112
+ state.abandoningClaimCount = abandoningClaimCount;
1113
+ else
1114
+ delete state.abandoningClaimCount;
1115
+ if (historicalClaimCount > 0)
1116
+ state.historicalClaimCount = historicalClaimCount;
1117
+ else
1118
+ delete state.historicalClaimCount;
945
1119
  };
946
1120
  const releaseDeferredClaims = (updateIds) => {
947
1121
  let changed = false;
@@ -949,6 +1123,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
949
1123
  if (claims.get(updateId) !== "deferred")
950
1124
  continue;
951
1125
  claims.delete(updateId);
1126
+ deferredSources.delete(updateId);
952
1127
  changed = true;
953
1128
  }
954
1129
  if (!changed)
@@ -992,7 +1167,13 @@ export function createTelegramUpdateWorkerRuntime(deps) {
992
1167
  ? { journalBindingKey: deps.getJournalBindingKey() }
993
1168
  : {}),
994
1169
  });
995
- const publishCommittedQueueReceipt = (receipt, queueOwner, ctx) => {
1170
+ const inspectQueueReceipt = deps.journal.isQueueReceiptCurrent?.bind(deps.journal);
1171
+ const inspectQueuedSources = deps.journal.inspectQueuedReceipt?.bind(deps.journal);
1172
+ const prepareQueueReceipt = deps.beforeQueueReceiptPublished;
1173
+ const pendingQueuePublications = new Map();
1174
+ const publishCommittedQueueReceipt = async (receipt, queueOwner, expectedOwner) => {
1175
+ const ctx = expectedOwner.ctx;
1176
+ let sourceCompletions;
996
1177
  const normalized = bindQueueReceiptToJournal(receipt);
997
1178
  const existing = committedQueueReceipts.get(receipt.receiptId);
998
1179
  if (existing) {
@@ -1002,28 +1183,90 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1002
1183
  }
1003
1184
  return false;
1004
1185
  }
1005
- committedQueueReceipts.set(receipt.receiptId, {
1006
- receipt: normalized,
1007
- queueOwner: { ...queueOwner },
1008
- });
1009
- try {
1010
- deps.onQueueReceiptCommitted?.(normalized, ctx);
1186
+ if (prepareQueueReceipt) {
1187
+ const pending = pendingQueuePublications.get(normalized.receiptId);
1188
+ if (pending) {
1189
+ if (pending.owner !== expectedOwner || !areTelegramQueueAdmissionReceiptsEqual(pending.receipt, normalized) ||
1190
+ !areTelegramUpdateJournalQueueOwnersEqual(pending.queueOwner, queueOwner)) {
1191
+ throw new TelegramUpdateAdmissionOutcomeError("Telegram queue publication has conflicting in-flight authority.");
1192
+ }
1193
+ await pending.task;
1194
+ return false;
1195
+ }
1196
+ const journalBindingKey = deps.getJournalBindingKey?.();
1197
+ const current = () => owner === expectedOwner && !expectedOwner.controller.signal.aborted && deps.hasAuthority(ctx) && deps.isContextCurrent?.(ctx) !== false &&
1198
+ isQueueOwnerIdentityCurrent(expectedOwner) && deps.getJournalBindingKey?.() === journalBindingKey;
1199
+ if (!journalBindingKey || !current() || !inspectQueueReceipt ||
1200
+ inspectQueueReceipt(normalizeQueueReceipt(normalized), { ...queueOwner }) !== true || !current()) {
1201
+ throw new TelegramUpdateAdmissionOutcomeError("Telegram queue publication requires exact current receipt inspection.");
1202
+ }
1203
+ const task = Promise.resolve().then(async () => {
1204
+ if (!current())
1205
+ throw new TelegramUpdateAdmissionOutcomeError("Telegram queue publication authority ended before acceptance.");
1206
+ const prepared = await prepareQueueReceipt(normalizeQueueReceipt(normalized), { ...queueOwner }, ctx, current);
1207
+ if (prepared !== undefined) {
1208
+ sourceCompletions = normalizeTelegramQueueSourceCompletions(prepared, new Set(normalized.sourceUpdateIds), journalBindingKey, false);
1209
+ if (!completeQueuedExact || !inspectCompletion)
1210
+ throw new Error("Telegram scoped queue terminal capabilities are unavailable.");
1211
+ if (sourceCompletions.length !== normalized.sourceUpdateIds.length) {
1212
+ if (!current() || !inspectQueuedSources)
1213
+ throw new Error("Telegram partial queue scopes require strict current whole-receipt origin inspection.");
1214
+ const expected = { queueKind: normalized.queueKind, receiptId: normalized.receiptId,
1215
+ sourceUpdateIds: [...normalized.sourceUpdateIds], queueOwner: { ...queueOwner } };
1216
+ const proof = inspectQueuedSources(structuredClone(expected));
1217
+ if (!current() || !proof || !isDeepStrictEqual(proof.receipt, expected) ||
1218
+ !Array.isArray(proof.sources) || proof.sources.length !== expected.sourceUpdateIds.length ||
1219
+ proof.sources.some((source, index) => source.updateId !== expected.sourceUpdateIds[index] || typeof source.sourceSha256 !== "string" || !/^[a-f0-9]{64}$/u.test(source.sourceSha256)) ||
1220
+ sourceCompletions.some(scope => proof.sources.find(source => source.updateId === scope.updateId)?.sourceSha256 !== scope.sourceSha256)) {
1221
+ throw new Error("Telegram partial queue scope origin was not confirmed under whole-receipt authority.");
1222
+ }
1223
+ }
1224
+ for (const { journalBindingKey: _binding, ...completion } of sourceCompletions) {
1225
+ if (!current() || inspectCompletion({ ...completion }) !== undefined || !current()) {
1226
+ throw new Error("Telegram queued source completion authority is contradictory.");
1227
+ }
1228
+ }
1229
+ }
1230
+ if (!current() || inspectQueueReceipt(normalizeQueueReceipt(normalized), { ...queueOwner }) !== true || !current()) {
1231
+ throw new TelegramUpdateAdmissionOutcomeError("Telegram queue publication authority changed after acceptance.");
1232
+ }
1233
+ publishReady();
1234
+ }).finally(() => { if (pendingQueuePublications.get(normalized.receiptId)?.task === task)
1235
+ pendingQueuePublications.delete(normalized.receiptId); });
1236
+ pendingQueuePublications.set(normalized.receiptId, { receipt: normalizeQueueReceipt(normalized), queueOwner: { ...queueOwner }, owner: expectedOwner, task });
1237
+ await task;
1238
+ return true;
1011
1239
  }
1012
- catch (error) {
1013
- recordRuntimeEvent(error, {
1014
- phase: "queue-receipt-observer",
1015
- receiptId: normalized.receiptId,
1240
+ publishReady();
1241
+ return true;
1242
+ function publishReady() {
1243
+ committedQueueReceipts.set(receipt.receiptId, {
1244
+ receipt: normalized,
1245
+ queueOwner: { ...queueOwner },
1246
+ ...(sourceCompletions ? { sourceCompletions: sourceCompletions.map(value => ({ ...value })) } : {}),
1016
1247
  });
1248
+ if (sourceCompletions)
1249
+ requiredScopedQueueReceipts.add(receipt.receiptId);
1250
+ try {
1251
+ deps.onQueueReceiptCommitted?.(normalizeQueueReceipt(normalized), ctx);
1252
+ }
1253
+ catch (error) {
1254
+ recordRuntimeEvent(error, {
1255
+ phase: "queue-receipt-observer",
1256
+ receiptId: normalized.receiptId,
1257
+ });
1258
+ }
1017
1259
  }
1018
- return true;
1019
1260
  };
1020
1261
  const getCurrentQueueReceipt = (receipt) => {
1262
+ if (scopedQueueCompletionAttempts.has(receipt.receiptId))
1263
+ return undefined;
1021
1264
  const committed = committedQueueReceipts.get(receipt.receiptId);
1022
1265
  if (!committed || !areTelegramQueueAdmissionReceiptsEqual(committed.receipt, normalizeQueueReceipt(receipt)))
1023
1266
  return undefined;
1024
1267
  try {
1025
- if (deps.journal.isQueueReceiptCurrent &&
1026
- deps.journal.isQueueReceiptCurrent(committed.receipt, committed.queueOwner) !== true) {
1268
+ if (inspectQueueReceipt &&
1269
+ inspectQueueReceipt(normalizeQueueReceipt(committed.receipt), { ...committed.queueOwner }) !== true) {
1027
1270
  return undefined;
1028
1271
  }
1029
1272
  }
@@ -1066,7 +1309,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1066
1309
  launchDrain();
1067
1310
  }, Math.max(0, nextRetryAtMs - getNowMs()));
1068
1311
  };
1069
- const refreshJournalState = (snapshot, expectedOwner) => {
1312
+ const refreshJournalState = async (snapshot, expectedOwner) => {
1070
1313
  // Validate the whole snapshot before reconstructing any queue authority.
1071
1314
  if ((snapshot.version !== TELEGRAM_UPDATE_JOURNAL_VERSION &&
1072
1315
  snapshot.version !== TELEGRAM_UPDATE_JOURNAL_EXCLUSION_VERSION &&
@@ -1095,7 +1338,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1095
1338
  oldestAdmittedAtMs === undefined
1096
1339
  ? entry.admittedAtMs
1097
1340
  : Math.min(oldestAdmittedAtMs, entry.admittedAtMs);
1098
- if (entry.state === "retry-wait" && entry.nextRetryAtMs !== undefined) {
1341
+ if (entry.state === "retry-wait" && entry.nextRetryAtMs !== undefined && !claims.has(entry.updateId)) {
1099
1342
  scheduledRetryAtMs =
1100
1343
  scheduledRetryAtMs === undefined
1101
1344
  ? entry.nextRetryAtMs
@@ -1156,17 +1399,22 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1156
1399
  }
1157
1400
  }
1158
1401
  for (const [updateId, claim] of claims) {
1159
- if (!availableUpdateIds.has(updateId) ||
1402
+ if ((claim !== "abandoning" && !availableUpdateIds.has(updateId)) ||
1160
1403
  (claim === "queued" && !locallyOwnedQueuedUpdateIds.has(updateId))) {
1161
1404
  claims.delete(updateId);
1162
1405
  }
1163
1406
  }
1407
+ for (const updateId of deferredSources.keys()) {
1408
+ const claim = claims.get(updateId);
1409
+ if (claim !== "deferred" && claim !== "abandoning" && claim !== "historical" && claim !== "retained")
1410
+ deferredSources.delete(updateId);
1411
+ }
1164
1412
  for (const [receiptId, receipt] of queuedReceiptEntries) {
1165
- publishCommittedQueueReceipt({
1413
+ await publishCommittedQueueReceipt({
1166
1414
  queueKind: receipt.queueKind,
1167
1415
  receiptId,
1168
1416
  sourceUpdateIds: receipt.sourceUpdateIds,
1169
- }, receipt.queueOwner, expectedOwner.ctx);
1417
+ }, receipt.queueOwner, expectedOwner);
1170
1418
  }
1171
1419
  for (const [receiptId] of committedQueueReceipts) {
1172
1420
  if (!queuedReceiptEntries.has(receiptId)) {
@@ -1307,13 +1555,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1307
1555
  for (const sourceUpdateId of normalized.sourceUpdateIds) {
1308
1556
  claims.set(sourceUpdateId, "queued");
1309
1557
  }
1310
- try {
1311
- publishCommittedQueueReceipt(normalized, queueOwner, expectedOwner.ctx);
1312
- }
1313
- catch (error) {
1314
- return blockWithFailure("invalid-outcome", "queue-receipt-publish", error, currentUpdateId);
1315
- }
1316
- return "committed";
1558
+ return publishCommittedQueueReceipt(normalized, queueOwner, expectedOwner).then(() => "committed", error => blockWithFailure("invalid-outcome", "queue-receipt-publish", error, currentUpdateId));
1317
1559
  };
1318
1560
  const persistExecutionFailure = (expectedOwner, entry, error) => {
1319
1561
  const authorityResult = checkAuthority(expectedOwner, entry.updateId);
@@ -1372,14 +1614,52 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1372
1614
  transition(disposition, entry.updateId);
1373
1615
  return disposition;
1374
1616
  };
1375
- const commitCompletedBatch = (expectedOwner, updateIds) => {
1617
+ const commitCompletedBatch = (expectedOwner, completions) => {
1618
+ const updateIds = completions.map(value => value.updateId);
1376
1619
  if (updateIds.length === 0)
1377
1620
  return undefined;
1378
1621
  const commitAuthority = checkAuthority(expectedOwner, updateIds[updateIds.length - 1]);
1379
1622
  if (commitAuthority)
1380
1623
  return commitAuthority;
1624
+ const journalBindingKey = deps.getJournalBindingKey?.();
1381
1625
  try {
1382
- const removed = deps.journal.removeCompleted(updateIds);
1626
+ const expectedSources = completions.flatMap(value => value.expectedSource ? [value.expectedSource] : []);
1627
+ if (expectedSources.some(value => value.journalBindingKey !== journalBindingKey) ||
1628
+ (expectedSources.length > 0 && (deps.isContextCurrent?.(expectedOwner.ctx) === false || !isQueueOwnerIdentityCurrent(expectedOwner)))) {
1629
+ throw new Error("Telegram guarded completion source authority changed.");
1630
+ }
1631
+ const scoped = expectedSources.flatMap(({ updateId, sourceSha256, completionSha256 }) => completionSha256 !== undefined
1632
+ ? [{ updateId, sourceSha256, completionSha256 }] : []).sort((a, b) => a.updateId - b.updateId);
1633
+ if (scoped.length > 0 && !inspectCompletion)
1634
+ throw new Error("Telegram source completion inspection is unavailable.");
1635
+ let removed;
1636
+ if (expectedSources.length > 0) {
1637
+ if (!completeExact)
1638
+ throw new Error("Telegram exact source completion is unavailable.");
1639
+ removed = completeExact(updateIds, expectedSources.map(({ updateId, sourceSha256 }) => ({ updateId, sourceSha256 })), scoped.length > 0 ? scoped.map(completion => ({ ...completion })) : undefined);
1640
+ }
1641
+ else
1642
+ removed = deps.journal.removeCompleted(updateIds);
1643
+ if (scoped.length > 0) {
1644
+ const afterCommit = checkAuthority(expectedOwner, updateIds.at(-1));
1645
+ if (afterCommit)
1646
+ return afterCommit;
1647
+ if (deps.isContextCurrent?.(expectedOwner.ctx) === false || !isQueueOwnerIdentityCurrent(expectedOwner) ||
1648
+ deps.getJournalBindingKey?.() !== journalBindingKey)
1649
+ throw new Error("Telegram source completion ACK authority changed.");
1650
+ if (!isDeepStrictEqual(removed.sourceCompletions, scoped))
1651
+ throw new Error("Telegram source completion ACK was not confirmed.");
1652
+ for (const completion of scoped) {
1653
+ if (!isDeepStrictEqual(inspectCompletion({ ...completion }), completion))
1654
+ throw new Error("Telegram source completion ACK was not retained.");
1655
+ const afterRead = checkAuthority(expectedOwner, completion.updateId);
1656
+ if (afterRead)
1657
+ return afterRead;
1658
+ if (deps.isContextCurrent?.(expectedOwner.ctx) === false || !isQueueOwnerIdentityCurrent(expectedOwner) ||
1659
+ deps.getJournalBindingKey?.() !== journalBindingKey)
1660
+ throw new Error("Telegram source completion ACK authority changed.");
1661
+ }
1662
+ }
1383
1663
  const removedIds = new Set(removed.removedUpdateIds);
1384
1664
  if (updateIds.some((updateId) => !removedIds.has(updateId))) {
1385
1665
  throw new Error("Telegram update batch changed before completion commit.");
@@ -1389,12 +1669,16 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1389
1669
  return blockWithFailure("journal-write", "completion-commit", error, updateIds[updateIds.length - 1]);
1390
1670
  }
1391
1671
  const completedAtMs = getNowMs();
1392
- for (const updateId of updateIds) {
1672
+ for (const { updateId, spent } of completions) {
1393
1673
  claims.delete(updateId);
1674
+ deferredSources.delete(updateId);
1675
+ // Spending is disposition, not completion: no task, cleanup or session observer runs.
1676
+ if (spent)
1677
+ continue;
1394
1678
  state.lastCompletedUpdateId = updateId;
1395
1679
  state.lastCompletedAtMs = completedAtMs;
1396
1680
  try {
1397
- deps.onUpdateCompleted?.(updateId, expectedOwner.ctx);
1681
+ deps.onUpdateCompleted?.(updateId, expectedOwner.ctx, journalBindingKey);
1398
1682
  }
1399
1683
  catch (error) {
1400
1684
  recordRuntimeEvent(error, {
@@ -1405,6 +1689,122 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1405
1689
  }
1406
1690
  return undefined;
1407
1691
  };
1692
+ const clearRoutingTimer = () => {
1693
+ if (routingTimer !== undefined)
1694
+ cancelRetry(routingTimer);
1695
+ routingTimer = undefined;
1696
+ routingTimerToken = undefined;
1697
+ };
1698
+ const reconcileRoutingInputs = async (snapshot, expectedOwner) => {
1699
+ clearRoutingTimer();
1700
+ if (!isTelegramUpdateJournalLegacyFamilyVersion(snapshot.version) || !expireRouting || !expireRoutingObserver)
1701
+ return false;
1702
+ let nextAtMs, changed = false;
1703
+ const binding = deps.getJournalBindingKey?.(), snapshotIds = new Set(snapshot.entries.map(entry => entry.updateId));
1704
+ // Lost expiry ACKs reconcile the exact held original against its body-free discard tombstone.
1705
+ const candidates = [...snapshot.entries, ...[...deferredSources.values()]
1706
+ .filter(source => source.journalBindingKey === binding && !snapshotIds.has(source.entry.updateId))
1707
+ .map(source => source.entry)];
1708
+ for (const entry of candidates) {
1709
+ const lifetime = entry.routingInput;
1710
+ if (!lifetime || entry.state !== "pending" || !binding || claims.get(entry.updateId) === "queued" ||
1711
+ unsettledExecutionsByUpdateId.get(entry.updateId)?.size)
1712
+ continue;
1713
+ // Classify the first startup snapshot before constructing any historical expiry carrier.
1714
+ if (expectedOwner.startupUpdateIds?.has(entry.updateId) && !claims.has(entry.updateId) && deps.shouldReviewHistoricalInput) {
1715
+ nextAtMs = Math.min(nextAtMs ?? Infinity, Math.max(getNowMs() + 1, lifetime.expiresAtMs));
1716
+ continue;
1717
+ }
1718
+ if (getNowMs() < lifetime.expiresAtMs) {
1719
+ nextAtMs = Math.min(nextAtMs ?? Infinity, lifetime.expiresAtMs);
1720
+ continue;
1721
+ }
1722
+ const existing = deferredSources.get(entry.updateId);
1723
+ if (existing && !isDeepStrictEqual(existing.entry, entry)) {
1724
+ // Lost clock/selection ACKs may leave only this immutable metadata behind its exact source; reconciliation grants expiry, never another dispatch.
1725
+ const previous = existing.entry.routingInput;
1726
+ if (!isDeepStrictEqual({ ...existing.entry, routingInput: lifetime }, entry) ||
1727
+ (previous && (!isDeepStrictEqual({ ...previous, phase: lifetime.phase }, lifetime) ||
1728
+ (previous.phase === "selected" && lifetime.phase !== "selected"))))
1729
+ continue;
1730
+ existing.entry = structuredClone(entry);
1731
+ }
1732
+ const source = existing ?? { entry: structuredClone(entry), journalBindingKey: binding, historical: true };
1733
+ deferredSources.set(entry.updateId, source);
1734
+ if (!claims.has(entry.updateId))
1735
+ claims.set(entry.updateId, "historical");
1736
+ const current = () => owner === expectedOwner && !expectedOwner.controller.signal.aborted &&
1737
+ deps.isContextCurrent?.(expectedOwner.ctx) !== false && deps.getJournalBindingKey?.() === binding &&
1738
+ deps.hasAuthority(expectedOwner.ctx) && isQueueOwnerIdentityCurrent(expectedOwner) && deferredSources.get(entry.updateId) === source &&
1739
+ isDeepStrictEqual(source.entry, entry) && claims.get(entry.updateId) !== "queued";
1740
+ let committed;
1741
+ try {
1742
+ if (current())
1743
+ await expireRoutingObserver({ original: structuredClone(entry), journalBindingKey: binding, isCurrent: current,
1744
+ expire() {
1745
+ if (!current())
1746
+ return undefined;
1747
+ if (committed)
1748
+ return { ...committed, duplicate: true };
1749
+ claims.set(entry.updateId, "abandoning");
1750
+ committed = expireRouting({ entry: source.entry, journalBindingKey: binding, operatorUserId: lifetime.operatorUserId, isCurrent: current });
1751
+ // Source expiry is terminal before chooser/cleanup awaits; later failures must not retain a deferred/retry source.
1752
+ claims.delete(entry.updateId);
1753
+ deferredSources.delete(entry.updateId);
1754
+ state.journalEntryCount = committed.entryCount;
1755
+ state.journalSerializedBytes = committed.serializedBytes;
1756
+ updateClaimCounts();
1757
+ notifyStateChange();
1758
+ changed ||= !committed.duplicate;
1759
+ return committed;
1760
+ } }, expectedOwner.ctx, expectedOwner.controller.signal);
1761
+ }
1762
+ catch (error) {
1763
+ recordRuntimeEvent(error, { phase: "routing-input-expiry", updateId: entry.updateId });
1764
+ }
1765
+ if (!committed)
1766
+ nextAtMs = Math.min(nextAtMs ?? Infinity, getNowMs() + 60_000);
1767
+ }
1768
+ if (nextAtMs !== undefined && owner === expectedOwner && !expectedOwner.controller.signal.aborted) {
1769
+ const token = {};
1770
+ routingTimerToken = token;
1771
+ routingTimer = scheduleRetry(() => {
1772
+ if (routingTimerToken !== token || owner !== expectedOwner || expectedOwner.controller.signal.aborted)
1773
+ return;
1774
+ routingTimer = undefined;
1775
+ routingTimerToken = undefined;
1776
+ pendingSignal = true;
1777
+ launchDrain();
1778
+ }, Math.max(1, nextAtMs - getNowMs()));
1779
+ }
1780
+ return changed;
1781
+ };
1782
+ const mutateRoutingSource = (input, select) => {
1783
+ const expectedOwner = owner, ids = [...input.sourceUpdateIds];
1784
+ if (!ids.length || !ids.includes(input.updateId) || new Set(ids).size !== ids.length || ids.some(id => !Number.isSafeInteger(id) || id < 0))
1785
+ return undefined;
1786
+ const sources = ids.map(id => deferredSources.get(id)), binding = sources[0]?.journalBindingKey;
1787
+ const current = () => !!expectedOwner && owner === expectedOwner && expectedOwner.controller.signal === input.signal && !input.signal.aborted &&
1788
+ !!binding && sources.every((source, index) => !!source && deferredSources.get(ids[index]) === source && !source.historical &&
1789
+ source.journalBindingKey === binding && claims.get(ids[index]) !== "queued" && claims.get(ids[index]) !== "abandoning") &&
1790
+ deps.getJournalBindingKey?.() === binding && deps.isContextCurrent?.(expectedOwner.ctx) !== false &&
1791
+ deps.hasAuthority(expectedOwner.ctx) && isQueueOwnerIdentityCurrent(expectedOwner);
1792
+ if (!current() || !armRouting || !selectRouting || !expireRoutingObserver)
1793
+ return undefined;
1794
+ const authority = { journalBindingKey: binding, entries: sources.map(source => structuredClone(source.entry)), operatorUserId: input.operatorUserId, isCurrent: current };
1795
+ const result = select ? selectRouting(authority) : { issued: false, entries: armRouting({ ...authority, publishedAtMs: getNowMs() }) };
1796
+ if (result.entries.length !== ids.length || result.entries.some((entry, index) => entry.updateId !== ids[index] || !entry.routingInput) || !current()) {
1797
+ throw new Error("Telegram routing input lifetime ACK was not confirmed.");
1798
+ }
1799
+ const retained = deps.journal.read().entries;
1800
+ if (result.entries.some(entry => !isDeepStrictEqual(retained.find(value => value.updateId === entry.updateId), entry)) || !current()) {
1801
+ throw new Error("Telegram routing input lifetime ACK was not retained.");
1802
+ }
1803
+ result.entries.forEach((entry, index) => { sources[index].entry = structuredClone(entry); });
1804
+ pendingSignal = true;
1805
+ launchDrain();
1806
+ return { issued: result.issued, lifetime: { ...result.entries[0].routingInput } };
1807
+ };
1408
1808
  const drain = async (expectedOwner) => {
1409
1809
  while (owner === expectedOwner && !expectedOwner.controller.signal.aborted) {
1410
1810
  const authorityResult = checkAuthority(expectedOwner);
@@ -1414,11 +1814,18 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1414
1814
  let scheduledRetryAtMs;
1415
1815
  try {
1416
1816
  snapshot = deps.journal.read();
1417
- scheduledRetryAtMs = refreshJournalState(snapshot, expectedOwner);
1817
+ scheduledRetryAtMs = await refreshJournalState(snapshot, expectedOwner);
1818
+ // One complete validated baseline per generation, before batching or execution.
1819
+ // Replayed v1 entries do not prove that a prior process never forwarded them.
1820
+ expectedOwner.startupUpdateIds ??= new Set(snapshot.entries.map(entry => entry.updateId));
1418
1821
  }
1419
1822
  catch (error) {
1420
1823
  return blockWithFailure("journal-read", "journal-read", error);
1421
1824
  }
1825
+ if (await reconcileRoutingInputs(snapshot, expectedOwner))
1826
+ continue;
1827
+ if (owner !== expectedOwner || expectedOwner.controller.signal.aborted)
1828
+ return "aborted";
1422
1829
  const nowMs = getNowMs();
1423
1830
  const entries = [];
1424
1831
  let hasMoreEntries = false;
@@ -1489,9 +1896,101 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1489
1896
  break;
1490
1897
  }
1491
1898
  if (entry.preApprovalExcluded === true) {
1492
- completedUpdateIds.push(entry.updateId);
1899
+ completedUpdateIds.push({ updateId: entry.updateId });
1493
1900
  continue;
1494
1901
  }
1902
+ if (isTelegramUpdateJournalLegacyFamilyVersion(snapshot.version) && entry.state === "pending" && deps.journal.inspectPendingRetention) {
1903
+ const authorityResult = checkAuthority(expectedOwner, entry.updateId);
1904
+ if (authorityResult)
1905
+ return authorityResult;
1906
+ let retained;
1907
+ let unverifiable = false;
1908
+ try {
1909
+ retained = deps.journal.inspectPendingRetention(entry);
1910
+ }
1911
+ catch (error) {
1912
+ unverifiable = true;
1913
+ recordRuntimeEvent(error, { phase: "pending-retention-inspection", updateId: entry.updateId });
1914
+ }
1915
+ if (retained || unverifiable) {
1916
+ // A copy is protection evidence only. A fresh owner must explicitly retry
1917
+ // cancellation; neither startup nor a malformed copy may dispatch the source.
1918
+ claims.set(entry.updateId, "abandoning");
1919
+ const journalBindingKey = retained?.journalBindingKey ?? deps.getJournalBindingKey?.();
1920
+ if (journalBindingKey)
1921
+ deferredSources.set(entry.updateId, { entry, journalBindingKey });
1922
+ transition("deferred", entry.updateId);
1923
+ continue;
1924
+ }
1925
+ }
1926
+ if (isTelegramUpdateJournalLegacyFamilyVersion(snapshot.version) && (entry.state === "pending" || entry.state === "retry-wait")) {
1927
+ const historical = expectedOwner.startupUpdateIds.has(entry.updateId);
1928
+ const journalBindingKey = deps.getJournalBindingKey?.();
1929
+ // Classifiers and handler projections receive no mutable alias to CAS evidence.
1930
+ if (journalBindingKey)
1931
+ deferredSources.set(entry.updateId, { entry: structuredClone(entry), journalBindingKey,
1932
+ ...(historical ? { historical: true } : {}) });
1933
+ const classifyPending = historical && entry.state === "pending" ? deps.shouldReviewHistoricalInput : deps.shouldHoldPendingInput;
1934
+ if (classifyPending) {
1935
+ const checkHistoricalAuthority = () => {
1936
+ if (owner !== expectedOwner || expectedOwner.controller.signal.aborted)
1937
+ return "aborted";
1938
+ if (deps.getJournalBindingKey?.() !== journalBindingKey || deps.isContextCurrent?.(expectedOwner.ctx) === false) {
1939
+ transition("blocked", entry.updateId, "authority-lost");
1940
+ return "blocked";
1941
+ }
1942
+ return checkAuthority(expectedOwner, entry.updateId);
1943
+ };
1944
+ const beforeClassification = checkHistoricalAuthority();
1945
+ if (beforeClassification)
1946
+ return beforeClassification;
1947
+ let review;
1948
+ try {
1949
+ review = await classifyPending(structuredClone(entry), expectedOwner.ctx, expectedOwner.controller.signal);
1950
+ }
1951
+ catch (error) {
1952
+ const authorityResult = checkHistoricalAuthority();
1953
+ if (authorityResult)
1954
+ return authorityResult;
1955
+ return blockWithFailure("execution", "historical-review-classification", error, entry.updateId);
1956
+ }
1957
+ const authorityResult = checkHistoricalAuthority();
1958
+ if (authorityResult)
1959
+ return authorityResult;
1960
+ if (typeof review !== "boolean" && !(historical && entry.state === "pending" && review === "retain"))
1961
+ return blockWithFailure("execution", "historical-review-classification", new TelegramUpdateAdmissionOutcomeError("Historical classification must return a boolean or retain verdict."), entry.updateId);
1962
+ // Classification may await a domain read; accepted/changed source evidence wins.
1963
+ try {
1964
+ const current = deps.journal.read();
1965
+ if (current.version !== snapshot.version || JSON.stringify(current.entries.find(candidate => candidate.updateId === entry.updateId)) !== JSON.stringify(entry)) {
1966
+ snapshotInvalidated = true;
1967
+ break;
1968
+ }
1969
+ }
1970
+ catch (error) {
1971
+ return blockWithFailure("journal-read", "historical-source-recheck", error, entry.updateId);
1972
+ }
1973
+ if (review === "retain") {
1974
+ claims.set(entry.updateId, "retained");
1975
+ transition("deferred", entry.updateId);
1976
+ continue;
1977
+ }
1978
+ if (review && historical && spendHistorical && !entry.routingInput && classifyPending === deps.shouldReviewHistoricalInput) {
1979
+ completedUpdateIds.push({ updateId: entry.updateId, spent: true });
1980
+ continue;
1981
+ }
1982
+ if (review) {
1983
+ claims.set(entry.updateId, "historical");
1984
+ transition("deferred", entry.updateId);
1985
+ continue;
1986
+ }
1987
+ }
1988
+ if (entry.state === "pending" && entry.routingInput) {
1989
+ claims.set(entry.updateId, "historical");
1990
+ transition("deferred", entry.updateId);
1991
+ continue;
1992
+ }
1993
+ }
1495
1994
  clearRetryTimer();
1496
1995
  transition("executing", entry.updateId);
1497
1996
  const execution = await executeWithinOwner(expectedOwner, entry.update);
@@ -1532,7 +2031,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1532
2031
  for (const sourceUpdateId of receipt.sourceUpdateIds)
1533
2032
  claims.set(sourceUpdateId, "queued");
1534
2033
  try {
1535
- publishCommittedQueueReceipt(receipt, outcome.queueReceipt.queueOwner, expectedOwner.ctx);
2034
+ await publishCommittedQueueReceipt(receipt, outcome.queueReceipt.queueOwner, expectedOwner);
1536
2035
  }
1537
2036
  catch (error) {
1538
2037
  return blockWithFailure("invalid-outcome", "queue-receipt-publish", error, entry.updateId);
@@ -1547,7 +2046,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1547
2046
  return postExecutionAuthority;
1548
2047
  const claimableUpdateIds = new Set([
1549
2048
  entry.updateId,
1550
- ...claims.keys(),
2049
+ ...[...claims].filter(([, claim]) => claim !== "abandoning" && claim !== "historical" && claim !== "retained").map(([id]) => id),
1551
2050
  ]);
1552
2051
  let outcome;
1553
2052
  try {
@@ -1557,7 +2056,10 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1557
2056
  return blockWithFailure("invalid-outcome", "invalid-outcome", error, entry.updateId);
1558
2057
  }
1559
2058
  if (outcome.kind === "deferred") {
1560
- claims.set(entry.updateId, "deferred");
2059
+ if (outcome.routingReview && !deferredSources.get(entry.updateId)?.historical) {
2060
+ return blockWithFailure("invalid-outcome", "historical-review-source", new TelegramUpdateAdmissionOutcomeError("Historical review needs an exact startup source."), entry.updateId);
2061
+ }
2062
+ claims.set(entry.updateId, outcome.routingReview ? "historical" : "deferred");
1561
2063
  transition("deferred", entry.updateId);
1562
2064
  continue;
1563
2065
  }
@@ -1565,7 +2067,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1565
2067
  const completionResult = commitCompletedBatch(expectedOwner, completedUpdateIds);
1566
2068
  if (completionResult)
1567
2069
  return completionResult;
1568
- const queuedResult = commitQueuedOutcome(expectedOwner, entry.updateId, outcome);
2070
+ const queuedResult = await commitQueuedOutcome(expectedOwner, entry.updateId, outcome);
1569
2071
  if (queuedResult === "blocked" || queuedResult === "aborted") {
1570
2072
  return queuedResult;
1571
2073
  }
@@ -1573,7 +2075,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1573
2075
  snapshotInvalidated = true;
1574
2076
  break;
1575
2077
  }
1576
- completedUpdateIds.push(entry.updateId);
2078
+ completedUpdateIds.push({ updateId: entry.updateId, ...(outcome.expectedSource ? { expectedSource: outcome.expectedSource } : {}) });
1577
2079
  }
1578
2080
  const completionResult = commitCompletedBatch(expectedOwner, completedUpdateIds);
1579
2081
  if (completionResult)
@@ -1592,6 +2094,14 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1592
2094
  const expectedOwner = owner;
1593
2095
  if (!expectedOwner || drainPromise)
1594
2096
  return;
2097
+ let startupKey;
2098
+ if (onHeldSourcesPrepared) {
2099
+ try {
2100
+ startupKey = deps.getJournalBindingKey?.();
2101
+ }
2102
+ catch { /* Missing startup identity grants no notification. */ }
2103
+ }
2104
+ let lastResult;
1595
2105
  const run = async () => {
1596
2106
  while (pendingSignal &&
1597
2107
  owner === expectedOwner &&
@@ -1599,7 +2109,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1599
2109
  pendingSignal = false;
1600
2110
  // Keep newer wakes in this run so waitForDrain also covers them. A failure
1601
2111
  // observed after signal() still keeps its latch; this loop never clears it.
1602
- await drain(expectedOwner);
2112
+ lastResult = await drain(expectedOwner);
1603
2113
  }
1604
2114
  };
1605
2115
  const operation = run();
@@ -1610,6 +2120,32 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1610
2120
  if (pendingSignal && !expectedOwner.controller.signal.aborted) {
1611
2121
  launchDrain();
1612
2122
  }
2123
+ else if (lastResult === "idle" && startupKey && onHeldSourcesPrepared && !expectedOwner.heldSourcesPreparedIssued) {
2124
+ try {
2125
+ const ctx = expectedOwner.ctx, key = startupKey;
2126
+ const isCurrent = () => owner === expectedOwner && !blocked && !expectedOwner.controller.signal.aborted &&
2127
+ deps.getJournalBindingKey?.() === key && deps.isContextCurrent?.(ctx) !== false && deps.hasAuthority(ctx);
2128
+ if (!isCurrent())
2129
+ return;
2130
+ expectedOwner.heldSourcesPreparedIssued = true;
2131
+ // Do not couple lifecycle/stop or the worker drain to a held controller/API reply.
2132
+ void Promise.resolve().then(() => isCurrent() ? onHeldSourcesPrepared({ ctx,
2133
+ journalBindingKey: key, signal: expectedOwner.controller.signal, isCurrent,
2134
+ routingSourceIds: [...deferredSources].filter(([, source]) => source.journalBindingKey === key && !!source.entry.routingInput)
2135
+ .map(([id]) => id) }) : undefined).catch(error => {
2136
+ try {
2137
+ deps.recordRuntimeEvent?.("inbound-worker", error, { phase: "held-source-preparation" });
2138
+ }
2139
+ catch { /* Diagnostic only. */ }
2140
+ });
2141
+ }
2142
+ catch (error) {
2143
+ try {
2144
+ deps.recordRuntimeEvent?.("inbound-worker", error, { phase: "held-source-preparation" });
2145
+ }
2146
+ catch { /* Diagnostic only. */ }
2147
+ }
2148
+ }
1613
2149
  }
1614
2150
  };
1615
2151
  tracked = operation.then(() => finish(), (error) => {
@@ -1621,6 +2157,76 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1621
2157
  });
1622
2158
  drainPromise = tracked;
1623
2159
  };
2160
+ const abandonSource = (input, historicalSource) => {
2161
+ const expectedOwner = owner;
2162
+ const source = deferredSources.get(input.updateId);
2163
+ const abandonPending = deps.journal.abandonPending;
2164
+ const isCurrent = () => {
2165
+ if (!expectedOwner || owner !== expectedOwner || input.signal.aborted ||
2166
+ expectedOwner.controller.signal !== input.signal || !source ||
2167
+ source.entry.routingInput?.phase === "selected" || (source.historical && historicalSource !== source) ||
2168
+ (claims.get(input.updateId) !== "deferred" && claims.get(input.updateId) !== "abandoning" &&
2169
+ !(claims.get(input.updateId) === "historical" && historicalSource === source)))
2170
+ return false;
2171
+ return input.isCurrent() && deps.isContextCurrent?.(expectedOwner.ctx) !== false &&
2172
+ deps.getJournalBindingKey?.() === source.journalBindingKey && deps.hasAuthority(expectedOwner.ctx);
2173
+ };
2174
+ if (!source || !abandonPending || !isCurrent())
2175
+ return undefined;
2176
+ // Retain this claim on failure, including unknown commit: never reopen dispatch.
2177
+ claims.set(input.updateId, "abandoning");
2178
+ try {
2179
+ const result = abandonPending({ ...input, entry: source.entry,
2180
+ journalBindingKey: source.journalBindingKey, isCurrent });
2181
+ claims.delete(input.updateId);
2182
+ deferredSources.delete(input.updateId);
2183
+ state.journalEntryCount = result.entryCount;
2184
+ state.journalSerializedBytes = result.serializedBytes;
2185
+ transition("idle");
2186
+ pendingSignal = true;
2187
+ launchDrain();
2188
+ return result;
2189
+ }
2190
+ catch (error) {
2191
+ recordRuntimeEvent(error, { phase: "pending-abandonment", updateId: input.updateId });
2192
+ updateClaimCounts();
2193
+ notifyStateChange();
2194
+ throw error;
2195
+ }
2196
+ };
2197
+ const inspectRecovery = (input, kind) => {
2198
+ const expectedOwner = owner;
2199
+ if (!expectedOwner)
2200
+ return undefined;
2201
+ const { signal, journalBindingKey, isCurrent: callerIsCurrent, afterUpdateId } = input;
2202
+ const isOwnerCurrent = () => owner === expectedOwner && expectedOwner.controller.signal === signal && !signal.aborted;
2203
+ const isCurrent = () => isOwnerCurrent() && callerIsCurrent() && deps.isContextCurrent?.(expectedOwner.ctx) !== false &&
2204
+ deps.getJournalBindingKey?.() === journalBindingKey && deps.hasAuthority(expectedOwner.ctx);
2205
+ if (!isCurrent() || !deps.journal.abandonPending || !deps.journal.inspectPendingRetention ||
2206
+ (afterUpdateId !== undefined && (!Number.isSafeInteger(afterUpdateId) || afterUpdateId < 0)))
2207
+ return undefined;
2208
+ const matches = [...deferredSources].filter(([id, source]) => claims.get(id) === kind &&
2209
+ source.journalBindingKey === journalBindingKey && (afterUpdateId === undefined || id > afterUpdateId))
2210
+ .sort(([a], [b]) => a - b);
2211
+ const sources = matches.slice(0, TELEGRAM_HELD_SOURCE_INSPECTION_LIMIT).map(([updateId, source]) => {
2212
+ let committed;
2213
+ return { original: structuredClone(source.entry), retry(authority) {
2214
+ const authorized = () => isOwnerCurrent() && authority.isCurrent() && isCurrent();
2215
+ if (!authorized())
2216
+ return undefined;
2217
+ if (committed)
2218
+ return { ...committed, duplicate: true };
2219
+ const stillProtected = () => authorized() && (claims.get(updateId) === kind || claims.get(updateId) === "abandoning") &&
2220
+ deferredSources.get(updateId) === source;
2221
+ if (!stillProtected())
2222
+ return undefined;
2223
+ committed = abandonSource({ ...authority, updateId, signal, isCurrent: stillProtected }, source);
2224
+ return committed;
2225
+ } };
2226
+ });
2227
+ return isCurrent() ? { sources,
2228
+ ...(matches.length > TELEGRAM_HELD_SOURCE_INSPECTION_LIMIT ? { nextAfterUpdateId: matches[TELEGRAM_HELD_SOURCE_INSPECTION_LIMIT - 1][0] } : {}) } : undefined;
2229
+ };
1624
2230
  return {
1625
2231
  start(ctx) {
1626
2232
  if (owner) {
@@ -1631,6 +2237,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1631
2237
  return;
1632
2238
  }
1633
2239
  clearRetryTimer();
2240
+ clearRoutingTimer();
1634
2241
  const nowMs = getNowMs();
1635
2242
  const generation = ++nextGeneration;
1636
2243
  const queueOwnerIdentity = resolveQueueOwnerIdentity(ctx, generation);
@@ -1641,6 +2248,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1641
2248
  queueOwnerIdentity,
1642
2249
  };
1643
2250
  claims.clear();
2251
+ deferredSources.clear();
1644
2252
  committedQueueReceipts.clear();
1645
2253
  blocked = false;
1646
2254
  pendingSignal = true;
@@ -1653,6 +2261,8 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1653
2261
  state.journalSerializedBytes = 0;
1654
2262
  state.oldestAdmittedAtMs = undefined;
1655
2263
  state.deferredClaimCount = 0;
2264
+ delete state.abandoningClaimCount;
2265
+ delete state.historicalClaimCount;
1656
2266
  state.queuedClaimCount = 0;
1657
2267
  state.foreignQueuedCount = 0;
1658
2268
  delete state.foreignQueuedOwner;
@@ -1683,7 +2293,56 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1683
2293
  pendingSignal = true;
1684
2294
  launchDrain();
1685
2295
  },
1686
- settleDeferred(input) {
2296
+ armRoutingInput(input) { return mutateRoutingSource(input, false)?.lifetime; },
2297
+ selectRoutingInput(input) { return mutateRoutingSource(input, true)?.issued === true; },
2298
+ isRoutingInputCurrent(input) {
2299
+ return owner?.controller.signal === input.signal && !input.signal.aborted &&
2300
+ deferredSources.has(input.updateId) && claims.get(input.updateId) !== "abandoning";
2301
+ },
2302
+ inspectDeferredSource(input) {
2303
+ const expectedOwner = owner;
2304
+ const source = deferredSources.get(input.updateId);
2305
+ const current = () => {
2306
+ if (!expectedOwner || owner !== expectedOwner || input.signal.aborted || expectedOwner.controller.signal !== input.signal ||
2307
+ !source || deferredSources.get(input.updateId) !== source || claims.get(input.updateId) !== "deferred" ||
2308
+ deps.getJournalBindingKey?.() !== source.journalBindingKey || deps.isContextCurrent?.(expectedOwner.ctx) === false ||
2309
+ !deps.hasAuthority(expectedOwner.ctx))
2310
+ return false;
2311
+ return isQueueOwnerIdentityCurrent(expectedOwner);
2312
+ };
2313
+ if (!current())
2314
+ return undefined;
2315
+ const snapshot = deps.journal.read();
2316
+ const entry = snapshot.entries.find(value => value.updateId === input.updateId);
2317
+ if (!isTelegramUpdateJournalLegacyFamilyVersion(snapshot.version) || !entry ||
2318
+ JSON.stringify(entry) !== JSON.stringify(source.entry) || !current())
2319
+ return undefined;
2320
+ return { journalBindingKey: source.journalBindingKey, ...createTelegramUpdateJournalEntryDigest(entry) };
2321
+ },
2322
+ isHistoricalSource(input) {
2323
+ const source = deferredSources.get(input.updateId);
2324
+ const isCurrent = () => owner?.controller.signal === input.signal && !input.signal.aborted &&
2325
+ source?.historical === true && deferredSources.get(input.updateId) === source;
2326
+ if (!isCurrent())
2327
+ return false;
2328
+ // Domain eligibility observes original evidence, never mutable handler projections.
2329
+ return (!input.matchesOriginal || input.matchesOriginal(structuredClone(source.entry)) === true) && isCurrent();
2330
+ },
2331
+ supportsDeferredAbandonment(input) {
2332
+ const expectedOwner = owner;
2333
+ const source = deferredSources.get(input.updateId);
2334
+ if (!expectedOwner || input.signal.aborted || expectedOwner.controller.signal !== input.signal ||
2335
+ !source || source.historical || input.journalBindingKey !== source.journalBindingKey ||
2336
+ !deps.journal.abandonPending || !deps.journal.inspectPendingRetention ||
2337
+ claims.get(input.updateId) === "queued")
2338
+ return false;
2339
+ return deps.isContextCurrent?.(expectedOwner.ctx) !== false &&
2340
+ deps.getJournalBindingKey?.() === source.journalBindingKey && deps.hasAuthority(expectedOwner.ctx);
2341
+ },
2342
+ abandonDeferred(input) { return abandonSource(input); },
2343
+ inspectAbandoning(input) { return inspectRecovery(input, "abandoning"); },
2344
+ inspectHistorical(input) { return inspectRecovery(input, "historical"); },
2345
+ async settleDeferred(input) {
1687
2346
  const expectedOwner = owner;
1688
2347
  if (!expectedOwner ||
1689
2348
  expectedOwner.controller.signal !== input.signal ||
@@ -1700,11 +2359,25 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1700
2359
  return;
1701
2360
  }
1702
2361
  const claim = claims.get(input.updateId);
2362
+ if (claim === "abandoning" || claim === "historical" || claim === "retained" ||
2363
+ (!claim && routingJournal?.inspectExpiry(input.updateId)))
2364
+ return;
1703
2365
  if (input.outcome.kind === "complete") {
1704
2366
  // Expiry may retire only a still-deferred source, never accepted queue work.
1705
2367
  if (claim !== "deferred")
1706
2368
  return;
1707
- const result = commitCompletedBatch(expectedOwner, [input.updateId]);
2369
+ let completion;
2370
+ try {
2371
+ completion = validateTelegramUpdateAdmissionOutcome(input.outcome, input.updateId, new Set([input.updateId]));
2372
+ }
2373
+ catch (error) {
2374
+ blockWithFailure("invalid-outcome", "late-invalid-completion", error, input.updateId);
2375
+ return;
2376
+ }
2377
+ if (completion.kind !== "complete")
2378
+ return;
2379
+ const result = commitCompletedBatch(expectedOwner, [{ updateId: input.updateId,
2380
+ ...(completion.expectedSource ? { expectedSource: completion.expectedSource } : {}) }]);
1708
2381
  if (!result)
1709
2382
  transition("idle", input.updateId);
1710
2383
  return;
@@ -1721,7 +2394,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1721
2394
  }
1722
2395
  let outcome;
1723
2396
  try {
1724
- outcome = validateTelegramUpdateAdmissionOutcome(input.outcome, input.updateId, new Set([input.updateId, ...claims.keys()]));
2397
+ outcome = validateTelegramUpdateAdmissionOutcome(input.outcome, input.updateId, new Set([input.updateId, ...[...claims].filter(([, claim]) => claim !== "abandoning" && claim !== "historical" && claim !== "retained").map(([id]) => id)]));
1725
2398
  }
1726
2399
  catch (error) {
1727
2400
  blockWithFailure("invalid-outcome", "late-invalid-outcome", error, input.updateId);
@@ -1737,7 +2410,8 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1737
2410
  releaseDeferredClaims(outcome.sourceUpdateIds);
1738
2411
  return;
1739
2412
  }
1740
- const result = commitQueuedOutcome(expectedOwner, input.updateId, outcome);
2413
+ const publication = commitQueuedOutcome(expectedOwner, input.updateId, outcome);
2414
+ const result = typeof publication === "string" ? publication : await publication;
1741
2415
  if (result === "blocked") {
1742
2416
  releaseDeferredClaims(outcome.sourceUpdateIds);
1743
2417
  return;
@@ -1746,7 +2420,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1746
2420
  return;
1747
2421
  transition("queued", input.updateId);
1748
2422
  },
1749
- settleCustodied(input) {
2423
+ async settleCustodied(input) {
1750
2424
  const expectedOwner = owner;
1751
2425
  if (!expectedOwner || expectedOwner.controller.signal !== input.signal || input.signal.aborted)
1752
2426
  return;
@@ -1791,7 +2465,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1791
2465
  for (const sourceUpdateId of receipt.sourceUpdateIds)
1792
2466
  claims.set(sourceUpdateId, "queued");
1793
2467
  try {
1794
- publishCommittedQueueReceipt(receipt, input.result.queueReceipt.queueOwner, expectedOwner.ctx);
2468
+ await publishCommittedQueueReceipt(receipt, input.result.queueReceipt.queueOwner, expectedOwner);
1795
2469
  }
1796
2470
  catch (error) {
1797
2471
  blockWithFailure("invalid-outcome", "queue-receipt-publish", error, input.updateId);
@@ -1804,16 +2478,32 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1804
2478
  const committed = getCurrentQueueReceipt(receipt);
1805
2479
  return committed ? { ...committed.queueOwner } : undefined;
1806
2480
  },
1807
- completeQueueReceipts(input) {
2481
+ getQueueReceiptSettlementOwner(receipt, ctx, reason) {
2482
+ const expectedOwner = owner, committed = committedQueueReceipts.get(receipt.receiptId);
2483
+ if (!expectedOwner || expectedOwner.controller.signal.aborted ||
2484
+ !(deps.isContextCurrent?.(ctx) ?? expectedOwner.ctx === ctx) || !isQueueOwnerIdentityCurrent(expectedOwner) ||
2485
+ !committed || !areTelegramQueueAdmissionReceiptsEqual(committed.receipt, normalizeQueueReceipt(receipt)) ||
2486
+ (deps.getJournalBindingKey && committed.receipt.journalBindingKey !== deps.getJournalBindingKey()) ||
2487
+ !isTelegramUpdateJournalQueueOwnerProcess(committed.queueOwner, expectedOwner.queueOwnerIdentity))
2488
+ return undefined;
2489
+ const attempt = scopedQueueCompletionAttempts.get(receipt.receiptId);
2490
+ if (attempt ? attempt.owner !== expectedOwner || attempt.reason !== reason || attempt.journalBindingKey !== deps.getJournalBindingKey?.()
2491
+ : !getCurrentQueueReceipt(receipt))
2492
+ return undefined;
2493
+ return { ...committed.queueOwner };
2494
+ },
2495
+ completeQueueReceipts: function completeQueueReceipts(input) {
1808
2496
  const expectedOwner = owner;
1809
2497
  if (!expectedOwner ||
1810
2498
  expectedOwner.controller.signal.aborted ||
1811
2499
  !(deps.isContextCurrent?.(input.ctx) ?? expectedOwner.ctx === input.ctx)) {
1812
2500
  return false;
1813
2501
  }
1814
- if (input.receipts.length === 0)
1815
- return true;
1816
- const normalizedReceipts = input.receipts.map(normalizeQueueReceipt);
2502
+ // An ACK permits only local completion reconciliation, never execution readiness.
2503
+ const receipts = input.sourceCompletions === undefined ? input.receipts.filter(receipt => !isQueueReceiptCompletionAcknowledged(receipt)) : input.receipts;
2504
+ if (receipts.length === 0)
2505
+ return input.sourceCompletions === undefined;
2506
+ const normalizedReceipts = receipts.map(normalizeQueueReceipt);
1817
2507
  const receiptIds = new Set();
1818
2508
  const sourceUpdateIds = new Set();
1819
2509
  const queuedCompletions = [];
@@ -1837,9 +2527,91 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1837
2527
  sourceUpdateIds.add(updateId);
1838
2528
  }
1839
2529
  }
2530
+ const completionBinding = deps.getJournalBindingKey?.();
2531
+ const ordinaryObserverBound = normalizedReceipts.every(receipt => committedQueueReceipts.get(receipt.receiptId).receipt.journalBindingKey === completionBinding);
2532
+ const ordinaryCompletionCurrent = () => ordinaryObserverBound && owner === expectedOwner &&
2533
+ !expectedOwner.controller.signal.aborted && (deps.isContextCurrent?.(input.ctx) ?? expectedOwner.ctx === input.ctx) &&
2534
+ isQueueOwnerIdentityCurrent(expectedOwner) && deps.getJournalBindingKey?.() === completionBinding;
1840
2535
  let removedUpdateIds;
2536
+ let scopedCompletionCurrent;
1841
2537
  try {
1842
- removedUpdateIds = deps.journal.completeQueued(queuedCompletions).removedUpdateIds;
2538
+ const prepared = normalizedReceipts.flatMap(receipt => committedQueueReceipts.get(receipt.receiptId).sourceCompletions ?? []);
2539
+ const sourceCompletions = input.sourceCompletions === undefined ? (prepared.length ? prepared : undefined) : input.sourceCompletions;
2540
+ if (input.sourceCompletions !== undefined && prepared.some(expected => !input.sourceCompletions.some(value => isDeepStrictEqual(value, expected)))) {
2541
+ throw new Error("Telegram queued completion conflicts with its prepared source scopes.");
2542
+ }
2543
+ if (input.sourceCompletions === undefined && prepared.length && receipts.some(receipt => !requiredScopedQueueReceipts.has(receipt.receiptId))) {
2544
+ const scoped = receipts.filter(receipt => requiredScopedQueueReceipts.has(receipt.receiptId));
2545
+ const ordinary = receipts.filter(receipt => !requiredScopedQueueReceipts.has(receipt.receiptId));
2546
+ if (!scoped.length || !ordinary.length)
2547
+ throw new Error("Telegram mixed queue completion has incomplete scoped authority.");
2548
+ const binding = deps.getJournalBindingKey?.();
2549
+ const current = () => owner === expectedOwner && !expectedOwner.controller.signal.aborted &&
2550
+ (deps.isContextCurrent?.(input.ctx) ?? expectedOwner.ctx === input.ctx) && isQueueOwnerIdentityCurrent(expectedOwner) &&
2551
+ !!binding && deps.getJournalBindingKey?.() === binding;
2552
+ // Independent whole receipts need independent ACKs. Ordinary siblings must not acquire invented scopes.
2553
+ if (!current() || !completeQueueReceipts({ receipts: scoped, ctx: input.ctx, reason: input.reason }) || !current())
2554
+ return false;
2555
+ return completeQueueReceipts({ receipts: ordinary, ctx: input.ctx, reason: input.reason });
2556
+ }
2557
+ if (sourceCompletions === undefined) {
2558
+ if (queuedCompletions.some(receipt => requiredScopedQueueReceipts.has(receipt.receiptId))) {
2559
+ throw new Error("Telegram scoped queue completion cannot downgrade required source scopes.");
2560
+ }
2561
+ removedUpdateIds = deps.journal.completeQueued(queuedCompletions).removedUpdateIds;
2562
+ }
2563
+ else {
2564
+ for (const receipt of queuedCompletions)
2565
+ requiredScopedQueueReceipts.add(receipt.receiptId);
2566
+ queuedCompletions.sort((a, b) => a.receiptId < b.receiptId ? -1 : a.receiptId > b.receiptId ? 1 : 0);
2567
+ const journalBindingKey = deps.getJournalBindingKey?.();
2568
+ const current = () => owner === expectedOwner && !expectedOwner.controller.signal.aborted &&
2569
+ (deps.isContextCurrent?.(input.ctx) ?? expectedOwner.ctx === input.ctx) && isQueueOwnerIdentityCurrent(expectedOwner) &&
2570
+ deps.getJournalBindingKey?.() === journalBindingKey;
2571
+ if (!journalBindingKey || !current() || !completeQueuedExact || !inspectCompletion ||
2572
+ !Array.isArray(sourceCompletions) || input.sourceCompletions !== undefined && sourceCompletions.length !== sourceUpdateIds.size) {
2573
+ throw new Error("Telegram scoped queue completion requires full current source authority.");
2574
+ }
2575
+ scopedCompletionCurrent = current;
2576
+ const completions = normalizeTelegramQueueSourceCompletions(sourceCompletions, sourceUpdateIds, journalBindingKey, input.sourceCompletions !== undefined)
2577
+ .map(({ journalBindingKey: _binding, ...completion }) => completion);
2578
+ if (queuedCompletions.some(receipt => !completions.some(scope => receipt.sourceUpdateIds.includes(scope.updateId)))) {
2579
+ throw new Error("Telegram scoped queue completion requires a proven origin for every whole receipt.");
2580
+ }
2581
+ const previous = queuedCompletions.map(receipt => scopedQueueCompletionAttempts.get(receipt.receiptId));
2582
+ const retained = previous.find(attempt => attempt !== undefined);
2583
+ if (retained && (previous.some(attempt => attempt !== retained) || retained.owner !== expectedOwner ||
2584
+ retained.journalBindingKey !== journalBindingKey || retained.reason !== input.reason ||
2585
+ !isDeepStrictEqual(retained.receipts, queuedCompletions) || !isDeepStrictEqual(retained.completions, completions))) {
2586
+ throw new Error("Telegram scoped queue completion has conflicting issued authority.");
2587
+ }
2588
+ if (!retained) {
2589
+ const attempt = { owner: expectedOwner, journalBindingKey, reason: input.reason,
2590
+ receipts: queuedCompletions.map(receipt => ({ ...receipt, sourceUpdateIds: [...receipt.sourceUpdateIds], queueOwner: { ...receipt.queueOwner } })),
2591
+ completions: completions.map(completion => ({ ...completion })) };
2592
+ if (!current())
2593
+ throw new Error("Telegram scoped queue completion authority changed before disposal.");
2594
+ // Mark before issuance: an exception may follow a committed rename, and cannot license another disposal.
2595
+ for (const receipt of queuedCompletions)
2596
+ scopedQueueCompletionAttempts.set(receipt.receiptId, attempt);
2597
+ const result = completeQueuedExact(queuedCompletions.map(receipt => ({ ...receipt, sourceUpdateIds: [...receipt.sourceUpdateIds], queueOwner: { ...receipt.queueOwner } })), completions.map(completion => ({ ...completion })));
2598
+ if (!current() || !isDeepStrictEqual(result.sourceCompletions, completions)) {
2599
+ throw new Error("Telegram scoped queue completion ACK was not confirmed.");
2600
+ }
2601
+ const removed = new Set(result.removedUpdateIds);
2602
+ if (removed.size !== sourceUpdateIds.size || [...sourceUpdateIds].some(id => !removed.has(id))) {
2603
+ throw new Error("Telegram scoped queue completion did not remove every source.");
2604
+ }
2605
+ }
2606
+ for (const completion of completions) {
2607
+ if (!current() || !isDeepStrictEqual(inspectCompletion({ ...completion }), completion) || !current()) {
2608
+ throw new Error("Telegram scoped queue completion ACK was not retained under current authority.");
2609
+ }
2610
+ }
2611
+ // Every immutable whole receipt has a scoped queued-origin witness; native queued ACK continuity proves its complete removal.
2612
+ // Unscoped siblings receive only receipt acknowledgement, never a source marker or absence-based proof.
2613
+ removedUpdateIds = [...sourceUpdateIds];
2614
+ }
1843
2615
  }
1844
2616
  catch (error) {
1845
2617
  blockWithFailure("journal-write", "queue-receipt-completion", error);
@@ -1851,13 +2623,15 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1851
2623
  blockWithFailure("journal-write", "queue-receipt-completion", new Error(`Telegram ${input.reason} did not complete every receipt source.`));
1852
2624
  return false;
1853
2625
  }
1854
- input.receipts.forEach((receipt, index) => {
2626
+ receipts.forEach((receipt, index) => {
1855
2627
  acknowledgedQueueReceiptCompletions.set(receipt, normalizedReceipts[index]);
1856
2628
  });
1857
2629
  for (const updateId of sourceUpdateIds)
1858
2630
  claims.delete(updateId);
1859
2631
  for (const receiptId of receiptIds) {
1860
2632
  committedQueueReceipts.delete(receiptId);
2633
+ requiredScopedQueueReceipts.delete(receiptId);
2634
+ scopedQueueCompletionAttempts.delete(receiptId);
1861
2635
  }
1862
2636
  const completedUpdateIds = [...sourceUpdateIds];
1863
2637
  state.lastCompletedUpdateId = Math.max(state.lastCompletedUpdateId ?? -1, ...completedUpdateIds);
@@ -1865,8 +2639,33 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1865
2639
  state.journalEntryCount = Math.max(0, state.journalEntryCount - completedUpdateIds.length);
1866
2640
  updateClaimCounts();
1867
2641
  notifyStateChange();
1868
- pendingSignal = true;
1869
- launchDrain();
2642
+ if (scopedCompletionCurrent)
2643
+ for (const receipt of normalizedReceipts) {
2644
+ if (!scopedCompletionCurrent())
2645
+ break;
2646
+ try {
2647
+ observeQueuedCompletion?.(normalizeQueueReceipt(receipt), input.ctx);
2648
+ }
2649
+ catch (error) {
2650
+ recordRuntimeEvent(error, { phase: "queue-receipt-completion-observer", receiptId: receipt.receiptId });
2651
+ }
2652
+ }
2653
+ // Ordinary queued disposal is source completion too; notify only after a whole positive ACK and fresh origin authority.
2654
+ if (!scopedCompletionCurrent)
2655
+ for (const updateId of completedUpdateIds) {
2656
+ try {
2657
+ if (!ordinaryCompletionCurrent())
2658
+ break;
2659
+ deps.onUpdateCompleted?.(updateId, input.ctx, completionBinding);
2660
+ }
2661
+ catch (error) {
2662
+ recordRuntimeEvent(error, { phase: "queue-source-completion-observer", updateId });
2663
+ }
2664
+ }
2665
+ if (!scopedCompletionCurrent || scopedCompletionCurrent()) {
2666
+ pendingSignal = true;
2667
+ launchDrain();
2668
+ }
1870
2669
  return true;
1871
2670
  },
1872
2671
  async stop() {
@@ -1875,6 +2674,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1875
2674
  return;
1876
2675
  pendingSignal = false;
1877
2676
  clearRetryTimer();
2677
+ clearRoutingTimer();
1878
2678
  expectedOwner.controller.abort();
1879
2679
  await drainPromise?.catch(() => undefined);
1880
2680
  if (owner !== expectedOwner)
@@ -1882,6 +2682,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1882
2682
  owner = undefined;
1883
2683
  drainPromise = undefined;
1884
2684
  claims.clear();
2685
+ deferredSources.clear();
1885
2686
  committedQueueReceipts.clear();
1886
2687
  blocked = false;
1887
2688
  state.phase = "stopped";
@@ -1889,6 +2690,8 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1889
2690
  state.currentUpdateId = undefined;
1890
2691
  state.blockedReason = undefined;
1891
2692
  state.deferredClaimCount = 0;
2693
+ delete state.abandoningClaimCount;
2694
+ delete state.historicalClaimCount;
1892
2695
  state.queuedClaimCount = 0;
1893
2696
  state.foreignQueuedCount = 0;
1894
2697
  delete state.foreignQueuedOwner;
@@ -1907,8 +2710,9 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1907
2710
  state.unsettledExecutionCount = unsettledExecutions.size;
1908
2711
  notifyStateChange();
1909
2712
  },
1910
- waitForDrain() {
1911
- return drainPromise ?? Promise.resolve();
2713
+ async waitForDrain() {
2714
+ await (drainPromise ?? Promise.resolve());
2715
+ await Promise.allSettled([...pendingQueuePublications.values()].map(value => value.task));
1912
2716
  },
1913
2717
  getState() {
1914
2718
  return getTelegramUpdateWorkerStateSnapshot(state);
@@ -2019,8 +2823,21 @@ function mergeTelegramReportedAdmissionOutcome(current, next, updateId) {
2019
2823
  return next;
2020
2824
  if (next.kind === "deferred")
2021
2825
  return current;
2022
- if (current.kind === "complete" && next.kind === "complete")
2023
- return current;
2826
+ if (current.kind === "complete" && next.kind === "complete") {
2827
+ if (!current.expectedSource)
2828
+ return next;
2829
+ if (!next.expectedSource)
2830
+ return current;
2831
+ if (current.expectedSource.updateId === next.expectedSource.updateId &&
2832
+ current.expectedSource.journalBindingKey === next.expectedSource.journalBindingKey &&
2833
+ current.expectedSource.sourceSha256 === next.expectedSource.sourceSha256) {
2834
+ if (!current.expectedSource.completionSha256)
2835
+ return next;
2836
+ if (!next.expectedSource.completionSha256 || current.expectedSource.completionSha256 === next.expectedSource.completionSha256)
2837
+ return current;
2838
+ }
2839
+ throw new TelegramUpdateAdmissionOutcomeError(`Telegram update ${updateId} reported conflicting completion sources.`);
2840
+ }
2024
2841
  if (current.kind === "queued" && next.kind === "queued" &&
2025
2842
  areTelegramQueueAdmissionReceiptsEqual(current, next))
2026
2843
  return current;
@@ -2736,13 +3553,22 @@ export function createTelegramUpdateAdmissionHandle(deps) {
2736
3553
  let nextExecutionGeneration = 0;
2737
3554
  return async (update, ctx, signal) => {
2738
3555
  const generation = ++nextExecutionGeneration;
3556
+ let suspended = false;
3557
+ let routingClaims = 0;
3558
+ let abandoned;
3559
+ let routingLifetime;
3560
+ let routingSourceIds;
3561
+ let routingClockUnknown = false;
3562
+ let routingSelectionAttempted = false;
3563
+ let routingSelectionConfirmed = false;
2739
3564
  const execution = {
2740
3565
  generation,
2741
3566
  updateId: update.update_id,
2742
3567
  signal,
2743
- isCurrent: () => !signal.aborted,
3568
+ isCurrent: () => !signal.aborted && !suspended && ((!routingLifetime && !routingClockUnknown) || immediate || settlementReported ||
3569
+ deps.isRoutingInputCurrent?.({ updateId: update.update_id, signal }) !== false),
2744
3570
  assertCurrent() {
2745
- if (signal.aborted) {
3571
+ if (!execution.isCurrent()) {
2746
3572
  throw signal.reason ?? new DOMException("Aborted", "AbortError");
2747
3573
  }
2748
3574
  },
@@ -2753,8 +3579,19 @@ export function createTelegramUpdateAdmissionHandle(deps) {
2753
3579
  if (verdict === "consume")
2754
3580
  return { kind: "complete" };
2755
3581
  let immediate = true;
3582
+ let settlementReported = false;
2756
3583
  let outcome;
2757
3584
  const boundUpdate = bindTelegramUpdateExecutionFence(bindTelegramUpdateAdmissionSource(update, (next) => {
3585
+ if (!execution.isCurrent())
3586
+ return;
3587
+ if (next.kind === "deferred" && next.routingReview) {
3588
+ if (!immediate || settlementReported || routingClaims > 0 || !deps.isHistoricalSource?.({ updateId: update.update_id, signal })) {
3589
+ throw new TelegramUpdateAdmissionOutcomeError("Historical review cannot override accepted or selected work.");
3590
+ }
3591
+ suspended = true;
3592
+ }
3593
+ if (next.kind !== "deferred")
3594
+ settlementReported = true;
2758
3595
  if (immediate) {
2759
3596
  outcome = mergeTelegramReportedAdmissionOutcome(outcome, next, update.update_id);
2760
3597
  return;
@@ -2763,11 +3600,15 @@ export function createTelegramUpdateAdmissionHandle(deps) {
2763
3600
  throw new TelegramUpdateAdmissionOutcomeError(`Telegram update ${update.update_id} reported a late outcome without an owner.`);
2764
3601
  }
2765
3602
  void Promise.resolve()
2766
- .then(() => deps.onLateOutcome(next, {
2767
- updateId: update.update_id,
2768
- ctx,
2769
- signal,
2770
- }))
3603
+ .then(() => {
3604
+ if (!execution.isCurrent())
3605
+ return;
3606
+ return deps.onLateOutcome(next, {
3607
+ updateId: update.update_id,
3608
+ ctx,
3609
+ signal,
3610
+ });
3611
+ })
2771
3612
  .catch((error) => {
2772
3613
  try {
2773
3614
  deps.onLateOutcomeError?.(error, update.update_id);
@@ -2776,11 +3617,81 @@ export function createTelegramUpdateAdmissionHandle(deps) {
2776
3617
  // Diagnostic sinks must not create an unhandled late Promise.
2777
3618
  }
2778
3619
  });
2779
- }), execution);
3620
+ }, { abandon: (authority) => {
3621
+ const isCurrent = () => !signal.aborted && routingClaims === 0 && !settlementReported && authority.isCurrent();
3622
+ if (immediate || outcome?.kind !== "deferred" || !deps.abandonDeferred || !isCurrent())
3623
+ return undefined;
3624
+ if (abandoned)
3625
+ return { ...abandoned, duplicate: true };
3626
+ const wasSuspended = suspended;
3627
+ suspended = true;
3628
+ // Failed/unknown publication keeps dispatch suspended for exact cancellation retry.
3629
+ // An ineligible request that attempted no publication remains inert.
3630
+ const result = deps.abandonDeferred({ ...authority, updateId: update.update_id, signal, isCurrent });
3631
+ if (result)
3632
+ abandoned = result;
3633
+ else
3634
+ suspended = wasSuspended;
3635
+ return result;
3636
+ }, inspectAbandoning: request => execution.isCurrent()
3637
+ ? deps.inspectAbandoning?.({ ...request, signal }) : undefined,
3638
+ inspectHistorical: request => execution.isCurrent() ? deps.inspectHistorical?.({ ...request, signal }) : undefined,
3639
+ inspectSource: () => execution.isCurrent() && !immediate && !settlementReported
3640
+ ? deps.inspectDeferredSource?.({ updateId: update.update_id, signal }) : undefined,
3641
+ isHistorical: matchesOriginal => !signal.aborted && deps.isHistoricalSource?.({ updateId: update.update_id, signal,
3642
+ ...(matchesOriginal ? { matchesOriginal } : {}) }) === true,
3643
+ isHistoricalReviewHeld: () => !signal.aborted && suspended && outcome?.kind === "deferred" && outcome.routingReview === true,
3644
+ supportsAbandonment: journalBindingKey => !signal.aborted && !settlementReported &&
3645
+ deps.supportsDeferredAbandonment?.({ updateId: update.update_id, signal, journalBindingKey }) === true,
3646
+ armRoutingInput: (operatorUserId, sourceUpdateIds) => {
3647
+ execution.assertCurrent();
3648
+ if (settlementReported || routingClaims || suspended)
3649
+ return undefined;
3650
+ if (routingClockUnknown)
3651
+ throw new TelegramUpdateAdmissionOutcomeError("Telegram routing input clock publication is unconfirmed.");
3652
+ const ids = [...sourceUpdateIds];
3653
+ if (routingSourceIds && !isDeepStrictEqual(ids, routingSourceIds))
3654
+ throw new TelegramUpdateAdmissionOutcomeError("Telegram routing input source membership changed.");
3655
+ try {
3656
+ routingLifetime = deps.armRoutingInput?.({ updateId: update.update_id, signal, operatorUserId, sourceUpdateIds: ids });
3657
+ }
3658
+ catch (error) {
3659
+ routingClockUnknown = true;
3660
+ throw error;
3661
+ }
3662
+ if (routingLifetime)
3663
+ routingSourceIds = ids;
3664
+ return routingLifetime ? { ...routingLifetime } : undefined;
3665
+ },
3666
+ acquireRouting: (select = false, sourceUpdateIds) => {
3667
+ execution.assertCurrent();
3668
+ if (select && routingClockUnknown)
3669
+ throw new TelegramUpdateAdmissionOutcomeError("Telegram routing input clock publication is unconfirmed; selection is protected.");
3670
+ if (select && routingLifetime && !routingSelectionConfirmed) {
3671
+ if (routingSelectionAttempted)
3672
+ throw new TelegramUpdateAdmissionOutcomeError("Telegram routing selection is unconfirmed; no replay is permitted.");
3673
+ if (!isDeepStrictEqual(sourceUpdateIds ?? [update.update_id], routingSourceIds))
3674
+ throw new TelegramUpdateAdmissionOutcomeError("Telegram routing input selection changed its source membership.");
3675
+ routingSelectionAttempted = true;
3676
+ routingSelectionConfirmed = deps.selectRoutingInput?.({ updateId: update.update_id, signal,
3677
+ operatorUserId: routingLifetime.operatorUserId, sourceUpdateIds: routingSourceIds }) === true;
3678
+ if (!routingSelectionConfirmed)
3679
+ throw new TelegramUpdateAdmissionOutcomeError("Telegram routing input choice is expired or unavailable.");
3680
+ }
3681
+ routingClaims += 1;
3682
+ let released = false;
3683
+ return () => {
3684
+ if (released)
3685
+ return;
3686
+ released = true;
3687
+ routingClaims -= 1;
3688
+ };
3689
+ } }), execution);
2780
3690
  try {
2781
3691
  execution.assertCurrent();
2782
3692
  await deps.defaultHandle(boundUpdate, ctx, execution);
2783
- execution.assertCurrent();
3693
+ if (outcome?.kind !== "deferred" || !outcome.routingReview || signal.aborted)
3694
+ execution.assertCurrent();
2784
3695
  }
2785
3696
  finally {
2786
3697
  immediate = false;
@@ -2813,17 +3724,21 @@ export function createTelegramQueueAdmissionSettlementMuxRuntime(runtimes) {
2813
3724
  // Plan the whole request before mutation; unready is not an acknowledgement of completion.
2814
3725
  const complete = (items, ctx, kind) => {
2815
3726
  const plan = new Map();
3727
+ const reason = kind === "prompt" ? "prompt-handoff" : kind === "control" ? "control-settlement" : "discard";
3728
+ const settlementOwner = (runtime, receipt) => runtime.getQueueReceiptSettlementOwner ? runtime.getQueueReceiptSettlementOwner(receipt, ctx, reason) :
3729
+ runtime.isItemReady({ admissionReceipts: [receipt] }) ? runtime.getQueueReceiptOwner(receipt) : undefined;
2816
3730
  for (const receipt of items.flatMap(item => item.admissionReceipts ?? [])) {
2817
- if (kind === "discard" && isQueueReceiptCompletionAcknowledged(receipt))
3731
+ if (isQueueReceiptCompletionAcknowledged(receipt))
2818
3732
  continue;
2819
- const candidates = runtimes.filter(runtime => runtime.isItemReady({ admissionReceipts: [receipt] }));
3733
+ const candidates = runtimes.filter(runtime => runtime.getQueueReceiptSettlementOwner ? !!settlementOwner(runtime, receipt)
3734
+ : runtime.isItemReady({ admissionReceipts: [receipt] }));
2820
3735
  const selected = candidates[0];
2821
3736
  if (!selected)
2822
3737
  return false;
2823
3738
  if (candidates.length > 1) {
2824
- const owner = selected.getQueueReceiptOwner(receipt);
3739
+ const owner = settlementOwner(selected, receipt);
2825
3740
  if (!owner || candidates.some(runtime => {
2826
- const candidate = runtime.getQueueReceiptOwner(receipt);
3741
+ const candidate = settlementOwner(runtime, receipt);
2827
3742
  return !candidate || !areTelegramUpdateJournalQueueOwnersEqual(owner, candidate);
2828
3743
  }))
2829
3744
  return false;
@@ -2868,7 +3783,7 @@ export function createTelegramQueueAdmissionSettlementRuntime(worker) {
2868
3783
  const receipts = [];
2869
3784
  for (const item of items) {
2870
3785
  for (const receipt of item.admissionReceipts ?? []) {
2871
- if (reason !== "discard" || !isQueueReceiptCompletionAcknowledged(receipt))
3786
+ if (!isQueueReceiptCompletionAcknowledged(receipt))
2872
3787
  receipts.push(receipt);
2873
3788
  }
2874
3789
  }
@@ -2877,6 +3792,7 @@ export function createTelegramQueueAdmissionSettlementRuntime(worker) {
2877
3792
  return {
2878
3793
  isItemReady: (item) => (item.admissionReceipts ?? []).every(worker.isQueueReceiptCommitted),
2879
3794
  getQueueReceiptOwner: worker.getQueueReceiptOwner,
3795
+ ...(worker.getQueueReceiptSettlementOwner ? { getQueueReceiptSettlementOwner: worker.getQueueReceiptSettlementOwner } : {}),
2880
3796
  onPromptHandedOff: (item, ctx) => complete([item], ctx, "prompt-handoff"),
2881
3797
  onControlSettled: (item, ctx) => complete([item], ctx, "control-settlement"),
2882
3798
  onItemsDiscarded: (items, ctx) => complete(items, ctx, "discard"),
@@ -3048,9 +3964,9 @@ export function createTelegramQueueHandoffReconciliationRuntimeAssembly(deps) {
3048
3964
  canHandoffWithLeader: deps.canHandoffWithLeader,
3049
3965
  listFollowers: deps.listFollowers,
3050
3966
  createRecipientJournalBindingKey(recipient) {
3051
- if (!recipient.profileKey)
3967
+ if (!recipient.profileKey || !recipient.sessionId)
3052
3968
  return undefined;
3053
- return deps.createRecipientJournalResolver(recipient.profileKey)()
3969
+ return deps.createRecipientJournalResolver(recipient.profileKey, recipient.sessionId)()
3054
3970
  ?.recoveryKey;
3055
3971
  },
3056
3972
  getQueuedItems: deps.queueStore.getQueuedItems,
@@ -3228,17 +4144,18 @@ export function createTelegramUpdateWorkerOwnerRuntime(deps) {
3228
4144
  sessionGeneration: deps.getSessionGeneration(),
3229
4145
  };
3230
4146
  },
3231
- onQueueReceiptCommitted(_receipt, ctx) {
4147
+ onQueueReceiptCommitted(receipt, ctx) {
3232
4148
  if (!deps.isContextCurrent(ctx))
3233
4149
  return;
4150
+ deps.afterQueueReceiptCommitted?.(receipt, ctx);
3234
4151
  deps.dispatchNext(ctx);
3235
4152
  deps.requestQueueHandoffReconciliation(ctx);
3236
4153
  },
3237
- onUpdateCompleted(updateId, ctx) {
4154
+ onUpdateCompleted(updateId, ctx, journalBindingKey) {
3238
4155
  if (!deps.isContextCurrent(ctx))
3239
4156
  return;
3240
4157
  deps.dispatchNext(ctx);
3241
- deps.afterUpdateCompleted?.(updateId);
4158
+ deps.afterUpdateCompleted?.(updateId, ctx, journalBindingKey);
3242
4159
  },
3243
4160
  };
3244
4161
  }
@@ -3247,6 +4164,7 @@ export function createTelegramUpdateAdmissionRuntimeBinding(deps) {
3247
4164
  let follower;
3248
4165
  let settlement;
3249
4166
  let inputCustodyBus;
4167
+ const getActive = () => deps.isFollowerRegistered() ? follower : leader;
3250
4168
  return {
3251
4169
  bind(input) {
3252
4170
  leader = input.leader;
@@ -3259,7 +4177,7 @@ export function createTelegramUpdateAdmissionRuntimeBinding(deps) {
3259
4177
  },
3260
4178
  getLeader: () => leader,
3261
4179
  getFollower: () => follower,
3262
- getActive: () => (deps.isFollowerRegistered() ? follower : leader),
4180
+ getActive,
3263
4181
  getSettlement: () => settlement,
3264
4182
  getInputCustodyBus: () => inputCustodyBus,
3265
4183
  getLifecycleForJournalBinding(journalBindingKey) {
@@ -3358,6 +4276,7 @@ export function createTelegramUpdateAdmissionLifecycleRuntime(deps) {
3358
4276
  binding.recoveryKey !== recoveryKey) {
3359
4277
  throw new Error("Telegram update source binding changed during startup.");
3360
4278
  }
4279
+ deps.prepareBinding?.(binding);
3361
4280
  const release = deps.acquireSourceReference?.(binding);
3362
4281
  const inputCustody = binding.journal.inputCustody;
3363
4282
  try {
@@ -3580,6 +4499,15 @@ export function createTelegramUpdateAdmissionWorkerRuntime(deps) {
3580
4499
  const executeUpdate = createTelegramUpdateAdmissionHandle({
3581
4500
  defaultHandle: deps.defaultHandle,
3582
4501
  registry: deps.registry,
4502
+ abandonDeferred: input => worker?.abandonDeferred?.(input),
4503
+ armRoutingInput: input => worker?.armRoutingInput?.(input),
4504
+ selectRoutingInput: input => worker?.selectRoutingInput?.(input) === true,
4505
+ isRoutingInputCurrent: input => worker?.isRoutingInputCurrent?.(input) !== false,
4506
+ supportsDeferredAbandonment: input => worker?.supportsDeferredAbandonment?.(input) === true,
4507
+ inspectAbandoning: input => worker?.inspectAbandoning?.(input),
4508
+ inspectHistorical: input => worker?.inspectHistorical?.(input),
4509
+ inspectDeferredSource: input => worker?.inspectDeferredSource?.(input),
4510
+ isHistoricalSource: input => worker?.isHistoricalSource?.(input) === true,
3583
4511
  onLateOutcome(outcome, details) {
3584
4512
  worker?.settleDeferred({
3585
4513
  updateId: details.updateId,
@@ -3647,6 +4575,7 @@ export function createTelegramUpdateAdmissionLifecycleAssembly(deps) {
3647
4575
  const follower = createTelegramUpdateAdmissionLifecycleRuntime({
3648
4576
  getQueueOwnerIdentity: deps.worker.getQueueOwnerIdentity,
3649
4577
  ...(deps.acquireSourceReference ? { acquireSourceReference: binding => deps.acquireSourceReference("follower", binding) } : {}),
4578
+ ...(deps.follower.prepareBinding ? { prepareBinding: deps.follower.prepareBinding } : {}),
3650
4579
  resolveBinding() {
3651
4580
  const generation = deps.follower.getGeneration();
3652
4581
  if (!deps.follower.isRegistered() || !generation)
@@ -3671,6 +4600,11 @@ export function createTelegramUpdateAdmissionLifecycleAssembly(deps) {
3671
4600
  getJournalBindingKey: () => binding.recoveryKey,
3672
4601
  getRecipientBindingKey: () => binding.recipientBindingKey,
3673
4602
  hasAuthority: () => binding.hasAuthority?.() ?? false,
4603
+ // Recipient custody is not an orphaned direct-bot source.
4604
+ shouldReviewHistoricalInput: undefined,
4605
+ shouldHoldPendingInput: undefined,
4606
+ spendHistoricalInput: undefined,
4607
+ onHeldSourcesPrepared: undefined,
3674
4608
  prepareUpdateForExecution: deps.follower.prepareUpdateForExecution,
3675
4609
  });
3676
4610
  },