@llblab/pi-telegram 0.50.0 → 0.51.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/AGENTS.md +9 -7
  2. package/BACKLOG.md +13 -32
  3. package/CHANGELOG.md +18 -0
  4. package/README.md +5 -5
  5. package/dist/lib/bindings.d.ts +3 -5
  6. package/dist/lib/bindings.js +4 -11
  7. package/dist/lib/bus-follower.d.ts +1 -1
  8. package/dist/lib/bus-follower.js +93 -9
  9. package/dist/lib/bus-leader.d.ts +9 -3
  10. package/dist/lib/bus-leader.js +231 -71
  11. package/dist/lib/bus.d.ts +4 -0
  12. package/dist/lib/bus.js +34 -7
  13. package/dist/lib/commands.d.ts +1 -4
  14. package/dist/lib/commands.js +13 -33
  15. package/dist/lib/extension.js +64 -6
  16. package/dist/lib/journal.d.ts +3 -0
  17. package/dist/lib/journal.js +13 -0
  18. package/dist/lib/lifecycle.d.ts +2 -0
  19. package/dist/lib/lifecycle.js +25 -4
  20. package/dist/lib/locks.d.ts +1 -0
  21. package/dist/lib/locks.js +32 -12
  22. package/dist/lib/menu-queue.d.ts +1 -0
  23. package/dist/lib/menu-queue.js +72 -1
  24. package/dist/lib/paths.d.ts +10 -0
  25. package/dist/lib/paths.js +22 -1
  26. package/dist/lib/polling.js +2 -2
  27. package/dist/lib/queue.d.ts +8 -1
  28. package/dist/lib/queue.js +28 -2
  29. package/dist/lib/replies.d.ts +3 -0
  30. package/dist/lib/replies.js +17 -0
  31. package/dist/lib/sync.d.ts +9 -0
  32. package/dist/lib/sync.js +49 -2
  33. package/dist/lib/telegram-api.d.ts +9 -1
  34. package/dist/lib/telegram-api.js +23 -3
  35. package/dist/lib/thread-cleanup-manager.js +7 -2
  36. package/dist/lib/threads.d.ts +2 -0
  37. package/dist/lib/threads.js +68 -37
  38. package/dist/lib/updates.d.ts +6 -4
  39. package/dist/lib/updates.js +134 -61
  40. package/dist/lib/workspace-admission.d.ts +9 -0
  41. package/dist/lib/workspace-admission.js +49 -3
  42. package/dist/lib/workspace-retirement.d.ts +77 -6
  43. package/dist/lib/workspace-retirement.js +384 -8
  44. package/dist/lib/workspace-slots.d.ts +3 -0
  45. package/dist/lib/workspace-slots.js +6 -0
  46. package/dist/package.json +1 -1
  47. package/docs/architecture.md +23 -14
  48. package/docs/multi-instance-bus.md +32 -11
  49. package/docs/public-api.md +1 -1
  50. package/docs/updates.md +2 -0
  51. package/lib/bindings.ts +5 -17
  52. package/lib/bus-follower.ts +69 -12
  53. package/lib/bus-leader.ts +236 -94
  54. package/lib/bus.ts +39 -9
  55. package/lib/commands.ts +18 -41
  56. package/lib/extension.ts +70 -10
  57. package/lib/journal.ts +14 -0
  58. package/lib/lifecycle.ts +23 -4
  59. package/lib/locks.ts +32 -13
  60. package/lib/menu-queue.ts +126 -1
  61. package/lib/paths.ts +29 -1
  62. package/lib/polling.ts +2 -2
  63. package/lib/queue.ts +48 -3
  64. package/lib/replies.ts +19 -0
  65. package/lib/sync.ts +53 -2
  66. package/lib/telegram-api.ts +30 -2
  67. package/lib/thread-cleanup-manager.ts +11 -3
  68. package/lib/threads.ts +73 -36
  69. package/lib/updates.ts +145 -78
  70. package/lib/workspace-admission.ts +67 -4
  71. package/lib/workspace-retirement.ts +465 -7
  72. package/lib/workspace-slots.ts +7 -0
  73. package/package.json +1 -1
package/lib/updates.ts CHANGED
@@ -698,10 +698,9 @@ export function buildTelegramUpdateFlowAction<
698
698
  NonNullable<TUpdate["message"] | TUpdate["edited_message"]>,
699
699
  NonNullable<TUpdate["guest_message"]>
