@llblab/pi-telegram 0.54.4 → 0.55.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 (80) hide show
  1. package/AGENTS.md +4 -2
  2. package/BACKLOG.md +1 -1
  3. package/CHANGELOG.md +16 -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 +3 -1
  8. package/dist/lib/bindings.js +8 -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 +50 -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 +7 -3
  16. package/dist/lib/bus.js +24 -16
  17. package/dist/lib/delivery.d.ts +4 -0
  18. package/dist/lib/delivery.js +48 -15
  19. package/dist/lib/extension.js +48 -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 +7 -2
  34. package/dist/lib/routing.js +585 -229
  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 +18 -5
  44. package/dist/lib/threads.js +107 -30
  45. package/dist/lib/turns.d.ts +6 -0
  46. package/dist/lib/turns.js +12 -1
  47. package/dist/lib/updates.d.ts +13 -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 +14 -11
  52. package/docs/delivery.md +7 -5
  53. package/docs/multi-instance-bus.md +20 -8
  54. package/docs/public-api.md +2 -2
  55. package/docs/ui-style.md +2 -2
  56. package/lib/activity.ts +29 -5
  57. package/lib/bindings.ts +9 -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 +30 -18
  62. package/lib/delivery.ts +47 -19
  63. package/lib/extension.ts +51 -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 +614 -257
  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 +120 -36
  78. package/lib/turns.ts +15 -1
  79. package/lib/updates.ts +60 -21
  80. package/package.json +1 -1
package/lib/activity.ts CHANGED
@@ -243,6 +243,7 @@ function createTelegramActivityContext(
243
243
  }
244
244
 
