@llblab/pi-telegram 0.51.6 → 0.52.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/AGENTS.md +15 -12
  2. package/BACKLOG.md +16 -9
  3. package/CHANGELOG.md +19 -0
  4. package/LICENSE +21 -0
  5. package/README.md +17 -6
  6. package/dist/lib/activity-verbosity.d.ts +1 -5
  7. package/dist/lib/activity-verbosity.js +5 -7
  8. package/dist/lib/bindings.d.ts +5 -1
  9. package/dist/lib/bindings.js +7 -1
  10. package/dist/lib/bus-api.js +8 -5
  11. package/dist/lib/bus-follower.d.ts +54 -56
  12. package/dist/lib/bus-follower.js +206 -139
  13. package/dist/lib/bus-leader.d.ts +21 -2
  14. package/dist/lib/bus-leader.js +181 -25
  15. package/dist/lib/bus-transport.d.ts +4 -1
  16. package/dist/lib/bus-transport.js +28 -4
  17. package/dist/lib/bus.d.ts +52 -56
  18. package/dist/lib/bus.js +78 -246
  19. package/dist/lib/channel-posts.d.ts +1 -3
  20. package/dist/lib/channel-posts.js +23 -26
  21. package/dist/lib/command-templates.d.ts +0 -5
  22. package/dist/lib/command-templates.js +5 -5
  23. package/dist/lib/commands.d.ts +0 -23
  24. package/dist/lib/commands.js +15 -15
  25. package/dist/lib/config.d.ts +0 -17
  26. package/dist/lib/config.js +12 -14
  27. package/dist/lib/delivery.d.ts +0 -4
  28. package/dist/lib/delivery.js +2 -5
  29. package/dist/lib/extension.js +258 -61
  30. package/dist/lib/generative-apps.d.ts +0 -19
  31. package/dist/lib/inbound.d.ts +0 -2
  32. package/dist/lib/inbound.js +2 -2
  33. package/dist/lib/journal.d.ts +183 -16
  34. package/dist/lib/journal.js +1175 -248
  35. package/dist/lib/keyboard.d.ts +0 -2
  36. package/dist/lib/keyboard.js +2 -2
  37. package/dist/lib/lifecycle.js +1 -1
  38. package/dist/lib/locks.d.ts +100 -12
  39. package/dist/lib/locks.js +380 -14
  40. package/dist/lib/logging.d.ts +30 -12
  41. package/dist/lib/logging.js +129 -72
  42. package/dist/lib/media.d.ts +28 -5
  43. package/dist/lib/media.js +26 -8
  44. package/dist/lib/menu-model.d.ts +0 -11
  45. package/dist/lib/menu-model.js +8 -8
  46. package/dist/lib/menu-settings.d.ts +0 -18
  47. package/dist/lib/menu-settings.js +18 -18
  48. package/dist/lib/menu-status.d.ts +4 -4
  49. package/dist/lib/menu-status.js +12 -7
  50. package/dist/lib/menu-thinking.d.ts +1 -1
  51. package/dist/lib/menu-thinking.js +1 -1
  52. package/dist/lib/menu.d.ts +1 -0
  53. package/dist/lib/menu.js +4 -3
  54. package/dist/lib/outbound-attachments.d.ts +0 -1
  55. package/dist/lib/outbound-attachments.js +1 -1
  56. package/dist/lib/outbound-buttons.js +1 -8
  57. package/dist/lib/outbound-markup.d.ts +1 -6
  58. package/dist/lib/outbound-markup.js +4 -21
  59. package/dist/lib/outbound.d.ts +0 -8
  60. package/dist/lib/outbound.js +5 -5
  61. package/dist/lib/paths.d.ts +50 -7
  62. package/dist/lib/paths.js +155 -17
  63. package/dist/lib/pi.d.ts +1 -0
  64. package/dist/lib/pi.js +2 -1
  65. package/dist/lib/polling.d.ts +0 -5
  66. package/dist/lib/polling.js +5 -5
  67. package/dist/lib/preview.d.ts +0 -3
  68. package/dist/lib/preview.js +3 -3
  69. package/dist/lib/prompt-templates.d.ts +0 -1
  70. package/dist/lib/prompt-templates.js +1 -1
  71. package/dist/lib/queue.d.ts +5 -15
  72. package/dist/lib/queue.js +94 -11
  73. package/dist/lib/recovery.d.ts +29 -9
  74. package/dist/lib/recovery.js +104 -33
  75. package/dist/lib/replies.d.ts +0 -4
  76. package/dist/lib/replies.js +1 -1
  77. package/dist/lib/routing.d.ts +74 -8
  78. package/dist/lib/routing.js +2280 -307
  79. package/dist/lib/sections.d.ts +0 -2
  80. package/dist/lib/sections.js +1 -1
  81. package/dist/lib/status.d.ts +44 -9
  82. package/dist/lib/status.js +149 -27
  83. package/dist/lib/sync.d.ts +0 -5
  84. package/dist/lib/sync.js +5 -4
  85. package/dist/lib/telegram-api.d.ts +13 -3
  86. package/dist/lib/telegram-api.js +38 -23
  87. package/dist/lib/thread-cleanup-manager.d.ts +1 -0
  88. package/dist/lib/thread-cleanup-manager.js +18 -21
  89. package/dist/lib/thread-naming.d.ts +32 -5
  90. package/dist/lib/thread-naming.js +191 -7
  91. package/dist/lib/thread-reconciler.d.ts +2 -0
  92. package/dist/lib/thread-reconciler.js +1 -1
  93. package/dist/lib/threads.d.ts +293 -59
  94. package/dist/lib/threads.js +1689 -369
  95. package/dist/lib/turns.d.ts +0 -1
  96. package/dist/lib/turns.js +1 -1
  97. package/dist/lib/updates.d.ts +191 -28
  98. package/dist/lib/updates.js +1048 -114
  99. package/dist/lib/wire.d.ts +12 -0
  100. package/dist/lib/wire.js +21 -0
  101. package/dist/lib/workspace-admission.d.ts +25 -17
  102. package/dist/lib/workspace-admission.js +95 -20
  103. package/dist/lib/workspace-identity.d.ts +20 -0
  104. package/dist/lib/workspace-identity.js +103 -0
  105. package/dist/lib/workspace-retirement.d.ts +43 -8
  106. package/dist/lib/workspace-retirement.js +148 -52
  107. package/dist/package.json +6 -6
  108. package/dist/skills/generated-control-surface/SKILL.md +3 -3
  109. package/dist/skills/telegram-bridge/references/diagnosis.md +3 -3
  110. package/docs/README.md +1 -1
  111. package/docs/activity.md +1 -1
  112. package/docs/architecture.md +156 -36
  113. package/docs/callback-namespaces.md +1 -1
  114. package/docs/delivery.md +1 -1
  115. package/docs/generative-apps.md +2 -2
  116. package/docs/inbound.md +1 -1
  117. package/docs/multi-instance-bus.md +125 -19
  118. package/docs/public-api.md +3 -1
  119. package/docs/ui-style.md +23 -9
  120. package/docs/updates.md +1 -1
  121. package/lib/activity-verbosity.ts +5 -9
  122. package/lib/bindings.ts +7 -0
  123. package/lib/bus-api.ts +9 -4
  124. package/lib/bus-follower.ts +243 -224
  125. package/lib/bus-leader.ts +178 -31
  126. package/lib/bus-transport.ts +30 -8
  127. package/lib/bus.ts +110 -334
  128. package/lib/channel-posts.ts +29 -29
  129. package/lib/command-templates.ts +5 -5
  130. package/lib/commands.ts +15 -15
  131. package/lib/config.ts +12 -17
  132. package/lib/delivery.ts +2 -8
  133. package/lib/extension.ts +261 -72
  134. package/lib/generative-apps.ts +0 -22
  135. package/lib/inbound.ts +2 -2
  136. package/lib/journal.ts +1250 -325
  137. package/lib/keyboard.ts +2 -2
  138. package/lib/lifecycle.ts +1 -1
  139. package/lib/locks.ts +402 -23
  140. package/lib/logging.ts +151 -101
  141. package/lib/media.ts +51 -9
  142. package/lib/menu-model.ts +8 -8
  143. package/lib/menu-settings.ts +18 -18
  144. package/lib/menu-status.ts +11 -0
  145. package/lib/menu-thinking.ts +2 -2
  146. package/lib/menu.ts +5 -1
  147. package/lib/outbound-attachments.ts +1 -1
  148. package/lib/outbound-buttons.ts +1 -10
  149. package/lib/outbound-markup.ts +5 -24
  150. package/lib/outbound.ts +5 -5
  151. package/lib/paths.ts +177 -17
  152. package/lib/pi.ts +5 -2
  153. package/lib/polling.ts +5 -5
  154. package/lib/preview.ts +3 -3
  155. package/lib/prompt-templates.ts +1 -1
  156. package/lib/queue.ts +93 -12
  157. package/lib/recovery.ts +104 -44
  158. package/lib/replies.ts +1 -1
  159. package/lib/routing.ts +2010 -375
  160. package/lib/sections.ts +1 -1
  161. package/lib/status.ts +156 -36
  162. package/lib/sync.ts +4 -4
  163. package/lib/telegram-api.ts +53 -33
  164. package/lib/thread-cleanup-manager.ts +24 -21
  165. package/lib/thread-naming.ts +267 -9
  166. package/lib/thread-reconciler.ts +3 -1
  167. package/lib/threads.ts +1736 -488
  168. package/lib/turns.ts +1 -1
  169. package/lib/updates.ts +1094 -151
  170. package/lib/wire.ts +28 -0
  171. package/lib/workspace-admission.ts +97 -47
  172. package/lib/workspace-identity.ts +147 -0
  173. package/lib/workspace-retirement.ts +188 -89
  174. package/package.json +6 -6
  175. package/scripts/audit-exports.mjs +100 -0
  176. package/scripts/check-downgrade.mjs +80 -43
  177. package/skills/generated-control-surface/SKILL.md +3 -3
  178. package/skills/telegram-bridge/references/diagnosis.md +3 -3
package/lib/updates.ts CHANGED
@@ -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,13 @@ 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,
59
+ type TelegramRoutingInputExpiryResult,
42
60
  type TelegramUpdateJournalQueueDiscardResult,
43
61
  type TelegramUpdateJournalQueueHandoffAcceptResult,
44
62
  type TelegramUpdateJournalQueueHandoffCancelResult,
@@ -134,30 +152,6 @@ function getTelegramReactionEmoji(
134
152
  return candidates.find((emoji) => emojis.has(emoji));
135
153
  }
136
154
 
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
155
  function getTelegramQueueReactionTransition(
162
156
  oldReactions: TelegramReactionType[],
163
157
  newReactions: TelegramReactionType[],
@@ -283,6 +277,8 @@ export interface TelegramGuestMessage {
283
277
  from?: TelegramUser;
284
278
  message_id?: number;
285
279
  text?: string;
280
+ /** Text sent together with a photo, document or other media. */
281
+ caption?: string;
286
282
  reply_to_message?: TelegramUpdateMessage;
287
283
  }
288
284
 
@@ -405,7 +401,7 @@ type TelegramForeignUpdateSettlementFailure =
405
401
  sourceUpdateId?: number;
406
402
  };
407
403
 
