@llblab/pi-kit 0.6.0 → 0.7.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 (129) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +4 -4
  3. package/node_modules/@llblab/pi-actors/AGENTS.md +1 -1
  4. package/node_modules/@llblab/pi-actors/CHANGELOG.md +6 -0
  5. package/node_modules/@llblab/pi-actors/README.md +1 -1
  6. package/node_modules/@llblab/pi-actors/dist/lib/async-runs.d.ts +3 -0
  7. package/node_modules/@llblab/pi-actors/dist/lib/async-runs.js +14 -1
  8. package/node_modules/@llblab/pi-actors/dist/lib/command-templates.js +45 -3
  9. package/node_modules/@llblab/pi-actors/dist/lib/extension-runtime.js +1 -1
  10. package/node_modules/@llblab/pi-actors/dist/lib/observability.d.ts +16 -3
  11. package/node_modules/@llblab/pi-actors/dist/lib/observability.js +92 -7
  12. package/node_modules/@llblab/pi-actors/dist/lib/pi.d.ts +0 -1
  13. package/node_modules/@llblab/pi-actors/dist/lib/pi.js +15 -24
  14. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery-lineage.d.ts +17 -0
  15. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery-lineage.js +44 -0
  16. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery.d.ts +4 -0
  17. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery.js +102 -4
  18. package/node_modules/@llblab/pi-actors/dist/lib/run-ui-runtime.js +58 -39
  19. package/node_modules/@llblab/pi-actors/dist/lib/runtime.js +14 -6
  20. package/node_modules/@llblab/pi-actors/dist/skills/actors/SKILL.md +1 -1
  21. package/node_modules/@llblab/pi-actors/docs/async-runs.md +1 -1
  22. package/node_modules/@llblab/pi-actors/docs/coordinator-delivery.md +18 -23
  23. package/node_modules/@llblab/pi-actors/lib/async-runs.ts +18 -1
  24. package/node_modules/@llblab/pi-actors/lib/command-templates.ts +41 -3
  25. package/node_modules/@llblab/pi-actors/lib/extension-runtime.ts +1 -1
  26. package/node_modules/@llblab/pi-actors/lib/observability.ts +119 -5
  27. package/node_modules/@llblab/pi-actors/lib/pi.ts +15 -28
  28. package/node_modules/@llblab/pi-actors/lib/run-delivery-lineage.ts +68 -0
  29. package/node_modules/@llblab/pi-actors/lib/run-delivery.ts +120 -4
  30. package/node_modules/@llblab/pi-actors/lib/run-ui-runtime.ts +69 -44
  31. package/node_modules/@llblab/pi-actors/lib/runtime.ts +17 -6
  32. package/node_modules/@llblab/pi-actors/package.json +1 -1
  33. package/node_modules/@llblab/pi-actors/skills/actors/SKILL.md +1 -1
  34. package/node_modules/@llblab/pi-state-flow/AGENTS.md +33 -12
  35. package/node_modules/@llblab/pi-state-flow/BACKLOG.md +125 -2
  36. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +87 -45
  37. package/node_modules/@llblab/pi-state-flow/README.md +203 -107
  38. package/node_modules/@llblab/pi-state-flow/docs/README.md +4 -0
  39. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +188 -0
  40. package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +42 -0
  41. package/node_modules/@llblab/pi-state-flow/index.ts +164 -2
  42. package/node_modules/@llblab/pi-state-flow/lib/acquisition.ts +138 -0
  43. package/node_modules/@llblab/pi-state-flow/lib/artifact.ts +273 -0
  44. package/node_modules/@llblab/pi-state-flow/lib/config.ts +48 -0
  45. package/node_modules/@llblab/pi-state-flow/lib/context.ts +18 -5
  46. package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +268 -0
  47. package/node_modules/@llblab/pi-state-flow/lib/discovery.ts +117 -0
  48. package/node_modules/@llblab/pi-state-flow/lib/durable.ts +562 -0
  49. package/node_modules/@llblab/pi-state-flow/lib/episode.ts +24 -12
  50. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +606 -70
  51. package/node_modules/@llblab/pi-state-flow/lib/git.ts +666 -0
  52. package/node_modules/@llblab/pi-state-flow/lib/history.ts +95 -0
  53. package/node_modules/@llblab/pi-state-flow/lib/json.ts +24 -0
  54. package/node_modules/@llblab/pi-state-flow/lib/maintenance.ts +141 -0
  55. package/node_modules/@llblab/pi-state-flow/lib/memory.ts +52 -0
  56. package/node_modules/@llblab/pi-state-flow/lib/migration.ts +88 -0
  57. package/node_modules/@llblab/pi-state-flow/lib/publication.ts +296 -0
  58. package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +23 -7
  59. package/node_modules/@llblab/pi-state-flow/lib/rehydration.ts +79 -0
  60. package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +264 -0
  61. package/node_modules/@llblab/pi-state-flow/lib/session.ts +6 -0
  62. package/node_modules/@llblab/pi-state-flow/lib/skills.ts +99 -7
  63. package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +305 -48
  64. package/node_modules/@llblab/pi-state-flow/lib/state.ts +74 -7
  65. package/node_modules/@llblab/pi-state-flow/lib/status.ts +125 -6
  66. package/node_modules/@llblab/pi-state-flow/lib/storage.ts +196 -0
  67. package/node_modules/@llblab/pi-state-flow/lib/temporal.ts +233 -0
  68. package/node_modules/@llblab/pi-state-flow/lib/terminal.ts +70 -24
  69. package/node_modules/@llblab/pi-state-flow/lib/transition.ts +254 -29
  70. package/node_modules/@llblab/pi-state-flow/package.json +8 -2
  71. package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +128 -0
  72. package/node_modules/@llblab/pi-telegram/AGENTS.md +14 -9
  73. package/node_modules/@llblab/pi-telegram/BACKLOG.md +23 -5
  74. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +18 -0
  75. package/node_modules/@llblab/pi-telegram/README.md +13 -9
  76. package/node_modules/@llblab/pi-telegram/docs/README.md +1 -0
  77. package/node_modules/@llblab/pi-telegram/docs/architecture.md +227 -23
  78. package/node_modules/@llblab/pi-telegram/docs/generative-apps.md +1 -1
  79. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +70 -19
  80. package/node_modules/@llblab/pi-telegram/docs/outbound.md +6 -6
  81. package/node_modules/@llblab/pi-telegram/docs/public-api.md +13 -6
  82. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +3 -1
  83. package/node_modules/@llblab/pi-telegram/index.ts +4 -1418
  84. package/node_modules/@llblab/pi-telegram/lib/agent-messages.ts +6 -3
  85. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +46 -1
  86. package/node_modules/@llblab/pi-telegram/lib/bus-api.ts +32 -19
  87. package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +600 -135
  88. package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +962 -55
  89. package/node_modules/@llblab/pi-telegram/lib/bus.ts +355 -26
  90. package/node_modules/@llblab/pi-telegram/lib/channel-posts.ts +718 -0
  91. package/node_modules/@llblab/pi-telegram/lib/commands.ts +237 -11
  92. package/node_modules/@llblab/pi-telegram/lib/config.ts +242 -26
  93. package/node_modules/@llblab/pi-telegram/lib/extension.ts +1851 -0
  94. package/node_modules/@llblab/pi-telegram/lib/generative-apps.ts +20 -2
  95. package/node_modules/@llblab/pi-telegram/lib/journal.ts +2184 -126
  96. package/node_modules/@llblab/pi-telegram/lib/locks.ts +44 -2
  97. package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +152 -13
  98. package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +97 -10
  99. package/node_modules/@llblab/pi-telegram/lib/paths.ts +29 -0
  100. package/node_modules/@llblab/pi-telegram/lib/polling.ts +85 -17
  101. package/node_modules/@llblab/pi-telegram/lib/preview.ts +17 -0
  102. package/node_modules/@llblab/pi-telegram/lib/prompts.ts +6 -2
  103. package/node_modules/@llblab/pi-telegram/lib/queue.ts +118 -26
  104. package/node_modules/@llblab/pi-telegram/lib/rendering.ts +4 -1
  105. package/node_modules/@llblab/pi-telegram/lib/replies.ts +21 -2
  106. package/node_modules/@llblab/pi-telegram/lib/routing.ts +344 -112
  107. package/node_modules/@llblab/pi-telegram/lib/setup.ts +44 -4
  108. package/node_modules/@llblab/pi-telegram/lib/status.ts +51 -4
  109. package/node_modules/@llblab/pi-telegram/lib/sync.ts +308 -39
  110. package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +353 -22
  111. package/node_modules/@llblab/pi-telegram/lib/thread-cleanup-manager.ts +664 -0
  112. package/node_modules/@llblab/pi-telegram/lib/thread-display.ts +226 -0
  113. package/node_modules/@llblab/pi-telegram/lib/thread-naming.ts +118 -0
  114. package/node_modules/@llblab/pi-telegram/lib/threads.ts +1686 -129
  115. package/node_modules/@llblab/pi-telegram/lib/turns.ts +7 -0
  116. package/node_modules/@llblab/pi-telegram/lib/updates.ts +1319 -97
  117. package/node_modules/@llblab/pi-telegram/lib/workspace-admission.ts +1643 -0
  118. package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +968 -0
  119. package/node_modules/@llblab/pi-telegram/lib/workspace-slots.ts +84 -0
  120. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  121. package/node_modules/@llblab/pi-telegram/screenshot.png +0 -0
  122. package/node_modules/@llblab/pi-telegram/scripts/measure-bus.mjs +83 -0
  123. package/node_modules/@llblab/pi-telegram/scripts/measure-workspace.mjs +101 -0
  124. package/node_modules/@llblab/{skills → pi-telegram/skills}/show-me/SKILL.md +28 -6
  125. package/node_modules/@llblab/pi-telegram/skills/show-me/references/telegram-surfaces.md +43 -0
  126. package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/delivery-and-threads.md +1 -1
  127. package/node_modules/@llblab/skills/package.json +2 -3
  128. package/package.json +6 -5
  129. /package/node_modules/@llblab/pi-telegram/lib/{logs.ts → logging.ts} +0 -0
@@ -19,6 +19,9 @@ import type {
19
19
  } from "./bus.ts";
20
20
  import type { TelegramMessageOwnershipStore } from "./ownership.ts";
