@llblab/pi-kit 0.27.5 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/BACKLOG.md +3 -1
  2. package/CHANGELOG.md +9 -0
  3. package/README.md +4 -4
  4. package/node_modules/@llblab/pi-state-flow/AGENTS.md +1 -1
  5. package/node_modules/@llblab/pi-state-flow/BACKLOG.md +6 -2
  6. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +4 -1
  7. package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.js +4 -3
  8. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +26 -13
  9. package/node_modules/@llblab/pi-state-flow/dist/package.json +1 -1
  10. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +3 -1
  11. package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +2 -2
  12. package/node_modules/@llblab/pi-state-flow/docs/usage.md +2 -1
  13. package/node_modules/@llblab/pi-state-flow/lib/compaction.ts +4 -3
  14. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +25 -12
  15. package/node_modules/@llblab/pi-state-flow/package.json +1 -1
  16. package/node_modules/@llblab/pi-telegram/AGENTS.md +10 -8
  17. package/node_modules/@llblab/pi-telegram/BACKLOG.md +1 -9
  18. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +13 -0
  19. package/node_modules/@llblab/pi-telegram/LICENSE +21 -0
  20. package/node_modules/@llblab/pi-telegram/README.md +15 -5
  21. package/node_modules/@llblab/pi-telegram/dist/lib/activity-verbosity.d.ts +1 -5
  22. package/node_modules/@llblab/pi-telegram/dist/lib/activity-verbosity.js +5 -7
  23. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +5 -1
  24. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +6 -1
  25. package/node_modules/@llblab/pi-telegram/dist/lib/bus-api.js +4 -2
  26. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.d.ts +54 -56
  27. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +206 -139
  28. package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.d.ts +21 -2
  29. package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +181 -25
  30. package/node_modules/@llblab/pi-telegram/dist/lib/bus-transport.d.ts +4 -1
  31. package/node_modules/@llblab/pi-telegram/dist/lib/bus-transport.js +28 -4
  32. package/node_modules/@llblab/pi-telegram/dist/lib/bus.d.ts +52 -56
  33. package/node_modules/@llblab/pi-telegram/dist/lib/bus.js +78 -246
  34. package/node_modules/@llblab/pi-telegram/dist/lib/channel-posts.d.ts +1 -3
  35. package/node_modules/@llblab/pi-telegram/dist/lib/channel-posts.js +23 -26
  36. package/node_modules/@llblab/pi-telegram/dist/lib/command-templates.d.ts +0 -5
  37. package/node_modules/@llblab/pi-telegram/dist/lib/command-templates.js +5 -5
  38. package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +0 -23
  39. package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +15 -15
  40. package/node_modules/@llblab/pi-telegram/dist/lib/config.d.ts +0 -17
  41. package/node_modules/@llblab/pi-telegram/dist/lib/config.js +12 -14
  42. package/node_modules/@llblab/pi-telegram/dist/lib/delivery.d.ts +0 -4
  43. package/node_modules/@llblab/pi-telegram/dist/lib/delivery.js +2 -5
  44. package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +255 -61
  45. package/node_modules/@llblab/pi-telegram/dist/lib/generative-apps.d.ts +0 -19
  46. package/node_modules/@llblab/pi-telegram/dist/lib/inbound.d.ts +0 -2
  47. package/node_modules/@llblab/pi-telegram/dist/lib/inbound.js +2 -2
  48. package/node_modules/@llblab/pi-telegram/dist/lib/journal.d.ts +177 -16
  49. package/node_modules/@llblab/pi-telegram/dist/lib/journal.js +1115 -249
  50. package/node_modules/@llblab/pi-telegram/dist/lib/keyboard.d.ts +0 -2
  51. package/node_modules/@llblab/pi-telegram/dist/lib/keyboard.js +2 -2
  52. package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.js +1 -1
  53. package/node_modules/@llblab/pi-telegram/dist/lib/locks.d.ts +100 -12
  54. package/node_modules/@llblab/pi-telegram/dist/lib/locks.js +380 -14
  55. package/node_modules/@llblab/pi-telegram/dist/lib/logging.d.ts +30 -12
  56. package/node_modules/@llblab/pi-telegram/dist/lib/logging.js +129 -72
  57. package/node_modules/@llblab/pi-telegram/dist/lib/media.d.ts +28 -5
  58. package/node_modules/@llblab/pi-telegram/dist/lib/media.js +26 -8
  59. package/node_modules/@llblab/pi-telegram/dist/lib/menu-model.d.ts +0 -11
  60. package/node_modules/@llblab/pi-telegram/dist/lib/menu-model.js +8 -8
  61. package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.d.ts +0 -18
  62. package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.js +18 -18
  63. package/node_modules/@llblab/pi-telegram/dist/lib/menu-status.d.ts +4 -4
  64. package/node_modules/@llblab/pi-telegram/dist/lib/menu-status.js +12 -7
  65. package/node_modules/@llblab/pi-telegram/dist/lib/menu-thinking.d.ts +1 -1
  66. package/node_modules/@llblab/pi-telegram/dist/lib/menu-thinking.js +1 -1
  67. package/node_modules/@llblab/pi-telegram/dist/lib/menu.d.ts +1 -0
  68. package/node_modules/@llblab/pi-telegram/dist/lib/menu.js +4 -3
  69. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.d.ts +0 -1
  70. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.js +1 -1
  71. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-buttons.js +1 -8
  72. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.d.ts +1 -6
  73. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.js +4 -21
  74. package/node_modules/@llblab/pi-telegram/dist/lib/outbound.d.ts +0 -8
  75. package/node_modules/@llblab/pi-telegram/dist/lib/outbound.js +5 -5
  76. package/node_modules/@llblab/pi-telegram/dist/lib/paths.d.ts +50 -7
  77. package/node_modules/@llblab/pi-telegram/dist/lib/paths.js +155 -17
  78. package/node_modules/@llblab/pi-telegram/dist/lib/pi.d.ts +1 -0
  79. package/node_modules/@llblab/pi-telegram/dist/lib/pi.js +2 -1
  80. package/node_modules/@llblab/pi-telegram/dist/lib/polling.d.ts +0 -5
  81. package/node_modules/@llblab/pi-telegram/dist/lib/polling.js +5 -5
  82. package/node_modules/@llblab/pi-telegram/dist/lib/preview.d.ts +0 -3
  83. package/node_modules/@llblab/pi-telegram/dist/lib/preview.js +3 -3
  84. package/node_modules/@llblab/pi-telegram/dist/lib/prompt-templates.d.ts +0 -1
  85. package/node_modules/@llblab/pi-telegram/dist/lib/prompt-templates.js +1 -1
  86. package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +3 -15
  87. package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +69 -8
  88. package/node_modules/@llblab/pi-telegram/dist/lib/recovery.d.ts +29 -9
  89. package/node_modules/@llblab/pi-telegram/dist/lib/recovery.js +104 -33
  90. package/node_modules/@llblab/pi-telegram/dist/lib/replies.d.ts +0 -4
  91. package/node_modules/@llblab/pi-telegram/dist/lib/replies.js +1 -1
  92. package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +73 -8
  93. package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +2206 -307
  94. package/node_modules/@llblab/pi-telegram/dist/lib/sections.d.ts +0 -2
  95. package/node_modules/@llblab/pi-telegram/dist/lib/sections.js +1 -1
  96. package/node_modules/@llblab/pi-telegram/dist/lib/status.d.ts +44 -9
  97. package/node_modules/@llblab/pi-telegram/dist/lib/status.js +149 -27
  98. package/node_modules/@llblab/pi-telegram/dist/lib/sync.d.ts +0 -5
  99. package/node_modules/@llblab/pi-telegram/dist/lib/sync.js +5 -4
  100. package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +11 -3
  101. package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +34 -23
  102. package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.d.ts +1 -0
  103. package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.js +18 -21
  104. package/node_modules/@llblab/pi-telegram/dist/lib/thread-naming.d.ts +32 -5
  105. package/node_modules/@llblab/pi-telegram/dist/lib/thread-naming.js +191 -7
  106. package/node_modules/@llblab/pi-telegram/dist/lib/thread-reconciler.d.ts +2 -0
  107. package/node_modules/@llblab/pi-telegram/dist/lib/thread-reconciler.js +1 -1
  108. package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +291 -59
  109. package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +1647 -369
  110. package/node_modules/@llblab/pi-telegram/dist/lib/turns.d.ts +0 -1
  111. package/node_modules/@llblab/pi-telegram/dist/lib/turns.js +1 -1
  112. package/node_modules/@llblab/pi-telegram/dist/lib/updates.d.ts +184 -28
  113. package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +1032 -114
  114. package/node_modules/@llblab/pi-telegram/dist/lib/wire.d.ts +12 -0
  115. package/node_modules/@llblab/pi-telegram/dist/lib/wire.js +21 -0
  116. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.d.ts +25 -17
  117. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.js +95 -20
  118. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-identity.d.ts +20 -0
  119. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-identity.js +103 -0
  120. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.d.ts +43 -8
  121. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.js +148 -52
  122. package/node_modules/@llblab/pi-telegram/dist/package.json +6 -6
  123. package/node_modules/@llblab/pi-telegram/dist/skills/generated-control-surface/SKILL.md +3 -3
  124. package/node_modules/@llblab/pi-telegram/dist/skills/telegram-bridge/references/diagnosis.md +3 -3
  125. package/node_modules/@llblab/pi-telegram/docs/README.md +1 -1
  126. package/node_modules/@llblab/pi-telegram/docs/activity.md +1 -1
  127. package/node_modules/@llblab/pi-telegram/docs/architecture.md +152 -34
  128. package/node_modules/@llblab/pi-telegram/docs/callback-namespaces.md +1 -1
  129. package/node_modules/@llblab/pi-telegram/docs/delivery.md +1 -1
  130. package/node_modules/@llblab/pi-telegram/docs/generative-apps.md +2 -2
  131. package/node_modules/@llblab/pi-telegram/docs/inbound.md +1 -1
  132. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +125 -19
  133. package/node_modules/@llblab/pi-telegram/docs/public-api.md +3 -1
  134. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +22 -8
  135. package/node_modules/@llblab/pi-telegram/docs/updates.md +1 -1
  136. package/node_modules/@llblab/pi-telegram/lib/activity-verbosity.ts +5 -9
  137. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +6 -0
  138. package/node_modules/@llblab/pi-telegram/lib/bus-api.ts +5 -2
  139. package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +243 -224
  140. package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +178 -31
  141. package/node_modules/@llblab/pi-telegram/lib/bus-transport.ts +30 -8
  142. package/node_modules/@llblab/pi-telegram/lib/bus.ts +110 -334
  143. package/node_modules/@llblab/pi-telegram/lib/channel-posts.ts +29 -29
  144. package/node_modules/@llblab/pi-telegram/lib/command-templates.ts +5 -5
  145. package/node_modules/@llblab/pi-telegram/lib/commands.ts +15 -15
  146. package/node_modules/@llblab/pi-telegram/lib/config.ts +12 -17
  147. package/node_modules/@llblab/pi-telegram/lib/delivery.ts +2 -8
  148. package/node_modules/@llblab/pi-telegram/lib/extension.ts +257 -72
  149. package/node_modules/@llblab/pi-telegram/lib/generative-apps.ts +0 -22
  150. package/node_modules/@llblab/pi-telegram/lib/inbound.ts +2 -2
  151. package/node_modules/@llblab/pi-telegram/lib/journal.ts +1182 -326
  152. package/node_modules/@llblab/pi-telegram/lib/keyboard.ts +2 -2
  153. package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +1 -1
  154. package/node_modules/@llblab/pi-telegram/lib/locks.ts +402 -23
  155. package/node_modules/@llblab/pi-telegram/lib/logging.ts +151 -101
  156. package/node_modules/@llblab/pi-telegram/lib/media.ts +51 -9
  157. package/node_modules/@llblab/pi-telegram/lib/menu-model.ts +8 -8
  158. package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +18 -18
  159. package/node_modules/@llblab/pi-telegram/lib/menu-status.ts +11 -0
  160. package/node_modules/@llblab/pi-telegram/lib/menu-thinking.ts +2 -2
  161. package/node_modules/@llblab/pi-telegram/lib/menu.ts +5 -1
  162. package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +1 -1
  163. package/node_modules/@llblab/pi-telegram/lib/outbound-buttons.ts +1 -10
  164. package/node_modules/@llblab/pi-telegram/lib/outbound-markup.ts +5 -24
  165. package/node_modules/@llblab/pi-telegram/lib/outbound.ts +5 -5
  166. package/node_modules/@llblab/pi-telegram/lib/paths.ts +177 -17
  167. package/node_modules/@llblab/pi-telegram/lib/pi.ts +5 -2
  168. package/node_modules/@llblab/pi-telegram/lib/polling.ts +5 -5
  169. package/node_modules/@llblab/pi-telegram/lib/preview.ts +3 -3
  170. package/node_modules/@llblab/pi-telegram/lib/prompt-templates.ts +1 -1
  171. package/node_modules/@llblab/pi-telegram/lib/queue.ts +67 -9
  172. package/node_modules/@llblab/pi-telegram/lib/recovery.ts +104 -44
  173. package/node_modules/@llblab/pi-telegram/lib/replies.ts +1 -1
  174. package/node_modules/@llblab/pi-telegram/lib/routing.ts +1955 -375
  175. package/node_modules/@llblab/pi-telegram/lib/sections.ts +1 -1
  176. package/node_modules/@llblab/pi-telegram/lib/status.ts +156 -36
  177. package/node_modules/@llblab/pi-telegram/lib/sync.ts +4 -4
  178. package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +48 -33
  179. package/node_modules/@llblab/pi-telegram/lib/thread-cleanup-manager.ts +24 -21
  180. package/node_modules/@llblab/pi-telegram/lib/thread-naming.ts +267 -9
  181. package/node_modules/@llblab/pi-telegram/lib/thread-reconciler.ts +3 -1
  182. package/node_modules/@llblab/pi-telegram/lib/threads.ts +1697 -488
  183. package/node_modules/@llblab/pi-telegram/lib/turns.ts +1 -1
  184. package/node_modules/@llblab/pi-telegram/lib/updates.ts +1072 -151
  185. package/node_modules/@llblab/pi-telegram/lib/wire.ts +28 -0
  186. package/node_modules/@llblab/pi-telegram/lib/workspace-admission.ts +97 -47
  187. package/node_modules/@llblab/pi-telegram/lib/workspace-identity.ts +147 -0
  188. package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +188 -89
  189. package/node_modules/@llblab/pi-telegram/package.json +6 -6
  190. package/node_modules/@llblab/pi-telegram/scripts/audit-exports.mjs +100 -0
  191. package/node_modules/@llblab/pi-telegram/scripts/check-downgrade.mjs +80 -43
  192. package/node_modules/@llblab/pi-telegram/skills/generated-control-surface/SKILL.md +3 -3
  193. package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/diagnosis.md +3 -3
  194. package/package.json +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,111 @@ 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 removal ACKs reconcile only the exact held original through the retained copy/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 (claims.get(entry.updateId) === "retained" || (expectedOwner.startupUpdateIds?.has(entry.updateId) &&
1711
+ !claims.has(entry.updateId) && deps.shouldReviewHistoricalInput))
1712
+ continue;
1713
+ if (!lifetime || lifetime.phase !== "waiting" || entry.state !== "pending" || !binding || unsettledExecutionsByUpdateId.get(entry.updateId)?.size)
1714
+ continue;
1715
+ if (getNowMs() < lifetime.expiresAtMs) {
1716
+ nextAtMs = Math.min(nextAtMs ?? Infinity, lifetime.expiresAtMs);
1717
+ continue;
1718
+ }
1719
+ const existing = deferredSources.get(entry.updateId);
1720
+ if (existing && !isDeepStrictEqual(existing.entry, entry))
1721
+ continue;
1722
+ const source = existing ?? { entry: structuredClone(entry), journalBindingKey: binding, historical: true };
1723
+ deferredSources.set(entry.updateId, source);
1724
+ if (!claims.has(entry.updateId))
1725
+ claims.set(entry.updateId, "historical");
1726
+ const current = () => owner === expectedOwner && !expectedOwner.controller.signal.aborted &&
1727
+ deps.isContextCurrent?.(expectedOwner.ctx) !== false && deps.getJournalBindingKey?.() === binding &&
1728
+ deps.hasAuthority(expectedOwner.ctx) && isQueueOwnerIdentityCurrent(expectedOwner) && deferredSources.get(entry.updateId) === source &&
1729
+ isDeepStrictEqual(source.entry, entry) && claims.get(entry.updateId) !== "queued";
1730
+ let committed;
1731
+ try {
1732
+ if (current())
1733
+ await expireRoutingObserver({ original: structuredClone(entry), journalBindingKey: binding, isCurrent: current,
1734
+ expire() {
1735
+ if (!current())
1736
+ return undefined;
1737
+ if (committed)
1738
+ return { ...committed, duplicate: true };
1739
+ claims.set(entry.updateId, "abandoning");
1740
+ committed = expireRouting({ entry: source.entry, journalBindingKey: binding, operatorUserId: lifetime.operatorUserId, isCurrent: current });
1741
+ claims.delete(entry.updateId);
1742
+ deferredSources.delete(entry.updateId);
1743
+ state.journalEntryCount = committed.entryCount;
1744
+ state.journalSerializedBytes = committed.serializedBytes;
1745
+ updateClaimCounts();
1746
+ notifyStateChange();
1747
+ changed = true;
1748
+ return committed;
1749
+ } }, expectedOwner.ctx, expectedOwner.controller.signal);
1750
+ }
1751
+ catch (error) {
1752
+ recordRuntimeEvent(error, { phase: "routing-input-expiry", updateId: entry.updateId });
1753
+ }
1754
+ if (!committed)
1755
+ nextAtMs = Math.min(nextAtMs ?? Infinity, getNowMs() + 60_000);
1756
+ }
1757
+ if (nextAtMs !== undefined && owner === expectedOwner && !expectedOwner.controller.signal.aborted) {
1758
+ const token = {};
1759
+ routingTimerToken = token;
1760
+ routingTimer = scheduleRetry(() => {
1761
+ if (routingTimerToken !== token || owner !== expectedOwner || expectedOwner.controller.signal.aborted)
1762
+ return;
1763
+ routingTimer = undefined;
1764
+ routingTimerToken = undefined;
1765
+ pendingSignal = true;
1766
+ launchDrain();
1767
+ }, Math.max(1, nextAtMs - getNowMs()));
1768
+ }
1769
+ return changed;
1770
+ };
1771
+ const mutateRoutingSource = (input, select) => {
1772
+ const expectedOwner = owner, ids = [...input.sourceUpdateIds];
1773
+ if (!ids.length || !ids.includes(input.updateId) || new Set(ids).size !== ids.length || ids.some(id => !Number.isSafeInteger(id) || id < 0))
1774
+ return undefined;
1775
+ const sources = ids.map(id => deferredSources.get(id)), binding = sources[0]?.journalBindingKey;
1776
+ const current = () => !!expectedOwner && owner === expectedOwner && expectedOwner.controller.signal === input.signal && !input.signal.aborted &&
1777
+ !!binding && sources.every((source, index) => !!source && deferredSources.get(ids[index]) === source && !source.historical &&
1778
+ source.journalBindingKey === binding && claims.get(ids[index]) !== "queued" && claims.get(ids[index]) !== "abandoning") &&
1779
+ deps.getJournalBindingKey?.() === binding && deps.isContextCurrent?.(expectedOwner.ctx) !== false &&
1780
+ deps.hasAuthority(expectedOwner.ctx) && isQueueOwnerIdentityCurrent(expectedOwner);
1781
+ if (!current() || !armRouting || !selectRouting || !expireRoutingObserver)
1782
+ return undefined;
1783
+ const authority = { journalBindingKey: binding, entries: sources.map(source => structuredClone(source.entry)), operatorUserId: input.operatorUserId, isCurrent: current };
1784
+ const result = select ? selectRouting(authority) : { issued: false, entries: armRouting({ ...authority, publishedAtMs: getNowMs() }) };
1785
+ if (result.entries.length !== ids.length || result.entries.some((entry, index) => entry.updateId !== ids[index] || !entry.routingInput) || !current()) {
1786
+ throw new Error("Telegram routing input lifetime ACK was not confirmed.");
1787
+ }
1788
+ const retained = deps.journal.read().entries;
1789
+ if (result.entries.some(entry => !isDeepStrictEqual(retained.find(value => value.updateId === entry.updateId), entry)) || !current()) {
1790
+ throw new Error("Telegram routing input lifetime ACK was not retained.");
1791
+ }
1792
+ result.entries.forEach((entry, index) => { sources[index].entry = structuredClone(entry); });
1793
+ pendingSignal = true;
1794
+ launchDrain();
1795
+ return { issued: result.issued, lifetime: { ...result.entries[0].routingInput } };
1796
+ };
1408
1797
  const drain = async (expectedOwner) => {
1409
1798
  while (owner === expectedOwner && !expectedOwner.controller.signal.aborted) {
1410
1799
  const authorityResult = checkAuthority(expectedOwner);
@@ -1414,11 +1803,18 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1414
1803
  let scheduledRetryAtMs;
1415
1804
  try {
1416
1805
  snapshot = deps.journal.read();
1417
- scheduledRetryAtMs = refreshJournalState(snapshot, expectedOwner);
1806
+ scheduledRetryAtMs = await refreshJournalState(snapshot, expectedOwner);
1807
+ // One complete validated baseline per generation, before batching or execution.
1808
+ // Replayed v1 entries do not prove that a prior process never forwarded them.
1809
+ expectedOwner.startupUpdateIds ??= new Set(snapshot.entries.map(entry => entry.updateId));
1418
1810
  }
1419
1811
  catch (error) {
1420
1812
  return blockWithFailure("journal-read", "journal-read", error);
1421
1813
  }
1814
+ if (await reconcileRoutingInputs(snapshot, expectedOwner))
1815
+ continue;
1816
+ if (owner !== expectedOwner || expectedOwner.controller.signal.aborted)
1817
+ return "aborted";
1422
1818
  const nowMs = getNowMs();
1423
1819
  const entries = [];
1424
1820
  let hasMoreEntries = false;
@@ -1489,9 +1885,105 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1489
1885
  break;
1490
1886
  }
1491
1887
  if (entry.preApprovalExcluded === true) {
1492
- completedUpdateIds.push(entry.updateId);
1888
+ completedUpdateIds.push({ updateId: entry.updateId });
1493
1889
  continue;
1494
1890
  }
1891
+ if (isTelegramUpdateJournalLegacyFamilyVersion(snapshot.version) && entry.state === "pending" && deps.journal.inspectPendingRetention) {
1892
+ const authorityResult = checkAuthority(expectedOwner, entry.updateId);
1893
+ if (authorityResult)
1894
+ return authorityResult;
1895
+ let retained;
1896
+ let unverifiable = false;
1897
+ try {
1898
+ retained = deps.journal.inspectPendingRetention(entry);
1899
+ }
1900
+ catch (error) {
1901
+ unverifiable = true;
1902
+ recordRuntimeEvent(error, { phase: "pending-retention-inspection", updateId: entry.updateId });
1903
+ }
1904
+ if (retained || unverifiable) {
1905
+ // A copy is protection evidence only. A fresh owner must explicitly retry
1906
+ // cancellation; neither startup nor a malformed copy may dispatch the source.
1907
+ claims.set(entry.updateId, "abandoning");
1908
+ const journalBindingKey = retained?.journalBindingKey ?? deps.getJournalBindingKey?.();
1909
+ if (journalBindingKey)
1910
+ deferredSources.set(entry.updateId, { entry, journalBindingKey });
1911
+ transition("deferred", entry.updateId);
1912
+ continue;
1913
+ }
1914
+ }
1915
+ if (isTelegramUpdateJournalLegacyFamilyVersion(snapshot.version) && (entry.state === "pending" || entry.state === "retry-wait")) {
1916
+ const historical = expectedOwner.startupUpdateIds.has(entry.updateId);
1917
+ const journalBindingKey = deps.getJournalBindingKey?.();
1918
+ // Classifiers and handler projections receive no mutable alias to CAS evidence.
1919
+ if (journalBindingKey)
1920
+ deferredSources.set(entry.updateId, { entry: structuredClone(entry), journalBindingKey,
1921
+ ...(historical ? { historical: true } : {}) });
1922
+ const classifyPending = historical && entry.state === "pending" ? deps.shouldReviewHistoricalInput : deps.shouldHoldPendingInput;
1923
+ if (classifyPending) {
1924
+ const checkHistoricalAuthority = () => {
1925
+ if (owner !== expectedOwner || expectedOwner.controller.signal.aborted)
1926
+ return "aborted";
1927
+ if (deps.getJournalBindingKey?.() !== journalBindingKey || deps.isContextCurrent?.(expectedOwner.ctx) === false) {
1928
+ transition("blocked", entry.updateId, "authority-lost");
1929
+ return "blocked";
1930
+ }
1931
+ return checkAuthority(expectedOwner, entry.updateId);
1932
+ };
1933
+ const beforeClassification = checkHistoricalAuthority();
1934
+ if (beforeClassification)
1935
+ return beforeClassification;
1936
+ let review;
1937
+ try {
1938
+ review = await classifyPending(structuredClone(entry), expectedOwner.ctx, expectedOwner.controller.signal);
1939
+ }
1940
+ catch (error) {
1941
+ const authorityResult = checkHistoricalAuthority();
1942
+ if (authorityResult)
1943
+ return authorityResult;
1944
+ return blockWithFailure("execution", "historical-review-classification", error, entry.updateId);
1945
+ }
1946
+ const authorityResult = checkHistoricalAuthority();
1947
+ if (authorityResult)
1948
+ return authorityResult;
1949
+ if (typeof review !== "boolean" && !(historical && entry.state === "pending" && review === "retain"))
1950
+ return blockWithFailure("execution", "historical-review-classification", new TelegramUpdateAdmissionOutcomeError("Historical classification must return a boolean or retain verdict."), entry.updateId);
1951
+ // Classification may await a domain read; accepted/changed source evidence wins.
1952
+ try {
1953
+ const current = deps.journal.read();
1954
+ if (current.version !== snapshot.version || JSON.stringify(current.entries.find(candidate => candidate.updateId === entry.updateId)) !== JSON.stringify(entry)) {
1955
+ snapshotInvalidated = true;
1956
+ break;
1957
+ }
1958
+ }
1959
+ catch (error) {
1960
+ return blockWithFailure("journal-read", "historical-source-recheck", error, entry.updateId);
1961
+ }
1962
+ if (review === "retain") {
1963
+ claims.set(entry.updateId, "retained");
1964
+ transition("deferred", entry.updateId);
1965
+ continue;
1966
+ }
1967
+ if (review && historical && spendHistorical && classifyPending === deps.shouldReviewHistoricalInput) {
1968
+ completedUpdateIds.push({ updateId: entry.updateId, spent: true });
1969
+ continue;
1970
+ }
1971
+ if (review) {
1972
+ claims.set(entry.updateId, "historical");
1973
+ transition("deferred", entry.updateId);
1974
+ continue;
1975
+ }
1976
+ }
1977
+ if (entry.state === "pending" && entry.routingInput) {
1978
+ if (historical && spendHistorical) {
1979
+ completedUpdateIds.push({ updateId: entry.updateId, spent: true });
1980
+ continue;
1981
+ }
1982
+ claims.set(entry.updateId, "historical");
1983
+ transition("deferred", entry.updateId);
1984
+ continue;
1985
+ }
1986
+ }
1495
1987
  clearRetryTimer();
1496
1988
  transition("executing", entry.updateId);
1497
1989
  const execution = await executeWithinOwner(expectedOwner, entry.update);
@@ -1532,7 +2024,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1532
2024
  for (const sourceUpdateId of receipt.sourceUpdateIds)
1533
2025
  claims.set(sourceUpdateId, "queued");
1534
2026
  try {
1535
- publishCommittedQueueReceipt(receipt, outcome.queueReceipt.queueOwner, expectedOwner.ctx);
2027
+ await publishCommittedQueueReceipt(receipt, outcome.queueReceipt.queueOwner, expectedOwner);
1536
2028
  }
1537
2029
  catch (error) {
1538
2030
  return blockWithFailure("invalid-outcome", "queue-receipt-publish", error, entry.updateId);
@@ -1547,7 +2039,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1547
2039
  return postExecutionAuthority;
1548
2040
  const claimableUpdateIds = new Set([
1549
2041
  entry.updateId,
1550
- ...claims.keys(),
2042
+ ...[...claims].filter(([, claim]) => claim !== "abandoning" && claim !== "historical" && claim !== "retained").map(([id]) => id),
1551
2043
  ]);
1552
2044
  let outcome;
1553
2045
  try {
@@ -1557,7 +2049,10 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1557
2049
  return blockWithFailure("invalid-outcome", "invalid-outcome", error, entry.updateId);
1558
2050
  }
1559
2051
  if (outcome.kind === "deferred") {
1560
- claims.set(entry.updateId, "deferred");
2052
+ if (outcome.routingReview && !deferredSources.get(entry.updateId)?.historical) {
2053
+ return blockWithFailure("invalid-outcome", "historical-review-source", new TelegramUpdateAdmissionOutcomeError("Historical review needs an exact startup source."), entry.updateId);
2054
+ }
2055
+ claims.set(entry.updateId, outcome.routingReview ? "historical" : "deferred");
1561
2056
  transition("deferred", entry.updateId);
1562
2057
  continue;
1563
2058
  }
@@ -1565,7 +2060,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1565
2060
  const completionResult = commitCompletedBatch(expectedOwner, completedUpdateIds);
1566
2061
  if (completionResult)
1567
2062
  return completionResult;
1568
- const queuedResult = commitQueuedOutcome(expectedOwner, entry.updateId, outcome);
2063
+ const queuedResult = await commitQueuedOutcome(expectedOwner, entry.updateId, outcome);
1569
2064
  if (queuedResult === "blocked" || queuedResult === "aborted") {
1570
2065
  return queuedResult;
1571
2066
  }
@@ -1573,7 +2068,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1573
2068
  snapshotInvalidated = true;
1574
2069
  break;
1575
2070
  }
1576
- completedUpdateIds.push(entry.updateId);
2071
+ completedUpdateIds.push({ updateId: entry.updateId, ...(outcome.expectedSource ? { expectedSource: outcome.expectedSource } : {}) });
1577
2072
  }
