@llblab/pi-telegram 0.51.0 → 0.51.2

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 (46) hide show
  1. package/AGENTS.md +4 -3
  2. package/BACKLOG.md +3 -8
  3. package/CHANGELOG.md +11 -0
  4. package/README.md +1 -1
  5. package/dist/lib/bindings.d.ts +2 -0
  6. package/dist/lib/bindings.js +3 -4
  7. package/dist/lib/bus-api.d.ts +0 -1
  8. package/dist/lib/bus-api.js +0 -3
  9. package/dist/lib/bus-follower.d.ts +3 -1
  10. package/dist/lib/bus-follower.js +6 -1
  11. package/dist/lib/commands.d.ts +5 -0
  12. package/dist/lib/commands.js +9 -0
  13. package/dist/lib/extension.js +6 -3
  14. package/dist/lib/menu-model.d.ts +3 -3
  15. package/dist/lib/menu-model.js +17 -3
  16. package/dist/lib/menu.d.ts +5 -7
  17. package/dist/lib/menu.js +4 -5
  18. package/dist/lib/model.d.ts +12 -12
  19. package/dist/lib/model.js +49 -35
  20. package/dist/lib/queue.d.ts +8 -2
  21. package/dist/lib/queue.js +64 -19
  22. package/dist/lib/replies.d.ts +4 -0
  23. package/dist/lib/replies.js +14 -0
  24. package/dist/lib/routing.d.ts +1 -0
  25. package/dist/lib/routing.js +5 -1
  26. package/dist/lib/runtime.d.ts +3 -6
  27. package/dist/lib/runtime.js +4 -55
  28. package/dist/lib/telegram-api.js +36 -10
  29. package/dist/package.json +1 -1
  30. package/docs/architecture.md +9 -9
  31. package/docs/multi-instance-bus.md +3 -3
  32. package/docs/public-api.md +1 -1
  33. package/lib/bindings.ts +14 -4
  34. package/lib/bus-api.ts +0 -7
  35. package/lib/bus-follower.ts +13 -2
  36. package/lib/commands.ts +19 -0
  37. package/lib/extension.ts +10 -3
  38. package/lib/menu-model.ts +25 -4
  39. package/lib/menu.ts +14 -9
  40. package/lib/model.ts +77 -44
  41. package/lib/queue.ts +76 -24
  42. package/lib/replies.ts +18 -0
  43. package/lib/routing.ts +7 -1
  44. package/lib/runtime.ts +7 -58
  45. package/lib/telegram-api.ts +43 -17
  46. package/package.json +1 -1
package/AGENTS.md CHANGED
@@ -74,7 +74,7 @@ Use the relevant local skill before non-trivial work in its domain. Keep skill o
74
74
  - Threaded Mode has exactly one live leader per bot profile. Followers are real operator-started Pi processes and must authenticate/register over local IPC; Telegram never spawns hidden Pi processes. A live but unreachable owner does not authorize split-brain polling.
75
75
  - Local IPC is a trust boundary, not merely a private socket. Unknown, stale, mismatched-generation, or unauthorized requests must not inject prompts, callbacks, API sends, artifacts, liveness, or bindings. Follower registration identity may prepare a binding, but inbound generation authority is published only after successful preparation for the same generation and current context; pending or failed startup cannot append into a retained old journal. Readiness also binds the active Pi context and supplied session generation. Session refresh awaits binding preparation without re-registering; reusing a context object cannot carry readiness across a session-generation change. Registration requests capture session authority before asynchronous startup and fence every publication/finalization against the current attempt. Stop or supersession invalidates that attempt; obsolete cleanup cannot erase a newer registration or a refreshed context.
76
76
  - Protocol compatibility is independent from package version. Registration negotiates protocol version, runtime build, and canonical capabilities before target provisioning or live publication. `durable-follower-admission-v1` gates source forwarding; `queue-handoff-v1` independently gates semantic queue transfer for every participant and is advertised only with exact source/recipient journal-binding composition. `follower.register` and capability-gated restore-only `follower.restoreWorkspace` are bootstrap requests; other requests require exact live-registry generation authority, and `bus.ack` is response-only. `thread-display-mode-v1` gates follower display-setting requests; the leader owns their serialized profile preference and title application.
77
- - Long-lived timers, pollers, watchers, receivers, heartbeats, background delivery, and deferred dispatch are session-bound. Replacement stops stale activity and makes late work inert; teardown must recheck the captured session generation even when context identity is reused. Same-process handoff may preserve exact profile/target identity but never stale Pi context or cross-profile authority. Participating source observations must hold a reference for the actual read, including pending-mutation/count queries against a stopped worker's retained source; a scoped observation never restores receipt readiness or execution authority. A donor cancellation resumed after remote handoff awaits must also hold a source reference; only the existing exact journal CAS may cancel, never undo accepted recipient custody. Stable source keys do not certify captured callable lifetimes: snapshot prepared worker capabilities at construction and replace them on source-handle renewal without replaying unsettled input. Aborting a durable update generation does not release that `update_id`: replacement replay waits for its actual handler settlement, and effectful handlers use the shared execution fence immediately before commit and after awaited delegation. Admission also rechecks that fence after the default handler returns, before its outcome can settle custody; a stopped handler's ordinary return is not completion authority. Internal clones explicitly carry the hidden fence; reroute forwarding, thread-store mutation, cleanup, and Bot API boundaries retain the originating generation.
77
+ - Long-lived timers, pollers, watchers, receivers, heartbeats, background delivery, and deferred dispatch are session-bound. Replacement stops stale activity and makes late work inert; teardown must recheck the captured session generation even when context identity is reused. Same-process handoff may preserve exact profile/target identity but never stale Pi context or cross-profile authority. Follower heartbeat response deadlines align with the eight-second leader stale-liveness window rather than the generic one-second local-RPC default; an ordinary multi-second Pi/TUI event-loop stall is not registration loss. Participating source observations must hold a reference for the actual read, including pending-mutation/count queries against a stopped worker's retained source; a scoped observation never restores receipt readiness or execution authority. A donor cancellation resumed after remote handoff awaits must also hold a source reference; only the existing exact journal CAS may cancel, never undo accepted recipient custody. Stable source keys do not certify captured callable lifetimes: snapshot prepared worker capabilities at construction and replace them on source-handle renewal without replaying unsettled input. Aborting a durable update generation does not release that `update_id`: replacement replay waits for its actual handler settlement, and effectful handlers use the shared execution fence immediately before commit and after awaited delegation. Admission also rechecks that fence after the default handler returns, before its outcome can settle custody; a stopped handler's ordinary return is not completion authority. Internal clones explicitly carry the hidden fence; reroute forwarding, thread-store mutation, cleanup, and Bot API boundaries retain the originating generation.
78
78
  - Runtime state is event-driven reconciliation of local assumptions against Telegram signals, not a complete bot read-model and not permission to query Telegram on every action. Destructive thread cleanup goes through `thread-reconciler` with current proof and leader fencing. Fresh Workspace Thread creation derives its initial Bot API title from the active display mode before issuance; the stable generated `threadName` remains separate from the acknowledged `displayTitle`.
79
79
 
80
80
  ### 4.3 Durable Admission And Settlement
@@ -95,11 +95,12 @@ Use the relevant local skill before non-trivial work in its domain. Keep skill o
95
95
  ### 4.4 Queue, Delivery, And User Surfaces
96
96
 