700
700
  > {
701
- const deletedMessageIds = extractDeletedTelegramMessageIds(update);
702
- if (deletedMessageIds.length > 0) {
703
- return { kind: "deleted", messageIds: deletedMessageIds };
704
- }
701
+ // Business chats are independent from bot chats even when chat/message IDs coincide.
702
+ // Raw handlers may own that namespace; the default DM runtime has no deletion authority.
703
+ if (update.deleted_business_messages !== undefined) return { kind: "ignore" };
705
704
  if (update.message_reaction) {
706
705
  return { kind: "reaction", reactionUpdate: update.message_reaction };
707
706
  }
@@ -1739,6 +1738,11 @@ export interface TelegramUpdateWorkerJournalSnapshot {
1739
1738
 
1740
1739
  export interface TelegramUpdateWorkerJournalPort {
1741
1740
  read: () => TelegramUpdateWorkerJournalSnapshot;
1741
+ /** Optional strict observation; it must not recover, repair or grant new receipt authority. */
1742
+ isQueueReceiptCurrent?: (
1743
+ receipt: TelegramQueueAdmissionReceiptLike,
1744
+ owner: TelegramUpdateJournalQueueOwner,
1745
+ ) => boolean;
1742
1746
  markQueued: (receipt: {
1743
1747
  queueKind: "prompt" | "control";
1744
1748
  receiptId: string;
@@ -1858,7 +1862,7 @@ export interface TelegramUpdateWorkerRuntime<TContext> {
1858
1862
  receipts: readonly TelegramQueueAdmissionReceiptLike[];
1859
1863
  ctx: TContext;
1860
1864
  reason: TelegramQueueReceiptCompletionReason;
1861
- }) => void;
1865
+ }) => boolean;
1862
1866
  stop: () => Promise<void>;
1863
1867
  waitForDrain: () => Promise<void>;
1864
1868
  getState: () => TelegramUpdateWorkerStateSnapshot;
@@ -2268,6 +2272,23 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
2268
2272
  return true;
2269
2273
  };
2270
2274
 
2275
+ const getCurrentQueueReceipt = (receipt: TelegramQueueAdmissionReceiptLike) => {
2276
+ const committed = committedQueueReceipts.get(receipt.receiptId);
2277
+ if (!committed || !areTelegramQueueAdmissionReceiptsEqual(
2278
+ committed.receipt, normalizeQueueReceipt(receipt),
2279
+ )) return undefined;
2280
+ try {
2281
+ if (deps.journal.isQueueReceiptCurrent &&
2282
+ deps.journal.isQueueReceiptCurrent(committed.receipt, committed.queueOwner) !== true) {
2283
+ return undefined;
2284
+ }
2285
+ } catch (error) {
2286
+ recordRuntimeEvent(error, { phase: "queue-receipt-observation" });
2287
+ return undefined;
2288
+ }
2289
+ return committed;
2290
+ };
2291
+
2271
2292
  const clearRetryTimer = (): void => {
2272
2293
  if (retryTimer !== undefined) cancelRetry(retryTimer);
2273
2294
  retryTimer = undefined;
@@ -3023,11 +3044,9 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3023
3044
  !expectedOwner.controller.signal.aborted
3024
3045
  ) {
3025
3046
  pendingSignal = false;
3026
- const result = await drain(expectedOwner);
3027
- if (result !== "idle") {
3028
- pendingSignal = false;
3029
- return;
3030
- }
3047
+ // Keep newer wakes in this run so waitForDrain also covers them. A failure
3048
+ // observed after signal() still keeps its latch; this loop never clears it.
3049
+ await drain(expectedOwner);
3031
3050
  }
3032
3051
  };
3033
3052
  const operation = run();
@@ -3265,25 +3284,10 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3265
3284
  }
3266
3285
  transition("queued", input.updateId);
3267
3286
  },
3268
- isQueueReceiptCommitted(receipt) {
3269
- const committed = committedQueueReceipts.get(receipt.receiptId);
3270
- return (
3271
- committed !== undefined &&
3272
- areTelegramQueueAdmissionReceiptsEqual(
3273
- committed.receipt,
3274
- normalizeQueueReceipt(receipt),
3275
- )
3276
- );
3277
- },
3287
+ isQueueReceiptCommitted: receipt => getCurrentQueueReceipt(receipt) !== undefined,
3278
3288
  getQueueReceiptOwner(receipt) {
3279
- const committed = committedQueueReceipts.get(receipt.receiptId);
3280
- return committed &&
3281
- areTelegramQueueAdmissionReceiptsEqual(
3282
- committed.receipt,
3283
- normalizeQueueReceipt(receipt),
3284
- )
3285
- ? { ...committed.queueOwner }
3286
- : undefined;
3289
+ const committed = getCurrentQueueReceipt(receipt);
3290
+ return committed ? { ...committed.queueOwner } : undefined;
3287
3291
  },
3288
3292
  completeQueueReceipts(input) {
3289
3293
  const expectedOwner = owner;
@@ -3292,9 +3296,9 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3292
3296
  expectedOwner.controller.signal.aborted ||
3293
3297
  !(deps.isContextCurrent?.(input.ctx) ?? expectedOwner.ctx === input.ctx)
3294
3298
  ) {
3295
- return;
3299
+ return false;
3296
3300
  }
