@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
@@ -5,6 +5,11 @@
5
5
  */
6
6
 
7
7
  import { randomUUID } from "node:crypto";
8
+ import { isDeepStrictEqual } from "node:util";
9
+ import {
10
+ isWireRecord as isTelegramUpdateAdmissionRecord,
11
+ isNonEmptyWireString as isTelegramUpdateAdmissionString,
12
+ } from "./wire.ts";
8
13
 
9
14
  import {
10
15
  createTelegramPrivateTarget,
@@ -23,10 +28,16 @@ import {
23
28
  TELEGRAM_UPDATE_JOURNAL_VERSION,
24
29
  TELEGRAM_UPDATE_JOURNAL_EXCLUSION_VERSION,
25
30
  TELEGRAM_UPDATE_JOURNAL_CUSTODY_VERSION,
31
+ isTelegramUpdateJournalLegacyFamilyVersion,
26
32
  TELEGRAM_UPDATE_JOURNAL_FAILURE_CLASS_MAX_LENGTH,
27
33
  TELEGRAM_UPDATE_JOURNAL_FAILURE_SUMMARY_MAX_LENGTH,
28
34
  TELEGRAM_UPDATE_JOURNAL_QUEUE_OWNER_ID_MAX_LENGTH,
29
35
  areTelegramUpdateJournalQueueOwnersEqual,
36
+ createTelegramUpdateJournalEntryDigest,
37
+ type TelegramUpdateJournalEntryDigest,
38
+ type TelegramUpdateJournalSourceCompletion,
39
+ type TelegramUpdateJournalQueuedCompletion,
40
+ type TelegramUpdateJournalQueuedReceiptEvidence,
30
41
  getTelegramUpdateJournalBindingPath,
31
42
  isTelegramUpdateJournalQueueOwnerProcess,
32
43
  parseTelegramUpdateJournalQueueOwner,
@@ -39,6 +50,12 @@ import {
39
50
  type TelegramUpdateJournalInputClaim,
40
51
  type TelegramUpdateJournalOperatorDispositionInput,
41
52
  type TelegramUpdateJournalOperatorDispositionResult,
53
+ type TelegramUpdateJournalPendingAbandonmentInput,
54
+ type TelegramUpdateJournalPendingAbandonmentResult,
55
+ type TelegramUpdateJournalPendingRetentionEvidence,
56
+ type TelegramUpdateJournalEntry,
57
+ type TelegramUpdateJournalRoutingInput,
58
+ type TelegramRoutingInputJournal,
42
59
  type TelegramUpdateJournalQueueDiscardResult,
43
60
  type TelegramUpdateJournalQueueHandoffAcceptResult,
44
61
  type TelegramUpdateJournalQueueHandoffCancelResult,
@@ -134,30 +151,6 @@ function getTelegramReactionEmoji(
134
151
  return candidates.find((emoji) => emojis.has(emoji));
135
152
  }
136
153
 
137
- export function getTelegramQueueReactionDisposition(
138
- reactions: TelegramReactionType[],
139
- ): TelegramQueueReactionDisposition {
140
- const emojis = collectTelegramReactionEmojis(reactions);
141
- const suppressionEmoji = getTelegramReactionEmoji(
142
- emojis,
143
- TELEGRAM_REMOVAL_REACTION_EMOJIS,
144
- );
145
- const priorityEmoji = getTelegramReactionEmoji(
146
- emojis,
147
- TELEGRAM_PRIORITY_REACTION_EMOJIS,
148
- );
149
- if (suppressionEmoji && priorityEmoji) {
150
- return {
151
- kind: "priority-suppressed",
152
- priorityEmoji,
153
- suppressionEmoji,
154
- };
155
- }
156
- if (suppressionEmoji) return { kind: "suppressed", emoji: suppressionEmoji };
157
- if (priorityEmoji) return { kind: "priority", emoji: priorityEmoji };
158
- return { kind: "default" };
159
- }
160
-
161
154
  function getTelegramQueueReactionTransition(
162
155
  oldReactions: TelegramReactionType[],
163
156
  newReactions: TelegramReactionType[],
@@ -283,6 +276,8 @@ export interface TelegramGuestMessage {
283
276
  from?: TelegramUser;
284
277
  message_id?: number;
285
278
  text?: string;
279
+ /** Text sent together with a photo, document or other media. */
280
+ caption?: string;
286
281
  reply_to_message?: TelegramUpdateMessage;
287
282
  }
288
283
 
@@ -405,7 +400,7 @@ type TelegramForeignUpdateSettlementFailure =
405
400
  sourceUpdateId?: number;
406
401
  };
407
402
 
408
- export class TelegramForeignUpdateSettlementError extends Error {
403
+ class TelegramForeignUpdateSettlementError extends Error {
409
404
  readonly settlement: TelegramForeignUpdateSettlementFailure;
410
405
 
411
406
  constructor(
@@ -463,8 +458,8 @@ export interface TelegramUpdateFlow
463
458
  }
464
459
 
465
460
  export type TelegramUpdateAdmissionOutcome =
466
- | { kind: "complete" }
467
- | { kind: "deferred" }
461
+ | { kind: "complete"; expectedSource?: TelegramDeferredSourceEvidence }
462
+ | { kind: "deferred"; routingReview?: true }
468
463
  | {
469
464
  kind: "queued";
470
465
  queueKind: "prompt" | "control";
@@ -481,9 +476,61 @@ const TELEGRAM_UPDATE_ADMISSION_BINDING = Symbol(
481
476
  "telegram.update-admission.binding",
482
477
  );
483
478
 
479
+ export type TelegramDeferredUpdateAbandonmentAuthority = Pick<
480
+ TelegramUpdateJournalPendingAbandonmentInput, "operatorAuthorityId" | "isCurrent"
481
+ >;
482
+
483
+ export interface TelegramDeferredAbandonmentRecoveryRequest {
484
+ journalBindingKey: string;
485
+ afterUpdateId?: number;
486
+ isCurrent: () => boolean;
487
+ }
488
+
489
+ const TELEGRAM_HELD_SOURCE_INSPECTION_LIMIT = 20;
490
+
491
+ /** A generation-owned post-drain hint for the new-world restart; never execution, completion or deletion authority by itself. */
492
+ export interface TelegramHeldSourcePreparation<TContext> {
493
+ ctx: TContext;
494
+ journalBindingKey: string;
495
+ /** Exact prepared worker owner, context, key and authority; the consumer adds its own domain fences. */
496
+ isCurrent: () => boolean;
497
+ signal: AbortSignal;
498
+ }
499
+
500
+ export interface TelegramDeferredAbandonmentRecoveryPage {
501
+ sources: {
502
+ /** Detached original evidence, not proof of current eligibility or archive integrity. */
503
+ original: TelegramUpdateJournalEntry;
504
+ retry: (authority: TelegramDeferredUpdateAbandonmentAuthority) =>
505
+ TelegramUpdateJournalPendingAbandonmentResult | undefined;
506
+ }[];
507
+ nextAfterUpdateId?: number;
508
+ }
509
+
510
+ type TelegramHistoricalInputPredicate = (original: TelegramUpdateJournalEntry) => boolean;
511
+
512
+ /** Read-only exact-source observation by its live worker; not an acceptance or removal grant. */
513
+ export interface TelegramDeferredSourceEvidence extends TelegramUpdateJournalEntryDigest {
514
+ journalBindingKey: string;
515
+ /** Supplied only after caller-owned acceptance publication; requires a durable removal ACK. */
516
+ completionSha256?: string;
517
+ }
518
+
484
519
  interface TelegramUpdateAdmissionBinding {
485
520
  sourceUpdateId: number;
486
521
  report: (outcome: TelegramUpdateAdmissionOutcome) => void;
522
+ abandon?: (authority: TelegramDeferredUpdateAbandonmentAuthority) =>
523
+ TelegramUpdateJournalPendingAbandonmentResult | undefined;
524
+ armRoutingInput?: (operatorUserId: number, sourceUpdateIds: readonly number[]) => TelegramUpdateJournalRoutingInput | undefined;
525
+ acquireRouting?: (select?: boolean, sourceUpdateIds?: readonly number[]) => () => void;
526
+ inspectSource?: () => TelegramDeferredSourceEvidence | undefined;
527
+ supportsAbandonment?: (journalBindingKey: string) => boolean;
528
+ inspectAbandoning?: (request: TelegramDeferredAbandonmentRecoveryRequest) =>
529
+ TelegramDeferredAbandonmentRecoveryPage | undefined;
530
+ inspectHistorical?: (request: TelegramDeferredAbandonmentRecoveryRequest) =>
531
+ TelegramDeferredAbandonmentRecoveryPage | undefined;
532
+ isHistorical?: (matchesOriginal?: TelegramHistoricalInputPredicate) => boolean;
533
+ isHistoricalReviewHeld?: () => boolean;
487
534
  }
488
535
 
489
536
  export type TelegramQueueAdmissionReceiptLike = TelegramQueueAdmissionReceipt;
@@ -520,6 +567,7 @@ export function bindTelegramUpdateAdmissionSource<
520
567
  >(
521
568
  update: TUpdate,
522
569
  report: TelegramUpdateAdmissionBinding["report"],
570
+ controls?: Pick<TelegramUpdateAdmissionBinding, "abandon" | "armRoutingInput" | "acquireRouting" | "inspectSource" | "supportsAbandonment" | "inspectAbandoning" | "inspectHistorical" | "isHistorical" | "isHistoricalReviewHeld">,
523
571
  ): TUpdate {
524
572
  if (!Number.isSafeInteger(update.update_id) || update.update_id < 0) {
525
573
  throw new TelegramUpdateAdmissionOutcomeError(
@@ -529,6 +577,7 @@ export function bindTelegramUpdateAdmissionSource<
529
577
  const binding: TelegramUpdateAdmissionBinding = {
530
578
  sourceUpdateId: update.update_id,
531
579
  report,
580
+ ...controls,
532
581
  };
533
582
  const callbackQuery = update.callback_query
534
583
  ? bindTelegramUpdateAdmissionCarrier(
@@ -575,6 +624,26 @@ export function bindTelegramUpdateAdmissionSource<
575
624
  } as TUpdate;
576
625
  }
577
626
 
627
+ /**
628
+ * Conservative read-only census: any retained entry naming this Thread, in any nested Telegram object or state,
629
+ * is unresolved custody. It cannot see updates still in transit before journal append.
630
+ */
631
+ export function collectTelegramJournalThreadUpdateIds(
632
+ entries: readonly { updateId: number; update: unknown }[],
633
+ target: { chatId: number; threadId: number },
634
+ ): number[] {
635
+ const matches = (value: unknown, depth: number): boolean => {
636
+ if (depth > 8 || !value || typeof value !== "object") return false;
637
+ const record = value as Record<string, unknown>;
638
+ if (record.message_thread_id === target.threadId) {
639
+ const chat = record.chat as { id?: unknown } | undefined;
640
+ if (!chat || typeof chat !== "object" || chat.id === target.chatId) return true;
641
+ }
642
+ return Object.values(record).some(child => matches(child, depth + 1));
643
+ };
644
+ return entries.filter(entry => matches(entry.update, 0)).map(entry => entry.updateId).sort((left, right) => left - right);
645
+ }
646
+
578
647
  export function collectTelegramAdmissionSourceUpdateIds(
579
648
  values: readonly unknown[],
580
649
  ): number[] {
@@ -586,29 +655,132 @@ export function collectTelegramAdmissionSourceUpdateIds(
586
655
  return [...sourceUpdateIds].sort((left, right) => left - right);
587
656
  }
588
657
 
658
+ /** Observe the original journal entry, never the mutable routed carrier, through current worker authority. */
659
+ export function inspectTelegramDeferredSource(value: unknown): TelegramDeferredSourceEvidence | undefined {
660
+ const execution = getTelegramUpdateExecutionFence(value);
661
+ if (execution?.isCurrent() !== true) return undefined;
662
+ return getTelegramUpdateAdmissionBinding(value)?.inspectSource?.();
663
+ }
664
+
665
+ /** A route-owned pre-disposition acceptance publisher; wraps only this carrier, never the shared worker binding. */
666
+ export function bindTelegramUpdateCompletionAcceptance<TValue>(value: TValue,
667
+ publish: () => TelegramDeferredSourceEvidence): TValue {
668
+ const binding = getTelegramUpdateAdmissionBinding(value);
669
+ if (!binding || getTelegramUpdateExecutionFence(value)?.isCurrent() !== true) {
670
+ throw new TelegramUpdateAdmissionOutcomeError("Telegram completion acceptance requires a live source carrier.");
671
+ }
672
+ let retained: TelegramDeferredSourceEvidence | undefined;
673
+ let queued = false;
674
+ const scoped: TelegramUpdateAdmissionBinding = { ...binding, report(outcome) {
675
+ if (outcome.kind !== "complete") {
676
+ if (outcome.kind === "queued") queued = true;
677
+ binding.report(outcome); return;
678
+ }
679
+ assertTelegramUpdateExecutionCurrent(value);
680
+ // A command's trailing implicit completion cannot dispose receipt-owned or uncertain queued work.
681
+ if (queued) {
682
+ if (outcome.expectedSource) throw new TelegramUpdateAdmissionOutcomeError("Telegram completion acceptance cannot dispose a queued source.");
683
+ return;
684
+ }
685
+ if (!retained) {
686
+ const published = validateTelegramUpdateAdmissionOutcome({ kind: "complete", expectedSource: publish() },
687
+ binding.sourceUpdateId, new Set([binding.sourceUpdateId]));
688
+ if (published.kind !== "complete" || !published.expectedSource?.completionSha256) {
689
+ throw new TelegramUpdateAdmissionOutcomeError("Telegram completion acceptance lacks its scoped journal proof.");
690
+ }
691
+ retained = published.expectedSource;
692
+ }
693
+ assertTelegramUpdateExecutionCurrent(value);
694
+ if (outcome.expectedSource && (outcome.expectedSource.updateId !== retained.updateId ||
695
+ outcome.expectedSource.journalBindingKey !== retained.journalBindingKey || outcome.expectedSource.sourceSha256 !== retained.sourceSha256 ||
696
+ outcome.expectedSource.completionSha256 !== undefined && outcome.expectedSource.completionSha256 !== retained.completionSha256)) {
697
+ throw new TelegramUpdateAdmissionOutcomeError("Telegram completion acceptance conflicts with its reported source.");
698
+ }
699
+ binding.report({ kind: "complete", expectedSource: { ...retained } });
700
+ } };
701
+ return carryTelegramUpdateExecutionFence(value, bindTelegramUpdateAdmissionCarrier(value, scoped)!);
702
+ }
703
+
589
704
  /** Report source completion; true means reported, not a durable settlement acknowledgement. */
590
- export function reportTelegramUpdateCompleted(value: unknown): boolean {
705
+ export function reportTelegramUpdateCompleted(value: unknown, expectedSource?: TelegramDeferredSourceEvidence): boolean {
591
706
  const binding = getTelegramUpdateAdmissionBinding(value);
592
707
  if (!binding) return false;
593
708
  const execution = getTelegramUpdateExecutionFence(value);
594
709
  if (execution && !execution.isCurrent()) return false;
595
- binding.report({ kind: "complete" });
710
+ binding.report(validateTelegramUpdateAdmissionOutcome({ kind: "complete", ...(expectedSource !== undefined ? { expectedSource } : {}) },
711
+ binding.sourceUpdateId, new Set([binding.sourceUpdateId])));
596
712
  return true;
597
713
  }
598
714
 
599
715
  export function reportTelegramUpdateDeferred(value: unknown): boolean {
600
716
  const binding = getTelegramUpdateAdmissionBinding(value);
601
717
  if (!binding) return false;
718
+ const execution = getTelegramUpdateExecutionFence(value);
719
+ if (execution && !execution.isCurrent()) return false;
602
720
  binding.report({ kind: "deferred" });
603
721
  return true;
604
722
  }
605
723
 
724
+ /** Exact source-bound cancellation; undefined means no eligible attempt was made. */
725
+ export function abandonTelegramDeferredUpdate(
726
+ value: unknown,
727
+ authority: TelegramDeferredUpdateAbandonmentAuthority,
728
+ ): TelegramUpdateJournalPendingAbandonmentResult | undefined {
729
+ return getTelegramUpdateAdmissionBinding(value)?.abandon?.(authority);
730
+ }
731
+
732
+ /** Capability for chooser publication; the eventual action still requires exact deferred authority. */
733
+ export function supportsTelegramDeferredAbandonment(value: unknown, journalBindingKey: string): boolean {
734
+ return getTelegramUpdateAdmissionBinding(value)?.supportsAbandonment?.(journalBindingKey) === true;
735
+ }
736
+
737
+ /** Observe protected attempts through a current carrier; the UI still owns human authorization. */
738
+ export function inspectTelegramAbandoningUpdates(value: unknown, request: TelegramDeferredAbandonmentRecoveryRequest):
739
+ TelegramDeferredAbandonmentRecoveryPage | undefined {
740
+ return getTelegramUpdateAdmissionBinding(value)?.inspectAbandoning?.(request);
741
+ }
742
+
743
+ /** Historical inspection is not human confirmation or permission to stop delivery. */
744
+ export function inspectTelegramHistoricalInputs(value: unknown, request: TelegramDeferredAbandonmentRecoveryRequest):
745
+ TelegramDeferredAbandonmentRecoveryPage | undefined {
746
+ return getTelegramUpdateAdmissionBinding(value)?.inspectHistorical?.(request);
747
+ }
748
+
749
+ export function isTelegramHistoricalInput(value: unknown, matchesOriginal?: TelegramHistoricalInputPredicate): boolean {
750
+ return getTelegramUpdateAdmissionBinding(value)?.isHistorical?.(matchesOriginal) === true;
751
+ }
752
+
753
+ /** Routing's last-boundary hold when ownership changed after early classification. */
754
+ export function reportTelegramHistoricalRoutingReview(value: unknown): boolean {
755
+ if (!isTelegramHistoricalInput(value) || getTelegramUpdateExecutionFence(value)?.isCurrent() !== true) return false;
756
+ getTelegramUpdateAdmissionBinding(value)!.report({ kind: "deferred", routingReview: true });
757
+ return true;
758
+ }
759
+
760
+ /** Arm only a positively published chooser; the journal owns the immutable hour deadline. */
761
+ export function armTelegramRoutingInputs(values: readonly unknown[], operatorUserId: number): TelegramUpdateJournalRoutingInput | undefined {
762
+ const first = values[0], execution = getTelegramUpdateExecutionFence(first);
763
+ if (!execution || values.some(value => getTelegramUpdateExecutionFence(value)?.signal !== execution.signal)) return undefined;
764
+ for (const value of values) assertTelegramUpdateExecutionCurrent(value);
765
+ const ids = collectTelegramAdmissionSourceUpdateIds(values);
766
+ if (!ids.length) return undefined;
767
+ return getTelegramUpdateAdmissionBinding(first)?.armRoutingInput?.(operatorUserId, ids);
768
+ }
769
+
770
+ /** Reserve source execution; an actual choice must also freeze its durable TTL before any effect. */
771
+ export function acquireTelegramUpdateRouting(value: unknown, select = false, sourceUpdateIds?: readonly number[]): () => void {
772
+ assertTelegramUpdateExecutionCurrent(value);
773
+ return getTelegramUpdateAdmissionBinding(value)?.acquireRouting?.(select, sourceUpdateIds) ?? (() => {});
774
+ }
775
+
606
776
  export function reportTelegramQueueAdmission(
607
777
  values: readonly unknown[],
608
778
  receipts: readonly TelegramQueueAdmissionReceiptLike[],
609
779
  ): boolean {
610
780
  const bindings = new Map<number, TelegramUpdateAdmissionBinding>();
611
781
  for (const value of values) {
782
+ const execution = getTelegramUpdateExecutionFence(value);
783
+ if (execution && !execution.isCurrent()) return false;
612
784
  const binding = getTelegramUpdateAdmissionBinding(value);
613
785
  if (!binding) continue;
614
786
  const existing = bindings.get(binding.sourceUpdateId);
@@ -1122,10 +1294,12 @@ function getForeignTelegramCallbackOwnership(
1122
1294
  getTargetOwnership?: TelegramTargetOwnershipLookup;
1123
1295
  },
1124
1296
  ): TelegramMessageOwnershipView | undefined {
1125
- const messageOwnership = getForeignTelegramMessageOwnership(
1126
- getTelegramCallbackMessageTarget(query),
1127
- deps,
1128
- );
1297
+ const messageTarget = getTelegramCallbackMessageTarget(query);
1298
+ const currentInstanceId = deps.getCurrentInstanceId?.();
1299
+ const knownOwner = messageTarget && deps.getMessageOwnership?.(messageTarget.chatId, messageTarget.messageId);
1300
+ // A locally published chooser stays local when Restore moves its Thread to a follower.
1301
+ if (currentInstanceId && knownOwner?.instanceId === currentInstanceId) return undefined;
1302
+ const messageOwnership = getForeignTelegramMessageOwnership(messageTarget, deps);
1129
1303
  const targetOwnership = getForeignTelegramTargetOwnership(
1130
1304
  query.message ? getTelegramMessageTarget(query.message) : undefined,
1131
1305
  deps,
@@ -1621,7 +1795,10 @@ export async function executeTelegramUpdatePlan<
1621
1795
  ) {
1622
1796
  assertExecutionCurrent();
1623
1797
  await deps.handleUnboundTelegramTopicMessage(plan.message, deps.ctx);
1624
- assertExecutionCurrent();
1798
+ // A terminal historical hold intentionally suspends this exact carrier.
1799
+ // Do not turn its acknowledged deferral into a retryable execution failure.
1800
+ if (getTelegramUpdateExecutionFence(plan.message) !== deps.execution ||
1801
+ getTelegramUpdateAdmissionBinding(plan.message)?.isHistoricalReviewHeld?.() !== true) assertExecutionCurrent();
1625
1802
  return;
1626
1803
  }
1627
1804
  if (plan.kind === "edited-message") {
@@ -1640,9 +1817,9 @@ export async function executeTelegramUpdatePlan<
1640
1817
 
1641
1818
  // --- Durable update worker ---
1642
1819
 
1643
- export const TELEGRAM_UPDATE_RETRY_BASE_DELAY_MS = 1_000;
1644
- export const TELEGRAM_UPDATE_RETRY_MAX_DELAY_MS = 60_000;
1645
- export const TELEGRAM_UPDATE_WORKER_BATCH_SIZE = 64;
1820
+ const TELEGRAM_UPDATE_RETRY_BASE_DELAY_MS = 1_000;
1821
+ const TELEGRAM_UPDATE_RETRY_MAX_DELAY_MS = 60_000;
1822
+ const TELEGRAM_UPDATE_WORKER_BATCH_SIZE = 64;
1646
1823
 
1647
1824
  export type TelegramUpdateWorkerPhase =
1648
1825
  | "stopped"
@@ -1679,6 +1856,10 @@ export interface TelegramUpdateWorkerStateSnapshot {
1679
1856
  journalSerializedBytes: number;
1680
1857
  oldestAdmittedAtMs?: number;
1681
1858
  deferredClaimCount: number;
1859
+ /** Protected cancellation attempts, not proof of a committed discard. */
1860
+ abandoningClaimCount?: number;
1861
+ /** Historical sources held before routing, not evidence of prior non-delivery. */
1862
+ historicalClaimCount?: number;
1682
1863
  queuedClaimCount: number;
1683
1864
  foreignQueuedCount: number;
1684
1865
  foreignQueuedOwner?: TelegramUpdateJournalQueueOwner;
@@ -1712,6 +1893,7 @@ export interface TelegramUpdateWorkerJournalSnapshot {
1712
1893
  update: TelegramJournaledUpdate;
1713
1894
  readonly preApprovalExcluded?: boolean;
1714
1895
  admittedAtMs: number;
1896
+ routingInput?: TelegramUpdateJournalRoutingInput;
1715
1897
  state: "pending" | "retry-wait" | "queued" | "failed";
1716
1898
  inputClaim?: TelegramUpdateJournalInputClaim;
1717
1899
  queueKind?: "prompt" | "control";
@@ -1736,13 +1918,27 @@ export interface TelegramUpdateWorkerJournalSnapshot {
1736
1918
  serializedBytes: number;
1737
1919
  }
1738
1920
 
1921
+ export interface TelegramRoutingInputExpirySource {
1922
+ original: TelegramUpdateJournalEntry;
1923
+ journalBindingKey: string;
1924
+ isCurrent(): boolean;
1925
+ expire(): TelegramUpdateJournalPendingAbandonmentResult | undefined;
1926
+ }
1927
+
1739
1928
  export interface TelegramUpdateWorkerJournalPort {
1929
+ routingInputs?: TelegramRoutingInputJournal;
1740
1930
  read: () => TelegramUpdateWorkerJournalSnapshot;
1931
+ inspectPendingRetention?: (entry: TelegramUpdateJournalEntry) =>
1932
+ TelegramUpdateJournalPendingRetentionEvidence | undefined;
1933
+ abandonPending?: (input: TelegramUpdateJournalPendingAbandonmentInput) =>
1934
+ TelegramUpdateJournalPendingAbandonmentResult;
1741
1935
  /** Optional strict observation; it must not recover, repair or grant new receipt authority. */
1742
1936
  isQueueReceiptCurrent?: (
1743
1937
  receipt: TelegramQueueAdmissionReceiptLike,
1744
1938
  owner: TelegramUpdateJournalQueueOwner,
1745
1939
  ) => boolean;
1940
+ /** Strict full-group origin observation for prepared partial scopes; no writer or readiness acquisition. */
1941
+ inspectQueuedReceipt?: (expected: TelegramUpdateJournalQueuedCompletion) => TelegramUpdateJournalQueuedReceiptEvidence | undefined;
1746
1942
  markQueued: (receipt: {
1747
1943
  queueKind: "prompt" | "control";
1748
1944
  receiptId: string;
@@ -1763,6 +1959,11 @@ export interface TelegramUpdateWorkerJournalPort {
1763
1959
  ) => {
1764
1960
  removedUpdateIds: readonly number[];
1765
1961
  };
1962
+ completeQueuedExact?: (receipts: readonly {
1963
+ queueKind: "prompt" | "control"; receiptId: string; sourceUpdateIds: readonly number[]; queueOwner: TelegramUpdateJournalQueueOwner;
1964
+ }[], completions: readonly TelegramUpdateJournalSourceCompletion[]) => {
1965
+ removedUpdateIds: readonly number[]; sourceCompletions?: readonly TelegramUpdateJournalSourceCompletion[];
1966
+ };
1766
1967
  markExecutionFailure: (input: {
1767
1968
  updateId: number;
1768
1969
  expectedAttemptCount: number;
@@ -1778,6 +1979,32 @@ export interface TelegramUpdateWorkerJournalPort {
1778
1979
  removeCompleted: (updateIds: readonly number[]) => {
1779
1980
  removedUpdateIds: readonly number[];
1780
1981
  };
1982
+ /** No ID-only fallback is allowed for a guarded completion report. */
1983
+ removeCompletedExact?: (updateIds: readonly number[], expectedSources: readonly TelegramUpdateJournalEntryDigest[],
1984
+ completions?: readonly TelegramUpdateJournalSourceCompletion[]) => {
1985
+ removedUpdateIds: readonly number[];
1986
+ sourceCompletions?: readonly TelegramUpdateJournalSourceCompletion[];
1987
+ };
1988
+ inspectSourceCompletion?: (expected: TelegramUpdateJournalSourceCompletion) => TelegramUpdateJournalSourceCompletion | undefined;
1989
+ }
1990
+
1991
+ export type TelegramQueueSourceCompletion = TelegramDeferredSourceEvidence & { completionSha256: string };
1992
+
1993
+ function normalizeTelegramQueueSourceCompletions(value: unknown, sourceUpdateIds: ReadonlySet<number>, journalBindingKey: string, full = true): TelegramQueueSourceCompletion[] {
1994
+ if (!Array.isArray(value) || (full ? value.length !== sourceUpdateIds.size : value.length > sourceUpdateIds.size) || value.length === 0) throw new Error("Telegram scoped queue completion requires full source coverage.");
1995
+ const seen = new Set<number>(), scopes = new Set<string>();
1996
+ return value.map(source => {
1997
+ if (!Number.isSafeInteger(source?.updateId) || !sourceUpdateIds.has(source.updateId) || seen.has(source.updateId)) {
1998
+ throw new Error("Telegram scoped queue completion has invalid source membership.");
1999
+ }
2000
+ const normalized = validateTelegramUpdateAdmissionOutcome({ kind: "complete", expectedSource: source }, source.updateId, sourceUpdateIds);
2001
+ if (normalized.kind !== "complete" || !normalized.expectedSource?.completionSha256 ||
2002
+ normalized.expectedSource.journalBindingKey !== journalBindingKey || scopes.has(normalized.expectedSource.completionSha256)) {
2003
+ throw new Error("Telegram scoped queue completion has invalid source scope.");
2004
+ }
2005
+ seen.add(source.updateId); scopes.add(normalized.expectedSource.completionSha256);
2006
+ return { ...normalized.expectedSource, completionSha256: normalized.expectedSource.completionSha256 };
2007
+ }).sort((a, b) => a.updateId - b.updateId);
1781
2008
  }
1782
2009
 
1783
2010
  export interface TelegramUpdateRetryPolicy {
@@ -1806,12 +2033,19 @@ export interface TelegramUpdateWorkerRuntimeDeps<TContext> {
1806
2033
  hasAuthority: (ctx: TContext) => boolean;
1807
2034
  getJournalBindingKey?: () => string | undefined;
1808
2035
  getRecipientBindingKey?: () => string | undefined;
2036
+ /** True selects legacy historical review/spending; retain protects unsupported originals without disposition authority. */
2037
+ shouldReviewHistoricalInput?: (entry: TelegramUpdateJournalEntry, ctx: TContext, signal: AbortSignal) => boolean | "retain" | Promise<boolean | "retain">;
2038
+ /** New-world restart: spend previous-process routing inputs (classified or armed) without delivery, copy or completion observers. Interrupted private abandonment keeps its own exact recovery. */
2039
+ spendHistoricalInput?: boolean;
2040
+ /** Holds protected live or retry sources before execution; never cancels or disposes them. */
2041
+ shouldHoldPendingInput?: (entry: TelegramUpdateJournalEntry, ctx: TContext, signal: AbortSignal) => boolean | Promise<boolean>;
1809
2042
  getQueueOwnerIdentity?: (
1810
2043
  ctx: TContext,
1811
2044
  ) => TelegramUpdateJournalQueueOwnerIdentity;
1812
2045
  isContextCurrent?: (ctx: TContext) => boolean;
1813
2046
  createAbortController?: () => AbortController;
1814
2047
  getNowMs?: () => number;
2048
+ expireRoutingInput?: (source: TelegramRoutingInputExpirySource, ctx: TContext, signal: AbortSignal) => Promise<void> | void;
1815
2049
  retryPolicy?: Partial<TelegramUpdateRetryPolicy>;
1816
2050
  classifyExecutionFailure?: (
1817
2051
  error: unknown,
@@ -1822,11 +2056,18 @@ export interface TelegramUpdateWorkerRuntimeDeps<TContext> {
1822
2056
  batchSize?: number;
1823
2057
  yieldToEventLoop?: () => Promise<void>;
1824
2058
  onStateChange?: (state: TelegramUpdateWorkerStateSnapshot) => void;
2059
+ /** One nonblocking hint after a quiescent validated startup projection, never execution or deletion authority. */
2060
+ onHeldSourcesPrepared?: (input: TelegramHeldSourcePreparation<TContext>) => Promise<void> | void;
2061
+ /** Prepared readiness barrier after durable queue admission; never authorizes replay or source removal. */
2062
+ beforeQueueReceiptPublished?: (receipt: TelegramQueueAdmissionReceiptLike, queueOwner: TelegramUpdateJournalQueueOwner,
2063
+ ctx: TContext, isCurrent: () => boolean) => Promise<void | readonly TelegramQueueSourceCompletion[]> | void | readonly TelegramQueueSourceCompletion[];
2064
+ /** Post-ACK hint only; it must not request Pi dispatch or inherit a Workspace admission lease. */
2065
+ onQueueReceiptCompleted?: (receipt: TelegramQueueAdmissionReceiptLike, ctx: TContext) => void;
1825
2066
  onQueueReceiptCommitted?: (
1826
2067
  receipt: TelegramQueueAdmissionReceiptLike,
1827
2068
  ctx: TContext,
1828
2069
  ) => void;
1829
- onUpdateCompleted?: (updateId: number, ctx: TContext) => void;
2070
+ onUpdateCompleted?: (updateId: number, ctx: TContext, journalBindingKey?: string) => void;
1830
2071
  recordRuntimeEvent?: (
1831
2072
  category: string,
1832
2073
  error: unknown,
@@ -1847,6 +2088,19 @@ export interface TelegramUpdateWorkerRuntime<TContext> {
1847
2088
  outcome: TelegramUpdateAdmissionOutcome;
1848
2089
  signal: AbortSignal;
1849
2090
  }) => void;
2091
+ armRoutingInput?: (input: { updateId: number; signal: AbortSignal; operatorUserId: number; sourceUpdateIds: readonly number[] }) => TelegramUpdateJournalRoutingInput | undefined;
2092
+ selectRoutingInput?: (input: { updateId: number; signal: AbortSignal; operatorUserId: number; sourceUpdateIds: readonly number[] }) => boolean;
2093
+ supportsDeferredAbandonment?: (input: { updateId: number; signal: AbortSignal; journalBindingKey: string }) => boolean;
2094
+ inspectAbandoning?: (input: TelegramDeferredAbandonmentRecoveryRequest & { signal: AbortSignal }) =>
2095
+ TelegramDeferredAbandonmentRecoveryPage | undefined;
2096
+ inspectHistorical?: (input: TelegramDeferredAbandonmentRecoveryRequest & { signal: AbortSignal }) =>
2097
+ TelegramDeferredAbandonmentRecoveryPage | undefined;
2098
+ inspectDeferredSource?: (input: { updateId: number; signal: AbortSignal }) => TelegramDeferredSourceEvidence | undefined;
2099
+ isHistoricalSource?: (input: { updateId: number; signal: AbortSignal; matchesOriginal?: TelegramHistoricalInputPredicate }) => boolean;
2100
+ abandonDeferred?: (input: TelegramDeferredUpdateAbandonmentAuthority & {
2101
+ updateId: number;
2102
+ signal: AbortSignal;
2103
+ }) => TelegramUpdateJournalPendingAbandonmentResult | undefined;
1850
2104
  settleCustodied: (input: {
1851
2105
  updateId: number;
1852
2106
  result: TelegramCustodiedExecutionResult;
@@ -1858,17 +2112,22 @@ export interface TelegramUpdateWorkerRuntime<TContext> {
1858
2112
  getQueueReceiptOwner: (
1859
2113
  receipt: TelegramQueueAdmissionReceiptLike,
1860
2114
  ) => TelegramUpdateJournalQueueOwner | undefined;
2115
+ /** Completion-only owner observation; issued attempts are never dispatch readiness. */
2116
+ getQueueReceiptSettlementOwner?: (receipt: TelegramQueueAdmissionReceiptLike, ctx: TContext,
2117
+ reason: TelegramQueueReceiptCompletionReason) => TelegramUpdateJournalQueueOwner | undefined;
1861
2118
  completeQueueReceipts: (input: {
1862
2119
  receipts: readonly TelegramQueueAdmissionReceiptLike[];
1863
2120
  ctx: TContext;
1864
2121
  reason: TelegramQueueReceiptCompletionReason;
2122
+ /** Prepared immutable scopes for every source; the exact receipt owner still authorizes disposal. */
2123
+ sourceCompletions?: readonly TelegramQueueSourceCompletion[];
1865
2124
  }) => boolean;
1866
2125
  stop: () => Promise<void>;
1867
2126
  waitForDrain: () => Promise<void>;
1868
2127
  getState: () => TelegramUpdateWorkerStateSnapshot;
1869
2128
  }
1870
2129
 
1871
- export class TelegramUpdateAdmissionOutcomeError extends Error {
2130
+ class TelegramUpdateAdmissionOutcomeError extends Error {
1872
2131
  constructor(message: string) {
1873
2132
  super(message);
1874
2133
  this.name = "TelegramUpdateAdmissionOutcomeError";
@@ -1880,9 +2139,11 @@ interface TelegramUpdateWorkerOwner<TContext> {
1880
2139
  ctx: TContext;
1881
2140
  controller: AbortController;
1882
2141
  queueOwnerIdentity: TelegramUpdateJournalQueueOwnerIdentity;
2142
+ startupUpdateIds?: ReadonlySet<number>;
2143
+ heldSourcesPreparedIssued?: true;
1883
2144
  }
1884
2145
 
1885
- type TelegramUpdateWorkerClaim = "deferred" | "queued";
2146
+ type TelegramUpdateWorkerClaim = "deferred" | "queued" | "abandoning" | "historical" | "retained";
1886
2147
  type TelegramUpdateWorkerDrainResult = "idle" | "blocked" | "aborted";
1887
2148
  type TelegramUpdateWorkerExecutionSettlement =
1888
2149
  | { ok: true; outcome: TelegramUpdateAdmissionOutcome; custodied?: false }
@@ -1899,16 +2160,6 @@ function getTelegramUpdateWorkerStateSnapshot(
1899
2160
  return { ...state };
1900
2161
  }
1901
2162
 
1902
- function isTelegramUpdateAdmissionRecord(
1903
- value: unknown,
1904
- ): value is Record<string, unknown> {
1905
- return !!value && typeof value === "object" && !Array.isArray(value);
1906
- }
1907
-
1908
- function isTelegramUpdateAdmissionString(value: unknown): value is string {
1909
- return typeof value === "string" && value.length > 0;
1910
- }
1911
-
1912
2163
  function validateTelegramUpdateAdmissionOutcome(
1913
2164
  value: unknown,
1914
2165
  currentUpdateId: number,
@@ -1919,8 +2170,23 @@ function validateTelegramUpdateAdmissionOutcome(
1919
2170
  `Telegram update ${currentUpdateId} returned no admission outcome.`,
1920
2171
  );
1921
2172
  }
1922
- if (value.kind === "complete") return { kind: "complete" };
1923
- if (value.kind === "deferred") return { kind: "deferred" };
2173
+ if (value.kind === "complete") {
2174
+ if (value.expectedSource === undefined) return { kind: "complete" };
2175
+ const source = value.expectedSource;
2176
+ if (!isTelegramUpdateAdmissionRecord(source) || Object.keys(source).some(key => !["journalBindingKey", "updateId", "sourceSha256", "completionSha256"].includes(key)) ||
2177
+ source.updateId !== currentUpdateId || !isTelegramUpdateAdmissionString(source.journalBindingKey) ||
2178
+ typeof source.sourceSha256 !== "string" || !/^[a-f0-9]{64}$/u.test(source.sourceSha256) ||
2179
+ source.completionSha256 !== undefined && (typeof source.completionSha256 !== "string" || !/^[a-f0-9]{64}$/u.test(source.completionSha256))) {
2180
+ throw new TelegramUpdateAdmissionOutcomeError(`Telegram update ${currentUpdateId} returned an invalid completion source.`);
2181
+ }
2182
+ return { kind: "complete", expectedSource: { journalBindingKey: source.journalBindingKey,
2183
+ updateId: currentUpdateId, sourceSha256: source.sourceSha256,
2184
+ ...(source.completionSha256 !== undefined ? { completionSha256: source.completionSha256 } : {}) } };
2185
+ }
2186
+ if (value.kind === "deferred") {
2187
+ if (value.routingReview !== undefined && value.routingReview !== true) throw new TelegramUpdateAdmissionOutcomeError("Invalid historical review outcome.");
2188
+ return { kind: "deferred", ...(value.routingReview === true ? { routingReview: true } : {}) };
2189
+ }
1924
2190
  if (value.kind === "queued") {
1925
2191
  if (
1926
2192
  (value.queueKind !== "prompt" && value.queueKind !== "control") ||
@@ -2072,12 +2338,23 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
2072
2338
  );
2073
2339
  }
2074
2340
  const getNowMs = deps.getNowMs ?? Date.now;
2341
+ const onHeldSourcesPrepared = deps.onHeldSourcesPrepared;
2342
+ const spendHistorical = deps.spendHistoricalInput === true;
2343
+ const routingJournal = deps.journal.routingInputs;
2344
+ const armRouting = routingJournal?.arm.bind(routingJournal);
2345
+ const selectRouting = routingJournal?.select.bind(routingJournal);
2346
+ const expireRouting = routingJournal?.expire.bind(routingJournal);
2347
+ const expireRoutingObserver = deps.expireRoutingInput;
2075
2348
  const batchSize = deps.batchSize ?? TELEGRAM_UPDATE_WORKER_BATCH_SIZE;
2076
2349
  if (!Number.isSafeInteger(batchSize) || batchSize <= 0) {
2077
2350
  throw new Error("Telegram update worker batch size must be positive.");
2078
2351
  }
2079
2352
  const yieldToEventLoop = deps.yieldToEventLoop ??
2080
2353
  (() => new Promise<void>((resolve) => setTimeout(resolve, 0)));
2354
+ const completeQueuedExact = deps.journal.completeQueuedExact?.bind(deps.journal);
2355
+ const completeExact = deps.journal.removeCompletedExact?.bind(deps.journal);
2356
+ const inspectCompletion = deps.journal.inspectSourceCompletion?.bind(deps.journal);
2357
+ const observeQueuedCompletion = deps.onQueueReceiptCompleted;
2081
2358
  const fallbackQueueOwnerInstanceId = `worker-${randomUUID()}`;
2082
2359
  const fallbackQueueOwnerProcessId = process.pid > 0 ? process.pid : 1;
2083
2360
  const createAbortController =
@@ -2100,6 +2377,12 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
2100
2377
  sessionGeneration: generation,
2101
2378
  },
2102
2379
  );
2380
+ const isQueueOwnerIdentityCurrent = (expected: TelegramUpdateWorkerOwner<TContext>): boolean => {
2381
+ if (!deps.getQueueOwnerIdentity) return true;
2382
+ const live = deps.getQueueOwnerIdentity(expected.ctx), captured = expected.queueOwnerIdentity;
2383
+ return !!live && live.instanceId === captured.instanceId && live.processId === captured.processId &&
2384
+ live.processBirthId === captured.processBirthId && live.sessionGeneration === captured.sessionGeneration;
2385
+ };
2103
2386
  const state: TelegramUpdateWorkerStateSnapshot = {
2104
2387
  phase: "stopped",
2105
2388
  generation: 0,
@@ -2113,6 +2396,11 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
2113
2396
  unsettledExecutionCount: 0,
2114
2397
  };
2115
2398
  const claims = new Map<number, TelegramUpdateWorkerClaim>();
2399
+ const deferredSources = new Map<number, {
2400
+ entry: TelegramUpdateWorkerJournalSnapshot["entries"][number];
2401
+ journalBindingKey: string;
2402
+ historical?: true;
2403
+ }>();
2116
2404
  const unsettledExecutionsByUpdateId = new Map<
2117
2405
  number,
2118
2406
  Set<Promise<TelegramUpdateWorkerExecutionSettlement>>
@@ -2122,8 +2410,15 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
2122
2410
  {
2123
2411
  receipt: TelegramQueueAdmissionReceiptLike;
2124
2412
  queueOwner: TelegramUpdateJournalQueueOwner;
2413
+ sourceCompletions?: TelegramQueueSourceCompletion[];
2125
2414
  }
2126
2415
  >();
2416
+ const requiredScopedQueueReceipts = new Set<string>();
2417
+ const scopedQueueCompletionAttempts = new Map<string, {
2418
+ owner: TelegramUpdateWorkerOwner<TContext>; journalBindingKey: string; reason: TelegramQueueReceiptCompletionReason;
2419
+ receipts: { queueKind: "prompt" | "control"; receiptId: string; sourceUpdateIds: readonly number[]; queueOwner: TelegramUpdateJournalQueueOwner }[];
2420
+ completions: TelegramUpdateJournalSourceCompletion[];
2421
+ }>();
2127
2422
  const unsettledExecutions =
2128
2423
  new Set<Promise<TelegramUpdateWorkerExecutionSettlement>>();
2129
2424
  let owner: TelegramUpdateWorkerOwner<TContext> | undefined;
@@ -2134,6 +2429,8 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
2134
2429
  let retryTimer: unknown;
2135
2430
  let retryTimerAtMs: number | undefined;
2136
2431
  let retryTimerToken: object | undefined;
2432
+ let routingTimer: unknown;
2433
+ let routingTimerToken: object | undefined;
2137
2434
  let launchDrain: () => void = () => {};
2138
2435
 
2139
2436
  const recordRuntimeEvent = (
@@ -2158,12 +2455,20 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
2158
2455
  const updateClaimCounts = (): void => {
2159
2456
  let deferredClaimCount = 0;
2160
2457
  let queuedClaimCount = 0;
2458
+ let abandoningClaimCount = 0;
2459
+ let historicalClaimCount = 0;
2161
2460
  for (const claim of claims.values()) {
2162
2461
  if (claim === "queued") queuedClaimCount += 1;
2163
2462
  else deferredClaimCount += 1;
2463
+ if (claim === "abandoning") abandoningClaimCount += 1;
2464
+ if (claim === "historical" || claim === "retained") historicalClaimCount += 1;
2164
2465
  }
2165
2466
  state.deferredClaimCount = deferredClaimCount;
2166
2467
  state.queuedClaimCount = queuedClaimCount;
2468
+ if (abandoningClaimCount > 0) state.abandoningClaimCount = abandoningClaimCount;
2469
+ else delete state.abandoningClaimCount;
2470
+ if (historicalClaimCount > 0) state.historicalClaimCount = historicalClaimCount;
2471
+ else delete state.historicalClaimCount;
2167
2472
  };
2168
2473
 
2169
2474
  const releaseDeferredClaims = (updateIds: readonly number[]): void => {
@@ -2171,6 +2476,7 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
2171
2476
  for (const updateId of updateIds) {
2172
2477
  if (claims.get(updateId) !== "deferred") continue;
2173
2478
  claims.delete(updateId);
2479
+ deferredSources.delete(updateId);
2174
2480
  changed = true;
2175
2481
  }
2176
2482
  if (!changed) return;
@@ -2233,11 +2539,18 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
2233
2539
  : {}),
2234
2540
  });
2235
2541
 
2236
- const publishCommittedQueueReceipt = (
2542
+ const inspectQueueReceipt = deps.journal.isQueueReceiptCurrent?.bind(deps.journal);
2543
+ const inspectQueuedSources = deps.journal.inspectQueuedReceipt?.bind(deps.journal);
2544
+ const prepareQueueReceipt = deps.beforeQueueReceiptPublished;
2545
+ const pendingQueuePublications = new Map<string, { receipt: TelegramQueueAdmissionReceiptLike;
2546
+ queueOwner: TelegramUpdateJournalQueueOwner; owner: TelegramUpdateWorkerOwner<TContext>; task: Promise<void> }>();
2547
+ const publishCommittedQueueReceipt = async (
2237
2548
  receipt: TelegramQueueAdmissionReceiptLike,
2238
2549
  queueOwner: TelegramUpdateJournalQueueOwner,
2239
- ctx: TContext,
2240
- ): boolean => {
2550
+ expectedOwner: TelegramUpdateWorkerOwner<TContext>,
2551
+ ): Promise<boolean> => {
2552
+ const ctx = expectedOwner.ctx;
2553
+ let sourceCompletions: TelegramQueueSourceCompletion[] | undefined;
2241
2554
  const normalized = bindQueueReceiptToJournal(receipt);
2242
2555
  const existing = committedQueueReceipts.get(receipt.receiptId);
2243
2556
  if (existing) {
@@ -2257,29 +2570,86 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
2257
2570
  }
2258
2571
  return false;
2259
2572
  }
2260
- committedQueueReceipts.set(receipt.receiptId, {
2261
- receipt: normalized,
2262
- queueOwner: { ...queueOwner },
2263
- });
2264
- try {
2265
- deps.onQueueReceiptCommitted?.(normalized, ctx);
2266
- } catch (error) {
2267
- recordRuntimeEvent(error, {
2268
- phase: "queue-receipt-observer",
2269
- receiptId: normalized.receiptId,
2270
- });
2573
+ if (prepareQueueReceipt) {
2574
+ const pending = pendingQueuePublications.get(normalized.receiptId);
2575
+ if (pending) {
2576
+ if (pending.owner !== expectedOwner || !areTelegramQueueAdmissionReceiptsEqual(pending.receipt, normalized) ||
2577
+ !areTelegramUpdateJournalQueueOwnersEqual(pending.queueOwner, queueOwner)) {
2578
+ throw new TelegramUpdateAdmissionOutcomeError("Telegram queue publication has conflicting in-flight authority.");
2579
+ }
2580
+ await pending.task;
2581
+ return false;
2582
+ }
2583
+ const journalBindingKey = deps.getJournalBindingKey?.();
2584
+ const current = (): boolean => owner === expectedOwner && !expectedOwner.controller.signal.aborted && deps.hasAuthority(ctx) && deps.isContextCurrent?.(ctx) !== false &&
2585
+ isQueueOwnerIdentityCurrent(expectedOwner) && deps.getJournalBindingKey?.() === journalBindingKey;
2586
+ if (!journalBindingKey || !current() || !inspectQueueReceipt ||
2587
+ inspectQueueReceipt(normalizeQueueReceipt(normalized), { ...queueOwner }) !== true || !current()) {
2588
+ throw new TelegramUpdateAdmissionOutcomeError("Telegram queue publication requires exact current receipt inspection.");
2589
+ }
2590
+ const task = Promise.resolve().then(async () => {
2591
+ if (!current()) throw new TelegramUpdateAdmissionOutcomeError("Telegram queue publication authority ended before acceptance.");
2592
+ const prepared = await prepareQueueReceipt(normalizeQueueReceipt(normalized), { ...queueOwner }, ctx, current);
2593
+ if (prepared !== undefined) {
2594
+ sourceCompletions = normalizeTelegramQueueSourceCompletions(prepared, new Set(normalized.sourceUpdateIds), journalBindingKey, false);
2595
+ if (!completeQueuedExact || !inspectCompletion) throw new Error("Telegram scoped queue terminal capabilities are unavailable.");
2596
+ if (sourceCompletions.length !== normalized.sourceUpdateIds.length) {
2597
+ if (!current() || !inspectQueuedSources) throw new Error("Telegram partial queue scopes require strict current whole-receipt origin inspection.");
2598
+ const expected = { queueKind: normalized.queueKind, receiptId: normalized.receiptId,
2599
+ sourceUpdateIds: [...normalized.sourceUpdateIds], queueOwner: { ...queueOwner } };
2600
+ const proof = inspectQueuedSources(structuredClone(expected));
2601
+ if (!current() || !proof || !isDeepStrictEqual(proof.receipt, expected) ||
2602
+ !Array.isArray(proof.sources) || proof.sources.length !== expected.sourceUpdateIds.length ||
2603
+ proof.sources.some((source, index) => source.updateId !== expected.sourceUpdateIds[index] || typeof source.sourceSha256 !== "string" || !/^[a-f0-9]{64}$/u.test(source.sourceSha256)) ||
2604
+ sourceCompletions.some(scope => proof.sources.find(source => source.updateId === scope.updateId)?.sourceSha256 !== scope.sourceSha256)) {
2605
+ throw new Error("Telegram partial queue scope origin was not confirmed under whole-receipt authority.");
2606
+ }
2607
+ }
2608
+ for (const { journalBindingKey: _binding, ...completion } of sourceCompletions) {
2609
+ if (!current() || inspectCompletion({ ...completion }) !== undefined || !current()) {
2610
+ throw new Error("Telegram queued source completion authority is contradictory.");
2611
+ }
2612
+ }
2613
+ }
2614
+ if (!current() || inspectQueueReceipt(normalizeQueueReceipt(normalized), { ...queueOwner }) !== true || !current()) {
2615
+ throw new TelegramUpdateAdmissionOutcomeError("Telegram queue publication authority changed after acceptance.");
2616
+ }
2617
+ publishReady();
2618
+ }).finally(() => { if (pendingQueuePublications.get(normalized.receiptId)?.task === task) pendingQueuePublications.delete(normalized.receiptId); });
2619
+ pendingQueuePublications.set(normalized.receiptId, { receipt: normalizeQueueReceipt(normalized), queueOwner: { ...queueOwner }, owner: expectedOwner, task });
2620
+ await task;
2621
+ return true;
2271
2622
  }
2623
+ publishReady();
2272
2624
  return true;
2625
+
2626
+ function publishReady(): void {
2627
+ committedQueueReceipts.set(receipt.receiptId, {
2628
+ receipt: normalized,
2629
+ queueOwner: { ...queueOwner },
2630
+ ...(sourceCompletions ? { sourceCompletions: sourceCompletions.map(value => ({ ...value })) } : {}),
2631
+ });
2632
+ if (sourceCompletions) requiredScopedQueueReceipts.add(receipt.receiptId);
2633
+ try {
2634
+ deps.onQueueReceiptCommitted?.(normalizeQueueReceipt(normalized), ctx);
2635
+ } catch (error) {
2636
+ recordRuntimeEvent(error, {
2637
+ phase: "queue-receipt-observer",
2638
+ receiptId: normalized.receiptId,
2639
+ });
2640
+ }
2641
+ }
2273
2642
  };
2274
2643
 
2275
2644
  const getCurrentQueueReceipt = (receipt: TelegramQueueAdmissionReceiptLike) => {
2645
+ if (scopedQueueCompletionAttempts.has(receipt.receiptId)) return undefined;
2276
2646
  const committed = committedQueueReceipts.get(receipt.receiptId);
2277
2647
  if (!committed || !areTelegramQueueAdmissionReceiptsEqual(
2278
2648
  committed.receipt, normalizeQueueReceipt(receipt),
2279
2649
  )) return undefined;
2280
2650
  try {
2281
- if (deps.journal.isQueueReceiptCurrent &&
2282
- deps.journal.isQueueReceiptCurrent(committed.receipt, committed.queueOwner) !== true) {
2651
+ if (inspectQueueReceipt &&
2652
+ inspectQueueReceipt(normalizeQueueReceipt(committed.receipt), { ...committed.queueOwner }) !== true) {
2283
2653
  return undefined;
2284
2654
  }
2285
2655
  } catch (error) {
@@ -2327,10 +2697,10 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
2327
2697
  }, Math.max(0, nextRetryAtMs - getNowMs()));
2328
2698
  };
2329
2699
 
2330
- const refreshJournalState = (
2700
+ const refreshJournalState = async (
2331
2701
  snapshot: TelegramUpdateWorkerJournalSnapshot,
2332
2702
  expectedOwner: TelegramUpdateWorkerOwner<TContext>,
2333
- ): number | undefined => {
2703
+ ): Promise<number | undefined> => {
2334
2704
  // Validate the whole snapshot before reconstructing any queue authority.
2335
2705
  if ((snapshot.version !== TELEGRAM_UPDATE_JOURNAL_VERSION &&
2336
2706
  snapshot.version !== TELEGRAM_UPDATE_JOURNAL_EXCLUSION_VERSION &&
@@ -2367,7 +2737,7 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
2367
2737
  oldestAdmittedAtMs === undefined
2368
2738
  ? entry.admittedAtMs
2369
2739
  : Math.min(oldestAdmittedAtMs, entry.admittedAtMs);
2370
- if (entry.state === "retry-wait" && entry.nextRetryAtMs !== undefined) {
2740
+ if (entry.state === "retry-wait" && entry.nextRetryAtMs !== undefined && !claims.has(entry.updateId)) {
2371
2741
  scheduledRetryAtMs =
2372
2742
  scheduledRetryAtMs === undefined
2373
2743
  ? entry.nextRetryAtMs
@@ -2449,21 +2819,25 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
2449
2819
  }
2450
2820
  for (const [updateId, claim] of claims) {
2451
2821
  if (
2452
- !availableUpdateIds.has(updateId) ||
2822
+ (claim !== "abandoning" && !availableUpdateIds.has(updateId)) ||
2453
2823
  (claim === "queued" && !locallyOwnedQueuedUpdateIds.has(updateId))
2454
2824
  ) {
2455
2825
  claims.delete(updateId);
2456
2826
  }
2457
2827
  }
2828
+ for (const updateId of deferredSources.keys()) {
2829
+ const claim = claims.get(updateId);
2830
+ if (claim !== "deferred" && claim !== "abandoning" && claim !== "historical" && claim !== "retained") deferredSources.delete(updateId);
2831
+ }
2458
2832
  for (const [receiptId, receipt] of queuedReceiptEntries) {
2459
- publishCommittedQueueReceipt(
2833
+ await publishCommittedQueueReceipt(
2460
2834
  {
2461
2835
  queueKind: receipt.queueKind,
2462
2836
  receiptId,
2463
2837
  sourceUpdateIds: receipt.sourceUpdateIds,
2464
2838
  },
2465
2839
  receipt.queueOwner,
2466
- expectedOwner.ctx,
2840
+ expectedOwner,
2467
2841
  );
2468
2842
  }
2469
2843
  for (const [receiptId] of committedQueueReceipts) {
@@ -2598,10 +2972,8 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
2598
2972
  expectedOwner: TelegramUpdateWorkerOwner<TContext>,
2599
2973
  currentUpdateId: number,
2600
2974
  outcome: TelegramQueuedUpdateAdmissionOutcome,
2601
- ):
2602
- | "committed"
2603
- | "duplicate"
2604
- | Exclude<TelegramUpdateWorkerDrainResult, "idle"> => {
2975
+ ): "committed" | "duplicate" | Exclude<TelegramUpdateWorkerDrainResult, "idle"> |
2976
+ Promise<"committed" | "duplicate" | Exclude<TelegramUpdateWorkerDrainResult, "idle">> => {
2605
2977
  const normalized = normalizeQueueReceipt(outcome);
2606
2978
  const existing = committedQueueReceipts.get(normalized.receiptId);
2607
2979
  if (existing) {
@@ -2674,21 +3046,8 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
2674
3046
  for (const sourceUpdateId of normalized.sourceUpdateIds) {
2675
3047
  claims.set(sourceUpdateId, "queued");
2676
3048
  }
2677
- try {
2678
- publishCommittedQueueReceipt(
2679
- normalized,
2680
- queueOwner,
2681
- expectedOwner.ctx,
2682
- );
2683
- } catch (error) {
2684
- return blockWithFailure(
2685
- "invalid-outcome",
2686
- "queue-receipt-publish",
2687
- error,
2688
- currentUpdateId,
2689
- );
2690
- }
2691
- return "committed";
3049
+ return publishCommittedQueueReceipt(normalized, queueOwner, expectedOwner).then(() => "committed" as const,
3050
+ error => blockWithFailure("invalid-outcome", "queue-receipt-publish", error, currentUpdateId));
2692
3051
  };
2693
3052
 
2694
3053
  const persistExecutionFailure = (
@@ -2774,16 +3133,45 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
2774
3133
 
2775
3134
  const commitCompletedBatch = (
2776
3135
  expectedOwner: TelegramUpdateWorkerOwner<TContext>,
2777
- updateIds: readonly number[],
3136
+ completions: readonly { updateId: number; expectedSource?: TelegramDeferredSourceEvidence; spent?: true }[],
2778
3137
  ): Exclude<TelegramUpdateWorkerDrainResult, "idle"> | undefined => {
3138
+ const updateIds = completions.map(value => value.updateId);
2779
3139
  if (updateIds.length === 0) return undefined;
2780
3140
  const commitAuthority = checkAuthority(
2781
3141
  expectedOwner,
2782
3142
  updateIds[updateIds.length - 1],
2783
3143
  );
2784
3144
  if (commitAuthority) return commitAuthority;
3145
+ const journalBindingKey = deps.getJournalBindingKey?.();
2785
3146
  try {
2786
- const removed = deps.journal.removeCompleted(updateIds);
3147
+ const expectedSources = completions.flatMap(value => value.expectedSource ? [value.expectedSource] : []);
3148
+ if (expectedSources.some(value => value.journalBindingKey !== journalBindingKey) ||
3149
+ (expectedSources.length > 0 && (deps.isContextCurrent?.(expectedOwner.ctx) === false || !isQueueOwnerIdentityCurrent(expectedOwner)))) {
3150
+ throw new Error("Telegram guarded completion source authority changed.");
3151
+ }
3152
+ const scoped = expectedSources.flatMap(({ updateId, sourceSha256, completionSha256 }) => completionSha256 !== undefined
3153
+ ? [{ updateId, sourceSha256, completionSha256 }] : []).sort((a, b) => a.updateId - b.updateId);
3154
+ if (scoped.length > 0 && !inspectCompletion) throw new Error("Telegram source completion inspection is unavailable.");
3155
+ let removed: { removedUpdateIds: readonly number[]; sourceCompletions?: readonly TelegramUpdateJournalSourceCompletion[] };
3156
+ if (expectedSources.length > 0) {
3157
+ if (!completeExact) throw new Error("Telegram exact source completion is unavailable.");
3158
+ removed = completeExact(updateIds, expectedSources.map(({ updateId, sourceSha256 }) => ({ updateId, sourceSha256 })),
3159
+ scoped.length > 0 ? scoped.map(completion => ({ ...completion })) : undefined);
3160
+ } else removed = deps.journal.removeCompleted(updateIds);
3161
+ if (scoped.length > 0) {
3162
+ const afterCommit = checkAuthority(expectedOwner, updateIds.at(-1));
3163
+ if (afterCommit) return afterCommit;
3164
+ if (deps.isContextCurrent?.(expectedOwner.ctx) === false || !isQueueOwnerIdentityCurrent(expectedOwner) ||
3165
+ deps.getJournalBindingKey?.() !== journalBindingKey) throw new Error("Telegram source completion ACK authority changed.");
3166
+ if (!isDeepStrictEqual(removed.sourceCompletions, scoped)) throw new Error("Telegram source completion ACK was not confirmed.");
3167
+ for (const completion of scoped) {
3168
+ if (!isDeepStrictEqual(inspectCompletion!({ ...completion }), completion)) throw new Error("Telegram source completion ACK was not retained.");
3169
+ const afterRead = checkAuthority(expectedOwner, completion.updateId);
3170
+ if (afterRead) return afterRead;
3171
+ if (deps.isContextCurrent?.(expectedOwner.ctx) === false || !isQueueOwnerIdentityCurrent(expectedOwner) ||
3172
+ deps.getJournalBindingKey?.() !== journalBindingKey) throw new Error("Telegram source completion ACK authority changed.");
3173
+ }
3174
+ }
2787
3175
  const removedIds = new Set(removed.removedUpdateIds);
2788
3176
  if (updateIds.some((updateId) => !removedIds.has(updateId))) {
2789
3177
  throw new Error(
@@ -2799,12 +3187,15 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
2799
3187
  );
2800
3188
  }
2801
3189
  const completedAtMs = getNowMs();
2802
- for (const updateId of updateIds) {
3190
+ for (const { updateId, spent } of completions) {
2803
3191
  claims.delete(updateId);
3192
+ deferredSources.delete(updateId);
3193
+ // Spending is disposition, not completion: no task, cleanup or session observer runs.
3194
+ if (spent) continue;
2804
3195
  state.lastCompletedUpdateId = updateId;
2805
3196
  state.lastCompletedAtMs = completedAtMs;
2806
3197
  try {
2807
- deps.onUpdateCompleted?.(updateId, expectedOwner.ctx);
3198
+ deps.onUpdateCompleted?.(updateId, expectedOwner.ctx, journalBindingKey);
2808
3199
  } catch (error) {
2809
3200
  recordRuntimeEvent(error, {
2810
3201
  phase: "update-completion-observer",
@@ -2815,6 +3206,82 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
2815
3206
  return undefined;
2816
3207
  };
2817
3208
 
3209
+ const clearRoutingTimer = (): void => {
3210
+ if (routingTimer !== undefined) cancelRetry(routingTimer);
3211
+ routingTimer = undefined; routingTimerToken = undefined;
3212
+ };
3213
+ const reconcileRoutingInputs = async (snapshot: TelegramUpdateWorkerJournalSnapshot, expectedOwner: TelegramUpdateWorkerOwner<TContext>): Promise<boolean> => {
3214
+ clearRoutingTimer();
3215
+ if (!isTelegramUpdateJournalLegacyFamilyVersion(snapshot.version) || !expireRouting || !expireRoutingObserver) return false;
3216
+ let nextAtMs: number | undefined, changed = false;
3217
+ const binding = deps.getJournalBindingKey?.(), snapshotIds = new Set(snapshot.entries.map(entry => entry.updateId));
3218
+ // Lost removal ACKs reconcile only the exact held original through the retained copy/tombstone.
3219
+ const candidates = [...snapshot.entries, ...[...deferredSources.values()]
3220
+ .filter(source => source.journalBindingKey === binding && !snapshotIds.has(source.entry.updateId))
3221
+ .map(source => source.entry)];
3222
+ for (const entry of candidates) {
3223
+ const lifetime = entry.routingInput;
3224
+ if (claims.get(entry.updateId) === "retained" || (expectedOwner.startupUpdateIds?.has(entry.updateId) &&
3225
+ !claims.has(entry.updateId) && deps.shouldReviewHistoricalInput)) continue;
3226
+ if (!lifetime || lifetime.phase !== "waiting" || entry.state !== "pending" || !binding || unsettledExecutionsByUpdateId.get(entry.updateId)?.size) continue;
3227
+ if (getNowMs() < lifetime.expiresAtMs) { nextAtMs = Math.min(nextAtMs ?? Infinity, lifetime.expiresAtMs); continue; }
3228
+ const existing = deferredSources.get(entry.updateId);
3229
+ if (existing && !isDeepStrictEqual(existing.entry, entry)) continue;
3230
+ const source = existing ?? { entry: structuredClone(entry), journalBindingKey: binding, historical: true as const };
3231
+ deferredSources.set(entry.updateId, source);
3232
+ if (!claims.has(entry.updateId)) claims.set(entry.updateId, "historical");
3233
+ const current = () => owner === expectedOwner && !expectedOwner.controller.signal.aborted &&
3234
+ deps.isContextCurrent?.(expectedOwner.ctx) !== false && deps.getJournalBindingKey?.() === binding &&
3235
+ deps.hasAuthority(expectedOwner.ctx) && isQueueOwnerIdentityCurrent(expectedOwner) && deferredSources.get(entry.updateId) === source &&
3236
+ isDeepStrictEqual(source.entry, entry) && claims.get(entry.updateId) !== "queued";
3237
+ let committed: TelegramUpdateJournalPendingAbandonmentResult | undefined;
3238
+ try {
3239
+ if (current()) await expireRoutingObserver({ original: structuredClone(entry), journalBindingKey: binding, isCurrent: current,
3240
+ expire() {
3241
+ if (!current()) return undefined;
3242
+ if (committed) return { ...committed, duplicate: true };
3243
+ claims.set(entry.updateId, "abandoning");
3244
+ committed = expireRouting!({ entry: source.entry, journalBindingKey: binding, operatorUserId: lifetime.operatorUserId, isCurrent: current });
3245
+ claims.delete(entry.updateId); deferredSources.delete(entry.updateId);
3246
+ state.journalEntryCount = committed.entryCount; state.journalSerializedBytes = committed.serializedBytes;
3247
+ updateClaimCounts(); notifyStateChange(); changed = true;
3248
+ return committed;
3249
+ } }, expectedOwner.ctx, expectedOwner.controller.signal);
3250
+ } catch (error) { recordRuntimeEvent(error, { phase: "routing-input-expiry", updateId: entry.updateId }); }
3251
+ if (!committed) nextAtMs = Math.min(nextAtMs ?? Infinity, getNowMs() + 60_000);
3252
+ }
3253
+ if (nextAtMs !== undefined && owner === expectedOwner && !expectedOwner.controller.signal.aborted) {
3254
+ const token = {}; routingTimerToken = token;
3255
+ routingTimer = scheduleRetry(() => {
3256
+ if (routingTimerToken !== token || owner !== expectedOwner || expectedOwner.controller.signal.aborted) return;
3257
+ routingTimer = undefined; routingTimerToken = undefined; pendingSignal = true; launchDrain();
3258
+ }, Math.max(1, nextAtMs - getNowMs()));
3259
+ }
3260
+ return changed;
3261
+ };
3262
+ const mutateRoutingSource = (input: { updateId: number; signal: AbortSignal; operatorUserId: number; sourceUpdateIds: readonly number[] }, select: boolean) => {
3263
+ const expectedOwner = owner, ids = [...input.sourceUpdateIds];
3264
+ if (!ids.length || !ids.includes(input.updateId) || new Set(ids).size !== ids.length || ids.some(id => !Number.isSafeInteger(id) || id < 0)) return undefined;
3265
+ const sources = ids.map(id => deferredSources.get(id)), binding = sources[0]?.journalBindingKey;
3266
+ const current = () => !!expectedOwner && owner === expectedOwner && expectedOwner.controller.signal === input.signal && !input.signal.aborted &&
3267
+ !!binding && sources.every((source, index) => !!source && deferredSources.get(ids[index]!) === source && !source.historical &&
3268
+ source.journalBindingKey === binding && claims.get(ids[index]!) !== "queued" && claims.get(ids[index]!) !== "abandoning") &&
3269
+ deps.getJournalBindingKey?.() === binding && deps.isContextCurrent?.(expectedOwner.ctx) !== false &&
3270
+ deps.hasAuthority(expectedOwner.ctx) && isQueueOwnerIdentityCurrent(expectedOwner);
3271
+ if (!current() || !armRouting || !selectRouting || !expireRoutingObserver) return undefined;
3272
+ const authority = { journalBindingKey: binding!, entries: sources.map(source => structuredClone(source!.entry)), operatorUserId: input.operatorUserId, isCurrent: current };
3273
+ const result = select ? selectRouting(authority) : { issued: false, entries: armRouting({ ...authority, publishedAtMs: getNowMs() }) };
3274
+ if (result.entries.length !== ids.length || result.entries.some((entry, index) => entry.updateId !== ids[index] || !entry.routingInput) || !current()) {
3275
+ throw new Error("Telegram routing input lifetime ACK was not confirmed.");
3276
+ }
3277
+ const retained = deps.journal.read().entries;
3278
+ if (result.entries.some(entry => !isDeepStrictEqual(retained.find(value => value.updateId === entry.updateId), entry)) || !current()) {
3279
+ throw new Error("Telegram routing input lifetime ACK was not retained.");
3280
+ }
3281
+ result.entries.forEach((entry, index) => { sources[index]!.entry = structuredClone(entry); });
3282
+ pendingSignal = true; launchDrain();
3283
+ return { issued: result.issued, lifetime: { ...result.entries[0]!.routingInput! } };
3284
+ };
2818
3285
  const drain = async (
2819
3286
  expectedOwner: TelegramUpdateWorkerOwner<TContext>,
2820
3287
  ): Promise<TelegramUpdateWorkerDrainResult> => {
@@ -2825,10 +3292,15 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
2825
3292
  let scheduledRetryAtMs: number | undefined;
2826
3293
  try {
2827
3294
  snapshot = deps.journal.read();
2828
- scheduledRetryAtMs = refreshJournalState(snapshot, expectedOwner);
3295
+ scheduledRetryAtMs = await refreshJournalState(snapshot, expectedOwner);
3296
+ // One complete validated baseline per generation, before batching or execution.
3297
+ // Replayed v1 entries do not prove that a prior process never forwarded them.
3298
+ expectedOwner.startupUpdateIds ??= new Set(snapshot.entries.map(entry => entry.updateId));
2829
3299
  } catch (error) {
2830
3300
  return blockWithFailure("journal-read", "journal-read", error);
2831
3301
  }
3302
+ if (await reconcileRoutingInputs(snapshot, expectedOwner)) continue;
3303
+ if (owner !== expectedOwner || expectedOwner.controller.signal.aborted) return "aborted";
2832
3304
  const nowMs = getNowMs();
2833
3305
  const entries: TelegramUpdateWorkerJournalSnapshot["entries"][number][] =
2834
3306
  [];
@@ -2885,7 +3357,7 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
2885
3357
  transition("idle");
2886
3358
  return "idle";
2887
3359
  }
2888
- const completedUpdateIds: number[] = [];
3360
+ const completedUpdateIds: { updateId: number; expectedSource?: TelegramDeferredSourceEvidence; spent?: true }[] = [];
2889
3361
  let snapshotInvalidated = false;
2890
3362
  for (const entry of entries) {
2891
3363
  const priorExecutions = unsettledExecutionsByUpdateId.get(entry.updateId);
@@ -2907,9 +3379,92 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
2907
3379
  break;
2908
3380
  }
2909
3381
  if (entry.preApprovalExcluded === true) {
2910
- completedUpdateIds.push(entry.updateId);
3382
+ completedUpdateIds.push({ updateId: entry.updateId });
2911
3383
  continue;
2912
3384
  }
3385
+ if (isTelegramUpdateJournalLegacyFamilyVersion(snapshot.version) && entry.state === "pending" && deps.journal.inspectPendingRetention) {
3386
+ const authorityResult = checkAuthority(expectedOwner, entry.updateId);
3387
+ if (authorityResult) return authorityResult;
3388
+ let retained: TelegramUpdateJournalPendingRetentionEvidence | undefined;
3389
+ let unverifiable = false;
3390
+ try { retained = deps.journal.inspectPendingRetention(entry); }
3391
+ catch (error) {
3392
+ unverifiable = true;
3393
+ recordRuntimeEvent(error, { phase: "pending-retention-inspection", updateId: entry.updateId });
3394
+ }
3395
+ if (retained || unverifiable) {
3396
+ // A copy is protection evidence only. A fresh owner must explicitly retry
3397
+ // cancellation; neither startup nor a malformed copy may dispatch the source.
3398
+ claims.set(entry.updateId, "abandoning");
3399
+ const journalBindingKey = retained?.journalBindingKey ?? deps.getJournalBindingKey?.();
3400
+ if (journalBindingKey) deferredSources.set(entry.updateId, { entry, journalBindingKey });
3401
+ transition("deferred", entry.updateId);
3402
+ continue;
3403
+ }
3404
+ }
3405
+ if (isTelegramUpdateJournalLegacyFamilyVersion(snapshot.version) && (entry.state === "pending" || entry.state === "retry-wait")) {
3406
+ const historical = expectedOwner.startupUpdateIds.has(entry.updateId);
3407
+ const journalBindingKey = deps.getJournalBindingKey?.();
3408
+ // Classifiers and handler projections receive no mutable alias to CAS evidence.
3409
+ if (journalBindingKey) deferredSources.set(entry.updateId, { entry: structuredClone(entry), journalBindingKey,
3410
+ ...(historical ? { historical: true } : {}) });
3411
+ const classifyPending = historical && entry.state === "pending" ? deps.shouldReviewHistoricalInput : deps.shouldHoldPendingInput;
3412
+ if (classifyPending) {
3413
+ const checkHistoricalAuthority = (): TelegramUpdateWorkerDrainResult | undefined => {
3414
+ if (owner !== expectedOwner || expectedOwner.controller.signal.aborted) return "aborted";
3415
+ if (deps.getJournalBindingKey?.() !== journalBindingKey || deps.isContextCurrent?.(expectedOwner.ctx) === false) {
3416
+ transition("blocked", entry.updateId, "authority-lost");
3417
+ return "blocked";
3418
+ }
3419
+ return checkAuthority(expectedOwner, entry.updateId);
3420
+ };
3421
+ const beforeClassification = checkHistoricalAuthority();
3422
+ if (beforeClassification) return beforeClassification;
3423
+ let review: boolean | "retain";
3424
+ try { review = await classifyPending(structuredClone(entry), expectedOwner.ctx, expectedOwner.controller.signal); }
3425
+ catch (error) {
3426
+ const authorityResult = checkHistoricalAuthority();
3427
+ if (authorityResult) return authorityResult;
3428
+ return blockWithFailure("execution", "historical-review-classification", error, entry.updateId);
3429
+ }
3430
+ const authorityResult = checkHistoricalAuthority();
3431
+ if (authorityResult) return authorityResult;
3432
+ if (typeof review !== "boolean" && !(historical && entry.state === "pending" && review === "retain"))
3433
+ return blockWithFailure("execution", "historical-review-classification",
3434
+ new TelegramUpdateAdmissionOutcomeError("Historical classification must return a boolean or retain verdict."), entry.updateId);
3435
+ // Classification may await a domain read; accepted/changed source evidence wins.
3436
+ try {
3437
+ const current = deps.journal.read();
3438
+ if (current.version !== snapshot.version || JSON.stringify(current.entries.find(candidate => candidate.updateId === entry.updateId)) !== JSON.stringify(entry)) {
3439
+ snapshotInvalidated = true;
3440
+ break;
3441
+ }
3442
+ } catch (error) { return blockWithFailure("journal-read", "historical-source-recheck", error, entry.updateId); }
3443
+ if (review === "retain") {
3444
+ claims.set(entry.updateId, "retained");
3445
+ transition("deferred", entry.updateId);
3446
+ continue;
3447
+ }
3448
+ if (review && historical && spendHistorical && classifyPending === deps.shouldReviewHistoricalInput) {
3449
+ completedUpdateIds.push({ updateId: entry.updateId, spent: true });
3450
+ continue;
3451
+ }
3452
+ if (review) {
3453
+ claims.set(entry.updateId, "historical");
3454
+ transition("deferred", entry.updateId);
3455
+ continue;
3456
+ }
3457
+ }
3458
+ if (entry.state === "pending" && entry.routingInput) {
3459
+ if (historical && spendHistorical) {
3460
+ completedUpdateIds.push({ updateId: entry.updateId, spent: true });
3461
+ continue;
3462
+ }
3463
+ claims.set(entry.updateId, "historical");
3464
+ transition("deferred", entry.updateId);
3465
+ continue;
3466
+ }
3467
+ }
2913
3468
  clearRetryTimer();
2914
3469
  transition("executing", entry.updateId);
2915
3470
  const execution = await executeWithinOwner(expectedOwner, entry.update);
@@ -2958,7 +3513,7 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
2958
3513
  }
2959
3514
  for (const sourceUpdateId of receipt.sourceUpdateIds) claims.set(sourceUpdateId, "queued");
2960
3515
  try {
2961
- publishCommittedQueueReceipt(receipt, outcome.queueReceipt.queueOwner, expectedOwner.ctx);
3516
+ await publishCommittedQueueReceipt(receipt, outcome.queueReceipt.queueOwner, expectedOwner);
2962
3517
  } catch (error) {
2963
3518
  return blockWithFailure("invalid-outcome", "queue-receipt-publish", error,
2964
3519
  entry.updateId);
@@ -2975,7 +3530,7 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
2975
3530
  if (postExecutionAuthority) return postExecutionAuthority;
2976
3531
  const claimableUpdateIds = new Set<number>([
2977
3532
  entry.updateId,
2978
- ...claims.keys(),
3533
+ ...[...claims].filter(([, claim]) => claim !== "abandoning" && claim !== "historical" && claim !== "retained").map(([id]) => id),
2979
3534
  ]);
2980
3535
  let outcome: TelegramUpdateAdmissionOutcome;
2981
3536
  try {
@@ -2993,7 +3548,10 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
2993
3548
  );
2994
3549
  }
2995
3550
  if (outcome.kind === "deferred") {
2996
- claims.set(entry.updateId, "deferred");
3551
+ if (outcome.routingReview && !deferredSources.get(entry.updateId)?.historical) {
3552
+ return blockWithFailure("invalid-outcome", "historical-review-source", new TelegramUpdateAdmissionOutcomeError("Historical review needs an exact startup source."), entry.updateId);
3553
+ }
3554
+ claims.set(entry.updateId, outcome.routingReview ? "historical" : "deferred");
2997
3555
  transition("deferred", entry.updateId);
2998
3556
  continue;
2999
3557
  }
@@ -3003,7 +3561,7 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3003
3561
  completedUpdateIds,
3004
3562
  );
3005
3563
  if (completionResult) return completionResult;
3006
- const queuedResult = commitQueuedOutcome(
3564
+ const queuedResult = await commitQueuedOutcome(
3007
3565
  expectedOwner,
3008
3566
  entry.updateId,
3009
3567
  outcome,
@@ -3015,7 +3573,7 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3015
3573
  snapshotInvalidated = true;
3016
3574
  break;
3017
3575
  }
3018
- completedUpdateIds.push(entry.updateId);
3576
+ completedUpdateIds.push({ updateId: entry.updateId, ...(outcome.expectedSource ? { expectedSource: outcome.expectedSource } : {}) });
3019
3577
  }
3020
3578
  const completionResult = commitCompletedBatch(
3021
3579
  expectedOwner,
@@ -3037,6 +3595,11 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3037
3595
  launchDrain = (): void => {
3038
3596
  const expectedOwner = owner;
3039
3597
  if (!expectedOwner || drainPromise) return;
3598
+ let startupKey: string | undefined;
3599
+ if (onHeldSourcesPrepared) {
3600
+ try { startupKey = deps.getJournalBindingKey?.(); } catch { /* Missing startup identity grants no notification. */ }
3601
+ }
3602
+ let lastResult: TelegramUpdateWorkerDrainResult | undefined;
3040
3603
  const run = async (): Promise<void> => {
3041
3604
  while (
3042
3605
  pendingSignal &&
@@ -3046,7 +3609,7 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3046
3609
  pendingSignal = false;
3047
3610
  // Keep newer wakes in this run so waitForDrain also covers them. A failure
3048
3611
  // observed after signal() still keeps its latch; this loop never clears it.
3049
- await drain(expectedOwner);
3612
+ lastResult = await drain(expectedOwner);
3050
3613
  }
3051
3614
  };
3052
3615
  const operation = run();
@@ -3056,6 +3619,21 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3056
3619
  drainPromise = undefined;
3057
3620
  if (pendingSignal && !expectedOwner.controller.signal.aborted) {
3058
3621
  launchDrain();
3622
+ } else if (lastResult === "idle" && startupKey && onHeldSourcesPrepared && !expectedOwner.heldSourcesPreparedIssued) {
3623
+ try {
3624
+ const ctx = expectedOwner.ctx, key = startupKey;
3625
+ const isCurrent = () => owner === expectedOwner && !blocked && !expectedOwner.controller.signal.aborted &&
3626
+ deps.getJournalBindingKey?.() === key && deps.isContextCurrent?.(ctx) !== false && deps.hasAuthority(ctx);
3627
+ if (!isCurrent()) return;
3628
+ expectedOwner.heldSourcesPreparedIssued = true;
3629
+ // Do not couple lifecycle/stop or the worker drain to a held controller/API reply.
3630
+ void Promise.resolve().then(() => isCurrent() ? onHeldSourcesPrepared({ ctx,
3631
+ journalBindingKey: key, signal: expectedOwner.controller.signal, isCurrent }) : undefined).catch(error => {
3632
+ try { deps.recordRuntimeEvent?.("inbound-worker", error, { phase: "held-source-preparation" }); } catch { /* Diagnostic only. */ }
3633
+ });
3634
+ } catch (error) {
3635
+ try { deps.recordRuntimeEvent?.("inbound-worker", error, { phase: "held-source-preparation" }); } catch { /* Diagnostic only. */ }
3636
+ }
3059
3637
  }
3060
3638
  }
3061
3639
  };
@@ -3072,6 +3650,73 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3072
3650
  drainPromise = tracked;
3073
3651
  };
3074
3652
 
3653
+ const abandonSource = (input: Parameters<NonNullable<TelegramUpdateWorkerRuntime<TContext>["abandonDeferred"]>>[0],
3654
+ historicalSource?: ReturnType<typeof deferredSources.get>): TelegramUpdateJournalPendingAbandonmentResult | undefined => {
3655
+ const expectedOwner = owner;
3656
+ const source = deferredSources.get(input.updateId);
3657
+ const abandonPending = deps.journal.abandonPending;
3658
+ const isCurrent = () => {
3659
+ if (!expectedOwner || owner !== expectedOwner || input.signal.aborted ||
3660
+ expectedOwner.controller.signal !== input.signal || !source ||
3661
+ source.entry.routingInput?.phase === "selected" || (source.historical && historicalSource !== source) ||
3662
+ (claims.get(input.updateId) !== "deferred" && claims.get(input.updateId) !== "abandoning" &&
3663
+ !(claims.get(input.updateId) === "historical" && historicalSource === source))) return false;
3664
+ return input.isCurrent() && deps.isContextCurrent?.(expectedOwner.ctx) !== false &&
3665
+ deps.getJournalBindingKey?.() === source.journalBindingKey && deps.hasAuthority(expectedOwner.ctx);
3666
+ };
3667
+ if (!source || !abandonPending || !isCurrent()) return undefined;
3668
+ // Retain this claim on failure, including unknown commit: never reopen dispatch.
3669
+ claims.set(input.updateId, "abandoning");
3670
+ try {
3671
+ const result = abandonPending({ ...input, entry: source.entry,
3672
+ journalBindingKey: source.journalBindingKey, isCurrent });
3673
+ claims.delete(input.updateId);
3674
+ deferredSources.delete(input.updateId);
3675
+ state.journalEntryCount = result.entryCount;
3676
+ state.journalSerializedBytes = result.serializedBytes;
3677
+ transition("idle");
3678
+ pendingSignal = true;
3679
+ launchDrain();
3680
+ return result;
3681
+ } catch (error) {
3682
+ recordRuntimeEvent(error, { phase: "pending-abandonment", updateId: input.updateId });
3683
+ updateClaimCounts();
3684
+ notifyStateChange();
3685
+ throw error;
3686
+ }
3687
+ };
3688
+
3689
+ const inspectRecovery = (input: TelegramDeferredAbandonmentRecoveryRequest & { signal: AbortSignal },
3690
+ kind: "abandoning" | "historical"): TelegramDeferredAbandonmentRecoveryPage | undefined => {
3691
+ const expectedOwner = owner;
3692
+ if (!expectedOwner) return undefined;
3693
+ const { signal, journalBindingKey, isCurrent: callerIsCurrent, afterUpdateId } = input;
3694
+ const isOwnerCurrent = () => owner === expectedOwner && expectedOwner.controller.signal === signal && !signal.aborted;
3695
+ const isCurrent = () => isOwnerCurrent() && callerIsCurrent() && deps.isContextCurrent?.(expectedOwner.ctx) !== false &&
3696
+ deps.getJournalBindingKey?.() === journalBindingKey && deps.hasAuthority(expectedOwner.ctx);
3697
+ if (!isCurrent() || !deps.journal.abandonPending || !deps.journal.inspectPendingRetention ||
3698
+ (afterUpdateId !== undefined && (!Number.isSafeInteger(afterUpdateId) || afterUpdateId < 0))) return undefined;
3699
+ const matches = [...deferredSources].filter(([id, source]) => claims.get(id) === kind &&
3700
+ source.journalBindingKey === journalBindingKey && (afterUpdateId === undefined || id > afterUpdateId))
3701
+ .sort(([a], [b]) => a - b);
3702
+ const sources = matches.slice(0, TELEGRAM_HELD_SOURCE_INSPECTION_LIMIT).map(([updateId, source]) => {
3703
+ let committed: TelegramUpdateJournalPendingAbandonmentResult | undefined;
3704
+ return { original: structuredClone(source.entry), retry(authority: TelegramDeferredUpdateAbandonmentAuthority) {
3705
+ const authorized = () => isOwnerCurrent() && authority.isCurrent() && isCurrent();
3706
+ if (!authorized()) return undefined;
3707
+ if (committed) return { ...committed, duplicate: true };
3708
+ const stillProtected = () => authorized() && (claims.get(updateId) === kind || claims.get(updateId) === "abandoning") &&
3709
+ deferredSources.get(updateId) === source;
3710
+ if (!stillProtected()) return undefined;
3711
+ committed = abandonSource({ ...authority, updateId, signal, isCurrent: stillProtected }, source);
3712
+ return committed;
3713
+ } };
3714
+ });
3715
+ return isCurrent() ? { sources,
3716
+ ...(matches.length > TELEGRAM_HELD_SOURCE_INSPECTION_LIMIT ? { nextAfterUpdateId: matches[TELEGRAM_HELD_SOURCE_INSPECTION_LIMIT - 1]![0] } : {}) } : undefined;
3717
+ };
3718
+
3719
+
3075
3720
  return {
3076
3721
  start(ctx) {
3077
3722
  if (owner) {
@@ -3082,6 +3727,7 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3082
3727
  return;
3083
3728
  }
3084
3729
  clearRetryTimer();
3730
+ clearRoutingTimer();
3085
3731
  const nowMs = getNowMs();
3086
3732
  const generation = ++nextGeneration;
3087
3733
  const queueOwnerIdentity = resolveQueueOwnerIdentity(ctx, generation);
@@ -3092,6 +3738,7 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3092
3738
  queueOwnerIdentity,
3093
3739
  };
3094
3740
  claims.clear();
3741
+ deferredSources.clear();
3095
3742
  committedQueueReceipts.clear();
3096
3743
  blocked = false;
3097
3744
  pendingSignal = true;
@@ -3104,6 +3751,8 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3104
3751
  state.journalSerializedBytes = 0;
3105
3752
  state.oldestAdmittedAtMs = undefined;
3106
3753
  state.deferredClaimCount = 0;
3754
+ delete state.abandoningClaimCount;
3755
+ delete state.historicalClaimCount;
3107
3756
  state.queuedClaimCount = 0;
3108
3757
  state.foreignQueuedCount = 0;
3109
3758
  delete state.foreignQueuedOwner;
@@ -3133,7 +3782,47 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3133
3782
  pendingSignal = true;
3134
3783
  launchDrain();
3135
3784
  },
3136
- settleDeferred(input) {
3785
+ armRoutingInput(input) { return mutateRoutingSource(input, false)?.lifetime; },
3786
+ selectRoutingInput(input) { return mutateRoutingSource(input, true)?.issued === true; },
3787
+ inspectDeferredSource(input) {
3788
+ const expectedOwner = owner;
3789
+ const source = deferredSources.get(input.updateId);
3790
+ const current = (): boolean => {
3791
+ if (!expectedOwner || owner !== expectedOwner || input.signal.aborted || expectedOwner.controller.signal !== input.signal ||
3792
+ !source || deferredSources.get(input.updateId) !== source || claims.get(input.updateId) !== "deferred" ||
3793
+ deps.getJournalBindingKey?.() !== source.journalBindingKey || deps.isContextCurrent?.(expectedOwner.ctx) === false ||
3794
+ !deps.hasAuthority(expectedOwner.ctx)) return false;
3795
+ return isQueueOwnerIdentityCurrent(expectedOwner);
3796
+ };
3797
+ if (!current()) return undefined;
3798
+ const snapshot = deps.journal.read();
3799
+ const entry = snapshot.entries.find(value => value.updateId === input.updateId);
3800
+ if (!isTelegramUpdateJournalLegacyFamilyVersion(snapshot.version) || !entry ||
3801
+ JSON.stringify(entry) !== JSON.stringify(source!.entry) || !current()) return undefined;
3802
+ return { journalBindingKey: source!.journalBindingKey, ...createTelegramUpdateJournalEntryDigest(entry) };
3803
+ },
3804
+ isHistoricalSource(input) {
3805
+ const source = deferredSources.get(input.updateId);
3806
+ const isCurrent = () => owner?.controller.signal === input.signal && !input.signal.aborted &&
3807
+ source?.historical === true && deferredSources.get(input.updateId) === source;
3808
+ if (!isCurrent()) return false;
3809
+ // Domain eligibility observes original evidence, never mutable handler projections.
3810
+ return (!input.matchesOriginal || input.matchesOriginal(structuredClone(source!.entry)) === true) && isCurrent();
3811
+ },
3812
+ supportsDeferredAbandonment(input) {
3813
+ const expectedOwner = owner;
3814
+ const source = deferredSources.get(input.updateId);
3815
+ if (!expectedOwner || input.signal.aborted || expectedOwner.controller.signal !== input.signal ||
3816
+ !source || source.historical || input.journalBindingKey !== source.journalBindingKey ||
3817
+ !deps.journal.abandonPending || !deps.journal.inspectPendingRetention ||
3818
+ claims.get(input.updateId) === "queued") return false;
3819
+ return deps.isContextCurrent?.(expectedOwner.ctx) !== false &&
3820
+ deps.getJournalBindingKey?.() === source.journalBindingKey && deps.hasAuthority(expectedOwner.ctx);
3821
+ },
3822
+ abandonDeferred(input) { return abandonSource(input); },
3823
+ inspectAbandoning(input) { return inspectRecovery(input, "abandoning"); },
3824
+ inspectHistorical(input) { return inspectRecovery(input, "historical"); },
3825
+ async settleDeferred(input) {
3137
3826
  const expectedOwner = owner;
3138
3827
  if (
3139
3828
  !expectedOwner ||
@@ -3154,10 +3843,19 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3154
3843
  return;
3155
3844
  }
3156
3845
  const claim = claims.get(input.updateId);
3846
+ if (claim === "abandoning" || claim === "historical" || claim === "retained") return;
3157
3847
  if (input.outcome.kind === "complete") {
3158
3848
  // Expiry may retire only a still-deferred source, never accepted queue work.
3159
3849
  if (claim !== "deferred") return;
3160
- const result = commitCompletedBatch(expectedOwner, [input.updateId]);
3850
+ let completion: TelegramUpdateAdmissionOutcome;
3851
+ try { completion = validateTelegramUpdateAdmissionOutcome(input.outcome, input.updateId, new Set([input.updateId])); }
3852
+ catch (error) {
3853
+ blockWithFailure("invalid-outcome", "late-invalid-completion", error, input.updateId);
3854
+ return;
3855
+ }
3856
+ if (completion.kind !== "complete") return;
3857
+ const result = commitCompletedBatch(expectedOwner, [{ updateId: input.updateId,
3858
+ ...(completion.expectedSource ? { expectedSource: completion.expectedSource } : {}) }]);
3161
3859
  if (!result) transition("idle", input.updateId);
3162
3860
  return;
3163
3861
  }
@@ -3182,7 +3880,7 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3182
3880
  outcome = validateTelegramUpdateAdmissionOutcome(
3183
3881
  input.outcome,
3184
3882
  input.updateId,
3185
- new Set([input.updateId, ...claims.keys()]),
3883
+ new Set([input.updateId, ...[...claims].filter(([, claim]) => claim !== "abandoning" && claim !== "historical" && claim !== "retained").map(([id]) => id)]),
3186
3884
  );
3187
3885
  } catch (error) {
3188
3886
  blockWithFailure(
@@ -3217,11 +3915,8 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3217
3915
  releaseDeferredClaims(outcome.sourceUpdateIds);
3218
3916
  return;
3219
3917
  }
3220
- const result = commitQueuedOutcome(
3221
- expectedOwner,
3222
- input.updateId,
3223
- outcome,
3224
- );
3918
+ const publication = commitQueuedOutcome(expectedOwner, input.updateId, outcome);
3919
+ const result = typeof publication === "string" ? publication : await publication;
3225
3920
  if (result === "blocked") {
3226
3921
  releaseDeferredClaims(outcome.sourceUpdateIds);
3227
3922
  return;
@@ -3229,7 +3924,7 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3229
3924
  if (result === "aborted") return;
3230
3925
  transition("queued", input.updateId);
3231
3926
  },
3232
- settleCustodied(input) {
3927
+ async settleCustodied(input) {
3233
3928
  const expectedOwner = owner;
3234
3929
  if (!expectedOwner || expectedOwner.controller.signal !== input.signal || input.signal.aborted)
3235
3930
  return;
@@ -3277,7 +3972,7 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3277
3972
  }
3278
3973
  for (const sourceUpdateId of receipt.sourceUpdateIds) claims.set(sourceUpdateId, "queued");
3279
3974
  try {
3280
- publishCommittedQueueReceipt(receipt, input.result.queueReceipt.queueOwner, expectedOwner.ctx);
3975
+ await publishCommittedQueueReceipt(receipt, input.result.queueReceipt.queueOwner, expectedOwner);
3281
3976
  } catch (error) {
3282
3977
  blockWithFailure("invalid-outcome", "queue-receipt-publish", error, input.updateId);
3283
3978
  return;
@@ -3289,7 +3984,19 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3289
3984
  const committed = getCurrentQueueReceipt(receipt);
3290
3985
  return committed ? { ...committed.queueOwner } : undefined;
3291
3986
  },
3292
- completeQueueReceipts(input) {
3987
+ getQueueReceiptSettlementOwner(receipt, ctx, reason) {
3988
+ const expectedOwner = owner, committed = committedQueueReceipts.get(receipt.receiptId);
3989
+ if (!expectedOwner || expectedOwner.controller.signal.aborted ||
3990
+ !(deps.isContextCurrent?.(ctx) ?? expectedOwner.ctx === ctx) || !isQueueOwnerIdentityCurrent(expectedOwner) ||
3991
+ !committed || !areTelegramQueueAdmissionReceiptsEqual(committed.receipt, normalizeQueueReceipt(receipt)) ||
3992
+ (deps.getJournalBindingKey && committed.receipt.journalBindingKey !== deps.getJournalBindingKey()) ||
3993
+ !isTelegramUpdateJournalQueueOwnerProcess(committed.queueOwner, expectedOwner.queueOwnerIdentity)) return undefined;
3994
+ const attempt = scopedQueueCompletionAttempts.get(receipt.receiptId);
3995
+ if (attempt ? attempt.owner !== expectedOwner || attempt.reason !== reason || attempt.journalBindingKey !== deps.getJournalBindingKey?.()
3996
+ : !getCurrentQueueReceipt(receipt)) return undefined;
3997
+ return { ...committed.queueOwner };
3998
+ },
3999
+ completeQueueReceipts: function completeQueueReceipts(input): boolean {
3293
4000
  const expectedOwner = owner;
3294
4001
  if (
3295
4002
  !expectedOwner ||
@@ -3298,8 +4005,10 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3298
4005
  ) {
3299
4006
  return false;
3300
4007
  }
3301
- if (input.receipts.length === 0) return true;
3302
- const normalizedReceipts = input.receipts.map(normalizeQueueReceipt);
4008
+ // An ACK permits only local completion reconciliation, never execution readiness.
4009
+ const receipts = input.sourceCompletions === undefined ? input.receipts.filter(receipt => !isQueueReceiptCompletionAcknowledged(receipt)) : input.receipts;
4010
+ if (receipts.length === 0) return input.sourceCompletions === undefined;
4011
+ const normalizedReceipts = receipts.map(normalizeQueueReceipt);
3303
4012
  const receiptIds = new Set<string>();
3304
4013
  const sourceUpdateIds = new Set<number>();
3305
4014
  const queuedCompletions: Array<{
@@ -3343,11 +4052,87 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3343
4052
  sourceUpdateIds.add(updateId);
3344
4053
  }
3345
4054
  }
4055
+ const completionBinding = deps.getJournalBindingKey?.();
4056
+ const ordinaryObserverBound = normalizedReceipts.every(receipt =>
4057
+ committedQueueReceipts.get(receipt.receiptId)!.receipt.journalBindingKey === completionBinding);
4058
+ const ordinaryCompletionCurrent = (): boolean => ordinaryObserverBound && owner === expectedOwner &&
4059
+ !expectedOwner.controller.signal.aborted && (deps.isContextCurrent?.(input.ctx) ?? expectedOwner.ctx === input.ctx) &&
4060
+ isQueueOwnerIdentityCurrent(expectedOwner) && deps.getJournalBindingKey?.() === completionBinding;
3346
4061
  let removedUpdateIds: readonly number[];
4062
+ let scopedCompletionCurrent: (() => boolean) | undefined;
3347
4063
  try {
3348
- removedUpdateIds = deps.journal.completeQueued(
3349
- queuedCompletions,
3350
- ).removedUpdateIds;
4064
+ const prepared = normalizedReceipts.flatMap(receipt => committedQueueReceipts.get(receipt.receiptId)!.sourceCompletions ?? []);
4065
+ const sourceCompletions = input.sourceCompletions === undefined ? (prepared.length ? prepared : undefined) : input.sourceCompletions;
4066
+ if (input.sourceCompletions !== undefined && prepared.some(expected => !input.sourceCompletions!.some(value => isDeepStrictEqual(value, expected)))) {
4067
+ throw new Error("Telegram queued completion conflicts with its prepared source scopes.");
4068
+ }
4069
+ if (input.sourceCompletions === undefined && prepared.length && receipts.some(receipt => !requiredScopedQueueReceipts.has(receipt.receiptId))) {
4070
+ const scoped = receipts.filter(receipt => requiredScopedQueueReceipts.has(receipt.receiptId));
4071
+ const ordinary = receipts.filter(receipt => !requiredScopedQueueReceipts.has(receipt.receiptId));
4072
+ if (!scoped.length || !ordinary.length) throw new Error("Telegram mixed queue completion has incomplete scoped authority.");
4073
+ const binding = deps.getJournalBindingKey?.();
4074
+ const current = (): boolean => owner === expectedOwner && !expectedOwner.controller.signal.aborted &&
4075
+ (deps.isContextCurrent?.(input.ctx) ?? expectedOwner.ctx === input.ctx) && isQueueOwnerIdentityCurrent(expectedOwner) &&
4076
+ !!binding && deps.getJournalBindingKey?.() === binding;
4077
+ // Independent whole receipts need independent ACKs. Ordinary siblings must not acquire invented scopes.
4078
+ if (!current() || !completeQueueReceipts({ receipts: scoped, ctx: input.ctx, reason: input.reason }) || !current()) return false;
4079
+ return completeQueueReceipts({ receipts: ordinary, ctx: input.ctx, reason: input.reason });
4080
+ }
4081
+ if (sourceCompletions === undefined) {
4082
+ if (queuedCompletions.some(receipt => requiredScopedQueueReceipts.has(receipt.receiptId))) {
4083
+ throw new Error("Telegram scoped queue completion cannot downgrade required source scopes.");
4084
+ }
4085
+ removedUpdateIds = deps.journal.completeQueued(queuedCompletions).removedUpdateIds;
4086
+ } else {
4087
+ for (const receipt of queuedCompletions) requiredScopedQueueReceipts.add(receipt.receiptId);
4088
+ queuedCompletions.sort((a, b) => a.receiptId < b.receiptId ? -1 : a.receiptId > b.receiptId ? 1 : 0);
4089
+ const journalBindingKey = deps.getJournalBindingKey?.();
4090
+ const current = (): boolean => owner === expectedOwner && !expectedOwner.controller.signal.aborted &&
4091
+ (deps.isContextCurrent?.(input.ctx) ?? expectedOwner.ctx === input.ctx) && isQueueOwnerIdentityCurrent(expectedOwner) &&
4092
+ deps.getJournalBindingKey?.() === journalBindingKey;
4093
+ if (!journalBindingKey || !current() || !completeQueuedExact || !inspectCompletion ||
4094
+ !Array.isArray(sourceCompletions) || input.sourceCompletions !== undefined && sourceCompletions.length !== sourceUpdateIds.size) {
4095
+ throw new Error("Telegram scoped queue completion requires full current source authority.");
4096
+ }
4097
+ scopedCompletionCurrent = current;
4098
+ const completions = normalizeTelegramQueueSourceCompletions(sourceCompletions, sourceUpdateIds, journalBindingKey, input.sourceCompletions !== undefined)
4099
+ .map(({ journalBindingKey: _binding, ...completion }) => completion);
4100
+ if (queuedCompletions.some(receipt => !completions.some(scope => receipt.sourceUpdateIds.includes(scope.updateId)))) {
4101
+ throw new Error("Telegram scoped queue completion requires a proven origin for every whole receipt.");
4102
+ }
4103
+ const previous = queuedCompletions.map(receipt => scopedQueueCompletionAttempts.get(receipt.receiptId));
4104
+ const retained = previous.find(attempt => attempt !== undefined);
4105
+ if (retained && (previous.some(attempt => attempt !== retained) || retained.owner !== expectedOwner ||
4106
+ retained.journalBindingKey !== journalBindingKey || retained.reason !== input.reason ||
4107
+ !isDeepStrictEqual(retained.receipts, queuedCompletions) || !isDeepStrictEqual(retained.completions, completions))) {
4108
+ throw new Error("Telegram scoped queue completion has conflicting issued authority.");
4109
+ }
4110
+ if (!retained) {
4111
+ const attempt = { owner: expectedOwner, journalBindingKey, reason: input.reason,
4112
+ receipts: queuedCompletions.map(receipt => ({ ...receipt, sourceUpdateIds: [...receipt.sourceUpdateIds], queueOwner: { ...receipt.queueOwner } })),
4113
+ completions: completions.map(completion => ({ ...completion })) };
4114
+ if (!current()) throw new Error("Telegram scoped queue completion authority changed before disposal.");
4115
+ // Mark before issuance: an exception may follow a committed rename, and cannot license another disposal.
4116
+ for (const receipt of queuedCompletions) scopedQueueCompletionAttempts.set(receipt.receiptId, attempt);
4117
+ const result = completeQueuedExact(queuedCompletions.map(receipt => ({ ...receipt, sourceUpdateIds: [...receipt.sourceUpdateIds], queueOwner: { ...receipt.queueOwner } })),
4118
+ completions.map(completion => ({ ...completion })));
4119
+ if (!current() || !isDeepStrictEqual(result.sourceCompletions, completions)) {
4120
+ throw new Error("Telegram scoped queue completion ACK was not confirmed.");
4121
+ }
4122
+ const removed = new Set(result.removedUpdateIds);
4123
+ if (removed.size !== sourceUpdateIds.size || [...sourceUpdateIds].some(id => !removed.has(id))) {
4124
+ throw new Error("Telegram scoped queue completion did not remove every source.");
4125
+ }
4126
+ }
4127
+ for (const completion of completions) {
4128
+ if (!current() || !isDeepStrictEqual(inspectCompletion({ ...completion }), completion) || !current()) {
4129
+ throw new Error("Telegram scoped queue completion ACK was not retained under current authority.");
4130
+ }
4131
+ }
4132
+ // Every immutable whole receipt has a scoped queued-origin witness; native queued ACK continuity proves its complete removal.
4133
+ // Unscoped siblings receive only receipt acknowledgement, never a source marker or absence-based proof.
4134
+ removedUpdateIds = [...sourceUpdateIds];
4135
+ }
3351
4136
  } catch (error) {
3352
4137
  blockWithFailure(
3353
4138
  "journal-write",
@@ -3370,12 +4155,14 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3370
4155
  );
3371
4156
  return false;
3372
4157
  }
3373
- input.receipts.forEach((receipt, index) => {
4158
+ receipts.forEach((receipt, index) => {
3374
4159
  acknowledgedQueueReceiptCompletions.set(receipt, normalizedReceipts[index]!);
3375
4160
  });
3376
4161
  for (const updateId of sourceUpdateIds) claims.delete(updateId);
3377
4162
  for (const receiptId of receiptIds) {
3378
4163
  committedQueueReceipts.delete(receiptId);
4164
+ requiredScopedQueueReceipts.delete(receiptId);
4165
+ scopedQueueCompletionAttempts.delete(receiptId);
3379
4166
  }
3380
4167
  const completedUpdateIds = [...sourceUpdateIds];
3381
4168
  state.lastCompletedUpdateId = Math.max(
@@ -3389,8 +4176,19 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3389
4176
  );
3390
4177
  updateClaimCounts();
3391
4178
  notifyStateChange();
3392
- pendingSignal = true;
3393
- launchDrain();
4179
+ if (scopedCompletionCurrent) for (const receipt of normalizedReceipts) {
4180
+ if (!scopedCompletionCurrent()) break;
4181
+ try { observeQueuedCompletion?.(normalizeQueueReceipt(receipt), input.ctx); }
4182
+ catch (error) { recordRuntimeEvent(error, { phase: "queue-receipt-completion-observer", receiptId: receipt.receiptId }); }
4183
+ }
4184
+ // Ordinary queued disposal is source completion too; notify only after a whole positive ACK and fresh origin authority.
4185
+ if (!scopedCompletionCurrent) for (const updateId of completedUpdateIds) {
4186
+ try {
4187
+ if (!ordinaryCompletionCurrent()) break;
4188
+ deps.onUpdateCompleted?.(updateId, input.ctx, completionBinding);
4189
+ } catch (error) { recordRuntimeEvent(error, { phase: "queue-source-completion-observer", updateId }); }
4190
+ }
4191
+ if (!scopedCompletionCurrent || scopedCompletionCurrent()) { pendingSignal = true; launchDrain(); }
3394
4192
  return true;
3395
4193
  },
3396
4194
  async stop() {
@@ -3398,12 +4196,14 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3398
4196
  if (!expectedOwner) return;
3399
4197
  pendingSignal = false;
3400
4198
  clearRetryTimer();
4199
+ clearRoutingTimer();
3401
4200
  expectedOwner.controller.abort();
3402
4201
  await drainPromise?.catch(() => undefined);
3403
4202
  if (owner !== expectedOwner) return;
3404
4203
  owner = undefined;
3405
4204
  drainPromise = undefined;
3406
4205
  claims.clear();
4206
+ deferredSources.clear();
3407
4207
  committedQueueReceipts.clear();
3408
4208
  blocked = false;
3409
4209
  state.phase = "stopped";
@@ -3411,6 +4211,8 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3411
4211
  state.currentUpdateId = undefined;
3412
4212
  state.blockedReason = undefined;
3413
4213
  state.deferredClaimCount = 0;
4214
+ delete state.abandoningClaimCount;
4215
+ delete state.historicalClaimCount;
3414
4216
  state.queuedClaimCount = 0;
3415
4217
  state.foreignQueuedCount = 0;
3416
4218
  delete state.foreignQueuedOwner;
@@ -3429,8 +4231,9 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3429
4231
  state.unsettledExecutionCount = unsettledExecutions.size;
3430
4232
  notifyStateChange();
3431
4233
  },
3432
- waitForDrain() {
3433
- return drainPromise ?? Promise.resolve();
4234
+ async waitForDrain() {
4235
+ await (drainPromise ?? Promise.resolve());
4236
+ await Promise.allSettled([...pendingQueuePublications.values()].map(value => value.task));
3434
4237
  },
3435
4238
  getState() {
3436
4239
  return getTelegramUpdateWorkerStateSnapshot(state);
@@ -3647,6 +4450,14 @@ export interface TelegramUpdateAdmissionHandleDeps<
3647
4450
  },
3648
4451
  ) => void | Promise<void>;
3649
4452
  onLateOutcomeError?: (error: unknown, updateId: number) => void;
4453
+ abandonDeferred?: TelegramUpdateWorkerRuntime<TContext>["abandonDeferred"];
4454
+ armRoutingInput?: TelegramUpdateWorkerRuntime<TContext>["armRoutingInput"];
4455
+ selectRoutingInput?: TelegramUpdateWorkerRuntime<TContext>["selectRoutingInput"];
4456
+ supportsDeferredAbandonment?: TelegramUpdateWorkerRuntime<TContext>["supportsDeferredAbandonment"];
4457
+ inspectAbandoning?: TelegramUpdateWorkerRuntime<TContext>["inspectAbandoning"];
4458
+ inspectHistorical?: TelegramUpdateWorkerRuntime<TContext>["inspectHistorical"];
4459
+ inspectDeferredSource?: TelegramUpdateWorkerRuntime<TContext>["inspectDeferredSource"];
4460
+ isHistoricalSource?: TelegramUpdateWorkerRuntime<TContext>["isHistoricalSource"];
3650
4461
  }
3651
4462
 
3652
4463
  function mergeTelegramReportedAdmissionOutcome(
@@ -3656,7 +4467,17 @@ function mergeTelegramReportedAdmissionOutcome(
3656
4467
  ): TelegramUpdateAdmissionOutcome {
3657
4468
  if (!current || current.kind === "deferred") return next;
3658
4469
  if (next.kind === "deferred") return current;
3659
- if (current.kind === "complete" && next.kind === "complete") return current;
4470
+ if (current.kind === "complete" && next.kind === "complete") {
4471
+ if (!current.expectedSource) return next;
4472
+ if (!next.expectedSource) return current;
4473
+ if (current.expectedSource.updateId === next.expectedSource.updateId &&
4474
+ current.expectedSource.journalBindingKey === next.expectedSource.journalBindingKey &&
4475
+ current.expectedSource.sourceSha256 === next.expectedSource.sourceSha256) {
4476
+ if (!current.expectedSource.completionSha256) return next;
4477
+ if (!next.expectedSource.completionSha256 || current.expectedSource.completionSha256 === next.expectedSource.completionSha256) return current;
4478
+ }
4479
+ throw new TelegramUpdateAdmissionOutcomeError(`Telegram update ${updateId} reported conflicting completion sources.`);
4480
+ }
3660
4481
  if (current.kind === "queued" && next.kind === "queued" &&
3661
4482
  areTelegramQueueAdmissionReceiptsEqual(current, next)) return current;
3662
4483
  throw new TelegramUpdateAdmissionOutcomeError(
@@ -4650,13 +5471,21 @@ export function createTelegramUpdateAdmissionHandle<
4650
5471
  let nextExecutionGeneration = 0;
4651
5472
  return async (update, ctx, signal) => {
4652
5473
  const generation = ++nextExecutionGeneration;
5474
+ let suspended = false;
5475
+ let routingClaims = 0;
5476
+ let abandoned: TelegramUpdateJournalPendingAbandonmentResult | undefined;
5477
+ let routingLifetime: TelegramUpdateJournalRoutingInput | undefined;
5478
+ let routingSourceIds: readonly number[] | undefined;
5479
+ let routingClockUnknown = false;
5480
+ let routingSelectionAttempted = false;
5481
+ let routingSelectionConfirmed = false;
4653
5482
  const execution: TelegramUpdateExecutionFence = {
4654
5483
  generation,
4655
5484
  updateId: update.update_id,
4656
5485
  signal,
4657
- isCurrent: () => !signal.aborted,
5486
+ isCurrent: () => !signal.aborted && !suspended,
4658
5487
  assertCurrent() {
4659
- if (signal.aborted) {
5488
+ if (signal.aborted || suspended) {
4660
5489
  throw signal.reason ?? new DOMException("Aborted", "AbortError");
4661
5490
  }
4662
5491
  },
@@ -4666,9 +5495,18 @@ export function createTelegramUpdateAdmissionHandle<
4666
5495
  execution.assertCurrent();
4667
5496
  if (verdict === "consume") return { kind: "complete" };
4668
5497
  let immediate = true;
5498
+ let settlementReported = false;
4669
5499
  let outcome: TelegramUpdateAdmissionOutcome | undefined;
4670
5500
  const boundUpdate = bindTelegramUpdateExecutionFence(
4671
5501
  bindTelegramUpdateAdmissionSource(update, (next) => {
5502
+ if (suspended || signal.aborted) return;
5503
+ if (next.kind === "deferred" && next.routingReview) {
5504
+ if (!immediate || settlementReported || routingClaims > 0 || !deps.isHistoricalSource?.({ updateId: update.update_id, signal })) {
5505
+ throw new TelegramUpdateAdmissionOutcomeError("Historical review cannot override accepted or selected work.");
5506
+ }
5507
+ suspended = true;
5508
+ }
5509
+ if (next.kind !== "deferred") settlementReported = true;
4672
5510
  if (immediate) {
4673
5511
  outcome = mergeTelegramReportedAdmissionOutcome(
4674
5512
  outcome,
@@ -4683,13 +5521,14 @@ export function createTelegramUpdateAdmissionHandle<
4683
5521
  );
4684
5522
  }
4685
5523
  void Promise.resolve()
4686
- .then(() =>
4687
- deps.onLateOutcome!(next, {
5524
+ .then(() => {
5525
+ if (suspended || signal.aborted) return;
5526
+ return deps.onLateOutcome!(next, {
4688
5527
  updateId: update.update_id,
4689
5528
  ctx,
4690
5529
  signal,
4691
- }),
4692
- )
5530
+ });
5531
+ })
4693
5532
  .catch((error) => {
4694
5533
  try {
4695
5534
  deps.onLateOutcomeError?.(error, update.update_id);
@@ -4697,13 +5536,64 @@ export function createTelegramUpdateAdmissionHandle<
4697
5536
  // Diagnostic sinks must not create an unhandled late Promise.
4698
5537
  }
4699
5538
  });
4700
- }),
5539
+ }, { abandon: (authority) => {
5540
+ const isCurrent = () => !signal.aborted && routingClaims === 0 && !settlementReported && authority.isCurrent();
5541
+ if (immediate || outcome?.kind !== "deferred" || !deps.abandonDeferred || !isCurrent()) return undefined;
5542
+ if (abandoned) return { ...abandoned, duplicate: true };
5543
+ const wasSuspended = suspended;
5544
+ suspended = true;
5545
+ // Failed/unknown publication keeps dispatch suspended for exact cancellation retry.
5546
+ // An ineligible request that attempted no publication remains inert.
5547
+ const result = deps.abandonDeferred({ ...authority, updateId: update.update_id, signal, isCurrent });
5548
+ if (result) abandoned = result;
5549
+ else suspended = wasSuspended;
5550
+ return result;
5551
+ }, inspectAbandoning: request => execution.isCurrent()
5552
+ ? deps.inspectAbandoning?.({ ...request, signal }) : undefined,
5553
+ inspectHistorical: request => execution.isCurrent() ? deps.inspectHistorical?.({ ...request, signal }) : undefined,
5554
+ inspectSource: () => execution.isCurrent() && !immediate && !settlementReported
5555
+ ? deps.inspectDeferredSource?.({ updateId: update.update_id, signal }) : undefined,
5556
+ isHistorical: matchesOriginal => !signal.aborted && deps.isHistoricalSource?.({ updateId: update.update_id, signal,
5557
+ ...(matchesOriginal ? { matchesOriginal } : {}) }) === true,
5558
+ isHistoricalReviewHeld: () => !signal.aborted && suspended && outcome?.kind === "deferred" && outcome.routingReview === true,
5559
+ supportsAbandonment: journalBindingKey => !signal.aborted && !settlementReported &&
5560
+ deps.supportsDeferredAbandonment?.({ updateId: update.update_id, signal, journalBindingKey }) === true,
5561
+ armRoutingInput: (operatorUserId, sourceUpdateIds) => {
5562
+ execution.assertCurrent();
5563
+ if (settlementReported || routingClaims || suspended) return undefined;
5564
+ if (routingClockUnknown) throw new TelegramUpdateAdmissionOutcomeError("Telegram routing input clock publication is unconfirmed.");
5565
+ const ids = [...sourceUpdateIds];
5566
+ if (routingSourceIds && !isDeepStrictEqual(ids, routingSourceIds)) throw new TelegramUpdateAdmissionOutcomeError("Telegram routing input source membership changed.");
5567
+ try { routingLifetime = deps.armRoutingInput?.({ updateId: update.update_id, signal, operatorUserId, sourceUpdateIds: ids }); }
5568
+ catch (error) { routingClockUnknown = true; throw error; }
5569
+ if (routingLifetime) routingSourceIds = ids;
5570
+ return routingLifetime ? { ...routingLifetime } : undefined;
5571
+ },
5572
+ acquireRouting: (select = false, sourceUpdateIds) => {
5573
+ execution.assertCurrent();
5574
+ if (select && routingClockUnknown) throw new TelegramUpdateAdmissionOutcomeError("Telegram routing input clock publication is unconfirmed; selection is protected.");
5575
+ if (select && routingLifetime && !routingSelectionConfirmed) {
5576
+ if (routingSelectionAttempted) throw new TelegramUpdateAdmissionOutcomeError("Telegram routing selection is unconfirmed; no replay is permitted.");
5577
+ if (!isDeepStrictEqual(sourceUpdateIds ?? [update.update_id], routingSourceIds)) throw new TelegramUpdateAdmissionOutcomeError("Telegram routing input selection changed its source membership.");
5578
+ routingSelectionAttempted = true;
5579
+ routingSelectionConfirmed = deps.selectRoutingInput?.({ updateId: update.update_id, signal,
5580
+ operatorUserId: routingLifetime.operatorUserId, sourceUpdateIds: routingSourceIds! }) === true;
5581
+ if (!routingSelectionConfirmed) throw new TelegramUpdateAdmissionOutcomeError("Telegram routing input choice is expired or unavailable.");
5582
+ }
5583
+ routingClaims += 1;
5584
+ let released = false;
5585
+ return () => {
5586
+ if (released) return;
5587
+ released = true;
5588
+ routingClaims -= 1;
5589
+ };
5590
+ } }),
4701
5591
  execution,
4702
5592
  );
4703
5593
  try {
4704
5594
  execution.assertCurrent();
4705
5595
  await deps.defaultHandle(boundUpdate, ctx, execution);
4706
- execution.assertCurrent();
5596
+ if (outcome?.kind !== "deferred" || !outcome.routingReview || signal.aborted) execution.assertCurrent();
4707
5597
  } finally {
4708
5598
  immediate = false;
4709
5599
  }
@@ -4715,7 +5605,7 @@ export function createTelegramCustodiedUpdateAdmissionHandle<
4715
5605
  TUpdate extends TelegramJournaledUpdate & TelegramUpdateFlow,
4716
5606
  TContext,
4717
5607
  >(deps: Omit<TelegramUpdateAdmissionHandleDeps<TUpdate, TContext>,
4718
- "onLateOutcome" | "onLateOutcomeError"> & {
5608
+ "onLateOutcome" | "onLateOutcomeError" | "abandonDeferred" | "armRoutingInput" | "selectRoutingInput" | "supportsDeferredAbandonment" | "inspectAbandoning" | "inspectHistorical" | "inspectDeferredSource" | "isHistoricalSource"> & {
4719
5609
  journal: TelegramCustodyExecutionJournal;
4720
5610
  recipientBindingKey: string;
4721
5611
  onLateOutcomeError(error: unknown, updateId: number): void;
@@ -4755,6 +5645,8 @@ const isQueueReceiptCompletionAcknowledged = (receipt: TelegramQueueAdmissionRec
4755
5645
 
4756
5646
  export interface TelegramQueueAdmissionSettlementRuntime<TContext> {
4757
5647
  isItemReady: (item: TelegramQueueAdmissionItemLike) => boolean;
5648
+ getQueueReceiptSettlementOwner?: (receipt: TelegramQueueAdmissionReceiptLike, ctx: TContext,
5649
+ reason: TelegramQueueReceiptCompletionReason) => TelegramUpdateJournalQueueOwner | undefined;
4758
5650
  getQueueReceiptOwner: (
4759
5651
  receipt: TelegramQueueAdmissionReceiptLike,
4760
5652
  ) => TelegramUpdateJournalQueueOwner | undefined;
@@ -4782,15 +5674,20 @@ export function createTelegramQueueAdmissionSettlementMuxRuntime<TContext>(
4782
5674
  kind: "prompt" | "control" | "discard",
4783
5675
  ): boolean => {
4784
5676
  const plan = new Map<TelegramQueueAdmissionSettlementRuntime<TContext>, TelegramQueueAdmissionReceiptLike[]>();
5677
+ const reason = kind === "prompt" ? "prompt-handoff" : kind === "control" ? "control-settlement" : "discard";
5678
+ const settlementOwner = (runtime: TelegramQueueAdmissionSettlementRuntime<TContext>, receipt: TelegramQueueAdmissionReceiptLike) =>
5679
+ runtime.getQueueReceiptSettlementOwner ? runtime.getQueueReceiptSettlementOwner(receipt, ctx, reason) :
5680
+ runtime.isItemReady({ admissionReceipts: [receipt] }) ? runtime.getQueueReceiptOwner(receipt) : undefined;
4785
5681
  for (const receipt of items.flatMap(item => item.admissionReceipts ?? [])) {
4786
- if (kind === "discard" && isQueueReceiptCompletionAcknowledged(receipt)) continue;
4787
- const candidates = runtimes.filter(runtime => runtime.isItemReady({ admissionReceipts: [receipt] }));
5682
+ if (isQueueReceiptCompletionAcknowledged(receipt)) continue;
5683
+ const candidates = runtimes.filter(runtime => runtime.getQueueReceiptSettlementOwner ? !!settlementOwner(runtime, receipt)
5684
+ : runtime.isItemReady({ admissionReceipts: [receipt] }));
4788
5685
  const selected = candidates[0];
4789
5686
  if (!selected) return false;
4790
5687
  if (candidates.length > 1) {
4791
- const owner = selected.getQueueReceiptOwner(receipt);
5688
+ const owner = settlementOwner(selected, receipt);
4792
5689
  if (!owner || candidates.some(runtime => {
4793
- const candidate = runtime.getQueueReceiptOwner(receipt);
5690
+ const candidate = settlementOwner(runtime, receipt);
4794
5691
  return !candidate || !areTelegramUpdateJournalQueueOwnersEqual(owner, candidate);
4795
5692
  })) return false;
4796
5693
  }
@@ -4848,7 +5745,7 @@ export function createTelegramQueueAdmissionSettlementRuntime<TContext>(
4848
5745
  const receipts: TelegramQueueAdmissionReceiptLike[] = [];
4849
5746
  for (const item of items) {
4850
5747
  for (const receipt of item.admissionReceipts ?? []) {
4851
- if (reason !== "discard" || !isQueueReceiptCompletionAcknowledged(receipt)) receipts.push(receipt);
5748
+ if (!isQueueReceiptCompletionAcknowledged(receipt)) receipts.push(receipt);
4852
5749
  }
4853
5750
  }
4854
5751
  return receipts.length === 0 || worker.completeQueueReceipts({ receipts, ctx, reason });
@@ -4859,6 +5756,7 @@ export function createTelegramQueueAdmissionSettlementRuntime<TContext>(
4859
5756
  worker.isQueueReceiptCommitted,
4860
5757
  ),
4861
5758
  getQueueReceiptOwner: worker.getQueueReceiptOwner,
5759
+ ...(worker.getQueueReceiptSettlementOwner ? { getQueueReceiptSettlementOwner: worker.getQueueReceiptSettlementOwner } : {}),
4862
5760
  onPromptHandedOff: (item, ctx) =>
4863
5761
  complete([item], ctx, "prompt-handoff"),
4864
5762
  onControlSettled: (item, ctx) =>
@@ -5246,6 +6144,7 @@ export interface TelegramQueueHandoffReconciliationRuntimeAssemblyDeps<
5246
6144
  listFollowers: () => readonly TelegramBusFollowerView[];
5247
6145
  createRecipientJournalResolver: (
5248
6146
  profileKey: string,
6147
+ sessionId: string,
5249
6148
  ) => (() => { recoveryKey: string } | undefined);
5250
6149
  queueStore: {
5251
6150
  getQueuedItems: () => TelegramQueueItem<TContext>[];
@@ -5291,8 +6190,8 @@ export function createTelegramQueueHandoffReconciliationRuntimeAssembly<
5291
6190
  canHandoffWithLeader: deps.canHandoffWithLeader,
5292
6191
  listFollowers: deps.listFollowers,
5293
6192
  createRecipientJournalBindingKey(recipient) {
5294
- if (!recipient.profileKey) return undefined;
5295
- return deps.createRecipientJournalResolver(recipient.profileKey)()
6193
+ if (!recipient.profileKey || !recipient.sessionId) return undefined;
6194
+ return deps.createRecipientJournalResolver(recipient.profileKey, recipient.sessionId)()
5296
6195
  ?.recoveryKey;
5297
6196
  },
5298
6197
  getQueuedItems: deps.queueStore.getQueuedItems,
@@ -5502,6 +6401,8 @@ export interface TelegramUpdateAdmissionLifecycleRuntimeDeps<TContext> {
5502
6401
  acquireSourceReference?: (
5503
6402
  binding: TelegramUpdateAdmissionLifecycleJournalBinding,
5504
6403
  ) => () => void;
6404
+ /** Runs after the previous worker stopped and before the replacement worker exists; throwing refuses binding. */
6405
+ prepareBinding?: (binding: TelegramUpdateAdmissionLifecycleJournalBinding) => void;
5505
6406
  recordRuntimeEvent?: TelegramUpdateWorkerRuntimeDeps<TContext>["recordRuntimeEvent"];
5506
6407
  }
5507
6408
 
@@ -5558,8 +6459,8 @@ export interface TelegramUpdateAdmissionLifecycleRuntime<TContext>
5558
6459
 
5559
6460
  export interface TelegramUpdateWorkerOwnerRuntime<TContext> {
5560
6461
  getQueueOwnerIdentity: () => TelegramUpdateJournalQueueOwnerIdentity;
5561
- onQueueReceiptCommitted: (receipt: unknown, ctx: TContext) => void;
5562
- onUpdateCompleted: (updateId: number, ctx: TContext) => void;
6462
+ onQueueReceiptCommitted: (receipt: TelegramQueueAdmissionReceiptLike, ctx: TContext) => void;
6463
+ onUpdateCompleted: (updateId: number, ctx: TContext, journalBindingKey?: string) => void;
5563
6464
  }
5564
6465
 
5565
6466
  export interface TelegramUpdateWorkerOwnerRuntimeDeps<TContext> {
@@ -5570,7 +6471,8 @@ export interface TelegramUpdateWorkerOwnerRuntimeDeps<TContext> {
5570
6471
  isContextCurrent: (ctx: TContext) => boolean;
5571
6472
  dispatchNext: (ctx: TContext) => void;
5572
6473
  requestQueueHandoffReconciliation: (ctx: TContext) => void;
5573
- afterUpdateCompleted?: (updateId: number) => void;
6474
+ afterQueueReceiptCommitted?: (receipt: TelegramQueueAdmissionReceiptLike, ctx: TContext) => void;
6475
+ afterUpdateCompleted?: (updateId: number, ctx: TContext, journalBindingKey?: string) => void;
5574
6476
  }
5575
6477
 
5576
6478
  export function createTelegramUpdateWorkerOwnerRuntime<TContext>(
@@ -5585,15 +6487,16 @@ export function createTelegramUpdateWorkerOwnerRuntime<TContext>(
5585
6487
  sessionGeneration: deps.getSessionGeneration(),
5586
6488
  };
5587
6489
  },
5588
- onQueueReceiptCommitted(_receipt, ctx) {
6490
+ onQueueReceiptCommitted(receipt, ctx) {
5589
6491
  if (!deps.isContextCurrent(ctx)) return;
6492
+ deps.afterQueueReceiptCommitted?.(receipt, ctx);
5590
6493
  deps.dispatchNext(ctx);
5591
6494
  deps.requestQueueHandoffReconciliation(ctx);
5592
6495
  },
5593
- onUpdateCompleted(updateId, ctx) {
6496
+ onUpdateCompleted(updateId, ctx, journalBindingKey) {
5594
6497
  if (!deps.isContextCurrent(ctx)) return;
5595
6498
  deps.dispatchNext(ctx);
5596
- deps.afterUpdateCompleted?.(updateId);
6499
+ deps.afterUpdateCompleted?.(updateId, ctx, journalBindingKey);
5597
6500
  },
5598
6501
  };
5599
6502
  }
@@ -5625,6 +6528,7 @@ export function createTelegramUpdateAdmissionRuntimeBinding<TContext>(deps: {
5625
6528
  let follower: TelegramUpdateAdmissionLifecycleRuntime<TContext> | undefined;
5626
6529
  let settlement: TelegramQueueAdmissionSettlementRuntime<TContext> | undefined;
5627
6530
  let inputCustodyBus: TelegramInputCustodyBusBindingRuntime<TContext> | undefined;
6531
+ const getActive = () => deps.isFollowerRegistered() ? follower : leader;
5628
6532
  return {
5629
6533
  bind(input) {
5630
6534
  leader = input.leader;
@@ -5637,7 +6541,7 @@ export function createTelegramUpdateAdmissionRuntimeBinding<TContext>(deps: {
5637
6541
  },
5638
6542
  getLeader: () => leader,
5639
6543
  getFollower: () => follower,
5640
- getActive: () => (deps.isFollowerRegistered() ? follower : leader),
6544
+ getActive,
5641
6545
  getSettlement: () => settlement,
5642
6546
  getInputCustodyBus: () => inputCustodyBus,
5643
6547
  getLifecycleForJournalBinding(journalBindingKey) {
@@ -5743,6 +6647,7 @@ export function createTelegramUpdateAdmissionLifecycleRuntime<TContext>(
5743
6647
  binding.recoveryKey !== recoveryKey) {
5744
6648
  throw new Error("Telegram update source binding changed during startup.");
5745
6649
  }
6650
+ deps.prepareBinding?.(binding);
5746
6651
  const release = deps.acquireSourceReference?.(binding);
5747
6652
  const inputCustody = binding.journal.inputCustody;
5748
6653
  try {
@@ -6043,6 +6948,8 @@ export interface TelegramUpdateAdmissionLifecycleAssemblyDeps<
6043
6948
  isRegistered: () => boolean;
6044
6949
  getGeneration: () => string | undefined;
6045
6950
  prepareUpdateForExecution: (update: TUpdate) => TUpdate;
6951
+ /** Session succession before worker creation, under the exact generation-fenced binding. */
6952
+ prepareBinding?: (binding: TelegramUpdateAdmissionLifecycleJournalBinding & { hasAuthority?: () => boolean }) => void;
6046
6953
  };
6047
6954
  recordRuntimeEvent?: TelegramUpdateWorkerRuntimeDeps<TContext>["recordRuntimeEvent"];
6048
6955
  }
@@ -6073,6 +6980,14 @@ export function createTelegramUpdateAdmissionWorkerRuntime<
6073
6980
  const executeUpdate = createTelegramUpdateAdmissionHandle<TUpdate, TContext>({
6074
6981
  defaultHandle: deps.defaultHandle,
6075
6982
  registry: deps.registry,
6983
+ abandonDeferred: input => worker?.abandonDeferred?.(input),
6984
+ armRoutingInput: input => worker?.armRoutingInput?.(input),
6985
+ selectRoutingInput: input => worker?.selectRoutingInput?.(input) === true,
6986
+ supportsDeferredAbandonment: input => worker?.supportsDeferredAbandonment?.(input) === true,
6987
+ inspectAbandoning: input => worker?.inspectAbandoning?.(input),
6988
+ inspectHistorical: input => worker?.inspectHistorical?.(input),
6989
+ inspectDeferredSource: input => worker?.inspectDeferredSource?.(input),
6990
+ isHistoricalSource: input => worker?.isHistoricalSource?.(input) === true,
6076
6991
  onLateOutcome(outcome, details) {
6077
6992
  worker?.settleDeferred({
6078
6993
  updateId: details.updateId,
@@ -6155,6 +7070,7 @@ export function createTelegramUpdateAdmissionLifecycleAssembly<
6155
7070
  getQueueOwnerIdentity: deps.worker.getQueueOwnerIdentity,
6156
7071
  ...(deps.acquireSourceReference ? { acquireSourceReference: binding =>
6157
7072
  deps.acquireSourceReference!("follower", binding) } : {}),
7073
+ ...(deps.follower.prepareBinding ? { prepareBinding: deps.follower.prepareBinding } : {}),
6158
7074
  resolveBinding() {
6159
7075
  const generation = deps.follower.getGeneration();
6160
7076
  if (!deps.follower.isRegistered() || !generation) return undefined;
@@ -6179,6 +7095,11 @@ export function createTelegramUpdateAdmissionLifecycleAssembly<
6179
7095
  getJournalBindingKey: () => binding.recoveryKey,
6180
7096
  getRecipientBindingKey: () => binding.recipientBindingKey,
6181
7097
  hasAuthority: () => binding.hasAuthority?.() ?? false,
7098
+ // Recipient custody is not an orphaned direct-bot source.
7099
+ shouldReviewHistoricalInput: undefined,
7100
+ shouldHoldPendingInput: undefined,
7101
+ spendHistoricalInput: undefined,
7102
+ onHeldSourcesPrepared: undefined,
6182
7103
  prepareUpdateForExecution: deps.follower.prepareUpdateForExecution,
6183
7104
  });
6184
7105
  },