1578
2073
  const completionResult = commitCompletedBatch(expectedOwner, completedUpdateIds);
1579
2074
  if (completionResult)
@@ -1592,6 +2087,14 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1592
2087
  const expectedOwner = owner;
1593
2088
  if (!expectedOwner || drainPromise)
1594
2089
  return;
2090
+ let startupKey;
2091
+ if (onHeldSourcesPrepared) {
2092
+ try {
2093
+ startupKey = deps.getJournalBindingKey?.();
2094
+ }
2095
+ catch { /* Missing startup identity grants no notification. */ }
2096
+ }
2097
+ let lastResult;
1595
2098
  const run = async () => {
1596
2099
  while (pendingSignal &&
1597
2100
  owner === expectedOwner &&
@@ -1599,7 +2102,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1599
2102
  pendingSignal = false;
1600
2103
  // Keep newer wakes in this run so waitForDrain also covers them. A failure
1601
2104
  // observed after signal() still keeps its latch; this loop never clears it.
1602
- await drain(expectedOwner);
2105
+ lastResult = await drain(expectedOwner);
1603
2106
  }
1604
2107
  };
1605
2108
  const operation = run();
@@ -1610,6 +2113,30 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1610
2113
  if (pendingSignal && !expectedOwner.controller.signal.aborted) {
1611
2114
  launchDrain();
1612
2115
  }
