@llblab/pi-telegram 0.54.3 → 0.55.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 (80) hide show
  1. package/AGENTS.md +4 -2
  2. package/BACKLOG.md +1 -1
  3. package/CHANGELOG.md +21 -0
  4. package/README.md +5 -3
  5. package/dist/lib/activity.d.ts +5 -1
  6. package/dist/lib/activity.js +31 -2
  7. package/dist/lib/bindings.d.ts +5 -1
  8. package/dist/lib/bindings.js +9 -2
  9. package/dist/lib/bus-api.d.ts +2 -2
  10. package/dist/lib/bus-api.js +7 -2
  11. package/dist/lib/bus-follower.d.ts +49 -3
  12. package/dist/lib/bus-follower.js +313 -146
  13. package/dist/lib/bus-leader.d.ts +3 -0
  14. package/dist/lib/bus-leader.js +99 -18
  15. package/dist/lib/bus.d.ts +5 -3
  16. package/dist/lib/bus.js +13 -10
  17. package/dist/lib/delivery.d.ts +4 -0
  18. package/dist/lib/delivery.js +48 -15
  19. package/dist/lib/extension.js +56 -6
  20. package/dist/lib/lifecycle.d.ts +68 -0
  21. package/dist/lib/lifecycle.js +178 -58
  22. package/dist/lib/machine.d.ts +23 -0
  23. package/dist/lib/machine.js +37 -0
  24. package/dist/lib/media.d.ts +114 -28
  25. package/dist/lib/media.js +292 -119
  26. package/dist/lib/model.d.ts +54 -15
  27. package/dist/lib/model.js +98 -53
  28. package/dist/lib/polling.js +2 -0
  29. package/dist/lib/preview.d.ts +128 -113
  30. package/dist/lib/preview.js +448 -366
  31. package/dist/lib/queue.d.ts +2 -2
  32. package/dist/lib/queue.js +3 -3
  33. package/dist/lib/routing.d.ts +14 -2
  34. package/dist/lib/routing.js +546 -207
  35. package/dist/lib/runtime.d.ts +77 -11
  36. package/dist/lib/runtime.js +174 -92
  37. package/dist/lib/sections.js +0 -1
  38. package/dist/lib/telegram-api.d.ts +11 -9
  39. package/dist/lib/telegram-api.js +53 -4
  40. package/dist/lib/text-groups.d.ts +0 -26
  41. package/dist/lib/text-groups.js +51 -98
  42. package/dist/lib/thread-display.js +14 -6
  43. package/dist/lib/threads.d.ts +20 -5
  44. package/dist/lib/threads.js +111 -32
  45. package/dist/lib/turns.d.ts +2 -0
  46. package/dist/lib/turns.js +6 -1
  47. package/dist/lib/updates.d.ts +11 -4
  48. package/dist/lib/updates.js +54 -19
  49. package/dist/package.json +1 -1
  50. package/docs/activity.md +1 -1
  51. package/docs/architecture.md +16 -15
  52. package/docs/delivery.md +7 -5
  53. package/docs/multi-instance-bus.md +18 -8
  54. package/docs/public-api.md +2 -2
  55. package/docs/ui-style.md +8 -6
  56. package/lib/activity.ts +29 -5
  57. package/lib/bindings.ts +13 -1
  58. package/lib/bus-api.ts +11 -3
  59. package/lib/bus-follower.ts +417 -203
  60. package/lib/bus-leader.ts +88 -14
  61. package/lib/bus.ts +17 -15
  62. package/lib/delivery.ts +47 -19
  63. package/lib/extension.ts +59 -9
  64. package/lib/lifecycle.ts +229 -55
  65. package/lib/machine.ts +60 -0
  66. package/lib/media.ts +438 -172
  67. package/lib/model.ts +169 -74
  68. package/lib/polling.ts +2 -0
  69. package/lib/preview.ts +715 -566
  70. package/lib/queue.ts +5 -5
  71. package/lib/routing.ts +644 -283
  72. package/lib/runtime.ts +260 -104
  73. package/lib/sections.ts +0 -2
  74. package/lib/telegram-api.ts +61 -8
  75. package/lib/text-groups.ts +75 -136
  76. package/lib/thread-display.ts +15 -4
  77. package/lib/threads.ts +130 -38
  78. package/lib/turns.ts +8 -1
  79. package/lib/updates.ts +58 -21
  80. package/package.json +1 -1
package/lib/updates.ts CHANGED
@@ -6,6 +6,7 @@
6
6
 
