@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/AGENTS.md CHANGED
@@ -60,6 +60,7 @@ Use the relevant local skill before non-trivial work in its domain. Keep skill o
60
60
  ### 4.1 Flat Domain DAG
61
61
 
62
62
  - Cohesive domains live as flat `/lib/*.ts` modules whose local import graph is acyclic.
63
+ - `In-memory lifecycles`: Use pure transitions and a domain-owned effect shell when competing asynchronous sources, generation/cancellation fencing or timers justify the pattern. The shared `machine` leaf owns runner mechanics only; state, events, effects and authority stay with the domain. Durable admission, custody, Workspace and IPC retain their transactional contracts.
63
64
  - `lib/extension.ts` constructs high-level runtimes and wires live ports. Domain policy, mutable state, sequencing, identity, retries, normalization, and lifecycle recovery belong to the owning `/lib` module.
64
65
  - During refactoring, add a domain only for demonstrated cross-domain reuse or a concrete dependency-direction/cycle problem. Prefer an existing cohesive owner; a closed helper cluster, independent tests or file length alone is insufficient. An acyclic validator result does not justify a new boundary. Do not atomize cohesive modules or create one-use wrappers merely to shrink `index.ts`.
65
66
  - `bindings` owns Pi-facing registration and narrow cross-domain assembly; it may connect established ports but must not absorb routing, rendering, transport, or mutable policy.
@@ -85,7 +86,7 @@ Use the relevant local skill before non-trivial work in its domain. Keep skill o
85
86
  - Admission is journal-first: validate and persist the complete `getUpdates` response before one monotonic offset commit, then signal an independent worker without awaiting semantic execution. Missing cursor with a non-empty journal, malformed/foreign authority, or capacity exhaustion fails closed. “Durable” means process-crash recovery after atomic rename, not unflushed host/kernel/filesystem/device/power-loss survival.
86
87
  - Storage cutovers must reconcile actual consumer locations before correcting path adapters. Never normalize a relative historical reference into new authority or treat equal reference strings / empty canonical storage as source completeness. Exact-path preflight is lexical only; physical identity, historical coverage, writer closure and migration remain separate proofs.
87
88
  - Workspace mutations acquire cross-process admission before their shared process-local gate and hold it through asynchronous API work and durable settlement. Topic lifecycle, complete unbound/reroute target handling, manual disconnect, and session-restart cleanup use profile-wide scope; either retained retirement-fence phase rejects them before state access. Cleanup admission spans intent publication, target mutation, persistence, and successful transport release. Explicit manual disconnect may stop only its captured local transport after cleanup admission/effects fail; it must not bypass Thread mutation fences, release a replacement connection, claim unconfirmed deletion, or retry an uncertain stop. Session-restart cleanup retains its admission-required behavior. Detached reconciliation that mutates Thread state must reacquire fresh profile admission through the same gate; it cannot inherit a caller lease that ended before its timer runs. A live operation ID has one process-local caller: concurrent reuse is rejected before lease acquisition, while retry after the caller exits may resume exact durable authority.
88
- - Workspace retirement is capacity-pressure-only. Elapsed time and heartbeat silence never trigger deletion; only complete `A`–`Z` exhaustion may propose the oldest continuously proven inactive, fully unprotected binding. Exact deletion, durable retirement, and fence completion precede slot reuse. Authorized demand-driven rotation retries one failed fresh allocation after retirement; restore-only follower startup never evicts. Release ordinary registration/provisioning leases before acquiring the destructive fence, retain the shared mutation gate across retirement, and validate the exact permit immediately before one non-retried deletion. Persist an exact method/target-matched rejection before withdrawing its intent; release that fence only after durable withdrawal, retaining the binding. A later attempt requires fresh operation authority. Protection reads must never repair, quarantine, or reset journals. Non-destructive owner detachment must atomically retain one exact Workspace binding and its letter while removing only its uniquely matched owner record and stamping first inactivity; it never manufactures Thread-deletion evidence or clears accepted work. During authenticated follower registration, the exact Workspace claim owns the letter: a mismatched retained target record is repaired to that claim before binding commit, while any unrelated binding that owns the stale letter remains untouched. Retained prune observations are bounded, non-routing and registration/profile/epoch/runtime-fenced. One unfinished preservation operation retains its admission identity across fresh-PID-proof retries; it can never become deletion authority. Leader quit requires completed delivery/polling/worker teardown under the captured session generation, profile and epoch; reload/new/resume/fork never establish inactivity. Unknown deletion outcomes retain their fence; only confirmed durable completion permits reuse.
89
+ - Workspace retirement is capacity-pressure-only. Elapsed time and heartbeat silence never trigger deletion; only complete `A`–`Z` exhaustion may propose the oldest continuously proven inactive, fully unprotected binding. Exact deletion, durable retirement, and fence completion precede slot reuse. Authorized demand-driven rotation retries one failed fresh allocation after retirement; restore-only follower startup never evicts. Release ordinary registration/provisioning leases before acquiring the destructive fence, retain the shared mutation gate across retirement, and validate the exact permit immediately before one non-retried deletion. Persist an exact method/target-matched rejection before withdrawing its intent; release that fence only after durable withdrawal, retaining the binding. A later attempt requires fresh operation authority. Protection reads must never repair, quarantine, or reset journals. Non-destructive owner detachment must atomically retain one exact Workspace binding and its letter while removing only its uniquely matched owner record and stamping first inactivity; it never manufactures Thread-deletion evidence or clears accepted work. During authenticated follower registration, a mismatched retained target record is repaired to the exact Workspace claim before binding commit; if same-target commit adopts the retained binding's slot, its returned binding then owns the registration result and owner-record slot. Unrelated bindings that own a stale or provisional letter remain untouched. Same-target re-keying retains historical journal keys and exact source tuples without upgrading unknown completeness. Retained prune observations are bounded, non-routing and registration/profile/epoch/runtime-fenced. One unfinished preservation operation retains its admission identity across fresh-PID-proof retries; it can never become deletion authority. Leader quit requires completed delivery/polling/worker teardown under the captured session generation, profile and epoch; reload/new/resume/fork never establish inactivity. Unknown deletion outcomes retain their fence; only confirmed durable completion permits reuse.
89
90
  - Foreign forwarding settles as `accepted`, `retryable`, or `terminal-rejected`. Only an authenticated acknowledgement carrying the expected `deliveryId` and `sourceUpdateId` releases leader journal authority. Negative, missing, stale, mismatched, or capacity-failed settlement remains durable; callback error answers are side effects only. Exception: the operator-approved fixed chooser deadline may discard its exact unclaimed donor pending source without a body archive or delivery-success claim. Accepted recipient queue/work and bound Threads are untouched; old donor controls and late reports cannot replay it. The lifetime and temporary cleanup contract lives in `docs/architecture.md`.