2116
+ else if (lastResult === "idle" && startupKey && onHeldSourcesPrepared && !expectedOwner.heldSourcesPreparedIssued) {
2117
+ try {
2118
+ const ctx = expectedOwner.ctx, key = startupKey;
2119
+ const isCurrent = () => owner === expectedOwner && !blocked && !expectedOwner.controller.signal.aborted &&
2120
+ deps.getJournalBindingKey?.() === key && deps.isContextCurrent?.(ctx) !== false && deps.hasAuthority(ctx);
2121
+ if (!isCurrent())
2122
+ return;
2123
+ expectedOwner.heldSourcesPreparedIssued = true;
2124
+ // Do not couple lifecycle/stop or the worker drain to a held controller/API reply.
2125
+ void Promise.resolve().then(() => isCurrent() ? onHeldSourcesPrepared({ ctx,
2126
+ journalBindingKey: key, signal: expectedOwner.controller.signal, isCurrent }) : undefined).catch(error => {
2127
+ try {
2128
+ deps.recordRuntimeEvent?.("inbound-worker", error, { phase: "held-source-preparation" });
2129
+ }
2130
+ catch { /* Diagnostic only. */ }
2131
+ });
2132
+ }
2133
+ catch (error) {
2134
+ try {
2135
+ deps.recordRuntimeEvent?.("inbound-worker", error, { phase: "held-source-preparation" });
2136
+ }
2137
+ catch { /* Diagnostic only. */ }
2138
+ }
2139
+ }
1613
2140
  }