97
97
  - Queue lane/kind admission is explicit. Dispatch waits for active-turn, pending-dispatch, control, compaction, `ctx.isIdle()`, and Pi pending-message guards; a dispatched prompt stays queued until `agent_start` consumes it. The terminal `+N` suffix is a yellow count of executable prompts still waiting, excludes the dispatched head immediately, and never counts current agent work from any source. Each prompt is one object with one active lane and no reserved return slot. Normal and Priority are separate FIFO lanes: crossing lanes removes it from the source and appends it at the destination tail, while Keep/Skip and same-category emoji changes preserve lane position. Complete reaction sets independently derive Priority from recognized positive emoji and Skip from recognized negative emoji; both may coexist, suppressed turns retain durable receipts while waiting, and Skip settles them only when the prompt reaches dispatch before dropping it without inference. Suppressed turns remain visible at a struck-through physical ordinal without contributing to executable queue counters, while graceful session shutdown discards all remaining queue authority before clearing memory.
98
- - `/stop`, `/abort`, `/next`, and `/continue` respectively reset+abort, abort while preserving queue, force the next turn, and enqueue a control-lane continuation. Abort-history folding applies only to Telegram-owned active turns.
98
+ - `/stop`, `/abort`, `/next`, and `/continue` respectively reset+abort, abort while preserving queue, force the next turn, and enqueue a control-lane continuation. Abort-history folding applies only to Telegram-owned active turns. A busy `/next` marks only its exact active Telegram turn: terminal abort settlement attempts one explicit abort notice before the exact selected queued prompt receives its dispatch notice. Notice failure is diagnostic and cannot block dispatch; a later `/abort` or `/stop` cancels both pending transition notices before taking ownership. Target command adapters must forward these transition ports. A successful dispatch notice keeps the queued prompt's one reply-header claim across agent start, so later messages in that turn never repeat it.
99
+ - A model selection from an authorized Telegram target may stop and continue any interruptible active agent run in that same Pi session, including local/TUI work. Queue the synthetic control-lane continuation before aborting; when no Telegram prompt owns the run, bind continuation and reply ownership to the exact model-menu chat/Thread/message. Delay abort until every active tool execution settles, and clear both selection and fallback target on cancellation, agent start, settlement, or session replacement.
99
100
  - Telegram extension side effects must not hold Pi's core lifecycle hostage after semantic completion. Preserve ordering in extension-owned background work, record failures, and fence target/profile/transport/session authority.
100
101
  - Complete assistant/guest model answers use Telegram-native Rich Markdown. Harness-owned menus, status, diagnostics, thinking, and tool evidence remain explicit HTML/plain or their documented native surface. Before Telegram preview or final delivery, strip every assistant-authored HTML comment regardless of Markdown position while keeping action activation top-level-only; a comment-only result sends no text message. Preserve literal code outside comments and structurally safe chunking; never split invalid markup.
101
102
  - `preview` owns streaming lifecycle only, not assistant rendering. Finalization waits for active preview flushes and must not issue pre/post-final draft-clear calls that create transient Telegram draft UI. Turns that already answer as one atomic reply (voice replies, Guest Mode queries) never stream previews.
102
- - Native `sendChatAction(typing)` is the automatic activity signal for unsettled agent and compaction work while Telegram transport is authorized. Extension-owned blocking UI prompts pause it and completion resumes it while either work owner remains active. Do not invent extra in-chat work indicators or emit activity for startup/connect/reload/recovery alone.
103
+ - Native `sendChatAction(typing)` is the automatic activity signal for unsettled agent and compaction work while Telegram transport is authorized. Extension-owned blocking UI prompts pause it and completion resumes it while either work owner remains active. Compaction may stop only a typing loop it actually started; it must preserve a pre-existing agent-owned loop. Refresh only the assigned Thread on a conservative three-second cadence; do not mirror typing to aggregate `All` and multiply shared-chat flood pressure. Typing is best-effort presence only: a failed exact-target action remains a structured diagnostic and must not replace a healthy connected/leader/follower status with `error`. Do not invent extra in-chat work indicators or emit activity for startup/connect/reload/recovery alone.
103
104
  - Public activity handlers and connected companion delivery are asynchronous, target-bound, generation-fenced surfaces. Connected companion projection has no independent opt-out: disconnect or authority loss is its boundary. Token deltas, hidden reasoning, unknown sources, and stale authority never enter public projection.
104
105
  - Thread display defaults to the profile-scoped Letters strategy, with Names, Directory Snake, and Directory Title as the other automatic choices; Names projects the generated dictionary name for the slot. Unsupported retained display keys resolve to Letters without rewriting persisted configuration. A durable manual Thread display name retained on its Workspace binding overrides any automatic projection until exact reset; keep generated/recovery identity separate from manual and acknowledged display fields. After leader startup, automatic display contraction waits one follower-staleness window so election and follower re-registration cannot briefly remove and restore an acknowledged same-cwd suffix; stop/start generation cancels stale reconciliation. UI labels, emoji semantics, navigation, settings controls, callback namespaces, voice behavior, command templates, and assistant markup follow the linked `/docs` contracts. Generated human-readable prompt-button labels use `emoji + space + text`; emoji-free text is only a reasoned no-semantic-marker fallback. Non-spatial generated controls default to top-level vertical cells, with nested rows reserved for unmistakably compact peers. Do not restate other evolving UI details here.
105
106
 
package/BACKLOG.md CHANGED
@@ -2,16 +2,11 @@
2
2
 
3
3
  _This file owns unresolved project work only. Completed behavior belongs in `CHANGELOG.md`; durable contracts belong in `AGENTS.md` and `/docs`._
4
4
 