408
- export class TelegramForeignUpdateSettlementError extends Error {
404
+ class TelegramForeignUpdateSettlementError extends Error {
409
405
  readonly settlement: TelegramForeignUpdateSettlementFailure;
410
406
 
411
407
  constructor(
@@ -463,8 +459,8 @@ export interface TelegramUpdateFlow
463
459
  }
464
460
 
465
461
  export type TelegramUpdateAdmissionOutcome =
466
- | { kind: "complete" }
467
- | { kind: "deferred" }
462
+ | { kind: "complete"; expectedSource?: TelegramDeferredSourceEvidence }
463
+ | { kind: "deferred"; routingReview?: true }
468
464
  | {
469
465
  kind: "queued";
470
466
  queueKind: "prompt" | "control";
@@ -481,9 +477,63 @@ const TELEGRAM_UPDATE_ADMISSION_BINDING = Symbol(
481
477
  "telegram.update-admission.binding",
482
478
  );
483
479
 
480
+ export type TelegramDeferredUpdateAbandonmentAuthority = Pick<
481
+ TelegramUpdateJournalPendingAbandonmentInput, "operatorAuthorityId" | "isCurrent"
482
+ >;
483
+
484
+ export interface TelegramDeferredAbandonmentRecoveryRequest {
485
+ journalBindingKey: string;
486
+ afterUpdateId?: number;
487
+ isCurrent: () => boolean;
488
+ }
489
+
490
+ const TELEGRAM_HELD_SOURCE_INSPECTION_LIMIT = 20;
491
+
492
+ /** A generation-owned post-drain hint for the new-world restart; never execution, completion or deletion authority by itself. */
493
+ export interface TelegramHeldSourcePreparation<TContext> {
494
+ ctx: TContext;
495
+ journalBindingKey: string;
496
+ /** Exact prepared worker owner, context, key and authority; the consumer adds its own domain fences. */
497
+ isCurrent: () => boolean;
498
+ signal: AbortSignal;
499
+ /** Chooser-clock sources that must survive cold spending until their fixed expiry. */
500
+ routingSourceIds?: readonly number[];
501
+ }
502
+
503
+ export interface TelegramDeferredAbandonmentRecoveryPage {
504
+ sources: {
505
+ /** Detached original evidence, not proof of current eligibility or archive integrity. */
506
+ original: TelegramUpdateJournalEntry;
507
+ retry: (authority: TelegramDeferredUpdateAbandonmentAuthority) =>
508
+ TelegramUpdateJournalPendingAbandonmentResult | undefined;
509
+ }[];
510
+ nextAfterUpdateId?: number;
511
+ }
512
+
513
+ type TelegramHistoricalInputPredicate = (original: TelegramUpdateJournalEntry) => boolean;
514
+
515
+ /** Read-only exact-source observation by its live worker; not an acceptance or removal grant. */
516
+ export interface TelegramDeferredSourceEvidence extends TelegramUpdateJournalEntryDigest {
517
+ journalBindingKey: string;
518
+ /** Supplied only after caller-owned acceptance publication; requires a durable removal ACK. */
519
+ completionSha256?: string;
520
+ }
521
+
484
522
  interface TelegramUpdateAdmissionBinding {
485
523
  sourceUpdateId: number;
486
524
  report: (outcome: TelegramUpdateAdmissionOutcome) => void;
525
+ abandon?: (authority: TelegramDeferredUpdateAbandonmentAuthority) =>
526
+ TelegramUpdateJournalPendingAbandonmentResult | undefined;
527
+ armRoutingInput?: (operatorUserId: number, sourceUpdateIds: readonly number[]) => TelegramUpdateJournalRoutingInput | undefined;
528
+ acquireRouting?: (select?: boolean, sourceUpdateIds?: readonly number[]) => () => void;
529
+ inspectSource?: () => TelegramDeferredSourceEvidence | undefined;
530
+ supportsAbandonment?: (journalBindingKey: string) => boolean;
531
+ inspectAbandoning?: (request: TelegramDeferredAbandonmentRecoveryRequest) =>
532
+ TelegramDeferredAbandonmentRecoveryPage | undefined;
533
+ inspectHistorical?: (request: TelegramDeferredAbandonmentRecoveryRequest) =>
534
+ TelegramDeferredAbandonmentRecoveryPage | undefined;
535
+ isHistorical?: (matchesOriginal?: TelegramHistoricalInputPredicate) => boolean;
536
+ isHistoricalReviewHeld?: () => boolean;
487
537
  }
488
538
 
489
539
  export type TelegramQueueAdmissionReceiptLike = TelegramQueueAdmissionReceipt;
@@ -520,6 +570,7 @@ export function bindTelegramUpdateAdmissionSource<
520
570
  >(
521
571
  update: TUpdate,
522
572
  report: TelegramUpdateAdmissionBinding["report"],
573
+ controls?: Pick<TelegramUpdateAdmissionBinding, "abandon" | "armRoutingInput" | "acquireRouting" | "inspectSource" | "supportsAbandonment" | "inspectAbandoning" | "inspectHistorical" | "isHistorical" | "isHistoricalReviewHeld">,
523
574
  ): TUpdate {
524
575
  if (!Number.isSafeInteger(update.update_id) || update.update_id < 0) {
525
576
  throw new TelegramUpdateAdmissionOutcomeError(
@@ -529,6 +580,7 @@ export function bindTelegramUpdateAdmissionSource<
529
580
  const binding: TelegramUpdateAdmissionBinding = {
530
581
  sourceUpdateId: update.update_id,
531
582
  report,
583
+ ...controls,
532
584
  };
533
585
  const callbackQuery = update.callback_query
534
586
  ? bindTelegramUpdateAdmissionCarrier(
@@ -575,6 +627,26 @@ export function bindTelegramUpdateAdmissionSource<
575
627
  } as TUpdate;
576
628
  }
577
629
 
630
+ /**
631
+ * Conservative read-only census: any retained entry naming this Thread, in any nested Telegram object or state,
632
+ * is unresolved custody. It cannot see updates still in transit before journal append.
633
+ */
634
+ export function collectTelegramJournalThreadUpdateIds(
635
+ entries: readonly { updateId: number; update: unknown }[],
636
+ target: { chatId: number; threadId: number },
637
+ ): number[] {
638
+ const matches = (value: unknown, depth: number): boolean => {
639
+ if (depth > 8 || !value || typeof value !== "object") return false;
640
+ const record = value as Record<string, unknown>;
641
+ if (record.message_thread_id === target.threadId) {
642
+ const chat = record.chat as { id?: unknown } | undefined;
643
+ if (!chat || typeof chat !== "object" || chat.id === target.chatId) return true;
644
+ }
645
+ return Object.values(record).some(child => matches(child, depth + 1));
646
+ };
647
+ return entries.filter(entry => matches(entry.update, 0)).map(entry => entry.updateId).sort((left, right) => left - right);
648
+ }
649
+
578
650
  export function collectTelegramAdmissionSourceUpdateIds(
579
651
  values: readonly unknown[],
580
652
  ): number[] {
@@ -586,29 +658,132 @@ export function collectTelegramAdmissionSourceUpdateIds(
586
658
  return [...sourceUpdateIds].sort((left, right) => left - right);
587
659
  }
588
660
 
661
+ /** Observe the original journal entry, never the mutable routed carrier, through current worker authority. */
662
+ export function inspectTelegramDeferredSource(value: unknown): TelegramDeferredSourceEvidence | undefined {
663
+ const execution = getTelegramUpdateExecutionFence(value);
664
+ if (execution?.isCurrent() !== true) return undefined;
665
+ return getTelegramUpdateAdmissionBinding(value)?.inspectSource?.();
666
+ }
667
+
668
+ /** A route-owned pre-disposition acceptance publisher; wraps only this carrier, never the shared worker binding. */
669
+ export function bindTelegramUpdateCompletionAcceptance<TValue>(value: TValue,
670
+ publish: () => TelegramDeferredSourceEvidence): TValue {
671
+ const binding = getTelegramUpdateAdmissionBinding(value);
672
+ if (!binding || getTelegramUpdateExecutionFence(value)?.isCurrent() !== true) {
673
+ throw new TelegramUpdateAdmissionOutcomeError("Telegram completion acceptance requires a live source carrier.");
674
+ }
675
+ let retained: TelegramDeferredSourceEvidence | undefined;
676
+ let queued = false;
677
+ const scoped: TelegramUpdateAdmissionBinding = { ...binding, report(outcome) {
678
+ if (outcome.kind !== "complete") {
679
+ if (outcome.kind === "queued") queued = true;
680
+ binding.report(outcome); return;
681
+ }
682
+ assertTelegramUpdateExecutionCurrent(value);
683
+ // A command's trailing implicit completion cannot dispose receipt-owned or uncertain queued work.
684
+ if (queued) {
685
+ if (outcome.expectedSource) throw new TelegramUpdateAdmissionOutcomeError("Telegram completion acceptance cannot dispose a queued source.");
686
+ return;
687
+ }
688
+ if (!retained) {
689
+ const published = validateTelegramUpdateAdmissionOutcome({ kind: "complete", expectedSource: publish() },
690
+ binding.sourceUpdateId, new Set([binding.sourceUpdateId]));
691
+ if (published.kind !== "complete" || !published.expectedSource?.completionSha256) {
692
+ throw new TelegramUpdateAdmissionOutcomeError("Telegram completion acceptance lacks its scoped journal proof.");
693
+ }
694
+ retained = published.expectedSource;
695
+ }
696
+ assertTelegramUpdateExecutionCurrent(value);
697
+ if (outcome.expectedSource && (outcome.expectedSource.updateId !== retained.updateId ||
698
+ outcome.expectedSource.journalBindingKey !== retained.journalBindingKey || outcome.expectedSource.sourceSha256 !== retained.sourceSha256 ||
699
+ outcome.expectedSource.completionSha256 !== undefined && outcome.expectedSource.completionSha256 !== retained.completionSha256)) {
700
+ throw new TelegramUpdateAdmissionOutcomeError("Telegram completion acceptance conflicts with its reported source.");
701
+ }
702
+ binding.report({ kind: "complete", expectedSource: { ...retained } });
703
+ } };
704
+ return carryTelegramUpdateExecutionFence(value, bindTelegramUpdateAdmissionCarrier(value, scoped)!);
705
+ }
706
+
589
707
  /** Report source completion; true means reported, not a durable settlement acknowledgement. */
590
- export function reportTelegramUpdateCompleted(value: unknown): boolean {
708
+ export function reportTelegramUpdateCompleted(value: unknown, expectedSource?: TelegramDeferredSourceEvidence): boolean {
591
709
  const binding = getTelegramUpdateAdmissionBinding(value);
592
710
  if (!binding) return false;
593
711
  const execution = getTelegramUpdateExecutionFence(value);
594
712
  if (execution && !execution.isCurrent()) return false;
595
- binding.report({ kind: "complete" });
713
+ binding.report(validateTelegramUpdateAdmissionOutcome({ kind: "complete", ...(expectedSource !== undefined ? { expectedSource } : {}) },
714
+ binding.sourceUpdateId, new Set([binding.sourceUpdateId])));
596
715
  return true;
597
716
  }
598
717
 
599
718
  export function reportTelegramUpdateDeferred(value: unknown): boolean {
600
719
  const binding = getTelegramUpdateAdmissionBinding(value);
601
720
  if (!binding) return false;
721
+ const execution = getTelegramUpdateExecutionFence(value);
722
+ if (execution && !execution.isCurrent()) return false;
602
723
  binding.report({ kind: "deferred" });
603
724
  return true;
604
725
  }
605
726
 
727
+ /** Exact source-bound cancellation; undefined means no eligible attempt was made. */
728
+ export function abandonTelegramDeferredUpdate(
729
+ value: unknown,
730
+ authority: TelegramDeferredUpdateAbandonmentAuthority,
731
+ ): TelegramUpdateJournalPendingAbandonmentResult | undefined {
732
+ return getTelegramUpdateAdmissionBinding(value)?.abandon?.(authority);
733
+ }
734
+
735
+ /** Capability for chooser publication; the eventual action still requires exact deferred authority. */
736
+ export function supportsTelegramDeferredAbandonment(value: unknown, journalBindingKey: string): boolean {
737
+ return getTelegramUpdateAdmissionBinding(value)?.supportsAbandonment?.(journalBindingKey) === true;
738
+ }
739
+
740
+ /** Observe protected attempts through a current carrier; the UI still owns human authorization. */
741
+ export function inspectTelegramAbandoningUpdates(value: unknown, request: TelegramDeferredAbandonmentRecoveryRequest):
742
+ TelegramDeferredAbandonmentRecoveryPage | undefined {
743
+ return getTelegramUpdateAdmissionBinding(value)?.inspectAbandoning?.(request);
744
+ }
745
+
746
+ /** Historical inspection is not human confirmation or permission to stop delivery. */
747
+ export function inspectTelegramHistoricalInputs(value: unknown, request: TelegramDeferredAbandonmentRecoveryRequest):
748
+ TelegramDeferredAbandonmentRecoveryPage | undefined {
749
+ return getTelegramUpdateAdmissionBinding(value)?.inspectHistorical?.(request);
750
+ }
751
+
752
+ export function isTelegramHistoricalInput(value: unknown, matchesOriginal?: TelegramHistoricalInputPredicate): boolean {
753
+ return getTelegramUpdateAdmissionBinding(value)?.isHistorical?.(matchesOriginal) === true;
754
+ }
755
+
756
+ /** Routing's last-boundary hold when ownership changed after early classification. */
757
+ export function reportTelegramHistoricalRoutingReview(value: unknown): boolean {
758
+ if (!isTelegramHistoricalInput(value) || getTelegramUpdateExecutionFence(value)?.isCurrent() !== true) return false;
759
+ getTelegramUpdateAdmissionBinding(value)!.report({ kind: "deferred", routingReview: true });
760
+ return true;
761
+ }
762
+
763
+ /** Arm only a positively published chooser; the journal owns the immutable hour deadline. */
764
+ export function armTelegramRoutingInputs(values: readonly unknown[], operatorUserId: number): TelegramUpdateJournalRoutingInput | undefined {
765
+ const first = values[0], execution = getTelegramUpdateExecutionFence(first);
766
+ if (!execution || values.some(value => getTelegramUpdateExecutionFence(value)?.signal !== execution.signal)) return undefined;
767
+ for (const value of values) assertTelegramUpdateExecutionCurrent(value);
768
+ const ids = collectTelegramAdmissionSourceUpdateIds(values);
769
+ if (!ids.length) return undefined;
770
+ return getTelegramUpdateAdmissionBinding(first)?.armRoutingInput?.(operatorUserId, ids);
771
+ }
772
+
773
+ /** Reserve source execution; an actual choice must also freeze its durable TTL before any effect. */
774
+ export function acquireTelegramUpdateRouting(value: unknown, select = false, sourceUpdateIds?: readonly number[]): () => void {
775
+ assertTelegramUpdateExecutionCurrent(value);
776
+ return getTelegramUpdateAdmissionBinding(value)?.acquireRouting?.(select, sourceUpdateIds) ?? (() => {});
777
+ }
778
+
606
779
  export function reportTelegramQueueAdmission(
607
780
  values: readonly unknown[],
608
781
  receipts: readonly TelegramQueueAdmissionReceiptLike[],
609
782
  ): boolean {
610
783
  const bindings = new Map<number, TelegramUpdateAdmissionBinding>();
611
784
  for (const value of values) {
785
+ const execution = getTelegramUpdateExecutionFence(value);
786
+ if (execution && !execution.isCurrent()) return false;
612
787
  const binding = getTelegramUpdateAdmissionBinding(value);
613
788
  if (!binding) continue;
614
789
  const existing = bindings.get(binding.sourceUpdateId);
@@ -1122,10 +1297,12 @@ function getForeignTelegramCallbackOwnership(
1122
1297
  getTargetOwnership?: TelegramTargetOwnershipLookup;
1123
1298
  },
1124
1299
  ): TelegramMessageOwnershipView | undefined {
1125
- const messageOwnership = getForeignTelegramMessageOwnership(
1126
- getTelegramCallbackMessageTarget(query),
1127
- deps,
1128
- );
1300
+ const messageTarget = getTelegramCallbackMessageTarget(query);
1301
+ const currentInstanceId = deps.getCurrentInstanceId?.();
1302
+ const knownOwner = messageTarget && deps.getMessageOwnership?.(messageTarget.chatId, messageTarget.messageId);
1303
+ // A locally published chooser stays local when Restore moves its Thread to a follower.
1304
+ if (currentInstanceId && knownOwner?.instanceId === currentInstanceId) return undefined;
1305
+ const messageOwnership = getForeignTelegramMessageOwnership(messageTarget, deps);
1129
1306
  const targetOwnership = getForeignTelegramTargetOwnership(
1130
1307
  query.message ? getTelegramMessageTarget(query.message) : undefined,
1131
1308
  deps,
@@ -1621,7 +1798,10 @@ export async function executeTelegramUpdatePlan<
1621
1798
  ) {
1622
1799
  assertExecutionCurrent();
1623
1800
  await deps.handleUnboundTelegramTopicMessage(plan.message, deps.ctx);
1624
- assertExecutionCurrent();
1801
+ // A terminal historical hold intentionally suspends this exact carrier.
1802
+ // Do not turn its acknowledged deferral into a retryable execution failure.
1803
+ if (getTelegramUpdateExecutionFence(plan.message) !== deps.execution ||
1804
+ getTelegramUpdateAdmissionBinding(plan.message)?.isHistoricalReviewHeld?.() !== true) assertExecutionCurrent();
1625
1805
  return;
1626
1806
  }
1627
1807
  if (plan.kind === "edited-message") {
@@ -1640,9 +1820,9 @@ export async function executeTelegramUpdatePlan<
1640
1820
 
1641
1821
  // --- Durable update worker ---
1642
1822
 
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;
1823
+ const TELEGRAM_UPDATE_RETRY_BASE_DELAY_MS = 1_000;
1824
+ const TELEGRAM_UPDATE_RETRY_MAX_DELAY_MS = 60_000;
1825
+ const TELEGRAM_UPDATE_WORKER_BATCH_SIZE = 64;
1646
1826
 
1647
1827
  export type TelegramUpdateWorkerPhase =
1648
1828
  | "stopped"
@@ -1679,6 +1859,10 @@ export interface TelegramUpdateWorkerStateSnapshot {
1679
1859
  journalSerializedBytes: number;
1680
1860
  oldestAdmittedAtMs?: number;
1681
1861
  deferredClaimCount: number;
1862
+ /** Protected cancellation attempts, not proof of a committed discard. */
1863
+ abandoningClaimCount?: number;
1864
+ /** Historical sources held before routing, not evidence of prior non-delivery. */
1865
+ historicalClaimCount?: number;
1682
1866
  queuedClaimCount: number;
1683
1867
  foreignQueuedCount: number;
1684
1868
  foreignQueuedOwner?: TelegramUpdateJournalQueueOwner;
@@ -1712,6 +1896,7 @@ export interface TelegramUpdateWorkerJournalSnapshot {
1712
1896
  update: TelegramJournaledUpdate;
1713
1897
  readonly preApprovalExcluded?: boolean;
1714
1898
  admittedAtMs: number;
1899
+ routingInput?: TelegramUpdateJournalRoutingInput;
1715
1900
  state: "pending" | "retry-wait" | "queued" | "failed";
1716
1901
  inputClaim?: TelegramUpdateJournalInputClaim;
1717
1902
  queueKind?: "prompt" | "control";
@@ -1736,13 +1921,27 @@ export interface TelegramUpdateWorkerJournalSnapshot {
1736
1921
  serializedBytes: number;
1737
1922
  }
1738
1923
 
1924
+ export interface TelegramRoutingInputExpirySource {
1925
+ original: TelegramUpdateJournalEntry;
1926
+ journalBindingKey: string;
1927
+ isCurrent(): boolean;
1928
+ expire(): TelegramRoutingInputExpiryResult | undefined;
1929
+ }
1930
+
1739
1931
  export interface TelegramUpdateWorkerJournalPort {
1932
+ routingInputs?: TelegramRoutingInputJournal;
1740
1933
  read: () => TelegramUpdateWorkerJournalSnapshot;
1934
+ inspectPendingRetention?: (entry: TelegramUpdateJournalEntry) =>
1935
+ TelegramUpdateJournalPendingRetentionEvidence | undefined;
1936
+ abandonPending?: (input: TelegramUpdateJournalPendingAbandonmentInput) =>
1937
+ TelegramUpdateJournalPendingAbandonmentResult;
1741
1938
  /** Optional strict observation; it must not recover, repair or grant new receipt authority. */
1742
1939
  isQueueReceiptCurrent?: (
1743
1940
  receipt: TelegramQueueAdmissionReceiptLike,
1744
1941
  owner: TelegramUpdateJournalQueueOwner,
1745
1942
  ) => boolean;
1943
+ /** Strict full-group origin observation for prepared partial scopes; no writer or readiness acquisition. */
1944
+ inspectQueuedReceipt?: (expected: TelegramUpdateJournalQueuedCompletion) => TelegramUpdateJournalQueuedReceiptEvidence | undefined;
1746
1945
  markQueued: (receipt: {
1747
1946
  queueKind: "prompt" | "control";
1748
1947
  receiptId: string;
@@ -1763,6 +1962,11 @@ export interface TelegramUpdateWorkerJournalPort {
1763
1962
  ) => {
1764
1963
  removedUpdateIds: readonly number[];
1765
1964
  };
1965
+ completeQueuedExact?: (receipts: readonly {
1966
+ queueKind: "prompt" | "control"; receiptId: string; sourceUpdateIds: readonly number[]; queueOwner: TelegramUpdateJournalQueueOwner;
1967
+ }[], completions: readonly TelegramUpdateJournalSourceCompletion[]) => {
1968
+ removedUpdateIds: readonly number[]; sourceCompletions?: readonly TelegramUpdateJournalSourceCompletion[];
1969
+ };
1766
1970
  markExecutionFailure: (input: {
1767
1971
  updateId: number;
1768
1972
  expectedAttemptCount: number;
@@ -1778,6 +1982,32 @@ export interface TelegramUpdateWorkerJournalPort {
1778
1982
  removeCompleted: (updateIds: readonly number[]) => {
1779
1983
  removedUpdateIds: readonly number[];
1780
1984
  };
1985
+ /** No ID-only fallback is allowed for a guarded completion report. */
1986
+ removeCompletedExact?: (updateIds: readonly number[], expectedSources: readonly TelegramUpdateJournalEntryDigest[],
1987
+ completions?: readonly TelegramUpdateJournalSourceCompletion[]) => {
1988
+ removedUpdateIds: readonly number[];
1989
+ sourceCompletions?: readonly TelegramUpdateJournalSourceCompletion[];
1990
+ };
1991
+ inspectSourceCompletion?: (expected: TelegramUpdateJournalSourceCompletion) => TelegramUpdateJournalSourceCompletion | undefined;
1992
+ }
1993
+
1994
+ export type TelegramQueueSourceCompletion = TelegramDeferredSourceEvidence & { completionSha256: string };
1995
+
1996
+ function normalizeTelegramQueueSourceCompletions(value: unknown, sourceUpdateIds: ReadonlySet<number>, journalBindingKey: string, full = true): TelegramQueueSourceCompletion[] {
1997
+ 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.");
1998
+ const seen = new Set<number>(), scopes = new Set<string>();
1999
+ return value.map(source => {
2000
+ if (!Number.isSafeInteger(source?.updateId) || !sourceUpdateIds.has(source.updateId) || seen.has(source.updateId)) {
2001
+ throw new Error("Telegram scoped queue completion has invalid source membership.");
2002
+ }
2003
+ const normalized = validateTelegramUpdateAdmissionOutcome({ kind: "complete", expectedSource: source }, source.updateId, sourceUpdateIds);
2004
+ if (normalized.kind !== "complete" || !normalized.expectedSource?.completionSha256 ||
2005
+ normalized.expectedSource.journalBindingKey !== journalBindingKey || scopes.has(normalized.expectedSource.completionSha256)) {
2006
+ throw new Error("Telegram scoped queue completion has invalid source scope.");
2007
+ }
2008
+ seen.add(source.updateId); scopes.add(normalized.expectedSource.completionSha256);
2009
+ return { ...normalized.expectedSource, completionSha256: normalized.expectedSource.completionSha256 };
2010
+ }).sort((a, b) => a.updateId - b.updateId);
1781
2011
  }
1782
2012
 
1783
2013
  export interface TelegramUpdateRetryPolicy {
@@ -1806,12 +2036,19 @@ export interface TelegramUpdateWorkerRuntimeDeps<TContext> {
1806
2036
  hasAuthority: (ctx: TContext) => boolean;
1807
2037
  getJournalBindingKey?: () => string | undefined;
1808
2038
  getRecipientBindingKey?: () => string | undefined;
2039
+ /** True selects legacy historical review/spending; retain protects unsupported originals without disposition authority. */
2040
+ shouldReviewHistoricalInput?: (entry: TelegramUpdateJournalEntry, ctx: TContext, signal: AbortSignal) => boolean | "retain" | Promise<boolean | "retain">;
2041
+ /** New-world restart: spend unclocked classified routing input without delivery, copy or completion. Chooser clocks wait for expiry; interrupted private abandonment keeps exact recovery. */
2042
+ spendHistoricalInput?: boolean;
2043
+ /** Holds protected live or retry sources before execution; never cancels or disposes them. */
2044
+ shouldHoldPendingInput?: (entry: TelegramUpdateJournalEntry, ctx: TContext, signal: AbortSignal) => boolean | Promise<boolean>;
1809
2045
  getQueueOwnerIdentity?: (
1810
2046
  ctx: TContext,
1811
2047
  ) => TelegramUpdateJournalQueueOwnerIdentity;
1812
2048
  isContextCurrent?: (ctx: TContext) => boolean;
1813
2049
  createAbortController?: () => AbortController;
1814
2050
  getNowMs?: () => number;
2051
+ expireRoutingInput?: (source: TelegramRoutingInputExpirySource, ctx: TContext, signal: AbortSignal) => Promise<void> | void;
1815
2052
  retryPolicy?: Partial<TelegramUpdateRetryPolicy>;
1816
2053
  classifyExecutionFailure?: (
1817
2054
  error: unknown,
@@ -1822,11 +2059,18 @@ export interface TelegramUpdateWorkerRuntimeDeps<TContext> {
1822
2059
  batchSize?: number;
1823
2060
  yieldToEventLoop?: () => Promise<void>;
1824
2061
  onStateChange?: (state: TelegramUpdateWorkerStateSnapshot) => void;
2062
+ /** One nonblocking hint after a quiescent validated startup projection, never execution or deletion authority. */
2063
+ onHeldSourcesPrepared?: (input: TelegramHeldSourcePreparation<TContext>) => Promise<void> | void;
2064
+ /** Prepared readiness barrier after durable queue admission; never authorizes replay or source removal. */
2065
+ beforeQueueReceiptPublished?: (receipt: TelegramQueueAdmissionReceiptLike, queueOwner: TelegramUpdateJournalQueueOwner,
2066
+ ctx: TContext, isCurrent: () => boolean) => Promise<void | readonly TelegramQueueSourceCompletion[]> | void | readonly TelegramQueueSourceCompletion[];
2067
+ /** Post-ACK hint only; it must not request Pi dispatch or inherit a Workspace admission lease. */
2068
+ onQueueReceiptCompleted?: (receipt: TelegramQueueAdmissionReceiptLike, ctx: TContext) => void;
1825
2069
  onQueueReceiptCommitted?: (
1826
2070
  receipt: TelegramQueueAdmissionReceiptLike,
1827
2071
  ctx: TContext,
1828
2072
  ) => void;
1829
- onUpdateCompleted?: (updateId: number, ctx: TContext) => void;
2073
+ onUpdateCompleted?: (updateId: number, ctx: TContext, journalBindingKey?: string) => void;
1830
2074
  recordRuntimeEvent?: (
1831
2075
  category: string,
1832
2076
  error: unknown,
@@ -1847,6 +2091,20 @@ export interface TelegramUpdateWorkerRuntime<TContext> {
1847
2091
  outcome: TelegramUpdateAdmissionOutcome;
1848
2092
  signal: AbortSignal;
1849
2093
  }) => void;
2094
+ armRoutingInput?: (input: { updateId: number; signal: AbortSignal; operatorUserId: number; sourceUpdateIds: readonly number[] }) => TelegramUpdateJournalRoutingInput | undefined;
2095
+ selectRoutingInput?: (input: { updateId: number; signal: AbortSignal; operatorUserId: number; sourceUpdateIds: readonly number[] }) => boolean;
2096
+ isRoutingInputCurrent?: (input: { updateId: number; signal: AbortSignal }) => boolean;
2097
+ supportsDeferredAbandonment?: (input: { updateId: number; signal: AbortSignal; journalBindingKey: string }) => boolean;
2098
+ inspectAbandoning?: (input: TelegramDeferredAbandonmentRecoveryRequest & { signal: AbortSignal }) =>
2099
+ TelegramDeferredAbandonmentRecoveryPage | undefined;
2100
+ inspectHistorical?: (input: TelegramDeferredAbandonmentRecoveryRequest & { signal: AbortSignal }) =>
2101
+ TelegramDeferredAbandonmentRecoveryPage | undefined;
2102
+ inspectDeferredSource?: (input: { updateId: number; signal: AbortSignal }) => TelegramDeferredSourceEvidence | undefined;
2103
+ isHistoricalSource?: (input: { updateId: number; signal: AbortSignal; matchesOriginal?: TelegramHistoricalInputPredicate }) => boolean;
2104
+ abandonDeferred?: (input: TelegramDeferredUpdateAbandonmentAuthority & {
2105
+ updateId: number;
2106
+ signal: AbortSignal;
2107
+ }) => TelegramUpdateJournalPendingAbandonmentResult | undefined;
1850
2108
  settleCustodied: (input: {
1851
2109
  updateId: number;
1852
2110
  result: TelegramCustodiedExecutionResult;
@@ -1858,17 +2116,22 @@ export interface TelegramUpdateWorkerRuntime<TContext> {
1858
2116
  getQueueReceiptOwner: (
1859
2117
  receipt: TelegramQueueAdmissionReceiptLike,
1860
2118
  ) => TelegramUpdateJournalQueueOwner | undefined;
2119
+ /** Completion-only owner observation; issued attempts are never dispatch readiness. */
2120
+ getQueueReceiptSettlementOwner?: (receipt: TelegramQueueAdmissionReceiptLike, ctx: TContext,
2121
+ reason: TelegramQueueReceiptCompletionReason) => TelegramUpdateJournalQueueOwner | undefined;
1861
2122
  completeQueueReceipts: (input: {
1862
2123
  receipts: readonly TelegramQueueAdmissionReceiptLike[];
1863
2124
  ctx: TContext;
1864
2125
  reason: TelegramQueueReceiptCompletionReason;
2126
+ /** Prepared immutable scopes for every source; the exact receipt owner still authorizes disposal. */
2127
+ sourceCompletions?: readonly TelegramQueueSourceCompletion[];
1865
2128
  }) => boolean;
1866
2129
  stop: () => Promise<void>;
1867
2130
  waitForDrain: () => Promise<void>;
1868
2131
  getState: () => TelegramUpdateWorkerStateSnapshot;
1869
2132
  }
1870
2133
 
1871
- export class TelegramUpdateAdmissionOutcomeError extends Error {
2134
+ class TelegramUpdateAdmissionOutcomeError extends Error {
1872
2135
  constructor(message: string) {
1873
2136
  super(message);
1874
2137
  this.name = "TelegramUpdateAdmissionOutcomeError";
@@ -1880,9 +2143,11 @@ interface TelegramUpdateWorkerOwner<TContext> {
1880
2143
  ctx: TContext;
1881
2144
  controller: AbortController;
1882
2145
  queueOwnerIdentity: TelegramUpdateJournalQueueOwnerIdentity;
2146
+ startupUpdateIds?: ReadonlySet<number>;
2147
+ heldSourcesPreparedIssued?: true;
1883
2148
  }
1884
2149
 
1885
- type TelegramUpdateWorkerClaim = "deferred" | "queued";
2150
+ type TelegramUpdateWorkerClaim = "deferred" | "queued" | "abandoning" | "historical" | "retained";
1886
2151
  type TelegramUpdateWorkerDrainResult = "idle" | "blocked" | "aborted";
1887
2152
  type TelegramUpdateWorkerExecutionSettlement =
1888
2153
  | { ok: true; outcome: TelegramUpdateAdmissionOutcome; custodied?: false }
@@ -1899,16 +2164,6 @@ function getTelegramUpdateWorkerStateSnapshot(
1899
2164
  return { ...state };
1900
2165
  }
1901
2166
 
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
2167
  function validateTelegramUpdateAdmissionOutcome(
1913
2168
  value: unknown,
1914
2169
  currentUpdateId: number,
@@ -1919,8 +2174,23 @@ function validateTelegramUpdateAdmissionOutcome(
1919
2174
  `Telegram update ${currentUpdateId} returned no admission outcome.`,
1920
2175
  );
1921
2176
  }
1922
- if (value.kind === "complete") return { kind: "complete" };
1923
- if (value.kind === "deferred") return { kind: "deferred" };
2177
+ if (value.kind === "complete") {
2178
+ if (value.expectedSource === undefined) return { kind: "complete" };
2179
+ const source = value.expectedSource;
2180
+ if (!isTelegramUpdateAdmissionRecord(source) || Object.keys(source).some(key => !["journalBindingKey", "updateId", "sourceSha256", "completionSha256"].includes(key)) ||
2181
+ source.updateId !== currentUpdateId || !isTelegramUpdateAdmissionString(source.journalBindingKey) ||
2182
+ typeof source.sourceSha256 !== "string" || !/^[a-f0-9]{64}$/u.test(source.sourceSha256) ||
2183
+ source.completionSha256 !== undefined && (typeof source.completionSha256 !== "string" || !/^[a-f0-9]{64}$/u.test(source.completionSha256))) {
2184
+ throw new TelegramUpdateAdmissionOutcomeError(`Telegram update ${currentUpdateId} returned an invalid completion source.`);
2185
+ }
2186
+ return { kind: "complete", expectedSource: { journalBindingKey: source.journalBindingKey,
2187
+ updateId: currentUpdateId, sourceSha256: source.sourceSha256,
2188
+ ...(source.completionSha256 !== undefined ? { completionSha256: source.completionSha256 } : {}) } };
2189
+ }
2190
+ if (value.kind === "deferred") {
2191
+ if (value.routingReview !== undefined && value.routingReview !== true) throw new TelegramUpdateAdmissionOutcomeError("Invalid historical review outcome.");
2192
+ return { kind: "deferred", ...(value.routingReview === true ? { routingReview: true } : {}) };
2193
+ }
1924
2194
  if (value.kind === "queued") {
1925
2195
  if (
1926
2196
  (value.queueKind !== "prompt" && value.queueKind !== "control") ||
@@ -2072,12 +2342,23 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
2072
2342
  );
2073
2343
  }
2074
2344
  const getNowMs = deps.getNowMs ?? Date.now;
2345
+ const onHeldSourcesPrepared = deps.onHeldSourcesPrepared;
2346
+ const spendHistorical = deps.spendHistoricalInput === true;
2347
+ const routingJournal = deps.journal.routingInputs;
2348
+ const armRouting = routingJournal?.arm.bind(routingJournal);
2349
+ const selectRouting = routingJournal?.select.bind(routingJournal);
2350
+ const expireRouting = routingJournal?.expire.bind(routingJournal);
2351
+ const expireRoutingObserver = deps.expireRoutingInput;
2075
2352
  const batchSize = deps.batchSize ?? TELEGRAM_UPDATE_WORKER_BATCH_SIZE;
2076
2353
  if (!Number.isSafeInteger(batchSize) || batchSize <= 0) {
2077
2354
  throw new Error("Telegram update worker batch size must be positive.");
2078
2355
  }
2079
2356
  const yieldToEventLoop = deps.yieldToEventLoop ??
2080
2357
  (() => new Promise<void>((resolve) => setTimeout(resolve, 0)));
2358
+ const completeQueuedExact = deps.journal.completeQueuedExact?.bind(deps.journal);
2359
+ const completeExact = deps.journal.removeCompletedExact?.bind(deps.journal);
2360
+ const inspectCompletion = deps.journal.inspectSourceCompletion?.bind(deps.journal);
2361
+ const observeQueuedCompletion = deps.onQueueReceiptCompleted;
2081
2362
  const fallbackQueueOwnerInstanceId = `worker-${randomUUID()}`;
2082
2363
  const fallbackQueueOwnerProcessId = process.pid > 0 ? process.pid : 1;
2083
2364
  const createAbortController =
@@ -2100,6 +2381,12 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
2100
2381
  sessionGeneration: generation,
2101
2382
  },
2102
2383
  );
2384
+ const isQueueOwnerIdentityCurrent = (expected: TelegramUpdateWorkerOwner<TContext>): boolean => {
2385
+ if (!deps.getQueueOwnerIdentity) return true;
2386
+ const live = deps.getQueueOwnerIdentity(expected.ctx), captured = expected.queueOwnerIdentity;
2387
+ return !!live && live.instanceId === captured.instanceId && live.processId === captured.processId &&
2388
+ live.processBirthId === captured.processBirthId && live.sessionGeneration === captured.sessionGeneration;
2389
+ };
2103
2390
  const state: TelegramUpdateWorkerStateSnapshot = {
2104
2391
  phase: "stopped",
2105
2392
  generation: 0,
@@ -2113,6 +2400,11 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
2113
2400
  unsettledExecutionCount: 0,
2114
2401
  };
2115
2402
  const claims = new Map<number, TelegramUpdateWorkerClaim>();
2403
+ const deferredSources = new Map<number, {
2404
+ entry: TelegramUpdateWorkerJournalSnapshot["entries"][number];
2405
+ journalBindingKey: string;
2406
+ historical?: true;
2407
+ }>();
2116
2408
  const unsettledExecutionsByUpdateId = new Map<
2117
2409
  number,
2118
2410
  Set<Promise<TelegramUpdateWorkerExecutionSettlement>>
@@ -2122,8 +2414,15 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
2122
2414
  {
2123
2415
  receipt: TelegramQueueAdmissionReceiptLike;
2124
2416
  queueOwner: TelegramUpdateJournalQueueOwner;
2417
+ sourceCompletions?: TelegramQueueSourceCompletion[];
2125
2418
  }
2126
2419
  >();
2420
+ const requiredScopedQueueReceipts = new Set<string>();
2421
+ const scopedQueueCompletionAttempts = new Map<string, {
2422
+ owner: TelegramUpdateWorkerOwner<TContext>; journalBindingKey: string; reason: TelegramQueueReceiptCompletionReason;
2423
+ receipts: { queueKind: "prompt" | "control"; receiptId: string; sourceUpdateIds: readonly number[]; queueOwner: TelegramUpdateJournalQueueOwner }[];
2424
+ completions: TelegramUpdateJournalSourceCompletion[];
2425
+ }>();
2127
2426
  const unsettledExecutions =
2128
2427
  new Set<Promise<TelegramUpdateWorkerExecutionSettlement>>();
2129
2428
  let owner: TelegramUpdateWorkerOwner<TContext> | undefined;
@@ -2134,6 +2433,8 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
2134
2433
  let retryTimer: unknown;
2135
2434
  let retryTimerAtMs: number | undefined;
2136
2435
  let retryTimerToken: object | undefined;
2436
+ let routingTimer: unknown;
2437
+ let routingTimerToken: object | undefined;
2137
2438
  let launchDrain: () => void = () => {};
2138
2439
 
2139
2440
  const recordRuntimeEvent = (
@@ -2158,12 +2459,20 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
2158
2459
  const updateClaimCounts = (): void => {
2159
2460
  let deferredClaimCount = 0;
2160
2461
  let queuedClaimCount = 0;
2462
+ let abandoningClaimCount = 0;
2463
+ let historicalClaimCount = 0;
2161
2464
  for (const claim of claims.values()) {
2162
2465
  if (claim === "queued") queuedClaimCount += 1;
2163
2466
  else deferredClaimCount += 1;
2467
+ if (claim === "abandoning") abandoningClaimCount += 1;
2468
+ if (claim === "historical" || claim === "retained") historicalClaimCount += 1;
2164
2469
  }
2165
2470
  state.deferredClaimCount = deferredClaimCount;
2166
2471
  state.queuedClaimCount = queuedClaimCount;
2472
+ if (abandoningClaimCount > 0) state.abandoningClaimCount = abandoningClaimCount;
2473
+ else delete state.abandoningClaimCount;
2474
+ if (historicalClaimCount > 0) state.historicalClaimCount = historicalClaimCount;
2475
+ else delete state.historicalClaimCount;
2167
2476
  };
2168
2477
 
2169
2478
  const releaseDeferredClaims = (updateIds: readonly number[]): void => {
@@ -2171,6 +2480,7 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
2171
2480
  for (const updateId of updateIds) {
2172
2481
  if (claims.get(updateId) !== "deferred") continue;
2173
2482
  claims.delete(updateId);
2483
+ deferredSources.delete(updateId);
2174
2484
  changed = true;
2175
2485
  }
2176
2486
  if (!changed) return;
@@ -2233,11 +2543,18 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
2233
2543
  : {}),
2234
2544
  });
2235
2545
 
2236
- const publishCommittedQueueReceipt = (
2546
+ const inspectQueueReceipt = deps.journal.isQueueReceiptCurrent?.bind(deps.journal);
2547
+ const inspectQueuedSources = deps.journal.inspectQueuedReceipt?.bind(deps.journal);
2548
+ const prepareQueueReceipt = deps.beforeQueueReceiptPublished;
2549
+ const pendingQueuePublications = new Map<string, { receipt: TelegramQueueAdmissionReceiptLike;
2550
+ queueOwner: TelegramUpdateJournalQueueOwner; owner: TelegramUpdateWorkerOwner<TContext>; task: Promise<void> }>();
2551
+ const publishCommittedQueueReceipt = async (
2237
2552
  receipt: TelegramQueueAdmissionReceiptLike,
2238
2553
  queueOwner: TelegramUpdateJournalQueueOwner,
2239
- ctx: TContext,
2240
- ): boolean => {
2554
+ expectedOwner: TelegramUpdateWorkerOwner<TContext>,
2555
+ ): Promise<boolean> => {
2556
+ const ctx = expectedOwner.ctx;
2557
+ let sourceCompletions: TelegramQueueSourceCompletion[] | undefined;
2241
2558
  const normalized = bindQueueReceiptToJournal(receipt);
2242
2559
  const existing = committedQueueReceipts.get(receipt.receiptId);
2243
2560
  if (existing) {
@@ -2257,29 +2574,86 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
2257
2574
  }
2258
2575
  return false;
2259
2576
  }
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
- });
2577
+ if (prepareQueueReceipt) {
2578
+ const pending = pendingQueuePublications.get(normalized.receiptId);
2579
+ if (pending) {
2580
+ if (pending.owner !== expectedOwner || !areTelegramQueueAdmissionReceiptsEqual(pending.receipt, normalized) ||
2581
+ !areTelegramUpdateJournalQueueOwnersEqual(pending.queueOwner, queueOwner)) {
2582
+ throw new TelegramUpdateAdmissionOutcomeError("Telegram queue publication has conflicting in-flight authority.");
2583
+ }
2584
+ await pending.task;
2585
+ return false;
2586
+ }
2587
+ const journalBindingKey = deps.getJournalBindingKey?.();
2588
+ const current = (): boolean => owner === expectedOwner && !expectedOwner.controller.signal.aborted && deps.hasAuthority(ctx) && deps.isContextCurrent?.(ctx) !== false &&
2589
+ isQueueOwnerIdentityCurrent(expectedOwner) && deps.getJournalBindingKey?.() === journalBindingKey;
2590
+ if (!journalBindingKey || !current() || !inspectQueueReceipt ||
2591
+ inspectQueueReceipt(normalizeQueueReceipt(normalized), { ...queueOwner }) !== true || !current()) {
2592
+ throw new TelegramUpdateAdmissionOutcomeError("Telegram queue publication requires exact current receipt inspection.");
2593
+ }
2594
+ const task = Promise.resolve().then(async () => {
2595
+ if (!current()) throw new TelegramUpdateAdmissionOutcomeError("Telegram queue publication authority ended before acceptance.");
2596
+ const prepared = await prepareQueueReceipt(normalizeQueueReceipt(normalized), { ...queueOwner }, ctx, current);
2597
+ if (prepared !== undefined) {
2598
+ sourceCompletions = normalizeTelegramQueueSourceCompletions(prepared, new Set(normalized.sourceUpdateIds), journalBindingKey, false);
2599
+ if (!completeQueuedExact || !inspectCompletion) throw new Error("Telegram scoped queue terminal capabilities are unavailable.");
2600
+ if (sourceCompletions.length !== normalized.sourceUpdateIds.length) {
2601
+ if (!current() || !inspectQueuedSources) throw new Error("Telegram partial queue scopes require strict current whole-receipt origin inspection.");
2602
+ const expected = { queueKind: normalized.queueKind, receiptId: normalized.receiptId,
2603
+ sourceUpdateIds: [...normalized.sourceUpdateIds], queueOwner: { ...queueOwner } };
2604
+ const proof = inspectQueuedSources(structuredClone(expected));
2605
+ if (!current() || !proof || !isDeepStrictEqual(proof.receipt, expected) ||
2606
+ !Array.isArray(proof.sources) || proof.sources.length !== expected.sourceUpdateIds.length ||
2607
+ proof.sources.some((source, index) => source.updateId !== expected.sourceUpdateIds[index] || typeof source.sourceSha256 !== "string" || !/^[a-f0-9]{64}$/u.test(source.sourceSha256)) ||
2608
+ sourceCompletions.some(scope => proof.sources.find(source => source.updateId === scope.updateId)?.sourceSha256 !== scope.sourceSha256)) {
2609
+ throw new Error("Telegram partial queue scope origin was not confirmed under whole-receipt authority.");
2610
+ }
2611
+ }
2612
+ for (const { journalBindingKey: _binding, ...completion } of sourceCompletions) {
2613
+ if (!current() || inspectCompletion({ ...completion }) !== undefined || !current()) {
2614
+ throw new Error("Telegram queued source completion authority is contradictory.");
2615
+ }
2616
+ }
2617
+ }
2618
+ if (!current() || inspectQueueReceipt(normalizeQueueReceipt(normalized), { ...queueOwner }) !== true || !current()) {
2619
+ throw new TelegramUpdateAdmissionOutcomeError("Telegram queue publication authority changed after acceptance.");
2620
+ }
2621
+ publishReady();
2622
+ }).finally(() => { if (pendingQueuePublications.get(normalized.receiptId)?.task === task) pendingQueuePublications.delete(normalized.receiptId); });
2623
+ pendingQueuePublications.set(normalized.receiptId, { receipt: normalizeQueueReceipt(normalized), queueOwner: { ...queueOwner }, owner: expectedOwner, task });
2624
+ await task;
2625
+ return true;
2271
2626
  }
2627
+ publishReady();
2272
2628
  return true;
2629
+
2630
+ function publishReady(): void {
2631
+ committedQueueReceipts.set(receipt.receiptId, {
2632
+ receipt: normalized,
2633
+ queueOwner: { ...queueOwner },
2634
+ ...(sourceCompletions ? { sourceCompletions: sourceCompletions.map(value => ({ ...value })) } : {}),
2635
+ });
2636
+ if (sourceCompletions) requiredScopedQueueReceipts.add(receipt.receiptId);
2637
+ try {
2638
+ deps.onQueueReceiptCommitted?.(normalizeQueueReceipt(normalized), ctx);
2639
+ } catch (error) {
2640
+ recordRuntimeEvent(error, {
2641
+ phase: "queue-receipt-observer",
2642
+ receiptId: normalized.receiptId,
2643
+ });
2644
+ }
2645
+ }
2273
2646
  };
2274
2647
 
2275
2648
  const getCurrentQueueReceipt = (receipt: TelegramQueueAdmissionReceiptLike) => {
2649
+ if (scopedQueueCompletionAttempts.has(receipt.receiptId)) return undefined;
2276
2650
  const committed = committedQueueReceipts.get(receipt.receiptId);
2277
2651
  if (!committed || !areTelegramQueueAdmissionReceiptsEqual(
2278
2652
  committed.receipt, normalizeQueueReceipt(receipt),
2279
2653
  )) return undefined;
2280
2654
  try {
2281
- if (deps.journal.isQueueReceiptCurrent &&
2282
- deps.journal.isQueueReceiptCurrent(committed.receipt, committed.queueOwner) !== true) {
2655
+ if (inspectQueueReceipt &&
2656
+ inspectQueueReceipt(normalizeQueueReceipt(committed.receipt), { ...committed.queueOwner }) !== true) {
2283
2657
  return undefined;
2284
2658
  }
2285
2659
  } catch (error) {
@@ -2327,10 +2701,10 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
2327
2701
  }, Math.max(0, nextRetryAtMs - getNowMs()));
2328
2702
  };
2329
2703
 
2330
- const refreshJournalState = (
2704
+ const refreshJournalState = async (
2331
2705
  snapshot: TelegramUpdateWorkerJournalSnapshot,
2332
2706
  expectedOwner: TelegramUpdateWorkerOwner<TContext>,
2333
- ): number | undefined => {
2707
+ ): Promise<number | undefined> => {
2334
2708
  // Validate the whole snapshot before reconstructing any queue authority.
2335
2709
  if ((snapshot.version !== TELEGRAM_UPDATE_JOURNAL_VERSION &&
2336
2710
  snapshot.version !== TELEGRAM_UPDATE_JOURNAL_EXCLUSION_VERSION &&
@@ -2367,7 +2741,7 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
2367
2741
  oldestAdmittedAtMs === undefined
2368
2742
  ? entry.admittedAtMs
2369
2743
  : Math.min(oldestAdmittedAtMs, entry.admittedAtMs);
2370
- if (entry.state === "retry-wait" && entry.nextRetryAtMs !== undefined) {
2744
+ if (entry.state === "retry-wait" && entry.nextRetryAtMs !== undefined && !claims.has(entry.updateId)) {
2371
2745
  scheduledRetryAtMs =
2372
2746
  scheduledRetryAtMs === undefined
2373
2747
  ? entry.nextRetryAtMs
@@ -2449,21 +2823,25 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
2449
2823
  }
2450
2824
  for (const [updateId, claim] of claims) {
2451
2825
  if (
2452
- !availableUpdateIds.has(updateId) ||
2826
+ (claim !== "abandoning" && !availableUpdateIds.has(updateId)) ||
2453
2827
  (claim === "queued" && !locallyOwnedQueuedUpdateIds.has(updateId))
2454
2828
  ) {
2455
2829
  claims.delete(updateId);
2456
2830
  }
2457
2831
  }
2832
+ for (const updateId of deferredSources.keys()) {
2833
+ const claim = claims.get(updateId);
2834
+ if (claim !== "deferred" && claim !== "abandoning" && claim !== "historical" && claim !== "retained") deferredSources.delete(updateId);
2835
+ }
2458
2836
  for (const [receiptId, receipt] of queuedReceiptEntries) {
2459
- publishCommittedQueueReceipt(
2837
+ await publishCommittedQueueReceipt(
2460
2838
  {
2461
2839
  queueKind: receipt.queueKind,
2462
2840
  receiptId,
2463
2841
  sourceUpdateIds: receipt.sourceUpdateIds,
2464
2842
  },
2465
2843
  receipt.queueOwner,
2466
- expectedOwner.ctx,
2844
+ expectedOwner,
2467
2845
  );
2468
2846
  }
2469
2847
  for (const [receiptId] of committedQueueReceipts) {
@@ -2598,10 +2976,8 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
2598
2976
  expectedOwner: TelegramUpdateWorkerOwner<TContext>,
2599
2977
  currentUpdateId: number,
2600
2978
  outcome: TelegramQueuedUpdateAdmissionOutcome,
2601
- ):
2602
- | "committed"
2603
- | "duplicate"
2604
- | Exclude<TelegramUpdateWorkerDrainResult, "idle"> => {
2979
+ ): "committed" | "duplicate" | Exclude<TelegramUpdateWorkerDrainResult, "idle"> |
2980
+ Promise<"committed" | "duplicate" | Exclude<TelegramUpdateWorkerDrainResult, "idle">> => {
2605
2981
  const normalized = normalizeQueueReceipt(outcome);
2606
2982
  const existing = committedQueueReceipts.get(normalized.receiptId);
2607
2983
  if (existing) {
@@ -2674,21 +3050,8 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
2674
3050
  for (const sourceUpdateId of normalized.sourceUpdateIds) {
2675
3051
  claims.set(sourceUpdateId, "queued");
2676
3052
  }
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";
3053
+ return publishCommittedQueueReceipt(normalized, queueOwner, expectedOwner).then(() => "committed" as const,
3054
+ error => blockWithFailure("invalid-outcome", "queue-receipt-publish", error, currentUpdateId));
2692
3055
  };
2693
3056
 
2694
3057
  const persistExecutionFailure = (
@@ -2774,16 +3137,45 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
2774
3137
 
2775
3138
  const commitCompletedBatch = (
2776
3139
  expectedOwner: TelegramUpdateWorkerOwner<TContext>,
2777
- updateIds: readonly number[],
3140
+ completions: readonly { updateId: number; expectedSource?: TelegramDeferredSourceEvidence; spent?: true }[],
2778
3141
  ): Exclude<TelegramUpdateWorkerDrainResult, "idle"> | undefined => {
3142
+ const updateIds = completions.map(value => value.updateId);
2779
3143
  if (updateIds.length === 0) return undefined;
2780
3144
  const commitAuthority = checkAuthority(
2781
3145
  expectedOwner,
2782
3146
  updateIds[updateIds.length - 1],
2783
3147
  );
2784
3148
  if (commitAuthority) return commitAuthority;
3149
+ const journalBindingKey = deps.getJournalBindingKey?.();
2785
3150
  try {
2786
- const removed = deps.journal.removeCompleted(updateIds);
3151
+ const expectedSources = completions.flatMap(value => value.expectedSource ? [value.expectedSource] : []);
3152
+ if (expectedSources.some(value => value.journalBindingKey !== journalBindingKey) ||
3153
+ (expectedSources.length > 0 && (deps.isContextCurrent?.(expectedOwner.ctx) === false || !isQueueOwnerIdentityCurrent(expectedOwner)))) {
3154
+ throw new Error("Telegram guarded completion source authority changed.");
3155
+ }
3156
+ const scoped = expectedSources.flatMap(({ updateId, sourceSha256, completionSha256 }) => completionSha256 !== undefined
3157
+ ? [{ updateId, sourceSha256, completionSha256 }] : []).sort((a, b) => a.updateId - b.updateId);
3158
+ if (scoped.length > 0 && !inspectCompletion) throw new Error("Telegram source completion inspection is unavailable.");
3159
+ let removed: { removedUpdateIds: readonly number[]; sourceCompletions?: readonly TelegramUpdateJournalSourceCompletion[] };
3160
+ if (expectedSources.length > 0) {
3161
+ if (!completeExact) throw new Error("Telegram exact source completion is unavailable.");
3162
+ removed = completeExact(updateIds, expectedSources.map(({ updateId, sourceSha256 }) => ({ updateId, sourceSha256 })),
3163
+ scoped.length > 0 ? scoped.map(completion => ({ ...completion })) : undefined);
3164
+ } else removed = deps.journal.removeCompleted(updateIds);
3165
+ if (scoped.length > 0) {
3166
+ const afterCommit = checkAuthority(expectedOwner, updateIds.at(-1));
3167
+ if (afterCommit) return afterCommit;
3168
+ if (deps.isContextCurrent?.(expectedOwner.ctx) === false || !isQueueOwnerIdentityCurrent(expectedOwner) ||
3169
+ deps.getJournalBindingKey?.() !== journalBindingKey) throw new Error("Telegram source completion ACK authority changed.");
3170
+ if (!isDeepStrictEqual(removed.sourceCompletions, scoped)) throw new Error("Telegram source completion ACK was not confirmed.");
3171
+ for (const completion of scoped) {
3172
+ if (!isDeepStrictEqual(inspectCompletion!({ ...completion }), completion)) throw new Error("Telegram source completion ACK was not retained.");
3173
+ const afterRead = checkAuthority(expectedOwner, completion.updateId);
3174
+ if (afterRead) return afterRead;
3175
+ if (deps.isContextCurrent?.(expectedOwner.ctx) === false || !isQueueOwnerIdentityCurrent(expectedOwner) ||
3176
+ deps.getJournalBindingKey?.() !== journalBindingKey) throw new Error("Telegram source completion ACK authority changed.");
3177
+ }
3178
+ }
2787
3179
  const removedIds = new Set(removed.removedUpdateIds);
2788
3180
  if (updateIds.some((updateId) => !removedIds.has(updateId))) {
2789
3181
  throw new Error(
@@ -2799,12 +3191,15 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
2799
3191
  );
2800
3192
  }
2801
3193
  const completedAtMs = getNowMs();
2802
- for (const updateId of updateIds) {
3194
+ for (const { updateId, spent } of completions) {
2803
3195
  claims.delete(updateId);
3196
+ deferredSources.delete(updateId);
3197
+ // Spending is disposition, not completion: no task, cleanup or session observer runs.
3198
+ if (spent) continue;
2804
3199
  state.lastCompletedUpdateId = updateId;
2805
3200
  state.lastCompletedAtMs = completedAtMs;
2806
3201
  try {
2807
- deps.onUpdateCompleted?.(updateId, expectedOwner.ctx);
3202
+ deps.onUpdateCompleted?.(updateId, expectedOwner.ctx, journalBindingKey);
2808
3203
  } catch (error) {
2809
3204
  recordRuntimeEvent(error, {
2810
3205
  phase: "update-completion-observer",
@@ -2815,6 +3210,94 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
2815
3210
  return undefined;
2816
3211
  };
2817
3212
 
3213
+ const clearRoutingTimer = (): void => {
3214
+ if (routingTimer !== undefined) cancelRetry(routingTimer);
3215
+ routingTimer = undefined; routingTimerToken = undefined;
3216
+ };
3217
+ const reconcileRoutingInputs = async (snapshot: TelegramUpdateWorkerJournalSnapshot, expectedOwner: TelegramUpdateWorkerOwner<TContext>): Promise<boolean> => {
3218
+ clearRoutingTimer();
3219
+ if (!isTelegramUpdateJournalLegacyFamilyVersion(snapshot.version) || !expireRouting || !expireRoutingObserver) return false;
3220
+ let nextAtMs: number | undefined, changed = false;
3221
+ const binding = deps.getJournalBindingKey?.(), snapshotIds = new Set(snapshot.entries.map(entry => entry.updateId));
3222
+ // Lost expiry ACKs reconcile the exact held original against its body-free discard tombstone.
3223
+ const candidates = [...snapshot.entries, ...[...deferredSources.values()]
3224
+ .filter(source => source.journalBindingKey === binding && !snapshotIds.has(source.entry.updateId))
3225
+ .map(source => source.entry)];
3226
+ for (const entry of candidates) {
3227
+ const lifetime = entry.routingInput;
3228
+ if (!lifetime || entry.state !== "pending" || !binding || claims.get(entry.updateId) === "queued" ||
3229
+ unsettledExecutionsByUpdateId.get(entry.updateId)?.size) continue;
3230
+ // Classify the first startup snapshot before constructing any historical expiry carrier.
3231
+ if (expectedOwner.startupUpdateIds?.has(entry.updateId) && !claims.has(entry.updateId) && deps.shouldReviewHistoricalInput) {
3232
+ nextAtMs = Math.min(nextAtMs ?? Infinity, Math.max(getNowMs() + 1, lifetime.expiresAtMs));
3233
+ continue;
3234
+ }
3235
+ if (getNowMs() < lifetime.expiresAtMs) { nextAtMs = Math.min(nextAtMs ?? Infinity, lifetime.expiresAtMs); continue; }
3236
+ const existing = deferredSources.get(entry.updateId);
3237
+ if (existing && !isDeepStrictEqual(existing.entry, entry)) {
3238
+ // Lost clock/selection ACKs may leave only this immutable metadata behind its exact source; reconciliation grants expiry, never another dispatch.
3239
+ const previous = existing.entry.routingInput;
3240
+ if (!isDeepStrictEqual({ ...existing.entry, routingInput: lifetime }, entry) ||
3241
+ (previous && (!isDeepStrictEqual({ ...previous, phase: lifetime.phase }, lifetime) ||
3242
+ (previous.phase === "selected" && lifetime.phase !== "selected")))) continue;
3243
+ existing.entry = structuredClone(entry);
3244
+ }
3245
+ const source = existing ?? { entry: structuredClone(entry), journalBindingKey: binding, historical: true as const };
3246
+ deferredSources.set(entry.updateId, source);
3247
+ if (!claims.has(entry.updateId)) claims.set(entry.updateId, "historical");
3248
+ const current = () => owner === expectedOwner && !expectedOwner.controller.signal.aborted &&
3249
+ deps.isContextCurrent?.(expectedOwner.ctx) !== false && deps.getJournalBindingKey?.() === binding &&
3250
+ deps.hasAuthority(expectedOwner.ctx) && isQueueOwnerIdentityCurrent(expectedOwner) && deferredSources.get(entry.updateId) === source &&
3251
+ isDeepStrictEqual(source.entry, entry) && claims.get(entry.updateId) !== "queued";
3252
+ let committed: TelegramRoutingInputExpiryResult | undefined;
3253
+ try {
3254
+ if (current()) await expireRoutingObserver({ original: structuredClone(entry), journalBindingKey: binding, isCurrent: current,
3255
+ expire() {
3256
+ if (!current()) return undefined;
3257
+ if (committed) return { ...committed, duplicate: true };
3258
+ claims.set(entry.updateId, "abandoning");
3259
+ committed = expireRouting!({ entry: source.entry, journalBindingKey: binding, operatorUserId: lifetime.operatorUserId, isCurrent: current });
3260
+ // Source expiry is terminal before chooser/cleanup awaits; later failures must not retain a deferred/retry source.
3261
+ claims.delete(entry.updateId); deferredSources.delete(entry.updateId);
3262
+ state.journalEntryCount = committed.entryCount; state.journalSerializedBytes = committed.serializedBytes;
3263
+ updateClaimCounts(); notifyStateChange(); changed ||= !committed.duplicate;
3264
+ return committed;
3265
+ } }, expectedOwner.ctx, expectedOwner.controller.signal);
3266
+ } catch (error) { recordRuntimeEvent(error, { phase: "routing-input-expiry", updateId: entry.updateId }); }
3267
+ if (!committed) nextAtMs = Math.min(nextAtMs ?? Infinity, getNowMs() + 60_000);
3268
+ }
3269
+ if (nextAtMs !== undefined && owner === expectedOwner && !expectedOwner.controller.signal.aborted) {
3270
+ const token = {}; routingTimerToken = token;
3271
+ routingTimer = scheduleRetry(() => {
3272
+ if (routingTimerToken !== token || owner !== expectedOwner || expectedOwner.controller.signal.aborted) return;
3273
+ routingTimer = undefined; routingTimerToken = undefined; pendingSignal = true; launchDrain();
3274
+ }, Math.max(1, nextAtMs - getNowMs()));
3275
+ }
3276
+ return changed;
3277
+ };
3278
+ const mutateRoutingSource = (input: { updateId: number; signal: AbortSignal; operatorUserId: number; sourceUpdateIds: readonly number[] }, select: boolean) => {
3279
+ const expectedOwner = owner, ids = [...input.sourceUpdateIds];
3280
+ if (!ids.length || !ids.includes(input.updateId) || new Set(ids).size !== ids.length || ids.some(id => !Number.isSafeInteger(id) || id < 0)) return undefined;
3281
+ const sources = ids.map(id => deferredSources.get(id)), binding = sources[0]?.journalBindingKey;
3282
+ const current = () => !!expectedOwner && owner === expectedOwner && expectedOwner.controller.signal === input.signal && !input.signal.aborted &&
3283
+ !!binding && sources.every((source, index) => !!source && deferredSources.get(ids[index]!) === source && !source.historical &&
3284
+ source.journalBindingKey === binding && claims.get(ids[index]!) !== "queued" && claims.get(ids[index]!) !== "abandoning") &&
3285
+ deps.getJournalBindingKey?.() === binding && deps.isContextCurrent?.(expectedOwner.ctx) !== false &&
3286
+ deps.hasAuthority(expectedOwner.ctx) && isQueueOwnerIdentityCurrent(expectedOwner);
3287
+ if (!current() || !armRouting || !selectRouting || !expireRoutingObserver) return undefined;
3288
+ const authority = { journalBindingKey: binding!, entries: sources.map(source => structuredClone(source!.entry)), operatorUserId: input.operatorUserId, isCurrent: current };
3289
+ const result = select ? selectRouting(authority) : { issued: false, entries: armRouting({ ...authority, publishedAtMs: getNowMs() }) };
3290
+ if (result.entries.length !== ids.length || result.entries.some((entry, index) => entry.updateId !== ids[index] || !entry.routingInput) || !current()) {
3291
+ throw new Error("Telegram routing input lifetime ACK was not confirmed.");
3292
+ }
3293
+ const retained = deps.journal.read().entries;
3294
+ if (result.entries.some(entry => !isDeepStrictEqual(retained.find(value => value.updateId === entry.updateId), entry)) || !current()) {
3295
+ throw new Error("Telegram routing input lifetime ACK was not retained.");
3296
+ }
3297
+ result.entries.forEach((entry, index) => { sources[index]!.entry = structuredClone(entry); });
3298
+ pendingSignal = true; launchDrain();
3299
+ return { issued: result.issued, lifetime: { ...result.entries[0]!.routingInput! } };
3300
+ };
2818
3301
  const drain = async (
2819
3302
  expectedOwner: TelegramUpdateWorkerOwner<TContext>,
2820
3303
  ): Promise<TelegramUpdateWorkerDrainResult> => {
@@ -2825,10 +3308,15 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
2825
3308
  let scheduledRetryAtMs: number | undefined;
2826
3309
  try {
2827
3310
  snapshot = deps.journal.read();
2828
- scheduledRetryAtMs = refreshJournalState(snapshot, expectedOwner);
3311
+ scheduledRetryAtMs = await refreshJournalState(snapshot, expectedOwner);
3312
+ // One complete validated baseline per generation, before batching or execution.
3313
+ // Replayed v1 entries do not prove that a prior process never forwarded them.
3314
+ expectedOwner.startupUpdateIds ??= new Set(snapshot.entries.map(entry => entry.updateId));
2829
3315
  } catch (error) {
2830
3316
  return blockWithFailure("journal-read", "journal-read", error);
2831
3317
  }
3318
+ if (await reconcileRoutingInputs(snapshot, expectedOwner)) continue;
3319
+ if (owner !== expectedOwner || expectedOwner.controller.signal.aborted) return "aborted";
2832
3320
  const nowMs = getNowMs();
2833
3321
  const entries: TelegramUpdateWorkerJournalSnapshot["entries"][number][] =
2834
3322
  [];
@@ -2885,7 +3373,7 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
2885
3373
  transition("idle");
2886
3374
  return "idle";
2887
3375
  }
2888
- const completedUpdateIds: number[] = [];
3376
+ const completedUpdateIds: { updateId: number; expectedSource?: TelegramDeferredSourceEvidence; spent?: true }[] = [];
2889
3377
  let snapshotInvalidated = false;
2890
3378
  for (const entry of entries) {
2891
3379
  const priorExecutions = unsettledExecutionsByUpdateId.get(entry.updateId);
@@ -2907,9 +3395,88 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
2907
3395
  break;
2908
3396
  }
2909
3397
  if (entry.preApprovalExcluded === true) {
2910
- completedUpdateIds.push(entry.updateId);
3398
+ completedUpdateIds.push({ updateId: entry.updateId });
2911
3399
  continue;
2912
3400
  }
3401
+ if (isTelegramUpdateJournalLegacyFamilyVersion(snapshot.version) && entry.state === "pending" && deps.journal.inspectPendingRetention) {
3402
+ const authorityResult = checkAuthority(expectedOwner, entry.updateId);
3403
+ if (authorityResult) return authorityResult;
3404
+ let retained: TelegramUpdateJournalPendingRetentionEvidence | undefined;
3405
+ let unverifiable = false;
3406
+ try { retained = deps.journal.inspectPendingRetention(entry); }
3407
+ catch (error) {
3408
+ unverifiable = true;
3409
+ recordRuntimeEvent(error, { phase: "pending-retention-inspection", updateId: entry.updateId });
3410
+ }
3411
+ if (retained || unverifiable) {
3412
+ // A copy is protection evidence only. A fresh owner must explicitly retry
3413
+ // cancellation; neither startup nor a malformed copy may dispatch the source.
3414
+ claims.set(entry.updateId, "abandoning");
3415
+ const journalBindingKey = retained?.journalBindingKey ?? deps.getJournalBindingKey?.();
3416
+ if (journalBindingKey) deferredSources.set(entry.updateId, { entry, journalBindingKey });
3417
+ transition("deferred", entry.updateId);
3418
+ continue;
3419
+ }
3420
+ }
3421
+ if (isTelegramUpdateJournalLegacyFamilyVersion(snapshot.version) && (entry.state === "pending" || entry.state === "retry-wait")) {
3422
+ const historical = expectedOwner.startupUpdateIds.has(entry.updateId);
3423
+ const journalBindingKey = deps.getJournalBindingKey?.();
3424
+ // Classifiers and handler projections receive no mutable alias to CAS evidence.
3425
+ if (journalBindingKey) deferredSources.set(entry.updateId, { entry: structuredClone(entry), journalBindingKey,
3426
+ ...(historical ? { historical: true } : {}) });
3427
+ const classifyPending = historical && entry.state === "pending" ? deps.shouldReviewHistoricalInput : deps.shouldHoldPendingInput;
3428
+ if (classifyPending) {
3429
+ const checkHistoricalAuthority = (): TelegramUpdateWorkerDrainResult | undefined => {
3430
+ if (owner !== expectedOwner || expectedOwner.controller.signal.aborted) return "aborted";
3431
+ if (deps.getJournalBindingKey?.() !== journalBindingKey || deps.isContextCurrent?.(expectedOwner.ctx) === false) {
3432
+ transition("blocked", entry.updateId, "authority-lost");
3433
+ return "blocked";
3434
+ }
3435
+ return checkAuthority(expectedOwner, entry.updateId);
3436
+ };
3437
+ const beforeClassification = checkHistoricalAuthority();
3438
+ if (beforeClassification) return beforeClassification;
3439
+ let review: boolean | "retain";
3440
+ try { review = await classifyPending(structuredClone(entry), expectedOwner.ctx, expectedOwner.controller.signal); }
3441
+ catch (error) {
3442
+ const authorityResult = checkHistoricalAuthority();
3443
+ if (authorityResult) return authorityResult;
3444
+ return blockWithFailure("execution", "historical-review-classification", error, entry.updateId);
3445
+ }
3446
+ const authorityResult = checkHistoricalAuthority();
3447
+ if (authorityResult) return authorityResult;
3448
+ if (typeof review !== "boolean" && !(historical && entry.state === "pending" && review === "retain"))
3449
+ return blockWithFailure("execution", "historical-review-classification",
3450
+ new TelegramUpdateAdmissionOutcomeError("Historical classification must return a boolean or retain verdict."), entry.updateId);
3451
+ // Classification may await a domain read; accepted/changed source evidence wins.
3452
+ try {
3453
+ const current = deps.journal.read();
3454
+ if (current.version !== snapshot.version || JSON.stringify(current.entries.find(candidate => candidate.updateId === entry.updateId)) !== JSON.stringify(entry)) {
3455
+ snapshotInvalidated = true;
3456
+ break;
3457
+ }
3458
+ } catch (error) { return blockWithFailure("journal-read", "historical-source-recheck", error, entry.updateId); }
3459
+ if (review === "retain") {
3460
+ claims.set(entry.updateId, "retained");
3461
+ transition("deferred", entry.updateId);
3462
+ continue;
3463
+ }
3464
+ if (review && historical && spendHistorical && !entry.routingInput && classifyPending === deps.shouldReviewHistoricalInput) {
3465
+ completedUpdateIds.push({ updateId: entry.updateId, spent: true });
3466
+ continue;
3467
+ }
3468
+ if (review) {
3469
+ claims.set(entry.updateId, "historical");
3470
+ transition("deferred", entry.updateId);
3471
+ continue;
3472
+ }
3473
+ }
3474
+ if (entry.state === "pending" && entry.routingInput) {
3475
+ claims.set(entry.updateId, "historical");
3476
+ transition("deferred", entry.updateId);
3477
+ continue;
3478
+ }
3479
+ }
2913
3480
  clearRetryTimer();
2914
3481
  transition("executing", entry.updateId);
2915
3482
  const execution = await executeWithinOwner(expectedOwner, entry.update);
@@ -2958,7 +3525,7 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
2958
3525
  }
2959
3526
  for (const sourceUpdateId of receipt.sourceUpdateIds) claims.set(sourceUpdateId, "queued");
2960
3527
  try {
2961
- publishCommittedQueueReceipt(receipt, outcome.queueReceipt.queueOwner, expectedOwner.ctx);
3528
+ await publishCommittedQueueReceipt(receipt, outcome.queueReceipt.queueOwner, expectedOwner);
2962
3529
  } catch (error) {
2963
3530
  return blockWithFailure("invalid-outcome", "queue-receipt-publish", error,
2964
3531
  entry.updateId);
@@ -2975,7 +3542,7 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
2975
3542
  if (postExecutionAuthority) return postExecutionAuthority;
2976
3543
  const claimableUpdateIds = new Set<number>([
2977
3544
  entry.updateId,
2978
- ...claims.keys(),
3545
+ ...[...claims].filter(([, claim]) => claim !== "abandoning" && claim !== "historical" && claim !== "retained").map(([id]) => id),
2979
3546
  ]);
2980
3547
  let outcome: TelegramUpdateAdmissionOutcome;
2981
3548
  try {
@@ -2993,7 +3560,10 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
2993
3560
  );
2994
3561
  }
2995
3562
  if (outcome.kind === "deferred") {
2996
- claims.set(entry.updateId, "deferred");
3563
+ if (outcome.routingReview && !deferredSources.get(entry.updateId)?.historical) {
3564
+ return blockWithFailure("invalid-outcome", "historical-review-source", new TelegramUpdateAdmissionOutcomeError("Historical review needs an exact startup source."), entry.updateId);
3565
+ }
3566
+ claims.set(entry.updateId, outcome.routingReview ? "historical" : "deferred");
2997
3567
  transition("deferred", entry.updateId);
2998
3568
  continue;
2999
3569
  }
@@ -3003,7 +3573,7 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3003
3573
  completedUpdateIds,
3004
3574
  );
3005
3575
  if (completionResult) return completionResult;
3006
- const queuedResult = commitQueuedOutcome(
3576
+ const queuedResult = await commitQueuedOutcome(
3007
3577
  expectedOwner,
3008
3578
  entry.updateId,
3009
3579
  outcome,
@@ -3015,7 +3585,7 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3015
3585
  snapshotInvalidated = true;
3016
3586
  break;
3017
3587
  }
3018
- completedUpdateIds.push(entry.updateId);
3588
+ completedUpdateIds.push({ updateId: entry.updateId, ...(outcome.expectedSource ? { expectedSource: outcome.expectedSource } : {}) });
3019
3589
  }
3020
3590
  const completionResult = commitCompletedBatch(
3021
3591
  expectedOwner,
@@ -3037,6 +3607,11 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3037
3607
  launchDrain = (): void => {
3038
3608
  const expectedOwner = owner;
3039
3609
  if (!expectedOwner || drainPromise) return;
3610
+ let startupKey: string | undefined;
3611
+ if (onHeldSourcesPrepared) {
3612
+ try { startupKey = deps.getJournalBindingKey?.(); } catch { /* Missing startup identity grants no notification. */ }
3613
+ }
3614
+ let lastResult: TelegramUpdateWorkerDrainResult | undefined;
3040
3615
  const run = async (): Promise<void> => {
3041
3616
  while (
3042
3617
  pendingSignal &&
@@ -3046,7 +3621,7 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3046
3621
  pendingSignal = false;
3047
3622
  // Keep newer wakes in this run so waitForDrain also covers them. A failure
3048
3623
  // observed after signal() still keeps its latch; this loop never clears it.
3049
- await drain(expectedOwner);
3624
+ lastResult = await drain(expectedOwner);
3050
3625
  }
3051
3626
  };
3052
3627
  const operation = run();
@@ -3056,6 +3631,23 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3056
3631
  drainPromise = undefined;
3057
3632
  if (pendingSignal && !expectedOwner.controller.signal.aborted) {
3058
3633
  launchDrain();
3634
+ } else if (lastResult === "idle" && startupKey && onHeldSourcesPrepared && !expectedOwner.heldSourcesPreparedIssued) {
3635
+ try {
3636
+ const ctx = expectedOwner.ctx, key = startupKey;
3637
+ const isCurrent = () => owner === expectedOwner && !blocked && !expectedOwner.controller.signal.aborted &&
3638
+ deps.getJournalBindingKey?.() === key && deps.isContextCurrent?.(ctx) !== false && deps.hasAuthority(ctx);
3639
+ if (!isCurrent()) return;
3640
+ expectedOwner.heldSourcesPreparedIssued = true;
3641
+ // Do not couple lifecycle/stop or the worker drain to a held controller/API reply.
3642
+ void Promise.resolve().then(() => isCurrent() ? onHeldSourcesPrepared({ ctx,
3643
+ journalBindingKey: key, signal: expectedOwner.controller.signal, isCurrent,
3644
+ routingSourceIds: [...deferredSources].filter(([, source]) => source.journalBindingKey === key && !!source.entry.routingInput)
3645
+ .map(([id]) => id) }) : undefined).catch(error => {
3646
+ try { deps.recordRuntimeEvent?.("inbound-worker", error, { phase: "held-source-preparation" }); } catch { /* Diagnostic only. */ }
3647
+ });
3648
+ } catch (error) {
3649
+ try { deps.recordRuntimeEvent?.("inbound-worker", error, { phase: "held-source-preparation" }); } catch { /* Diagnostic only. */ }
3650
+ }
3059
3651
  }
3060
3652
  }
3061
3653
  };
@@ -3072,6 +3664,73 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3072
3664
  drainPromise = tracked;
3073
3665
  };
3074
3666
 
3667
+ const abandonSource = (input: Parameters<NonNullable<TelegramUpdateWorkerRuntime<TContext>["abandonDeferred"]>>[0],
3668
+ historicalSource?: ReturnType<typeof deferredSources.get>): TelegramUpdateJournalPendingAbandonmentResult | undefined => {
3669
+ const expectedOwner = owner;
3670
+ const source = deferredSources.get(input.updateId);
3671
+ const abandonPending = deps.journal.abandonPending;
3672
+ const isCurrent = () => {
3673
+ if (!expectedOwner || owner !== expectedOwner || input.signal.aborted ||
3674
+ expectedOwner.controller.signal !== input.signal || !source ||
3675
+ source.entry.routingInput?.phase === "selected" || (source.historical && historicalSource !== source) ||
3676
+ (claims.get(input.updateId) !== "deferred" && claims.get(input.updateId) !== "abandoning" &&
3677
+ !(claims.get(input.updateId) === "historical" && historicalSource === source))) return false;
3678
+ return input.isCurrent() && deps.isContextCurrent?.(expectedOwner.ctx) !== false &&
3679
+ deps.getJournalBindingKey?.() === source.journalBindingKey && deps.hasAuthority(expectedOwner.ctx);
3680
+ };
3681
+ if (!source || !abandonPending || !isCurrent()) return undefined;
3682
+ // Retain this claim on failure, including unknown commit: never reopen dispatch.
3683
+ claims.set(input.updateId, "abandoning");
3684
+ try {
3685
+ const result = abandonPending({ ...input, entry: source.entry,
3686
+ journalBindingKey: source.journalBindingKey, isCurrent });
3687
+ claims.delete(input.updateId);
3688
+ deferredSources.delete(input.updateId);
3689
+ state.journalEntryCount = result.entryCount;
3690
+ state.journalSerializedBytes = result.serializedBytes;
3691
+ transition("idle");
3692
+ pendingSignal = true;
3693
+ launchDrain();
3694
+ return result;
3695
+ } catch (error) {
3696
+ recordRuntimeEvent(error, { phase: "pending-abandonment", updateId: input.updateId });
3697
+ updateClaimCounts();
3698
+ notifyStateChange();
3699
+ throw error;
3700
+ }
3701
+ };
3702
+
3703
+ const inspectRecovery = (input: TelegramDeferredAbandonmentRecoveryRequest & { signal: AbortSignal },
3704
+ kind: "abandoning" | "historical"): TelegramDeferredAbandonmentRecoveryPage | undefined => {
3705
+ const expectedOwner = owner;
3706
+ if (!expectedOwner) return undefined;
3707
+ const { signal, journalBindingKey, isCurrent: callerIsCurrent, afterUpdateId } = input;
3708
+ const isOwnerCurrent = () => owner === expectedOwner && expectedOwner.controller.signal === signal && !signal.aborted;
3709
+ const isCurrent = () => isOwnerCurrent() && callerIsCurrent() && deps.isContextCurrent?.(expectedOwner.ctx) !== false &&
3710
+ deps.getJournalBindingKey?.() === journalBindingKey && deps.hasAuthority(expectedOwner.ctx);
3711
+ if (!isCurrent() || !deps.journal.abandonPending || !deps.journal.inspectPendingRetention ||
3712
+ (afterUpdateId !== undefined && (!Number.isSafeInteger(afterUpdateId) || afterUpdateId < 0))) return undefined;
3713
+ const matches = [...deferredSources].filter(([id, source]) => claims.get(id) === kind &&
3714
+ source.journalBindingKey === journalBindingKey && (afterUpdateId === undefined || id > afterUpdateId))
3715
+ .sort(([a], [b]) => a - b);
3716
+ const sources = matches.slice(0, TELEGRAM_HELD_SOURCE_INSPECTION_LIMIT).map(([updateId, source]) => {
3717
+ let committed: TelegramUpdateJournalPendingAbandonmentResult | undefined;
3718
+ return { original: structuredClone(source.entry), retry(authority: TelegramDeferredUpdateAbandonmentAuthority) {
3719
+ const authorized = () => isOwnerCurrent() && authority.isCurrent() && isCurrent();
3720
+ if (!authorized()) return undefined;
3721
+ if (committed) return { ...committed, duplicate: true };
3722
+ const stillProtected = () => authorized() && (claims.get(updateId) === kind || claims.get(updateId) === "abandoning") &&
3723
+ deferredSources.get(updateId) === source;
3724
+ if (!stillProtected()) return undefined;
3725
+ committed = abandonSource({ ...authority, updateId, signal, isCurrent: stillProtected }, source);
3726
+ return committed;
3727
+ } };
3728
+ });
3729
+ return isCurrent() ? { sources,
3730
+ ...(matches.length > TELEGRAM_HELD_SOURCE_INSPECTION_LIMIT ? { nextAfterUpdateId: matches[TELEGRAM_HELD_SOURCE_INSPECTION_LIMIT - 1]![0] } : {}) } : undefined;
3731
+ };
3732
+
3733
+
3075
3734
  return {
3076
3735
  start(ctx) {
3077
3736
  if (owner) {
@@ -3082,6 +3741,7 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3082
3741
  return;
3083
3742
  }
3084
3743
  clearRetryTimer();
3744
+ clearRoutingTimer();
3085
3745
  const nowMs = getNowMs();
3086
3746
  const generation = ++nextGeneration;
3087
3747
  const queueOwnerIdentity = resolveQueueOwnerIdentity(ctx, generation);
@@ -3092,6 +3752,7 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3092
3752
  queueOwnerIdentity,
3093
3753
  };
3094
3754
  claims.clear();
3755
+ deferredSources.clear();
3095
3756
  committedQueueReceipts.clear();
3096
3757
  blocked = false;
3097
3758
  pendingSignal = true;
@@ -3104,6 +3765,8 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3104
3765
  state.journalSerializedBytes = 0;
3105
3766
  state.oldestAdmittedAtMs = undefined;
3106
3767
  state.deferredClaimCount = 0;
3768
+ delete state.abandoningClaimCount;
3769
+ delete state.historicalClaimCount;
3107
3770
  state.queuedClaimCount = 0;
3108
3771
  state.foreignQueuedCount = 0;
3109
3772
  delete state.foreignQueuedOwner;
@@ -3133,7 +3796,51 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3133
3796
  pendingSignal = true;
3134
3797
  launchDrain();
3135
3798
  },
3136
- settleDeferred(input) {
3799
+ armRoutingInput(input) { return mutateRoutingSource(input, false)?.lifetime; },
3800
+ selectRoutingInput(input) { return mutateRoutingSource(input, true)?.issued === true; },
3801
+ isRoutingInputCurrent(input) {
3802
+ return owner?.controller.signal === input.signal && !input.signal.aborted &&
3803
+ deferredSources.has(input.updateId) && claims.get(input.updateId) !== "abandoning";
3804
+ },
3805
+ inspectDeferredSource(input) {
3806
+ const expectedOwner = owner;
3807
+ const source = deferredSources.get(input.updateId);
3808
+ const current = (): boolean => {
3809
+ if (!expectedOwner || owner !== expectedOwner || input.signal.aborted || expectedOwner.controller.signal !== input.signal ||
3810
+ !source || deferredSources.get(input.updateId) !== source || claims.get(input.updateId) !== "deferred" ||
3811
+ deps.getJournalBindingKey?.() !== source.journalBindingKey || deps.isContextCurrent?.(expectedOwner.ctx) === false ||
3812
+ !deps.hasAuthority(expectedOwner.ctx)) return false;
3813
+ return isQueueOwnerIdentityCurrent(expectedOwner);
3814
+ };
3815
+ if (!current()) return undefined;
3816
+ const snapshot = deps.journal.read();
3817
+ const entry = snapshot.entries.find(value => value.updateId === input.updateId);
3818
+ if (!isTelegramUpdateJournalLegacyFamilyVersion(snapshot.version) || !entry ||
3819
+ JSON.stringify(entry) !== JSON.stringify(source!.entry) || !current()) return undefined;
3820
+ return { journalBindingKey: source!.journalBindingKey, ...createTelegramUpdateJournalEntryDigest(entry) };
3821
+ },
3822
+ isHistoricalSource(input) {
3823
+ const source = deferredSources.get(input.updateId);
3824
+ const isCurrent = () => owner?.controller.signal === input.signal && !input.signal.aborted &&
3825
+ source?.historical === true && deferredSources.get(input.updateId) === source;
3826
+ if (!isCurrent()) return false;
3827
+ // Domain eligibility observes original evidence, never mutable handler projections.
3828
+ return (!input.matchesOriginal || input.matchesOriginal(structuredClone(source!.entry)) === true) && isCurrent();
3829
+ },
3830
+ supportsDeferredAbandonment(input) {
3831
+ const expectedOwner = owner;
3832
+ const source = deferredSources.get(input.updateId);
3833
+ if (!expectedOwner || input.signal.aborted || expectedOwner.controller.signal !== input.signal ||
3834
+ !source || source.historical || input.journalBindingKey !== source.journalBindingKey ||
3835
+ !deps.journal.abandonPending || !deps.journal.inspectPendingRetention ||
3836
+ claims.get(input.updateId) === "queued") return false;
3837
+ return deps.isContextCurrent?.(expectedOwner.ctx) !== false &&
3838
+ deps.getJournalBindingKey?.() === source.journalBindingKey && deps.hasAuthority(expectedOwner.ctx);
3839
+ },
3840
+ abandonDeferred(input) { return abandonSource(input); },
3841
+ inspectAbandoning(input) { return inspectRecovery(input, "abandoning"); },
3842
+ inspectHistorical(input) { return inspectRecovery(input, "historical"); },
3843
+ async settleDeferred(input) {
3137
3844
  const expectedOwner = owner;
3138
3845
  if (
3139
3846
  !expectedOwner ||
@@ -3154,10 +3861,20 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3154
3861
  return;
3155
3862
  }
3156
3863
  const claim = claims.get(input.updateId);
3864
+ if (claim === "abandoning" || claim === "historical" || claim === "retained" ||
3865
+ (!claim && routingJournal?.inspectExpiry(input.updateId))) return;
3157
3866
  if (input.outcome.kind === "complete") {
3158
3867
  // Expiry may retire only a still-deferred source, never accepted queue work.
3159
3868
  if (claim !== "deferred") return;
3160
- const result = commitCompletedBatch(expectedOwner, [input.updateId]);
3869
+ let completion: TelegramUpdateAdmissionOutcome;
3870
+ try { completion = validateTelegramUpdateAdmissionOutcome(input.outcome, input.updateId, new Set([input.updateId])); }
3871
+ catch (error) {
3872
+ blockWithFailure("invalid-outcome", "late-invalid-completion", error, input.updateId);
3873
+ return;
3874
+ }
3875
+ if (completion.kind !== "complete") return;
3876
+ const result = commitCompletedBatch(expectedOwner, [{ updateId: input.updateId,
3877
+ ...(completion.expectedSource ? { expectedSource: completion.expectedSource } : {}) }]);
3161
3878
  if (!result) transition("idle", input.updateId);
3162
3879
  return;
3163
3880
  }
@@ -3182,7 +3899,7 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3182
3899
  outcome = validateTelegramUpdateAdmissionOutcome(
3183
3900
  input.outcome,
3184
3901
  input.updateId,
3185
- new Set([input.updateId, ...claims.keys()]),
3902
+ new Set([input.updateId, ...[...claims].filter(([, claim]) => claim !== "abandoning" && claim !== "historical" && claim !== "retained").map(([id]) => id)]),
3186
3903
  );
3187
3904
  } catch (error) {
3188
3905
  blockWithFailure(
@@ -3217,11 +3934,8 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3217
3934
  releaseDeferredClaims(outcome.sourceUpdateIds);
3218
3935
  return;
3219
3936
  }
3220
- const result = commitQueuedOutcome(
3221
- expectedOwner,
3222
- input.updateId,
3223
- outcome,
3224
- );
3937
+ const publication = commitQueuedOutcome(expectedOwner, input.updateId, outcome);
3938
+ const result = typeof publication === "string" ? publication : await publication;
3225
3939
  if (result === "blocked") {
3226
3940
  releaseDeferredClaims(outcome.sourceUpdateIds);
3227
3941
  return;
@@ -3229,7 +3943,7 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3229
3943
  if (result === "aborted") return;
3230
3944
  transition("queued", input.updateId);
3231
3945
  },
3232
- settleCustodied(input) {
3946
+ async settleCustodied(input) {
3233
3947
  const expectedOwner = owner;
3234
3948
  if (!expectedOwner || expectedOwner.controller.signal !== input.signal || input.signal.aborted)
3235
3949
  return;
@@ -3277,7 +3991,7 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3277
3991
  }
3278
3992
  for (const sourceUpdateId of receipt.sourceUpdateIds) claims.set(sourceUpdateId, "queued");
3279
3993
  try {
3280
- publishCommittedQueueReceipt(receipt, input.result.queueReceipt.queueOwner, expectedOwner.ctx);
3994
+ await publishCommittedQueueReceipt(receipt, input.result.queueReceipt.queueOwner, expectedOwner);
3281
3995
  } catch (error) {
3282
3996
  blockWithFailure("invalid-outcome", "queue-receipt-publish", error, input.updateId);
3283
3997
  return;
@@ -3289,7 +4003,19 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3289
4003
  const committed = getCurrentQueueReceipt(receipt);
3290
4004
  return committed ? { ...committed.queueOwner } : undefined;
3291
4005
  },
3292
- completeQueueReceipts(input) {
4006
+ getQueueReceiptSettlementOwner(receipt, ctx, reason) {
4007
+ const expectedOwner = owner, committed = committedQueueReceipts.get(receipt.receiptId);
4008
+ if (!expectedOwner || expectedOwner.controller.signal.aborted ||
4009
+ !(deps.isContextCurrent?.(ctx) ?? expectedOwner.ctx === ctx) || !isQueueOwnerIdentityCurrent(expectedOwner) ||
4010
+ !committed || !areTelegramQueueAdmissionReceiptsEqual(committed.receipt, normalizeQueueReceipt(receipt)) ||
4011
+ (deps.getJournalBindingKey && committed.receipt.journalBindingKey !== deps.getJournalBindingKey()) ||
4012
+ !isTelegramUpdateJournalQueueOwnerProcess(committed.queueOwner, expectedOwner.queueOwnerIdentity)) return undefined;
4013
+ const attempt = scopedQueueCompletionAttempts.get(receipt.receiptId);
4014
+ if (attempt ? attempt.owner !== expectedOwner || attempt.reason !== reason || attempt.journalBindingKey !== deps.getJournalBindingKey?.()
4015
+ : !getCurrentQueueReceipt(receipt)) return undefined;
4016
+ return { ...committed.queueOwner };
4017
+ },
4018
+ completeQueueReceipts: function completeQueueReceipts(input): boolean {
3293
4019
  const expectedOwner = owner;
3294
4020
  if (
3295
4021
  !expectedOwner ||
@@ -3298,8 +4024,10 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3298
4024
  ) {
3299
4025
  return false;
3300
4026
  }
3301
- if (input.receipts.length === 0) return true;
3302
- const normalizedReceipts = input.receipts.map(normalizeQueueReceipt);
4027
+ // An ACK permits only local completion reconciliation, never execution readiness.
4028
+ const receipts = input.sourceCompletions === undefined ? input.receipts.filter(receipt => !isQueueReceiptCompletionAcknowledged(receipt)) : input.receipts;
4029
+ if (receipts.length === 0) return input.sourceCompletions === undefined;
4030
+ const normalizedReceipts = receipts.map(normalizeQueueReceipt);
3303
4031
  const receiptIds = new Set<string>();
3304
4032
  const sourceUpdateIds = new Set<number>();
3305
4033
  const queuedCompletions: Array<{
@@ -3343,11 +4071,87 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3343
4071
  sourceUpdateIds.add(updateId);
3344
4072
  }
3345
4073
  }
4074
+ const completionBinding = deps.getJournalBindingKey?.();
4075
+ const ordinaryObserverBound = normalizedReceipts.every(receipt =>
4076
+ committedQueueReceipts.get(receipt.receiptId)!.receipt.journalBindingKey === completionBinding);
4077
+ const ordinaryCompletionCurrent = (): boolean => ordinaryObserverBound && owner === expectedOwner &&
4078
+ !expectedOwner.controller.signal.aborted && (deps.isContextCurrent?.(input.ctx) ?? expectedOwner.ctx === input.ctx) &&
4079
+ isQueueOwnerIdentityCurrent(expectedOwner) && deps.getJournalBindingKey?.() === completionBinding;
3346
4080
  let removedUpdateIds: readonly number[];
4081
+ let scopedCompletionCurrent: (() => boolean) | undefined;
3347
4082
  try {
3348
- removedUpdateIds = deps.journal.completeQueued(
3349
- queuedCompletions,
3350
- ).removedUpdateIds;
4083
+ const prepared = normalizedReceipts.flatMap(receipt => committedQueueReceipts.get(receipt.receiptId)!.sourceCompletions ?? []);
4084
+ const sourceCompletions = input.sourceCompletions === undefined ? (prepared.length ? prepared : undefined) : input.sourceCompletions;
4085
+ if (input.sourceCompletions !== undefined && prepared.some(expected => !input.sourceCompletions!.some(value => isDeepStrictEqual(value, expected)))) {
4086
+ throw new Error("Telegram queued completion conflicts with its prepared source scopes.");
4087
+ }
4088
+ if (input.sourceCompletions === undefined && prepared.length && receipts.some(receipt => !requiredScopedQueueReceipts.has(receipt.receiptId))) {
4089
+ const scoped = receipts.filter(receipt => requiredScopedQueueReceipts.has(receipt.receiptId));
4090
+ const ordinary = receipts.filter(receipt => !requiredScopedQueueReceipts.has(receipt.receiptId));
4091
+ if (!scoped.length || !ordinary.length) throw new Error("Telegram mixed queue completion has incomplete scoped authority.");
4092
+ const binding = deps.getJournalBindingKey?.();
4093
+ const current = (): boolean => owner === expectedOwner && !expectedOwner.controller.signal.aborted &&
4094
+ (deps.isContextCurrent?.(input.ctx) ?? expectedOwner.ctx === input.ctx) && isQueueOwnerIdentityCurrent(expectedOwner) &&
4095
+ !!binding && deps.getJournalBindingKey?.() === binding;
4096
+ // Independent whole receipts need independent ACKs. Ordinary siblings must not acquire invented scopes.
4097
+ if (!current() || !completeQueueReceipts({ receipts: scoped, ctx: input.ctx, reason: input.reason }) || !current()) return false;
4098
+ return completeQueueReceipts({ receipts: ordinary, ctx: input.ctx, reason: input.reason });
4099
+ }
4100
+ if (sourceCompletions === undefined) {
4101
+ if (queuedCompletions.some(receipt => requiredScopedQueueReceipts.has(receipt.receiptId))) {
4102
+ throw new Error("Telegram scoped queue completion cannot downgrade required source scopes.");
4103
+ }
4104
+ removedUpdateIds = deps.journal.completeQueued(queuedCompletions).removedUpdateIds;
4105
+ } else {
4106
+ for (const receipt of queuedCompletions) requiredScopedQueueReceipts.add(receipt.receiptId);
4107
+ queuedCompletions.sort((a, b) => a.receiptId < b.receiptId ? -1 : a.receiptId > b.receiptId ? 1 : 0);
4108
+ const journalBindingKey = deps.getJournalBindingKey?.();
4109
+ const current = (): boolean => owner === expectedOwner && !expectedOwner.controller.signal.aborted &&
4110
+ (deps.isContextCurrent?.(input.ctx) ?? expectedOwner.ctx === input.ctx) && isQueueOwnerIdentityCurrent(expectedOwner) &&
4111
+ deps.getJournalBindingKey?.() === journalBindingKey;
4112
+ if (!journalBindingKey || !current() || !completeQueuedExact || !inspectCompletion ||
4113
+ !Array.isArray(sourceCompletions) || input.sourceCompletions !== undefined && sourceCompletions.length !== sourceUpdateIds.size) {
4114
+ throw new Error("Telegram scoped queue completion requires full current source authority.");
4115
+ }
4116
+ scopedCompletionCurrent = current;
4117
+ const completions = normalizeTelegramQueueSourceCompletions(sourceCompletions, sourceUpdateIds, journalBindingKey, input.sourceCompletions !== undefined)
4118
+ .map(({ journalBindingKey: _binding, ...completion }) => completion);
4119
+ if (queuedCompletions.some(receipt => !completions.some(scope => receipt.sourceUpdateIds.includes(scope.updateId)))) {
4120
+ throw new Error("Telegram scoped queue completion requires a proven origin for every whole receipt.");
4121
+ }
4122
+ const previous = queuedCompletions.map(receipt => scopedQueueCompletionAttempts.get(receipt.receiptId));
4123
+ const retained = previous.find(attempt => attempt !== undefined);
4124
+ if (retained && (previous.some(attempt => attempt !== retained) || retained.owner !== expectedOwner ||
4125
+ retained.journalBindingKey !== journalBindingKey || retained.reason !== input.reason ||
4126
+ !isDeepStrictEqual(retained.receipts, queuedCompletions) || !isDeepStrictEqual(retained.completions, completions))) {
4127
+ throw new Error("Telegram scoped queue completion has conflicting issued authority.");
4128
+ }
4129
+ if (!retained) {
4130
+ const attempt = { owner: expectedOwner, journalBindingKey, reason: input.reason,
4131
+ receipts: queuedCompletions.map(receipt => ({ ...receipt, sourceUpdateIds: [...receipt.sourceUpdateIds], queueOwner: { ...receipt.queueOwner } })),
4132
+ completions: completions.map(completion => ({ ...completion })) };
4133
+ if (!current()) throw new Error("Telegram scoped queue completion authority changed before disposal.");
4134
+ // Mark before issuance: an exception may follow a committed rename, and cannot license another disposal.
4135
+ for (const receipt of queuedCompletions) scopedQueueCompletionAttempts.set(receipt.receiptId, attempt);
4136
+ const result = completeQueuedExact(queuedCompletions.map(receipt => ({ ...receipt, sourceUpdateIds: [...receipt.sourceUpdateIds], queueOwner: { ...receipt.queueOwner } })),
4137
+ completions.map(completion => ({ ...completion })));
4138
+ if (!current() || !isDeepStrictEqual(result.sourceCompletions, completions)) {
4139
+ throw new Error("Telegram scoped queue completion ACK was not confirmed.");
4140
+ }
4141
+ const removed = new Set(result.removedUpdateIds);
4142
+ if (removed.size !== sourceUpdateIds.size || [...sourceUpdateIds].some(id => !removed.has(id))) {
4143
+ throw new Error("Telegram scoped queue completion did not remove every source.");
4144
+ }
4145
+ }
4146
+ for (const completion of completions) {
4147
+ if (!current() || !isDeepStrictEqual(inspectCompletion({ ...completion }), completion) || !current()) {
4148
+ throw new Error("Telegram scoped queue completion ACK was not retained under current authority.");
4149
+ }
4150
+ }
4151
+ // Every immutable whole receipt has a scoped queued-origin witness; native queued ACK continuity proves its complete removal.
4152
+ // Unscoped siblings receive only receipt acknowledgement, never a source marker or absence-based proof.
4153
+ removedUpdateIds = [...sourceUpdateIds];
4154
+ }
3351
4155
  } catch (error) {
3352
4156
  blockWithFailure(
3353
4157
  "journal-write",
@@ -3370,12 +4174,14 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3370
4174
  );
3371
4175
  return false;
3372
4176
  }
3373
- input.receipts.forEach((receipt, index) => {
4177
+ receipts.forEach((receipt, index) => {
3374
4178
  acknowledgedQueueReceiptCompletions.set(receipt, normalizedReceipts[index]!);
3375
4179
  });
3376
4180
  for (const updateId of sourceUpdateIds) claims.delete(updateId);
3377
4181
  for (const receiptId of receiptIds) {
3378
4182
  committedQueueReceipts.delete(receiptId);
4183
+ requiredScopedQueueReceipts.delete(receiptId);
4184
+ scopedQueueCompletionAttempts.delete(receiptId);
3379
4185
  }
3380
4186
  const completedUpdateIds = [...sourceUpdateIds];
3381
4187
  state.lastCompletedUpdateId = Math.max(
@@ -3389,8 +4195,19 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3389
4195
  );
3390
4196
  updateClaimCounts();
3391
4197
  notifyStateChange();
3392
- pendingSignal = true;
3393
- launchDrain();
4198
+ if (scopedCompletionCurrent) for (const receipt of normalizedReceipts) {
4199
+ if (!scopedCompletionCurrent()) break;
4200
+ try { observeQueuedCompletion?.(normalizeQueueReceipt(receipt), input.ctx); }
4201
+ catch (error) { recordRuntimeEvent(error, { phase: "queue-receipt-completion-observer", receiptId: receipt.receiptId }); }
4202
+ }
4203
+ // Ordinary queued disposal is source completion too; notify only after a whole positive ACK and fresh origin authority.
4204
+ if (!scopedCompletionCurrent) for (const updateId of completedUpdateIds) {
4205
+ try {
4206
+ if (!ordinaryCompletionCurrent()) break;
4207
+ deps.onUpdateCompleted?.(updateId, input.ctx, completionBinding);
4208
+ } catch (error) { recordRuntimeEvent(error, { phase: "queue-source-completion-observer", updateId }); }
4209
+ }
4210
+ if (!scopedCompletionCurrent || scopedCompletionCurrent()) { pendingSignal = true; launchDrain(); }
3394
4211
  return true;
3395
4212
  },
3396
4213
  async stop() {
@@ -3398,12 +4215,14 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3398
4215
  if (!expectedOwner) return;
3399
4216
  pendingSignal = false;
3400
4217
  clearRetryTimer();
4218
+ clearRoutingTimer();
3401
4219
  expectedOwner.controller.abort();
3402
4220
  await drainPromise?.catch(() => undefined);
3403
4221
  if (owner !== expectedOwner) return;
3404
4222
  owner = undefined;
3405
4223
  drainPromise = undefined;
3406
4224
  claims.clear();
4225
+ deferredSources.clear();
3407
4226
  committedQueueReceipts.clear();
3408
4227
  blocked = false;
3409
4228
  state.phase = "stopped";
@@ -3411,6 +4230,8 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3411
4230
  state.currentUpdateId = undefined;
3412
4231
  state.blockedReason = undefined;
3413
4232
  state.deferredClaimCount = 0;
4233
+ delete state.abandoningClaimCount;
4234
+ delete state.historicalClaimCount;
3414
4235
  state.queuedClaimCount = 0;
3415
4236
  state.foreignQueuedCount = 0;
3416
4237
  delete state.foreignQueuedOwner;
@@ -3429,8 +4250,9 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3429
4250
  state.unsettledExecutionCount = unsettledExecutions.size;
3430
4251
  notifyStateChange();
3431
4252
  },
3432
- waitForDrain() {
3433
- return drainPromise ?? Promise.resolve();
4253
+ async waitForDrain() {
4254
+ await (drainPromise ?? Promise.resolve());
4255
+ await Promise.allSettled([...pendingQueuePublications.values()].map(value => value.task));
3434
4256
  },
3435
4257
  getState() {
3436
4258
  return getTelegramUpdateWorkerStateSnapshot(state);
@@ -3647,6 +4469,15 @@ export interface TelegramUpdateAdmissionHandleDeps<
3647
4469
  },
3648
4470
  ) => void | Promise<void>;
3649
4471
  onLateOutcomeError?: (error: unknown, updateId: number) => void;
4472
+ abandonDeferred?: TelegramUpdateWorkerRuntime<TContext>["abandonDeferred"];
4473
+ armRoutingInput?: TelegramUpdateWorkerRuntime<TContext>["armRoutingInput"];
4474
+ selectRoutingInput?: TelegramUpdateWorkerRuntime<TContext>["selectRoutingInput"];
4475
+ isRoutingInputCurrent?: TelegramUpdateWorkerRuntime<TContext>["isRoutingInputCurrent"];
4476
+ supportsDeferredAbandonment?: TelegramUpdateWorkerRuntime<TContext>["supportsDeferredAbandonment"];
4477
+ inspectAbandoning?: TelegramUpdateWorkerRuntime<TContext>["inspectAbandoning"];
4478
+ inspectHistorical?: TelegramUpdateWorkerRuntime<TContext>["inspectHistorical"];
4479
+ inspectDeferredSource?: TelegramUpdateWorkerRuntime<TContext>["inspectDeferredSource"];
4480
+ isHistoricalSource?: TelegramUpdateWorkerRuntime<TContext>["isHistoricalSource"];
3650
4481
  }
3651
4482
 
3652
4483
  function mergeTelegramReportedAdmissionOutcome(
@@ -3656,7 +4487,17 @@ function mergeTelegramReportedAdmissionOutcome(
3656
4487
  ): TelegramUpdateAdmissionOutcome {
3657
4488
  if (!current || current.kind === "deferred") return next;
3658
4489
  if (next.kind === "deferred") return current;
3659
- if (current.kind === "complete" && next.kind === "complete") return current;
4490
+ if (current.kind === "complete" && next.kind === "complete") {
4491
+ if (!current.expectedSource) return next;
4492
+ if (!next.expectedSource) return current;
4493
+ if (current.expectedSource.updateId === next.expectedSource.updateId &&
4494
+ current.expectedSource.journalBindingKey === next.expectedSource.journalBindingKey &&
4495
+ current.expectedSource.sourceSha256 === next.expectedSource.sourceSha256) {
4496
+ if (!current.expectedSource.completionSha256) return next;
4497
+ if (!next.expectedSource.completionSha256 || current.expectedSource.completionSha256 === next.expectedSource.completionSha256) return current;
4498
+ }
4499
+ throw new TelegramUpdateAdmissionOutcomeError(`Telegram update ${updateId} reported conflicting completion sources.`);
4500
+ }
3660
4501
  if (current.kind === "queued" && next.kind === "queued" &&
3661
4502
  areTelegramQueueAdmissionReceiptsEqual(current, next)) return current;
3662
4503
  throw new TelegramUpdateAdmissionOutcomeError(
@@ -4650,13 +5491,22 @@ export function createTelegramUpdateAdmissionHandle<
4650
5491
  let nextExecutionGeneration = 0;
4651
5492
  return async (update, ctx, signal) => {
4652
5493
  const generation = ++nextExecutionGeneration;
5494
+ let suspended = false;
5495
+ let routingClaims = 0;
5496
+ let abandoned: TelegramUpdateJournalPendingAbandonmentResult | undefined;
5497
+ let routingLifetime: TelegramUpdateJournalRoutingInput | undefined;
5498
+ let routingSourceIds: readonly number[] | undefined;
5499
+ let routingClockUnknown = false;
5500
+ let routingSelectionAttempted = false;
5501
+ let routingSelectionConfirmed = false;
4653
5502
  const execution: TelegramUpdateExecutionFence = {
4654
5503
  generation,
4655
5504
  updateId: update.update_id,
4656
5505
  signal,
4657
- isCurrent: () => !signal.aborted,
5506
+ isCurrent: () => !signal.aborted && !suspended && ((!routingLifetime && !routingClockUnknown) || immediate || settlementReported ||
5507
+ deps.isRoutingInputCurrent?.({ updateId: update.update_id, signal }) !== false),
4658
5508
  assertCurrent() {
4659
- if (signal.aborted) {
5509
+ if (!execution.isCurrent()) {
4660
5510
  throw signal.reason ?? new DOMException("Aborted", "AbortError");
4661
5511
  }
4662
5512
  },
@@ -4666,9 +5516,18 @@ export function createTelegramUpdateAdmissionHandle<
4666
5516
  execution.assertCurrent();
4667
5517
  if (verdict === "consume") return { kind: "complete" };
4668
5518
  let immediate = true;
5519
+ let settlementReported = false;
4669
5520
  let outcome: TelegramUpdateAdmissionOutcome | undefined;
4670
5521
  const boundUpdate = bindTelegramUpdateExecutionFence(
4671
5522
  bindTelegramUpdateAdmissionSource(update, (next) => {
5523
+ if (!execution.isCurrent()) return;
5524
+ if (next.kind === "deferred" && next.routingReview) {
5525
+ if (!immediate || settlementReported || routingClaims > 0 || !deps.isHistoricalSource?.({ updateId: update.update_id, signal })) {
5526
+ throw new TelegramUpdateAdmissionOutcomeError("Historical review cannot override accepted or selected work.");
5527
+ }
5528
+ suspended = true;
5529
+ }
5530
+ if (next.kind !== "deferred") settlementReported = true;
4672
5531
  if (immediate) {
4673
5532
  outcome = mergeTelegramReportedAdmissionOutcome(
4674
5533
  outcome,
@@ -4683,13 +5542,14 @@ export function createTelegramUpdateAdmissionHandle<
4683
5542
  );
4684
5543
  }
4685
5544
  void Promise.resolve()
4686
- .then(() =>
4687
- deps.onLateOutcome!(next, {
5545
+ .then(() => {
5546
+ if (!execution.isCurrent()) return;
5547
+ return deps.onLateOutcome!(next, {
4688
5548
  updateId: update.update_id,
4689
5549
  ctx,
4690
5550
  signal,
4691
- }),
4692
- )
5551
+ });
5552
+ })
4693
5553
  .catch((error) => {
4694
5554
  try {
4695
5555
  deps.onLateOutcomeError?.(error, update.update_id);
@@ -4697,13 +5557,64 @@ export function createTelegramUpdateAdmissionHandle<
4697
5557
  // Diagnostic sinks must not create an unhandled late Promise.
4698
5558
  }
4699
5559
  });
4700
- }),
5560
+ }, { abandon: (authority) => {
5561
+ const isCurrent = () => !signal.aborted && routingClaims === 0 && !settlementReported && authority.isCurrent();
5562
+ if (immediate || outcome?.kind !== "deferred" || !deps.abandonDeferred || !isCurrent()) return undefined;
5563
+ if (abandoned) return { ...abandoned, duplicate: true };
5564
+ const wasSuspended = suspended;
5565
+ suspended = true;
5566
+ // Failed/unknown publication keeps dispatch suspended for exact cancellation retry.
5567
+ // An ineligible request that attempted no publication remains inert.
5568
+ const result = deps.abandonDeferred({ ...authority, updateId: update.update_id, signal, isCurrent });
5569
+ if (result) abandoned = result;
5570
+ else suspended = wasSuspended;
5571
+ return result;
5572
+ }, inspectAbandoning: request => execution.isCurrent()
5573
+ ? deps.inspectAbandoning?.({ ...request, signal }) : undefined,
5574
+ inspectHistorical: request => execution.isCurrent() ? deps.inspectHistorical?.({ ...request, signal }) : undefined,
5575
+ inspectSource: () => execution.isCurrent() && !immediate && !settlementReported
5576
+ ? deps.inspectDeferredSource?.({ updateId: update.update_id, signal }) : undefined,
5577
+ isHistorical: matchesOriginal => !signal.aborted && deps.isHistoricalSource?.({ updateId: update.update_id, signal,
5578
+ ...(matchesOriginal ? { matchesOriginal } : {}) }) === true,
5579
+ isHistoricalReviewHeld: () => !signal.aborted && suspended && outcome?.kind === "deferred" && outcome.routingReview === true,
5580
+ supportsAbandonment: journalBindingKey => !signal.aborted && !settlementReported &&
5581
+ deps.supportsDeferredAbandonment?.({ updateId: update.update_id, signal, journalBindingKey }) === true,
5582
+ armRoutingInput: (operatorUserId, sourceUpdateIds) => {
5583
+ execution.assertCurrent();
5584
+ if (settlementReported || routingClaims || suspended) return undefined;
5585
+ if (routingClockUnknown) throw new TelegramUpdateAdmissionOutcomeError("Telegram routing input clock publication is unconfirmed.");
5586
+ const ids = [...sourceUpdateIds];
5587
+ if (routingSourceIds && !isDeepStrictEqual(ids, routingSourceIds)) throw new TelegramUpdateAdmissionOutcomeError("Telegram routing input source membership changed.");
5588
+ try { routingLifetime = deps.armRoutingInput?.({ updateId: update.update_id, signal, operatorUserId, sourceUpdateIds: ids }); }
5589
+ catch (error) { routingClockUnknown = true; throw error; }
5590
+ if (routingLifetime) routingSourceIds = ids;
5591
+ return routingLifetime ? { ...routingLifetime } : undefined;
5592
+ },
5593
+ acquireRouting: (select = false, sourceUpdateIds) => {
5594
+ execution.assertCurrent();
5595
+ if (select && routingClockUnknown) throw new TelegramUpdateAdmissionOutcomeError("Telegram routing input clock publication is unconfirmed; selection is protected.");
5596
+ if (select && routingLifetime && !routingSelectionConfirmed) {
5597
+ if (routingSelectionAttempted) throw new TelegramUpdateAdmissionOutcomeError("Telegram routing selection is unconfirmed; no replay is permitted.");
5598
+ if (!isDeepStrictEqual(sourceUpdateIds ?? [update.update_id], routingSourceIds)) throw new TelegramUpdateAdmissionOutcomeError("Telegram routing input selection changed its source membership.");
5599
+ routingSelectionAttempted = true;
5600
+ routingSelectionConfirmed = deps.selectRoutingInput?.({ updateId: update.update_id, signal,
5601
+ operatorUserId: routingLifetime.operatorUserId, sourceUpdateIds: routingSourceIds! }) === true;
5602
+ if (!routingSelectionConfirmed) throw new TelegramUpdateAdmissionOutcomeError("Telegram routing input choice is expired or unavailable.");
5603
+ }
5604
+ routingClaims += 1;
5605
+ let released = false;
5606
+ return () => {
5607
+ if (released) return;
5608
+ released = true;
5609
+ routingClaims -= 1;
5610
+ };
5611
+ } }),
4701
5612
  execution,
4702
5613
  );
4703
5614
  try {
4704
5615
  execution.assertCurrent();
4705
5616
  await deps.defaultHandle(boundUpdate, ctx, execution);
4706
- execution.assertCurrent();
5617
+ if (outcome?.kind !== "deferred" || !outcome.routingReview || signal.aborted) execution.assertCurrent();
4707
5618
  } finally {
4708
5619
  immediate = false;
4709
5620
  }
@@ -4715,7 +5626,7 @@ export function createTelegramCustodiedUpdateAdmissionHandle<
4715
5626
  TUpdate extends TelegramJournaledUpdate & TelegramUpdateFlow,
4716
5627
  TContext,
4717
5628
  >(deps: Omit<TelegramUpdateAdmissionHandleDeps<TUpdate, TContext>,
4718
- "onLateOutcome" | "onLateOutcomeError"> & {
5629
+ "onLateOutcome" | "onLateOutcomeError" | "abandonDeferred" | "armRoutingInput" | "selectRoutingInput" | "isRoutingInputCurrent" | "supportsDeferredAbandonment" | "inspectAbandoning" | "inspectHistorical" | "inspectDeferredSource" | "isHistoricalSource"> & {
4719
5630
  journal: TelegramCustodyExecutionJournal;
4720
5631
  recipientBindingKey: string;
4721
5632
  onLateOutcomeError(error: unknown, updateId: number): void;
@@ -4755,6 +5666,8 @@ const isQueueReceiptCompletionAcknowledged = (receipt: TelegramQueueAdmissionRec
4755
5666
 
4756
5667
  export interface TelegramQueueAdmissionSettlementRuntime<TContext> {
4757
5668
  isItemReady: (item: TelegramQueueAdmissionItemLike) => boolean;
5669
+ getQueueReceiptSettlementOwner?: (receipt: TelegramQueueAdmissionReceiptLike, ctx: TContext,
5670
+ reason: TelegramQueueReceiptCompletionReason) => TelegramUpdateJournalQueueOwner | undefined;
4758
5671
  getQueueReceiptOwner: (
4759
5672
  receipt: TelegramQueueAdmissionReceiptLike,
4760
5673
  ) => TelegramUpdateJournalQueueOwner | undefined;
@@ -4782,15 +5695,20 @@ export function createTelegramQueueAdmissionSettlementMuxRuntime<TContext>(
4782
5695
  kind: "prompt" | "control" | "discard",
4783
5696
  ): boolean => {
4784
5697
  const plan = new Map<TelegramQueueAdmissionSettlementRuntime<TContext>, TelegramQueueAdmissionReceiptLike[]>();
5698
+ const reason = kind === "prompt" ? "prompt-handoff" : kind === "control" ? "control-settlement" : "discard";
5699
+ const settlementOwner = (runtime: TelegramQueueAdmissionSettlementRuntime<TContext>, receipt: TelegramQueueAdmissionReceiptLike) =>
5700
+ runtime.getQueueReceiptSettlementOwner ? runtime.getQueueReceiptSettlementOwner(receipt, ctx, reason) :
5701
+ runtime.isItemReady({ admissionReceipts: [receipt] }) ? runtime.getQueueReceiptOwner(receipt) : undefined;
4785
5702
  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] }));
5703
+ if (isQueueReceiptCompletionAcknowledged(receipt)) continue;
5704
+ const candidates = runtimes.filter(runtime => runtime.getQueueReceiptSettlementOwner ? !!settlementOwner(runtime, receipt)
5705
+ : runtime.isItemReady({ admissionReceipts: [receipt] }));
4788
5706
  const selected = candidates[0];
4789
5707
  if (!selected) return false;
4790
5708
  if (candidates.length > 1) {
4791
- const owner = selected.getQueueReceiptOwner(receipt);
5709
+ const owner = settlementOwner(selected, receipt);
4792
5710
  if (!owner || candidates.some(runtime => {
4793
- const candidate = runtime.getQueueReceiptOwner(receipt);
5711
+ const candidate = settlementOwner(runtime, receipt);
4794
5712
  return !candidate || !areTelegramUpdateJournalQueueOwnersEqual(owner, candidate);
4795
5713
  })) return false;
4796
5714
  }
@@ -4848,7 +5766,7 @@ export function createTelegramQueueAdmissionSettlementRuntime<TContext>(
4848
5766
  const receipts: TelegramQueueAdmissionReceiptLike[] = [];
4849
5767
  for (const item of items) {
4850
5768
  for (const receipt of item.admissionReceipts ?? []) {
4851
- if (reason !== "discard" || !isQueueReceiptCompletionAcknowledged(receipt)) receipts.push(receipt);
5769
+ if (!isQueueReceiptCompletionAcknowledged(receipt)) receipts.push(receipt);
4852
5770
  }
4853
5771
  }
4854
5772
  return receipts.length === 0 || worker.completeQueueReceipts({ receipts, ctx, reason });
@@ -4859,6 +5777,7 @@ export function createTelegramQueueAdmissionSettlementRuntime<TContext>(
4859
5777
  worker.isQueueReceiptCommitted,
4860
5778
  ),
4861
5779
  getQueueReceiptOwner: worker.getQueueReceiptOwner,
5780
+ ...(worker.getQueueReceiptSettlementOwner ? { getQueueReceiptSettlementOwner: worker.getQueueReceiptSettlementOwner } : {}),
4862
5781
  onPromptHandedOff: (item, ctx) =>
4863
5782
  complete([item], ctx, "prompt-handoff"),
4864
5783
  onControlSettled: (item, ctx) =>
@@ -5246,6 +6165,7 @@ export interface TelegramQueueHandoffReconciliationRuntimeAssemblyDeps<
5246
6165
  listFollowers: () => readonly TelegramBusFollowerView[];
5247
6166
  createRecipientJournalResolver: (
5248
6167
  profileKey: string,
6168
+ sessionId: string,
5249
6169
  ) => (() => { recoveryKey: string } | undefined);
5250
6170
  queueStore: {
5251
6171
  getQueuedItems: () => TelegramQueueItem<TContext>[];
@@ -5291,8 +6211,8 @@ export function createTelegramQueueHandoffReconciliationRuntimeAssembly<
5291
6211
  canHandoffWithLeader: deps.canHandoffWithLeader,
5292
6212
  listFollowers: deps.listFollowers,
5293
6213
  createRecipientJournalBindingKey(recipient) {
5294
- if (!recipient.profileKey) return undefined;
5295
- return deps.createRecipientJournalResolver(recipient.profileKey)()
6214
+ if (!recipient.profileKey || !recipient.sessionId) return undefined;
6215
+ return deps.createRecipientJournalResolver(recipient.profileKey, recipient.sessionId)()
5296
6216
  ?.recoveryKey;
5297
6217
  },
5298
6218
  getQueuedItems: deps.queueStore.getQueuedItems,
@@ -5502,6 +6422,8 @@ export interface TelegramUpdateAdmissionLifecycleRuntimeDeps<TContext> {
5502
6422
  acquireSourceReference?: (
5503
6423
  binding: TelegramUpdateAdmissionLifecycleJournalBinding,
5504
6424
  ) => () => void;
6425
+ /** Runs after the previous worker stopped and before the replacement worker exists; throwing refuses binding. */
6426
+ prepareBinding?: (binding: TelegramUpdateAdmissionLifecycleJournalBinding) => void;
5505
6427
  recordRuntimeEvent?: TelegramUpdateWorkerRuntimeDeps<TContext>["recordRuntimeEvent"];
5506
6428
  }
5507
6429
 
@@ -5558,8 +6480,8 @@ export interface TelegramUpdateAdmissionLifecycleRuntime<TContext>
5558
6480
 
5559
6481
  export interface TelegramUpdateWorkerOwnerRuntime<TContext> {
5560
6482
  getQueueOwnerIdentity: () => TelegramUpdateJournalQueueOwnerIdentity;
5561
- onQueueReceiptCommitted: (receipt: unknown, ctx: TContext) => void;
5562
- onUpdateCompleted: (updateId: number, ctx: TContext) => void;
6483
+ onQueueReceiptCommitted: (receipt: TelegramQueueAdmissionReceiptLike, ctx: TContext) => void;
6484
+ onUpdateCompleted: (updateId: number, ctx: TContext, journalBindingKey?: string) => void;
5563
6485
  }
5564
6486
 
5565
6487
  export interface TelegramUpdateWorkerOwnerRuntimeDeps<TContext> {
@@ -5570,7 +6492,8 @@ export interface TelegramUpdateWorkerOwnerRuntimeDeps<TContext> {
5570
6492
  isContextCurrent: (ctx: TContext) => boolean;
5571
6493
  dispatchNext: (ctx: TContext) => void;
5572
6494
  requestQueueHandoffReconciliation: (ctx: TContext) => void;
5573
- afterUpdateCompleted?: (updateId: number) => void;
6495
+ afterQueueReceiptCommitted?: (receipt: TelegramQueueAdmissionReceiptLike, ctx: TContext) => void;
6496
+ afterUpdateCompleted?: (updateId: number, ctx: TContext, journalBindingKey?: string) => void;
5574
6497
  }
5575
6498
 
5576
6499
  export function createTelegramUpdateWorkerOwnerRuntime<TContext>(
@@ -5585,15 +6508,16 @@ export function createTelegramUpdateWorkerOwnerRuntime<TContext>(
5585
6508
  sessionGeneration: deps.getSessionGeneration(),
5586
6509
  };
5587
6510
  },
5588
- onQueueReceiptCommitted(_receipt, ctx) {
6511
+ onQueueReceiptCommitted(receipt, ctx) {
5589
6512
  if (!deps.isContextCurrent(ctx)) return;
6513
+ deps.afterQueueReceiptCommitted?.(receipt, ctx);
5590
6514
  deps.dispatchNext(ctx);
5591
6515
  deps.requestQueueHandoffReconciliation(ctx);
5592
6516
  },
5593
- onUpdateCompleted(updateId, ctx) {
6517
+ onUpdateCompleted(updateId, ctx, journalBindingKey) {
5594
6518
  if (!deps.isContextCurrent(ctx)) return;
5595
6519
  deps.dispatchNext(ctx);
5596
- deps.afterUpdateCompleted?.(updateId);
6520
+ deps.afterUpdateCompleted?.(updateId, ctx, journalBindingKey);
5597
6521
  },
5598
6522
  };
5599
6523
  }
@@ -5625,6 +6549,7 @@ export function createTelegramUpdateAdmissionRuntimeBinding<TContext>(deps: {
5625
6549
  let follower: TelegramUpdateAdmissionLifecycleRuntime<TContext> | undefined;
5626
6550
  let settlement: TelegramQueueAdmissionSettlementRuntime<TContext> | undefined;
5627
6551
  let inputCustodyBus: TelegramInputCustodyBusBindingRuntime<TContext> | undefined;
6552
+ const getActive = () => deps.isFollowerRegistered() ? follower : leader;
5628
6553
  return {
5629
6554
  bind(input) {
5630
6555
  leader = input.leader;
@@ -5637,7 +6562,7 @@ export function createTelegramUpdateAdmissionRuntimeBinding<TContext>(deps: {
5637
6562
  },
5638
6563
  getLeader: () => leader,
5639
6564
  getFollower: () => follower,
5640
- getActive: () => (deps.isFollowerRegistered() ? follower : leader),
6565
+ getActive,
5641
6566
  getSettlement: () => settlement,
5642
6567
  getInputCustodyBus: () => inputCustodyBus,
5643
6568
  getLifecycleForJournalBinding(journalBindingKey) {
@@ -5743,6 +6668,7 @@ export function createTelegramUpdateAdmissionLifecycleRuntime<TContext>(
5743
6668
  binding.recoveryKey !== recoveryKey) {
5744
6669
  throw new Error("Telegram update source binding changed during startup.");
5745
6670
  }
6671
+ deps.prepareBinding?.(binding);
5746
6672
  const release = deps.acquireSourceReference?.(binding);
5747
6673
  const inputCustody = binding.journal.inputCustody;
5748
6674
  try {
@@ -6043,6 +6969,8 @@ export interface TelegramUpdateAdmissionLifecycleAssemblyDeps<
6043
6969
  isRegistered: () => boolean;
6044
6970
  getGeneration: () => string | undefined;
6045
6971
  prepareUpdateForExecution: (update: TUpdate) => TUpdate;
6972
+ /** Session succession before worker creation, under the exact generation-fenced binding. */
6973
+ prepareBinding?: (binding: TelegramUpdateAdmissionLifecycleJournalBinding & { hasAuthority?: () => boolean }) => void;
6046
6974
  };
6047
6975
  recordRuntimeEvent?: TelegramUpdateWorkerRuntimeDeps<TContext>["recordRuntimeEvent"];
6048
6976
  }
@@ -6073,6 +7001,15 @@ export function createTelegramUpdateAdmissionWorkerRuntime<
6073
7001
  const executeUpdate = createTelegramUpdateAdmissionHandle<TUpdate, TContext>({
6074
7002
  defaultHandle: deps.defaultHandle,
6075
7003
  registry: deps.registry,
7004
+ abandonDeferred: input => worker?.abandonDeferred?.(input),
7005
+ armRoutingInput: input => worker?.armRoutingInput?.(input),
7006
+ selectRoutingInput: input => worker?.selectRoutingInput?.(input) === true,
7007
+ isRoutingInputCurrent: input => worker?.isRoutingInputCurrent?.(input) !== false,
7008
+ supportsDeferredAbandonment: input => worker?.supportsDeferredAbandonment?.(input) === true,
7009
+ inspectAbandoning: input => worker?.inspectAbandoning?.(input),
7010
+ inspectHistorical: input => worker?.inspectHistorical?.(input),
7011
+ inspectDeferredSource: input => worker?.inspectDeferredSource?.(input),
7012
+ isHistoricalSource: input => worker?.isHistoricalSource?.(input) === true,
6076
7013
  onLateOutcome(outcome, details) {
6077
7014
  worker?.settleDeferred({
6078
7015
  updateId: details.updateId,
@@ -6155,6 +7092,7 @@ export function createTelegramUpdateAdmissionLifecycleAssembly<
6155
7092
  getQueueOwnerIdentity: deps.worker.getQueueOwnerIdentity,
6156
7093
  ...(deps.acquireSourceReference ? { acquireSourceReference: binding =>
6157
7094
  deps.acquireSourceReference!("follower", binding) } : {}),
7095
+ ...(deps.follower.prepareBinding ? { prepareBinding: deps.follower.prepareBinding } : {}),
6158
7096
  resolveBinding() {
6159
7097
  const generation = deps.follower.getGeneration();
6160
7098
  if (!deps.follower.isRegistered() || !generation) return undefined;
@@ -6179,6 +7117,11 @@ export function createTelegramUpdateAdmissionLifecycleAssembly<
6179
7117
  getJournalBindingKey: () => binding.recoveryKey,
6180
7118
  getRecipientBindingKey: () => binding.recipientBindingKey,
6181
7119
  hasAuthority: () => binding.hasAuthority?.() ?? false,
7120
+ // Recipient custody is not an orphaned direct-bot source.
7121
+ shouldReviewHistoricalInput: undefined,
7122
+ shouldHoldPendingInput: undefined,
7123
+ spendHistoricalInput: undefined,
7124
+ onHeldSourcesPrepared: undefined,
6182
7125
  prepareUpdateForExecution: deps.follower.prepareUpdateForExecution,
6183
7126
  });
6184
7127
  },