90
91
  - A forwarding delivery id is stable across registration replacement and derives from envelope kind, source `update_id`, and stable recipient binding. Runtime instance and registration generation remain separate attempt fences. Persisted message ownership carries the stable binding so replay can rebind only to its current authenticated registration.
91
92
  - A queued receipt persists its acquiring runtime instance, OS pid/process-birth identity, session generation, acquisition id, and acquisition time. Only exact authority may settle or discard it. Cached presence is not current execution proof: prepared custody must revalidate the exact queued owner/group without recovery and refuse offers or uncertain reads. Completion requires an exact removal acknowledgement, never merely `!ready`; a retained acknowledgement permits local cleanup only, not replay. Same-process session replacement may reconstruct the claim and the original process may settle after transport ownership moves; a foreign process may neither replay nor settle it through generic removal or a copied acquisition id.
@@ -102,8 +103,9 @@ Use the relevant local skill before non-trivial work in its domain. Keep skill o
102
103
  - `/stop`, `/abort`, `/next`, and `/continue` respectively reset+abort, abort while preserving queue, force the next turn, and enqueue a control-lane continuation. A negative reaction to an original standalone `/continue` settles and removes only its exact waiting source-addressed continuation; positive reactions do not change its control lane. Protect the Pi-owned pending head and already-started work, preserve other queue positions, and never address synthetic model continuations by their reply anchor. Settlement failure retains the 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.
103
104
  - 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.
104
105
  - 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.
106
+ - In Threaded Mode, All is service/input-only. New output, previews, typing and routing UI require a concrete Thread; neither implicit fallback nor explicit aggregate delivery may write to All. Unavailable routing authority retains input without an All notice or silent settlement. Classic DM and explicit channel/business destinations remain separate; historical All output is not auto-cleaned.
105
107
  - 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.