7
7
  import { randomUUID } from "node:crypto";
8
8
  import { isDeepStrictEqual } from "node:util";
9
+ import { withoutTelegramTurnReplyAnchor } from "./turns.ts";
9
10
 
10
11
  import type {
11
12
  TelegramBusEnvelope,
@@ -552,7 +553,10 @@ interface TelegramUpdateAdmissionBinding {
552
553
  prepareLiveInput?: (
553
554
  sourceUpdateIds: readonly number[],
554
555
  isCurrent: () => boolean,
556
+ onTransferred?: () => void,
555
557
  ) => TelegramLiveDeferredInputPreparation | undefined;
558
+ /** Internal notification after the worker's exact donor-removal ACK; never issues completion itself. */
559
+ acknowledgeTransferred?: () => void;
556
560
  prepareQueueAdmission?: () =>
557
561
  TelegramDeferredQueueAdmissionPreparation | undefined;
558
562
  prepareSourceCompletion?: () =>
@@ -614,6 +618,7 @@ export function bindTelegramUpdateAdmissionSource<
614
618
  | "inspectSourceSnapshot"
615
619
  | "inspectCompletion"
616
620
  | "isSourceUnsettled"
621
+ | "acknowledgeTransferred"
617
622
  | "prepareLiveInput"
618
623
  | "prepareQueueAdmission"
619
624
  | "prepareSourceCompletion"
@@ -761,20 +766,24 @@ export function prepareTelegramLiveDeferredInput(
761
766
  )
762
767
  )
763
768
  return undefined;
764
- const unsettled = () =>
765
- carriers.every(
766
- (value) =>
767
- getTelegramUpdateAdmissionBinding(value)?.isSourceUnsettled?.() ===
768
- true,
769
- );
769
+ const bindings = carriers.map(getTelegramUpdateAdmissionBinding);
770
+ const acknowledgers = bindings.map(binding => binding?.acknowledgeTransferred);
771
+ const bindingsCurrent = () => carriers.every((value, index) =>
772
+ getTelegramUpdateAdmissionBinding(value) === bindings[index] && bindings[index]?.acknowledgeTransferred === acknowledgers[index]);
773
+ const unsettled = () => bindingsCurrent() && bindings.every(binding => binding?.isSourceUnsettled?.() === true);
770
774
  if (!unsettled()) return undefined;
775
+ let transferred = false;
771
776
  const prepared = getTelegramUpdateAdmissionBinding(first)?.prepareLiveInput?.(
772
777
  ids,
773
778
  () =>
774
- isCurrent() &&
779
+ bindingsCurrent() && isCurrent() &&
775
780
  carriers.every(
776
781
  (value) => getTelegramUpdateExecutionFence(value)?.isCurrent() === true,
777
782
  ),
783
+ () => {
784
+ transferred = true;
785
+ for (let index = 0; index < bindings.length; index++) acknowledgers[index]?.call(bindings[index]);
786
+ },
778
787
  );
779
788
  return (
780
789
  prepared && {
@@ -785,7 +794,7 @@ export function prepareTelegramLiveDeferredInput(
785
794
  unsettled() && prepared.beginRelease(() => unsettled() && canRelease()),
786
795
  settleTransferred: (canSettle) =>
787
796
  unsettled()
788
- ? prepared.settleTransferred(() => unsettled() && canSettle())
797
+ ? prepared.settleTransferred(() => (transferred || unsettled()) && canSettle())
789
798
  : "protected",
790
799
  cancel: () => unsettled() && prepared.cancel(),
791
800
  }
@@ -2350,6 +2359,7 @@ export interface TelegramUpdateWorkerRuntimeDeps<TContext> {
2350
2359
  update: TelegramJournaledUpdate,
2351
2360
  ctx: TContext,
2352
2361
  signal: AbortSignal,
2362
+ target?: TelegramTarget & { threadId: number },
2353
2363
  ) => Promise<TelegramUpdateAdmissionOutcome> | TelegramUpdateAdmissionOutcome;
2354
2364
  /** Native source-only binding, installed by the admission-worker assembly rather than ordinary dispatch. */
2355
2365
  admitPreparedLiveInput?: (
@@ -2466,7 +2476,7 @@ export interface TelegramLiveInputPreparation {
2466
2476
  prepareStatusConsumption?(input: TelegramLiveStatusConsumption): boolean;
2467
2477
  /** Native owner availability before recipient append; not admission or future target activation. */
2468
2478
  canPrepareStatusConsumption?(): boolean;
2469
- release(canRelease: () => boolean): boolean;
2479
+ release(canRelease: () => boolean, target?: TelegramTarget & { threadId: number }): boolean;
2470
2480
  /** Ends a failed pre-append attempt only when no selected input was saved. */
2471
2481
  cancelEmpty(): boolean;
2472
2482
  /** One fenced removal of this never-dispatched recipient copy; unknown issuance cannot be retried. */
@@ -2532,6 +2542,7 @@ export interface TelegramUpdateWorkerRuntime<TContext> {
2532
2542
  sourceUpdateIds: readonly number[];
2533
2543
  signal: AbortSignal;
2534
2544
  isCurrent(): boolean;
2545
+ onTransferred?(): void;
2535
2546
  }) => TelegramLiveDeferredInputPreparation | undefined;
2536
2547
  prepareDeferredQueueAdmission?: (input: {
2537
2548
  updateId: number;
@@ -2652,6 +2663,7 @@ interface TelegramLiveInputPreparationState<TContext> {
2652
2663
  saved?: TelegramUpdateWorkerJournalSnapshot["entries"][number][];
2653
2664
  discardIssued?: true;
2654
2665
  released?: true;
2666
+ target?: TelegramTarget & { threadId: number };
2655
2667
  status?: {
2656
2668
  current(): boolean;
2657
2669
  assertCurrent(): void;
@@ -3707,6 +3719,7 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3707
3719
  expectedOwner: TelegramUpdateWorkerOwner<TContext>,
3708
3720
  update: TelegramJournaledUpdate,
3709
3721
  preparedExecute?: TelegramUpdateWorkerRuntimeDeps<TContext>["executeUpdate"],
3722
+ target?: TelegramTarget & { threadId: number },
3710
3723
  ): Promise<
3711
3724
  | TelegramUpdateWorkerExecutionSettlement
3712
3725
  | typeof TELEGRAM_UPDATE_WORKER_EXECUTION_ABORTED
@@ -3735,6 +3748,7 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3735
3748
  update,
3736
3749
  expectedOwner.ctx,
3737
3750
  expectedOwner.controller.signal,
3751
+ target,
3738
3752
  ),
3739
3753
  );
3740
3754
  const settlement: Promise<TelegramUpdateWorkerExecutionSettlement> =
@@ -4739,6 +4753,7 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
4739
4753
  return admitted.outcome;
4740
4754
  }
4741
4755
  : undefined,
4756
+ prepared?.target,
4742
4757
  );
4743
4758
  if (execution === TELEGRAM_UPDATE_WORKER_EXECUTION_ABORTED) {
4744
4759
  return "aborted";
@@ -4821,6 +4836,11 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
4821
4836
  snapshotInvalidated = true;
4822
4837
  break;
4823
4838
  }
4839
+ if (prepared?.target) {
4840
+ prepared.ids = new Set([...prepared.ids].filter(id => id !== entry.updateId));
4841
+ if (!prepared.ids.size && livePreparation === prepared) livePreparation = undefined;
4842
+ updateClaimCounts();
4843
+ }
4824
4844
  const postExecutionAuthority = checkAuthority(
4825
4845
  expectedOwner,
4826
4846
  entry.updateId,
@@ -5270,6 +5290,7 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
5270
5290
  prepareDeferredLiveInput(input) {
5271
5291
  const expectedOwner = owner,
5272
5292
  binding = deps.getJournalBindingKey?.(),
5293
+ onTransferred = input.onTransferred,
5273
5294
  ids = [...input.sourceUpdateIds];
5274
5295
  const sources = ids.map((id) => deferredSources.get(id));
5275
5296
  const ownerCurrent = () =>
@@ -5282,7 +5303,7 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
5282
5303
  deps.hasAuthority(expectedOwner.ctx) &&
5283
5304
  deps.isContextCurrent?.(expectedOwner.ctx) !== false &&
5284
5305
  isQueueOwnerIdentityCurrent(expectedOwner) &&
5285
- input.isCurrent();
5306
+ input.onTransferred === onTransferred && input.isCurrent();
5286
5307
  const sourcesCurrent = () =>
5287
5308
  ownerCurrent() &&
5288
5309
  sources.every(
@@ -5381,13 +5402,11 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
5381
5402
  undefined,
5382
5403
  () => current() && canSettle(),
5383
5404
  );
5384
- if (
5385
- !current() ||
5386
- !canSettle() ||
5387
- result.removedUpdateIds.length !== ids.length ||
5388
- !ids.every((id) => result.removedUpdateIds.includes(id))
5389
- )
5405
+ if (result.removedUpdateIds.length !== ids.length || !ids.every(id => result.removedUpdateIds.includes(id)))
5390
5406
  return "unknown";
5407
+ // A consumed routing clock is expected only after this exact ACK, never after uncertain removal.
5408
+ onTransferred?.call(input);
5409
+ if (!current() || !canSettle()) return "unknown";
5391
5410
  } catch (error) {
5392
5411
  recordRuntimeEvent(error, {
5393
5412
  phase: "live-deferred-transfer-settlement",
@@ -5511,7 +5530,7 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
5511
5530
  }
5512
5531
  };
5513
5532
  const finish = () => {
5514
- if (!held.released || !held.status) livePreparation = undefined;
5533
+ if (!held.released || !held.status && !held.target) livePreparation = undefined;
5515
5534
  updateClaimCounts();
5516
5535
  notifyStateChange();
5517
5536
  blocked = false;
@@ -5659,7 +5678,7 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
5659
5678
  };
5660
5679
  return held.status.current();
5661
5680
  },
5662
- release(canRelease) {
5681
+ release(canRelease, target) {
5663
5682
  if (
5664
5683
  !current() ||
5665
5684
  held.discardIssued ||
@@ -5676,6 +5695,14 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
5676
5695
  !current()
5677
5696
  )
5678
5697
  return false;
5698
+ if (target) {
5699
+ if (!Number.isSafeInteger(target.chatId) || target.chatId === 0 || !Number.isSafeInteger(target.threadId) || target.threadId <= 0 ||
5700
+ entries.some(entry => {
5701
+ const message = (entry.update.message ?? entry.update.edited_message) as { chat?: { id?: number } } | undefined;
5702
+ return message?.chat?.id !== target.chatId;
5703
+ })) return false;
5704
+ if (!held.status) held.target = { ...target };
5705
+ }
5679
5706
  released = true;
5680
5707
  held.released = true;
5681
5708
  finish();
@@ -8846,7 +8873,8 @@ function createTelegramUpdateSourceAdmissionHandle<
8846
8873
  signal,
8847
8874
  })
8848
8875
  : undefined,
8849
- prepareLiveInput: (sourceUpdateIds, isCurrent) =>
8876
+ acknowledgeTransferred: () => { settlementReported = true; },
8877
+ prepareLiveInput: (sourceUpdateIds, isCurrent, onTransferred) =>
8850
8878
  execution.isCurrent() &&
8851
8879
  !immediate &&
8852
8880
  !settlementReported &&
@@ -8856,6 +8884,7 @@ function createTelegramUpdateSourceAdmissionHandle<
8856
8884
  sourceUpdateIds,
8857
8885
  signal,
8858
8886
  isCurrent: () => execution.isCurrent() && isCurrent(),
8887
+ onTransferred,
8859
8888
  })
8860
8889
  : undefined,
8861
8890
  isHistorical: (matchesOriginal) =>
@@ -10599,11 +10628,19 @@ export function createTelegramUpdateAdmissionWorkerRuntime<
10599
10628
  ),
10600
10629
  }
