@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/model.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  * Owns model identity, thinking levels, scoped resolution, current-model state, and in-flight model switching
5
5
  */
6
6
 
7
+ import * as Machine from "./machine.ts";
7
8
  import {
8
9
  truncateTelegramQueueSummary,
9
10
  type PendingTelegramTurn,
@@ -368,34 +369,11 @@ export function sortScopedModels<TModel extends MenuModel = MenuModel>(
368
369
 
369
370
  // --- In-Flight Model Switching ---
370
371
 
371
- export interface PendingModelSwitchStore<TSelection> {
372
- get: () => TSelection | undefined;
373
- set: (selection: TSelection | undefined) => void;
374
- clear: () => void;
375
- has: () => boolean;
376
- }
377
-
378
372
  export interface TelegramInFlightModelSwitchState {
379
373
  isIdle: boolean;
380
374
  hasAbortHandler: boolean;
381
375
  }
382
376
 
383
- export function createPendingModelSwitchStore<
384
- TSelection,
385
- >(): PendingModelSwitchStore<TSelection> {
386
- let selection: TSelection | undefined;
387
- return {
388
- get: () => selection,
389
- set: (nextSelection) => {
390
- selection = nextSelection;
391
- },
392
- clear: () => {
393
- selection = undefined;
394
- },
395
- has: () => selection !== undefined,
396
- };
397
- }
398
-
399
377
  export function canRestartAgentRunForTelegramModelSwitch(
400
378
  state: TelegramInFlightModelSwitchState,
401
379
  ): boolean {
@@ -416,21 +394,6 @@ export function shouldTriggerPendingTelegramModelSwitchAbort(state: {
416
394
  );
417
395
  }
418
396
 
419
- export function restartTelegramModelSwitchContinuation<
420
- TTurn,
421
- TSelection,
422
- >(state: {
423
- activeTurn: TTurn | undefined;
424
- abort: (() => void) | undefined;
425
- selection: TSelection;
426
- queueContinuation: (turn: TTurn, selection: TSelection) => void;
427
- }): boolean {
428
- if (!state.activeTurn || !state.abort) return false;
429
- state.queueContinuation(state.activeTurn, state.selection);
430
- state.abort();
431
- return true;
432
- }
433
-
434
397
  export function buildTelegramModelSwitchContinuationText<
435
398
  TModel extends MenuModel,
436
399
  >(
@@ -554,8 +517,6 @@ export function createTelegramModelSwitchContinuationQueueRuntime<
554
517
 
555
518
  export interface TelegramModelSwitchControllerDeps<TContext, TSelection> {
556
519
  isIdle: (ctx: TContext) => boolean;
557
- getPendingModelSwitch: () => TSelection | undefined;
558
- setPendingModelSwitch: (selection: TSelection | undefined) => void;
559
520
  getActiveTurn: () => PendingTelegramTurn | undefined;
560
521
  getAbortHandler: () => (() => void) | undefined;
561
522
  hasAbortHandler: () => boolean;
@@ -570,6 +531,8 @@ export interface TelegramModelSwitchControllerDeps<TContext, TSelection> {
570
531
 
571
532
  export interface TelegramModelSwitchController<TContext, TSelection> {
572
533
  canOfferInFlightSwitch: (ctx: TContext) => boolean;
534
+ /** A staged switch waits for its abort; queue dispatch and live-work observation treat it as pending control. */
535
+ hasPendingSwitch: () => boolean;
573
536
  stagePendingSwitch: (
574
537
  selection: TSelection,
575
538
  ctx: TContext,
@@ -610,8 +573,6 @@ export function createTelegramModelSwitchControllerRuntime<
610
573
  ): TelegramModelSwitchController<TContext, TSelection> {
611
574
  return createTelegramModelSwitchController({
612
575
  isIdle: deps.isIdle,
613
- getPendingModelSwitch: deps.getPendingModelSwitch,
614
- setPendingModelSwitch: deps.setPendingModelSwitch,
615
576
  getActiveTurn: deps.getActiveTurn,
616
577
  getAbortHandler: deps.getAbortHandler,
617
578
  hasAbortHandler: deps.hasAbortHandler,
@@ -626,31 +587,163 @@ export function createTelegramModelSwitchControllerRuntime<
626
587
  });
627
588
  }
628
589
 
590
+ /** The whole model-switch state; only `transitionTelegramModelSwitch` produces a new one. */
591
+ export interface TelegramModelSwitchMachineState<TSelection> {
592
+ /** One staged switch: the selection and the turn its continuation answers move and clear together. */
593
+ readonly pending: {
594
+ readonly selection: TSelection;
595
+ /** The active Telegram turn at staging, or the model menu's own target for a local run. */
596
+ readonly turn?: TelegramModelSwitchContinuationSource;
597
+ } | null;
598
+ }
599
+
600
+ /** Facts owned by other domains, sampled by the shell when the event is dispatched. */
601
+ interface TelegramModelSwitchRunFacts {
602
+ activeTurn?: TelegramModelSwitchContinuationSource;
603
+ hasAbortHandler: boolean;
604
+ activeToolExecutions: number;
605
+ }
606
+
607
+ export type TelegramModelSwitchEvent<TSelection> =
608
+ | {
609
+ type: "staged";
610
+ selection: TSelection;
611
+ turn?: TelegramModelSwitchContinuationSource;
612
+ }
613
+ | ({ type: "abortRequested" } & TelegramModelSwitchRunFacts)
614
+ | {
615
+ type: "restartRequested";
616
+ selection: TSelection;
617
+ turn?: TelegramModelSwitchContinuationSource;
618
+ hasAbortHandler: boolean;
619
+ }
620
+ | { type: "cleared" };
621
+
622
+ export type TelegramModelSwitchEffect<TSelection> =
623
+ | {
624
+ type: "queueContinuation";
625
+ turn: TelegramModelSwitchContinuationSource;
626
+ selection: TSelection;
627
+ }
628
+ | { type: "abort" };
629
+
630
+ export interface TelegramModelSwitchStep<
631
+ TSelection,
632
+ > extends Machine.TelegramMachineStep<
633
+ TelegramModelSwitchMachineState<TSelection>,
634
+ TelegramModelSwitchEffect<TSelection>
635
+ > {
636
+ /** `abortRequested` / `restartRequested`: the run was stopped for a continuation. */
637
+ result: boolean;
638
+ }
639
+
640
+ export function createTelegramModelSwitchMachineState<
641
+ TSelection,
642
+ >(): TelegramModelSwitchMachineState<TSelection> {
643
+ return { pending: null };
644
+ }
645
+
646
+ /** The continuation is queued before the abort, so the stopped run always has its successor waiting. */
647
+ const restartFor = <TSelection>(
648
+ turn: TelegramModelSwitchContinuationSource,
649
+ selection: TSelection,
650
+ ): TelegramModelSwitchStep<TSelection> => ({
651
+ state: { pending: null },
652
+ effects: [{ type: "queueContinuation", turn, selection }, { type: "abort" }],
653
+ result: true,
654
+ });
655
+
656
+ /**
657
+ * Pure model-switch transition. A staged switch fires only once no tool runs and an abort handler exists; any restart
658
+ * or clear consumes the staged switch too, so a stale selection can never abort the continuation run it started.
659
+ */
660
+ export function transitionTelegramModelSwitch<TSelection>(
661
+ state: TelegramModelSwitchMachineState<TSelection>,
662
+ event: TelegramModelSwitchEvent<TSelection>,
663
+ ): TelegramModelSwitchStep<TSelection> {
664
+ const unchanged = { state, effects: [], result: false };
665
+ switch (event.type) {
666
+ case "staged":
667
+ return {
668
+ state: {
669
+ pending: {
670
+ selection: event.selection,
671
+ ...(event.turn ? { turn: event.turn } : {}),
672
+ },
673
+ },
674
+ effects: [],
675
+ result: false,
676
+ };
677
+ case "abortRequested": {
678
+ const pending = state.pending;
679
+ const turn = pending?.turn ?? event.activeTurn;
680
+ if (
681
+ !pending ||
682
+ !turn ||
683
+ !shouldTriggerPendingTelegramModelSwitchAbort({
684
+ hasPendingModelSwitch: true,
685
+ hasContinuationTurn: true,
686
+ hasAbortHandler: event.hasAbortHandler,
687
+ activeToolExecutions: event.activeToolExecutions,
688
+ })
689
+ )
690
+ return unchanged;
691
+ return restartFor(turn, pending.selection);
692
+ }
693
+ case "restartRequested":
694
+ return event.turn && event.hasAbortHandler
695
+ ? restartFor(event.turn, event.selection)
696
+ : unchanged;
697
+ case "cleared":
698
+ return { state: { pending: null }, effects: [], result: false };
699
+ }
700
+ }
701
+
702
+ /**
703
+ * The shell around `transitionTelegramModelSwitch`: it samples the active turn, abort handler and tool count into
704
+ * events, and runs the continuation and abort effects with the caller's context and the handler it sampled.
705
+ */
629
706
  export function createTelegramModelSwitchController<TContext, TSelection>(
630
707
  deps: TelegramModelSwitchControllerDeps<TContext, TSelection>,
631
708
  ): TelegramModelSwitchController<TContext, TSelection> {
632
- let pendingContinuationTurn:
633
- TelegramModelSwitchContinuationSource | undefined;
634
- const triggerPendingAbort = (ctx: TContext): boolean => {
635
- const turn = pendingContinuationTurn ?? deps.getActiveTurn();
636
- if (
637
- !shouldTriggerPendingTelegramModelSwitchAbort({
638
- hasPendingModelSwitch: !!deps.getPendingModelSwitch(),
639
- hasContinuationTurn: !!turn,
640
- hasAbortHandler: deps.hasAbortHandler(),
641
- activeToolExecutions: deps.getActiveToolExecutions(),
642
- })
643
- ) {
644
- return false;
709
+ // The context and abort handler of the call being dispatched; effects run synchronously inside that dispatch.
710
+ let call: { ctx: TContext; abort?: () => void } | undefined;
711
+ const machine = Machine.createTelegramMachineRunner<
712
+ TelegramModelSwitchMachineState<TSelection>,
713
+ TelegramModelSwitchEvent<TSelection>,
714
+ TelegramModelSwitchEffect<TSelection>,
715
+ TelegramModelSwitchStep<TSelection>
716
+ >({
717
+ initial: createTelegramModelSwitchMachineState<TSelection>(),
718
+ transition: transitionTelegramModelSwitch,
719
+ run(effect) {
720
+ if (!call) return;
721
+ if (effect.type === "queueContinuation")
722
+ deps.queueContinuation(effect.turn, effect.selection, call.ctx);
723
+ else call.abort?.();
724
+ },
725
+ });
726
+ const dispatchFor = (
727
+ ctx: TContext,
728
+ abort: (() => void) | undefined,
729
+ event: TelegramModelSwitchEvent<TSelection>,
730
+ ): boolean => {
731
+ const preceding = call;
732
+ call = { ctx, abort };
733
+ try {
734
+ return machine.dispatch(event).result;
735
+ } finally {
736
+ call = preceding;
645
737
  }
646
- const selection = deps.getPendingModelSwitch();
738
+ };
739
+ const triggerPendingAbort = (ctx: TContext): boolean => {
647
740
  const abort = deps.getAbortHandler();
648
- if (!selection || !turn || !abort) return false;
649
- pendingContinuationTurn = undefined;
650
- deps.setPendingModelSwitch(undefined);
651
- deps.queueContinuation(turn, selection, ctx);
652
- abort();
653
- return true;
741
+ return dispatchFor(ctx, abort, {
742
+ type: "abortRequested",
743
+ activeTurn: deps.getActiveTurn(),
744
+ hasAbortHandler: deps.hasAbortHandler() && !!abort,
745
+ activeToolExecutions: deps.getActiveToolExecutions(),
746
+ });
654
747
  };
655
748
  return {
656
749
  canOfferInFlightSwitch: (ctx) =>
@@ -658,9 +751,14 @@ export function createTelegramModelSwitchController<TContext, TSelection>(
658
751
  isIdle: deps.isIdle(ctx),
659
752
  hasAbortHandler: deps.hasAbortHandler(),
660
753
  }),
754
+ hasPendingSwitch: () => machine.state.pending !== null,
661
755
  stagePendingSwitch: (selection, ctx, continuationTurn) => {
662
- pendingContinuationTurn = deps.getActiveTurn() ?? continuationTurn;
663
- deps.setPendingModelSwitch(selection);
756
+ const turn = deps.getActiveTurn() ?? continuationTurn;
757
+ machine.dispatch({
758
+ type: "staged",
759
+ selection,
760
+ ...(turn ? { turn } : {}),
761
+ });
664
762
  try {
665
763
  deps.updateStatus(ctx);
666
764
  } finally {
@@ -668,22 +766,19 @@ export function createTelegramModelSwitchController<TContext, TSelection>(
668
766
  }
669
767
  },
670
768
  clearPendingSwitch: () => {
671
- pendingContinuationTurn = undefined;
672
- deps.setPendingModelSwitch(undefined);
769
+ machine.dispatch({ type: "cleared" });
673
770
  },
674
771
  queueContinuation: deps.queueContinuation,
675
772
  triggerPendingAbort,
676
773
  restartInterruptedTurn: (selection, ctx, continuationTurn) => {
677
- const restarted = restartTelegramModelSwitchContinuation({
678
- activeTurn: deps.getActiveTurn() ?? continuationTurn,
679
- abort: deps.getAbortHandler(),
774
+ const abort = deps.getAbortHandler();
775
+ const turn = deps.getActiveTurn() ?? continuationTurn;
776
+ return dispatchFor(ctx, abort, {
777
+ type: "restartRequested",
680
778
  selection,
681
- queueContinuation: (turn, nextSelection) => {
682
- deps.queueContinuation(turn, nextSelection, ctx);
683
- },
779
+ ...(turn ? { turn } : {}),
780
+ hasAbortHandler: !!abort,
684
781
  });
685
- if (restarted) pendingContinuationTurn = undefined;
686
- return restarted;
687
782
  },
688
783
  };
689
784
  }
package/lib/polling.ts CHANGED
@@ -1366,6 +1366,8 @@ export function createTelegramThreadCapabilityMonitor<TContext>(
1366
1366
  return {
1367
1367
  start(ctx) {
1368
1368
  stop();
1369
+ // Disabled-mode confirmation counts consecutive probes of one run, never observations from before a restart.
1370
+ consecutiveDisabledProbes = 0;
1369
1371
  const expectedGeneration = generation;
1370
1372
  interval = setInterval(() => {
1371
1373
  if (generation !== expectedGeneration) return;