245
245
  interface TelegramActivityHandlerQueue {
246
+ notifyDrained?: () => void;
246
247
  registration: RegisteredTelegramActivityHandler;
247
248
  events: TelegramActivityEvent[];
248
249
  running: boolean;
@@ -287,6 +288,19 @@ function coalesceActivityEvents(
287
288
  return next;
288
289
  }
289
290
 
291
+ /** Optional observation must not change queue/transport settlement. */
292
+ function captureActivityDrainObserver(capture?: () => (() => void) | undefined): () => void {
293
+ try {
294
+ const notify = capture?.();
295
+ return () => {
296
+ try {
297
+ const emitted: unknown = notify?.();
298
+ if (emitted) void Promise.resolve(emitted).catch(() => undefined);
299
+ } catch { /* Observer failure is inert. */ }
300
+ };
301
+ } catch { return () => {}; }
302
+ }
303
+
290
304
  /** @internal */
291
305
  export interface TelegramActivityDispatcher {
292
306
  hasPending?: () => boolean;
@@ -297,6 +311,7 @@ export interface TelegramActivityDispatcher {
297
311
  /** @internal */
298
312
  export function createTelegramActivityDispatcher(
299
313
  deps: {
314
+ captureDrain?: () => (() => void) | undefined;
300
315
  recordFailure?: (
301
316
  handlerId: string,
302
317
  event: TelegramActivityEvent,
@@ -306,6 +321,7 @@ export function createTelegramActivityDispatcher(
306
321
  ): TelegramActivityDispatcher {
307
322
  const queues = new Map<string, TelegramActivityHandlerQueue>();
308
323
  let stopped = false;
324
+ const hasPending = () => Array.from(queues.values()).some(queue => queue.active && (queue.running || queue.events.length > 0));
309
325
  const drain = async (queue: TelegramActivityHandlerQueue): Promise<void> => {
310
326
  if (queue.running || !queue.active) return;
311
327
  queue.running = true;
@@ -341,13 +357,11 @@ export function createTelegramActivityDispatcher(
341
357
  }
342
358
  } finally {
343
359
  queue.running = false;
360
+ if (!stopped && !hasPending()) queue.notifyDrained?.();
344
361
  }
345
362
  };
346
363
  return {
347
- hasPending: () =>
348
- Array.from(queues.values()).some(
349
- (queue) => queue.active && (queue.running || queue.events.length > 0),
350
- ),
364
+ hasPending,
351
365
  dispatch(event) {
352
366
  if (stopped) return;
353
367
  for (const registration of getTelegramActivityHandlers()) {
@@ -361,6 +375,8 @@ export function createTelegramActivityDispatcher(
361
375
  };
362
376
  queues.set(registration.id, queue);
363
377
  }
378
+ if (!queue.running && queue.events.length === 0)
379
+ queue.notifyDrained = captureActivityDrainObserver(deps.captureDrain);
364
380
  const previous = queue.events.at(-1);
365
381
  if (previous && canCoalesceActivityEvents(previous, event)) {
366
382
  queue.events[queue.events.length - 1] = coalesceActivityEvents(
@@ -389,6 +405,7 @@ export function createTelegramActivityDispatcher(
389
405
  /** @internal */
390
406
  export function createTelegramActivityBridgeRuntime(deps: {
391
407
  generation: string;
408
+ captureDrain?: () => (() => void) | undefined;
392
409
  observeEvent?: (event: TelegramActivityEvent) => void;
393
410
  recordFailure?: (
394
411
  handlerId: string,
@@ -407,6 +424,7 @@ export function createTelegramActivityBridgeRuntime(deps: {
407
424
  runtime = createTelegramActivityRuntime({
408
425
  generation: `${deps.generation}:${++generationSequence}`,
409
426
  dispatcher: createTelegramActivityDispatcher({
427
+ captureDrain: deps.captureDrain,
410
428
  recordFailure: deps.recordFailure,
411
429
  }),
412
430
  observeEvent: deps.observeEvent,
@@ -766,7 +784,9 @@ export interface TelegramActivityPublicationRuntime {
766
784
  reset: () => void;
767
785
  }
768
786
 
769
- export function createTelegramActivityPublicationRuntime(): TelegramActivityPublicationRuntime {
787
+ export function createTelegramActivityPublicationRuntime(deps: {
788
+ captureDrain?: () => (() => void) | undefined;
789
+ } = {}): TelegramActivityPublicationRuntime {
770
790
  let generation = 0;
771
791
  let tail = Promise.resolve();
772
792
  let outstanding = 0,
@@ -774,6 +794,7 @@ export function createTelegramActivityPublicationRuntime(): TelegramActivityPubl
774
794
  const pending = new Set<() => void>();
775
795
  const reserve = (): TelegramActivityPublicationReservation => {
776
796
  const admittedGeneration = generation;
797
+ const notifyDrained = captureActivityDrainObserver(deps.captureDrain);
777
798
  let state: "pending" | "published" | "cancelled" = "pending";
778
799
  let resolve!: (task: (() => Promise<void>) | undefined) => void;
779
800
  const ready = new Promise<(() => Promise<void>) | undefined>((accept) => {
@@ -794,6 +815,7 @@ export function createTelegramActivityPublicationRuntime(): TelegramActivityPubl
794
815
  })
795
816
  .finally(() => {
796
817
  outstanding -= 1;
818
+ if (admittedGeneration === generation && outstanding === 0 && !unconfirmed) notifyDrained();
797
819
  });
798
820
  tail = result.catch(() => {});
799
821
  return {
@@ -818,6 +840,7 @@ export function createTelegramActivityPublicationRuntime(): TelegramActivityPubl
818
840
  hasUnconfirmed: () => unconfirmed,
819
841
  beginWork() {
820
842
  const admittedGeneration = generation;
843
+ const notifyDrained = captureActivityDrainObserver(deps.captureDrain);
821
844
  let settled = false;
822
845
  outstanding += 1;
823
846
  return {
@@ -827,6 +850,7 @@ export function createTelegramActivityPublicationRuntime(): TelegramActivityPubl
827
850
  outstanding -= 1;
828
851
  if (outcome === "unconfirmed" && admittedGeneration === generation)
829
852
  unconfirmed = true;
853
+ if (admittedGeneration === generation && outstanding === 0 && !unconfirmed) notifyDrained();
830
854
  },
831
855
  };
832
856
  },
package/lib/bindings.ts CHANGED
@@ -654,6 +654,7 @@ export interface TelegramActivityBindingRuntime {
654
654
  /** Compose public activity fanout, verbosity, and assistant output ordering. */
655
655
  export function createTelegramActivityBindingRuntime<TTransportStamp>(deps: {
656
656
  generation: string;
657
+ captureDrain?: () => (() => void) | undefined;
657
658
  assistantOutput: Omit<
658
659
  Parameters<
659
660
  typeof createTelegramAssistantOutputBindingRuntime<TTransportStamp>
@@ -669,7 +670,7 @@ export function createTelegramActivityBindingRuntime<TTransportStamp>(deps: {
669
670
  "captureAuthority" | "isAuthorityActive" | "recordFailure" | "enqueue"
670
671
  >;
671
672
  }): TelegramActivityBindingRuntime {
672
- const publication = Activity.createTelegramActivityPublicationRuntime();
673
+ const publication = Activity.createTelegramActivityPublicationRuntime({ captureDrain: deps.captureDrain });
673
674
  const assistantOutputBinding = createTelegramAssistantOutputBindingRuntime({
674
675
  ...deps.assistantOutput,
675
676
  enqueue: publication.enqueue,
@@ -690,6 +691,7 @@ export function createTelegramActivityBindingRuntime<TTransportStamp>(deps: {
690
691
  });
691
692
  const activityRuntime = Activity.createTelegramActivityBridgeRuntime({
692
693
  generation: deps.generation,
694
+ captureDrain: deps.captureDrain,
693
695
  observeEvent(event) {
694
696
  assistantOutputBinding.observeEvent(event);
695
697
  activityVerbosityRuntime.accept(event);
@@ -1196,6 +1198,7 @@ interface TelegramLifecycleBindingDeps {
1196
1198
  }) => void;
1197
1199
  canSendAgentActivity: (ctx: Pi.ExtensionContext) => boolean;
1198
1200
  isSessionContextActive: (ctx: Pi.ExtensionContext) => boolean;
1201
+ getSessionGeneration?: () => number;
1199
1202
  isTurnTransportActive?: (turn: Queue.PendingTelegramTurn) => boolean;
1200
1203
  updateStatus: TelegramBridgeStatusUpdater;
1201
1204
  recordRuntimeEvent: TelegramRuntimeEventRecorder;
@@ -1246,6 +1249,7 @@ export function registerTelegramLifecycleRuntimeHooks({
1246
1249
  recordMessageOwnership,
1247
1250
  canSendAgentActivity,
1248
1251
  isSessionContextActive = () => true,
1252
+ getSessionGeneration,
1249
1253
  isTurnTransportActive,
1250
1254
  updateStatus,
1251
1255
  recordRuntimeEvent,
@@ -1550,6 +1554,8 @@ export function registerTelegramLifecycleRuntimeHooks({
1550
1554
  const messageActivityTypingHooks =
1551
1555
  Lifecycle.createTelegramMessageActivityTypingHooks({
1552
1556
  hasActiveTurn: activeTurnRuntime.has,
1557
+ isContextActive: isSessionContextActive,
1558
+ getSessionGeneration,
1553
1559
  startTypingLoop: startActiveTurnTypingLoop,
1554
1560
  onMessageStart: previewRuntime.onMessageStart,
1555
1561
  onMessageUpdate: previewRuntime.onMessageUpdate,
@@ -1638,9 +1644,11 @@ export function registerTelegramLifecycleRuntimeHooks({
1638
1644
  },
1639
1645
  async onAgentStart(event, ctx) {
1640
1646
  if (!isSessionContextActive(ctx)) return;
1647
+ const generation = getSessionGeneration?.();
1641
1648
  agentWorkActive = true;
1642
1649
  cancelPendingFinalPublication();
1643
1650
  await agentStartWithDedupReset(event, ctx);
1651
+ if (!isSessionContextActive(ctx) || (getSessionGeneration && !Object.is(getSessionGeneration(), generation))) return;
1644
1652
  const turn = activeTurnRuntime.get();
1645
1653
  activityRuntime.onAgentStart(turn?.target, turn?.replyToMessageId);
1646
1654
  startAgentActivityTypingLoop(ctx);
package/lib/bus-api.ts CHANGED
@@ -23,10 +23,12 @@ import type {
23
23
  TelegramSendRichMessageBody,
24
24
  TelegramSendRichMessageDraftBody,
25
25
  TelegramSentMessage,
26
+ TelegramTypingActionOptions,
26
27
  TelegramUpdate,
27
28
  } from "./telegram-api.ts";
28
29
  import {
29
30
  buildTelegramAnswerGuestQueryBody,
31
+ assertTelegramApiCallAuthority,
30
32
  isTelegramApiCommitUnknownError,
31
33
  isTelegramMessageNotModifiedError,
32
34
  TelegramApiAuthorityError,
@@ -36,6 +38,7 @@ import {
36
38
  export type TelegramBusApiCall = (
37
39
  method: string,
38
40
  args: unknown[],
41
+ localOptions?: Pick<TelegramApiCallOptions, "assertAuthority">,
39
42
  ) => Promise<unknown>;
40
43
 
41
44
  export interface TelegramBusAwareApiRuntimeDeps {
@@ -235,8 +238,9 @@ function sendTelegramFollowerChatAction(
235
238
  >,
236
239
  chatId: number,
237
240
  action: string,
238
- options?: { message_thread_id?: number },
241
+ options?: TelegramTypingActionOptions,
239
242
  ): Promise<boolean> {
243
+ assertTelegramApiCallAuthority(options?.assertAuthority, false);
240
244
  const body = withDefaultThreadTarget(
241
245
  {
242
246
  chat_id: chatId,
@@ -247,7 +251,11 @@ function sendTelegramFollowerChatAction(
247
251
  },
248
252
  deps.getDefaultTarget?.(),
249
253
  );
250
- return deps.callFollowerApi("call", ["sendChatAction", body]).then(asBoolean);
254
+ // Only plain single-attempt policy crosses IPC; the captured assertion is a local issuance fence.
255
+ const args = ["sendChatAction", body, ...(action === "typing"
256
+ ? [{ maxAttempts: 1, retrySafety: "non-idempotent" }] : [])];
257
+ return deps.callFollowerApi("call", args, options?.assertAuthority
258
+ ? { assertAuthority: options.assertAuthority } : undefined).then(asBoolean);
251
259
  }
252
260
 
253
261
  export function createTelegramBusAwareApiRuntime(
@@ -345,7 +353,7 @@ export function createTelegramBusAwareApiRuntime(
345
353
  },
346
354
  sendTypingAction(
347
355
  chatId: number,
348
- options?: { message_thread_id?: number },
356
+ options?: TelegramTypingActionOptions,
349
357
  ): Promise<unknown> {
350
358
  return deps.ownsDirect()
351
359
  ? deps.directRuntime.sendTypingAction(chatId, options)