10601
10630
  : {}),
10602
- async executeUpdate(update, ctx, signal) {
10631
+ async executeUpdate(update, ctx, signal, target) {
10603
10632
  const typedUpdate = update as TUpdate;
10633
+ const prepared = deps.prepareUpdateForExecution?.(typedUpdate) ?? typedUpdate;
10634
+ // Journal/source evidence remains the original; only the warm released handler projection changes destination.
10635
+ const routed = target ? { ...prepared,
10636
+ ...(prepared.message ? { message: prepared.message.message_thread_id === target.threadId ? { ...prepared.message }
10637
+ : withoutTelegramTurnReplyAnchor({ ...prepared.message, message_thread_id: target.threadId, reply_to_message: undefined }) } : {}),
10638
+ ...(prepared.edited_message ? { edited_message: prepared.edited_message.message_thread_id === target.threadId ? { ...prepared.edited_message }
10639
+ : withoutTelegramTurnReplyAnchor({ ...prepared.edited_message, message_thread_id: target.threadId, reply_to_message: undefined }) } : {}),
10640
+ } as TUpdate : prepared;
10604
10641
  try {
10605
10642
  return await executeUpdate(
10606
- deps.prepareUpdateForExecution?.(typedUpdate) ?? typedUpdate,
10643
+ routed,
10607
10644
  ctx,
10608
10645
  signal,
10609
10646
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.54.3",
3
+ "version": "0.55.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"