5
- - [ ] `Unified Telegram /new continuity` ([#81](https://github.com/llblab/pi-telegram/issues/81), `live-acceptance-gated`): Run operator-authorized Classic and Threaded live smoke for the implemented one-shot successor handoff, then close the issue. Verify confirmation deletion, exact-target/session continuity, one terminal result, and no duplicate settlement.
6
- - [ ] `Manual Thread naming` (`live-acceptance-gated`): Exercise command ordering, dialog/cancel, invalid input, duplicate callbacks, leader/follower rename, manual override, and reset against disposable Threads. Local generation/target fencing and Bot-API-wait replacement regressions are complete.
7
- - [ ] [`Workspace Thread recovery`](./docs/multi-instance-bus.md) (`operator-/environment-gated`): Finish live and platform evidence without weakening exact owner, admission, journal, or deletion fences.
8
- - [ ] Confirm the retired K Thread/history is absent after the successful full-A–Z pressure rotation, and repeat the rebuilt leader-election smoke to verify a returning same-directory follower causes no temporary suffix contraction.
9
- - [ ] Complete follower Restore, inaccessible-callback, already-absent chooser, stale-target, Singleton↔Threaded, and native-Windows acceptance with disposable targets; preserve accepted work and current ownership.
5
+ - [ ] [`Workspace Thread recovery`](./docs/multi-instance-bus.md) (`environment-gated`): Complete native-Windows coverage for follower restore, inaccessible callbacks, stale targets, and Singleton↔Threaded transitions while preserving accepted work and current ownership.
10
6
  - [ ] Select an approved exact-absence observation before implementing ambiguous `deletion-issued` recovery. Never replay an unknown deletion or infer absence from cache, heartbeat silence, empty `editForumTopic`, or `sendChatAction` success.
11
7
  - [ ] [`Automatic pairing and follower input custody`](./docs/architecture.md#automatic-pairing-confirmation-design) (`activation-gated`): Keep production custody disabled until historical sources and physical references are reconciled, all legacy writers/consumers are retired or excluded under operator authority, migration is complete, and every participating peer supports the final protocol. Preserve outcome-unknown running work, exact receipt/group authority, authenticated handoff, and rollback/downgrade safety. Then compose the prepared v3 lifecycle/bus ports, add bounded operator disposition for retained legacy retry state, and run real-process plus native-Windows acceptance.
12
- - [ ] `Thread Cleanup Manager` (`activation-gated`): Production currently exposes non-destructive review only. Before enabling deletion, compose the exact cleanup fence/permit coordinator, prove successor recovery for `commit-ready` without transport replay, and run operator-authorized live acceptance against disposable inactive Threads. Unknown outcomes remain retained.
13
- - [ ] `Channel operations` (`live-acceptance-gated`): On a disposable channel with real sender rights, verify media rejection, duplicate-send suppression, caption edit, and the durable authored-post create/list/edit/delete flow. Preserve direct-leader authority and outcome-unknown no-replay semantics.
8
+ - [ ] `Thread Cleanup Manager` (`activation-gated`): Production currently exposes non-destructive review only. Before enabling deletion, compose the exact cleanup fence/permit coordinator and prove successor recovery for `commit-ready` without transport replay. Unknown outcomes remain retained.
14
9
  - [ ] [`Inference-bypass Generative Apps`](./docs/generative-apps.md) (`hardening`): Complete removal/replacement recovery, process-birth lock recovery, follower routing, refresh/backoff, stale revision rejection, redacted failure diagnostics, CML/JSON parity, and no-model-turn acceptance. Extend the Music Player adapter evidence across volume, toggle, previous, status, restart, unavailable backend, and bounded process failures.
15
10
  - [ ] `External compatibility acceptance` (`environment-gated`): Confirm one `telegram_bind` request through the reporter's OMP + llama-server build ([#267](https://github.com/llblab/pi-telegram/issues/267)); validate environment-backed bot-token setup/status for default and named profiles; and extend native in-body controls to a second client, follower routing, disabled cells, and app-method revision rejection.
16
- - [ ] `Queue recovery evidence` (`operator-gated`): Establish a supported preservation/discard path for any already-wedged in-memory queue before mutation. Optional disposable-follower smoke may overlap delayed voice processing with turn completion to confirm one-time ordered consumption. Never clear journals, replay settled input, or treat restart as repair.
11
+ - [ ] `Queue recovery evidence` (`operator-gated`): Establish a supported preservation/discard path for any already-wedged in-memory queue before mutation. Never clear journals, replay settled input, or treat restart as repair.
17
12
  - [ ] `Show Me ownership transfer` (`release-coordination`): After a pi-telegram release containing the bundled `show-me` Skill is available, remove the former copy from `@llblab/skills` under that package's release policy, then update Pi Kit. The coordinated final install must expose exactly one Skill identity.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,17 @@
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.51.2: Model switching and typing continuity
8
+
9
+ - `In-flight model switching`: Telegram model selection can again stop, switch, and continue any interruptible agent run in the current Pi session, including local/TUI work without an active Telegram prompt. The exact model-menu chat/Thread/message supplies fallback continuation ownership, active tools defer abort until settlement, and cancellation/session boundaries clear both selection and target state instead of returning a false busy response.
10
+ - `Typing continuity`: Native presence now refreshes only the assigned Thread every three seconds instead of duplicating each action into aggregate `All`. The leader admits at most one concurrent action per chat and shares Telegram 429 cooldown across that chat's Thread keys. Compaction preserves a pre-existing agent typing loop instead of claiming and stopping it, addressing both multi-instance flicker and lifecycle gaps.
11
+ - `Typing status truthfulness`: Failed `sendChatAction(typing)` calls remain bounded structured diagnostics but no longer replace a healthy connected/leader/follower status with `error`. Optional presence failures therefore cannot misreport transport health after the actual turn and delivery continue successfully.
12
+
13
+ ## 0.51.1: Queue transitions and follower heartbeat stability
14
+
15
+ - `/next lifecycle notices`: Target command composition now forwards request, exact-turn marker, and cancellation ports. Busy `/next` orders abort then selected-prompt dispatch notices; failures cannot block dispatch, and `/abort`/`/stop` cancel stale notices. A successful dispatch notice keeps first-reply ownership through agent start, so later answer messages do not repeat the queued-prompt reply. Rapid ordinary messages remain distinct turns.
16
+ - `Follower heartbeat stability`: Follower heartbeats now use a dedicated eight-second response deadline aligned with leader stale-liveness policy instead of the generic one-second local-RPC default. Ordinary multi-second TUI/event-loop stalls therefore retain registration and stable `follower` status rather than destroying the socket, producing leader EPIPE noise, and entering repeated reconnect cycles.
17
+
7
18
  ## 0.51.0: Safe Workspace rotation and follower readiness
8
19
 
9
20
  - `Workspace slot rotation`: Fresh allocation reclaims the oldest proven inactive slot only at full A–Z pressure. Exact grouped receipts may be journal-CAS discarded only after complete source inspection proves every process-birth owner dead. Live/unknown owners, partial groups, handoffs and unrelated work stay protected. Fresh protection precedes Thread/history deletion and slot reuse. Pi sessions/files/memory/journals remain; restore-only never evicts; ambiguous deletion never replays.
package/README.md CHANGED
@@ -123,7 +123,7 @@ Enable the optional capabilities the bridge needs in the [@BotFather](https://t.
123
123
  | Queue control | Inspect waiting turns, keep or skip stale work, promote important prompts, continue, abort, stop, or force the next queued item. | Long Pi tasks keep running while new mobile prompts stay visible and controllable instead of interrupting or disappearing. |
124
124
  | Operator menu | Use `/start` for status, prompt templates, model, thinking, settings, queue, extension sections, and diagnostics. | The bot is an operator panel, not a command cheat sheet. |
125
125
  | Prompt templates | Run Pi prompt templates as Telegram-safe commands such as `/fix_tests`. | Reusable local workflows become phone-accessible without exposing arbitrary terminal commands. |
126
- | Model and thinking | Switch model or thinking level from Telegram through safe continuation flows. | Mobile control can adjust execution strategy without tearing down the current session. |
126
+ | Model and thinking | Switch model or thinking level from Telegram through safe continuation flows, including an active local/TUI agent run. | Mobile control can stop, switch, and resume in the same session context instead of returning a false busy dead end. |
127
127
  | Compaction | Confirm `/compact`, show native active status during compaction, and preserve Telegram-owned turn semantics. | Context maintenance is visible and safe from the phone. |
128
128
  | Draft previews | Show Telegram's native `…typing` indicator whenever the connected instance is doing agent work, or enable Rich Draft previews for streamed answer text. | Local prompts, Telegram turns, and autonomous continuations remain visibly active while draft visibility stays independent from final rendering. |
129
129
  | Activity | Keep the default `verbose` technical surface, show only `thinking`, show only `tools`, or select `quiet` for answer-only delivery. Every instance reloads this shared file-backed choice before a new agent run; thinking accumulates for two seconds and then updates at most every two seconds in a headerless expandable quote, while each tool uses one iconless closed root row containing nested evidence details. | Persistent collapsed technical activity minimizes chat height and stays bounded, redacted, target-fenced, free of URL previews, and visually separate from semantic assistant answers. |
@@ -34,6 +34,7 @@ export interface TelegramQueueBindingRuntime<TContext> {
34
34
  mutation: Queue.TelegramQueueMutationController<TContext>;
35
35
  dispatchNext: (ctx: TContext) => void;
36
36
  requestNextDispatchAnnouncement: () => void;
37
+ cancelNextDispatchAnnouncement: () => void;
37
38
  watchdog: Queue.TelegramQueueDispatchWatchdogRuntime<TContext>;
38
39
  }
39
40
  export declare function createTelegramQueueBindingRuntime<TContext>(deps: {
@@ -58,6 +59,7 @@ export declare function createTelegramQueueBindingRuntime<TContext>(deps: {
58
59
  updateStatus: (ctx: TContext, error?: string) => void;
59
60
  sendTextReply: Queue.TelegramQueueDispatchRuntimeDeps<TContext>["sendTextReply"];
60
61
  sendUserMessage: Queue.TelegramQueueDispatchRuntimeDeps<TContext>["sendUserMessage"];
62
+ reconcileNextDispatchAnnouncementReplyOwnership?: (item: Queue.PendingTelegramTurn) => void;
61
63
  recordRuntimeEvent?: TelegramRuntimeEventRecorder;
62
64
  }): TelegramQueueBindingRuntime<TContext>;
63
65
  export interface TelegramGenerativeAppLiveSurfaceBinding {
@@ -79,6 +79,7 @@ export function createTelegramQueueBindingRuntime(deps) {
79
79
  },
80
80
  updateStatus: deps.updateStatus,
81
81
  sendTextReply: deps.sendTextReply,
82
+ reconcileNextDispatchAnnouncementReplyOwnership: deps.reconcileNextDispatchAnnouncementReplyOwnership,
82
83
  recordRuntimeEvent: deps.recordRuntimeEvent,
83
84
  ...deps.promptDispatch,
84
85
  sendUserMessage: deps.sendUserMessage,
@@ -87,6 +88,7 @@ export function createTelegramQueueBindingRuntime(deps) {
87
88
  mutation,
88
89
  dispatchNext: dispatch.dispatchNext,
89
90
  requestNextDispatchAnnouncement: dispatch.requestNextDispatchAnnouncement,
91
+ cancelNextDispatchAnnouncement: dispatch.cancelNextDispatchAnnouncement,
90
92
  watchdog: Queue.createTelegramQueueDispatchWatchdogRuntime({
91
93
  hasQueuedItems: deps.store.hasQueuedItems,
92
94
  dispatchNextQueuedTelegramTurn: dispatch.dispatchNext,
@@ -674,10 +676,7 @@ export function registerTelegramLifecycleRuntimeHooks({ pi, publicationRuntime,
674
676
  return false;
675
677
  const turn = activeTurnRuntime.get();
676
678
  const target = turn?.target ?? proactivePushTargetGetter();
677
- promptDispatchRuntime.startTypingLoop(ctx, turn?.chatId ?? target?.chatId, {
678
- target,
679
- });
680
- return true;
679
+ return (promptDispatchRuntime.startTypingLoop(ctx, turn?.chatId ?? target?.chatId, { target }) !== false);
681
680
  };
682
681
  const startActiveTurnTypingLoop = (ctx) => {
683
682
  if (uiPromptActive)
@@ -14,5 +14,4 @@ export interface TelegramBusAwareApiRuntimeDeps {
14
14
  threadId?: number;
15
15
  } | undefined;
16
16
  }
17
- export declare function createTelegramAggregateTypingActionSender(runtime: Pick<TelegramBridgeApiRuntime, "call">): (chatId: number) => Promise<unknown>;
18
17
  export declare function createTelegramBusAwareApiRuntime(deps: TelegramBusAwareApiRuntimeDeps): TelegramBridgeApiRuntime;
@@ -37,9 +37,6 @@ function markFollowerCrossTargetDelivery(body, defaultTarget) {
37
37
  function rejectTelegramDirectOwnership(method) {
38
38
  return Promise.reject(new Error(`Telegram ${method} requires direct transport ownership.`));
39
39
  }
40
- export function createTelegramAggregateTypingActionSender(runtime) {
41
- return (chatId) => runtime.call("sendChatAction", { chat_id: chatId, action: "typing" });
42
- }
43
40
  export function createTelegramBusAwareApiRuntime(deps) {
44
41
  return {
45
42
  call(method, body, options) {
@@ -8,7 +8,7 @@
8
8
  import * as Sync from "./sync.ts";
9
9
  import * as Threads from "./threads.ts";
10
10
  import { type TelegramUpdateJournalStoreOptions } from "./journal.ts";
11
- import type { TelegramLockEntry, TelegramLockState } from "./locks.ts";
11
+ import { type TelegramLockEntry, type TelegramLockState } from "./locks.ts";
12
12
  import type { TelegramQueueHandoffPayload, TelegramQueueHandoffStageResult } from "./queue.ts";
13
13
  import type { TelegramTarget } from "./target.ts";
14
14
  import { type TelegramBusAgentMessage, type TelegramBusAgentTargetSelector, type TelegramBusEnvelope, type TelegramBusForwardOwnership, type TelegramBusProtocolIdentity, type TelegramBusSocketPathSource } from "./bus.ts";
@@ -18,6 +18,7 @@ export declare const TELEGRAM_BUS_FOLLOWER_PROMOTION_GRACE_MS = 2500;
18
18
  export declare const TELEGRAM_FOLLOWER_SESSION_HANDOFF_TTL_MS = 30000;
19
19
  export declare const TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS = 30000;
20
20
  export declare const TELEGRAM_BUS_FOLLOWER_REGISTRATION_WAIT_MS = 30000;
21
+ export declare const TELEGRAM_BUS_FOLLOWER_HEARTBEAT_TIMEOUT_MS = 8000;
21
22
  export declare const TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_ATTEMPTS: number;
22
23
  export declare const TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_DELAY_MS: number;
23
24
  export interface TelegramFollowerSessionHandoff {
@@ -170,6 +171,7 @@ export interface TelegramBusFollowerRegistrationRuntimeDeps<TContext extends {
170
171
  registrationRetryAttempts?: number;
171
172
  registrationRetryDelayMs?: number;
172
173
  heartbeatMs?: number;
174
+ heartbeatTimeoutMs?: number;
173
175
  recordRuntimeEvent?: (category: string, error: unknown, details?: Record<string, unknown>) => void;
174
176
  onHeartbeatFailure?: (error: unknown, ctx: TContext) => Promise<void> | void;
175
177
  onRegistered?: (ctx: TContext) => Promise<void> | void;
@@ -9,6 +9,7 @@ import { basename } from "node:path";
9
9
  import * as Sync from "./sync.js";
10
10
  import * as Threads from "./threads.js";
11
11
  import { parseTelegramUpdateJournalQueueOwner } from "./journal.js";
12
+ import { TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS, } from "./locks.js";
12
13
  import { isTelegramApiMethodRetrySafe, TelegramApiCommitUnknownError, TelegramApiStaleTargetError, } from "./telegram-api.js";
13
14
  import { createTelegramBusFollowerDeliveryIdentity, createTelegramBusFollowerTargetController, createTelegramBusForeignOwnedUpdateForwarder, createTelegramBusLocalServer, createTelegramBusRequestIdFactory, createUnauthorizedBusAck, getTelegramBusProtocolCompatibility, getTelegramBusSocketPath, hasTelegramBusCapability, isTelegramBusEnvelopeAuthorized, resolveTelegramBusSocketPath, sendTelegramBusLocalEnvelope, TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE, TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT, } from "./bus.js";
14
15
  import { getTelegramBusTransportRetryPolicy, TELEGRAM_BUS_REGISTRATION_RETRY, } from "./bus-transport.js";
@@ -17,6 +18,7 @@ export const TELEGRAM_BUS_FOLLOWER_PROMOTION_GRACE_MS = 2_500;
17
18
  export const TELEGRAM_FOLLOWER_SESSION_HANDOFF_TTL_MS = 30_000;
18
19
  export const TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS = 30_000;
19
20
  export const TELEGRAM_BUS_FOLLOWER_REGISTRATION_WAIT_MS = 30_000;
21
+ export const TELEGRAM_BUS_FOLLOWER_HEARTBEAT_TIMEOUT_MS = TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS;
20
22
  export const TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_ATTEMPTS = TELEGRAM_BUS_REGISTRATION_RETRY.attempts;
21
23
  export const TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_DELAY_MS = TELEGRAM_BUS_REGISTRATION_RETRY.delayMs;
22
24
  const TELEGRAM_FOLLOWER_SESSION_HANDOFF_KEY = "__piTelegramFollowerSessionHandoff";
@@ -920,6 +922,9 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
920
922
  const getNowMs = deps.getNowMs ?? Date.now;
921
923
  const getPid = deps.getPid ?? (() => process.pid);
922
924
  const heartbeatMs = deps.heartbeatMs ?? 1000;
925
+ const heartbeatTimeoutMs = deps.heartbeatTimeoutMs ??
926
+ deps.timeoutMs ??
927
+ TELEGRAM_BUS_FOLLOWER_HEARTBEAT_TIMEOUT_MS;
923
928
  const registrationTimeoutMs = deps.registrationTimeoutMs ?? deps.timeoutMs ?? 30000;
924
929
  const registrationRetryAttempts = deps.registrationRetryAttempts ??
925
930
  TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_ATTEMPTS;
@@ -979,7 +984,7 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
979
984
  try {
980
985
  const response = await sendTelegramBusLocalEnvelope({
981
986
  socketPath: leaderSocketPath,
982
- timeoutMs: deps.timeoutMs,
987
+ timeoutMs: heartbeatTimeoutMs,
983
988
  retry: getTelegramBusTransportRetryPolicy({
984
989
  endpoint: leaderSocketPath,
985
990
  operation: "operation",
@@ -210,6 +210,7 @@ export interface TelegramCommandActionDeps<TMessage, TContext> {
210
210
  export interface TelegramStopCommandDeps {
211
211
  hasAbortHandler: () => boolean;
212
212
  clearPendingModelSwitch: () => void;
213
+ cancelNextTransitionAnnouncements?: () => void;
213
214
  clearQueuedTelegramItems: () => number;
214
215
  setFoldQueuedPromptsIntoHistory: (fold: boolean) => void;
215
216
  abortCurrentTurn: () => void;
@@ -392,6 +393,8 @@ export interface TelegramCommandRuntimeDeps<TMessage extends TelegramCommandRunt
392
393
  isContextActive?: (ctx: TContext) => boolean;
393
394
  dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
394
395
  requestNextDispatchAnnouncement?: () => void;
396
+ markActiveTurnNextAbortAnnouncement?: () => boolean;
397
+ cancelNextTransitionAnnouncements?: () => void;
395
398
  requestDeferredDispatchNextQueuedTelegramTurn?: (dispatch: (ctx: TContext) => void) => void;
396
399
  startTypingLoop?: (ctx: TContext, chatId?: number, options?: {
397
400
  target?: {
@@ -504,6 +507,7 @@ export declare function handleTelegramAbortCommand(deps: {
504
507
  hasAbortHandler: () => boolean;
505
508
  hasActiveTelegramTurn: () => boolean;
506
509
  clearPendingModelSwitch: () => void;
510
+ cancelNextTransitionAnnouncements?: () => void;
507
511
  abortCurrentTurn: () => void;
508
512
  setFoldQueuedPromptsIntoHistory: (fold: boolean) => void;
509
513
  updateStatus: () => void;
@@ -519,6 +523,7 @@ export declare function handleTelegramNextCommand(deps: {
519
523
  abortCurrentTurn: () => void;
520
524
  dispatchNextQueuedTurn: () => void;
521
525
  requestNextDispatchAnnouncement?: () => void;
526
+ markActiveTurnNextAbortAnnouncement?: () => boolean;
522
527
  clearFoldForDispatch: () => void;
523
528
  updateStatus: () => void;
524
529
  sendTextReply: (text: string, options?: {
@@ -631,6 +631,7 @@ function formatTelegramQueuedTurnCount(count) {
631
631
  }
632
632
  export async function handleTelegramStopCommand(deps) {
633
633
  deps.clearPendingModelSwitch();
634
+ deps.cancelNextTransitionAnnouncements?.();
634
635
  const clearedCount = deps.clearQueuedTelegramItems();
635
636
  deps.setFoldQueuedPromptsIntoHistory(false);
636
637
  if (!deps.hasAbortHandler()) {
@@ -651,6 +652,7 @@ export async function handleTelegramStopCommand(deps) {
651
652
  }
652
653
  export async function handleTelegramAbortCommand(deps) {
653
654
  deps.clearPendingModelSwitch();
655
+ deps.cancelNextTransitionAnnouncements?.();
654
656
  if (!deps.hasAbortHandler()) {
655
657
  await deps.sendTextReply(formatTelegramInformationHeading("💤", "No active turn."), { parseMode: "HTML" });
656
658
  return;
@@ -669,6 +671,7 @@ export async function handleTelegramNextCommand(deps) {
669
671
  if (!deps.isIdle() && deps.hasAbortHandler()) {
670
672
  deps.clearFoldForDispatch();
671
673
  deps.requestNextDispatchAnnouncement?.();
674
+ deps.markActiveTurnNextAbortAnnouncement?.();
672
675
  deps.abortCurrentTurn();
673
676
  deps.updateStatus();
674
677
  return;
@@ -934,6 +937,9 @@ export function createTelegramCommandHandlerTargetRuntime(deps) {
934
937
  updateStatus: deps.updateStatus,
935
938
  isContextActive: deps.isContextActive,
936
939
  dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
940
+ requestNextDispatchAnnouncement: deps.requestNextDispatchAnnouncement,
941
+ markActiveTurnNextAbortAnnouncement: deps.markActiveTurnNextAbortAnnouncement,
942
+ cancelNextTransitionAnnouncements: deps.cancelNextTransitionAnnouncements,
937
943
  startTypingLoop: deps.startTypingLoop,
938
944
  stopTypingLoop: deps.stopTypingLoop,
939
945
  enqueueContinueTurn: deps.enqueueContinueTurn,
@@ -1039,6 +1045,7 @@ async function handleTelegramCommandRuntime(commandName, message, ctx, deps, com
1039
1045
  await handleTelegramStopCommand({
1040
1046
  hasAbortHandler: deps.hasAbortHandler,
1041
1047
  clearPendingModelSwitch: deps.clearPendingModelSwitch,
1048
+ cancelNextTransitionAnnouncements: deps.cancelNextTransitionAnnouncements,
1042
1049
  clearQueuedTelegramItems: () => deps.clearQueuedTelegramItems(commandCtx),
1043
1050
  setFoldQueuedPromptsIntoHistory: deps.setFoldQueuedPromptsIntoHistory,
1044
1051
  abortCurrentTurn: deps.abortCurrentTurn,
@@ -1085,6 +1092,7 @@ async function handleTelegramCommandRuntime(commandName, message, ctx, deps, com
1085
1092
  hasAbortHandler: deps.hasAbortHandler,
1086
1093
  hasActiveTelegramTurn: deps.hasActiveTelegramTurn,
1087
1094
  clearPendingModelSwitch: deps.clearPendingModelSwitch,
1095
+ cancelNextTransitionAnnouncements: deps.cancelNextTransitionAnnouncements,
1088
1096
  abortCurrentTurn: deps.abortCurrentTurn,
1089
1097
  setFoldQueuedPromptsIntoHistory: deps.setFoldQueuedPromptsIntoHistory,
1090
1098
  updateStatus: updateStatusFor(commandCtx),
@@ -1100,6 +1108,7 @@ async function handleTelegramCommandRuntime(commandName, message, ctx, deps, com
1100
1108
  abortCurrentTurn: deps.abortCurrentTurn,
1101
1109
  dispatchNextQueuedTurn: () => deps.dispatchNextQueuedTelegramTurn(commandCtx),
1102
1110
  requestNextDispatchAnnouncement: deps.requestNextDispatchAnnouncement,
1111
+ markActiveTurnNextAbortAnnouncement: deps.markActiveTurnNextAbortAnnouncement,
1103
1112
  clearFoldForDispatch: () => deps.setFoldQueuedPromptsIntoHistory(false),
1104
1113
  updateStatus: updateStatusFor(commandCtx),
1105
1114
  sendTextReply: sendReplyFor(nextMessage),
@@ -514,7 +514,6 @@ export default function (pi) {
514
514
  typing,
515
515
  getDefaultChatId: proactivePushChatIdGetter,
516
516
  sendTypingAction,
517
- sendAggregateTypingAction: BusApi.createTelegramAggregateTypingActionSender(telegramApiRuntime),
518
517
  updateStatus,
519
518
  isContextActive: telegramSessionContextStore.isCurrent,
520
519
  getTransportAuthority() {
@@ -642,7 +641,7 @@ export default function (pi) {
642
641
  editMessageText: editTelegramMessageText,
643
642
  },
644
643
  });
645
- const { mutation: queueMutationRuntime, dispatchNext: dispatchNextQueuedTelegramTurn, requestNextDispatchAnnouncement, watchdog: queueDispatchWatchdogRuntime, } = Bindings.createTelegramQueueBindingRuntime({
644
+ const { mutation: queueMutationRuntime, dispatchNext: dispatchNextQueuedTelegramTurn, requestNextDispatchAnnouncement, cancelNextDispatchAnnouncement, watchdog: queueDispatchWatchdogRuntime, } = Bindings.createTelegramQueueBindingRuntime({
646
645
  store: telegramQueueStore,
647
646
  queue,
648
647
  lifecycle,
@@ -656,6 +655,9 @@ export default function (pi) {
656
655
  updateStatus,
657
656
  sendTextReply,
658
657
  sendUserMessage,
658
+ reconcileNextDispatchAnnouncementReplyOwnership(item) {
659
+ Replies.preserveTransportReplyDedupOnNextReset(item.chatId, item.replyToMessageId, item.target);
660
+ },
659
661
  recordRuntimeEvent,
660
662
  });
661
663
  const { finalizeMarkdownPreview, preparePreviewDelivery } = Outbound.createTelegramOutboundTextPreviewRuntime({
@@ -887,6 +889,7 @@ export default function (pi) {
887
889
  isContextActive: telegramSessionContextStore.isCurrent,
888
890
  dispatchNextQueuedTelegramTurn,
889
891
  requestNextDispatchAnnouncement,
892
+ cancelNextDispatchAnnouncement,
890
893
  requestDeferredDispatchNextQueuedTelegramTurn: deferredQueueDispatchRuntime.request,
891
894
  hasDeferredDispatchContext: deferredQueueDispatchRuntime.isBound,
892
895
  startTypingLoop: promptDispatchRuntime.startTypingLoop,
@@ -1349,7 +1352,7 @@ export default function (pi) {
1349
1352
  loadConfig: configStore.load,
1350
1353
  setQueuedItems: telegramQueueStore.setQueuedItems,
1351
1354
  setCurrentModel: currentModelRuntime.set,
1352
- setPendingModelSwitch: pendingModelSwitchStore.set,
1355
+ setPendingModelSwitch: modelSwitchController.clearPendingSwitch,
1353
1356
  syncCounters: queue.syncCounters,
1354
1357
  syncFlags: lifecycle.syncFlags,
1355
1358
  bindDeferredDispatchContext: deferredQueueDispatchRuntime.bind,
@@ -4,7 +4,7 @@
4
4
  * Owns model-menu state, scoped model pages, model callback planning, and model-menu message rendering
5
5
  */
6
6
  import type { TelegramInlineKeyboardMarkup } from "./keyboard.ts";
7
- import { type MenuModel, type ScopedTelegramModel, type ThinkingLevel } from "./model.ts";
7
+ import { type MenuModel, type ScopedTelegramModel, type TelegramModelSwitchContinuationSource, type ThinkingLevel } from "./model.ts";
8
8
  export type TelegramModelScope = "all" | "scoped";
9
9
  export interface TelegramModelMenuState<TModel extends MenuModel = MenuModel> {
10
10
  chatId: number;
@@ -93,8 +93,8 @@ export type TelegramModelMenuCallbackDeps<TModel extends MenuModel = MenuModel>
93
93
  setModel: (model: TModel) => Promise<boolean>;
94
94
  setCurrentModel: (model: TModel) => void;
95
95
  setThinkingLevel: (level: ThinkingLevel) => void;
96
- stagePendingModelSwitch: (selection: ScopedTelegramModel<TModel>) => void;
97
- restartInterruptedTelegramTurn: (selection: ScopedTelegramModel<TModel>) => Promise<boolean> | boolean;
96
+ stagePendingModelSwitch: (selection: ScopedTelegramModel<TModel>, continuationTurn: TelegramModelSwitchContinuationSource) => void;
97
+ restartInterruptedTelegramTurn: (selection: ScopedTelegramModel<TModel>, continuationTurn: TelegramModelSwitchContinuationSource) => Promise<boolean> | boolean;
98
98
  };
99
99
  export interface TelegramModelMenuOpenDeps<TModel extends MenuModel = MenuModel> {
100
100
  isIdle: () => boolean;
@@ -506,19 +506,33 @@ export async function handleTelegramModelMenuCallbackAction(callbackQueryId, par
506
506
  if (plan.selection.thinkingLevel) {
507
507
  deps.setThinkingLevel(plan.selection.thinkingLevel);
508
508
  }
509
- await deps.updateModelMenuMessage();
509
+ const continuationTurn = {
510
+ chatId: params.state.chatId,
511
+ replyToMessageId: params.state.messageId,
512
+ target: {
513
+ chatId: params.state.chatId,
514
+ ...(params.state.threadId === undefined
515
+ ? {}
516
+ : { threadId: params.state.threadId }),
517
+ },
518
+ };
510
519
  if (plan.mode === "restart-after-tool") {
511
- deps.stagePendingModelSwitch(plan.selection);
520
+ deps.stagePendingModelSwitch(plan.selection, continuationTurn);
521
+ await deps.updateModelMenuMessage();
512
522
  await deps.answerCallbackQuery(callbackQueryId, plan.callbackText);
513
523
  return true;
514
524
  }
515
525
  if (plan.mode === "restart-now") {
516
- const restarted = await deps.restartInterruptedTelegramTurn(plan.selection);
526
+ const restarted = await deps.restartInterruptedTelegramTurn(plan.selection, continuationTurn);
527
+ await deps.updateModelMenuMessage();
517
528
  if (!restarted) {
518
529
  await deps.answerCallbackQuery(callbackQueryId, "Pi is busy. Send /abort, /next, or /stop.");
519
530
  return true;
520
531
  }
521
532
  }
533
+ else {
534
+ await deps.updateModelMenuMessage();
535
+ }
522
536
  await deps.answerCallbackQuery(callbackQueryId, plan.callbackText);
523
537
  return true;
524
538
  }
@@ -4,7 +4,7 @@
4
4
  * Owns app-menu/status state, inline UI text, and callback composition while model/thinking/queue menu details live in dedicated domains
5
5
  */
6
6
  import { type TelegramMenuMessageRuntimeDeps, type TelegramModelMenuState, type TelegramModelMenuStateBuilderContext, type TelegramModelMenuStateBuilderDeps, type TelegramReplyMarkup } from "./menu-model.ts";
7
- import { type MenuModel, type ScopedTelegramModel, type ThinkingLevel } from "./model.ts";
7
+ import { type MenuModel, type ScopedTelegramModel, type TelegramModelSwitchContinuationSource, type ThinkingLevel } from "./model.ts";
8
8
  import type { TelegramInputRichMessage } from "./telegram-api.ts";
9
9
  import { type TelegramSectionRegistry } from "./sections.ts";
10
10
  export { applyTelegramModelPageSelection, applyTelegramModelScopeSelection, buildModelMenuReplyMarkup, buildModelPageMenuReplyMarkup, buildTelegramModelCallbackPlan, buildTelegramModelMenuRenderPayload, buildTelegramModelMenuState, buildTelegramModelMenuStateRuntime, buildTelegramModelPageMenuRenderPayload, createTelegramModelMenuRuntime, createTelegramModelMenuStateBuilder, formatScopedModelButtonText, getModelMenuItems, getStoredTelegramModelMenuState, getTelegramModelMenuPage, getTelegramModelSelection, handleTelegramModelMenuCallbackAction, MODEL_MENU_TITLE, MODEL_PAGE_MENU_TITLE, openTelegramModelMenu, pruneStoredTelegramModelMenus, resolveCachedTelegramModelMenuInputs, sendTelegramModelMenuMessage, storeTelegramModelMenuState, TELEGRAM_MODEL_PAGE_SIZE, updateTelegramModelMenuMessage, } from "./menu-model.ts";
@@ -49,14 +49,13 @@ export interface TelegramMenuCallbackRuntimeDeps<TContext, TModel extends MenuMo
49
49
  updateSettingsMenuMessage?: (state: TelegramModelMenuState<TModel>, ctx: TContext) => Promise<void>;
50
50
  answerCallbackQuery: (callbackQueryId: string, text?: string) => Promise<void>;
51
51
  isIdle: (ctx: TContext) => boolean;
52
- hasActiveTelegramTurn: () => boolean;
53
52
  hasAbortHandler: () => boolean;
54
53
  hasActiveToolExecutions: () => boolean;
55
54
  persistScopedModelPatterns?: (patterns: string[], ctx: TContext) => Promise<void>;
56
55
  setModel: (model: TModel) => Promise<boolean>;
57
56
  setCurrentModel: (model: TModel, ctx: TContext) => void;
58
- stagePendingModelSwitch: (selection: ScopedTelegramModel<TModel>, ctx: TContext) => void;
59
- restartInterruptedTelegramTurn: (selection: ScopedTelegramModel<TModel>, ctx: TContext) => Promise<boolean> | boolean;
57
+ stagePendingModelSwitch: (selection: ScopedTelegramModel<TModel>, ctx: TContext, continuationTurn: TelegramModelSwitchContinuationSource) => void;
58
+ restartInterruptedTelegramTurn: (selection: ScopedTelegramModel<TModel>, ctx: TContext, continuationTurn: TelegramModelSwitchContinuationSource) => Promise<boolean> | boolean;
60
59
  sectionRegistry?: TelegramSectionRegistry;
61
60
  editInteractiveMessage?: (chatId: number, messageId: number, text: string, mode: "markdown" | "html" | "plain", replyMarkup: TelegramReplyMarkup) => Promise<void>;
62
61
  sendInteractiveMessage?: (chatId: number, text: string, mode: "markdown" | "html" | "plain", replyMarkup: TelegramReplyMarkup, options?: {
@@ -133,14 +132,13 @@ export interface TelegramMenuCallbackRuntimeAdapterDeps<TContext, TModel extends
133
132
  updateSettingsMenuMessage?: (state: TelegramModelMenuState<TModel>, ctx: TContext) => Promise<void>;
134
133
  answerCallbackQuery: (callbackQueryId: string, text?: string) => Promise<void>;
135
134
  isIdle: (ctx: TContext) => boolean;
136
- hasActiveTelegramTurn: () => boolean;
137
135
  hasAbortHandler: () => boolean;
138
136
  getActiveToolExecutions: () => number;
139
137
  persistScopedModelPatterns?: (patterns: string[], ctx: TContext) => Promise<void>;
140
138
  setModel: (model: TModel) => Promise<boolean>;
141
139
  setCurrentModel: (model: TModel, ctx: TContext) => void;
142
- stagePendingModelSwitch: (selection: ScopedTelegramModel<TModel>, ctx: TContext) => void;
143
- restartInterruptedTelegramTurn: (selection: ScopedTelegramModel<TModel>, ctx: TContext) => Promise<boolean> | boolean;
140
+ stagePendingModelSwitch: (selection: ScopedTelegramModel<TModel>, ctx: TContext, continuationTurn: TelegramModelSwitchContinuationSource) => void;
141
+ restartInterruptedTelegramTurn: (selection: ScopedTelegramModel<TModel>, ctx: TContext, continuationTurn: TelegramModelSwitchContinuationSource) => Promise<boolean> | boolean;
144
142
  sectionRegistry?: TelegramSectionRegistry;
145
143
  editInteractiveMessage?: (chatId: number, messageId: number, text: string, mode: "markdown" | "html" | "plain", replyMarkup: TelegramReplyMarkup) => Promise<void>;
146
144
  sendInteractiveMessage?: (chatId: number, text: string, mode: "markdown" | "html" | "plain", replyMarkup: TelegramReplyMarkup, options?: {
package/dist/lib/menu.js CHANGED
@@ -99,7 +99,6 @@ export function createTelegramMenuCallbackHandlerForContext(deps) {
99
99
  updateSettingsMenuMessage: deps.updateSettingsMenuMessage,
100
100
  answerCallbackQuery: deps.answerCallbackQuery,
101
101
  isIdle: deps.isIdle,
102
- hasActiveTelegramTurn: deps.hasActiveTelegramTurn,
103
102
  hasAbortHandler: deps.hasAbortHandler,
104
103
  hasActiveToolExecutions: () => deps.getActiveToolExecutions() > 0,
105
104
  persistScopedModelPatterns: deps.persistScopedModelPatterns,
@@ -228,7 +227,7 @@ export async function handleTelegramMenuCallbackRuntime(query, ctx, deps) {
228
227
  activeModel: deps.getActiveModel(ctx),
229
228
  currentThinkingLevel: deps.getThinkingLevel(),
230
229
  isIdle: deps.isIdle(ctx),
231
- canRestartBusyRun: deps.hasActiveTelegramTurn() && deps.hasAbortHandler(),
230
+ canRestartBusyRun: deps.hasAbortHandler(),
232
231
  hasActiveToolExecutions: deps.hasActiveToolExecutions(),
233
232
  }, {
234
233
  updateModelMenuMessage: () => deps.updateModelMenuMessage(state, ctx),
@@ -243,10 +242,10 @@ export async function handleTelegramMenuCallbackRuntime(query, ctx, deps) {
243
242
  deps.setThinkingLevel(level);
244
243
  deps.updateStatus(ctx);
245
244
  },
246
- stagePendingModelSwitch: (selection) => {
247
- deps.stagePendingModelSwitch(selection, ctx);
245
+ stagePendingModelSwitch: (selection, continuationTurn) => {
246
+ deps.stagePendingModelSwitch(selection, ctx, continuationTurn);
248
247
  },
249
- restartInterruptedTelegramTurn: (selection) => deps.restartInterruptedTelegramTurn(selection, ctx),
248
+ restartInterruptedTelegramTurn: (selection, continuationTurn) => deps.restartInterruptedTelegramTurn(selection, ctx, continuationTurn),
250
249
  });
251
250
  }
252
251
  catch (error) {
@@ -52,14 +52,13 @@ export interface PendingModelSwitchStore<TSelection> {
52
52
  }
53
53
  export interface TelegramInFlightModelSwitchState {
54
54
  isIdle: boolean;
55
- hasActiveTelegramTurn: boolean;
56
55
  hasAbortHandler: boolean;
57
56
  }
58
57
  export declare function createPendingModelSwitchStore<TSelection>(): PendingModelSwitchStore<TSelection>;
59
- export declare function canRestartTelegramTurnForModelSwitch(state: TelegramInFlightModelSwitchState): boolean;
58
+ export declare function canRestartAgentRunForTelegramModelSwitch(state: TelegramInFlightModelSwitchState): boolean;
60
59
  export declare function shouldTriggerPendingTelegramModelSwitchAbort(state: {
61
60
  hasPendingModelSwitch: boolean;
62
- hasActiveTelegramTurn: boolean;
61
+ hasContinuationTurn: boolean;
63
62
  hasAbortHandler: boolean;
64
63
  activeToolExecutions: number;
65
64
  }): boolean;
@@ -70,8 +69,9 @@ export declare function restartTelegramModelSwitchContinuation<TTurn, TSelection
70
69
  queueContinuation: (turn: TTurn, selection: TSelection) => void;
71
70
  }): boolean;
72
71
  export declare function buildTelegramModelSwitchContinuationText<TModel extends MenuModel>(telegramPrefix: string, model: TModel, thinkingLevel?: ScopedTelegramModel<TModel>["thinkingLevel"]): string;
72
+ export type TelegramModelSwitchContinuationSource = Pick<PendingTelegramTurn, "chatId" | "replyToMessageId" | "target">;
73
73
  export declare function buildTelegramModelSwitchContinuationTurn<TModel extends MenuModel>(options: {
74
- turn: Pick<PendingTelegramTurn, "chatId" | "replyToMessageId" | "target">;
74
+ turn: TelegramModelSwitchContinuationSource;
75
75
  selection: ScopedTelegramModel<TModel>;
76
76
  telegramPrefix?: string;
77
77
  queueOrder: number;
@@ -82,22 +82,22 @@ export declare function createTelegramModelSwitchContinuationTurnBuilder<TModel
82
82
  allocateItemOrder: () => number;
83
83
  allocateControlOrder: () => number;
84
84
  }): (options: {
85
- turn: Pick<PendingTelegramTurn, "chatId" | "replyToMessageId" | "target">;
85
+ turn: TelegramModelSwitchContinuationSource;
86
86
  selection: ScopedTelegramModel<TModel>;
87
87
  }) => PendingTelegramTurn;
88
88
  export declare function createTelegramModelSwitchContinuationQueue<TContext, TSelection extends ScopedTelegramModel>(deps: {
89
89
  createContinuationTurn: (options: {
90
- turn: Pick<PendingTelegramTurn, "chatId" | "replyToMessageId" | "target">;
90
+ turn: TelegramModelSwitchContinuationSource;
91
91
  selection: TSelection;
92
92
  }) => PendingTelegramTurn;
93
93
  appendQueuedItem: (item: PendingTelegramTurn, ctx: TContext) => void;
94
- }): (turn: PendingTelegramTurn, selection: TSelection, ctx: TContext) => void;
94
+ }): (turn: TelegramModelSwitchContinuationSource, selection: TSelection, ctx: TContext) => void;
95
95
  export declare function createTelegramModelSwitchContinuationQueueRuntime<TContext, TSelection extends ScopedTelegramModel>(deps: {
96
96
  telegramPrefix?: string;
97
97
  allocateItemOrder: () => number;
98
98
  allocateControlOrder: () => number;
99
99
  appendQueuedItem: (item: PendingTelegramTurn, ctx: TContext) => void;
100
- }): (turn: PendingTelegramTurn, selection: TSelection, ctx: TContext) => void;
100
+ }): (turn: TelegramModelSwitchContinuationSource, selection: TSelection, ctx: TContext) => void;
101
101
  export interface TelegramModelSwitchControllerDeps<TContext, TSelection> {
102
102
  isIdle: (ctx: TContext) => boolean;
103
103
  getPendingModelSwitch: () => TSelection | undefined;
@@ -106,16 +106,16 @@ export interface TelegramModelSwitchControllerDeps<TContext, TSelection> {
106
106
  getAbortHandler: () => (() => void) | undefined;
107
107
  hasAbortHandler: () => boolean;
108
108
  getActiveToolExecutions: () => number;
109
- queueContinuation: (turn: PendingTelegramTurn, selection: TSelection, ctx: TContext) => void;
109
+ queueContinuation: (turn: TelegramModelSwitchContinuationSource, selection: TSelection, ctx: TContext) => void;
110
110
  updateStatus: (ctx: TContext) => void;
111
111
  }
112
112
  export interface TelegramModelSwitchController<TContext, TSelection> {
113
113
  canOfferInFlightSwitch: (ctx: TContext) => boolean;
114
- stagePendingSwitch: (selection: TSelection, ctx: TContext) => void;
114
+ stagePendingSwitch: (selection: TSelection, ctx: TContext, continuationTurn?: TelegramModelSwitchContinuationSource) => void;
115
115
  clearPendingSwitch: () => void;
116
- queueContinuation: (turn: PendingTelegramTurn, selection: TSelection, ctx: TContext) => void;
116
+ queueContinuation: (turn: TelegramModelSwitchContinuationSource, selection: TSelection, ctx: TContext) => void;
117
117
  triggerPendingAbort: (ctx: TContext) => boolean;
118
- restartInterruptedTurn: (selection: TSelection, ctx: TContext) => boolean;
118
+ restartInterruptedTurn: (selection: TSelection, ctx: TContext, continuationTurn?: TelegramModelSwitchContinuationSource) => boolean;
119
119
  }
120
120
  export interface TelegramModelSwitchControllerRuntimeDeps<TContext, TSelection extends ScopedTelegramModel> extends Omit<TelegramModelSwitchControllerDeps<TContext, TSelection>, "queueContinuation"> {
121
121
  telegramPrefix?: string;