3297
- if (input.receipts.length === 0) return;
3301
+ if (input.receipts.length === 0) return true;
3298
3302
  const normalizedReceipts = input.receipts.map(normalizeQueueReceipt);
3299
3303
  const receiptIds = new Set<string>();
3300
3304
  const sourceUpdateIds = new Set<number>();
@@ -3306,7 +3310,7 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3306
3310
  }> = [];
3307
3311
  for (const receipt of normalizedReceipts) {
3308
3312
  const committed = committedQueueReceipts.get(receipt.receiptId);
3309
- if (!committed) continue;
3313
+ if (!committed) return false;
3310
3314
  if (
3311
3315
  receiptIds.has(receipt.receiptId) ||
3312
3316
  !areTelegramQueueAdmissionReceiptsEqual(
@@ -3328,7 +3332,7 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3328
3332
  `Telegram ${input.reason} requested invalid queue receipt ${receipt.receiptId}.`,
3329
3333
  ),
3330
3334
  );
3331
- return;
3335
+ return false;
3332
3336
  }
3333
3337
  receiptIds.add(receipt.receiptId);
3334
3338
  queuedCompletions.push({
@@ -3339,7 +3343,6 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3339
3343
  sourceUpdateIds.add(updateId);
3340
3344
  }
3341
3345
  }
3342
- if (receiptIds.size === 0) return;
3343
3346
  let removedUpdateIds: readonly number[];
3344
3347
  try {
3345
3348
  removedUpdateIds = deps.journal.completeQueued(
@@ -3351,7 +3354,7 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3351
3354
  "queue-receipt-completion",
3352
3355
  error,
3353
3356
  );
3354
- return;
3357
+ return false;
3355
3358
  }
3356
3359
  const removed = new Set(removedUpdateIds);
3357
3360
  if (
@@ -3365,8 +3368,11 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3365
3368
  `Telegram ${input.reason} did not complete every receipt source.`,
3366
3369
  ),
3367
3370
  );
3368
- return;
3371
+ return false;
3369
3372
  }
3373
+ input.receipts.forEach((receipt, index) => {
3374
+ acknowledgedQueueReceiptCompletions.set(receipt, normalizedReceipts[index]!);
3375
+ });
3370
3376
  for (const updateId of sourceUpdateIds) claims.delete(updateId);
3371
3377
  for (const receiptId of receiptIds) {
3372
3378
  committedQueueReceipts.delete(receiptId);
@@ -3385,6 +3391,7 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3385
3391
  notifyStateChange();
3386
3392
  pendingSignal = true;
3387
3393
  launchDrain();
3394
+ return true;
3388
3395
  },
