@llblab/pi-kit 0.21.2 → 0.22.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 (41) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +2 -2
  3. package/node_modules/@llblab/pi-clean-room/CHANGELOG.md +5 -0
  4. package/node_modules/@llblab/pi-clean-room/README.md +4 -3
  5. package/node_modules/@llblab/pi-clean-room/index.ts +45 -1
  6. package/node_modules/@llblab/pi-clean-room/package.json +1 -1
  7. package/node_modules/@llblab/pi-telegram/AGENTS.md +2 -1
  8. package/node_modules/@llblab/pi-telegram/BACKLOG.md +3 -8
  9. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +6 -0
  10. package/node_modules/@llblab/pi-telegram/README.md +1 -1
  11. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +1 -4
  12. package/node_modules/@llblab/pi-telegram/dist/lib/bus-api.d.ts +0 -1
  13. package/node_modules/@llblab/pi-telegram/dist/lib/bus-api.js +0 -3
  14. package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +1 -2
  15. package/node_modules/@llblab/pi-telegram/dist/lib/menu-model.d.ts +3 -3
  16. package/node_modules/@llblab/pi-telegram/dist/lib/menu-model.js +17 -3
  17. package/node_modules/@llblab/pi-telegram/dist/lib/menu.d.ts +5 -7
  18. package/node_modules/@llblab/pi-telegram/dist/lib/menu.js +4 -5
  19. package/node_modules/@llblab/pi-telegram/dist/lib/model.d.ts +12 -12
  20. package/node_modules/@llblab/pi-telegram/dist/lib/model.js +49 -35
  21. package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +0 -2
  22. package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +1 -8
  23. package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +0 -1
  24. package/node_modules/@llblab/pi-telegram/dist/lib/runtime.d.ts +3 -6
  25. package/node_modules/@llblab/pi-telegram/dist/lib/runtime.js +4 -55
  26. package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +36 -10
  27. package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
  28. package/node_modules/@llblab/pi-telegram/docs/architecture.md +8 -8
  29. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +2 -2
  30. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +7 -4
  31. package/node_modules/@llblab/pi-telegram/lib/bus-api.ts +0 -7
  32. package/node_modules/@llblab/pi-telegram/lib/extension.ts +1 -3
  33. package/node_modules/@llblab/pi-telegram/lib/menu-model.ts +25 -4
  34. package/node_modules/@llblab/pi-telegram/lib/menu.ts +14 -9
  35. package/node_modules/@llblab/pi-telegram/lib/model.ts +77 -44
  36. package/node_modules/@llblab/pi-telegram/lib/queue.ts +1 -8
  37. package/node_modules/@llblab/pi-telegram/lib/routing.ts +0 -1
  38. package/node_modules/@llblab/pi-telegram/lib/runtime.ts +7 -58
  39. package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +43 -17
  40. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  41. package/package.json +3 -3
@@ -30,6 +30,7 @@ import {
30
30
  import {
31
31
  type MenuModel,
32
32
  type ScopedTelegramModel,
33
+ type TelegramModelSwitchContinuationSource,
33
34
  type ThinkingLevel,
34
35
  } from "./model.ts";
35
36
  import type { TelegramInputRichMessage } from "./telegram-api.ts";
@@ -194,7 +195,6 @@ export interface TelegramMenuCallbackRuntimeDeps<
194
195
  text?: string,
195
196
  ) => Promise<void>;
196
197
  isIdle: (ctx: TContext) => boolean;
197
- hasActiveTelegramTurn: () => boolean;
198
198
  hasAbortHandler: () => boolean;
199
199
  hasActiveToolExecutions: () => boolean;