106
- - `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.
108
+ - `preview` owns streaming lifecycle only, not assistant rendering. Finalization waits for the captured generation's preceding preview work, never later drafts blocked by that publication, and must not issue pre/post-final draft-clear calls that create transient Telegram draft UI. Reset/invalidation detaches predecessor dependencies; late settlements cannot mutate replacement state. Turns that already answer as one atomic reply (voice replies, Guest Mode queries) never stream previews.
107
109
  - 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.
108
110
  - 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.
109
111
  - 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 action labels use `emoji + space + text`; emoji-free action text is only a reasoned no-semantic-marker fallback. State/value controls follow the [button control hierarchy](./docs/ui-style.md#button-control-hierarchy), not the action-label grammar. 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.
package/BACKLOG.md CHANGED
@@ -2,4 +2,4 @@
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
- No open work.
5
+ No open tasks.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,27 @@
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.55.0: Native Restore and fenced lifecycles
8
+
9
+ - `Restore cleanup`: Live Restore binds the new tab and decides old-tab cleanup before native leader/follower inputs run. Protected work defers cleanup; session replacement carries no deletion grant, and unknown deletion never repeats. Follower execution targets the rebound tab without rewriting journal originals or quoting the former tab. Exact donor ACKs seal routing clocks and retire the menu. Reader/execution capabilities, source custody and detached replies stay separate; busy hints coalesce.
10
+ - `Runtime state`: In-memory lifecycles now use isolated pure TypeScript transitions and domain-owned effect shells. A shared leaf runner drives preview, typing, model switching, grouped input, compaction and follower recovery without adding a runtime dependency; durable admission, custody, Workspace and IPC retain their existing transactional owners.
11
+ - `Preview ordering`: A publication waits only for its preceding draft and publications, never a later draft that waits for it. New turns and sessions detach old preview dependencies, and late draft, timer or publication settlements cannot unblock or overwrite replacement work. Final replies still drain their own issued draft without post-final clearing.
12
+ - `Model switching`: Restarting or clearing a switch now consumes its staged model and continuation together, preventing an old tool settlement from aborting the continuation again. Synchronous nested controller calls retain the outer continuation context and abort handler.
13
+ - `Grouped input`: Albums and split text share one fenced pending-group lifecycle. Separate text batches end at a message identity rather than a mutable count, and a synchronous dispatcher failure retains the input and schedules retry instead of leaving the group permanently dispatching.
14
+ - `Typing ownership`: Queue dispatch preserves its exact Thread; Threaded Mode refuses missing targets and stops Classic chat-only loops on mode changes. Context/generation and loop-owner fences guard starts, queued HTTP/IPC issuance, ticks and awaited lifecycle continuations without letting stale work claim replacement typing. Typing never replays HTTP or transport fallback after failure; follower callbacks stay local. Compaction retains its own loop and preserves agent-owned activity.
15
+ - `Reconnect lifecycle`: Follower startup restores its exact session's retained Thread after proving the former runtime PID absent and no registered peer owns it; live/unknown predecessors stay protected. Connect notices use acknowledged display titles, not provisional generated names. Manual Connect/Restore retain prepared context, read-only checks, committed slots and journal history. Stale recovery cannot change a replacement connection; stopped leader startup cannot poll or publish its endpoint.
16
+ - `Thread surfaces`: All is input-only: sends, drafts, media, forwards and activity require a concrete Thread; implicit fallback and aggregate output are refused. Routing failures retain input, not All notices. Voice/files/albums enter one routing tab. Exact Linux/macOS home displays as Home/home with normal suffixes; manual names and other modes stay unchanged. Restore publishes acknowledged titles to heartbeat/TUI without changing generated identity; late old-target title ACKs are ignored.
17
+
18
+ ## 0.54.4: Restored tabs take their Pi's name
19
+
20
+ - `Restore title`: Restoring a Pi into a routing tab ("move Pi into this tab") now renames the tab to that Pi's Thread display title, as Workspace Restore already did; previously a live-rebind Restore left the tab named `🚦 Routing`. A failed rename is recorded and never blocks the Restore.
21
+ - `Native routing tabs`: An `All` input now also adopts a generically named native tab (desktop `New Chat`) that its client created immediately before it, not only a tab named after the input (mobile). Desktop and Android clients, with user topic creation allowed or not, each end with exactly one routing tab; a client's own brief native name before the rename cannot be prevented.
22
+ - `Reclaimed leader tab`: When the leader's own tab is gone (for example after deleting every tab) and a new tab from `All` is reclaimed for it, the leader's Workspace binding now moves to that tab with its slot and name instead of gaining a second slot, and the tab is renamed to its display title instead of staying `New Chat`. Restore and labels no longer point at the deleted tab.
23
+ - `Display titles`: A Thread whose tab has no acknowledged title yet (fresh, moved or reclaimed) now shows its display-mode title, such as the directory title, in the Restore menu and after Restore, instead of falling back to its generated slot name.
24
+ - `One-shot routing menus`: Every final routing choice now deletes its menu. A live-rebind Restore left the chooser in the tab, a Workspace Restore kept it until old-Thread cleanup, which may never come, and Cancel replaced it with a notice. A restored tab stays as the Pi's Thread; a routing tab with nothing left to route is still removed. A delivered Workspace Restore answers `Restored`, not `Restore unconfirmed`, and the Restore submenu now reads `🔁 Restore a Pi into this tab:`.
25
+ - `Restore history`: An input sent from `All` now moves into the routing tab opened for it, on desktop and mobile alike: the tab starts with a silent forward of the input, its menu replies to it, and the original leaves `All` once the copy is confirmed. A Thread restored into that tab keeps the prompt it answers instead of losing it, since the Bot API files such inputs in `All` even when mobile first draws them in the tab.
26
+ - `Old tab cleanup`: After a live-rebind Restore, the old tab is deleted right after Pi finishes answering the moved input, instead of at the next paced retry, which could add up to about 30 seconds and later a full minute. Cleanup still waits for an idle session; a settled agent only wakes the waiting retry early.
27
+
7
28
  ## 0.54.3: One routing tab on mobile
8
29
 
9
30
  - `Mobile routing tab`: Sending a command or prompt from `All` in mobile Telegram no longer leaves an extra tab named after the input. The mobile client creates that native tab itself before delivering the input to `All`; the bridge now adopts it as the routing tab and renames it, instead of creating a second tab beside it. Desktop behavior is unchanged.
package/README.md CHANGED
@@ -143,7 +143,7 @@ Enable the optional capabilities the bridge needs in the [@BotFather](https://t.
143
143
  | Generative Apps | Install or explicitly replace a reviewed `.mjs` application whose generated JSON button view may mix direct `app::method` actions with ordinary model prompts. | Repeated games, controls, tutors, and adapters compile routine interaction without losing selective model interpretation, explanation, or adaptation. |
144
144
  | Callback routing | Route known callbacks to the owner extension and unknown callbacks back into Pi. | Companion extensions can build UI without polling Telegram themselves. |
145
145
  | Threaded Mode | Run one leader plus visible follower Pi instances through named private-chat threads. | One bot can host a local multi-instance Pi organism without hidden process spawning. |
146
- | Reroute and restore | Give unknown and command-created temporary threads explicit forward and replace/restore choices. | Forward removes the temporary tab; restore rebinds it and may remove only the replaced old tab after positive cleanup proofs. Blocked cleanup preserves the relocation and protected work. |
146
+ | Reroute and restore | Give text, voice, files and albums sent from `All`, plus unknown threads, explicit reroute and replace/restore choices. | Forward removes the temporary tab; restore rebinds it and may remove only the replaced old tab after positive cleanup proofs. Blocked cleanup preserves the relocation and protected work. |
147
147
  | Extension sections | Add menu sections, commands, status rows, settings, callbacks, and delivery helpers from companion extensions. | `pi-telegram` becomes a platform surface for other Pi extensions. |
148
148
  | Runtime diagnostics | Use `/telegram-status` and recent runtime events for connection, role, negotiated bus protocol/build/capabilities, separate polling and inbound-worker progress, journal depth, local/foreign queue ownership, automatic retry waits, transport, and failures. | Compatible build skew, foreign semantic authority, a healthy poller, durable backoff and an infrastructure-blocked worker remain distinguishable without hidden logs. |
149
149
  | Safety and ownership | Pair one owner, lock transport, scope targets, and reject fake terminal behavior. | Remote access remains explicit, bounded, and understandable. |
@@ -196,6 +196,8 @@ Named profile identifiers contain only lowercase ASCII letters and digits (maxim
196
196
 
197
197
  ## Main Surfaces
198
198
 
199
+ In Threaded Mode, `All` is input-only: messages enter temporary routing tabs, and Pi replies stay in bound Threads. Missing routing or delivery targets fail closed instead of sending fallback messages to All. There is no automatic cleanup of old All output. Classic DM behavior is unchanged.
200
+
199
201
  ### Operator Menu
200
202
 
201
203
  `/start` opens the Telegram-native control panel: status, prompt-template commands, model selection, thinking level, settings, queue controls, and extension sections. It is the primary Telegram UI; reaction shortcuts are secondary queue affordances.
@@ -248,14 +250,14 @@ Classic private DM mode is the base product mode. When Telegram private-chat Thr
248
250
  - When all `A`–`Z` slots are reserved, a new connection may reclaim the oldest inactive, unprotected Thread. **This deletes that Thread and all its messages**, not Pi session history or project files. Active or protected work blocks reclamation; restoring a remembered session does not evict another binding.
249
251
  - Telegram never launches hidden Pi processes.
250
252
 
251
- In Threaded Mode, open Settings → **🧵 Thread display** to choose `letters` (default), `names`, `directory-title`, or `directory-snake` for this bot profile; a retained **directories** value remains available as an unchanged legacy presentation. Fresh tabs are created with the active mode's title instead of being visibly renamed afterward. Telegram tab titles, Pi terminal status, live Thread choosers/notices, prompt attribution, and named `telegram_message` targeting use the same acknowledged display name; target IDs and live registrations still own routing. Names shows the generated dictionary name chosen for the slot, such as `Anchor` for slot `A`; the directory formats produce `api_tools` or `Api Tools` and append `_a` or ` A` only while two or more authenticated live instances share that exact directory. Dormant retained bindings do not keep those suffixes visible. After leader election, automatic title contraction waits one follower-staleness window for live registrations to settle, avoiding a temporary suffix removal and restoration during startup. `/name` sets a manual Thread display name; **Reset to automatic** restores the selected automatic projection. Switching preserves Thread IDs, slots, generated recovery identity, and queue ownership. Partial application reports an error and can be retried without recreating Threads.
253
+ In Threaded Mode, open Settings → **🧵 Thread display** to choose `letters` (default), `names`, `directory-title`, or `directory-snake` for this bot profile. Unsupported retained values, including the retired `directories` mode, fall back to `letters` without rewriting the saved configuration. Fresh tabs are created with the active mode's title instead of being visibly renamed afterward. Telegram tab titles, Pi terminal status, live Thread choosers/notices, prompt attribution, and named `telegram_message` targeting use the same acknowledged display name; target IDs and live registrations still own routing. Names shows the generated dictionary name chosen for the slot, such as `Anchor` for slot `A`; the directory formats produce `api_tools` or `Api Tools` and append `_a` or ` A` only while two or more authenticated live instances share that exact directory. On Linux/macOS, the current user's exact home directory displays as `home` or `Home`; subdirectories keep their own names, and Windows remains unchanged. Dormant retained bindings do not keep those suffixes visible. After leader election, automatic title contraction waits one follower-staleness window for live registrations to settle, avoiding a temporary suffix removal and restoration during startup. `/name` sets a manual Thread display name; **Reset to automatic** restores the selected automatic projection. Switching preserves Thread IDs, slots, generated recovery identity, and queue ownership. Partial application reports an error and can be retried without recreating Threads.
252
254
 
253
255
  | Mode | Best for | Runtime shape |
254
256
  | --- | --- | --- |
255
257
  | Classic DM | One running Pi instance and its active session controlled from one private bot chat | One polling owner, one queue/runtime surface |
256
258
  | Threaded Mode | Several visible Pi sessions sharing one bot | One leader owns transport; each private-chat Thread retains one session-qualified Workspace binding |
257
259
 
258
- **Live rebinding (0.53.0):** Restore uses one same-session save/apply/release channel for prompts and supported commands/templates, with separately fenced best-effort old-Thread cleanup. Existing work and exact source/receipt custody remain protected; legacy Restore readers/receivers retain continuity with older instances. Windows runs the same paths, and CI covers it. See [Multi-Instance Bus](./docs/multi-instance-bus.md#live-rebind-channel).
260
+ **Live rebinding:** Restore confirms the new binding, makes one old-tab cleanup decision, then releases the saved input through its native prompt, command or template owner. Negotiated followers use the same order without another queue. Earlier protected work defers cleanup; `/new` cannot carry an old-session deletion grant into its replacement. An unknown delete never repeats or blocks ordinary input on the confirmed new tab. Exact source/receipt custody remains separate from replies and model completion. Upgrade participating readers for the new metadata; older peers retain their supported legacy path. See [Multi-Instance Bus](./docs/multi-instance-bus.md#live-rebind-channel).
259
261
 
260
262
  ## Environment Configuration
261
263
 
@@ -101,11 +101,13 @@ export interface TelegramActivityDispatcher {
101
101
  }
102
102
  /** @internal */
103
103
  export declare function createTelegramActivityDispatcher(deps?: {
104
+ captureDrain?: () => (() => void) | undefined;
104
105
  recordFailure?: (handlerId: string, event: TelegramActivityEvent, error: unknown) => void;
105
106
  }): TelegramActivityDispatcher;
106
107
  /** @internal */
107
108
  export declare function createTelegramActivityBridgeRuntime(deps: {
108
109
  generation: string;
110
+ captureDrain?: () => (() => void) | undefined;
109
111
  observeEvent?: (event: TelegramActivityEvent) => void;
110
112
  recordFailure?: (handlerId: string, event: TelegramActivityEvent, error: unknown) => void;
111
113
  now?: () => number;
@@ -207,7 +209,9 @@ export interface TelegramActivityPublicationRuntime {
207
209
  hasUnconfirmed: () => boolean;
208
210
  reset: () => void;
209
211
  }
210
- export declare function createTelegramActivityPublicationRuntime(): TelegramActivityPublicationRuntime;
212
+ export declare function createTelegramActivityPublicationRuntime(deps?: {
213
+ captureDrain?: () => (() => void) | undefined;
214
+ }): TelegramActivityPublicationRuntime;
211
215
  export interface TelegramAssistantOutputRuntime {
212
216
  start: () => void;
213
217
  beginTurn: () => void;
@@ -116,10 +116,28 @@ function coalesceActivityEvents(previous, next) {
116
116
  }
117
117
  return next;
118
118
  }
119
+ /** Optional observation must not change queue/transport settlement. */
120
+ function captureActivityDrainObserver(capture) {
121
+ try {
122
+ const notify = capture?.();
123
+ return () => {
124
+ try {
125
+ const emitted = notify?.();
126
+ if (emitted)
127
+ void Promise.resolve(emitted).catch(() => undefined);
128
+ }
129
+ catch { /* Observer failure is inert. */ }
130
+ };
131
+ }
132
+ catch {
133
+ return () => { };
134
+ }
135
+ }
119
136
  /** @internal */
120
137
  export function createTelegramActivityDispatcher(deps = {}) {
121
138
  const queues = new Map();
122
139
  let stopped = false;
140
+ const hasPending = () => Array.from(queues.values()).some(queue => queue.active && (queue.running || queue.events.length > 0));
123
141
  const drain = async (queue) => {
124
142
  if (queue.running || !queue.active)
125
143
  return;
@@ -146,10 +164,12 @@ export function createTelegramActivityDispatcher(deps = {}) {
146
164
  }
147
165
  finally {
148
166
  queue.running = false;
167
+ if (!stopped && !hasPending())
168
+ queue.notifyDrained?.();
149
169
  }
150
170
  };
151
171
  return {
152
- hasPending: () => Array.from(queues.values()).some((queue) => queue.active && (queue.running || queue.events.length > 0)),
172
+ hasPending,
153
173
  dispatch(event) {
154
174
  if (stopped)
155
175
  return;
@@ -164,6 +184,8 @@ export function createTelegramActivityDispatcher(deps = {}) {
164
184
  };
165
185
  queues.set(registration.id, queue);
166
186
  }
187
+ if (!queue.running && queue.events.length === 0)
188
+ queue.notifyDrained = captureActivityDrainObserver(deps.captureDrain);
167
189
  const previous = queue.events.at(-1);
168
190
  if (previous && canCoalesceActivityEvents(previous, event)) {
169
191
  queue.events[queue.events.length - 1] = coalesceActivityEvents(previous, event);
@@ -198,6 +220,7 @@ export function createTelegramActivityBridgeRuntime(deps) {
198
220
  runtime = createTelegramActivityRuntime({
199
221
  generation: `${deps.generation}:${++generationSequence}`,
200
222
  dispatcher: createTelegramActivityDispatcher({
223
+ captureDrain: deps.captureDrain,
201
224
  recordFailure: deps.recordFailure,
202
225
  }),
203
226
  observeEvent: deps.observeEvent,
@@ -470,13 +493,14 @@ export function createTelegramActivityRuntime(deps) {
470
493
  },
471
494
  };
472
495
  }
473
- export function createTelegramActivityPublicationRuntime() {
496
+ export function createTelegramActivityPublicationRuntime(deps = {}) {
474
497
  let generation = 0;
475
498
  let tail = Promise.resolve();
476
499
  let outstanding = 0, unconfirmed = false;
477
500
  const pending = new Set();
478
501
  const reserve = () => {
479
502
  const admittedGeneration = generation;
503
+ const notifyDrained = captureActivityDrainObserver(deps.captureDrain);
480
504
  let state = "pending";
481
505
  let resolve;
482
506
  const ready = new Promise((accept) => {
@@ -499,6 +523,8 @@ export function createTelegramActivityPublicationRuntime() {
499
523
  })
500
524
  .finally(() => {
501
525
  outstanding -= 1;
526
+ if (admittedGeneration === generation && outstanding === 0 && !unconfirmed)
527
+ notifyDrained();
502
528
  });
503
529
  tail = result.catch(() => { });
504
530
  return {
@@ -522,6 +548,7 @@ export function createTelegramActivityPublicationRuntime() {
522
548
  hasUnconfirmed: () => unconfirmed,
523
549
  beginWork() {
524
550
  const admittedGeneration = generation;
551
+ const notifyDrained = captureActivityDrainObserver(deps.captureDrain);
525
552
  let settled = false;
526
553
  outstanding += 1;
527
554
  return {
@@ -532,6 +559,8 @@ export function createTelegramActivityPublicationRuntime() {
532
559
  outstanding -= 1;
533
560
  if (outcome === "unconfirmed" && admittedGeneration === generation)
534
561
  unconfirmed = true;
562
+ if (admittedGeneration === generation && outstanding === 0 && !unconfirmed)
563
+ notifyDrained();
535
564
  },
536
565
  };
537
566
  },
@@ -177,6 +177,7 @@ export interface TelegramActivityBindingRuntime {
177
177
  /** Compose public activity fanout, verbosity, and assistant output ordering. */
178
178
  export declare function createTelegramActivityBindingRuntime<TTransportStamp>(deps: {
179
179
  generation: string;
180
+ captureDrain?: () => (() => void) | undefined;
180
181
  assistantOutput: Omit<Parameters<typeof createTelegramAssistantOutputBindingRuntime<TTransportStamp>>[0], "waitForActivityIdle" | "enqueue">;
181
182
  activityVerbosity: Omit<Parameters<typeof ActivityVerbosity.createTelegramActivityVerbosityRuntime<TelegramAssistantOutputAuthority<TTransportStamp>>>[0], "captureAuthority" | "isAuthorityActive" | "recordFailure" | "enqueue">;
182
183
  }): TelegramActivityBindingRuntime;
@@ -288,9 +289,12 @@ interface TelegramLifecycleBindingDeps {
288
289
  }) => void;
289
290
  canSendAgentActivity: (ctx: Pi.ExtensionContext) => boolean;
290
291
  isSessionContextActive: (ctx: Pi.ExtensionContext) => boolean;
292
+ getSessionGeneration?: () => number;
291
293
  isTurnTransportActive?: (turn: Queue.PendingTelegramTurn) => boolean;
292
294
  updateStatus: TelegramBridgeStatusUpdater;
293
295
  recordRuntimeEvent: TelegramRuntimeEventRecorder;
296
+ /** Settled-session hint for owners that wait on agent idleness, such as old-tab cleanup after a live rebind. */
297
+ onSessionSettled?: (ctx: Pi.ExtensionContext) => void;
294
298
  }
295
- export declare function registerTelegramLifecycleRuntimeHooks({ pi, publicationRuntime, activityRuntime, activityVerbosityRuntime, diagnostics, assistantOutputRuntime, sessionLifecycleRuntime, configStore, abort, typing, lifecycle, activeTurnRuntime, telegramQueueStore, modelSwitchController, previewRuntime, promptDispatchRuntime, deferredQueueDispatchRuntime, modelContextAvailabilityRuntime, disconnectOnQuit, onSessionStarted, shutdownGenerativeAppLiveSurfaces, resolveAutomaticThreadCleanupEnabled, buttonActionStore, callMultipart, sendChatAction, sendRecordVoiceAction, sendMarkdownReply, sendTextReply, dispatchNextQueuedTelegramTurn, onPromptHandedOff, answerGuestQuery, deleteMessage, sendGuestReply, editGuestReply, stopGuestPlaceholder, preparePreviewDelivery, finalizeMarkdownPreview, proactivePushTargetGetter, getAssistantRenderingMode, recordMessageOwnership, canSendAgentActivity, isSessionContextActive, isTurnTransportActive, updateStatus, recordRuntimeEvent, }: TelegramLifecycleBindingDeps): void;
299
+ export declare function registerTelegramLifecycleRuntimeHooks({ pi, publicationRuntime, activityRuntime, activityVerbosityRuntime, diagnostics, assistantOutputRuntime, sessionLifecycleRuntime, configStore, abort, typing, lifecycle, activeTurnRuntime, telegramQueueStore, modelSwitchController, previewRuntime, promptDispatchRuntime, deferredQueueDispatchRuntime, modelContextAvailabilityRuntime, disconnectOnQuit, onSessionStarted, shutdownGenerativeAppLiveSurfaces, resolveAutomaticThreadCleanupEnabled, buttonActionStore, callMultipart, sendChatAction, sendRecordVoiceAction, sendMarkdownReply, sendTextReply, dispatchNextQueuedTelegramTurn, onPromptHandedOff, answerGuestQuery, deleteMessage, sendGuestReply, editGuestReply, stopGuestPlaceholder, preparePreviewDelivery, finalizeMarkdownPreview, proactivePushTargetGetter, getAssistantRenderingMode, recordMessageOwnership, canSendAgentActivity, isSessionContextActive, getSessionGeneration, isTurnTransportActive, updateStatus, recordRuntimeEvent, onSessionSettled, }: TelegramLifecycleBindingDeps): void;
296
300
  export {};
@@ -354,7 +354,7 @@ export function createTelegramAssistantOutputBindingRuntime(deps) {
354
354
  }
355
355
  /** Compose public activity fanout, verbosity, and assistant output ordering. */
356
356
  export function createTelegramActivityBindingRuntime(deps) {
357
- const publication = Activity.createTelegramActivityPublicationRuntime();
357
+ const publication = Activity.createTelegramActivityPublicationRuntime({ captureDrain: deps.captureDrain });
358
358
  const assistantOutputBinding = createTelegramAssistantOutputBindingRuntime({
359
359
  ...deps.assistantOutput,
360
360
  enqueue: publication.enqueue,
@@ -374,6 +374,7 @@ export function createTelegramActivityBindingRuntime(deps) {
374
374
  });
375
375
  const activityRuntime = Activity.createTelegramActivityBridgeRuntime({
376
376
  generation: deps.generation,
377
+ captureDrain: deps.captureDrain,
377
378
  observeEvent(event) {
378
379
  assistantOutputBinding.observeEvent(event);
379
380
  activityVerbosityRuntime.accept(event);
@@ -626,7 +627,7 @@ export function registerTelegramCommandsAndTools({ pi, agentDir, configStore, pe
626
627
  },
627
628
  });
628
629
  }
629
- export function registerTelegramLifecycleRuntimeHooks({ pi, publicationRuntime, activityRuntime, activityVerbosityRuntime, diagnostics, assistantOutputRuntime, sessionLifecycleRuntime, configStore, abort, typing, lifecycle, activeTurnRuntime, telegramQueueStore, modelSwitchController, previewRuntime, promptDispatchRuntime, deferredQueueDispatchRuntime, modelContextAvailabilityRuntime, disconnectOnQuit, onSessionStarted, shutdownGenerativeAppLiveSurfaces, resolveAutomaticThreadCleanupEnabled, buttonActionStore, callMultipart, sendChatAction, sendRecordVoiceAction, sendMarkdownReply, sendTextReply, dispatchNextQueuedTelegramTurn, onPromptHandedOff, answerGuestQuery, deleteMessage, sendGuestReply, editGuestReply, stopGuestPlaceholder, preparePreviewDelivery, finalizeMarkdownPreview, proactivePushTargetGetter, getAssistantRenderingMode, recordMessageOwnership, canSendAgentActivity, isSessionContextActive = () => true, isTurnTransportActive, updateStatus, recordRuntimeEvent, }) {
630
+ export function registerTelegramLifecycleRuntimeHooks({ pi, publicationRuntime, activityRuntime, activityVerbosityRuntime, diagnostics, assistantOutputRuntime, sessionLifecycleRuntime, configStore, abort, typing, lifecycle, activeTurnRuntime, telegramQueueStore, modelSwitchController, previewRuntime, promptDispatchRuntime, deferredQueueDispatchRuntime, modelContextAvailabilityRuntime, disconnectOnQuit, onSessionStarted, shutdownGenerativeAppLiveSurfaces, resolveAutomaticThreadCleanupEnabled, buttonActionStore, callMultipart, sendChatAction, sendRecordVoiceAction, sendMarkdownReply, sendTextReply, dispatchNextQueuedTelegramTurn, onPromptHandedOff, answerGuestQuery, deleteMessage, sendGuestReply, editGuestReply, stopGuestPlaceholder, preparePreviewDelivery, finalizeMarkdownPreview, proactivePushTargetGetter, getAssistantRenderingMode, recordMessageOwnership, canSendAgentActivity, isSessionContextActive = () => true, getSessionGeneration, isTurnTransportActive, updateStatus, recordRuntimeEvent, onSessionSettled, }) {
630
631
  const agentEndResetter = Runtime.createTelegramAgentEndResetter({
631
632
  abort,
632
633
  typing,
@@ -864,6 +865,8 @@ export function registerTelegramLifecycleRuntimeHooks({ pi, publicationRuntime,
864
865
  });
865
866
  const messageActivityTypingHooks = Lifecycle.createTelegramMessageActivityTypingHooks({
866
867
  hasActiveTurn: activeTurnRuntime.has,
868
+ isContextActive: isSessionContextActive,
869
+ getSessionGeneration,
867
870
  startTypingLoop: startActiveTurnTypingLoop,
868
871
  onMessageStart: previewRuntime.onMessageStart,
869
872
  onMessageUpdate: previewRuntime.onMessageUpdate,
@@ -955,9 +958,12 @@ export function registerTelegramLifecycleRuntimeHooks({ pi, publicationRuntime,
955
958
  async onAgentStart(event, ctx) {
956
959
  if (!isSessionContextActive(ctx))
957
960
  return;
961
+ const generation = getSessionGeneration?.();
958
962
  agentWorkActive = true;
959
963
  cancelPendingFinalPublication();
960
964
  await agentStartWithDedupReset(event, ctx);
965
+ if (!isSessionContextActive(ctx) || (getSessionGeneration && !Object.is(getSessionGeneration(), generation)))
966
+ return;
961
967
  const turn = activeTurnRuntime.get();
962
968
  activityRuntime.onAgentStart(turn?.target, turn?.replyToMessageId);
963
969
  startAgentActivityTypingLoop(ctx);
@@ -1077,6 +1083,7 @@ export function registerTelegramLifecycleRuntimeHooks({ pi, publicationRuntime,
1077
1083
  agentWorkActive = false;
1078
1084
  activityRuntime.onAgentSettled();
1079
1085
  modelContextAvailabilityRuntime.reconcile();
1086
+ onSessionSettled?.(ctx);
1080
1087
  },
1081
1088
  onBeforeAgentStart: Prompts.createTelegramProactiveBeforeAgentStartHook({
1082
1089
  reconcileAvailability: modelContextAvailabilityRuntime.reconcile,
@@ -4,8 +4,8 @@
4
4
  * Wraps the direct Telegram Bot API runtime so follower instances can route outbound calls through the bus leader
5
5
  */
6
6
  import { type TelegramBusSelectedMenuDeliveryObservation, type TelegramBusSelectedMenuTextEffect } from "./bus.ts";
7
- import type { TelegramBridgeApiRuntime } from "./telegram-api.ts";
8
- export type TelegramBusApiCall = (method: string, args: unknown[]) => Promise<unknown>;
7
+ import type { TelegramApiCallOptions, TelegramBridgeApiRuntime } from "./telegram-api.ts";
8
+ export type TelegramBusApiCall = (method: string, args: unknown[], localOptions?: Pick<TelegramApiCallOptions, "assertAuthority">) => Promise<unknown>;
9
9
  export interface TelegramBusAwareApiRuntimeDeps {
10
10
  directRuntime: TelegramBridgeApiRuntime;
11
11
  ownsDirect: () => boolean;
@@ -4,7 +4,7 @@
4
4
  * Wraps the direct Telegram Bot API runtime so follower instances can route outbound calls through the bus leader
5
5
  */
6
6
  import { markTelegramBusCrossTargetDelivery, parseTelegramBusSelectedMenuTextEffect, stripTelegramBusApiMetadata, } from "./bus.js";
7
- import { buildTelegramAnswerGuestQueryBody, isTelegramApiCommitUnknownError, isTelegramMessageNotModifiedError, TelegramApiAuthorityError, TelegramApiCommitUnknownError, } from "./telegram-api.js";
7
+ import { buildTelegramAnswerGuestQueryBody, assertTelegramApiCallAuthority, isTelegramApiCommitUnknownError, isTelegramMessageNotModifiedError, TelegramApiAuthorityError, TelegramApiCommitUnknownError, } from "./telegram-api.js";
8
8
  function asRecord(value) {
9
9
  return value && typeof value === "object" && !Array.isArray(value)
10
10
  ? value
@@ -142,6 +142,7 @@ export function createTelegramSelectedMenuTextApi(deps) {
142
142
  }
143
143
  /** Followers route every chat action through the leader's generic `sendChatAction` call. */
144
144
  function sendTelegramFollowerChatAction(deps, chatId, action, options) {
145
+ assertTelegramApiCallAuthority(options?.assertAuthority, false);
145
146
  const body = withDefaultThreadTarget({
146
147
  chat_id: chatId,
147
148
  action,
@@ -149,7 +150,11 @@ function sendTelegramFollowerChatAction(deps, chatId, action, options) {
149
150
  ? { message_thread_id: options.message_thread_id }
150
151
  : {}),
151
152
  }, deps.getDefaultTarget?.());
152
- return deps.callFollowerApi("call", ["sendChatAction", body]).then(asBoolean);
153
+ // Only plain single-attempt policy crosses IPC; the captured assertion is a local issuance fence.
154
+ const args = ["sendChatAction", body, ...(action === "typing"
155
+ ? [{ maxAttempts: 1, retrySafety: "non-idempotent" }] : [])];
156
+ return deps.callFollowerApi("call", args, options?.assertAuthority
157
+ ? { assertAuthority: options.assertAuthority } : undefined).then(asBoolean);
153
158
  }
154
159
  export function createTelegramBusAwareApiRuntime(deps) {
155
160
  return {
@@ -12,8 +12,10 @@ import { TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_APPLY, TELEGRAM_BUS_CAPABILITY_SELE
12
12
  import type { TelegramConfigStore, TelegramThreadDisplayMode } from "./config.ts";
13
13
  import { type TelegramUpdateJournalStoreOptions } from "./journal.ts";
14
14
  import { type TelegramLockEntry, type TelegramLockState } from "./locks.ts";
15
+ import * as Machine from "./machine.ts";
15
16
  import type { TelegramQueueHandoffStageResult } from "./queue.ts";
16
17
  import { type TelegramTarget } from "./target.ts";
18
+ import { type TelegramApiCallOptions } from "./telegram-api.ts";
17
19
  import * as Threads from "./threads.ts";
18
20
  import { type TelegramWorkspaceAdmissionLedger } from "./workspace-admission.ts";
19
21
  export declare const TELEGRAM_BUS_FOLLOWER_HEARTBEAT_TIMEOUT_MS = 8000;
@@ -343,7 +345,9 @@ export declare function createTelegramBusFollowerWorkspaceRestoreHandler<TContex
343
345
  expectedTarget?: TelegramTarget & {
344
346
  threadId: number;
345
347
  };
346
- release?: (canRelease: () => boolean) => void;
348
+ release?: (canRelease: () => boolean, target: TelegramTarget & {
349
+ threadId: number;
350
+ }) => void;
347
351
  observeWork?: (oldTarget: TelegramTarget & {
348
352
  threadId: number;
349
353
  }) => void;
@@ -362,7 +366,7 @@ export declare function createTelegramBusFollowerWorkspaceRestoreHandler<TContex
362
366
  };
363
367
  export declare function createTelegramBusFollowerClientRuntime<TContext, TReactionUpdate, TCallbackQuery, TMessage = unknown>(deps: TelegramBusFollowerClientRuntimeDeps<TMessage>): {
364
368
  createRequestId: () => string;
365
- callApi: (method: string, args: unknown[]) => Promise<unknown>;
369
+ callApi: (method: string, args: unknown[], localOptions?: Pick<TelegramApiCallOptions, "assertAuthority">) => Promise<unknown>;
366
370
  agentMessages: {
367
371
  resolveTarget: (selector: TelegramBusAgentTargetSelector) => Promise<TelegramTarget & {
368
372
  threadId: number;
@@ -414,7 +418,7 @@ export declare function createTelegramBusFollowerSelectedMenuCaller<TContext>(de
414
418
  effect: TelegramBusSelectedMenuTextEffect;
415
419
  assertAuthority: () => void;
416
420
  }) => Promise<TelegramBusSelectedMenuDeliveryObservation>;
417
- export declare function createTelegramBusFollowerApiCaller(deps: TelegramBusFollowerApiCallerDeps): (method: string, args: unknown[]) => Promise<unknown>;
421
+ export declare function createTelegramBusFollowerApiCaller(deps: TelegramBusFollowerApiCallerDeps): (method: string, args: unknown[], localOptions?: Pick<TelegramApiCallOptions, "assertAuthority">) => Promise<unknown>;
418
422
  export declare function createTelegramBusFollowerSessionReplacementSuspender(deps: TelegramBusFollowerSessionReplacementSuspenderDeps): (preserveTarget?: boolean) => Promise<void>;
419
423
  export declare function createTelegramBusFollowerSessionRefreshHook<TContext>(deps: TelegramBusFollowerSessionRefreshHookDeps<TContext>): (_event: unknown, ctx: TContext) => Promise<void>;
420
424
  export declare function createTelegramBusFollowerControlState(): TelegramBusFollowerControlState;
@@ -426,6 +430,48 @@ export type TelegramBusFollowerHeartbeatRecoveryHandler<TContext> = ((error: unk
426
430
  halt: () => void;
427
431
  resume: () => void;
428
432
  };
433
+ /** The whole recovery lifecycle state; only `transitionTelegramBusFollowerRecovery` produces a new one. */
434
+ export interface TelegramBusFollowerRecoveryState {
435
+ /** Bumped by halt and resume, so a recovery or retry from before an operator stop or connect is inert. */
436
+ readonly epoch: number;
437
+ readonly halted: boolean;
438
+ /** One recovery runs at a time; it belongs to the epoch it started in. */
439
+ readonly recovering: boolean;
440
+ readonly retryId: number | null;
441
+ readonly nextRetryId: number;
442
+ }
443
+ export type TelegramBusFollowerRecoveryEvent = {
444
+ type: "recoverRequested";
445
+ } | {
446
+ type: "recoverySettled";
447
+ epoch: number;
448
+ } | {
449
+ type: "retryRequested";
450
+ epoch: number;
451
+ } | {
452
+ type: "retryFired";
453
+ retryId: number;
454
+ } | {
455
+ type: "halted";
456
+ } | {
457
+ type: "resumed";
458
+ };
459
+ export type TelegramBusFollowerRecoveryEffect = {
460
+ type: "runRecovery";
461
+ epoch: number;
462
+ } | {
463
+ type: "armRetry";
464
+ retryId: number;
465
+ } | {
466
+ type: "cancelRetry";
467
+ retryId: number;
468
+ };
469
+ export declare function createTelegramBusFollowerRecoveryState(): TelegramBusFollowerRecoveryState;
470
+ /**
471
+ * Pure follower-recovery lifecycle. An operator stop halts it and a connect resumes it, each in a new epoch: work
472
+ * from the old epoch, a recovery still awaiting or a retry still armed, can no longer register or promote.
473
+ */
474
+ export declare function transitionTelegramBusFollowerRecovery(state: TelegramBusFollowerRecoveryState, event: TelegramBusFollowerRecoveryEvent): Machine.TelegramMachineStep<TelegramBusFollowerRecoveryState, TelegramBusFollowerRecoveryEffect>;
429
475
  export declare function createTelegramBusFollowerHeartbeatRecoveryHandler<TContext>(deps: TelegramBusFollowerHeartbeatRecoveryHandlerDeps<TContext>): TelegramBusFollowerHeartbeatRecoveryHandler<TContext>;
430
476
  export declare function createTelegramBusFollowerRegistrationRuntime<TContext extends {
431
477
  cwd?: string;