3389
3396
  async stop() {
3390
3397
  const expectedOwner = owner;
@@ -3676,9 +3683,17 @@ export function createTelegramInputCustodyWorkerJournalPort(
3676
3683
  };
3677
3684
  return {
3678
3685
  read: () => store.read() as unknown as TelegramUpdateWorkerJournalSnapshot,
3686
+ isQueueReceiptCurrent(receipt, owner) {
3687
+ const entries = store.read().entries.filter(entry => entry.queueReceiptId === receipt.receiptId);
3688
+ return receipt.sourceUpdateIds.length > 0 && entries.length === receipt.sourceUpdateIds.length &&
3689
+ entries.every((entry, index) => entry.updateId === receipt.sourceUpdateIds[index] &&
3690
+ entry.state === "queued" && entry.queueKind === receipt.queueKind && !entry.queueHandoff &&
3691
+ entry.queueOwner !== undefined && areTelegramUpdateJournalQueueOwnersEqual(entry.queueOwner, owner));
3692
+ },
3679
3693
  markQueued: legacyMutation,
3680
3694
  markExecutionFailure: legacyMutation,
3681
- removeCompleted: legacyMutation,
3695
+ // The common drain batches excluded IDs here; the store rejects all non-vetoed input atomically.
3696
+ removeCompleted: updateIds => store.removeExcluded(updateIds),
3682
3697
  completeQueued: receipts => store.completeQueued(receipts),
3683
3698
  inputCustody: {
3684
3699
  acquireInput: store.acquireInput,
@@ -4511,13 +4526,20 @@ export function createTelegramInputCustodyLifecycleBindingResolver(deps: {
4511
4526
  journal: TelegramInputJournalStore } | undefined;
4512
4527
  getRecipientBindingKey(): string | undefined;
4513
4528
  }): () => TelegramUpdateAdmissionLifecycleJournalBinding | undefined {
4529
+ // Stable per store, not per descriptor: a renewed source has new captured worker ports.
4530
+ const ports = new WeakMap<TelegramInputJournalStore,
4531
+ ReturnType<typeof createTelegramInputCustodyWorkerJournalPort>>();
4514
4532
  return () => {
4515
4533
  if (!deps.isEnabled()) return undefined;
4516
4534
  const source = deps.resolveInputJournal();
4517
4535
  const recipientBindingKey = deps.getRecipientBindingKey()?.trim();
4518
4536
  if (!source || !source.runtimeKey || !source.recoveryKey || !recipientBindingKey)
4519
4537
  return undefined;
4520
- const port = createTelegramInputCustodyWorkerJournalPort(source.journal);
4538
+ let port = ports.get(source.journal);
4539
+ if (!port) {
4540
+ port = createTelegramInputCustodyWorkerJournalPort(source.journal);
4541
+ ports.set(source.journal, port);
4542
+ }
4521
4543
  return { runtimeKey: JSON.stringify({ source: source.runtimeKey, recipientBindingKey }),
4522
4544
  recoveryKey: source.recoveryKey,
4523
4545
  recipientBindingKey, journal: { ...port, appendBatch: source.journal.appendBatch,
@@ -4681,6 +4703,7 @@ export function createTelegramUpdateAdmissionHandle<
4681
4703
  try {
4682
4704
  execution.assertCurrent();
4683
4705
  await deps.defaultHandle(boundUpdate, ctx, execution);
4706
+ execution.assertCurrent();
4684
4707
  } finally {
4685
4708
  immediate = false;
4686
4709
  }
@@ -4720,6 +4743,16 @@ export interface TelegramQueueAdmissionItemLike {
4720
4743
  admissionReceipts?: readonly TelegramQueueAdmissionReceiptLike[];
4721
4744
  }
4722
4745
 
4746
+ // A dispatched prompt can remain in memory until agent_start. Remember only acknowledged
4747
+ // removal of these exact receipt objects for later local discard, never for dispatch/replay.
4748
+ const acknowledgedQueueReceiptCompletions = new WeakMap<
4749
+ TelegramQueueAdmissionReceiptLike, TelegramQueueAdmissionReceiptLike
4750
+ >();
4751
+ const isQueueReceiptCompletionAcknowledged = (receipt: TelegramQueueAdmissionReceiptLike): boolean => {
4752
+ const acknowledged = acknowledgedQueueReceiptCompletions.get(receipt);
4753
+ return acknowledged !== undefined && areTelegramQueueAdmissionReceiptsEqual(acknowledged, receipt);
4754
+ };
4755
+
4723
4756
  export interface TelegramQueueAdmissionSettlementRuntime<TContext> {
4724
4757
  isItemReady: (item: TelegramQueueAdmissionItemLike) => boolean;
4725
4758
  getQueueReceiptOwner: (
@@ -4728,26 +4761,51 @@ export interface TelegramQueueAdmissionSettlementRuntime<TContext> {
4728
4761
  onPromptHandedOff: (
4729
4762
  item: TelegramQueueAdmissionItemLike,
4730
4763
  ctx: TContext,
4731
- ) => void;
4764
+ ) => boolean;
4732
4765
  onControlSettled: (
4733
4766
  item: TelegramQueueAdmissionItemLike,
4734
4767
  ctx: TContext,
4735
- ) => void;
4768
+ ) => boolean;
4736
4769
  onItemsDiscarded: (
4737
4770
  items: readonly TelegramQueueAdmissionItemLike[],
4738
4771
  ctx: TContext,
4739
- ) => void;
4772
+ ) => boolean;
4740
4773
  }
4741
4774
 
4742
4775
  export function createTelegramQueueAdmissionSettlementMuxRuntime<TContext>(
4743
4776
  runtimes: readonly TelegramQueueAdmissionSettlementRuntime<TContext>[],
4744
4777
  ): TelegramQueueAdmissionSettlementRuntime<TContext> {
4745
- const settle = (
4746
- operation: (
4747
- runtime: TelegramQueueAdmissionSettlementRuntime<TContext>,
4748
- ) => void,
4749
- ): void => {
4750
- for (const runtime of runtimes) operation(runtime);
4778
+ // Plan the whole request before mutation; unready is not an acknowledgement of completion.
4779
+ const complete = (
4780
+ items: readonly TelegramQueueAdmissionItemLike[],
4781
+ ctx: TContext,
4782
+ kind: "prompt" | "control" | "discard",
4783
+ ): boolean => {
4784
+ const plan = new Map<TelegramQueueAdmissionSettlementRuntime<TContext>, TelegramQueueAdmissionReceiptLike[]>();
4785
+ for (const receipt of items.flatMap(item => item.admissionReceipts ?? [])) {
4786
+ if (kind === "discard" && isQueueReceiptCompletionAcknowledged(receipt)) continue;
4787
+ const candidates = runtimes.filter(runtime => runtime.isItemReady({ admissionReceipts: [receipt] }));
4788
+ const selected = candidates[0];
4789
+ if (!selected) return false;
4790
+ if (candidates.length > 1) {
4791
+ const owner = selected.getQueueReceiptOwner(receipt);
4792
+ if (!owner || candidates.some(runtime => {
4793
+ const candidate = runtime.getQueueReceiptOwner(receipt);
4794
+ return !candidate || !areTelegramUpdateJournalQueueOwnersEqual(owner, candidate);
4795
+ })) return false;
4796
+ }
4797
+ const receipts = plan.get(selected) ?? [];
4798
+ receipts.push(receipt);
4799
+ plan.set(selected, receipts);
4800
+ }
4801
+ for (const [runtime, receipts] of plan) {
4802
+ const item = { admissionReceipts: receipts };
4803
+ const completed = kind === "discard" ? runtime.onItemsDiscarded([item], ctx)
4804
+ : kind === "prompt" ? runtime.onPromptHandedOff(item, ctx)
4805
+ : runtime.onControlSettled(item, ctx);
4806
+ if (completed !== true) return false;
4807
+ }
4808
+ return true;
4751
4809
  };
4752
4810
  return {
4753
4811
  isItemReady: (item) =>
@@ -4773,12 +4831,9 @@ export function createTelegramQueueAdmissionSettlementMuxRuntime<TContext>(
4773
4831
  }
4774
4832
  return owner ? { ...owner } : undefined;
4775
4833
  },
4776
- onPromptHandedOff: (item, ctx) =>
4777
- settle((runtime) => runtime.onPromptHandedOff(item, ctx)),
4778
- onControlSettled: (item, ctx) =>
4779
- settle((runtime) => runtime.onControlSettled(item, ctx)),
4780
- onItemsDiscarded: (items, ctx) =>
4781
- settle((runtime) => runtime.onItemsDiscarded(items, ctx)),
4834
+ onPromptHandedOff: (item, ctx) => complete([item], ctx, "prompt"),
4835
+ onControlSettled: (item, ctx) => complete([item], ctx, "control"),
4836
+ onItemsDiscarded: (items, ctx) => complete(items, ctx, "discard"),
4782
4837
  };
4783
4838
  }
4784
4839
 
@@ -4789,12 +4844,14 @@ export function createTelegramQueueAdmissionSettlementRuntime<TContext>(
4789
4844
  items: readonly TelegramQueueAdmissionItemLike[],
4790
4845
  ctx: TContext,
4791
4846
  reason: TelegramQueueReceiptCompletionReason,
4792
- ): void => {
4847
+ ): boolean => {
4793
4848
  const receipts: TelegramQueueAdmissionReceiptLike[] = [];
4794
4849
  for (const item of items) {
4795
- if (item.admissionReceipts) receipts.push(...item.admissionReceipts);
4850
+ for (const receipt of item.admissionReceipts ?? []) {
4851
+ if (reason !== "discard" || !isQueueReceiptCompletionAcknowledged(receipt)) receipts.push(receipt);
4852
+ }
4796
4853
  }
4797
- worker.completeQueueReceipts({ receipts, ctx, reason });
4854
+ return receipts.length === 0 || worker.completeQueueReceipts({ receipts, ctx, reason });
4798
4855
  };
4799
4856
  return {
4800
4857
  isItemReady: (item) =>
@@ -5322,10 +5379,6 @@ export function createTelegramQueueHandoffReconciler<TContext>(
5322
5379
  const recipientJournalBindingKey =
5323
5380
  deps.createRecipientJournalBindingKey(recipient);
5324
5381
  if (!recipientJournalBindingKey) continue;
5325
- const recipientItem = structuredClone(item);
5326
- recipientItem.admissionReceipts = [
5327
- { ...receipt, journalBindingKey: recipientJournalBindingKey },
5328
- ];
5329
5382
  const expectedOwner = deps.getReceiptOwner(receipt);
5330
5383
  const lifecycle = deps.getLifecycleForReceipt(receipt);
5331
5384
  if (
@@ -5350,11 +5403,11 @@ export function createTelegramQueueHandoffReconciler<TContext>(
5350
5403
  },
5351
5404
  handoffToken,
5352
5405
  lifecycle,
5353
- stageRemote: async () => {
5354
- const payload = createTelegramQueueHandoff({
5355
- handoffToken,
5356
- item: recipientItem,
5357
- }).payload;
5406
+ stageRemote: async ({ payload: sourcePayload }) => {
5407
+ const payload = {
5408
+ ...sourcePayload,
5409
+ admissionReceipts: [{ ...sourcePayload.admissionReceipts[0]!, journalBindingKey: recipientJournalBindingKey }],
5410
+ };
5358
5411
  if (followerRegistered) {
5359
5412
  return deps.stageThroughFollower({
5360
5413
  recipient,
@@ -5637,11 +5690,25 @@ export function createTelegramUpdateAdmissionLifecycleRuntime<TContext>(
5637
5690
  let activeRuntimeKey: string | undefined;
5638
5691
  let journal: TelegramUpdateAdmissionLifecycleJournalBinding["journal"] | undefined;
5639
5692
  let worker: TelegramUpdateWorkerRuntime<TContext> | undefined;
5693
+ let workerInputCustody: TelegramCustodyExecutionJournal | undefined;
5640
5694
  let settlement: TelegramQueueAdmissionSettlementRuntime<TContext> | undefined;
5641
5695
  let journalBindingKey: string | undefined;
5642
5696
  let operation: Promise<void> = Promise.resolve();
5643
5697
  let foreignQueueOwnerLiveness: TelegramProcessLiveness | undefined;
5644
5698
  let releaseSourceReference: (() => void) | undefined;
5699
+ let sourceReferenceBinding: TelegramUpdateAdmissionLifecycleJournalBinding | undefined;
5700
+
5701
+ const withRetainedSourceReference = <T>(
5702
+ operation: (source: TelegramUpdateAdmissionLifecycleJournalBinding["journal"]) => T,
5703
+ ): T => {
5704
+ if (!journal || !worker) throw new Error("Telegram update admission worker is not active.");
5705
+ // Synchronous retained-source access needs a reference, not renewed execution authority.
5706
+ const release = !releaseSourceReference && sourceReferenceBinding
5707
+ ? deps.acquireSourceReference?.(sourceReferenceBinding) : undefined;
5708
+ try { return operation(journal); } finally { release?.(); }
5709
+ };
5710
+ const readObservedJournal = (): TelegramUpdateWorkerJournalSnapshot =>
5711
+ withRetainedSourceReference(source => source.read());
5645
5712
 
5646
5713
  const stopCurrent = async (forget: boolean): Promise<void> => {
5647
5714
  await worker?.stop();
@@ -5652,8 +5719,10 @@ export function createTelegramUpdateAdmissionLifecycleRuntime<TContext>(
5652
5719
  { phase: "source-reference-release" });
5653
5720
  }
5654
5721
  if (!forget) return;
5722
+ sourceReferenceBinding = undefined;
5655
5723
  journal = undefined;
5656
5724
  worker = undefined;
5725
+ workerInputCustody = undefined;
5657
5726
  settlement = undefined;
5658
5727
  journalBindingKey = undefined;
5659
5728
  activeRuntimeKey = undefined;
@@ -5665,7 +5734,8 @@ export function createTelegramUpdateAdmissionLifecycleRuntime<TContext>(
5665
5734
  await stopCurrent(true);
5666
5735
  return;
5667
5736
  }
5668
- if (replace || !worker || activeRuntimeKey !== binding.runtimeKey) {
5737
+ if (replace || !worker || activeRuntimeKey !== binding.runtimeKey ||
5738
+ workerInputCustody !== binding.journal.inputCustody) {
5669
5739
  const { runtimeKey, recoveryKey } = binding;
5670
5740
  await stopCurrent(true);
5671
5741
  binding = deps.resolveBinding();
@@ -5674,10 +5744,13 @@ export function createTelegramUpdateAdmissionLifecycleRuntime<TContext>(
5674
5744
  throw new Error("Telegram update source binding changed during startup.");
5675
5745
  }
5676
5746
  const release = deps.acquireSourceReference?.(binding);
5747
+ const inputCustody = binding.journal.inputCustody;
5677
5748
  try {
5678
5749
  journal = binding.journal;
5679
5750
  worker = deps.createWorker(binding.journal, binding);
5751
+ workerInputCustody = inputCustody;
5680
5752
  settlement = createTelegramQueueAdmissionSettlementRuntime(worker);
5753
+ sourceReferenceBinding = { ...binding, journal };
5681
5754
  releaseSourceReference = release;
5682
5755
  } catch (error) {
5683
5756
  release?.();
@@ -5843,7 +5916,7 @@ export function createTelegramUpdateAdmissionLifecycleRuntime<TContext>(
5843
5916
  "Telegram update journal queue handoff cancellation is not available.",
5844
5917
  );
5845
5918
  }
5846
- const result = journal.cancelQueuedHandoff(input);
5919
+ const result = withRetainedSourceReference(source => source.cancelQueuedHandoff!(input));
5847
5920
  worker.signal();
5848
5921
  return result;
5849
5922
  },
@@ -5897,21 +5970,15 @@ export function createTelegramUpdateAdmissionLifecycleRuntime<TContext>(
5897
5970
  ownsJournalBinding: (candidate) =>
5898
5971
  journalBindingKey !== undefined && journalBindingKey === candidate,
5899
5972
  getForeignQueueOwnerLiveness: () => foreignQueueOwnerLiveness,
5900
- getJournalEntryCount() {
5901
- if (!journal || !worker) {
5902
- throw new Error("Telegram update admission worker is not active.");
5903
- }
5904
- return journal.read().entries.length;
5905
- },
5973
+ getJournalEntryCount: () => readObservedJournal().entries.length,
5906
5974
  hasPendingQueueMutationForItem(item) {
5907
5975
  return Boolean(
5908
5976
  journal &&
5909
5977
  worker &&
5910
- journal
5911
- .read()
5912
- .entries.some(
5978
+ readObservedJournal().entries.some(
5913
5979
  (entry) =>
5914
5980
  entry.state !== "queued" &&
5981
+ entry.preApprovalExcluded !== true &&
5915
5982
  isTelegramReactionDependencyForQueueItem(
5916
5983
  entry.update,
5917
5984
  item,
@@ -5934,11 +6001,11 @@ export function createTelegramUpdateAdmissionLifecycleRuntime<TContext>(
5934
6001
  settlement?.isItemReady(item) ??
5935
6002
  (item.admissionReceipts?.length ?? 0) === 0,
5936
6003
  onPromptHandedOff: (item, ctx) =>
5937
- settlement?.onPromptHandedOff(item, ctx),
6004
+ settlement?.onPromptHandedOff(item, ctx) ?? false,
5938
6005
  onControlSettled: (item, ctx) =>
5939
- settlement?.onControlSettled(item, ctx),
6006
+ settlement?.onControlSettled(item, ctx) ?? false,
5940
6007
  onItemsDiscarded: (items, ctx) =>
5941
- settlement?.onItemsDiscarded(items, ctx),
6008
+ settlement?.onItemsDiscarded(items, ctx) ?? false,
5942
6009
  };
5943
6010
  }
5944
6011
 
@@ -141,6 +141,12 @@ export type TelegramWorkspaceRetirementFence =
141
141
  phase: "deletion-issued";
142
142
  deletionIssuedAtMs: number;
143
143
  })
144
+ | (TelegramWorkspaceRetirementFenceBase & {
145
+ destructiveKind?: "pressure-retirement";
146
+ phase: "deletion-rejected";
147
+ deletionIssuedAtMs: number;
148
+ rejectionConfirmedAtMs: number;
149
+ })
144
150
  | (TelegramWorkspaceRetirementFenceBase & {
145
151
  phase: "commit-ready";
146
152
  absenceConfirmedAtMs: number;
@@ -318,6 +324,14 @@ export interface TelegramWorkspaceAdmissionLedger {
318
324
  expected: TelegramWorkspaceRetirementFence,
319
325
  ) => TelegramWorkspaceRetirementFence;
320
326
  confirmThreadCleanupAbsence: TelegramWorkspaceAdmissionLedger["confirmRetirementAbsence"];
327
+ /** Caller must have exact transport proof of rejection, never merely target presence. */
328
+ confirmRetirementRejection: (
329
+ expected: TelegramWorkspaceRetirementFence,
330
+ ) => TelegramWorkspaceRetirementFence;
331
+ /** Caller must first durably withdraw the exact rejected intent, retaining its binding. */
332
+ completeRejectedRetirementFence: (
333
+ expected: TelegramWorkspaceRetirementFence,
334
+ ) => boolean;
321
335
  releaseUnissuedRetirementFence: (
322
336
  expected: TelegramWorkspaceRetirementFence,
323
337
  ) => boolean;
@@ -491,6 +505,12 @@ function normalizeFence(
491
505
  deletionIssuedAtMs: value.deletionIssuedAtMs,
492
506
  };
493
507
  }
508
+ if (value.phase === "deletion-rejected" &&
509
+ (value.destructiveKind === undefined || value.destructiveKind === "pressure-retirement") &&
510
+ isSafeTimestamp(value.deletionIssuedAtMs) && isSafeTimestamp(value.rejectionConfirmedAtMs)) {
511
+ return { ...base, destructiveKind: value.destructiveKind, phase: "deletion-rejected",
512
+ deletionIssuedAtMs: value.deletionIssuedAtMs, rejectionConfirmedAtMs: value.rejectionConfirmedAtMs };
513
+ }
494
514
  if (
495
515
  value.phase === "commit-ready" &&
496
516
  isSafeTimestamp(value.absenceConfirmedAtMs) &&
@@ -768,6 +788,10 @@ function areFencesEqual(
768
788
  ) {
769
789
  return left.deletionIssuedAtMs === right.deletionIssuedAtMs;
770
790
  }
791
+ if (left.phase === "deletion-rejected" && right.phase === "deletion-rejected") {
792
+ return left.deletionIssuedAtMs === right.deletionIssuedAtMs &&
793
+ left.rejectionConfirmedAtMs === right.rejectionConfirmedAtMs;
794
+ }
771
795
  return (
772
796
  left.phase === "commit-ready" &&
773
797
  right.phase === "commit-ready" &&
@@ -1519,6 +1543,9 @@ export function createTelegramWorkspaceAdmissionLedger(
1519
1543
  if (!areOwnersEqual(expected.owner, options.owner)) {
1520
1544
  authorityChanged("Telegram Workspace absence confirmation requires fence owner authority.");
1521
1545
  }
1546
+ if (expected.phase === "deletion-rejected") {
1547
+ authorityChanged("Rejected Telegram Workspace deletion cannot confirm absence.");
1548
+ }
1522
1549
  return transact((state) => {
1523
1550
  if (!state.fence || !isTelegramWorkspaceRetirementFence(state.fence)) {
1524
1551
  authorityChanged("Telegram Workspace retirement fence is absent or has another kind.");
@@ -1571,9 +1598,10 @@ export function createTelegramWorkspaceAdmissionLedger(
1571
1598
  });
1572
1599
  }
1573
1600
 
1574
- function completeRetirementFence(
1601
+ function completeDestructiveFence(
1575
1602
  expected: TelegramWorkspaceRetirementFence,
1576
- expectedKind: TelegramWorkspaceDeletionFenceKind = "pressure-retirement",
1603
+ expectedKind: TelegramWorkspaceDeletionFenceKind,
1604
+ requiredPhase: "commit-ready" | "deletion-rejected",
1577
1605
  ): boolean {
1578
1606
  validateExpectedFence(expected, options.profileKey);
1579
1607
  if (resolveTelegramWorkspaceDestructiveFenceKind(expected) !== expectedKind)
@@ -1588,14 +1616,47 @@ export function createTelegramWorkspaceAdmissionLedger(
1588
1616
  if (!areFencesEqual(state.fence, expected)) {
1589
1617
  authorityChanged("Telegram Workspace retirement fence authority changed.");
1590
1618
  }
1591
- if (state.fence.phase !== "commit-ready") {
1592
- authorityChanged("Telegram Workspace retirement commit is not ready.");
1619
+ if (state.fence.phase !== requiredPhase) {
1620
+ authorityChanged(requiredPhase === "commit-ready"
1621
+ ? "Telegram Workspace retirement commit is not ready."
1622
+ : "Telegram Workspace retirement rejection is not confirmed.");
1593
1623
  }
1594
1624
  delete state.fence;
1595
1625
  return { result: true, changed: true };
1596
1626
  });
1597
1627
  }
1598
1628
 
1629
+ function confirmRetirementRejection(expected: TelegramWorkspaceRetirementFence): TelegramWorkspaceRetirementFence {
1630
+ validateExpectedFence(expected, options.profileKey);
1631
+ if (resolveTelegramWorkspaceDestructiveFenceKind(expected) !== "pressure-retirement" ||
1632
+ !areOwnersEqual(expected.owner, options.owner) ||
1633
+ (expected.phase !== "deletion-issued" && expected.phase !== "deletion-rejected")) {
1634
+ authorityChanged("Telegram Workspace rejection requires exact issued retirement authority.");
1635
+ }
1636
+ return transact((state) => {
1637
+ const current = state.fence;
1638
+ if (!current || !isTelegramWorkspaceRetirementFence(current) || !areFenceBasesEqual(current, expected) ||
1639
+ (current.phase !== "deletion-issued" && current.phase !== "deletion-rejected") ||
1640
+ current.deletionIssuedAtMs !== expected.deletionIssuedAtMs ||
1641
+ (expected.phase === "deletion-rejected" && !areFencesEqual(current, expected))) {
1642
+ authorityChanged("Telegram Workspace retirement fence authority changed.");
1643
+ }
1644
+ if (current.phase === "deletion-rejected") return { result: cloneFence(current), changed: false };
1645
+ const rejected: TelegramWorkspaceRetirementFence = { ...current,
1646
+ destructiveKind: "pressure-retirement", phase: "deletion-rejected", rejectionConfirmedAtMs: getNowMs() };
1647
+ state.fence = rejected;
1648
+ return { result: cloneFence(rejected), changed: true };
1649
+ });
1650
+ }
1651
+
1652
+ function completeRetirementFence(expected: TelegramWorkspaceRetirementFence,
1653
+ expectedKind: TelegramWorkspaceDeletionFenceKind = "pressure-retirement") {
1654
+ return completeDestructiveFence(expected, expectedKind, "commit-ready");
1655
+ }
1656
+ function completeRejectedRetirementFence(expected: TelegramWorkspaceRetirementFence) {
1657
+ return completeDestructiveFence(expected, "pressure-retirement", "deletion-rejected");
1658
+ }
1659
+
1599
1660
  function adoptThreadCleanupFence(expected: TelegramWorkspaceRetirementFence,
1600
1661
  replacement: { owner: TelegramWorkspaceAdmissionOwner; leaderEpoch: number | string }) {
1601
1662
  return adoptRetirementFence(expected, replacement, "manual-thread-cleanup");
@@ -1635,6 +1696,8 @@ export function createTelegramWorkspaceAdmissionLedger(
1635
1696
  issueThreadCleanupDeletionPermit,
1636
1697
  confirmRetirementAbsence,
1637
1698
  confirmThreadCleanupAbsence,
1699
+ confirmRetirementRejection,
1700
+ completeRejectedRetirementFence,
1638
1701
  releaseUnissuedRetirementFence,
1639
1702
  releaseUnissuedThreadCleanupFence,
1640
1703
  completeRetirementFence,