200
200
  persistScopedModelPatterns?: (
@@ -206,10 +206,12 @@ export interface TelegramMenuCallbackRuntimeDeps<
206
206
  stagePendingModelSwitch: (
207
207
  selection: ScopedTelegramModel<TModel>,
208
208
  ctx: TContext,
209
+ continuationTurn: TelegramModelSwitchContinuationSource,
209
210
  ) => void;
210
211
  restartInterruptedTelegramTurn: (
211
212
  selection: ScopedTelegramModel<TModel>,
212
213
  ctx: TContext,
214
+ continuationTurn: TelegramModelSwitchContinuationSource,
213
215
  ) => Promise<boolean> | boolean;
214
216
  sectionRegistry?: TelegramSectionRegistry;
215
217
  editInteractiveMessage?: (
@@ -452,7 +454,6 @@ export interface TelegramMenuCallbackRuntimeAdapterDeps<
452
454
  text?: string,
453
455
  ) => Promise<void>;
454
456
  isIdle: (ctx: TContext) => boolean;
455
- hasActiveTelegramTurn: () => boolean;
456
457
  hasAbortHandler: () => boolean;
457
458
  getActiveToolExecutions: () => number;
458
459
  persistScopedModelPatterns?: (
@@ -464,10 +465,12 @@ export interface TelegramMenuCallbackRuntimeAdapterDeps<
464
465
  stagePendingModelSwitch: (
465
466
  selection: ScopedTelegramModel<TModel>,
466
467
  ctx: TContext,
468
+ continuationTurn: TelegramModelSwitchContinuationSource,
467
469
  ) => void;
468
470
  restartInterruptedTelegramTurn: (
469
471
  selection: ScopedTelegramModel<TModel>,
470
472
  ctx: TContext,
473
+ continuationTurn: TelegramModelSwitchContinuationSource,
471
474
  ) => Promise<boolean> | boolean;
472
475
  sectionRegistry?: TelegramSectionRegistry;
473
476
  editInteractiveMessage?: (
@@ -527,7 +530,6 @@ export function createTelegramMenuCallbackHandlerForContext<
527
530
  updateSettingsMenuMessage: deps.updateSettingsMenuMessage,
528
531
  answerCallbackQuery: deps.answerCallbackQuery,
529
532
  isIdle: deps.isIdle,
530
- hasActiveTelegramTurn: deps.hasActiveTelegramTurn,
531
533
  hasAbortHandler: deps.hasAbortHandler,
532
534
  hasActiveToolExecutions: () => deps.getActiveToolExecutions() > 0,
533
535
  persistScopedModelPatterns: deps.persistScopedModelPatterns,
@@ -715,8 +717,7 @@ export async function handleTelegramMenuCallbackRuntime<
715
717
  activeModel: deps.getActiveModel(ctx),
716
718
  currentThinkingLevel: deps.getThinkingLevel(),
717
719
  isIdle: deps.isIdle(ctx),
718
- canRestartBusyRun:
719
- deps.hasActiveTelegramTurn() && deps.hasAbortHandler(),
720
+ canRestartBusyRun: deps.hasAbortHandler(),
720
721
  hasActiveToolExecutions: deps.hasActiveToolExecutions(),
721
722
  },
722
723
  {
@@ -733,11 +734,15 @@ export async function handleTelegramMenuCallbackRuntime<
733
734
  deps.setThinkingLevel(level);
734
735
  deps.updateStatus(ctx);
735
736
  },
736
- stagePendingModelSwitch: (selection) => {
737
- deps.stagePendingModelSwitch(selection, ctx);
737
+ stagePendingModelSwitch: (selection, continuationTurn) => {
738
+ deps.stagePendingModelSwitch(selection, ctx, continuationTurn);
738
739
  },
739
- restartInterruptedTelegramTurn: (selection) =>
740
- deps.restartInterruptedTelegramTurn(selection, ctx),
740
+ restartInterruptedTelegramTurn: (selection, continuationTurn) =>
741
+ deps.restartInterruptedTelegramTurn(
742
+ selection,
743
+ ctx,
744
+ continuationTurn,
745
+ ),
741
746
  },
742
747
  );
743
748
  } catch (error) {
@@ -374,7 +374,6 @@ export interface PendingModelSwitchStore<TSelection> {
374
374
 
375
375
  export interface TelegramInFlightModelSwitchState {
376
376
  isIdle: boolean;
377
- hasActiveTelegramTurn: boolean;
378
377
  hasAbortHandler: boolean;
379
378
  }
380
379
 
@@ -394,21 +393,21 @@ export function createPendingModelSwitchStore<
394
393
  };
395
394
  }
396
395
 