1614
2141
  };
1615
2142
  tracked = operation.then(() => finish(), (error) => {
@@ -1621,6 +2148,76 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1621
2148
  });
1622
2149
  drainPromise = tracked;
1623
2150
  };
2151
+ const abandonSource = (input, historicalSource) => {
2152
+ const expectedOwner = owner;
2153
+ const source = deferredSources.get(input.updateId);
2154
+ const abandonPending = deps.journal.abandonPending;
2155
+ const isCurrent = () => {
2156
+ if (!expectedOwner || owner !== expectedOwner || input.signal.aborted ||
2157
+ expectedOwner.controller.signal !== input.signal || !source ||
2158
+ source.entry.routingInput?.phase === "selected" || (source.historical && historicalSource !== source) ||
2159
+ (claims.get(input.updateId) !== "deferred" && claims.get(input.updateId) !== "abandoning" &&
2160
+ !(claims.get(input.updateId) === "historical" && historicalSource === source)))
2161
+ return false;
2162
+ return input.isCurrent() && deps.isContextCurrent?.(expectedOwner.ctx) !== false &&
2163
+ deps.getJournalBindingKey?.() === source.journalBindingKey && deps.hasAuthority(expectedOwner.ctx);
2164
+ };
2165
+ if (!source || !abandonPending || !isCurrent())
2166
+ return undefined;
2167
+ // Retain this claim on failure, including unknown commit: never reopen dispatch.
2168
+ claims.set(input.updateId, "abandoning");
2169
+ try {
2170
+ const result = abandonPending({ ...input, entry: source.entry,
2171
+ journalBindingKey: source.journalBindingKey, isCurrent });
2172
+ claims.delete(input.updateId);
2173
+ deferredSources.delete(input.updateId);
2174
+ state.journalEntryCount = result.entryCount;
2175
+ state.journalSerializedBytes = result.serializedBytes;
2176
+ transition("idle");
2177
+ pendingSignal = true;
2178
+ launchDrain();
2179
+ return result;
2180
+ }
2181
+ catch (error) {
2182
+ recordRuntimeEvent(error, { phase: "pending-abandonment", updateId: input.updateId });
2183
+ updateClaimCounts();
2184
+ notifyStateChange();
2185
+ throw error;
2186
+ }
2187
+ };
2188
+ const inspectRecovery = (input, kind) => {
2189
+ const expectedOwner = owner;
2190
+ if (!expectedOwner)
2191
+ return undefined;
2192
+ const { signal, journalBindingKey, isCurrent: callerIsCurrent, afterUpdateId } = input;
2193
+ const isOwnerCurrent = () => owner === expectedOwner && expectedOwner.controller.signal === signal && !signal.aborted;
2194
+ const isCurrent = () => isOwnerCurrent() && callerIsCurrent() && deps.isContextCurrent?.(expectedOwner.ctx) !== false &&
2195
+ deps.getJournalBindingKey?.() === journalBindingKey && deps.hasAuthority(expectedOwner.ctx);
2196
+ if (!isCurrent() || !deps.journal.abandonPending || !deps.journal.inspectPendingRetention ||
2197
+ (afterUpdateId !== undefined && (!Number.isSafeInteger(afterUpdateId) || afterUpdateId < 0)))
2198
+ return undefined;
2199
+ const matches = [...deferredSources].filter(([id, source]) => claims.get(id) === kind &&
2200
+ source.journalBindingKey === journalBindingKey && (afterUpdateId === undefined || id > afterUpdateId))
2201
+ .sort(([a], [b]) => a - b);
2202
+ const sources = matches.slice(0, TELEGRAM_HELD_SOURCE_INSPECTION_LIMIT).map(([updateId, source]) => {
2203
+ let committed;
2204
+ return { original: structuredClone(source.entry), retry(authority) {
2205
+ const authorized = () => isOwnerCurrent() && authority.isCurrent() && isCurrent();
2206
+ if (!authorized())
2207
+ return undefined;
2208
+ if (committed)
2209
+ return { ...committed, duplicate: true };
2210
+ const stillProtected = () => authorized() && (claims.get(updateId) === kind || claims.get(updateId) === "abandoning") &&
2211
+ deferredSources.get(updateId) === source;
2212
+ if (!stillProtected())
2213
+ return undefined;
2214
+ committed = abandonSource({ ...authority, updateId, signal, isCurrent: stillProtected }, source);
2215
+ return committed;
2216
+ } };
2217
+ });
2218
+ return isCurrent() ? { sources,
2219
+ ...(matches.length > TELEGRAM_HELD_SOURCE_INSPECTION_LIMIT ? { nextAfterUpdateId: matches[TELEGRAM_HELD_SOURCE_INSPECTION_LIMIT - 1][0] } : {}) } : undefined;
2220
+ };
1624
2221
  return {
1625
2222
  start(ctx) {
1626
2223
  if (owner) {
@@ -1631,6 +2228,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1631
2228
  return;
1632
2229
  }
1633
2230
  clearRetryTimer();
2231
+ clearRoutingTimer();
1634
2232
  const nowMs = getNowMs();
1635
2233
  const generation = ++nextGeneration;
1636
2234
  const queueOwnerIdentity = resolveQueueOwnerIdentity(ctx, generation);
@@ -1641,6 +2239,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1641
2239
  queueOwnerIdentity,
1642
2240
  };
1643
2241
  claims.clear();
2242
+ deferredSources.clear();
1644
2243
  committedQueueReceipts.clear();
1645
2244
  blocked = false;
1646
2245
  pendingSignal = true;
@@ -1653,6 +2252,8 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1653
2252
  state.journalSerializedBytes = 0;
1654
2253
  state.oldestAdmittedAtMs = undefined;
1655
2254
  state.deferredClaimCount = 0;
2255
+ delete state.abandoningClaimCount;
2256
+ delete state.historicalClaimCount;
1656
2257
  state.queuedClaimCount = 0;
1657
2258
  state.foreignQueuedCount = 0;
1658
2259
  delete state.foreignQueuedOwner;
@@ -1683,7 +2284,52 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1683
2284
  pendingSignal = true;
1684
2285
  launchDrain();
1685
2286
  },