21
21
  import {
22
+ TELEGRAM_UPDATE_JOURNAL_VERSION,
23
+ TELEGRAM_UPDATE_JOURNAL_EXCLUSION_VERSION,
24
+ TELEGRAM_UPDATE_JOURNAL_CUSTODY_VERSION,
22
25
  TELEGRAM_UPDATE_JOURNAL_FAILURE_CLASS_MAX_LENGTH,
23
26
  TELEGRAM_UPDATE_JOURNAL_FAILURE_SUMMARY_MAX_LENGTH,
24
27
  TELEGRAM_UPDATE_JOURNAL_QUEUE_OWNER_ID_MAX_LENGTH,
@@ -26,8 +29,13 @@ import {
26
29
  getTelegramUpdateJournalBindingPath,
27
30
  isTelegramUpdateJournalQueueOwnerProcess,
28
31
  parseTelegramUpdateJournalQueueOwner,
32
+ type TelegramInputJournalReceipt,
33
+ type TelegramInputJournalSourceReference,
34
+ type TelegramInputJournalStore,
29
35
  type TelegramJournaledUpdate,
30
36
  type TelegramUpdateJournalDeadQueueOwnerRecoveryResult,
37
+ type TelegramUpdateJournalAppendResult,
38
+ type TelegramUpdateJournalInputClaim,
31
39
  type TelegramUpdateJournalOperatorDispositionInput,
32
40
  type TelegramUpdateJournalOperatorDispositionResult,
33
41
  type TelegramUpdateJournalQueueDiscardResult,
@@ -445,6 +453,7 @@ function rejectTelegramForeignUpdateSettlement(
445
453
  export interface TelegramMessageReactionUpdated {
446
454
  chat: { id?: number; type: string };
447
455
  user?: TelegramUser;
456
+ actor_chat?: unknown;
448
457
  message_id: number;
449
458
  old_reaction: TelegramReactionType[];
450
459
  new_reaction: TelegramReactionType[];
@@ -1121,16 +1130,23 @@ function getForeignTelegramCallbackOwnership(
1121
1130
  getTargetOwnership?: TelegramTargetOwnershipLookup;
1122
1131
  },
1123
1132
  ): TelegramMessageOwnershipView | undefined {
1124
- return (
1125
- getForeignTelegramMessageOwnership(
1126
- getTelegramCallbackMessageTarget(query),
1127
- deps,
1128
- ) ??
1129
- getForeignTelegramTargetOwnership(
1130
- query.message ? getTelegramMessageTarget(query.message) : undefined,
1131
- deps,
1132
- )
1133
+ const messageOwnership = getForeignTelegramMessageOwnership(
1134
+ getTelegramCallbackMessageTarget(query),
1135
+ deps,
1133
1136
  );
1137
+ const targetOwnership = getForeignTelegramTargetOwnership(
1138
+ query.message ? getTelegramMessageTarget(query.message) : undefined,
1139
+ deps,
1140
+ );
1141
+ if (!messageOwnership) return targetOwnership;
1142
+ if (
1143
+ targetOwnership &&
1144
+ messageOwnership.recipientBindingKey &&
1145
+ messageOwnership.recipientBindingKey === targetOwnership.recipientBindingKey
1146
+ ) {
1147
+ return targetOwnership;
1148
+ }
1149
+ return messageOwnership;
1134
1150
  }
1135
1151
 
1136
1152
  function getTelegramCallbackMessageTarget(
@@ -1226,8 +1242,7 @@ export function createTelegramPairedUpdateRuntime<
1226
1242
  pairTelegramUserIfNeeded: (userId, ctx, assertExecutionCurrent) =>
1227
1243
  createTelegramUserPairingRuntime({
1228
1244
  getAllowedUserId: deps.getAllowedUserId,
1229
- setAllowedUserId: deps.setAllowedUserId,
1230
- persistConfig: deps.persistConfig,
1245
+ persistAllowedUserId: deps.persistAllowedUserId,
1231
1246
  updateStatus: deps.updateStatus,
1232
1247
  }).pairIfNeeded(userId, ctx, assertExecutionCurrent),
1233
1248
  answerCallbackQuery: deps.answerCallbackQuery,
@@ -1328,6 +1343,13 @@ export async function handleAuthorizedTelegramReactionUpdate<TContext>(
1328
1343
  reactionUpdate: TelegramMessageReactionUpdated,
1329
1344
  deps: AuthorizedTelegramReactionUpdateDeps<TContext>,
1330
1345
  ): Promise<void> {
1346
+ const reactionUser = reactionUpdate.user;
1347
+ const allowedUserId = deps.allowedUserId;
1348
+ if (
1349
+ allowedUserId === undefined || !Number.isSafeInteger(allowedUserId) || allowedUserId <= 0 ||
1350
+ !reactionUser || reactionUser.is_bot || reactionUser.id !== allowedUserId ||
1351
+ reactionUpdate.actor_chat !== undefined
1352
+ ) return;
1331
1353
  const foreignOwnership = getForeignTelegramMessageOwnership(
1332
1354
  getTelegramReactionMessageTarget(reactionUpdate),
1333
1355
  deps,
@@ -1350,14 +1372,6 @@ export async function handleAuthorizedTelegramReactionUpdate<TContext>(
1350
1372
  }
1351
1373
  return;
1352
1374
  }
1353
- const reactionUser = reactionUpdate.user;
1354
- if (!reactionUser || reactionUser.is_bot) return;
1355
- if (
1356
- reactionUpdate.chat.type !== "private" &&
1357
- reactionUser.id !== deps.allowedUserId
1358
- ) {
1359
- return;
1360
- }
1361
1375
  const reactionScope =
1362
1376
  typeof reactionUpdate.chat.id === "number"
1363
1377
  ? { chatId: reactionUpdate.chat.id }
@@ -1428,6 +1442,26 @@ export async function executeTelegramUpdatePlan<
1428
1442
  return;
1429
1443
  }
1430
1444
  if (plan.kind === "callback") {
1445
+ let pairingAllowed = true;
1446
+ if (plan.shouldPair) {
1447
+ assertExecutionCurrent();
1448
+ pairingAllowed = await deps.pairTelegramUserIfNeeded(
1449
+ plan.query.from.id,
1450
+ deps.ctx,
1451
+ assertExecutionCurrent,
1452
+ );
1453
+ }
1454
+ if (plan.shouldDeny || !pairingAllowed) {
1455
+ const callbackQueryId = getTelegramCallbackQueryId(plan.query);
1456
+ if (callbackQueryId) {
1457
+ assertExecutionCurrent();
1458
+ await deps.answerCallbackQuery(
1459
+ callbackQueryId,
1460
+ formatTelegramUnauthorizedDenial("plain"),
1461
+ );
1462
+ }
1463
+ return;
1464
+ }
1431
1465
  const foreignOwnership = getForeignTelegramCallbackOwnership(
1432
1466
  plan.query,
1433
1467
  deps,
@@ -1461,25 +1495,6 @@ export async function executeTelegramUpdatePlan<
1461
1495
  assertExecutionCurrent();
1462
1496
  return;
1463
1497
  }
1464
- if (plan.shouldPair) {
1465
- assertExecutionCurrent();
1466
- await deps.pairTelegramUserIfNeeded(
1467
- plan.query.from.id,
1468
- deps.ctx,
1469
- assertExecutionCurrent,
1470
- );
1471
- }
1472
- if (plan.shouldDeny) {
1473
- const callbackQueryId = getTelegramCallbackQueryId(plan.query);
1474
- if (callbackQueryId) {
1475
- assertExecutionCurrent();
1476
- await deps.answerCallbackQuery(
1477
- callbackQueryId,
1478
- formatTelegramUnauthorizedDenial("plain"),
1479
- );
1480
- }
1481
- return;
1482
- }
1483
1498
  assertExecutionCurrent();
1484
1499
  await deps.handleAuthorizedTelegramCallbackQuery(plan.query, deps.ctx);
1485
1500
  assertExecutionCurrent();
@@ -1505,8 +1520,44 @@ export async function executeTelegramUpdatePlan<
1505
1520
  }
1506
1521
  return;
1507
1522
  }
1523
+ if (plan.shouldPair) assertExecutionCurrent();
1524
+ const pairedNow = plan.shouldPair
1525
+ ? await deps.pairTelegramUserIfNeeded(
1526
+ plan.message.from.id,
1527
+ deps.ctx,
1528
+ assertExecutionCurrent,
1529
+ )
1530
+ : false;
1531
+ const replyTarget = getTelegramMessageReplyTarget(plan.message);
1532
+ if (plan.shouldDeny || (plan.shouldPair && !pairedNow)) {
1533
+ if (replyTarget) {
1534
+ assertExecutionCurrent();
1535
+ await deps.sendTextReply(
1536
+ replyTarget.chatId,
1537
+ replyTarget.messageId,
1538
+ formatTelegramUnauthorizedDenial("html"),
1539
+ { parseMode: "HTML", target: replyTarget },
1540
+ );
1541
+ }
1542
+ return;
1543
+ }
1544
+ if (
1545
+ plan.kind === "message" &&
1546
+ pairedNow &&
1547
+ plan.shouldNotifyPaired &&
1548
+ replyTarget
1549
+ ) {
1550
+ assertExecutionCurrent();
1551
+ await deps.sendTextReply(
1552
+ replyTarget.chatId,
1553
+ replyTarget.messageId,
1554
+ "Telegram bridge paired with this account.",
1555
+ { target: replyTarget },
1556
+ );
1557
+ assertExecutionCurrent();
1558
+ }
1508
1559
  const foreignMessageOwnership = getForeignTelegramMessageOwnership(
1509
- getTelegramMessageReplyTarget(plan.message),
1560
+ replyTarget,
1510
1561
  deps,
1511
1562
  );
1512
1563
  if (foreignMessageOwnership) {
@@ -1581,42 +1632,6 @@ export async function executeTelegramUpdatePlan<
1581
1632
  assertExecutionCurrent();
1582
1633
  return;
1583
1634
  }
1584
- if (plan.shouldPair) assertExecutionCurrent();
1585
- const pairedNow = plan.shouldPair
1586
- ? await deps.pairTelegramUserIfNeeded(
1587
- plan.message.from.id,
1588
- deps.ctx,
1589
- assertExecutionCurrent,
1590
- )
1591
- : false;
1592
- const replyTarget = getTelegramMessageReplyTarget(plan.message);
1593
- if (
1594
- plan.kind === "message" &&
1595
- pairedNow &&
1596
- plan.shouldNotifyPaired &&
1597
- replyTarget
1598
- ) {
1599
- assertExecutionCurrent();
1600
- await deps.sendTextReply(
1601
- replyTarget.chatId,
1602
- replyTarget.messageId,
1603
- "Telegram bridge paired with this account.",
1604
- { target: replyTarget },
1605
- );
1606
- assertExecutionCurrent();
1607
- }
1608
- if (plan.shouldDeny) {
1609
- if (replyTarget) {
1610
- assertExecutionCurrent();
1611
- await deps.sendTextReply(
1612
- replyTarget.chatId,
1613
- replyTarget.messageId,
1614
- formatTelegramUnauthorizedDenial("html"),
1615
- { parseMode: "HTML", target: replyTarget },
1616
- );
1617
- }
1618
- return;
1619
- }
1620
1635
  if (plan.kind === "edited-message") {
1621
1636
  assertExecutionCurrent();
1622
1637
  await deps.handleAuthorizedTelegramEditedMessage(plan.message, deps.ctx);
@@ -1653,6 +1668,7 @@ export type TelegramUpdateWorkerBlockedReason =
1653
1668
  | "journal-read"
1654
1669
  | "journal-write"
1655
1670
  | "execution"
1671
+ | "input-custody"
1656
1672
  | "prior-generation-executing"
1657
1673
  | "invalid-outcome";
1658
1674
 
@@ -1662,6 +1678,11 @@ export interface TelegramUpdateWorkerStateSnapshot {
1662
1678
  phaseStartedAtMs?: number;
1663
1679
  currentUpdateId?: number;
1664
1680
  blockedReason?: TelegramUpdateWorkerBlockedReason;
1681
+ blockedInputCustody?: {
1682
+ updateId: number;
1683
+ kind: "running-outcome-unknown" | "foreign-ready" | "handoff-frozen" |
1684
+ "legacy-retry-state";
1685
+ };
1665
1686
  journalEntryCount: number;
1666
1687
  journalSerializedBytes: number;
1667
1688
  oldestAdmittedAtMs?: number;
@@ -1690,12 +1711,17 @@ export interface TelegramUpdateWorkerStateSnapshot {
1690
1711
  }
1691
1712
 
1692
1713
  export interface TelegramUpdateWorkerJournalSnapshot {
1714
+ version: typeof TELEGRAM_UPDATE_JOURNAL_VERSION |
1715
+ typeof TELEGRAM_UPDATE_JOURNAL_EXCLUSION_VERSION |
1716
+ typeof TELEGRAM_UPDATE_JOURNAL_CUSTODY_VERSION;
1693
1717
  acceptedThroughUpdateId?: number;
1694
1718
  entries: readonly {
1695
1719
  updateId: number;
1696
1720
  update: TelegramJournaledUpdate;
1721
+ readonly preApprovalExcluded?: boolean;
1697
1722
  admittedAtMs: number;
1698
1723
  state: "pending" | "retry-wait" | "queued" | "failed";
1724
+ inputClaim?: TelegramUpdateJournalInputClaim;
1699
1725
  queueKind?: "prompt" | "control";
1700
1726
  queueReceiptId?: string;
1701
1727
  queueOwner?: TelegramUpdateJournalQueueOwner;
@@ -1775,8 +1801,14 @@ export interface TelegramUpdateWorkerRuntimeDeps<TContext> {
1775
1801
  ctx: TContext,
1776
1802
  signal: AbortSignal,
1777
1803
  ) => Promise<TelegramUpdateAdmissionOutcome> | TelegramUpdateAdmissionOutcome;
1804
+ executeCustodiedUpdate?: (
1805
+ update: TelegramJournaledUpdate,
1806
+ ctx: TContext,
1807
+ signal: AbortSignal,
1808
+ ) => Promise<TelegramCustodiedExecutionResult>;
1778
1809
  hasAuthority: (ctx: TContext) => boolean;
1779
1810
  getJournalBindingKey?: () => string | undefined;
1811
+ getRecipientBindingKey?: () => string | undefined;
1780
1812
  getQueueOwnerIdentity?: (
1781
1813
  ctx: TContext,
1782
1814
  ) => TelegramUpdateJournalQueueOwnerIdentity;
@@ -1818,6 +1850,11 @@ export interface TelegramUpdateWorkerRuntime<TContext> {
1818
1850
  outcome: TelegramUpdateAdmissionOutcome;
1819
1851
  signal: AbortSignal;
1820
1852
  }) => void;
1853
+ settleCustodied: (input: {
1854
+ updateId: number;
1855
+ result: TelegramCustodiedExecutionResult;
1856
+ signal: AbortSignal;
1857
+ }) => void;
1821
1858
  isQueueReceiptCommitted: (
1822
1859
  receipt: TelegramQueueAdmissionReceiptLike,
1823
1860
  ) => boolean;
@@ -1851,7 +1888,8 @@ interface TelegramUpdateWorkerOwner<TContext> {
1851
1888
  type TelegramUpdateWorkerClaim = "deferred" | "queued";
1852
1889
  type TelegramUpdateWorkerDrainResult = "idle" | "blocked" | "aborted";
1853
1890
  type TelegramUpdateWorkerExecutionSettlement =
1854
- | { ok: true; outcome: TelegramUpdateAdmissionOutcome }
1891
+ | { ok: true; outcome: TelegramUpdateAdmissionOutcome; custodied?: false }
1892
+ | { ok: true; outcome: TelegramCustodiedExecutionResult; custodied: true }
1855
1893
  | { ok: false; error: unknown };
1856
1894
 
1857
1895
  const TELEGRAM_UPDATE_WORKER_EXECUTION_ABORTED = Symbol(
@@ -2279,6 +2317,18 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
2279
2317
  snapshot: TelegramUpdateWorkerJournalSnapshot,
2280
2318
  expectedOwner: TelegramUpdateWorkerOwner<TContext>,
2281
2319
  ): number | undefined => {
2320
+ // Validate the whole snapshot before reconstructing any queue authority.
2321
+ if ((snapshot.version !== TELEGRAM_UPDATE_JOURNAL_VERSION &&
2322
+ snapshot.version !== TELEGRAM_UPDATE_JOURNAL_EXCLUSION_VERSION &&
2323
+ snapshot.version !== TELEGRAM_UPDATE_JOURNAL_CUSTODY_VERSION) ||
2324
+ (snapshot.version === TELEGRAM_UPDATE_JOURNAL_CUSTODY_VERSION &&
2325
+ !deps.executeCustodiedUpdate) ||
2326
+ snapshot.entries.some((entry) =>
2327
+ ((snapshot.version === TELEGRAM_UPDATE_JOURNAL_EXCLUSION_VERSION || entry.preApprovalExcluded !== undefined) &&
2328
+ typeof entry.preApprovalExcluded !== "boolean") ||
2329
+ (entry.preApprovalExcluded === true && entry.state === "queued"))) {
2330
+ throw new TelegramUpdateAdmissionOutcomeError("Telegram journal snapshot has invalid pairing exclusion evidence.");
2331
+ }
2282
2332
  const availableUpdateIds = new Set<number>();
2283
2333
  const queuedReceiptEntries = new Map<
2284
2334
  string,
@@ -2464,16 +2514,17 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
2464
2514
  if (expectedOwner.controller.signal.aborted) {
2465
2515
  return TELEGRAM_UPDATE_WORKER_EXECUTION_ABORTED;
2466
2516
  }
2467
- const execution = Promise.resolve().then(() =>
2468
- deps.executeUpdate(
2469
- update,
2470
- expectedOwner.ctx,
2471
- expectedOwner.controller.signal,
2472
- ),
2473
- );
2517
+ const custodied = deps.executeCustodiedUpdate !== undefined;
2518
+ const execution = Promise.resolve().then(async (): Promise<
2519
+ TelegramUpdateAdmissionOutcome | TelegramCustodiedExecutionResult
2520
+ > => deps.executeCustodiedUpdate
2521
+ ? deps.executeCustodiedUpdate(update, expectedOwner.ctx, expectedOwner.controller.signal)
2522
+ : deps.executeUpdate(update, expectedOwner.ctx, expectedOwner.controller.signal));
2474
2523
  const settlement: Promise<TelegramUpdateWorkerExecutionSettlement> =
2475
2524
  execution.then(
2476
- (outcome) => ({ ok: true, outcome }),
2525
+ (outcome) => custodied
2526
+ ? ({ ok: true, outcome: outcome as unknown as TelegramCustodiedExecutionResult, custodied: true })
2527
+ : ({ ok: true, outcome: outcome as TelegramUpdateAdmissionOutcome }),
2477
2528
  (error: unknown) => ({ ok: false, error }),
2478
2529
  );
2479
2530
  unsettledExecutions.add(settlement);
@@ -2768,7 +2819,33 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
2768
2819
  const entries: TelegramUpdateWorkerJournalSnapshot["entries"][number][] =
2769
2820
  [];
2770
2821
  let hasMoreEntries = false;
2822
+ let hasOutcomeUnknownInput = false;
2823
+ let hasUnavailableInputCustody = false;
2824
+ let blockedInputCustody: TelegramUpdateWorkerStateSnapshot["blockedInputCustody"];
2825
+ delete state.blockedInputCustody;
2771
2826
  for (const candidate of snapshot.entries) {
2827
+ if (snapshot.version === TELEGRAM_UPDATE_JOURNAL_CUSTODY_VERSION &&
2828
+ (candidate.state === "retry-wait" || candidate.state === "failed")) {
2829
+ hasUnavailableInputCustody = true;
2830
+ blockedInputCustody ??= { updateId: candidate.updateId, kind: "legacy-retry-state" };
2831
+ continue;
2832
+ }
2833
+ if (snapshot.version === TELEGRAM_UPDATE_JOURNAL_CUSTODY_VERSION && candidate.inputClaim) {
2834
+ if (candidate.inputClaim.phase === "running") {
2835
+ hasOutcomeUnknownInput = true;
2836
+ if (!blockedInputCustody || blockedInputCustody.kind !== "running-outcome-unknown")
2837
+ blockedInputCustody = { updateId: candidate.updateId,
2838
+ kind: "running-outcome-unknown" };
2839
+ continue;
2840
+ }
2841
+ if (candidate.inputClaim.handoff || !isTelegramUpdateJournalQueueOwnerProcess(
2842
+ candidate.inputClaim.owner, expectedOwner.queueOwnerIdentity)) {
2843
+ hasUnavailableInputCustody = true;
2844
+ blockedInputCustody ??= { updateId: candidate.updateId,
2845
+ kind: candidate.inputClaim.handoff ? "handoff-frozen" : "foreign-ready" };
2846
+ continue;
2847
+ }
2848
+ }
2772
2849
  if (
2773
2850
  !claims.has(candidate.updateId) &&
2774
2851
  (candidate.state === "pending" ||
@@ -2785,6 +2862,12 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
2785
2862
  }
2786
2863
  if (entries.length === 0) {
2787
2864
  scheduleNextRetry(scheduledRetryAtMs, expectedOwner);
2865
+ if (hasOutcomeUnknownInput || hasUnavailableInputCustody) {
2866
+ if (blockedInputCustody) state.blockedInputCustody = blockedInputCustody;
2867
+ transition("blocked", blockedInputCustody?.updateId,
2868
+ hasOutcomeUnknownInput ? "execution" : "input-custody");
2869
+ return "blocked";
2870
+ }
2788
2871
  transition("idle");
2789
2872
  return "idle";
2790
2873
  }
@@ -2809,6 +2892,10 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
2809
2892
  snapshotInvalidated = true;
2810
2893
  break;
2811
2894
  }
2895
+ if (entry.preApprovalExcluded === true) {
2896
+ completedUpdateIds.push(entry.updateId);
2897
+ continue;
2898
+ }
2812
2899
  clearRetryTimer();
2813
2900
  transition("executing", entry.updateId);
2814
2901
  const execution = await executeWithinOwner(expectedOwner, entry.update);
@@ -2816,6 +2903,10 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
2816
2903
  return "aborted";
2817
2904
  }
2818
2905
  if (!execution.ok) {
2906
+ if (deps.executeCustodiedUpdate) {
2907
+ transition("blocked", entry.updateId, "execution");
2908
+ return "blocked";
2909
+ }
2819
2910
  const completionResult = commitCompletedBatch(
2820
2911
  expectedOwner,
2821
2912
  completedUpdateIds,
@@ -2832,6 +2923,37 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
2832
2923
  snapshotInvalidated = true;
2833
2924
  break;
2834
2925
  }
2926
+ if (execution.custodied) {
2927
+ const outcome = execution.outcome;
2928
+ if (outcome.status === "outcome-unknown") {
2929
+ transition("blocked", entry.updateId, "execution");
2930
+ return "blocked";
2931
+ }
2932
+ if (outcome.status === "deferred") {
2933
+ claims.set(entry.updateId, "deferred");
2934
+ transition("deferred", entry.updateId);
2935
+ continue;
2936
+ }
2937
+ if (outcome.status === "queued") {
2938
+ const receipt = normalizeQueueReceipt(outcome.queueReceipt);
2939
+ if (!isTelegramUpdateJournalQueueOwnerProcess(
2940
+ outcome.queueReceipt.queueOwner, expectedOwner.queueOwnerIdentity)) {
2941
+ return blockWithFailure("invalid-outcome", "custody-queue-owner",
2942
+ new Error("Telegram custodied queue receipt belongs to another process."),
2943
+ entry.updateId);
2944
+ }
2945
+ for (const sourceUpdateId of receipt.sourceUpdateIds) claims.set(sourceUpdateId, "queued");
2946
+ try {
2947
+ publishCommittedQueueReceipt(receipt, outcome.queueReceipt.queueOwner, expectedOwner.ctx);
2948
+ } catch (error) {
2949
+ return blockWithFailure("invalid-outcome", "queue-receipt-publish", error,
2950
+ entry.updateId);
2951
+ }
2952
+ transition("queued", entry.updateId);
2953
+ }
2954
+ snapshotInvalidated = true;
2955
+ break;
2956
+ }
2835
2957
  const postExecutionAuthority = checkAuthority(
2836
2958
  expectedOwner,
2837
2959
  entry.updateId,
@@ -3095,6 +3217,61 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3095
3217
  if (result === "aborted") return;
3096
3218
  transition("queued", input.updateId);
3097
3219
  },
3220
+ settleCustodied(input) {
3221
+ const expectedOwner = owner;
3222
+ if (!expectedOwner || expectedOwner.controller.signal !== input.signal || input.signal.aborted)
3223
+ return;
3224
+ if (checkAuthority(expectedOwner, input.updateId)) return;
3225
+ const claim = claims.get(input.updateId);
3226
+ if (claim !== "deferred" && claim !== "queued") return;
3227
+ if (input.result.status === "deferred") return;
3228
+ if (input.result.status === "outcome-unknown") {
3229
+ transition("blocked", input.updateId, "execution");
3230
+ return;
3231
+ }
3232
+ if (input.result.status === "completed") {
3233
+ if (claim !== "deferred") return;
3234
+ claims.delete(input.updateId);
3235
+ transition("idle", input.updateId);
3236
+ pendingSignal = true;
3237
+ launchDrain();
3238
+ return;
3239
+ }
3240
+ const receipt = normalizeQueueReceipt(input.result.queueReceipt);
3241
+ const existing = committedQueueReceipts.get(receipt.receiptId);
3242
+ if (existing && areTelegramQueueAdmissionReceiptsEqual(existing.receipt, receipt) &&
3243
+ areTelegramUpdateJournalQueueOwnersEqual(
3244
+ existing.queueOwner, input.result.queueReceipt.queueOwner)) return;
3245
+ if (existing) {
3246
+ blockWithFailure("invalid-outcome", "queue-receipt-conflict",
3247
+ new TelegramUpdateAdmissionOutcomeError(
3248
+ `Telegram queue receipt ${receipt.receiptId} conflicts with custodied authority.`),
3249
+ input.updateId);
3250
+ return;
3251
+ }
3252
+ if (!receipt.sourceUpdateIds.includes(input.updateId) ||
3253
+ receipt.sourceUpdateIds.some(updateId => claims.get(updateId) !== "deferred")) {
3254
+ blockWithFailure("invalid-outcome", "late-custody-unclaimed",
3255
+ new TelegramUpdateAdmissionOutcomeError(
3256
+ `Telegram update ${input.updateId} reported custodied queue without exact deferred claims.`),
3257
+ input.updateId);
3258
+ return;
3259
+ }
3260
+ if (!isTelegramUpdateJournalQueueOwnerProcess(
3261
+ input.result.queueReceipt.queueOwner, expectedOwner.queueOwnerIdentity)) {
3262
+ blockWithFailure("invalid-outcome", "custody-queue-owner",
3263
+ new Error("Telegram custodied queue receipt belongs to another process."), input.updateId);
3264
+ return;
3265
+ }
3266
+ for (const sourceUpdateId of receipt.sourceUpdateIds) claims.set(sourceUpdateId, "queued");
3267
+ try {
3268
+ publishCommittedQueueReceipt(receipt, input.result.queueReceipt.queueOwner, expectedOwner.ctx);
3269
+ } catch (error) {
3270
+ blockWithFailure("invalid-outcome", "queue-receipt-publish", error, input.updateId);
3271
+ return;
3272
+ }
3273
+ transition("queued", input.updateId);
3274
+ },
3098
3275
  isQueueReceiptCommitted(receipt) {
3099
3276
  const committed = committedQueueReceipts.get(receipt.receiptId);
3100
3277
  return (
@@ -3487,6 +3664,954 @@ function mergeTelegramReportedAdmissionOutcome(
3487
3664
  );
3488
3665
  }
3489
3666
 
3667
+ export type TelegramCustodiedExecutionResult =
3668
+ | { status: "completed" }
3669
+ | { status: "deferred"; receipt: TelegramInputJournalReceipt }
3670
+ | { status: "queued"; queueReceipt: ReturnType<TelegramInputJournalStore["queueInputs"]>["queueReceipt"] }
3671
+ | { status: "outcome-unknown"; receipt: TelegramInputJournalReceipt };
3672
+
3673
+ type TelegramCustodyExecutionJournal = Pick<TelegramInputJournalStore,
3674
+ "acquireInput" | "startInput" | "completeInput" | "queueInputs">;
3675
+
3676
+ export function createTelegramInputCustodyWorkerJournalPort(
3677
+ store: TelegramInputJournalStore,
3678
+ ): TelegramUpdateWorkerJournalPort & { inputCustody: TelegramCustodyExecutionJournal } {
3679
+ const legacyMutation = (): never => {
3680
+ throw new TelegramUpdateAdmissionOutcomeError(
3681
+ "Telegram v3 custody forbids legacy raw worker settlement.",
3682
+ );
3683
+ };
3684
+ return {
3685
+ read: () => store.read() as unknown as TelegramUpdateWorkerJournalSnapshot,
3686
+ markQueued: legacyMutation,
3687
+ markExecutionFailure: legacyMutation,
3688
+ removeCompleted: legacyMutation,
3689
+ completeQueued: receipts => store.completeQueued(receipts),
3690
+ inputCustody: {
3691
+ acquireInput: store.acquireInput,
3692
+ startInput: store.startInput,
3693
+ completeInput: store.completeInput,
3694
+ queueInputs: store.queueInputs,
3695
+ },
3696
+ };
3697
+ }
3698
+
3699
+ export function createTelegramInputCustodyLegacyDispositionRuntime(deps: {
3700
+ withBindingReference<T>(recoveryKey: string, operation: (binding: {
3701
+ recoveryKey: string; journal: Pick<TelegramInputJournalStore,
3702
+ "listLegacyCustodyCandidates" | "applyLegacyCustodyDisposition">;
3703
+ }) => T): T;
3704
+ }) {
3705
+ const withBinding = <T>(recoveryKey: string, operation: (journal: Pick<
3706
+ TelegramInputJournalStore, "listLegacyCustodyCandidates" |
3707
+ "applyLegacyCustodyDisposition">) => T): T => {
3708
+ if (!recoveryKey) throw new TelegramUpdateAdmissionOutcomeError(
3709
+ "Telegram legacy custody disposition binding is unavailable.");
3710
+ return deps.withBindingReference(recoveryKey, binding => {
3711
+ if (binding.recoveryKey !== recoveryKey)
3712
+ throw new TelegramUpdateAdmissionOutcomeError(
3713
+ "Telegram legacy custody disposition binding is unavailable.");
3714
+ return operation(binding.journal);
3715
+ });
3716
+ };
3717
+ return {
3718
+ list(recoveryKey: string) {
3719
+ return withBinding(recoveryKey, journal => journal.listLegacyCustodyCandidates());
3720
+ },
3721
+ apply(recoveryKey: string,
3722
+ authority: Parameters<TelegramInputJournalStore["applyLegacyCustodyDisposition"]>[0]) {
3723
+ return withBinding(recoveryKey,
3724
+ journal => journal.applyLegacyCustodyDisposition(authority));
3725
+ },
3726
+ };
3727
+ }
3728
+
3729
+ export function createTelegramInputCustodyHandoffClient(deps: {
3730
+ journal: Pick<TelegramInputJournalStore, "offerInputHandoff">;
3731
+ resolveAcceptedReference?: (input: {
3732
+ sourceUpdateId: number; recipientBindingKey: string;
3733
+ }) => { sourceRecoveryKey: string; source: { updateId: number; owner: {
3734
+ acquisitionId: string; handoffId: string } } } | undefined;
3735
+ sendEnvelope(envelope: Extract<TelegramBusEnvelope,
3736
+ { kind: "leader.offerInputCustodyHandoff" }>): Promise<TelegramBusEnvelope | undefined>;
3737
+ }): { transfer(input: {
3738
+ requestId: string;
3739
+ receipt: TelegramInputJournalReceipt;
3740
+ recipientInstanceId: string;
3741
+ recipientRegistrationGeneration: string;
3742
+ recipientBindingKey: string;
3743
+ recipientOwner: TelegramUpdateJournalQueueOwnerIdentity;
3744
+ handoffToken: string;
3745
+ sentAtMs: number;
3746
+ auth?: string;
3747
+ }): Promise<{ sourceRecoveryKey: string; source: { updateId: number; owner: {
3748
+ acquisitionId: string; handoffId: string } }; duplicate: boolean }> } {
3749
+ return {
3750
+ async transfer(input) {
3751
+ const reconciled = deps.resolveAcceptedReference?.({
3752
+ sourceUpdateId: input.receipt.updateId,
3753
+ recipientBindingKey: input.recipientBindingKey });
3754
+ if (reconciled) return { ...reconciled, duplicate: true };
3755
+ const offered = deps.journal.offerInputHandoff({ receipt: input.receipt,
3756
+ recipientOwner: input.recipientOwner, handoffToken: input.handoffToken });
3757
+ const envelope: Extract<TelegramBusEnvelope,
3758
+ { kind: "leader.offerInputCustodyHandoff" }> = {
3759
+ kind: "leader.offerInputCustodyHandoff", requestId: input.requestId,
3760
+ recipientInstanceId: input.recipientInstanceId,
3761
+ recipientRegistrationGeneration: input.recipientRegistrationGeneration,
3762
+ recipientBindingKey: input.recipientBindingKey,
3763
+ sourceRecoveryKey: offered.source.journalBindingKey,
3764
+ source: offered.source, handoffId: offered.handoff.handoffId,
3765
+ sentAtMs: input.sentAtMs, ...(input.auth ? { auth: input.auth } : {}),
3766
+ };
3767
+ const response = await deps.sendEnvelope(envelope);
3768
+ if (response?.kind !== "bus.ack" || response.requestId !== input.requestId ||
3769
+ !response.ok || !response.result || typeof response.result !== "object" ||
3770
+ Array.isArray(response.result)) throw new Error(
3771
+ "Telegram input custody handoff acknowledgement is missing or rejected.",
3772
+ );
3773
+ const result = response.result as Record<string, unknown>;
3774
+ const source = result.source;
3775
+ if (result.sourceRecoveryKey !== offered.source.journalBindingKey ||
3776
+ !source || typeof source !== "object" || Array.isArray(source) ||
3777
+ (source as Record<string, unknown>).updateId !== offered.source.updateId ||
3778
+ !(source as Record<string, unknown>).owner ||
3779
+ typeof (source as Record<string, unknown>).owner !== "object" ||
3780
+ Array.isArray((source as Record<string, unknown>).owner) ||
3781
+ typeof ((source as Record<string, unknown>).owner as Record<string, unknown>).acquisitionId !== "string" ||
3782
+ ((source as Record<string, unknown>).owner as Record<string, unknown>).handoffId !== offered.handoff.handoffId ||
3783
+ typeof result.duplicate !== "boolean") throw new Error(
3784
+ "Telegram input custody handoff acknowledgement returned mismatched authority.",
3785
+ );
3786
+ return { sourceRecoveryKey: result.sourceRecoveryKey as string,
3787
+ source: { updateId: (source as Record<string, unknown>).updateId as number, owner: {
3788
+ acquisitionId: ((source as Record<string, unknown>).owner as Record<string, unknown>).acquisitionId as string,
3789
+ handoffId: ((source as Record<string, unknown>).owner as Record<string, unknown>).handoffId as string } },
3790
+ duplicate: result.duplicate as boolean };
3791
+ },
3792
+ };
3793
+ }
3794
+
3795
+ export interface TelegramInputCustodyHandoffAcceptanceInput {
3796
+ sourceRecoveryKey: string;
3797
+ recipientBindingKey: string;
3798
+ source: TelegramInputJournalSourceReference;
3799
+ handoffId: string;
3800
+ }
3801
+
3802
+ export function createTelegramInputCustodyHandoffAcceptanceRuntime<TContext>(deps: {
3803
+ resolveBinding(recoveryKey: string): {
3804
+ recoveryKey: string;
3805
+ recipientBindingKey: string;
3806
+ recipientOwner: TelegramUpdateJournalQueueOwnerIdentity;
3807
+ journal: Pick<TelegramInputJournalStore, "acceptInputHandoff">;
3808
+ signalWorker(ctx: TContext): void;
3809
+ } | undefined;
3810
+ }): { accept(input: TelegramInputCustodyHandoffAcceptanceInput, ctx: TContext): {
3811
+ sourceRecoveryKey: string;
3812
+ source: { updateId: number; owner: { acquisitionId: string; handoffId: string } };
3813
+ duplicate: boolean;
3814
+ } } {
3815
+ return {
3816
+ accept(input, ctx) {
3817
+ const binding = deps.resolveBinding(input.sourceRecoveryKey);
3818
+ if (!binding || binding.recoveryKey !== input.sourceRecoveryKey ||
3819
+ binding.recipientBindingKey !== input.recipientBindingKey ||
3820
+ input.source.journalBindingKey !== input.sourceRecoveryKey) throw new Error(
3821
+ "Telegram input custody handoff binding is unavailable or changed.",
3822
+ );
3823
+ const accepted = binding.journal.acceptInputHandoff({ source: input.source,
3824
+ recipientOwner: binding.recipientOwner, handoffId: input.handoffId });
3825
+ const acceptedHandoffId = accepted.receipt.owner.handoffId;
3826
+ if (!acceptedHandoffId || acceptedHandoffId !== input.handoffId) throw new Error(
3827
+ "Telegram input custody handoff acceptance returned mismatched authority.",
3828
+ );
3829
+ binding.signalWorker(ctx);
3830
+ return { sourceRecoveryKey: binding.recoveryKey,
3831
+ source: { updateId: accepted.receipt.updateId,
3832
+ owner: { acquisitionId: accepted.receipt.owner.acquisitionId,
3833
+ handoffId: acceptedHandoffId } }, duplicate: accepted.duplicate };
3834
+ },
3835
+ };
3836
+ }
3837
+
3838
+ export function createTelegramInputCustodyForwardReferenceResolver(deps: {
3839
+ recoveryKey: string;
3840
+ recipientBindingKey: string;
3841
+ recipientOwner: TelegramUpdateJournalQueueOwnerIdentity;
3842
+ journal: Pick<TelegramInputJournalStore, "read">;
3843
+ }): (input: { sourceUpdateId: number; recipientBindingKey: string }) => {
3844
+ sourceRecoveryKey: string;
3845
+ source: { updateId: number; owner: {
3846
+ acquisitionId: string; handoffId: string;
3847
+ } };
3848
+ } | undefined {
3849
+ return input => {
3850
+ if (input.recipientBindingKey !== deps.recipientBindingKey) return undefined;
3851
+ const entry = deps.journal.read().entries.find(
3852
+ candidate => candidate.updateId === input.sourceUpdateId);
3853
+ const claim = entry?.inputClaim;
3854
+ if (!entry || entry.state !== "pending" || entry.preApprovalExcluded !== false ||
3855
+ !claim || claim.phase !== "ready" || claim.handoff ||
3856
+ claim.recipientBindingKey !== deps.recipientBindingKey ||
3857
+ !claim.owner.handoffId ||
3858
+ claim.owner.sessionGeneration !== deps.recipientOwner.sessionGeneration ||
3859
+ !isTelegramUpdateJournalQueueOwnerProcess(claim.owner, deps.recipientOwner)) return undefined;
3860
+ return { sourceRecoveryKey: deps.recoveryKey, source: { updateId: entry.updateId,
3861
+ owner: { acquisitionId: claim.owner.acquisitionId,
3862
+ handoffId: claim.owner.handoffId } } };
3863
+ };
3864
+ }
3865
+
3866
+ export interface TelegramCustodiedSourceReferenceWakeInput {
3867
+ deliveryId: string;
3868
+ sourceUpdateId: number;
3869
+ recipientBindingKey: string;
3870
+ sourceRecoveryKey: string;
3871
+ sourceClaim: { acquisitionId: string; handoffId: string };
3872
+ }
3873
+
3874
+ export function createTelegramInputCustodySourceReferenceWakeRuntime<TContext>(deps: {
3875
+ resolveBinding(recoveryKey: string): {
3876
+ recoveryKey: string;
3877
+ recipientBindingKey: string;
3878
+ recipientOwner: TelegramUpdateJournalQueueOwnerIdentity;
3879
+ journal: Pick<TelegramInputJournalStore, "read">;
3880
+ signalWorker(ctx: TContext): void;
3881
+ } | undefined;
3882
+ }): { wakeSource(input: TelegramCustodiedSourceReferenceWakeInput, ctx: TContext): void } {
3883
+ return {
3884
+ wakeSource(input, ctx) {
3885
+ const binding = deps.resolveBinding(input.sourceRecoveryKey);
3886
+ if (!binding || binding.recoveryKey !== input.sourceRecoveryKey ||
3887
+ binding.recipientBindingKey !== input.recipientBindingKey) throw new Error(
3888
+ "Telegram follower source-reference binding is unavailable or changed.",
3889
+ );
3890
+ const entry = binding.journal.read().entries.find(
3891
+ candidate => candidate.updateId === input.sourceUpdateId);
3892
+ const claim = entry?.inputClaim;
3893
+ if (!entry || entry.state !== "pending" || entry.preApprovalExcluded !== false ||
3894
+ !claim || claim.phase !== "ready" || claim.handoff ||
3895
+ claim.recipientBindingKey !== input.recipientBindingKey ||
3896
+ claim.owner.acquisitionId !== input.sourceClaim.acquisitionId ||
3897
+ claim.owner.handoffId !== input.sourceClaim.handoffId ||
3898
+ claim.owner.sessionGeneration !== binding.recipientOwner.sessionGeneration ||
3899
+ !isTelegramUpdateJournalQueueOwnerProcess(claim.owner, binding.recipientOwner)) throw new Error(
3900
+ "Telegram follower source-reference claim is unavailable or changed.",
3901
+ );
3902
+ binding.signalWorker(ctx);
3903
+ },
3904
+ };
3905
+ }
3906
+
3907
+ export interface TelegramInputCustodyBusBindingRuntime<TContext> {
3908
+ acceptHandoff(input: TelegramInputCustodyHandoffAcceptanceInput, ctx: TContext): {
3909
+ sourceRecoveryKey: string; source: { updateId: number; owner: {
3910
+ acquisitionId: string; handoffId: string } }; duplicate: boolean };
3911
+ wakeSource(input: TelegramCustodiedSourceReferenceWakeInput, ctx: TContext): void;
3912
+ resolveForwardReference(input: { sourceUpdateId: number; recipientBindingKey: string }): {
3913
+ sourceRecoveryKey: string; source: { updateId: number; owner: {
3914
+ acquisitionId: string; handoffId: string } } } | undefined;
3915
+ }
3916
+
3917
+ export function createTelegramInputCustodyBusBindingRuntime<TContext>(deps: {
3918
+ getForwardRecoveryKey(): string | undefined;
3919
+ resolveBinding(recoveryKey: string): {
3920
+ recoveryKey: string;
3921
+ recipientBindingKey: string;
3922
+ recipientOwner: TelegramUpdateJournalQueueOwnerIdentity;
3923
+ journal: Pick<TelegramInputJournalStore,
3924
+ "read" | "acceptInputHandoff">;
3925
+ signalWorker(ctx: TContext): void;
3926
+ } | undefined;
3927
+ }): TelegramInputCustodyBusBindingRuntime<TContext> {
3928
+ const acceptance = createTelegramInputCustodyHandoffAcceptanceRuntime<TContext>({
3929
+ resolveBinding: deps.resolveBinding });
3930
+ const wake = createTelegramInputCustodySourceReferenceWakeRuntime<TContext>({
3931
+ resolveBinding: deps.resolveBinding });
3932
+ return {
3933
+ acceptHandoff: acceptance.accept,
3934
+ wakeSource: wake.wakeSource,
3935
+ resolveForwardReference(input: {
3936
+ sourceUpdateId: number; recipientBindingKey: string;
3937
+ }) {
3938
+ const recoveryKey = deps.getForwardRecoveryKey();
3939
+ if (!recoveryKey) return undefined;
3940
+ const binding = deps.resolveBinding(recoveryKey);
3941
+ if (!binding || binding.recoveryKey !== recoveryKey) return undefined;
3942
+ return createTelegramInputCustodyForwardReferenceResolver({
3943
+ recoveryKey: binding.recoveryKey,
3944
+ recipientBindingKey: binding.recipientBindingKey,
3945
+ recipientOwner: binding.recipientOwner,
3946
+ journal: binding.journal,
3947
+ })(input);
3948
+ },
3949
+ };
3950
+ }
3951
+
3952
+ export type TelegramInputCustodyActivationBlocker =
3953
+ | "disabled"
3954
+ | "source-unready"
3955
+ | "legacy-writers-present"
3956
+ | "migration-incomplete"
3957
+ | "peer-capability-mismatch";
3958
+
3959
+ export function evaluateTelegramInputCustodyActivationReadiness(input: {
3960
+ requested: boolean;
3961
+ sourceStatus: "absent" | "v3" | "legacy" | "unsupported" | "ambiguous";
3962
+ legacyWritersExcluded: boolean;
3963
+ historicalMigrationComplete: boolean;
3964
+ peerReadiness: readonly ("ready" | "legacy" | "unknown")[];
3965
+ }): { enabled: true } | { enabled: false; blocker: TelegramInputCustodyActivationBlocker } {
3966
+ if (!input.requested) return { enabled: false, blocker: "disabled" };
3967
+ if (!input.legacyWritersExcluded) return { enabled: false, blocker: "legacy-writers-present" };
3968
+ if (!input.historicalMigrationComplete) return { enabled: false, blocker: "migration-incomplete" };
3969
+ if (input.sourceStatus !== "absent" && input.sourceStatus !== "v3")
3970
+ return { enabled: false, blocker: "source-unready" };
3971
+ if (input.peerReadiness.some(readiness => readiness !== "ready"))
3972
+ return { enabled: false, blocker: "peer-capability-mismatch" };
3973
+ return { enabled: true };
3974
+ }
3975
+
3976
+ export interface TelegramInputCustodyReadinessEvidenceSnapshot {
3977
+ version: 1;
3978
+ revision: number;
3979
+ writerExclusion?: TelegramInputCustodyWriterExclusionEvidence;
3980
+ migration?: TelegramInputCustodyMigrationEvidence;
3981
+ startupExclusion?: TelegramInputCustodyStartupExclusionAuthority;
3982
+ migrationCompletion?: TelegramInputCustodyMigrationCompletionAuthority;
3983
+ }
3984
+
3985
+ export function createTelegramInputCustodyReadinessEvidenceStore(deps: {
3986
+ readRetained(): string | undefined;
3987
+ publishRetained(serialized: string): void;
3988
+ withSerialization<T>(operation: () => T): T;
3989
+ authorizePublication(kind: "writer-exclusion" | "migration" | "startup-exclusion" |
3990
+ "migration-completion", evidence: TelegramInputCustodyWriterExclusionEvidence |
3991
+ TelegramInputCustodyMigrationEvidence | TelegramInputCustodyStartupExclusionAuthority |
3992
+ TelegramInputCustodyMigrationCompletionAuthority): boolean;
3993
+ }) {
3994
+ const decode = (serialized: string | undefined): TelegramInputCustodyReadinessEvidenceSnapshot => {
3995
+ if (serialized === undefined) return { version: 1, revision: 0 };
3996
+ if (serialized.length > 16_384) throw new Error("Telegram custody readiness evidence exceeds capacity.");
3997
+ let value: unknown;
3998
+ try { value = JSON.parse(serialized); } catch { throw new Error("Telegram custody readiness evidence is malformed."); }
3999
+ if (!value || typeof value !== "object" || Array.isArray(value))
4000
+ throw new Error("Telegram custody readiness evidence is malformed.");
4001
+ const snapshot = value as Record<string, unknown>;
4002
+ if (snapshot.version !== 1 || !Number.isSafeInteger(snapshot.revision) ||
4003
+ (snapshot.revision as number) < 0 || Object.keys(snapshot).some(
4004
+ key => !["version", "revision", "writerExclusion", "migration", "startupExclusion",
4005
+ "migrationCompletion"].includes(key)))
4006
+ throw new Error("Telegram custody readiness evidence is malformed.");
4007
+ const validate = (evidence: unknown, kind: "writer" | "migration") => {
4008
+ if (evidence === undefined) return undefined;
4009
+ if (!evidence || typeof evidence !== "object" || Array.isArray(evidence))
4010
+ throw new Error("Telegram custody readiness evidence is malformed.");
4011
+ const record = evidence as Record<string, unknown>;
4012
+ const statuses = kind === "writer" ? ["excluded", "present", "unknown"]
4013
+ : ["complete", "incomplete", "unknown"];
4014
+ const writerKeys = ["startupAuthorityId", "closureOperationId", "writerInventorySha256"];
4015
+ const migrationKeys = ["migrationAuthorityId", "startupAuthorityId", "closureOperationId",
4016
+ "migrationInventorySha256", "resultingSourceFamily"];
4017
+ const linkedKeys = kind === "writer" ? writerKeys : migrationKeys;
4018
+ const suppliedLinkedKeys = linkedKeys.filter(key => record[key] !== undefined);
4019
+ const linkedMalformed = suppliedLinkedKeys.length === linkedKeys.length && (
4020
+ typeof record.startupAuthorityId !== "string" || !record.startupAuthorityId ||
4021
+ typeof record.closureOperationId !== "string" || !record.closureOperationId ||
4022
+ (kind === "writer"
4023
+ ? typeof record.writerInventorySha256 !== "string" ||
4024
+ !/^[a-f0-9]{64}$/u.test(record.writerInventorySha256)
4025
+ : typeof record.migrationAuthorityId !== "string" || !record.migrationAuthorityId ||
4026
+ typeof record.migrationInventorySha256 !== "string" ||
4027
+ !/^[a-f0-9]{64}$/u.test(record.migrationInventorySha256) ||
4028
+ (record.resultingSourceFamily !== "absent" && record.resultingSourceFamily !== "v3")));
4029
+ if (record.version !== 1 || typeof record.profileKey !== "string" || !record.profileKey ||
4030
+ typeof record.recoveryKey !== "string" || !record.recoveryKey ||
4031
+ !statuses.includes(record.status as string) || Object.keys(record).some(
4032
+ key => !["version", "profileKey", "recoveryKey", "status", ...linkedKeys].includes(key)) ||
4033
+ (suppliedLinkedKeys.length !== 0 && suppliedLinkedKeys.length !== linkedKeys.length) ||
4034
+ linkedMalformed) throw new Error("Telegram custody readiness evidence is malformed.");
4035
+ return { version: 1 as const, profileKey: record.profileKey,
4036
+ recoveryKey: record.recoveryKey, status: record.status,
4037
+ ...(suppliedLinkedKeys.length === linkedKeys.length ? kind === "writer" ? {
4038
+ startupAuthorityId: record.startupAuthorityId,
4039
+ closureOperationId: record.closureOperationId,
4040
+ writerInventorySha256: record.writerInventorySha256,
4041
+ } : {
4042
+ migrationAuthorityId: record.migrationAuthorityId,
4043
+ startupAuthorityId: record.startupAuthorityId,
4044
+ closureOperationId: record.closureOperationId,
4045
+ migrationInventorySha256: record.migrationInventorySha256,
4046
+ resultingSourceFamily: record.resultingSourceFamily,
4047
+ } : {}) };
4048
+ };
4049
+ const writerExclusion = validate(snapshot.writerExclusion, "writer") as
4050
+ TelegramInputCustodyWriterExclusionEvidence | undefined;
4051
+ const migration = validate(snapshot.migration, "migration") as
4052
+ TelegramInputCustodyMigrationEvidence | undefined;
4053
+ let startupExclusion: TelegramInputCustodyStartupExclusionAuthority | undefined;
4054
+ if (snapshot.startupExclusion !== undefined) {
4055
+ const candidate = snapshot.startupExclusion as Record<string, unknown>;
4056
+ if (!candidate || typeof candidate.profileKey !== "string" ||
4057
+ typeof candidate.recoveryKey !== "string")
4058
+ throw new Error("Telegram custody readiness evidence is malformed.");
4059
+ startupExclusion = normalizeTelegramInputCustodyStartupExclusionAuthority(candidate,
4060
+ { profileKey: candidate.profileKey, recoveryKey: candidate.recoveryKey });
4061
+ if (!startupExclusion) throw new Error("Telegram custody readiness evidence is malformed.");
4062
+ }
4063
+ let migrationCompletion: TelegramInputCustodyMigrationCompletionAuthority | undefined;
4064
+ if (snapshot.migrationCompletion !== undefined) {
4065
+ const candidate = snapshot.migrationCompletion as Record<string, unknown>;
4066
+ if (!candidate || typeof candidate.profileKey !== "string" ||
4067
+ typeof candidate.recoveryKey !== "string")
4068
+ throw new Error("Telegram custody readiness evidence is malformed.");
4069
+ migrationCompletion = normalizeTelegramInputCustodyMigrationCompletionAuthority(candidate,
4070
+ { profileKey: candidate.profileKey, recoveryKey: candidate.recoveryKey });
4071
+ if (!migrationCompletion)
4072
+ throw new Error("Telegram custody readiness evidence is malformed.");
4073
+ }
4074
+ const identities: TelegramInputCustodyActivationEvidenceIdentity[] = [];
4075
+ if (writerExclusion) identities.push(writerExclusion);
4076
+ if (migration) identities.push(migration);
4077
+ if (startupExclusion) identities.push(startupExclusion);
4078
+ if (migrationCompletion) identities.push(migrationCompletion);
4079
+ if (identities.some(evidence => evidence.profileKey !== identities[0]?.profileKey ||
4080
+ evidence.recoveryKey !== identities[0]?.recoveryKey)) throw new Error(
4081
+ "Telegram custody readiness evidence has conflicting identities.",
4082
+ );
4083
+ if (writerExclusion?.startupAuthorityId && startupExclusion &&
4084
+ (writerExclusion.startupAuthorityId !== startupExclusion.authorityId ||
4085
+ writerExclusion.closureOperationId !== startupExclusion.closureOperationId ||
4086
+ writerExclusion.writerInventorySha256 !== startupExclusion.writerInventorySha256))
4087
+ throw new Error("Telegram custody readiness evidence has conflicting authorities.");
4088
+ if (migration?.migrationAuthorityId && migrationCompletion &&
4089
+ (migration.migrationAuthorityId !== migrationCompletion.authorityId ||
4090
+ migration.startupAuthorityId !== migrationCompletion.startupAuthorityId ||
4091
+ migration.closureOperationId !== migrationCompletion.closureOperationId ||
4092
+ migration.migrationInventorySha256 !== migrationCompletion.migrationInventorySha256 ||
4093
+ migration.resultingSourceFamily !== migrationCompletion.resultingSourceFamily))
4094
+ throw new Error("Telegram custody readiness evidence has conflicting migration authorities.");
4095
+ if (migrationCompletion && startupExclusion &&
4096
+ (migrationCompletion.startupAuthorityId !== startupExclusion.authorityId ||
4097
+ migrationCompletion.closureOperationId !== startupExclusion.closureOperationId))
4098
+ throw new Error("Telegram custody readiness evidence has conflicting cutover authorities.");
4099
+ return { version: 1, revision: snapshot.revision as number,
4100
+ ...(writerExclusion ? { writerExclusion } : {}),
4101
+ ...(migration ? { migration } : {}),
4102
+ ...(startupExclusion ? { startupExclusion } : {}),
4103
+ ...(migrationCompletion ? { migrationCompletion } : {}) };
4104
+ };
4105
+ const read = () => decode(deps.readRetained());
4106
+ const publish = (input: { expectedRevision: number } & (
4107
+ | { kind: "writer-exclusion"; evidence: TelegramInputCustodyWriterExclusionEvidence }
4108
+ | { kind: "migration"; evidence: TelegramInputCustodyMigrationEvidence }
4109
+ | { kind: "startup-exclusion"; evidence: TelegramInputCustodyStartupExclusionAuthority }
4110
+ | { kind: "migration-completion"; evidence: TelegramInputCustodyMigrationCompletionAuthority }
4111
+ )) => deps.withSerialization(() => {
4112
+ const current = read();
4113
+ if (current.revision !== input.expectedRevision) throw new Error(
4114
+ "Telegram custody readiness evidence revision changed.",
4115
+ );
4116
+ const next = { ...current, revision: current.revision + 1,
4117
+ ...(input.kind === "writer-exclusion" ? { writerExclusion: input.evidence } :
4118
+ input.kind === "migration" ? { migration: input.evidence } :
4119
+ input.kind === "startup-exclusion" ? { startupExclusion: input.evidence } :
4120
+ { migrationCompletion: input.evidence }) };
4121
+ const serialized = `${JSON.stringify(next)}\n`;
4122
+ const validated = decode(serialized);
4123
+ const normalizedEvidence = input.kind === "writer-exclusion"
4124
+ ? validated.writerExclusion : input.kind === "migration"
4125
+ ? validated.migration : input.kind === "startup-exclusion"
4126
+ ? validated.startupExclusion : validated.migrationCompletion;
4127
+ if (!normalizedEvidence || !deps.authorizePublication(input.kind, normalizedEvidence))
4128
+ throw new Error("Telegram custody readiness evidence publication is unauthorized.");
4129
+ deps.publishRetained(serialized);
4130
+ return validated;
4131
+ });
4132
+ return { read, publish };
4133
+ }
4134
+
4135
+ export interface TelegramInputCustodyActivationEvidenceIdentity {
4136
+ profileKey: string;
4137
+ recoveryKey: string;
4138
+ }
4139
+
4140
+ export interface TelegramInputCustodyStartupExclusionAuthority
4141
+ extends TelegramInputCustodyActivationEvidenceIdentity {
4142
+ version: 1;
4143
+ status: "enforced" | "revoked";
4144
+ authorityId: string;
4145
+ closureOperationId: string;
4146
+ writerInventorySha256: string;
4147
+ allowedWriterProtocol: "custody-v3";
4148
+ authorizedAtMs: number;
4149
+ }
4150
+
4151
+ export function normalizeTelegramInputCustodyStartupExclusionAuthority(
4152
+ value: unknown,
4153
+ expected: TelegramInputCustodyActivationEvidenceIdentity,
4154
+ ): TelegramInputCustodyStartupExclusionAuthority | undefined {
4155
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
4156
+ const record = value as Record<string, unknown>;
4157
+ const keys = ["version", "status", "authorityId", "closureOperationId", "profileKey",
4158
+ "recoveryKey", "writerInventorySha256", "allowedWriterProtocol", "authorizedAtMs"];
4159
+ if (Object.keys(record).some(key => !keys.includes(key)) || record.version !== 1 ||
4160
+ (record.status !== "enforced" && record.status !== "revoked") ||
4161
+ record.profileKey !== expected.profileKey || record.recoveryKey !== expected.recoveryKey ||
4162
+ typeof record.authorityId !== "string" || !record.authorityId || record.authorityId.length > 512 ||
4163
+ typeof record.closureOperationId !== "string" || !record.closureOperationId ||
4164
+ record.closureOperationId.length > 512 ||
4165
+ typeof record.writerInventorySha256 !== "string" ||
4166
+ !/^[a-f0-9]{64}$/u.test(record.writerInventorySha256) ||
4167
+ record.allowedWriterProtocol !== "custody-v3" ||
4168
+ !Number.isSafeInteger(record.authorizedAtMs) || (record.authorizedAtMs as number) < 0)
4169
+ return undefined;
4170
+ return { version: 1, status: record.status, authorityId: record.authorityId,
4171
+ closureOperationId: record.closureOperationId, profileKey: expected.profileKey,
4172
+ recoveryKey: expected.recoveryKey, writerInventorySha256: record.writerInventorySha256,
4173
+ allowedWriterProtocol: "custody-v3", authorizedAtMs: record.authorizedAtMs as number };
4174
+ }
4175
+
4176
+ export interface TelegramInputCustodyWriterExclusionEvidence
4177
+ extends TelegramInputCustodyActivationEvidenceIdentity {
4178
+ version: 1;
4179
+ status: "excluded" | "present" | "unknown";
4180
+ startupAuthorityId?: string;
4181
+ closureOperationId?: string;
4182
+ writerInventorySha256?: string;
4183
+ }
4184
+
4185
+ export interface TelegramInputCustodyWriterInventory
4186
+ extends TelegramInputCustodyActivationEvidenceIdentity {
4187
+ complete: boolean;
4188
+ writerInventorySha256: string;
4189
+ writers: readonly { processId: number; processBirthId: string }[];
4190
+ }
4191
+
4192
+ export function evaluateTelegramInputCustodyWriterExclusionEvidence(input: {
4193
+ expected: TelegramInputCustodyActivationEvidenceIdentity;
4194
+ inventory: TelegramInputCustodyWriterInventory;
4195
+ startupAuthority: TelegramInputCustodyStartupExclusionAuthority | undefined;
4196
+ getProcessLiveness(writer: {
4197
+ processId: number; processBirthId: string;
4198
+ }): TelegramProcessLiveness;
4199
+ }): TelegramInputCustodyWriterExclusionEvidence {
4200
+ const identity = { ...input.expected };
4201
+ const authority = input.startupAuthority;
4202
+ if (!input.inventory.complete ||
4203
+ input.inventory.profileKey !== input.expected.profileKey ||
4204
+ input.inventory.recoveryKey !== input.expected.recoveryKey ||
4205
+ !/^[a-f0-9]{64}$/u.test(input.inventory.writerInventorySha256) ||
4206
+ !authority || authority.status !== "enforced" ||
4207
+ authority.profileKey !== input.expected.profileKey ||
4208
+ authority.recoveryKey !== input.expected.recoveryKey ||
4209
+ authority.writerInventorySha256 !== input.inventory.writerInventorySha256)
4210
+ return { version: 1, status: "unknown", ...identity };
4211
+ const authorityBinding = { startupAuthorityId: authority.authorityId,
4212
+ closureOperationId: authority.closureOperationId,
4213
+ writerInventorySha256: authority.writerInventorySha256 };
4214
+ let unknown = false;
4215
+ for (const writer of input.inventory.writers) {
4216
+ let liveness: TelegramProcessLiveness;
4217
+ try { liveness = input.getProcessLiveness(writer); } catch { liveness = "unverifiable"; }
4218
+ if (liveness === "alive") return { version: 1, status: "present", ...identity,
4219
+ ...authorityBinding };
4220
+ if (liveness !== "dead") unknown = true;
4221
+ }
4222
+ return { version: 1, status: unknown ? "unknown" : "excluded", ...identity,
4223
+ ...authorityBinding };
4224
+ }
4225
+
4226
+ export interface TelegramInputCustodyMigrationCompletionAuthority
4227
+ extends TelegramInputCustodyActivationEvidenceIdentity {
4228
+ version: 1;
4229
+ status: "authorized" | "revoked";
4230
+ authorityId: string;
4231
+ startupAuthorityId: string;
4232
+ closureOperationId: string;
4233
+ migrationInventorySha256: string;
4234
+ resultingSourceFamily: "absent" | "v3";
4235
+ authorizedAtMs: number;
4236
+ }
4237
+
4238
+ export function normalizeTelegramInputCustodyMigrationCompletionAuthority(
4239
+ value: unknown,
4240
+ expected: TelegramInputCustodyActivationEvidenceIdentity,
4241
+ ): TelegramInputCustodyMigrationCompletionAuthority | undefined {
4242
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
4243
+ const record = value as Record<string, unknown>;
4244
+ const keys = ["version", "status", "authorityId", "startupAuthorityId",
4245
+ "closureOperationId", "profileKey", "recoveryKey", "migrationInventorySha256",
4246
+ "resultingSourceFamily", "authorizedAtMs"];
4247
+ if (Object.keys(record).some(key => !keys.includes(key)) || record.version !== 1 ||
4248
+ (record.status !== "authorized" && record.status !== "revoked") ||
4249
+ record.profileKey !== expected.profileKey || record.recoveryKey !== expected.recoveryKey ||
4250
+ typeof record.authorityId !== "string" || !record.authorityId || record.authorityId.length > 512 ||
4251
+ typeof record.startupAuthorityId !== "string" || !record.startupAuthorityId ||
4252
+ record.startupAuthorityId.length > 512 ||
4253
+ typeof record.closureOperationId !== "string" || !record.closureOperationId ||
4254
+ record.closureOperationId.length > 512 ||
4255
+ typeof record.migrationInventorySha256 !== "string" ||
4256
+ !/^[a-f0-9]{64}$/u.test(record.migrationInventorySha256) ||
4257
+ (record.resultingSourceFamily !== "absent" && record.resultingSourceFamily !== "v3") ||
4258
+ !Number.isSafeInteger(record.authorizedAtMs) || (record.authorizedAtMs as number) < 0)
4259
+ return undefined;
4260
+ return { version: 1, status: record.status, authorityId: record.authorityId,
4261
+ startupAuthorityId: record.startupAuthorityId,
4262
+ closureOperationId: record.closureOperationId, profileKey: expected.profileKey,
4263
+ recoveryKey: expected.recoveryKey,
4264
+ migrationInventorySha256: record.migrationInventorySha256,
4265
+ resultingSourceFamily: record.resultingSourceFamily,
4266
+ authorizedAtMs: record.authorizedAtMs as number };
4267
+ }
4268
+
4269
+ export interface TelegramInputCustodyMigrationEvidence
4270
+ extends TelegramInputCustodyActivationEvidenceIdentity {
4271
+ version: 1;
4272
+ status: "complete" | "incomplete" | "unknown";
4273
+ migrationAuthorityId?: string;
4274
+ startupAuthorityId?: string;
4275
+ closureOperationId?: string;
4276
+ migrationInventorySha256?: string;
4277
+ resultingSourceFamily?: "absent" | "v3";
4278
+ }
4279
+
4280
+ export type TelegramInputCustodyWriterCutoverResult<TMode> =
4281
+ | { kind: "blocked"; blocker: "startup-authority" | "writer-inventory";
4282
+ evidence?: TelegramInputCustodyWriterExclusionEvidence }
4283
+ | { kind: "completed"; mode: TMode; evidence: TelegramInputCustodyWriterExclusionEvidence;
4284
+ resumed: boolean };
4285
+
4286
+ export function executeTelegramInputCustodyWriterCutover<TClosure extends {
4287
+ operationId: string; profileKey: string; recoveryKey: string;
4288
+ }, TMode extends TelegramInputCustodyWriterProtocolModeEvidence>(input: {
4289
+ expected: TelegramInputCustodyActivationEvidenceIdentity;
4290
+ closure: TClosure;
4291
+ startupAuthority: TelegramInputCustodyStartupExclusionAuthority;
4292
+ inventory: TelegramInputCustodyWriterInventory;
4293
+ getProcessLiveness(writer: { processId: number; processBirthId: string }): TelegramProcessLiveness;
4294
+ installProtocolMode(closure: TClosure, authority: {
4295
+ startupAuthorityId: string; writerInventorySha256: string;
4296
+ }): { mode: TMode; resumed: boolean };
4297
+ evidenceStore: {
4298
+ read(): TelegramInputCustodyReadinessEvidenceSnapshot;
4299
+ publish(input: { expectedRevision: number; kind: "writer-exclusion";
4300
+ evidence: TelegramInputCustodyWriterExclusionEvidence }):
4301
+ TelegramInputCustodyReadinessEvidenceSnapshot;
4302
+ };
4303
+ }): TelegramInputCustodyWriterCutoverResult<TMode> {
4304
+ const matchesAuthority = (candidate: TelegramInputCustodyStartupExclusionAuthority | undefined) =>
4305
+ Boolean(candidate && candidate.status === "enforced" && candidate.version === 1 &&
4306
+ candidate.profileKey === input.expected.profileKey &&
4307
+ candidate.recoveryKey === input.expected.recoveryKey &&
4308
+ candidate.authorityId === input.startupAuthority.authorityId &&
4309
+ candidate.closureOperationId === input.closure.operationId &&
4310
+ candidate.writerInventorySha256 === input.startupAuthority.writerInventorySha256 &&
4311
+ candidate.allowedWriterProtocol === "custody-v3" &&
4312
+ candidate.authorizedAtMs === input.startupAuthority.authorizedAtMs);
4313
+ if (input.closure.profileKey !== input.expected.profileKey ||
4314
+ input.closure.recoveryKey !== input.expected.recoveryKey ||
4315
+ !matchesAuthority(input.evidenceStore.read().startupExclusion))
4316
+ return { kind: "blocked", blocker: "startup-authority" };
4317
+ const evidence = evaluateTelegramInputCustodyWriterExclusionEvidence({
4318
+ expected: input.expected, inventory: input.inventory,
4319
+ startupAuthority: input.startupAuthority, getProcessLiveness: input.getProcessLiveness });
4320
+ if (evidence.status !== "excluded")
4321
+ return { kind: "blocked", blocker: "writer-inventory", evidence };
4322
+ const installed = input.installProtocolMode(input.closure, {
4323
+ startupAuthorityId: input.startupAuthority.authorityId,
4324
+ writerInventorySha256: input.startupAuthority.writerInventorySha256,
4325
+ });
4326
+ if (installed.mode.protocol !== "custody-v3" ||
4327
+ installed.mode.profileKey !== input.expected.profileKey ||
4328
+ installed.mode.recoveryKey !== input.expected.recoveryKey ||
4329
+ installed.mode.startupAuthorityId !== evidence.startupAuthorityId ||
4330
+ installed.mode.closureOperationId !== evidence.closureOperationId ||
4331
+ installed.mode.writerInventorySha256 !== evidence.writerInventorySha256)
4332
+ throw new Error("Telegram custody writer protocol installation returned mismatched authority.");
4333
+ const current = input.evidenceStore.read();
4334
+ if (!matchesAuthority(current.startupExclusion))
4335
+ return { kind: "blocked", blocker: "startup-authority", evidence };
4336
+ const existing = current.writerExclusion;
4337
+ if (existing?.status === "excluded" && existing.profileKey === evidence.profileKey &&
4338
+ existing.recoveryKey === evidence.recoveryKey &&
4339
+ existing.startupAuthorityId === evidence.startupAuthorityId &&
4340
+ existing.closureOperationId === evidence.closureOperationId &&
4341
+ existing.writerInventorySha256 === evidence.writerInventorySha256)
4342
+ return { kind: "completed", mode: installed.mode, evidence: existing,
4343
+ resumed: installed.resumed };
4344
+ input.evidenceStore.publish({ expectedRevision: current.revision,
4345
+ kind: "writer-exclusion", evidence });
4346
+ return { kind: "completed", mode: installed.mode, evidence, resumed: false };
4347
+ }
4348
+
4349
+ function matchesTelegramInputCustodyActivationEvidence(
4350
+ evidence: TelegramInputCustodyActivationEvidenceIdentity | undefined,
4351
+ expected: TelegramInputCustodyActivationEvidenceIdentity,
4352
+ ): boolean {
4353
+ return Boolean(evidence && evidence.profileKey === expected.profileKey &&
4354
+ evidence.recoveryKey === expected.recoveryKey);
4355
+ }
4356
+
4357
+ export type TelegramInputCustodyMigrationCompletionResult =
4358
+ | { kind: "blocked"; blocker: "migration-authority" | "source-drift" |
4359
+ "inventory-drift" }
4360
+ | { kind: "completed"; evidence: TelegramInputCustodyMigrationEvidence;
4361
+ resumed: boolean };
4362
+
4363
+ export function executeTelegramInputCustodyMigrationCompletion(input: {
4364
+ expected: TelegramInputCustodyActivationEvidenceIdentity;
4365
+ authority: TelegramInputCustodyMigrationCompletionAuthority;
4366
+ migrationInventorySha256: string;
4367
+ inspectSource(): "absent" | "v3" | "legacy" | "unsupported" | "ambiguous";
4368
+ evidenceStore: {
4369
+ read(): TelegramInputCustodyReadinessEvidenceSnapshot;
4370
+ publish(input: { expectedRevision: number; kind: "migration";
4371
+ evidence: TelegramInputCustodyMigrationEvidence }):
4372
+ TelegramInputCustodyReadinessEvidenceSnapshot;
4373
+ };
4374
+ }): TelegramInputCustodyMigrationCompletionResult {
4375
+ const matchesAuthority = (candidate: TelegramInputCustodyMigrationCompletionAuthority |
4376
+ undefined) => Boolean(candidate && candidate.version === 1 && candidate.status === "authorized" &&
4377
+ candidate.profileKey === input.expected.profileKey &&
4378
+ candidate.recoveryKey === input.expected.recoveryKey &&
4379
+ candidate.authorityId === input.authority.authorityId &&
4380
+ candidate.startupAuthorityId === input.authority.startupAuthorityId &&
4381
+ candidate.closureOperationId === input.authority.closureOperationId &&
4382
+ candidate.migrationInventorySha256 === input.authority.migrationInventorySha256 &&
4383
+ candidate.resultingSourceFamily === input.authority.resultingSourceFamily &&
4384
+ candidate.authorizedAtMs === input.authority.authorizedAtMs);
4385
+ const current = input.evidenceStore.read();
4386
+ if (!matchesAuthority(current.migrationCompletion))
4387
+ return { kind: "blocked", blocker: "migration-authority" };
4388
+ if (input.migrationInventorySha256 !== input.authority.migrationInventorySha256)
4389
+ return { kind: "blocked", blocker: "inventory-drift" };
4390
+ let source: ReturnType<typeof input.inspectSource>;
4391
+ try { source = input.inspectSource(); } catch { source = "ambiguous"; }
4392
+ if (source !== input.authority.resultingSourceFamily)
4393
+ return { kind: "blocked", blocker: "source-drift" };
4394
+ const evidence: TelegramInputCustodyMigrationEvidence = { version: 1,
4395
+ status: "complete", ...input.expected,
4396
+ migrationAuthorityId: input.authority.authorityId,
4397
+ startupAuthorityId: input.authority.startupAuthorityId,
4398
+ closureOperationId: input.authority.closureOperationId,
4399
+ migrationInventorySha256: input.authority.migrationInventorySha256,
4400
+ resultingSourceFamily: input.authority.resultingSourceFamily };
4401
+ const existing = current.migration;
4402
+ if (existing?.status === "complete" && existing.profileKey === evidence.profileKey &&
4403
+ existing.recoveryKey === evidence.recoveryKey &&
4404
+ existing.migrationAuthorityId === evidence.migrationAuthorityId &&
4405
+ existing.startupAuthorityId === evidence.startupAuthorityId &&
4406
+ existing.closureOperationId === evidence.closureOperationId &&
4407
+ existing.migrationInventorySha256 === evidence.migrationInventorySha256 &&
4408
+ existing.resultingSourceFamily === evidence.resultingSourceFamily)
4409
+ return { kind: "completed", evidence: existing, resumed: true };
4410
+ input.evidenceStore.publish({ expectedRevision: current.revision, kind: "migration", evidence });
4411
+ return { kind: "completed", evidence, resumed: false };
4412
+ }
4413
+
4414
+ export interface TelegramInputCustodyWriterProtocolModeEvidence
4415
+ extends TelegramInputCustodyActivationEvidenceIdentity {
4416
+ protocol: "custody-v3";
4417
+ startupAuthorityId: string;
4418
+ closureOperationId: string;
4419
+ writerInventorySha256: string;
4420
+ }
4421
+
4422
+ export function createTelegramInputCustodyProvenReadinessResolver(deps: {
4423
+ isRequested(): boolean;
4424
+ expectedIdentity(): TelegramInputCustodyActivationEvidenceIdentity;
4425
+ inspectSource(): "absent" | "v3" | "legacy" | "unsupported" | "ambiguous";
4426
+ readWriterExclusionEvidence(): TelegramInputCustodyWriterExclusionEvidence | undefined;
4427
+ readStartupExclusionAuthority(): TelegramInputCustodyStartupExclusionAuthority | undefined;
4428
+ readWriterProtocolMode(): TelegramInputCustodyWriterProtocolModeEvidence | undefined;
4429
+ readMigrationCompletionAuthority(): TelegramInputCustodyMigrationCompletionAuthority | undefined;
4430
+ readMigrationEvidence(): TelegramInputCustodyMigrationEvidence | undefined;
4431
+ listPeerReadiness(): readonly ("ready" | "legacy" | "unknown")[];
4432
+ }): () => ReturnType<typeof evaluateTelegramInputCustodyActivationReadiness> {
4433
+ return createTelegramInputCustodyActivationReadinessResolver({
4434
+ isRequested: deps.isRequested,
4435
+ inspectSource() {
4436
+ let source: "absent" | "v3" | "legacy" | "unsupported" | "ambiguous";
4437
+ try { source = deps.inspectSource(); } catch { return "ambiguous"; }
4438
+ const expected = deps.expectedIdentity();
4439
+ const authority = deps.readMigrationCompletionAuthority();
4440
+ return matchesTelegramInputCustodyActivationEvidence(authority, expected) &&
4441
+ authority?.status === "authorized" && authority.resultingSourceFamily === source
4442
+ ? source : "ambiguous";
4443
+ },
4444
+ areLegacyWritersExcluded() {
4445
+ const expected = deps.expectedIdentity();
4446
+ const evidence = deps.readWriterExclusionEvidence();
4447
+ const authority = deps.readStartupExclusionAuthority();
4448
+ const mode = deps.readWriterProtocolMode();
4449
+ return matchesTelegramInputCustodyActivationEvidence(evidence, expected) &&
4450
+ matchesTelegramInputCustodyActivationEvidence(authority, expected) &&
4451
+ matchesTelegramInputCustodyActivationEvidence(mode, expected) &&
4452
+ evidence?.version === 1 && evidence.status === "excluded" &&
4453
+ authority?.version === 1 && authority.status === "enforced" &&
4454
+ mode?.protocol === "custody-v3" &&
4455
+ evidence.startupAuthorityId === authority.authorityId &&
4456
+ evidence.startupAuthorityId === mode.startupAuthorityId &&
4457
+ evidence.closureOperationId === authority.closureOperationId &&
4458
+ evidence.closureOperationId === mode.closureOperationId &&
4459
+ evidence.writerInventorySha256 === authority.writerInventorySha256 &&
4460
+ evidence.writerInventorySha256 === mode.writerInventorySha256;
4461
+ },
4462
+ isHistoricalMigrationComplete() {
4463
+ const expected = deps.expectedIdentity();
4464
+ const evidence = deps.readMigrationEvidence();
4465
+ const authority = deps.readMigrationCompletionAuthority();
4466
+ const startup = deps.readStartupExclusionAuthority();
4467
+ const mode = deps.readWriterProtocolMode();
4468
+ let source: "absent" | "v3" | "legacy" | "unsupported" | "ambiguous";
4469
+ try { source = deps.inspectSource(); } catch { source = "ambiguous"; }
4470
+ return matchesTelegramInputCustodyActivationEvidence(evidence, expected) &&
4471
+ matchesTelegramInputCustodyActivationEvidence(authority, expected) &&
4472
+ evidence?.version === 1 && evidence.status === "complete" &&
4473
+ authority?.version === 1 && authority.status === "authorized" &&
4474
+ authority.resultingSourceFamily === source &&
4475
+ evidence.migrationAuthorityId === authority.authorityId &&
4476
+ evidence.startupAuthorityId === authority.startupAuthorityId &&
4477
+ evidence.startupAuthorityId === startup?.authorityId &&
4478
+ evidence.startupAuthorityId === mode?.startupAuthorityId &&
4479
+ evidence.closureOperationId === authority.closureOperationId &&
4480
+ evidence.closureOperationId === mode?.closureOperationId &&
4481
+ evidence.migrationInventorySha256 === authority.migrationInventorySha256 &&
4482
+ evidence.resultingSourceFamily === authority.resultingSourceFamily;
4483
+ },
4484
+ listPeerReadiness: deps.listPeerReadiness,
4485
+ });
4486
+ }
4487
+
4488
+ export function createTelegramInputCustodyActivationReadinessResolver(deps: {
4489
+ isRequested(): boolean;
4490
+ inspectSource(): "absent" | "v3" | "legacy" | "unsupported" | "ambiguous";
4491
+ areLegacyWritersExcluded(): boolean;
4492
+ isHistoricalMigrationComplete(): boolean;
4493
+ listPeerReadiness(): readonly ("ready" | "legacy" | "unknown")[];
4494
+ }): () => ReturnType<typeof evaluateTelegramInputCustodyActivationReadiness> {
4495
+ return () => {
4496
+ const requested = deps.isRequested();
4497
+ if (!requested) return { enabled: false, blocker: "disabled" };
4498
+ let legacyWritersExcluded = false;
4499
+ try { legacyWritersExcluded = deps.areLegacyWritersExcluded(); } catch { /* blocked */ }
4500
+ if (!legacyWritersExcluded) return { enabled: false, blocker: "legacy-writers-present" };
4501
+ let historicalMigrationComplete = false;
4502
+ try { historicalMigrationComplete = deps.isHistoricalMigrationComplete(); } catch { /* blocked */ }
4503
+ if (!historicalMigrationComplete) return { enabled: false, blocker: "migration-incomplete" };
4504
+ let sourceStatus: "absent" | "v3" | "legacy" | "unsupported" | "ambiguous";
4505
+ try { sourceStatus = deps.inspectSource(); } catch { sourceStatus = "ambiguous"; }
4506
+ if (sourceStatus !== "absent" && sourceStatus !== "v3")
4507
+ return { enabled: false, blocker: "source-unready" };
4508
+ let peerReadiness: readonly ("ready" | "legacy" | "unknown")[];
4509
+ try { peerReadiness = deps.listPeerReadiness(); } catch { peerReadiness = ["unknown"]; }
4510
+ return evaluateTelegramInputCustodyActivationReadiness({ requested,
4511
+ sourceStatus, legacyWritersExcluded, historicalMigrationComplete, peerReadiness });
4512
+ };
4513
+ }
4514
+
4515
+ export function createTelegramInputCustodyLifecycleBindingResolver(deps: {
4516
+ isEnabled(): boolean;
4517
+ resolveInputJournal(): { runtimeKey: string; recoveryKey: string;
4518
+ journal: TelegramInputJournalStore } | undefined;
4519
+ getRecipientBindingKey(): string | undefined;
4520
+ }): () => TelegramUpdateAdmissionLifecycleJournalBinding | undefined {
4521
+ return () => {
4522
+ if (!deps.isEnabled()) return undefined;
4523
+ const source = deps.resolveInputJournal();
4524
+ const recipientBindingKey = deps.getRecipientBindingKey()?.trim();
4525
+ if (!source || !source.runtimeKey || !source.recoveryKey || !recipientBindingKey)
4526
+ return undefined;
4527
+ const port = createTelegramInputCustodyWorkerJournalPort(source.journal);
4528
+ return { runtimeKey: JSON.stringify({ source: source.runtimeKey, recipientBindingKey }),
4529
+ recoveryKey: source.recoveryKey,
4530
+ recipientBindingKey, journal: { ...port, appendBatch: source.journal.appendBatch,
4531
+ discardQueued: source.journal.discardQueued,
4532
+ recoverDeadQueueOwner: source.journal.recoverDeadQueueOwner,
4533
+ offerQueuedHandoff: source.journal.offerQueuedHandoff,
4534
+ acceptQueuedHandoff: source.journal.acceptQueuedHandoff,
4535
+ cancelQueuedHandoff: source.journal.cancelQueuedHandoff } };
4536
+ };
4537
+ }
4538
+
4539
+ export function createTelegramCustodiedExecutionSession(input: {
4540
+ journal: TelegramCustodyExecutionJournal;
4541
+ recipientBindingKey: string;
4542
+ }): {
4543
+ execute(update: TelegramJournaledUpdate,
4544
+ handler: (update: TelegramJournaledUpdate) => Promise<TelegramUpdateAdmissionOutcome>):
4545
+ Promise<TelegramCustodiedExecutionResult>;
4546
+ settle(updateId: number, outcome: TelegramUpdateAdmissionOutcome):
4547
+ Promise<TelegramCustodiedExecutionResult>;
4548
+ } {
4549
+ const receipts = new Map<number, TelegramInputJournalReceipt>();
4550
+ const settledQueues = new Map<number, Extract<TelegramCustodiedExecutionResult,
4551
+ { status: "queued" }>>();
4552
+ const settle = async (updateId: number, outcome: TelegramUpdateAdmissionOutcome):
4553
+ Promise<TelegramCustodiedExecutionResult> => {
4554
+ const receipt = receipts.get(updateId);
4555
+ if (!receipt) {
4556
+ const duplicate = settledQueues.get(updateId);
4557
+ if (duplicate && outcome.kind === "queued" &&
4558
+ duplicate.queueReceipt.queueKind === outcome.queueKind &&
4559
+ duplicate.queueReceipt.receiptId === outcome.receiptId &&
4560
+ duplicate.queueReceipt.sourceUpdateIds.length === outcome.sourceUpdateIds.length &&
4561
+ duplicate.queueReceipt.sourceUpdateIds.every((id, index) => id === outcome.sourceUpdateIds[index]))
4562
+ { settledQueues.delete(updateId); return duplicate; }
4563
+ throw new TelegramUpdateAdmissionOutcomeError(
4564
+ `Telegram deferred custody ${updateId} has no exact running receipt.`,
4565
+ );
4566
+ }
4567
+ if (outcome.kind === "deferred") return { status: "deferred", receipt };
4568
+ if (outcome.kind === "queued") {
4569
+ if (!outcome.sourceUpdateIds.includes(updateId))
4570
+ throw new TelegramUpdateAdmissionOutcomeError(
4571
+ "Telegram grouped queue custody omitted the settling input.",
4572
+ );
4573
+ const grouped = outcome.sourceUpdateIds.map(sourceUpdateId => receipts.get(sourceUpdateId));
4574
+ if (grouped.some(candidate => !candidate)) throw new TelegramUpdateAdmissionOutcomeError(
4575
+ "Telegram grouped queue custody omitted an exact running source receipt.",
4576
+ );
4577
+ const queued = input.journal.queueInputs({ queueKind: outcome.queueKind,
4578
+ receiptId: outcome.receiptId, receipts: grouped as TelegramInputJournalReceipt[] });
4579
+ const result = { status: "queued" as const, queueReceipt: queued.queueReceipt };
4580
+ for (const sourceUpdateId of outcome.sourceUpdateIds) {
4581
+ receipts.delete(sourceUpdateId);
4582
+ if (sourceUpdateId !== updateId) settledQueues.set(sourceUpdateId, result);
4583
+ }
4584
+ return result;
4585
+ }
4586
+ input.journal.completeInput(receipt);
4587
+ receipts.delete(updateId);
4588
+ return { status: "completed" };
4589
+ };
4590
+ return {
4591
+ settle,
4592
+ async execute(update, handler) {
4593
+ const acquired = input.journal.acquireInput({ updateId: update.update_id,
4594
+ recipientBindingKey: input.recipientBindingKey, executionUpdate: update });
4595
+ const started = input.journal.startInput(acquired.receipt);
4596
+ if (!started.started) return { status: "outcome-unknown", receipt: acquired.receipt };
4597
+ receipts.set(update.update_id, acquired.receipt);
4598
+ let outcome: TelegramUpdateAdmissionOutcome;
4599
+ try { outcome = await handler(started.update); }
4600
+ catch (error) { receipts.delete(update.update_id); throw error; }
4601
+ return settle(update.update_id, outcome);
4602
+ },
4603
+ };
4604
+ }
4605
+
4606
+ export async function executeTelegramCustodiedInput(input: {
4607
+ journal: TelegramCustodyExecutionJournal;
4608
+ update: TelegramJournaledUpdate;
4609
+ recipientBindingKey: string;
4610
+ execute(update: TelegramJournaledUpdate): Promise<TelegramUpdateAdmissionOutcome>;
4611
+ }): Promise<TelegramCustodiedExecutionResult> {
4612
+ return createTelegramCustodiedExecutionSession(input).execute(input.update, input.execute);
4613
+ }
4614
+
3490
4615
  /**
3491
4616
  * Compose the stable public handler registry with source-bound semantic
3492
4617
  * admission. Production polling switches to this only with the journal worker.
@@ -3570,6 +4695,34 @@ export function createTelegramUpdateAdmissionHandle<
3570
4695
  };
3571
4696
  }
3572
4697
 
4698
+ export function createTelegramCustodiedUpdateAdmissionHandle<
4699
+ TUpdate extends TelegramJournaledUpdate & TelegramUpdateFlow,
4700
+ TContext,
4701
+ >(deps: Omit<TelegramUpdateAdmissionHandleDeps<TUpdate, TContext>,
4702
+ "onLateOutcome" | "onLateOutcomeError"> & {
4703
+ journal: TelegramCustodyExecutionJournal;
4704
+ recipientBindingKey: string;
4705
+ onLateOutcomeError(error: unknown, updateId: number): void;
4706
+ onCustodiedLateSettlement?: (result: TelegramCustodiedExecutionResult,
4707
+ details: { updateId: number; signal: AbortSignal }) => void;
4708
+ }): (update: TUpdate, ctx: TContext, signal: AbortSignal) =>
4709
+ Promise<TelegramCustodiedExecutionResult> {
4710
+ const session = createTelegramCustodiedExecutionSession({ journal: deps.journal,
4711
+ recipientBindingKey: deps.recipientBindingKey });
4712
+ const admission = createTelegramUpdateAdmissionHandle({
4713
+ defaultHandle: deps.defaultHandle,
4714
+ registry: deps.registry,
4715
+ async onLateOutcome(outcome, details) {
4716
+ const result = await session.settle(details.updateId, outcome);
4717
+ deps.onCustodiedLateSettlement?.(result,
4718
+ { updateId: details.updateId, signal: details.signal });
4719
+ },
4720
+ onLateOutcomeError: deps.onLateOutcomeError,
4721
+ });
4722
+ return (update, ctx, signal) => session.execute(update,
4723
+ started => admission(started as TUpdate, ctx, signal));
4724
+ }
4725
+
3573
4726
  export interface TelegramQueueAdmissionItemLike {
3574
4727
  admissionReceipts?: readonly TelegramQueueAdmissionReceiptLike[];
3575
4728
  }
@@ -3668,11 +4821,13 @@ export function createTelegramQueueAdmissionSettlementRuntime<TContext>(
3668
4821
  export interface TelegramUpdateAdmissionLifecycleJournalBinding {
3669
4822
  runtimeKey: string;
3670
4823
  recoveryKey: string;
4824
+ recipientBindingKey?: string;
3671
4825
  journal: TelegramUpdateWorkerJournalPort & {
4826
+ inputCustody?: TelegramCustodyExecutionJournal;
3672
4827
  appendBatch: (
3673
4828
  updates: readonly TelegramJournaledUpdate[],
3674
4829
  acceptedThroughUpdateId?: number,
3675
- ) => unknown;
4830
+ ) => Pick<TelegramUpdateJournalAppendResult, "nonExcludedUpdateIds">;
3676
4831
  applyOperatorDisposition?: (
3677
4832
  input: TelegramUpdateJournalOperatorDispositionInput,
3678
4833
  ) => TelegramUpdateJournalOperatorDispositionResult;
@@ -4298,6 +5453,9 @@ export interface TelegramUpdateAdmissionLifecycleRuntimeDeps<TContext> {
4298
5453
  journal: TelegramUpdateWorkerJournalPort,
4299
5454
  binding: TelegramUpdateAdmissionLifecycleJournalBinding,
4300
5455
  ) => TelegramUpdateWorkerRuntime<TContext>;
5456
+ acquireSourceReference?: (
5457
+ binding: TelegramUpdateAdmissionLifecycleJournalBinding,
5458
+ ) => () => void;
4301
5459
  recordRuntimeEvent?: TelegramUpdateWorkerRuntimeDeps<TContext>["recordRuntimeEvent"];
4302
5460
  }
4303
5461
 
@@ -4309,7 +5467,7 @@ export interface TelegramUpdateAdmissionLifecycleRuntime<TContext>
4309
5467
  appendBatch: (
4310
5468
  updates: readonly TelegramJournaledUpdate[],
4311
5469
  acceptedThroughUpdateId?: number,
4312
- ) => unknown;
5470
+ ) => Pick<TelegramUpdateJournalAppendResult, "nonExcludedUpdateIds">;
4313
5471
  discardQueueReceipt: (input: {
4314
5472
  queueKind: "prompt" | "control";
4315
5473
  receiptId: string;
@@ -4395,11 +5553,13 @@ export interface TelegramUpdateAdmissionRuntimeBinding<TContext> {
4395
5553
  bind: (input: {
4396
5554
  leader: TelegramUpdateAdmissionLifecycleRuntime<TContext>;
4397
5555
  follower: TelegramUpdateAdmissionLifecycleRuntime<TContext>;
5556
+ inputCustodyBus?: TelegramInputCustodyBusBindingRuntime<TContext>;
4398
5557
  }) => void;
4399
5558
  getLeader: () => TelegramUpdateAdmissionLifecycleRuntime<TContext> | undefined;
4400
5559
  getFollower: () => TelegramUpdateAdmissionLifecycleRuntime<TContext> | undefined;
4401
5560
  getActive: () => TelegramUpdateAdmissionLifecycleRuntime<TContext> | undefined;
4402
5561
  getSettlement: () => TelegramQueueAdmissionSettlementRuntime<TContext> | undefined;
5562
+ getInputCustodyBus: () => TelegramInputCustodyBusBindingRuntime<TContext> | undefined;
4403
5563
  getLifecycleForJournalBinding: (
4404
5564
  journalBindingKey: string,
4405
5565
  ) => TelegramUpdateAdmissionLifecycleRuntime<TContext> | undefined;
@@ -4415,6 +5575,7 @@ export function createTelegramUpdateAdmissionRuntimeBinding<TContext>(deps: {
4415
5575
  let leader: TelegramUpdateAdmissionLifecycleRuntime<TContext> | undefined;
4416
5576
  let follower: TelegramUpdateAdmissionLifecycleRuntime<TContext> | undefined;
4417
5577
  let settlement: TelegramQueueAdmissionSettlementRuntime<TContext> | undefined;
5578
+ let inputCustodyBus: TelegramInputCustodyBusBindingRuntime<TContext> | undefined;
4418
5579
  return {
4419
5580
  bind(input) {
4420
5581
  leader = input.leader;
@@ -4423,11 +5584,13 @@ export function createTelegramUpdateAdmissionRuntimeBinding<TContext>(deps: {
4423
5584
  leader,
4424
5585
  follower,
4425
5586
  ]);
5587
+ inputCustodyBus = input.inputCustodyBus;
4426
5588
  },
4427
5589
  getLeader: () => leader,
4428
5590
  getFollower: () => follower,
4429
5591
  getActive: () => (deps.isFollowerRegistered() ? follower : leader),
4430
5592
  getSettlement: () => settlement,
5593
+ getInputCustodyBus: () => inputCustodyBus,
4431
5594
  getLifecycleForJournalBinding(journalBindingKey) {
4432
5595
  if (follower?.ownsJournalBinding(journalBindingKey)) return follower;
4433
5596
  return leader?.ownsJournalBinding(journalBindingKey) ? leader : undefined;
@@ -4482,9 +5645,16 @@ export function createTelegramUpdateAdmissionLifecycleRuntime<TContext>(
4482
5645
  let journalBindingKey: string | undefined;
4483
5646
  let operation: Promise<void> = Promise.resolve();
4484
5647
  let foreignQueueOwnerLiveness: TelegramProcessLiveness | undefined;
5648
+ let releaseSourceReference: (() => void) | undefined;
4485
5649
 
4486
5650
  const stopCurrent = async (forget: boolean): Promise<void> => {
4487
5651
  await worker?.stop();
5652
+ const release = releaseSourceReference;
5653
+ releaseSourceReference = undefined;
5654
+ try { release?.(); } catch (error) {
5655
+ deps.recordRuntimeEvent?.("inbound-worker", error,
5656
+ { phase: "source-reference-release" });
5657
+ }
4488
5658
  if (!forget) return;
4489
5659
  journal = undefined;
4490
5660
  worker = undefined;
@@ -4493,20 +5663,35 @@ export function createTelegramUpdateAdmissionLifecycleRuntime<TContext>(
4493
5663
  activeRuntimeKey = undefined;
4494
5664
  foreignQueueOwnerLiveness = undefined;
4495
5665
  };
4496
- const bind = async (ctx: TContext): Promise<void> => {
4497
- const binding = deps.resolveBinding();
5666
+ const bind = async (ctx: TContext, replace = false): Promise<void> => {
5667
+ let binding = deps.resolveBinding();
4498
5668
  if (!binding) {
4499
5669
  await stopCurrent(true);
4500
5670
  return;
4501
5671
  }
4502
- if (!worker || activeRuntimeKey !== binding.runtimeKey) {
5672
+ if (replace || !worker || activeRuntimeKey !== binding.runtimeKey) {
5673
+ const { runtimeKey, recoveryKey } = binding;
4503
5674
  await stopCurrent(true);
4504
- journal = binding.journal;
4505
- worker = deps.createWorker(binding.journal, binding);
4506
- settlement = createTelegramQueueAdmissionSettlementRuntime(worker);
5675
+ binding = deps.resolveBinding();
5676
+ if (!binding || binding.runtimeKey !== runtimeKey ||
5677
+ binding.recoveryKey !== recoveryKey) {
5678
+ throw new Error("Telegram update source binding changed during startup.");
5679
+ }
5680
+ const release = deps.acquireSourceReference?.(binding);
5681
+ try {
5682
+ journal = binding.journal;
5683
+ worker = deps.createWorker(binding.journal, binding);
5684
+ settlement = createTelegramQueueAdmissionSettlementRuntime(worker);
5685
+ releaseSourceReference = release;
5686
+ } catch (error) {
5687
+ release?.();
5688
+ throw error;
5689
+ }
4507
5690
  journalBindingKey = binding.recoveryKey;
4508
5691
  activeRuntimeKey = binding.runtimeKey;
4509
5692
  }
5693
+ if (!releaseSourceReference && deps.acquireSourceReference)
5694
+ releaseSourceReference = deps.acquireSourceReference(binding);
4510
5695
  if (binding.journal.applyOperatorDisposition) {
4511
5696
  for (const entry of binding.journal.read().entries) {
4512
5697
  if (entry.state !== "failed" || !entry.terminalFailureId) continue;
@@ -4547,6 +5732,7 @@ export function createTelegramUpdateAdmissionLifecycleRuntime<TContext>(
4547
5732
  !entry.queueKind ||
4548
5733
  !entry.queueReceiptId ||
4549
5734
  !entry.queueOwner ||
5735
+ entry.queueHandoff ||
4550
5736
  isTelegramUpdateJournalQueueOwnerProcess(
4551
5737
  entry.queueOwner,
4552
5738
  recoveryOwner,
@@ -4606,8 +5792,8 @@ export function createTelegramUpdateAdmissionLifecycleRuntime<TContext>(
4606
5792
  onSessionShutdown: () => runExclusive(() => stopCurrent(false)),
4607
5793
  onTransportChanged: (ctx) =>
4608
5794
  runExclusive(async () => {
4609
- await stopCurrent(true);
4610
- if (ctx !== undefined) await bind(ctx);
5795
+ if (ctx !== undefined) await bind(ctx, true);
5796
+ else await stopCurrent(true);
4611
5797
  }),
4612
5798
  appendBatch(updates, acceptedThroughUpdateId) {
4613
5799
  if (!journal || !worker) {
@@ -4651,7 +5837,9 @@ export function createTelegramUpdateAdmissionLifecycleRuntime<TContext>(
4651
5837
  "Telegram update journal queue handoff acceptance is not available.",
4652
5838
  );
4653
5839
  }
4654
- return journal.acceptQueuedHandoff(input);
5840
+ const result = journal.acceptQueuedHandoff(input);
5841
+ worker.signal();
5842
+ return result;
4655
5843
  },
4656
5844
  cancelQueueReceiptHandoff(input) {
4657
5845
  if (!journal || !worker || !journal.cancelQueuedHandoff) {
@@ -4768,10 +5956,14 @@ export interface TelegramUpdateAdmissionLifecycleAssemblyDeps<
4768
5956
  TContext,
4769
5957
  > {
4770
5958
  runtimeBinding: TelegramUpdateAdmissionRuntimeBinding<TContext>;
5959
+ inputCustodyBus?: TelegramInputCustodyBusBindingRuntime<TContext>;
5960
+ acquireSourceReference?: (role: "leader" | "follower",
5961
+ binding: TelegramUpdateAdmissionLifecycleJournalBinding) => () => void;
4771
5962
  worker: Omit<
4772
5963
  TelegramUpdateAdmissionWorkerRuntimeDeps<TUpdate, TContext>,
4773
5964
  | "journal"
4774
5965
  | "getJournalBindingKey"
5966
+ | "getRecipientBindingKey"
4775
5967
  | "hasAuthority"
4776
5968
  | "prepareUpdateForExecution"
4777
5969
  >;
@@ -4795,7 +5987,9 @@ export interface TelegramUpdateAdmissionLifecycleAssemblyDeps<
4795
5987
  export type TelegramUpdateAdmissionWorkerRuntimeDeps<
4796
5988
  TUpdate extends TelegramJournaledUpdate & TelegramUpdateFlow,
4797
5989
  TContext,
4798
- > = Omit<TelegramUpdateWorkerRuntimeDeps<TContext>, "executeUpdate"> & {
5990
+ > = Omit<TelegramUpdateWorkerRuntimeDeps<TContext>,
5991
+ "executeUpdate" | "executeCustodiedUpdate"> & {
5992
+ inputCustody?: TelegramCustodyExecutionJournal;
4799
5993
  defaultHandle: (
4800
5994
  update: TUpdate,
4801
5995
  ctx: TContext,
@@ -4830,8 +6024,27 @@ export function createTelegramUpdateAdmissionWorkerRuntime<
4830
6024
  });
4831
6025
  },
4832
6026
  });
6027
+ const custodyBindingKey = deps.inputCustody ? deps.getRecipientBindingKey?.() : undefined;
6028
+ if (deps.inputCustody && !custodyBindingKey) throw new Error(
6029
+ "Telegram custodied admission worker requires one recipient binding key.",
6030
+ );
6031
+ const executeCustodiedUpdate = deps.inputCustody && custodyBindingKey
6032
+ ? createTelegramCustodiedUpdateAdmissionHandle<TUpdate, TContext>({
6033
+ journal: deps.inputCustody, recipientBindingKey: custodyBindingKey,
6034
+ defaultHandle: deps.defaultHandle, registry: deps.registry,
6035
+ onLateOutcomeError(error, updateId) {
6036
+ deps.recordRuntimeEvent?.("inbound-worker", error,
6037
+ { phase: "late-custodied-admission-handler", updateId });
6038
+ },
6039
+ onCustodiedLateSettlement(result, details) {
6040
+ worker?.settleCustodied({ updateId: details.updateId, result, signal: details.signal });
6041
+ },
6042
+ }) : undefined;
4833
6043
  worker = createTelegramUpdateWorkerRuntime({
4834
6044
  ...deps,
6045
+ ...(executeCustodiedUpdate ? { executeCustodiedUpdate: async (update: TelegramJournaledUpdate,
6046
+ ctx: TContext, signal: AbortSignal) => executeCustodiedUpdate(
6047
+ deps.prepareUpdateForExecution?.(update as TUpdate) ?? update as TUpdate, ctx, signal) } : {}),
4835
6048
  async executeUpdate(update, ctx, signal) {
4836
6049
  const typedUpdate = update as TUpdate;
4837
6050
  try {
@@ -4860,12 +6073,16 @@ export function createTelegramUpdateAdmissionLifecycleAssembly<
4860
6073
  ): TelegramUpdateAdmissionLifecycleAssembly<TContext> {
4861
6074
  const leader = createTelegramUpdateAdmissionLifecycleRuntime({
4862
6075
  resolveBinding: deps.leader.resolveBinding,
6076
+ ...(deps.acquireSourceReference ? { acquireSourceReference: binding =>
6077
+ deps.acquireSourceReference!("leader", binding) } : {}),
4863
6078
  getQueueOwnerIdentity: deps.worker.getQueueOwnerIdentity,
4864
6079
  createWorker(journal, binding) {
4865
6080
  return createTelegramUpdateAdmissionWorkerRuntime({
4866
6081
  ...deps.worker,
4867
6082
  journal,
6083
+ ...(binding.journal.inputCustody ? { inputCustody: binding.journal.inputCustody } : {}),
4868
6084
  getJournalBindingKey: () => binding.recoveryKey,
6085
+ getRecipientBindingKey: () => binding.recipientBindingKey,
4869
6086
  hasAuthority: deps.leader.hasAuthority,
4870
6087
  });
4871
6088
  },
@@ -4873,6 +6090,8 @@ export function createTelegramUpdateAdmissionLifecycleAssembly<
4873
6090
  });
4874
6091
  const follower = createTelegramUpdateAdmissionLifecycleRuntime({
4875
6092
  getQueueOwnerIdentity: deps.worker.getQueueOwnerIdentity,
6093
+ ...(deps.acquireSourceReference ? { acquireSourceReference: binding =>
6094
+ deps.acquireSourceReference!("follower", binding) } : {}),
4876
6095
  resolveBinding() {
4877
6096
  const generation = deps.follower.getGeneration();
4878
6097
  if (!deps.follower.isRegistered() || !generation) return undefined;
@@ -4893,14 +6112,17 @@ export function createTelegramUpdateAdmissionLifecycleAssembly<
4893
6112
  return createTelegramUpdateAdmissionWorkerRuntime({
4894
6113
  ...deps.worker,
4895
6114
  journal,
6115
+ ...(binding.journal.inputCustody ? { inputCustody: binding.journal.inputCustody } : {}),
4896
6116
  getJournalBindingKey: () => binding.recoveryKey,
6117
+ getRecipientBindingKey: () => binding.recipientBindingKey,
4897
6118
  hasAuthority: () => binding.hasAuthority?.() ?? false,
4898
6119
  prepareUpdateForExecution: deps.follower.prepareUpdateForExecution,
4899
6120
  });
4900
6121
  },
4901
6122
  recordRuntimeEvent: deps.recordRuntimeEvent,
4902
6123
  });
4903
- deps.runtimeBinding.bind({ leader, follower });
6124
+ deps.runtimeBinding.bind({ leader, follower,
6125
+ ...(deps.inputCustodyBus ? { inputCustodyBus: deps.inputCustodyBus } : {}) });
4904
6126
  return { leader, follower };
4905
6127
  }
4906
6128