397
- export function canRestartTelegramTurnForModelSwitch(
396
+ export function canRestartAgentRunForTelegramModelSwitch(
398
397
  state: TelegramInFlightModelSwitchState,
399
398
  ): boolean {
400
- return !state.isIdle && state.hasActiveTelegramTurn && state.hasAbortHandler;
399
+ return !state.isIdle && state.hasAbortHandler;
401
400
  }
402
401
 
403
402
  export function shouldTriggerPendingTelegramModelSwitchAbort(state: {
404
403
  hasPendingModelSwitch: boolean;
405
- hasActiveTelegramTurn: boolean;
404
+ hasContinuationTurn: boolean;
406
405
  hasAbortHandler: boolean;
407
406
  activeToolExecutions: number;
408
407
  }): boolean {
409
408
  return (
410
409
  state.hasPendingModelSwitch &&
411
- state.hasActiveTelegramTurn &&
410
+ state.hasContinuationTurn &&
412
411
  state.hasAbortHandler &&
413
412
  state.activeToolExecutions === 0
414
413
  );
@@ -458,13 +457,18 @@ export function buildTelegramModelSwitchContinuationText<
458
457
  const thinkingSuffix = thinkingLevel
459
458
  ? ` Keep the selected thinking level (${thinkingLevel}) if it still applies.`
460
459
  : "";
461
- return `${telegramPrefix} Continue the interrupted previous Telegram request using the newly selected model (${modelLabel}). Resume from the last unfinished step instead of restarting from scratch unless necessary.${thinkingSuffix}`;
460
+ return `${telegramPrefix} Continue the interrupted previous request using the newly selected model (${modelLabel}). Resume from the last unfinished step instead of restarting from scratch unless necessary.${thinkingSuffix}`;
462
461
  }
463
462
 
463
+ export type TelegramModelSwitchContinuationSource = Pick<
464
+ PendingTelegramTurn,
465
+ "chatId" | "replyToMessageId" | "target"
466
+ >;
467
+
464
468
  export function buildTelegramModelSwitchContinuationTurn<
465
469
  TModel extends MenuModel,
466
470
  >(options: {
467
- turn: Pick<PendingTelegramTurn, "chatId" | "replyToMessageId" | "target">;
471
+ turn: TelegramModelSwitchContinuationSource;
468
472
  selection: ScopedTelegramModel<TModel>;
469
473
  telegramPrefix?: string;
470
474
  queueOrder: number;
@@ -494,7 +498,7 @@ export function buildTelegramModelSwitchContinuationTurn<
494
498
  ),
495
499
  },
496
500
  ],
497
- historyText: `Continue interrupted Telegram request on ${modelLabel}`,
501
+ historyText: `Continue interrupted request on ${modelLabel}`,
498
502
  statusSummary: `↻ ${statusLabel || "continue"}`,
499
503
  };
500
504
  }
@@ -506,7 +510,7 @@ export function createTelegramModelSwitchContinuationTurnBuilder<
506
510
  allocateItemOrder: () => number;
507
511
  allocateControlOrder: () => number;