1686
- settleDeferred(input) {
2287
+ armRoutingInput(input) { return mutateRoutingSource(input, false)?.lifetime; },
2288
+ selectRoutingInput(input) { return mutateRoutingSource(input, true)?.issued === true; },
2289
+ inspectDeferredSource(input) {
2290
+ const expectedOwner = owner;
2291
+ const source = deferredSources.get(input.updateId);
2292
+ const current = () => {
2293
+ if (!expectedOwner || owner !== expectedOwner || input.signal.aborted || expectedOwner.controller.signal !== input.signal ||
2294
+ !source || deferredSources.get(input.updateId) !== source || claims.get(input.updateId) !== "deferred" ||
2295
+ deps.getJournalBindingKey?.() !== source.journalBindingKey || deps.isContextCurrent?.(expectedOwner.ctx) === false ||
2296
+ !deps.hasAuthority(expectedOwner.ctx))
2297
+ return false;
2298
+ return isQueueOwnerIdentityCurrent(expectedOwner);
2299
+ };
2300
+ if (!current())
2301
+ return undefined;
2302
+ const snapshot = deps.journal.read();
2303
+ const entry = snapshot.entries.find(value => value.updateId === input.updateId);
2304
+ if (!isTelegramUpdateJournalLegacyFamilyVersion(snapshot.version) || !entry ||
2305
+ JSON.stringify(entry) !== JSON.stringify(source.entry) || !current())
2306
+ return undefined;
2307
+ return { journalBindingKey: source.journalBindingKey, ...createTelegramUpdateJournalEntryDigest(entry) };
2308
+ },
2309
+ isHistoricalSource(input) {
2310
+ const source = deferredSources.get(input.updateId);
2311
+ const isCurrent = () => owner?.controller.signal === input.signal && !input.signal.aborted &&
2312
+ source?.historical === true && deferredSources.get(input.updateId) === source;
2313
+ if (!isCurrent())
2314
+ return false;
2315
+ // Domain eligibility observes original evidence, never mutable handler projections.
2316
+ return (!input.matchesOriginal || input.matchesOriginal(structuredClone(source.entry)) === true) && isCurrent();
2317
+ },
2318
+ supportsDeferredAbandonment(input) {
2319
+ const expectedOwner = owner;
2320
+ const source = deferredSources.get(input.updateId);
2321
+ if (!expectedOwner || input.signal.aborted || expectedOwner.controller.signal !== input.signal ||
2322
+ !source || source.historical || input.journalBindingKey !== source.journalBindingKey ||
2323
+ !deps.journal.abandonPending || !deps.journal.inspectPendingRetention ||
2324
+ claims.get(input.updateId) === "queued")
2325
+ return false;
2326
+ return deps.isContextCurrent?.(expectedOwner.ctx) !== false &&
2327
+ deps.getJournalBindingKey?.() === source.journalBindingKey && deps.hasAuthority(expectedOwner.ctx);
2328
+ },
2329
+ abandonDeferred(input) { return abandonSource(input); },
2330
+ inspectAbandoning(input) { return inspectRecovery(input, "abandoning"); },
2331
+ inspectHistorical(input) { return inspectRecovery(input, "historical"); },
2332
+ async settleDeferred(input) {
1687
2333
  const expectedOwner = owner;
1688
2334
  if (!expectedOwner ||
1689
2335
  expectedOwner.controller.signal !== input.signal ||
@@ -1700,11 +2346,24 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1700
2346
  return;
1701
2347
  }
1702
2348
  const claim = claims.get(input.updateId);
2349
+ if (claim === "abandoning" || claim === "historical" || claim === "retained")
2350
+ return;
1703
2351
  if (input.outcome.kind === "complete") {
1704
2352
  // Expiry may retire only a still-deferred source, never accepted queue work.
1705
2353
  if (claim !== "deferred")
1706
2354
  return;
1707
- const result = commitCompletedBatch(expectedOwner, [input.updateId]);
2355
+ let completion;
2356
+ try {
2357
+ completion = validateTelegramUpdateAdmissionOutcome(input.outcome, input.updateId, new Set([input.updateId]));
2358
+ }
2359
+ catch (error) {
2360
+ blockWithFailure("invalid-outcome", "late-invalid-completion", error, input.updateId);
2361
+ return;
2362
+ }
2363
+ if (completion.kind !== "complete")
2364
+ return;
2365
+ const result = commitCompletedBatch(expectedOwner, [{ updateId: input.updateId,
2366
+ ...(completion.expectedSource ? { expectedSource: completion.expectedSource } : {}) }]);
1708
2367
  if (!result)
1709
2368
  transition("idle", input.updateId);
1710
2369
  return;
@@ -1721,7 +2380,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1721
2380
  }
1722
2381
  let outcome;
1723
2382
  try {
1724
- outcome = validateTelegramUpdateAdmissionOutcome(input.outcome, input.updateId, new Set([input.updateId, ...claims.keys()]));
2383
+ outcome = validateTelegramUpdateAdmissionOutcome(input.outcome, input.updateId, new Set([input.updateId, ...[...claims].filter(([, claim]) => claim !== "abandoning" && claim !== "historical" && claim !== "retained").map(([id]) => id)]));
1725
2384
  }
1726
2385
  catch (error) {
1727
2386
  blockWithFailure("invalid-outcome", "late-invalid-outcome", error, input.updateId);
@@ -1737,7 +2396,8 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1737
2396
  releaseDeferredClaims(outcome.sourceUpdateIds);
1738
2397
  return;
1739
2398
  }
1740
- const result = commitQueuedOutcome(expectedOwner, input.updateId, outcome);
2399
+ const publication = commitQueuedOutcome(expectedOwner, input.updateId, outcome);
2400
+ const result = typeof publication === "string" ? publication : await publication;
1741
2401
  if (result === "blocked") {
1742
2402
  releaseDeferredClaims(outcome.sourceUpdateIds);
1743
2403
  return;
@@ -1746,7 +2406,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1746
2406
  return;
1747
2407
  transition("queued", input.updateId);
1748
2408
  },
1749
- settleCustodied(input) {
2409
+ async settleCustodied(input) {
1750
2410
  const expectedOwner = owner;
1751
2411
  if (!expectedOwner || expectedOwner.controller.signal !== input.signal || input.signal.aborted)
1752
2412
  return;
@@ -1791,7 +2451,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1791
2451
  for (const sourceUpdateId of receipt.sourceUpdateIds)
1792
2452
  claims.set(sourceUpdateId, "queued");
1793
2453
  try {
1794
- publishCommittedQueueReceipt(receipt, input.result.queueReceipt.queueOwner, expectedOwner.ctx);
2454
+ await publishCommittedQueueReceipt(receipt, input.result.queueReceipt.queueOwner, expectedOwner);
1795
2455
  }
1796
2456
  catch (error) {
1797
2457
  blockWithFailure("invalid-outcome", "queue-receipt-publish", error, input.updateId);
@@ -1804,16 +2464,32 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1804
2464
  const committed = getCurrentQueueReceipt(receipt);
1805
2465
  return committed ? { ...committed.queueOwner } : undefined;
1806
2466
  },
1807
- completeQueueReceipts(input) {
2467
+ getQueueReceiptSettlementOwner(receipt, ctx, reason) {
2468
+ const expectedOwner = owner, committed = committedQueueReceipts.get(receipt.receiptId);
2469
+ if (!expectedOwner || expectedOwner.controller.signal.aborted ||
2470
+ !(deps.isContextCurrent?.(ctx) ?? expectedOwner.ctx === ctx) || !isQueueOwnerIdentityCurrent(expectedOwner) ||
2471
+ !committed || !areTelegramQueueAdmissionReceiptsEqual(committed.receipt, normalizeQueueReceipt(receipt)) ||
2472
+ (deps.getJournalBindingKey && committed.receipt.journalBindingKey !== deps.getJournalBindingKey()) ||
2473
+ !isTelegramUpdateJournalQueueOwnerProcess(committed.queueOwner, expectedOwner.queueOwnerIdentity))
2474
+ return undefined;
2475
+ const attempt = scopedQueueCompletionAttempts.get(receipt.receiptId);
2476
+ if (attempt ? attempt.owner !== expectedOwner || attempt.reason !== reason || attempt.journalBindingKey !== deps.getJournalBindingKey?.()
2477
+ : !getCurrentQueueReceipt(receipt))
2478
+ return undefined;
2479
+ return { ...committed.queueOwner };
2480
+ },
2481
+ completeQueueReceipts: function completeQueueReceipts(input) {
1808
2482
  const expectedOwner = owner;
1809
2483
  if (!expectedOwner ||
1810
2484
  expectedOwner.controller.signal.aborted ||
1811
2485
  !(deps.isContextCurrent?.(input.ctx) ?? expectedOwner.ctx === input.ctx)) {
1812
2486
  return false;
1813
2487
  }
1814
- if (input.receipts.length === 0)
1815
- return true;
1816
- const normalizedReceipts = input.receipts.map(normalizeQueueReceipt);
2488
+ // An ACK permits only local completion reconciliation, never execution readiness.
2489
+ const receipts = input.sourceCompletions === undefined ? input.receipts.filter(receipt => !isQueueReceiptCompletionAcknowledged(receipt)) : input.receipts;
2490
+ if (receipts.length === 0)
2491
+ return input.sourceCompletions === undefined;
2492
+ const normalizedReceipts = receipts.map(normalizeQueueReceipt);
1817
2493
  const receiptIds = new Set();
1818
2494
  const sourceUpdateIds = new Set();
1819
2495
  const queuedCompletions = [];
@@ -1837,9 +2513,91 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1837
2513
  sourceUpdateIds.add(updateId);
1838
2514
  }
1839
2515
  }
2516
+ const completionBinding = deps.getJournalBindingKey?.();
2517
+ const ordinaryObserverBound = normalizedReceipts.every(receipt => committedQueueReceipts.get(receipt.receiptId).receipt.journalBindingKey === completionBinding);
2518
+ const ordinaryCompletionCurrent = () => ordinaryObserverBound && owner === expectedOwner &&
2519
+ !expectedOwner.controller.signal.aborted && (deps.isContextCurrent?.(input.ctx) ?? expectedOwner.ctx === input.ctx) &&
2520
+ isQueueOwnerIdentityCurrent(expectedOwner) && deps.getJournalBindingKey?.() === completionBinding;
1840
2521
  let removedUpdateIds;
2522
+ let scopedCompletionCurrent;
1841
2523
  try {
1842
- removedUpdateIds = deps.journal.completeQueued(queuedCompletions).removedUpdateIds;
2524
+ const prepared = normalizedReceipts.flatMap(receipt => committedQueueReceipts.get(receipt.receiptId).sourceCompletions ?? []);
2525
+ const sourceCompletions = input.sourceCompletions === undefined ? (prepared.length ? prepared : undefined) : input.sourceCompletions;
2526
+ if (input.sourceCompletions !== undefined && prepared.some(expected => !input.sourceCompletions.some(value => isDeepStrictEqual(value, expected)))) {
2527
+ throw new Error("Telegram queued completion conflicts with its prepared source scopes.");
2528
+ }
2529
+ if (input.sourceCompletions === undefined && prepared.length && receipts.some(receipt => !requiredScopedQueueReceipts.has(receipt.receiptId))) {
2530
+ const scoped = receipts.filter(receipt => requiredScopedQueueReceipts.has(receipt.receiptId));
2531
+ const ordinary = receipts.filter(receipt => !requiredScopedQueueReceipts.has(receipt.receiptId));
2532
+ if (!scoped.length || !ordinary.length)
2533
+ throw new Error("Telegram mixed queue completion has incomplete scoped authority.");
2534
+ const binding = deps.getJournalBindingKey?.();
2535
+ const current = () => owner === expectedOwner && !expectedOwner.controller.signal.aborted &&
2536
+ (deps.isContextCurrent?.(input.ctx) ?? expectedOwner.ctx === input.ctx) && isQueueOwnerIdentityCurrent(expectedOwner) &&
2537
+ !!binding && deps.getJournalBindingKey?.() === binding;
2538
+ // Independent whole receipts need independent ACKs. Ordinary siblings must not acquire invented scopes.
2539
+ if (!current() || !completeQueueReceipts({ receipts: scoped, ctx: input.ctx, reason: input.reason }) || !current())
2540
+ return false;
2541
+ return completeQueueReceipts({ receipts: ordinary, ctx: input.ctx, reason: input.reason });
2542
+ }
2543
+ if (sourceCompletions === undefined) {
2544
+ if (queuedCompletions.some(receipt => requiredScopedQueueReceipts.has(receipt.receiptId))) {
2545
+ throw new Error("Telegram scoped queue completion cannot downgrade required source scopes.");
2546
+ }
2547
+ removedUpdateIds = deps.journal.completeQueued(queuedCompletions).removedUpdateIds;
2548
+ }
2549
+ else {
2550
+ for (const receipt of queuedCompletions)
2551
+ requiredScopedQueueReceipts.add(receipt.receiptId);
2552
+ queuedCompletions.sort((a, b) => a.receiptId < b.receiptId ? -1 : a.receiptId > b.receiptId ? 1 : 0);
2553
+ const journalBindingKey = deps.getJournalBindingKey?.();
2554
+ const current = () => owner === expectedOwner && !expectedOwner.controller.signal.aborted &&
2555
+ (deps.isContextCurrent?.(input.ctx) ?? expectedOwner.ctx === input.ctx) && isQueueOwnerIdentityCurrent(expectedOwner) &&
2556
+ deps.getJournalBindingKey?.() === journalBindingKey;
2557
+ if (!journalBindingKey || !current() || !completeQueuedExact || !inspectCompletion ||
2558
+ !Array.isArray(sourceCompletions) || input.sourceCompletions !== undefined && sourceCompletions.length !== sourceUpdateIds.size) {
2559
+ throw new Error("Telegram scoped queue completion requires full current source authority.");
2560
+ }
2561
+ scopedCompletionCurrent = current;
2562
+ const completions = normalizeTelegramQueueSourceCompletions(sourceCompletions, sourceUpdateIds, journalBindingKey, input.sourceCompletions !== undefined)
2563
+ .map(({ journalBindingKey: _binding, ...completion }) => completion);
2564
+ if (queuedCompletions.some(receipt => !completions.some(scope => receipt.sourceUpdateIds.includes(scope.updateId)))) {
2565
+ throw new Error("Telegram scoped queue completion requires a proven origin for every whole receipt.");
2566
+ }
2567
+ const previous = queuedCompletions.map(receipt => scopedQueueCompletionAttempts.get(receipt.receiptId));
2568
+ const retained = previous.find(attempt => attempt !== undefined);
2569
+ if (retained && (previous.some(attempt => attempt !== retained) || retained.owner !== expectedOwner ||
2570
+ retained.journalBindingKey !== journalBindingKey || retained.reason !== input.reason ||
2571
+ !isDeepStrictEqual(retained.receipts, queuedCompletions) || !isDeepStrictEqual(retained.completions, completions))) {
2572
+ throw new Error("Telegram scoped queue completion has conflicting issued authority.");
2573
+ }
2574
+ if (!retained) {
2575
+ const attempt = { owner: expectedOwner, journalBindingKey, reason: input.reason,
2576
+ receipts: queuedCompletions.map(receipt => ({ ...receipt, sourceUpdateIds: [...receipt.sourceUpdateIds], queueOwner: { ...receipt.queueOwner } })),
2577
+ completions: completions.map(completion => ({ ...completion })) };
2578
+ if (!current())
2579
+ throw new Error("Telegram scoped queue completion authority changed before disposal.");
2580
+ // Mark before issuance: an exception may follow a committed rename, and cannot license another disposal.
2581
+ for (const receipt of queuedCompletions)
2582
+ scopedQueueCompletionAttempts.set(receipt.receiptId, attempt);
2583
+ const result = completeQueuedExact(queuedCompletions.map(receipt => ({ ...receipt, sourceUpdateIds: [...receipt.sourceUpdateIds], queueOwner: { ...receipt.queueOwner } })), completions.map(completion => ({ ...completion })));
2584
+ if (!current() || !isDeepStrictEqual(result.sourceCompletions, completions)) {
2585
+ throw new Error("Telegram scoped queue completion ACK was not confirmed.");
2586
+ }
2587
+ const removed = new Set(result.removedUpdateIds);
2588
+ if (removed.size !== sourceUpdateIds.size || [...sourceUpdateIds].some(id => !removed.has(id))) {
2589
+ throw new Error("Telegram scoped queue completion did not remove every source.");
2590
+ }
2591
+ }
2592
+ for (const completion of completions) {
2593
+ if (!current() || !isDeepStrictEqual(inspectCompletion({ ...completion }), completion) || !current()) {
2594
+ throw new Error("Telegram scoped queue completion ACK was not retained under current authority.");
2595
+ }
2596
+ }
2597
+ // Every immutable whole receipt has a scoped queued-origin witness; native queued ACK continuity proves its complete removal.
2598
+ // Unscoped siblings receive only receipt acknowledgement, never a source marker or absence-based proof.
2599
+ removedUpdateIds = [...sourceUpdateIds];
2600
+ }
1843
2601
  }
1844
2602
  catch (error) {
1845
2603
  blockWithFailure("journal-write", "queue-receipt-completion", error);
@@ -1851,13 +2609,15 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1851
2609
  blockWithFailure("journal-write", "queue-receipt-completion", new Error(`Telegram ${input.reason} did not complete every receipt source.`));
1852
2610
  return false;
1853
2611
  }
1854
- input.receipts.forEach((receipt, index) => {
2612
+ receipts.forEach((receipt, index) => {
1855
2613
  acknowledgedQueueReceiptCompletions.set(receipt, normalizedReceipts[index]);
1856
2614
  });
1857
2615
  for (const updateId of sourceUpdateIds)
1858
2616
  claims.delete(updateId);
1859
2617
  for (const receiptId of receiptIds) {
1860
2618
  committedQueueReceipts.delete(receiptId);
2619
+ requiredScopedQueueReceipts.delete(receiptId);
2620
+ scopedQueueCompletionAttempts.delete(receiptId);
1861
2621
  }
1862
2622
  const completedUpdateIds = [...sourceUpdateIds];
1863
2623
  state.lastCompletedUpdateId = Math.max(state.lastCompletedUpdateId ?? -1, ...completedUpdateIds);
@@ -1865,8 +2625,33 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1865
2625
  state.journalEntryCount = Math.max(0, state.journalEntryCount - completedUpdateIds.length);
1866
2626
  updateClaimCounts();
1867
2627
  notifyStateChange();
1868
- pendingSignal = true;
1869
- launchDrain();
2628
+ if (scopedCompletionCurrent)
2629
+ for (const receipt of normalizedReceipts) {
2630
+ if (!scopedCompletionCurrent())
2631
+ break;
2632
+ try {
2633
+ observeQueuedCompletion?.(normalizeQueueReceipt(receipt), input.ctx);
2634
+ }
2635
+ catch (error) {
2636
+ recordRuntimeEvent(error, { phase: "queue-receipt-completion-observer", receiptId: receipt.receiptId });
2637
+ }
2638
+ }
2639
+ // Ordinary queued disposal is source completion too; notify only after a whole positive ACK and fresh origin authority.
2640
+ if (!scopedCompletionCurrent)
2641
+ for (const updateId of completedUpdateIds) {
2642
+ try {
2643
+ if (!ordinaryCompletionCurrent())
2644
+ break;
2645
+ deps.onUpdateCompleted?.(updateId, input.ctx, completionBinding);
2646
+ }
2647
+ catch (error) {
2648
+ recordRuntimeEvent(error, { phase: "queue-source-completion-observer", updateId });
2649
+ }
2650
+ }
2651
+ if (!scopedCompletionCurrent || scopedCompletionCurrent()) {
2652
+ pendingSignal = true;
2653
+ launchDrain();
2654
+ }
1870
2655
  return true;
1871
2656
  },
1872
2657
  async stop() {
@@ -1875,6 +2660,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1875
2660
  return;
1876
2661
  pendingSignal = false;
1877
2662
  clearRetryTimer();
2663
+ clearRoutingTimer();
1878
2664
  expectedOwner.controller.abort();
1879
2665
  await drainPromise?.catch(() => undefined);
1880
2666
  if (owner !== expectedOwner)
@@ -1882,6 +2668,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1882
2668
  owner = undefined;
1883
2669
  drainPromise = undefined;
1884
2670
  claims.clear();
2671
+ deferredSources.clear();
1885
2672
  committedQueueReceipts.clear();
1886
2673
  blocked = false;
1887
2674
  state.phase = "stopped";
@@ -1889,6 +2676,8 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1889
2676
  state.currentUpdateId = undefined;
1890
2677
  state.blockedReason = undefined;
1891
2678
  state.deferredClaimCount = 0;
2679
+ delete state.abandoningClaimCount;
2680
+ delete state.historicalClaimCount;
1892
2681
  state.queuedClaimCount = 0;
1893
2682
  state.foreignQueuedCount = 0;
1894
2683
  delete state.foreignQueuedOwner;
@@ -1907,8 +2696,9 @@ export function createTelegramUpdateWorkerRuntime(deps) {
1907
2696
  state.unsettledExecutionCount = unsettledExecutions.size;
1908
2697
  notifyStateChange();
1909
2698
  },
1910
- waitForDrain() {
1911
- return drainPromise ?? Promise.resolve();
2699
+ async waitForDrain() {
2700
+ await (drainPromise ?? Promise.resolve());
2701
+ await Promise.allSettled([...pendingQueuePublications.values()].map(value => value.task));
1912
2702
  },
1913
2703
  getState() {
1914
2704
  return getTelegramUpdateWorkerStateSnapshot(state);
@@ -2019,8 +2809,21 @@ function mergeTelegramReportedAdmissionOutcome(current, next, updateId) {
2019
2809
  return next;
2020
2810
  if (next.kind === "deferred")
2021
2811
  return current;
2022
- if (current.kind === "complete" && next.kind === "complete")
2023
- return current;
2812
+ if (current.kind === "complete" && next.kind === "complete") {
2813
+ if (!current.expectedSource)
2814
+ return next;
2815
+ if (!next.expectedSource)
2816
+ return current;
2817
+ if (current.expectedSource.updateId === next.expectedSource.updateId &&
2818
+ current.expectedSource.journalBindingKey === next.expectedSource.journalBindingKey &&
2819
+ current.expectedSource.sourceSha256 === next.expectedSource.sourceSha256) {
2820
+ if (!current.expectedSource.completionSha256)
2821
+ return next;
2822
+ if (!next.expectedSource.completionSha256 || current.expectedSource.completionSha256 === next.expectedSource.completionSha256)
2823
+ return current;
2824
+ }
2825
+ throw new TelegramUpdateAdmissionOutcomeError(`Telegram update ${updateId} reported conflicting completion sources.`);
2826
+ }
2024
2827
  if (current.kind === "queued" && next.kind === "queued" &&
2025
2828
  areTelegramQueueAdmissionReceiptsEqual(current, next))
2026
2829
  return current;
@@ -2736,13 +3539,21 @@ export function createTelegramUpdateAdmissionHandle(deps) {
2736
3539
  let nextExecutionGeneration = 0;
2737
3540
  return async (update, ctx, signal) => {
2738
3541
  const generation = ++nextExecutionGeneration;
3542
+ let suspended = false;
3543
+ let routingClaims = 0;
3544
+ let abandoned;
3545
+ let routingLifetime;
3546
+ let routingSourceIds;
3547
+ let routingClockUnknown = false;
3548
+ let routingSelectionAttempted = false;
3549
+ let routingSelectionConfirmed = false;
2739
3550
  const execution = {
2740
3551
  generation,
2741
3552
  updateId: update.update_id,
2742
3553
  signal,
2743
- isCurrent: () => !signal.aborted,
3554
+ isCurrent: () => !signal.aborted && !suspended,
2744
3555
  assertCurrent() {
2745
- if (signal.aborted) {
3556
+ if (signal.aborted || suspended) {
2746
3557
  throw signal.reason ?? new DOMException("Aborted", "AbortError");
2747
3558
  }
2748
3559
  },
@@ -2753,8 +3564,19 @@ export function createTelegramUpdateAdmissionHandle(deps) {
2753
3564
  if (verdict === "consume")
2754
3565
  return { kind: "complete" };
2755
3566
  let immediate = true;
3567
+ let settlementReported = false;
2756
3568
  let outcome;
2757
3569
  const boundUpdate = bindTelegramUpdateExecutionFence(bindTelegramUpdateAdmissionSource(update, (next) => {
3570
+ if (suspended || signal.aborted)
3571
+ return;
3572
+ if (next.kind === "deferred" && next.routingReview) {
3573
+ if (!immediate || settlementReported || routingClaims > 0 || !deps.isHistoricalSource?.({ updateId: update.update_id, signal })) {
3574
+ throw new TelegramUpdateAdmissionOutcomeError("Historical review cannot override accepted or selected work.");
3575
+ }
3576
+ suspended = true;
3577
+ }
3578
+ if (next.kind !== "deferred")
3579
+ settlementReported = true;
2758
3580
  if (immediate) {
2759
3581
  outcome = mergeTelegramReportedAdmissionOutcome(outcome, next, update.update_id);
2760
3582
  return;
@@ -2763,11 +3585,15 @@ export function createTelegramUpdateAdmissionHandle(deps) {
2763
3585
  throw new TelegramUpdateAdmissionOutcomeError(`Telegram update ${update.update_id} reported a late outcome without an owner.`);
2764
3586
  }
2765
3587
  void Promise.resolve()
2766
- .then(() => deps.onLateOutcome(next, {
2767
- updateId: update.update_id,
2768
- ctx,
2769
- signal,
2770
- }))
3588
+ .then(() => {
3589
+ if (suspended || signal.aborted)
3590
+ return;
3591
+ return deps.onLateOutcome(next, {
3592
+ updateId: update.update_id,
3593
+ ctx,
3594
+ signal,
3595
+ });
3596
+ })
2771
3597
  .catch((error) => {
2772
3598
  try {
2773
3599
  deps.onLateOutcomeError?.(error, update.update_id);
@@ -2776,11 +3602,81 @@ export function createTelegramUpdateAdmissionHandle(deps) {
2776
3602
  // Diagnostic sinks must not create an unhandled late Promise.
2777
3603
  }
2778
3604
  });
2779
- }), execution);
3605
+ }, { abandon: (authority) => {
3606
+ const isCurrent = () => !signal.aborted && routingClaims === 0 && !settlementReported && authority.isCurrent();
3607
+ if (immediate || outcome?.kind !== "deferred" || !deps.abandonDeferred || !isCurrent())
3608
+ return undefined;
3609
+ if (abandoned)
3610
+ return { ...abandoned, duplicate: true };
3611
+ const wasSuspended = suspended;
3612
+ suspended = true;
3613
+ // Failed/unknown publication keeps dispatch suspended for exact cancellation retry.
3614
+ // An ineligible request that attempted no publication remains inert.
3615
+ const result = deps.abandonDeferred({ ...authority, updateId: update.update_id, signal, isCurrent });
3616
+ if (result)
3617
+ abandoned = result;
3618
+ else
3619
+ suspended = wasSuspended;
3620
+ return result;
3621
+ }, inspectAbandoning: request => execution.isCurrent()
3622
+ ? deps.inspectAbandoning?.({ ...request, signal }) : undefined,
3623
+ inspectHistorical: request => execution.isCurrent() ? deps.inspectHistorical?.({ ...request, signal }) : undefined,
3624
+ inspectSource: () => execution.isCurrent() && !immediate && !settlementReported
3625
+ ? deps.inspectDeferredSource?.({ updateId: update.update_id, signal }) : undefined,
3626
+ isHistorical: matchesOriginal => !signal.aborted && deps.isHistoricalSource?.({ updateId: update.update_id, signal,
3627
+ ...(matchesOriginal ? { matchesOriginal } : {}) }) === true,
3628
+ isHistoricalReviewHeld: () => !signal.aborted && suspended && outcome?.kind === "deferred" && outcome.routingReview === true,
3629
+ supportsAbandonment: journalBindingKey => !signal.aborted && !settlementReported &&
3630
+ deps.supportsDeferredAbandonment?.({ updateId: update.update_id, signal, journalBindingKey }) === true,
3631
+ armRoutingInput: (operatorUserId, sourceUpdateIds) => {
3632
+ execution.assertCurrent();
3633
+ if (settlementReported || routingClaims || suspended)
3634
+ return undefined;
3635
+ if (routingClockUnknown)
3636
+ throw new TelegramUpdateAdmissionOutcomeError("Telegram routing input clock publication is unconfirmed.");
3637
+ const ids = [...sourceUpdateIds];
3638
+ if (routingSourceIds && !isDeepStrictEqual(ids, routingSourceIds))
3639
+ throw new TelegramUpdateAdmissionOutcomeError("Telegram routing input source membership changed.");
3640
+ try {
3641
+ routingLifetime = deps.armRoutingInput?.({ updateId: update.update_id, signal, operatorUserId, sourceUpdateIds: ids });
3642
+ }
3643
+ catch (error) {
3644
+ routingClockUnknown = true;
3645
+ throw error;
3646
+ }
3647
+ if (routingLifetime)
3648
+ routingSourceIds = ids;
3649
+ return routingLifetime ? { ...routingLifetime } : undefined;
3650
+ },
3651
+ acquireRouting: (select = false, sourceUpdateIds) => {
3652
+ execution.assertCurrent();
3653
+ if (select && routingClockUnknown)
3654
+ throw new TelegramUpdateAdmissionOutcomeError("Telegram routing input clock publication is unconfirmed; selection is protected.");
3655
+ if (select && routingLifetime && !routingSelectionConfirmed) {
3656
+ if (routingSelectionAttempted)
3657
+ throw new TelegramUpdateAdmissionOutcomeError("Telegram routing selection is unconfirmed; no replay is permitted.");
3658
+ if (!isDeepStrictEqual(sourceUpdateIds ?? [update.update_id], routingSourceIds))
3659
+ throw new TelegramUpdateAdmissionOutcomeError("Telegram routing input selection changed its source membership.");
3660
+ routingSelectionAttempted = true;
3661
+ routingSelectionConfirmed = deps.selectRoutingInput?.({ updateId: update.update_id, signal,
3662
+ operatorUserId: routingLifetime.operatorUserId, sourceUpdateIds: routingSourceIds }) === true;
3663
+ if (!routingSelectionConfirmed)
3664
+ throw new TelegramUpdateAdmissionOutcomeError("Telegram routing input choice is expired or unavailable.");
3665
+ }
3666
+ routingClaims += 1;
3667
+ let released = false;
3668
+ return () => {
3669
+ if (released)
3670
+ return;
3671
+ released = true;
3672
+ routingClaims -= 1;
3673
+ };
3674
+ } }), execution);
2780
3675
  try {
2781
3676
  execution.assertCurrent();
2782
3677
  await deps.defaultHandle(boundUpdate, ctx, execution);
2783
- execution.assertCurrent();
3678
+ if (outcome?.kind !== "deferred" || !outcome.routingReview || signal.aborted)
3679
+ execution.assertCurrent();
2784
3680
  }
2785
3681
  finally {
2786
3682
  immediate = false;
@@ -2813,17 +3709,21 @@ export function createTelegramQueueAdmissionSettlementMuxRuntime(runtimes) {
2813
3709
  // Plan the whole request before mutation; unready is not an acknowledgement of completion.
2814
3710
  const complete = (items, ctx, kind) => {
2815
3711
  const plan = new Map();
3712
+ const reason = kind === "prompt" ? "prompt-handoff" : kind === "control" ? "control-settlement" : "discard";
3713
+ const settlementOwner = (runtime, receipt) => runtime.getQueueReceiptSettlementOwner ? runtime.getQueueReceiptSettlementOwner(receipt, ctx, reason) :
3714
+ runtime.isItemReady({ admissionReceipts: [receipt] }) ? runtime.getQueueReceiptOwner(receipt) : undefined;
2816
3715
  for (const receipt of items.flatMap(item => item.admissionReceipts ?? [])) {
2817
- if (kind === "discard" && isQueueReceiptCompletionAcknowledged(receipt))
3716
+ if (isQueueReceiptCompletionAcknowledged(receipt))
2818
3717
  continue;
2819
- const candidates = runtimes.filter(runtime => runtime.isItemReady({ admissionReceipts: [receipt] }));
3718
+ const candidates = runtimes.filter(runtime => runtime.getQueueReceiptSettlementOwner ? !!settlementOwner(runtime, receipt)
3719
+ : runtime.isItemReady({ admissionReceipts: [receipt] }));
2820
3720
  const selected = candidates[0];
2821
3721
  if (!selected)
2822
3722
  return false;
2823
3723
  if (candidates.length > 1) {
2824
- const owner = selected.getQueueReceiptOwner(receipt);
3724
+ const owner = settlementOwner(selected, receipt);
2825
3725
  if (!owner || candidates.some(runtime => {
2826
- const candidate = runtime.getQueueReceiptOwner(receipt);
3726
+ const candidate = settlementOwner(runtime, receipt);
2827
3727
  return !candidate || !areTelegramUpdateJournalQueueOwnersEqual(owner, candidate);
2828
3728
  }))
2829
3729
  return false;
@@ -2868,7 +3768,7 @@ export function createTelegramQueueAdmissionSettlementRuntime(worker) {
2868
3768
  const receipts = [];
2869
3769
  for (const item of items) {
2870
3770
  for (const receipt of item.admissionReceipts ?? []) {
2871
- if (reason !== "discard" || !isQueueReceiptCompletionAcknowledged(receipt))
3771
+ if (!isQueueReceiptCompletionAcknowledged(receipt))
2872
3772
  receipts.push(receipt);
2873
3773
  }
2874
3774
  }
@@ -2877,6 +3777,7 @@ export function createTelegramQueueAdmissionSettlementRuntime(worker) {
2877
3777
  return {
2878
3778
  isItemReady: (item) => (item.admissionReceipts ?? []).every(worker.isQueueReceiptCommitted),
2879
3779
  getQueueReceiptOwner: worker.getQueueReceiptOwner,
3780
+ ...(worker.getQueueReceiptSettlementOwner ? { getQueueReceiptSettlementOwner: worker.getQueueReceiptSettlementOwner } : {}),
2880
3781
  onPromptHandedOff: (item, ctx) => complete([item], ctx, "prompt-handoff"),
2881
3782
  onControlSettled: (item, ctx) => complete([item], ctx, "control-settlement"),
2882
3783
  onItemsDiscarded: (items, ctx) => complete(items, ctx, "discard"),
@@ -3048,9 +3949,9 @@ export function createTelegramQueueHandoffReconciliationRuntimeAssembly(deps) {
3048
3949
  canHandoffWithLeader: deps.canHandoffWithLeader,
3049
3950
  listFollowers: deps.listFollowers,
3050
3951
  createRecipientJournalBindingKey(recipient) {
3051
- if (!recipient.profileKey)
3952
+ if (!recipient.profileKey || !recipient.sessionId)
3052
3953
  return undefined;
3053
- return deps.createRecipientJournalResolver(recipient.profileKey)()
3954
+ return deps.createRecipientJournalResolver(recipient.profileKey, recipient.sessionId)()
3054
3955
  ?.recoveryKey;
3055
3956
  },
3056
3957
  getQueuedItems: deps.queueStore.getQueuedItems,
@@ -3228,17 +4129,18 @@ export function createTelegramUpdateWorkerOwnerRuntime(deps) {
3228
4129
  sessionGeneration: deps.getSessionGeneration(),
3229
4130
  };
3230
4131
  },
3231
- onQueueReceiptCommitted(_receipt, ctx) {
4132
+ onQueueReceiptCommitted(receipt, ctx) {
3232
4133
  if (!deps.isContextCurrent(ctx))
3233
4134
  return;
4135
+ deps.afterQueueReceiptCommitted?.(receipt, ctx);
3234
4136
  deps.dispatchNext(ctx);
3235
4137
  deps.requestQueueHandoffReconciliation(ctx);
3236
4138
  },
3237
- onUpdateCompleted(updateId, ctx) {
4139
+ onUpdateCompleted(updateId, ctx, journalBindingKey) {
3238
4140
  if (!deps.isContextCurrent(ctx))
3239
4141
  return;
3240
4142
  deps.dispatchNext(ctx);
3241
- deps.afterUpdateCompleted?.(updateId);
4143
+ deps.afterUpdateCompleted?.(updateId, ctx, journalBindingKey);
3242
4144
  },
3243
4145
  };
3244
4146
  }
@@ -3247,6 +4149,7 @@ export function createTelegramUpdateAdmissionRuntimeBinding(deps) {
3247
4149
  let follower;
3248
4150
  let settlement;
3249
4151
  let inputCustodyBus;
4152
+ const getActive = () => deps.isFollowerRegistered() ? follower : leader;
3250
4153
  return {
3251
4154
  bind(input) {
3252
4155
  leader = input.leader;
@@ -3259,7 +4162,7 @@ export function createTelegramUpdateAdmissionRuntimeBinding(deps) {
3259
4162
  },
3260
4163
  getLeader: () => leader,
3261
4164
  getFollower: () => follower,
3262
- getActive: () => (deps.isFollowerRegistered() ? follower : leader),
4165
+ getActive,
3263
4166
  getSettlement: () => settlement,
3264
4167
  getInputCustodyBus: () => inputCustodyBus,
3265
4168
  getLifecycleForJournalBinding(journalBindingKey) {
@@ -3358,6 +4261,7 @@ export function createTelegramUpdateAdmissionLifecycleRuntime(deps) {
3358
4261
  binding.recoveryKey !== recoveryKey) {
3359
4262
  throw new Error("Telegram update source binding changed during startup.");
3360
4263
  }
4264
+ deps.prepareBinding?.(binding);
3361
4265
  const release = deps.acquireSourceReference?.(binding);
3362
4266
  const inputCustody = binding.journal.inputCustody;
3363
4267
  try {
@@ -3580,6 +4484,14 @@ export function createTelegramUpdateAdmissionWorkerRuntime(deps) {
3580
4484
  const executeUpdate = createTelegramUpdateAdmissionHandle({
3581
4485
  defaultHandle: deps.defaultHandle,
3582
4486
  registry: deps.registry,
4487
+ abandonDeferred: input => worker?.abandonDeferred?.(input),
4488
+ armRoutingInput: input => worker?.armRoutingInput?.(input),
4489
+ selectRoutingInput: input => worker?.selectRoutingInput?.(input) === true,
4490
+ supportsDeferredAbandonment: input => worker?.supportsDeferredAbandonment?.(input) === true,
4491
+ inspectAbandoning: input => worker?.inspectAbandoning?.(input),
4492
+ inspectHistorical: input => worker?.inspectHistorical?.(input),
4493
+ inspectDeferredSource: input => worker?.inspectDeferredSource?.(input),
4494
+ isHistoricalSource: input => worker?.isHistoricalSource?.(input) === true,
3583
4495
  onLateOutcome(outcome, details) {
3584
4496
  worker?.settleDeferred({
3585
4497
  updateId: details.updateId,
@@ -3647,6 +4559,7 @@ export function createTelegramUpdateAdmissionLifecycleAssembly(deps) {
3647
4559
  const follower = createTelegramUpdateAdmissionLifecycleRuntime({
3648
4560
  getQueueOwnerIdentity: deps.worker.getQueueOwnerIdentity,
3649
4561
  ...(deps.acquireSourceReference ? { acquireSourceReference: binding => deps.acquireSourceReference("follower", binding) } : {}),
4562
+ ...(deps.follower.prepareBinding ? { prepareBinding: deps.follower.prepareBinding } : {}),
3650
4563
  resolveBinding() {
3651
4564
  const generation = deps.follower.getGeneration();
3652
4565
  if (!deps.follower.isRegistered() || !generation)
@@ -3671,6 +4584,11 @@ export function createTelegramUpdateAdmissionLifecycleAssembly(deps) {
3671
4584
  getJournalBindingKey: () => binding.recoveryKey,
3672
4585
  getRecipientBindingKey: () => binding.recipientBindingKey,
3673
4586
  hasAuthority: () => binding.hasAuthority?.() ?? false,
4587
+ // Recipient custody is not an orphaned direct-bot source.
4588
+ shouldReviewHistoricalInput: undefined,
4589
+ shouldHoldPendingInput: undefined,
4590
+ spendHistoricalInput: undefined,
4591
+ onHeldSourcesPrepared: undefined,
3674
4592
  prepareUpdateForExecution: deps.follower.prepareUpdateForExecution,
3675
4593
  });
3676
4594
  },