508
512
  }): (options: {
509
- turn: Pick<PendingTelegramTurn, "chatId" | "replyToMessageId" | "target">;
513
+ turn: TelegramModelSwitchContinuationSource;
510
514
  selection: ScopedTelegramModel<TModel>;
511
515
  }) => PendingTelegramTurn {
512
516
  return (options) =>
@@ -523,11 +527,15 @@ export function createTelegramModelSwitchContinuationQueue<
523
527
  TSelection extends ScopedTelegramModel,
524
528
  >(deps: {
525
529
  createContinuationTurn: (options: {
526
- turn: Pick<PendingTelegramTurn, "chatId" | "replyToMessageId" | "target">;
530
+ turn: TelegramModelSwitchContinuationSource;
527
531
  selection: TSelection;
528
532
  }) => PendingTelegramTurn;
529
533
  appendQueuedItem: (item: PendingTelegramTurn, ctx: TContext) => void;
530
- }): (turn: PendingTelegramTurn, selection: TSelection, ctx: TContext) => void {
534
+ }): (
535
+ turn: TelegramModelSwitchContinuationSource,
536
+ selection: TSelection,
537
+ ctx: TContext,
538
+ ) => void {
531
539
  return (turn, selection, ctx) => {
532
540
  deps.appendQueuedItem(
533
541
  deps.createContinuationTurn({ turn, selection }),
@@ -544,7 +552,11 @@ export function createTelegramModelSwitchContinuationQueueRuntime<
544
552
  allocateItemOrder: () => number;
545
553
  allocateControlOrder: () => number;
546
554
  appendQueuedItem: (item: PendingTelegramTurn, ctx: TContext) => void;
547
- }): (turn: PendingTelegramTurn, selection: TSelection, ctx: TContext) => void {
555
+ }): (
556
+ turn: TelegramModelSwitchContinuationSource,
557
+ selection: TSelection,
558
+ ctx: TContext,
559
+ ) => void {
548
560
  return createTelegramModelSwitchContinuationQueue<TContext, TSelection>({
549
561
  createContinuationTurn: createTelegramModelSwitchContinuationTurnBuilder({
550
562
  telegramPrefix: deps.telegramPrefix,
@@ -564,7 +576,7 @@ export interface TelegramModelSwitchControllerDeps<TContext, TSelection> {
564
576
  hasAbortHandler: () => boolean;
565
577
  getActiveToolExecutions: () => number;
566
578
  queueContinuation: (
567
- turn: PendingTelegramTurn,
579
+ turn: TelegramModelSwitchContinuationSource,
568
580
  selection: TSelection,
569
581
  ctx: TContext,
570
582
  ) => void;
@@ -573,15 +585,23 @@ export interface TelegramModelSwitchControllerDeps<TContext, TSelection> {
573
585
 
574
586
  export interface TelegramModelSwitchController<TContext, TSelection> {
575
587
  canOfferInFlightSwitch: (ctx: TContext) => boolean;
576
- stagePendingSwitch: (selection: TSelection, ctx: TContext) => void;
588
+ stagePendingSwitch: (
589
+ selection: TSelection,
590
+ ctx: TContext,
591
+ continuationTurn?: TelegramModelSwitchContinuationSource,
592
+ ) => void;
577
593
  clearPendingSwitch: () => void;
578
594
  queueContinuation: (
579
- turn: PendingTelegramTurn,
595
+ turn: TelegramModelSwitchContinuationSource,
580
596
  selection: TSelection,
581
597
  ctx: TContext,
582
598
  ) => void;
583
599
  triggerPendingAbort: (ctx: TContext) => boolean;
584
- restartInterruptedTurn: (selection: TSelection, ctx: TContext) => boolean;
600
+ restartInterruptedTurn: (
601
+ selection: TSelection,
602
+ ctx: TContext,
603
+ continuationTurn?: TelegramModelSwitchContinuationSource,
604
+ ) => boolean;
585
605
  }
586
606
 
587
607
  export interface TelegramModelSwitchControllerRuntimeDeps<
@@ -624,49 +644,62 @@ export function createTelegramModelSwitchControllerRuntime<
624
644
  export function createTelegramModelSwitchController<TContext, TSelection>(
625
645
  deps: TelegramModelSwitchControllerDeps<TContext, TSelection>,
626
646
  ): TelegramModelSwitchController<TContext, TSelection> {
647
+ let pendingContinuationTurn:
648
+ | TelegramModelSwitchContinuationSource
649
+ | undefined;
650
+ const triggerPendingAbort = (ctx: TContext): boolean => {
651
+ const turn = pendingContinuationTurn ?? deps.getActiveTurn();
652
+ if (
653
+ !shouldTriggerPendingTelegramModelSwitchAbort({
654
+ hasPendingModelSwitch: !!deps.getPendingModelSwitch(),
655
+ hasContinuationTurn: !!turn,
656
+ hasAbortHandler: deps.hasAbortHandler(),
657
+ activeToolExecutions: deps.getActiveToolExecutions(),
658
+ })
659
+ ) {
660
+ return false;
661
+ }
662
+ const selection = deps.getPendingModelSwitch();
663
+ const abort = deps.getAbortHandler();
664
+ if (!selection || !turn || !abort) return false;
665
+ pendingContinuationTurn = undefined;
666
+ deps.setPendingModelSwitch(undefined);
667
+ deps.queueContinuation(turn, selection, ctx);
668
+ abort();
669
+ return true;
670
+ };
627
671
  return {
628
672
  canOfferInFlightSwitch: (ctx) =>
629
- canRestartTelegramTurnForModelSwitch({
673
+ canRestartAgentRunForTelegramModelSwitch({
630
674
  isIdle: deps.isIdle(ctx),
631
- hasActiveTelegramTurn: !!deps.getActiveTurn(),
632
675
  hasAbortHandler: deps.hasAbortHandler(),
633
676
  }),
634
- stagePendingSwitch: (selection, ctx) => {
677
+ stagePendingSwitch: (selection, ctx, continuationTurn) => {
678
+ pendingContinuationTurn = deps.getActiveTurn() ?? continuationTurn;
635
679
  deps.setPendingModelSwitch(selection);
636
- deps.updateStatus(ctx);
680
+ try {
681
+ deps.updateStatus(ctx);
682
+ } finally {
683
+ triggerPendingAbort(ctx);
684
+ }
637
685
  },
638
686
  clearPendingSwitch: () => {
687
+ pendingContinuationTurn = undefined;
639
688
  deps.setPendingModelSwitch(undefined);
640
689
  },
641
690
  queueContinuation: deps.queueContinuation,
642
- triggerPendingAbort: (ctx) => {
643
- if (
644
- !shouldTriggerPendingTelegramModelSwitchAbort({
645
- hasPendingModelSwitch: !!deps.getPendingModelSwitch(),
646
- hasActiveTelegramTurn: !!deps.getActiveTurn(),
647
- hasAbortHandler: deps.hasAbortHandler(),
648
- activeToolExecutions: deps.getActiveToolExecutions(),
649
- })
650
- ) {
651
- return false;
652
- }
653
- const selection = deps.getPendingModelSwitch();
654
- const turn = deps.getActiveTurn();
655
- const abort = deps.getAbortHandler();
656
- if (!selection || !turn || !abort) return false;
657
- deps.setPendingModelSwitch(undefined);
658
- deps.queueContinuation(turn, selection, ctx);
659
- abort();
660
- return true;
661
- },
662
- restartInterruptedTurn: (selection, ctx) =>
663
- restartTelegramModelSwitchContinuation({
664
- activeTurn: deps.getActiveTurn(),
691
+ triggerPendingAbort,
692
+ restartInterruptedTurn: (selection, ctx, continuationTurn) => {
693
+ const restarted = restartTelegramModelSwitchContinuation({
694
+ activeTurn: deps.getActiveTurn() ?? continuationTurn,
665
695
  abort: deps.getAbortHandler(),
666
696
  selection,
667
697
  queueContinuation: (turn, nextSelection) => {
668
698
  deps.queueContinuation(turn, nextSelection, ctx);
669
699
  },
670
- }),
700
+ });
701
+ if (restarted) pendingContinuationTurn = undefined;
702
+ return restarted;
703
+ },
671
704
  };
672
705
  }
@@ -1253,7 +1253,6 @@ export interface TelegramAgentStartHookRuntimeDeps<
1253
1253
  export type TelegramAgentStartHookEvent = unknown;
1254
1254
 
1255
1255
  export interface TelegramToolExecutionRuntimeDeps {
1256
- hasActiveTurn: () => boolean;
1257
1256
  getActiveToolExecutions: () => number;
1258
1257
  setActiveToolExecutions: (count: number) => void;
1259
1258
  }
@@ -1361,11 +1360,9 @@ export function createTelegramAgentStartHook<
1361
1360
  }
1362
1361
 
1363
1362
  export function getNextTelegramToolExecutionCount(options: {
1364
- hasActiveTurn: boolean;
1365
1363
  currentCount: number;
1366
1364
  event: "start" | "end";
1367
1365
  }): number {
1368
- if (!options.hasActiveTurn) return options.currentCount;
1369
1366
  if (options.event === "start") {
1370
1367
  return options.currentCount + 1;
1371
1368
  }
@@ -1377,7 +1374,6 @@ export function handleTelegramToolExecutionStartRuntime(
1377
1374
  ): void {
1378
1375
  deps.setActiveToolExecutions(
1379
1376
  getNextTelegramToolExecutionCount({
1380
- hasActiveTurn: deps.hasActiveTurn(),
1381
1377
  currentCount: deps.getActiveToolExecutions(),
1382
1378
  event: "start",
1383
1379
  }),
@@ -1387,15 +1383,13 @@ export function handleTelegramToolExecutionStartRuntime(
1387
1383
  export function handleTelegramToolExecutionEndRuntime(
1388
1384
  deps: TelegramToolExecutionEndRuntimeDeps,
1389
1385
  ): void {
1390
- const hasActiveTurn = deps.hasActiveTurn();
1391
1386
  deps.setActiveToolExecutions(
1392
1387
  getNextTelegramToolExecutionCount({
1393
- hasActiveTurn,
1394
1388
  currentCount: deps.getActiveToolExecutions(),
1395
1389
  event: "end",
1396
1390
  }),
1397
1391
  );
1398
- if (hasActiveTurn) deps.triggerPendingModelSwitchAbort();
1392
+ deps.triggerPendingModelSwitchAbort();
1399
1393
  }
1400
1394
 
1401
1395
  export type TelegramAgentLifecycleHooksRuntimeDeps<
@@ -1485,7 +1479,6 @@ export function createTelegramToolExecutionHooks<TContext>(
1485
1479
  ctx: TContext,
1486
1480
  ): void => {
1487
1481
  handleTelegramToolExecutionEndRuntime({
1488
- hasActiveTurn: deps.hasActiveTurn,
1489
1482
  getActiveToolExecutions: deps.getActiveToolExecutions,
1490
1483
  setActiveToolExecutions: deps.setActiveToolExecutions,
1491
1484
  triggerPendingModelSwitchAbort: () => {
@@ -1010,7 +1010,6 @@ export function createTelegramInboundRouteRuntime<
1010
1010
  updateSettingsMenuMessage: deps.updateSettingsMenuMessage,
1011
1011
  answerCallbackQuery: deps.answerCallbackQuery,
1012
1012
  isIdle: deps.isIdle,
1013
- hasActiveTelegramTurn: deps.activeTurnRuntime.has,
1014
1013
  hasAbortHandler: deps.bridgeRuntime.abort.hasHandler,
1015
1014
  getActiveToolExecutions:
1016
1015
  deps.bridgeRuntime.lifecycle.getActiveToolExecutions,
@@ -4,7 +4,7 @@
4
4
  * Owns small session-local runtime primitives that are shared by orchestration but are not specific to queueing, rendering, polling, or Telegram transport
5
5
  */
6
6
 
7
- const TELEGRAM_TYPING_ACTION_INTERVAL_MS = 2500;
7
+ const TELEGRAM_TYPING_ACTION_INTERVAL_MS = 3_000;
8
8
  const TELEGRAM_TYPING_IDLE_DRAIN_MAX_MS = 250;
9
9
 
10
10
  export interface TelegramRuntimeQueueCounters {
@@ -319,7 +319,6 @@ export interface TelegramTypingLoopDeps {
319
319
  chatId: number,
320
320
  options?: { message_thread_id?: number },
321
321
  ) => Promise<unknown>;
322
- sendAggregateTypingAction?: (chatId: number) => Promise<unknown>;
323
322
  shouldContinue?: () => boolean;
324
323
  onStopped?: () => void;
325
324
  }
@@ -364,7 +363,6 @@ export interface TelegramTypingLoopStarterDeps<
364
363
  chatId: number,
365
364
  options?: { message_thread_id?: number },
366
365
  ) => Promise<unknown>;
367
- sendAggregateTypingAction?: (chatId: number) => Promise<unknown>;
368
366
  updateStatus: (ctx: TContext, error?: string) => void;
369
367
  isContextActive?: (ctx: TContext) => boolean;
370
368
  isTransportAvailable?: () => boolean;
@@ -378,7 +376,7 @@ export function createTelegramTypingLoopStarter<TContext>(
378
376
  ctx: TContext,
379
377
  chatId?: number,
380
378
  options?: { target?: TelegramTypingLoopTarget },
381
- ) => void {
379
+ ) => boolean {
382
380
  return (ctx, chatId, options) => {
383
381
  const transportAuthority = deps.getTransportAuthority?.();
384
382
  const hasTransport = (): boolean =>
@@ -386,9 +384,9 @@ export function createTelegramTypingLoopStarter<TContext>(
386
384
  ? transportAuthority !== undefined &&
387
385
  Object.is(deps.getTransportAuthority(), transportAuthority)
388
386
  : deps.isTransportAvailable?.() !== false;
389
- if (!hasTransport()) return;
387
+ if (!hasTransport()) return false;
390
388
  let active = true;
391
- deps.typing.start({
389
+ return deps.typing.start({
392
390
  chatId: chatId ?? deps.getDefaultChatId(),
393
391
  target: options?.target,
394
392
  intervalMs: deps.intervalMs ?? TELEGRAM_TYPING_ACTION_INTERVAL_MS,
@@ -407,14 +405,6 @@ export function createTelegramTypingLoopStarter<TContext>(
407
405
  deps.typing.stop();
408
406
  return;
409
407
  }
410
- const message =
411
- error instanceof Error ? error.message : String(error);
412
- updateTelegramRuntimeStatusSafely(deps.updateStatus, ctx, {
413
- error: message,
414
- category: "typing",
415
- phase: "status-update",
416
- recordRuntimeEvent: deps.recordRuntimeEvent,
417
- });
418
408
  try {
419
409
  deps.recordRuntimeEvent?.("typing", error, {
420
410
  chatId: targetChatId,
@@ -424,41 +414,6 @@ export function createTelegramTypingLoopStarter<TContext>(
424
414
  }
425
415
  }
426
416
  },
427
- sendAggregateTypingAction: deps.sendAggregateTypingAction
428
- ? async (targetChatId) => {
429
- if (!active) return;
430
- if (!hasTransport()) {
431
- deps.typing.stop();
432
- return;
433
- }
434
- try {
435
- await deps.sendAggregateTypingAction?.(targetChatId);
436
- } catch (error) {
437
- if (deps.isContextActive?.(ctx) === false) return;
438
- if (!active) return;
439
- if (!hasTransport()) {
440
- deps.typing.stop();
441
- return;
442
- }
443
- const message =
444
- error instanceof Error ? error.message : String(error);
445
- updateTelegramRuntimeStatusSafely(deps.updateStatus, ctx, {
446
- error: message,
447
- category: "typing",
448
- phase: "status-update",
449
- recordRuntimeEvent: deps.recordRuntimeEvent,
450
- });
451
- try {
452
- deps.recordRuntimeEvent?.("typing", error, {
453
- chatId: targetChatId,
454
- aggregate: true,
455
- });
456
- } catch {
457
- // Typing diagnostics cannot escape the in-flight action owner.
458
- }
459
- }
460
- }
461
- : undefined,
462
417
  shouldContinue: hasTransport,
463
418
  onStopped: () => {
464
419
  active = false;
@@ -502,12 +457,7 @@ export function startTelegramTypingLoop(
502
457
  const targetChatId = activeDeps.chatId;
503
458
  const threadParams = getTelegramTypingLoopThreadParams(activeDeps.target);
504
459
  const typing = Promise.resolve()
505
- .then(async () => {
506
- await activeDeps.sendTypingAction(targetChatId, threadParams);
507
- if (threadParams?.message_thread_id !== undefined) {
508
- await activeDeps.sendAggregateTypingAction?.(targetChatId);
509
- }
510
- })
460
+ .then(() => activeDeps.sendTypingAction(targetChatId, threadParams))
511
461
  .then(() => undefined)
512
462
  .catch(() => undefined);
513
463
  state.typingInFlight = typing;
@@ -602,7 +552,7 @@ export interface TelegramPromptDispatchLifecycleDeps<
602
552
  ctx: TContext,
603
553
  chatId?: number,
604
554
  options?: { target?: TelegramTypingLoopTarget },
605
- ) => void;
555
+ ) => boolean | void;
606
556
  updateStatus: (ctx: TContext, error?: string) => void;
607
557
  }
608
558
 
@@ -616,7 +566,6 @@ export interface TelegramPromptDispatchRuntimeDeps<
616
566
  chatId: number,
617
567
  options?: { message_thread_id?: number },
618
568
  ) => Promise<unknown>;
619
- sendAggregateTypingAction?: (chatId: number) => Promise<unknown>;
620
569
  updateStatus: (ctx: TContext, error?: string) => void;
621
570
  isContextActive?: (ctx: TContext) => boolean;
622
571
  isTransportAvailable?: () => boolean;
@@ -629,7 +578,7 @@ export interface TelegramPromptDispatchRuntime<TContext> {
629
578
  ctx: TContext,
630
579
  chatId?: number,
631
580
  options?: { target?: TelegramTypingLoopTarget },
632
- ) => void;
581
+ ) => boolean | void;
633
582
  onPromptDispatchStart: (ctx: TContext, chatId?: number) => void;
634
583
  onPromptDispatchFailure: (ctx: TContext, message: string) => void;
635
584
  }
@@ -1866,14 +1866,16 @@ export function createTelegramBridgeApiRuntime(
1866
1866
  deps.chatActionMinIntervalMs ?? 2_000,
1867
1867
  );
1868
1868
  const chatActionMaxGates = Math.max(1, deps.chatActionMaxGates ?? 256);
1869
- const chatActionGates = new Map<
1870
- string,
1871
- { inFlight?: Promise<unknown>; notBeforeMs: number }
1872
- >();
1873
- const getChatActionKey = (
1869
+ type ChatActionGate = {
1870
+ inFlight?: Promise<unknown>;
1871
+ notBeforeMs: number;
1872
+ };
1873
+ const chatActionGates = new Map<string, ChatActionGate>();
1874
+ const chatActionChatGates = new Map<string, ChatActionGate>();
1875
+ const getChatActionKeys = (
1874
1876
  method: string,
1875
1877
  body: Record<string, unknown>,
1876
- ): string | undefined => {
1878
+ ): { action: string; chat: string } | undefined => {
1877
1879
  if (method !== "sendChatAction") return undefined;
1878
1880
  const chatId = body.chat_id;
1879
1881
  const action = body.action;
@@ -1883,12 +1885,16 @@ export function createTelegramBridgeApiRuntime(
1883
1885
  ) {
1884
1886
  return undefined;
1885
1887
  }
1888
+ const chat = String(chatId);
1886
1889
  const threadId = body.message_thread_id;
1887
- return `${String(chatId)}:${
1888
- typeof threadId === "number" || typeof threadId === "string"
1889
- ? String(threadId)
1890
- : "all"
1891
- }:${action}`;
1890
+ return {
1891
+ action: `${chat}:${
1892
+ typeof threadId === "number" || typeof threadId === "string"
1893
+ ? String(threadId)
1894
+ : "all"
1895
+ }:${action}`,
1896
+ chat,
1897
+ };
1892
1898
  };
1893
1899
  const callRecorded = async <TResponse>(
1894
1900
  method: string,
@@ -1896,22 +1902,38 @@ export function createTelegramBridgeApiRuntime(
1896
1902
  options?: TelegramApiCallOptions,
1897
1903
  ): Promise<TResponse> => {
1898
1904
  const recoverError = deps.captureRequestErrorHandler?.(body);
1899
- const chatActionKey = getChatActionKey(method, body);
1900
- if (chatActionKey) {
1905
+ const chatActionKeys = getChatActionKeys(method, body);
1906
+ if (chatActionKeys) {
1901
1907
  const nowMs = now();
1902
1908
  for (const [key, candidate] of chatActionGates) {
1903
1909
  if (!candidate.inFlight && nowMs >= candidate.notBeforeMs) {
1904
1910
  chatActionGates.delete(key);
1905
1911
  }
1906
1912
  }
1907
- let gate = chatActionGates.get(chatActionKey);
1913
+ for (const [key, candidate] of chatActionChatGates) {
1914
+ if (!candidate.inFlight && nowMs >= candidate.notBeforeMs) {
1915
+ chatActionChatGates.delete(key);
1916
+ }
1917
+ }
1918
+ let gate = chatActionGates.get(chatActionKeys.action);
1908
1919
  if (!gate) {
1909
1920
  if (chatActionGates.size >= chatActionMaxGates) return true as TResponse;
1910
1921
  gate = { notBeforeMs: 0 };
1911
- chatActionGates.set(chatActionKey, gate);
1922
+ chatActionGates.set(chatActionKeys.action, gate);
1923
+ }
1924
+ let chatGate = chatActionChatGates.get(chatActionKeys.chat);
1925
+ if (!chatGate) {
1926
+ if (chatActionChatGates.size >= chatActionMaxGates) {
1927
+ return true as TResponse;
1928
+ }
1929
+ chatGate = { notBeforeMs: 0 };
1930
+ chatActionChatGates.set(chatActionKeys.chat, chatGate);
1912
1931
  }
1913
1932
  if (gate.inFlight) return (await gate.inFlight) as TResponse;
1914
- if (now() < gate.notBeforeMs) return true as TResponse;
1933
+ if (chatGate.inFlight) return true as TResponse;
1934
+ if (nowMs < gate.notBeforeMs || nowMs < chatGate.notBeforeMs) {
1935
+ return true as TResponse;
1936
+ }
1915
1937
  let request: Promise<TResponse>;
1916
1938
  request = Promise.resolve()
1917
1939
  .then(() =>
@@ -1931,7 +1953,9 @@ export function createTelegramBridgeApiRuntime(
1931
1953
  chatActionMinIntervalMs,
1932
1954
  (error.retryAfterSeconds ?? 0) * 1_000,
1933
1955
  );
1934
- gate.notBeforeMs = now() + retryAfterMs;
1956
+ const notBeforeMs = now() + retryAfterMs;
1957
+ gate.notBeforeMs = notBeforeMs;
1958
+ chatGate.notBeforeMs = Math.max(chatGate.notBeforeMs, notBeforeMs);
1935
1959
  deps.recordRuntimeEvent(
1936
1960
  "api",
1937
1961
  error,
@@ -1952,8 +1976,10 @@ export function createTelegramBridgeApiRuntime(
1952
1976
  })
1953
1977
  .finally(() => {
1954
1978
  if (gate.inFlight === request) gate.inFlight = undefined;
1979
+ if (chatGate.inFlight === request) chatGate.inFlight = undefined;
1955
1980
  });
1956
1981
  gate.inFlight = request;
1982
+ chatGate.inFlight = request;
1957
1983
  return request;
1958
1984
  }
1959
1985
  try {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.51.1",
3
+ "version": "0.51.2",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-kit",
3
- "version": "0.21.2",
3
+ "version": "0.22.1",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -41,11 +41,11 @@
41
41
  ],
42
42
  "dependencies": {
43
43
  "@llblab/pi-actors": "0.53.0",
44
- "@llblab/pi-clean-room": "0.1.1",
44
+ "@llblab/pi-clean-room": "0.2.0",
45
45
  "@llblab/pi-codex-usage": "0.10.0",
46
46
  "@llblab/pi-grow-loop": "0.8.1",
47
47
  "@llblab/pi-state-flow": "0.18.1",
48
- "@llblab/pi-telegram": "0.51.1",
48
+ "@llblab/pi-telegram": "0.51.2",
49
49
  "@llblab/skills": "1.15.0"
50
50
  },
51
51
  "bundledDependencies": [