@llblab/pi-telegram 0.55.1 → 0.56.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/AGENTS.md +5 -4
  2. package/BACKLOG.md +6 -1
  3. package/CHANGELOG.md +15 -0
  4. package/README.md +28 -17
  5. package/dist/lib/bindings.d.ts +8 -4
  6. package/dist/lib/bindings.js +41 -1
  7. package/dist/lib/bus-follower.d.ts +1 -0
  8. package/dist/lib/bus-follower.js +10 -5
  9. package/dist/lib/bus-leader.d.ts +2 -0
  10. package/dist/lib/bus-leader.js +34 -20
  11. package/dist/lib/bus.d.ts +8 -0
  12. package/dist/lib/bus.js +21 -7
  13. package/dist/lib/commands.d.ts +1 -1
  14. package/dist/lib/commands.js +33 -28
  15. package/dist/lib/extension.js +18 -3
  16. package/dist/lib/lifecycle.d.ts +2 -1
  17. package/dist/lib/lifecycle.js +28 -20
  18. package/dist/lib/locks.d.ts +6 -2
  19. package/dist/lib/locks.js +3 -3
  20. package/dist/lib/menu-queue.d.ts +1 -0
  21. package/dist/lib/menu-queue.js +102 -99
  22. package/dist/lib/pi.d.ts +3 -2
  23. package/dist/lib/pi.js +3 -0
  24. package/dist/lib/polling.d.ts +6 -2
  25. package/dist/lib/polling.js +5 -3
  26. package/dist/lib/queue.d.ts +57 -5
  27. package/dist/lib/queue.js +313 -25
  28. package/dist/lib/routing.d.ts +4 -0
  29. package/dist/lib/routing.js +62 -1
  30. package/dist/lib/status.d.ts +1 -2
  31. package/dist/lib/status.js +6 -7
  32. package/dist/lib/sync.js +11 -1
  33. package/dist/lib/threads.d.ts +11 -0
  34. package/dist/lib/threads.js +57 -6
  35. package/dist/lib/updates.d.ts +12 -7
  36. package/dist/lib/updates.js +44 -9
  37. package/dist/package.json +1 -1
  38. package/docs/README.md +1 -1
  39. package/docs/architecture.md +24 -13
  40. package/docs/callback-namespaces.md +4 -0
  41. package/docs/multi-instance-bus.md +6 -4
  42. package/docs/ui-style.md +14 -10
  43. package/lib/bindings.ts +52 -3
  44. package/lib/bus-follower.ts +13 -5
  45. package/lib/bus-leader.ts +37 -23
  46. package/lib/bus.ts +23 -7
  47. package/lib/commands.ts +52 -47
  48. package/lib/extension.ts +18 -2
  49. package/lib/lifecycle.ts +34 -28
  50. package/lib/locks.ts +5 -2
  51. package/lib/menu-queue.ts +116 -162
  52. package/lib/pi.ts +8 -0
  53. package/lib/polling.ts +7 -2
  54. package/lib/queue.ts +328 -33
  55. package/lib/routing.ts +51 -1
  56. package/lib/status.ts +7 -7
  57. package/lib/sync.ts +11 -0
  58. package/lib/threads.ts +57 -5
  59. package/lib/updates.ts +44 -15
  60. package/package.json +1 -1
package/AGENTS.md CHANGED
@@ -27,7 +27,7 @@ Canonical terms:
27
27
 
28
28
  Keep each fact in one authoritative layer:
29
29
 
30
- - [`README.md`](./README.md): Public product entrypoint. Preserve the flow identity → install/connect → examples → product model → compact capabilities → controls/safety → docs. Balance strong positioning with a practical catalogue of capabilities, operator gestures and meaningful consequences; keep detailed routing proofs, recovery algorithms and action-markup grammar in indexed `/docs` instead of accumulating release-by-release paragraphs.
30
+ - [`README.md`](./README.md): Public product entrypoint. Preserve the flow identity → install/connect → examples → product model → compact capabilities → controls/safety → docs. Describe current behavior, not version-by-version changes or release progress; compatibility requirements belong here only when needed for installation or safe operation, while historical version details belong in CHANGELOG or indexed technical docs. Balance strong positioning with a practical catalogue of capabilities, operator gestures and meaningful consequences; keep detailed routing proofs, recovery algorithms and action-markup grammar in indexed `/docs` instead of accumulating release-by-release paragraphs.
31
31
  - `AGENTS.md`: Stable engineering boundaries, recurring runtime invariants, and work protocol. Link to evolving subsystem contracts instead of copying them here.
32
32
  - [`BACKLOG.md`](./BACKLOG.md): Canonical unresolved work. Keep only open top-level outcomes with nested decomposition and done criteria. Remove completed outcomes rather than retaining checked history.
33
33
  - [`CHANGELOG.md`](./CHANGELOG.md): Completed user/operator/developer impact. A release has at most eight outcome bullets of at most 512 characters, each beginning with an inline-code domain label and colon. Exclude personal names and real user/chat/message/thread identifiers. Consolidate the current pre-release section before release; do not rewrite historical sections without an explicit retrospective request and evidence pass.
@@ -74,7 +74,7 @@ Use the relevant local skill before non-trivial work in its domain. Keep skill o
74
74
  - The bridge is session-local and paired to one allowed Telegram user. Preserve `{ chatId, threadId? }` through every inbound, queue, callback, reaction, media, preview, reply, menu, voice, attachment, and direct-delivery path.
75
75
  - First-contact pairing grants in-memory authority only after profile/token/execution-fenced durable publication confirms that exact user; it never overwrites another configured owner. `profiles.<name>.botToken` may store an exact `$NAME`/`${NAME}` environment reference instead of a copied secret: resolve it only at validation or activation boundaries, fail closed with a redacted named-variable diagnostic when unresolved, and keep literal tokens compatible. Sender admission precedes user message/edit/callback/reaction delegation, including foreign ownership and unbound-Thread fallback paths. Reactions require an existing exact human owner; private chat type is not authorization. Queued config persistence must not replay observed authority as local grant edits or erase later local unpair. Setup and retry details belong in [`docs/architecture.md`](./docs/architecture.md#setup-flow).
76
76
  - Telegram transport ownership is not semantic queue ownership. Losing the exact transport lock must not erase accepted local queue work or stop valid local Pi dispatch; direct Bot API mutations fail closed until exact direct or follower authority exists.
77
- - Runtime files live under `<agent-dir>/tmp/pi-telegram` (releases before 0.52.0 used `tmp/telegram`, which a new release never writes, migrates or deletes). The older `owners.json` is read-only evidence: a live fresh owner there blocks acquisition so two release generations never poll one bot, and only its identity is exposed, never its bus endpoint or secret. Only `state.json` and `logs.jsonl` are persistent root files: the per-profile `transport` section of `state.json` is the sole transport-owner authority, `workspace` owns canonical Workspace/Restore evidence and `admission` its leases; the `runtime` section and `logs.jsonl` never grant routing authority. Each section changes only through the shared transaction under its own domain authority (nonleader admission remains process/birth-owned, not transport-owned); Workspace/runtime publication captures exact session authority, and acquisition, refresh, release, takeover, and irreversible leader work fence the exact owner/epoch. Ordinary reads and section mutations still refuse a damaged shared envelope without repair. Only a non-election leader start (`/telegram-connect` or session auto-start) runs the operator-approved optimistic reset: under the shared transaction guard it replaces damaged envelope/transport/Workspace/admission evidence with an empty envelope, accepting loss of every profile's runtime continuity; filesystem access errors still refuse (see [Damaged-State Reset](./docs/architecture.md#damaged-state-reset-operator-approved)). Use the [session journal contract](./docs/multi-instance-bus.md#session-owned-journal-storage) for owners-named polling continuity and the explicitly approved optimistic session-family sweep; that sweep is not Thread deletion or receipt settlement.
77
+ - Runtime files live under `<agent-dir>/tmp/pi-telegram` (releases before 0.52.0 used `tmp/telegram`, which a new release never writes, migrates or deletes). The older `owners.json` is read-only evidence: a live fresh owner there blocks acquisition so two release generations never poll one bot, and only its identity is exposed, never its bus endpoint or secret. Only `state.json` and `logs.jsonl` are persistent root files: the per-profile `transport` section of `state.json` is the sole transport-owner authority, `workspace` owns canonical Workspace/Restore evidence and `admission` its leases; the `runtime` section and `logs.jsonl` never grant routing authority. Each section changes only through the shared transaction under its own domain authority (nonleader admission remains process/birth-owned, not transport-owned); Workspace/runtime publication captures exact session authority, and acquisition, refresh, release, takeover, and irreversible leader work fence the exact owner/epoch. Ordinary reads and section mutations still refuse a damaged shared envelope without repair. Explicit `/telegram-connect` additionally grants one profile-admitted reset of matching untargeted unfinished Thread-creation metadata before Workspace identity allocation; it may leave an unknown Telegram tab behind, but never deletes a known target, Workspace binding or journal receipt. Automatic startup/re-registration and Restore-only admission cannot grant that reset. Known pending targets retain their exact binding key and slot; fresh provisioning uses unique operation IDs so late recovery evidence cannot attach to a replacement attempt. Follower reset intent uses authenticated `explicit-connect-recovery-v1` registration. Only a non-election leader start (`/telegram-connect` or session auto-start) runs the operator-approved optimistic reset: under the shared transaction guard it replaces damaged envelope/transport/Workspace/admission evidence with an empty envelope, accepting loss of every profile's runtime continuity; filesystem access errors still refuse (see [Damaged-State Reset](./docs/architecture.md#damaged-state-reset-operator-approved)). Use the [session journal contract](./docs/multi-instance-bus.md#session-owned-journal-storage) for owners-named polling continuity and the explicitly approved optimistic session-family sweep; that sweep is not Thread deletion or receipt settlement.
78
78
  - 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.
79
79
  - 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. Same-session refresh awaits binding preparation without re-registering; a changed session ID cannot publish readiness until its exact leader registration is acknowledged. Reusing a context object cannot carry readiness across a session-generation or ID 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.
80
80
  - 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`, capability-gated restore-only `follower.restoreWorkspace` and side-effect-free liveness `bus.probe` 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.
@@ -99,14 +99,15 @@ Use the relevant local skill before non-trivial work in its domain. Keep skill o
99
99
 
100
100
  ### 4.4 Queue, Delivery, And User Surfaces
101
101
 
102
- - 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. For ordinary Normal/Priority prompts, 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.
102
+ - Queue lane/kind admission is explicit. Ordinary new-task 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 shows one warning-yellow `+N` suffix for executable waiting work across all lanes (Normal/Priority/Steer/Control), omitted when zero. It excludes the dispatched head, frozen attempts and Skip, and never counts current agent work from any source. Each prompt is one object with one active lane and no reserved return slot. Control/Steer/Priority/Normal have explicit ranks. For ordinary prompts, crossing lanes removes it from the source and appends it at the destination tail; Keep/Skip and unchanged effective intent preserve lane position. Complete reaction sets derive Steer from fire, otherwise Priority from thumbs-up/heart/dove, and independent Skip from negative emoji. Fire dominates weaker positives; lightning is a Control marker, never a command classifier or Priority alias. Decorative changes cannot reset a menu lane choice; positive intent and Skip 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.
103
+ - Experimental Steer stays in the existing Queue/admission owner until one protected whole-packet attempt. Active insertion uses public `turn_end` after one response/full tool batch without resetting current Telegram reply/preview ownership; idle uses ordinary `sendUserMessage`. Recheck exact context/generation/source/native/compaction guards, freeze original objects before whole-receipt journal disposition, and never treat it as SDK/model-use proof. Partial/refused/unknown attempts remain non-executable and immutable without replay. Idle originals stay behind one pinned pending head until `agent_start` projects one active reply/voice/attachment owner. Keep identity fencing separate from same-session readiness refusal so old failures cannot clear replacement flags and unissued packets cannot contaminate native agent starts. Queue transfer allows unspent Steer only with `queue-steer-v1` plus baseline handoff support at donor/leader/recipient; missing capability refuses before offer, and frozen/pending work never transfers as unspent input.
103
104
  - `/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.
104
105
  - 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.
105
106
  - 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
107
  - 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.
107
108
  - 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.
108
109
  - `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.
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.
110
+ - 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, never a Thread-existence or message-delivery probe: 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.
110
111
  - 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.
111
112
  - 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.
112
113
 
package/BACKLOG.md CHANGED
@@ -2,4 +2,9 @@
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 tasks.
5
+ - [ ] **Connection recovery live acceptance.** Verify operator-run leader/follower reconnect after interrupted creation on updated peers. Known target reuse and unknown-target abandonment have separate local regressions; do not infer live recovery from them. Also verify a long-deleted follower tab, follower `/start` Restore with negotiated menu-alias support, and leader Restore confirmation on the first click plus separately proven old-tab cleanup. Record leftover-tab behavior, exact peer capabilities and diagnostics; local regressions and release authorization do not replace this live evidence.
6
+ - [ ] **Steer follow-up live acceptance.** Release authorization explicitly defers these checks beyond 0.56.0; do not treat automated coverage or the accepted busy-run smoke as live proof for unrun scenarios. Preserve the [queue and dispatch contract](./docs/architecture.md#queue-and-dispatch-safety).
7
+ - [ ] **Operator scenarios.** Verify final selector/counter rendering; idle packets, native pending input, compaction, model switching, abort/Continue, complete media groups, Reroute/Restore, negotiated leader/follower transfer, reply/preview ownership and session-generation replacement. Record actual Pi/OS/Telegram versions, peer capabilities and redacted outcomes. Reload and disposable live tests require operator intent; do not manipulate runtime files to obtain acceptance.
8
+ - [ ] **Restore recipient refusal after leadership change.** Diagnose a retained routing chooser refusing the selected current leader with unavailable recipient authority. Identify the exact context/session/Workspace mismatch before proposing recovery; do not treat an absent follower registry as the cause of a leader-target refusal, bypass fences or mutate live state.
9
+ - [ ] **Post-reload diagnostics.** Investigate recurring typing-authority refusals and Workspace publication retries observed alongside successful leader/follower reconnection. Establish whether they are expected fenced stale work or affect current-session delivery; successful registration alone does not establish error-free operation.
10
+ - [ ] **Native Windows scenarios.** Verify named-pipe custody/ACK, journal/atomic rename, reused-context lifecycle and the phone scenarios on an actual Windows host. Record skipped tests and missing platform evidence explicitly; a hosted CI result does not establish phone or full live-runtime acceptance.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,21 @@
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.56.1: Recover interrupted connections
8
+
9
+ - `Connection recovery`: Explicit `/telegram-connect` can abandon matching unknown Thread-creation attempts and connect afresh without deleting tabs or clearing accepted work. Known pending targets keep their Workspace key and slot; unique creation IDs prevent late replies from attaching to a new attempt. Follower recovery requires `explicit-connect-recovery-v1` on both peers. A lost API reply may leave an extra tab; automatic startup and Restore-only registration never reset it.
10
+ - `Restore`: Reconnecting followers verify retained tabs with a connected message rather than typing, replacing a proven deleted tab. Negotiated `held-menu-aliases-v1` lets `/start` and `/help` restore a follower and open its restricted menu without pairing or bootstrap. A leader Restore waits briefly for the exact command-removal ACK on the original click, independently of menu delivery; lost ACKs never replay input, and old-tab cleanup remains separately fenced.
11
+ - `Windows validation`: Follower control fixtures use fresh registration timestamps and the production Live Restore deadline; a delayed apply case retains exact command and receipt assertions. Commentary/Next integration tests await observed progress under a shared test deadline and always stop their runtime before restoring fixtures, including on failure. Runtime authority and retry policy are unchanged.
12
+
13
+ ## 0.56.0: Steer from Telegram
14
+
15
+ - `Thread recovery`: A startup Telegram API timeout can leave the owner without a local bus despite retained Threaded Mode. A fresh positive capability probe now starts that owner's Threaded transport instead of treating the unchanged saved mode as readiness. Healthy buses stay running and followers cannot acquire leadership through this recovery.
16
+ - `Steer`: Send corrections into a busy Pi run with a fire reaction or the queue menu. Ready whole prompts form FIFO packets at public model-step boundaries; idle Steer starts one ordinary task. Waiting input stays in Telegram's queue, not Pi's native steering/follow-up queues. Native pending input and compaction defer attempts; delivery is not a transactional model-use acknowledgement, and uncertain attempts never automatically replay.
17
+ - `Queue controls`: Normal/Priority/Steer and independent Keep/Skip share menu and reaction state. Fire dominates Priority reactions; changing lanes appends at the destination tail. Blue Normal, purple Priority and yellow Steer selectors accompany one yellow terminal waiting-work count. Dispatched/frozen prompts are read-only; a dispatched guest cannot be removed by stale Skip controls or lose its reply owner.
18
+ - `Queue transfer`: Negotiated `queue-steer-v1` support preserves unspent Steer prompts, ordering, attachments and whole receipts across live handoff. Donor, leader and recipient must all support it alongside base queue handoff; incompatible peers, attempted work and pending packets refuse transfer before custody changes.
19
+ - `Session lifetime`: Queue startup/shutdown and follower refresh preserve captured session generations across asynchronous preparation and teardown. Reused context objects cannot let an obsolete operation discard a successor queue, publish stale registration success or start its monitors. Failed durable disposal retains work without replay.
20
+ - `Validation scope`: Operator smoke tests confirmed native and Telegram Steer, ordered text batching, voice-derived input and Normal work after completion. Broader live idle/native-pending, media/transfer/Restore, interruption and native-Windows scenarios remain follow-up acceptance work, not verified release guarantees.
21
+
7
22
  ## 0.55.1: Visible Reroute and follower cleanup
8
23
 
9
24
  - `Reroute visibility`: Temporary Reroute forwards text, voice, files and albums into the selected Thread before local/follower admission, then anchors replies to confirmed copies. Source/recipient/generation fences reach HTTP and IPC write/ACK boundaries; one native grant prevents replay after partial/lost ACKs. Unknown cold All copies stay held with Cancel; real native-source recovery remains supported. Journal originals, receipt/group cleanup protections, Restore and cleanup timing are unchanged.
package/README.md CHANGED
@@ -61,6 +61,8 @@ The connected Pi instance owns Telegram polling. Use `/telegram-connect <profile
61
61
 
62
62
  `/telegram-connect` keeps working when the shared runtime `state.json` is damaged. If it is malformed, foreign or contains invalid evidence, the connecting leader replaces it with a fresh empty state and continues. Every profile then forgets old Thread bindings, slots, unfinished Restore/temporary work and polling continuity; `telegram.json`, Pi history and the released `tmp/telegram` are untouched. Permission errors still stop connection. See [Damaged-State Reset](./docs/architecture.md#damaged-state-reset-operator-approved).
63
63
 
64
+ If Thread creation was interrupted, an explicit `/telegram-connect` can reset that unfinished attempt and try a fresh connection without clearing queued prompts or taking over a live leader. A lost Telegram response may leave an extra tab behind; known tabs are reused rather than blindly recreated. Both peers need connection-recovery support. See [Connection Recovery](./docs/architecture.md#explicit-connection-recovery).
65
+
64
66
  The prompt queue is session-local. Before a reload or restart, let it drain or knowingly accept losing waiting work and re-send what still matters afterwards; nothing is replayed automatically.
65
67
 
66
68
  Ordinary network or admission failures use exponential retry pauses capped at 30 seconds, without permanently stopping after an arbitrary attempt count. The poller recovers when the outage clears. Explicit `/telegram-disconnect` still attempts to stop its captured local transport if Thread cleanup is unavailable; it reports unconfirmed cleanup rather than successful deletion, and an old disconnect cannot stop a new connection. Incomplete stop/release remains visible; check `/telegram-status --debug` and keep Pi open.
@@ -206,24 +208,33 @@ In Threaded Mode, `All` is input-only: messages enter temporary routing tabs, an
206
208
 
207
209
  Messages sent while Pi is busy become queued turns. Queue controls let you inspect, prioritize, keep or skip, and dispatch work without touching the terminal.
208
210
 
209
- Queue policy:
211
+ Use reactions or the queue menu to control waiting prompts.
212
+
213
+ Priority — when the prompt runs:
214
+ - **Normal — no dedicated reaction emoji.** The default lane for ordinary queued work.
215
+ - **Priority — `👍`.** Moves waiting input ahead of Normal work, without interrupting the active task.
216
+ - **Steer — `🔥`.** Inserts ready input at a model-step boundary, or starts one ordinary task when idle. Fire takes precedence over Priority reactions; removing it falls back to remaining Priority intent or Normal.
210
217
 
211
- - One prompt is one queue object with exactly one current lane and one current position; it never reserves a shadow place in the other lane. The terminal's yellow `+N` suffix counts only executable prompts still waiting, never the current run; a dispatched Telegram prompt stops contributing before that run settles.
212
- - Priority and Normal are separate FIFO lanes; Priority dispatches first.
213
- - Moving `Normal → Priority` removes the prompt from Normal and places it at the Priority tail. Moving `Priority → Normal` removes it from Priority and places it at the Normal tail; no former position is restored.
214
- - Keep/Skip never changes lane position. Skip preserves durable authority while waiting so Keep remains reversible, then settles that authority and drops the prompt without a model turn when dispatch reaches it. Skipped prompts stay visible at their physical queue position with a struck-through ordinal, but are excluded immediately from the executable queue count shown in both the Pi status bar and Telegram main menu. Graceful session shutdown discards all remaining queued authority, so a new session starts empty.
215
- - For ordinary prompts, reactions control two independent dimensions; changing one category preserves the other:
216
- - `Positive`: `👍`, `⚡️`, `❤️`, `🕊`, `🔥` — controls Priority.
217
- - `Negative`: `👎`, `👻`, `💔`, `💩`, `🗑` — controls Skip.
218
- - Priority and Skip can coexist—for example `👍 + 💩`. Skip wins at dispatch, regardless of which negative emoji is selected.
219
- - Menu selectors and reactions share queue state, but the bot cannot remove a user's reaction; Keep may clear internal Skip while the user's emoji remains visible until they remove it.
220
- - A negative reaction to your original standalone `/continue` immediately cancels its exact waiting continuation, even while other work is active. This is irreversible cancellation, not reversible Skip. Positive reactions do not change its control lane; already-dispatched or running work and synthetic model-switch continuations are untouched.
218
+ Disposition — whether the prompt runs:
219
+ - **Keep — no dedicated reaction emoji.** The default disposition: the prompt remains eligible to run in its selected lane.
220
+ - **Skip — `👎`.** Suppresses the prompt without changing its lane or position. Priority and Skip can coexist—for example `👍 + 👎`; Skip wins at dispatch.
221
+
222
+ Queued commands and control actions use a separate **Control** lane ahead of Steer, Priority and Normal. They are marked with `⚡️`; this is a label, not a reaction shortcut.
223
+
224
+ Queue policy:
225
+ - Changing a prompt's lane moves it to the end of that lane; switching back does not restore its former position.
226
+ - Keep/Skip preserves position. Skip stays reversible until the prompt reaches dispatch, then drops it without a model turn. Skipped prompts remain visible with a struck-through number.
227
+ - One yellow terminal counter shows waiting work across all lanes, excluding current or already-dispatched work, Skip and frozen attempts.
228
+ - Dispatched or frozen prompts are inspection-only. An uncertain Steer attempt is not automatically retried.
229
+ - The bot cannot remove your Telegram reactions; changing a menu choice may leave the original reaction visible.
230
+ - A negative reaction to your waiting standalone `/continue` cancels it immediately rather than marking it for reversible Skip. Already-running work is unaffected.
231
+ - The queue is session-local. Let it drain before reload or shutdown, or accept losing waiting work.
221
232
 
222
- The detailed contract lives in [Priority, Reactions, Keep, and Skip](./docs/architecture.md#priority-reactions-keep-and-skip). If Pi automatically retries a transient provider failure, the active Telegram turn stays bound until the successful reply arrives or Pi confirms that the run has settled.
233
+ See [Queue Behavior and Safety](./docs/architecture.md#priority-reactions-keep-and-skip) for reaction precedence, dispatch rules and recovery details.
223
234
 
224
235
  ### Native Rich Markdown
225
236
 
226
- Rich Markdown is the default model-answer membrane. Complete assistant and guest model replies use Telegram's native Rich Message APIs; valid bot commands and URLs retain Telegram's native clickable affordances. Activity thinking uses persistent headerless expandable HTML, while each completed tool uses one iconless native Rich root details node whose arguments open with the root while secondary JSON evidence stays collapsed; `thinking`, `tools`, and `verbose` select the visible classes, while menus, status rows, queue controls, settings, diagnostics, and other operational UI retain explicit Telegram HTML/plain rendering. Three Settings controls keep the layers separate: `Draft previews` toggles streamed answer drafts, `Activity` chooses `quiet` or `verbose` technical activity, and `Assistant rendering` chooses final-answer delivery (`rich` Native Rich Markdown or `html` legacy Markdown-to-HTML).
237
+ Answers use Telegram-native Rich Markdown, with clickable commands and links. In Settings, use **Draft previews** to control streaming previews, **Activity** to choose the amount of thinking and tool detail, and **Assistant rendering** to switch between Rich Markdown and HTML compatibility mode. See [Rendering and Delivery](./docs/architecture.md#rendering-and-delivery) and [Activity](./docs/activity.md).
227
238
 
228
239
  ### Files And Artifacts
229
240
 
@@ -250,14 +261,14 @@ Classic private DM mode is the base product mode. When Telegram private-chat Thr
250
261
  - 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.
251
262
  - Telegram never launches hidden Pi processes.
252
263
 
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.
264
+ In Settings → **🧵 Thread display**, choose Letters (default), Names, Directory Title or Directory Snake. Use `/name` for a custom Thread name and **Reset to automatic** to restore the selected naming style. Renaming preserves the Thread and its queued work. See [Thread Display](./docs/architecture.md#threaded-mode-multi-instance-bus) for naming details.
254
265
 
255
266
  | Mode | Best for | Runtime shape |
256
267
  | --- | --- | --- |
257
268
  | Classic DM | One running Pi instance and its active session controlled from one private bot chat | One polling owner, one queue/runtime surface |
258
269
  | Threaded Mode | Several visible Pi sessions sharing one bot | One leader owns transport; each private-chat Thread retains one session-qualified Workspace binding |
259
270
 
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).
271
+ **Restore** binds the selected live Pi session to the current Thread and releases the waiting input there. It may delete the session's former Thread and its messages; unfinished work can defer cleanup. An unconfirmed deletion is not automatically retried. See [Restore Behavior and Safety](./docs/multi-instance-bus.md#live-rebind-channel).
261
272
 
262
273
  ## Environment Configuration
263
274
 
@@ -292,9 +303,9 @@ Stable public entrypoints are documented in [Public API](./docs/public-api.md),
292
303
 
293
304
  ## Safety Boundaries
294
305
 
295
- Durable inbound admission is a **process-crash recovery** guarantee. Atomic private-file replacement preserves acknowledged journal authority and its journal-owned `acceptedThroughUpdateId` polling cursor across ordinary process exit, crash, kill, and replacement, but the extension does not flush files or parent directories for host/kernel/filesystem/device/power-loss durability. `telegram.json` contains configuration only. Keep `~/.pi/agent` on appropriately managed storage and backups if that stronger operational guarantee is required. Downgrading below `0.37.0` is unsafe: an older runtime cannot recover the journal's polling cursor and could repoll admitted updates. See [Durable Admission And Recovery](./docs/architecture.md#durable-admission-and-recovery).
306
+ Inbound recovery protects against **process crashes**, not power loss or storage failure. Keep `~/.pi/agent` on reliable storage with appropriate backups. Do not downgrade to a runtime that cannot read the current journal format: it could process already admitted input again. See [Durable Admission and Recovery](./docs/architecture.md#durable-admission-and-recovery).
296
307
 
297
- Runtime files use `tmp/pi-telegram` with only `state.json` and `logs.jsonl` at its root; pre-0.52.0 `tmp/telegram` is left untouched and never migrated. The transport section of `state.json` names a polling journal hosted in a session folder; successor leaders continue it rather than moving its cursor. Follower journals are session-owned. Unbound session families are disposable under the approved sweep policy; in-process `/new` adopts eligible pending input, while cold startup does not. See [Session-Owned Journal Storage](./docs/multi-instance-bus.md#session-owned-journal-storage) for compatibility fallbacks, loss boundaries and pending acceptance.
308
+ Configuration lives in `telegram.json`; runtime data lives under `<agent-dir>/tmp/pi-telegram`. Pending-input retention and cleanup rules are documented in [Session-Owned Journal Storage](./docs/multi-instance-bus.md#session-owned-journal-storage).
298
309
 
299
310
  `pi-telegram` intentionally does not:
300
311
 
@@ -57,17 +57,18 @@ export interface TelegramQueueBindingRuntime<TContext> {
57
57
  requestNextDispatchAnnouncement: () => void;
58
58
  cancelNextDispatchAnnouncement: () => void;
59
59
  watchdog: Queue.TelegramQueueDispatchWatchdogRuntime<TContext>;
60
+ onTurnEnd: (event: Pick<Pi.TurnEndEvent, "turnIndex" | "outcome">, ctx: TContext) => Pi.TurnEndEventResult | undefined;
60
61
  }
61
62
  export declare function createTelegramQueueBindingRuntime<TContext>(deps: {
62
63
  store: Queue.TelegramQueueStateStore<TContext>;
63
64
  queue: Pick<Runtime.TelegramBridgeRuntime["queue"], "allocateItemOrder">;
64
- lifecycle: Pick<Runtime.TelegramBridgeRuntime["lifecycle"], "isCompactionInProgress" | "hasDispatchPending">;
65
- activeTurn: Pick<Queue.TelegramActiveTurnStore, "has">;
65
+ lifecycle: Pick<Runtime.TelegramBridgeRuntime["lifecycle"], "isCompactionInProgress" | "hasDispatchPending"> & Partial<Pick<Runtime.TelegramBridgeRuntime["lifecycle"], "getActiveToolExecutions">>;
66
+ activeTurn: Pick<Queue.TelegramActiveTurnStore, "has"> & Partial<Pick<Queue.TelegramActiveTurnStore, "get">>;
66
67
  admission: {
67
68
  getSettlement: () => {
68
69
  onItemsDiscarded: (items: readonly Queue.TelegramQueueItem<TContext>[], ctx: TContext) => boolean;
69
70
  isItemReady: (item: Queue.TelegramQueueItem<TContext>) => boolean;
70
- onPromptHandedOff?: (item: Queue.PendingTelegramTurn, ctx: TContext) => boolean;
71
+ onPromptHandedOff?: (item: Pick<Queue.PendingTelegramTurn, "admissionReceipts">, ctx: TContext) => boolean;
71
72
  onControlSettled: (item: Queue.PendingTelegramControlItem<TContext>, ctx: TContext) => void;
72
73
  } | undefined;
73
74
  hasPendingQueueMutationForItem: (item: Queue.TelegramQueueItem<TContext>) => boolean;
@@ -77,6 +78,8 @@ export declare function createTelegramQueueBindingRuntime<TContext>(deps: {
77
78
  promptDispatch: Runtime.TelegramPromptDispatchRuntime<TContext>;
78
79
  isIdle: (ctx: TContext) => boolean;
79
80
  hasPendingMessages: (ctx: TContext) => boolean;
81
+ isContextCurrent?: (ctx: TContext) => boolean;
82
+ isContextAborted?: (ctx: TContext) => boolean;
80
83
  updateStatus: (ctx: TContext, error?: string) => void;
81
84
  sendTextReply: Queue.TelegramQueueDispatchRuntimeDeps<TContext>["sendTextReply"];
82
85
  sendUserMessage: Queue.TelegramQueueDispatchRuntimeDeps<TContext>["sendUserMessage"];
@@ -272,6 +275,7 @@ interface TelegramLifecycleBindingDeps {
272
275
  sendMarkdownReply: Queue.TelegramAgentEndHookRuntimeDeps<Queue.PendingTelegramTurn, Pi.ExtensionContext, Pi.AgentEndEvent["messages"][number], Keyboard.TelegramInlineKeyboardMarkup>["sendMarkdownReply"];
273
276
  sendTextReply: Queue.TelegramAgentEndHookRuntimeDeps<Queue.PendingTelegramTurn, Pi.ExtensionContext, Pi.AgentEndEvent["messages"][number], Keyboard.TelegramInlineKeyboardMarkup>["sendTextReply"] & NonNullable<OutboundHandlers.TelegramVoiceReplySenderDeps["sendTextReply"]>;
274
277
  dispatchNextQueuedTelegramTurn: (ctx: Pi.ExtensionContext) => void;
278
+ onSteerTurnEnd?: TelegramQueueBindingRuntime<Pi.ExtensionContext>["onTurnEnd"];
275
279
  onPromptHandedOff?: (turn: Queue.PendingTelegramTurn, ctx: Pi.ExtensionContext) => void;
276
280
  answerGuestQuery: TelegramApi.TelegramBridgeApiRuntime["answerGuestQuery"];
277
281
  deleteMessage: TelegramApi.TelegramBridgeApiRuntime["deleteMessage"];
@@ -296,5 +300,5 @@ interface TelegramLifecycleBindingDeps {
296
300
  /** Settled-session hint for owners that wait on agent idleness, such as old-tab cleanup after a live rebind. */
297
301
  onSessionSettled?: (ctx: Pi.ExtensionContext) => void;
298
302
  }
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;
303
+ 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, onSteerTurnEnd, onPromptHandedOff, answerGuestQuery, deleteMessage, sendGuestReply, editGuestReply, stopGuestPlaceholder, preparePreviewDelivery, finalizeMarkdownPreview, proactivePushTargetGetter, getAssistantRenderingMode, recordMessageOwnership, canSendAgentActivity, isSessionContextActive, getSessionGeneration, isTurnTransportActive, updateStatus, recordRuntimeEvent, onSessionSettled, }: TelegramLifecycleBindingDeps): void;
300
304
  export {};
@@ -119,6 +119,10 @@ export function createTelegramQueueBindingRuntime(deps) {
119
119
  return false;
120
120
  return settlement.onItemsDiscarded(durableItems, ctx) === true;
121
121
  };
122
+ const settlePromptPacket = (items, ctx) => {
123
+ const receipts = items.flatMap(item => item.admissionReceipts ?? []);
124
+ return receipts.length === 0 || deps.admission.getSettlement()?.onPromptHandedOff?.({ admissionReceipts: receipts }, ctx) === true;
125
+ };
122
126
  const mutation = Queue.createTelegramQueueMutationController({
123
127
  ...deps.store,
124
128
  hasPendingDispatch: deps.lifecycle.hasDispatchPending,
@@ -159,6 +163,13 @@ export function createTelegramQueueBindingRuntime(deps) {
159
163
  return false;
160
164
  return settlement.onPromptHandedOff(item, ctx) === true;
161
165
  },
166
+ steerDispatch: {
167
+ commitPacket: settlePromptPacket,
168
+ isContextCurrent: (ctx) => deps.isContextCurrent?.(ctx) === true,
169
+ canSubmit: (ctx) => deps.isContextAborted?.(ctx) === false &&
170
+ deps.isIdle(ctx) && !deps.hasPendingMessages(ctx) && !deps.lifecycle.isCompactionInProgress() &&
171
+ !deps.activeTurn.has() && (deps.lifecycle.getActiveToolExecutions?.() ?? 0) === 0,
172
+ },
162
173
  onControlSettled(item, ctx) {
163
174
  deps.admission.getSettlement()?.onControlSettled(item, ctx);
164
175
  },
@@ -172,8 +183,31 @@ export function createTelegramQueueBindingRuntime(deps) {
172
183
  ...deps.promptDispatch,
173
184
  sendUserMessage: deps.sendUserMessage,
174
185
  });
186
+ const steer = Queue.createTelegramSteerBoundaryRuntime({
187
+ ...deps.store,
188
+ getGeneration: deps.deferredDispatch.getGeneration,
189
+ isContextCurrent: (ctx) => deps.isContextCurrent?.(ctx) === true,
190
+ isBlocked: (ctx) => deps.isContextAborted?.(ctx) !== false || !deps.deferredDispatch.isBound() ||
191
+ deps.isIdle(ctx) || deps.hasPendingMessages(ctx) ||
192
+ deps.lifecycle.isCompactionInProgress() || deps.lifecycle.hasDispatchPending() ||
193
+ (deps.lifecycle.getActiveToolExecutions?.() ?? 0) > 0 ||
194
+ (deps.activeTurn.has() && !deps.activeTurn.get?.()),
195
+ isItemReady(item) {
196
+ return deps.transportStamp.isActive(item.transportStamp) &&
197
+ !deps.admission.hasPendingQueueMutationForItem(item) &&
198
+ (deps.admission.getSettlement()?.isItemReady(item) ?? (item.admissionReceipts?.length ?? 0) === 0);
199
+ },
200
+ isItemCurrent: (item) => deps.transportStamp.isActive(item.transportStamp) &&
201
+ !deps.admission.hasPendingQueueMutationForItem(item),
202
+ getActiveTurn: () => deps.activeTurn.get?.(),
203
+ commitPacket: settlePromptPacket,
204
+ discardItem: (item, ctx) => settleDiscardedItems([item], ctx),
205
+ updateStatus: deps.updateStatus,
206
+ recordRuntimeEvent: deps.recordRuntimeEvent,
207
+ });
175
208
  return {
176
209
  mutation,
210
+ onTurnEnd: steer.onTurnEnd,
177
211
  dispatchNext: dispatch.dispatchNext,
178
212
  requestNextDispatchAnnouncement: dispatch.requestNextDispatchAnnouncement,
179
213
  cancelNextDispatchAnnouncement: dispatch.cancelNextDispatchAnnouncement,
@@ -627,7 +661,7 @@ export function registerTelegramCommandsAndTools({ pi, agentDir, configStore, pe
627
661
  },
628
662
  });
629
663
  }
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, }) {
664
+ 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, onSteerTurnEnd, onPromptHandedOff, answerGuestQuery, deleteMessage, sendGuestReply, editGuestReply, stopGuestPlaceholder, preparePreviewDelivery, finalizeMarkdownPreview, proactivePushTargetGetter, getAssistantRenderingMode, recordMessageOwnership, canSendAgentActivity, isSessionContextActive = () => true, getSessionGeneration, isTurnTransportActive, updateStatus, recordRuntimeEvent, onSessionSettled, }) {
631
665
  const agentEndResetter = Runtime.createTelegramAgentEndResetter({
632
666
  abort,
633
667
  typing,
@@ -877,6 +911,12 @@ export function registerTelegramLifecycleRuntimeHooks({ pi, publicationRuntime,
877
911
  isSessionActive: isSessionContextActive,
878
912
  ...sessionLifecycleRuntime,
879
913
  ...agentLifecycleHooks,
914
+ ...(onSteerTurnEnd ? {
915
+ onTurnEnd(event, ctx) {
916
+ const result = onSteerTurnEnd(event, ctx);
917
+ return result ? { ...result, entries: [...event.entries, ...(result.entries ?? [])] } : undefined;
918
+ },
919
+ } : {}),
880
920
  onInput(event) {
881
921
  activityRuntime.recordInputSource(event.source ?? "unknown");
882
922
  },
@@ -37,6 +37,7 @@ export interface TelegramBusFollowerRegistrationRuntime<TContext> {
37
37
  target?: TelegramTarget;
38
38
  previousInstanceId?: string;
39
39
  restoreWorkspace?: boolean;
40
+ recoverConnection?: boolean;
40
41
  }) => Promise<boolean>;
41
42
  setContext: (ctx: TContext) => void | Promise<void>;
42
43
  disconnectFromLeader?: () => Promise<boolean>;
@@ -9,7 +9,7 @@ import { createHash } from "node:crypto";
9
9
  import { basename } from "node:path";
10
10
  import { isDeepStrictEqual } from "node:util";
11
11
  import { getTelegramBusTransportRetryPolicy, TELEGRAM_BUS_REGISTRATION_RETRY, } from "./bus-transport.js";
12
- import { createTelegramBusFollowerDeliveryIdentity, createTelegramBusForeignOwnedUpdateForwarder, createTelegramBusLocalServer, createTelegramBusRequestIdFactory, createUnauthorizedBusAck, rejectTelegramBusRequest, getTelegramBusProtocolCompatibility, getTelegramBusSocketPath, hasTelegramBusCapability, isTelegramBusEnvelopeAuthorized, parseTelegramBusEnvelope, resolveTelegramBusSocketPath, sendTelegramBusLocalEnvelope, TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT, TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_APPLY, TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_COMMAND_SET, TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SAVE, TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SETTLE, TELEGRAM_BUS_CAPABILITY_PRE_DISPATCH_REBIND_EXECUTION, TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY, TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE, TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME, TELEGRAM_BUS_HELD_COMMAND_CAPABILITIES, TELEGRAM_BUS_LIVE_REBIND_CAPABILITIES, TelegramBusLocalAuthorityError, } from "./bus.js";
12
+ import { createTelegramBusFollowerDeliveryIdentity, createTelegramBusForeignOwnedUpdateForwarder, createTelegramBusLocalServer, createTelegramBusRequestIdFactory, createUnauthorizedBusAck, rejectTelegramBusRequest, getTelegramBusProtocolCompatibility, getTelegramBusSocketPath, hasTelegramBusCapability, hasTelegramBusSteerQueueHandoffCapability, isTelegramBusEnvelopeAuthorized, parseTelegramBusEnvelope, resolveTelegramBusSocketPath, sendTelegramBusLocalEnvelope, TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT, TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_APPLY, TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SAVE, TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SETTLE, TELEGRAM_BUS_CAPABILITY_PRE_DISPATCH_REBIND_EXECUTION, TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY, TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE, TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME, TELEGRAM_BUS_HELD_COMMAND_CAPABILITIES, getTelegramBusHeldCommandCapabilities, TELEGRAM_BUS_LIVE_REBIND_CAPABILITIES, TelegramBusLocalAuthorityError, } from "./bus.js";
13
13
  import { parseTelegramUpdateJournalQueueOwner, } from "./journal.js";
14
14
  import { TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS, } from "./locks.js";
15
15
  import * as Machine from "./machine.js";
@@ -1792,7 +1792,7 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
1792
1792
  deps.setActiveAuthSecret?.(undefined);
1793
1793
  await deps.stopReceiving?.();
1794
1794
  };
1795
- const pendingHandoff = options
1795
+ const pendingHandoff = options && !options.recoverConnection
1796
1796
  ? undefined
1797
1797
  : getTelegramFollowerSessionHandoff();
1798
1798
  const pendingHandoffOptions = isTelegramFollowerSessionHandoffFresh(pendingHandoff)
@@ -1801,7 +1801,7 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
1801
1801
  previousInstanceId: pendingHandoff.instanceId,
1802
1802
  }
1803
1803
  : undefined;
1804
- const registrationOptions = options ?? pendingHandoffOptions;
1804
+ const registrationOptions = options ? { ...pendingHandoffOptions, ...options } : pendingHandoffOptions;
1805
1805
  const leaderSocketPath = leader.busSocketPath ??
1806
1806
  deps.getLeaderSocketPath?.() ??
1807
1807
  getTelegramBusSocketPath();
@@ -1821,6 +1821,8 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
1821
1821
  : "follower.register",
1822
1822
  requestId: registrationGeneration,
1823
1823
  auth: activeAuthSecret,
1824
+ ...(registrationOptions?.recoverConnection && !registrationOptions.restoreWorkspace
1825
+ ? { recoverConnection: true } : {}),
1824
1826
  registration: {
1825
1827
  instanceId: deps.instanceId,
1826
1828
  ...(registrationOptions?.previousInstanceId
@@ -2875,8 +2877,7 @@ export function createTelegramBusForwardedUpdateReceiverRuntime(deps) {
2875
2877
  ? [
2876
2878
  TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_APPLY,
2877
2879
  TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SETTLE,
2878
- TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_COMMAND_SET,
2879
- TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY,
2880
+ ...getTelegramBusHeldCommandCapabilities(commandInput.name),
2880
2881
  ]
2881
2882
  : []),
2882
2883
  ];
@@ -3033,6 +3034,10 @@ export function createTelegramBusForwardedUpdateReceiverRuntime(deps) {
3033
3034
  result: receipt,
3034
3035
  };
3035
3036
  }
3037
+ if (envelope.payload.queueLane === "steer" &&
3038
+ ![deps.getLocalProtocol?.(), deps.getLeaderProtocol?.()].every(hasTelegramBusSteerQueueHandoffCapability)) {
3039
+ throw new Error("Steer queue handoff capability was not negotiated.");
3040
+ }
3036
3041
  if (!deps.handleQueueHandoff) {
3037
3042
  throw new Error("Telegram bus receiver cannot accept queue handoff payloads.");
3038
3043
  }
@@ -187,6 +187,7 @@ export declare function createTelegramBusInstanceLifecycleAnnouncement(input: {
187
187
  }): TelegramBusFollowerLifecycleAnnouncement;
188
188
  export declare function createTelegramBusFollowerTargetProvisioner(deps: TelegramBusFollowerTargetProvisionerDeps): (registration: TelegramBusInstanceRegistration, options?: {
189
189
  existingWorkspaceBindingOnly?: boolean;
190
+ recoverConnection?: boolean;
190
191
  }) => Promise<(TelegramTarget & {
191
192
  slot?: string;
192
193
  threadName?: string;
@@ -248,6 +249,7 @@ export interface TelegramBusLeaderEnvelopeHandlerDeps {
248
249
  }>) => Promise<unknown> | unknown;
249
250
  provisionFollowerTarget?: (registration: TelegramBusInstanceRegistration, options?: {
250
251
  existingWorkspaceBindingOnly?: boolean;
252
+ recoverConnection?: boolean;
251
253
  }) => Promise<(TelegramTarget & {
252
254
  slot?: string;
253
255
  threadName?: string;
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import { isDeepStrictEqual } from "node:util";
8
8
  import { getTelegramBusTransportRetryPolicy } from "./bus-transport.js";
9
- import { createTelegramBusLocalServer, createUnauthorizedBusAck, rejectTelegramBusRequest, getTelegramBusEnvelopeTrafficClass, getTelegramBusFollowerSocketPath, getTelegramBusProtocolCompatibility, hasTelegramBusCapability, isTelegramBusEnvelopeAuthorized, parseTelegramBusEnvelope, sendTelegramBusLocalEnvelope, stripTelegramBusApiMetadata, TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT, TELEGRAM_BUS_CAPABILITY_DURABLE_FOLLOWER_ADMISSION, TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF, TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY, TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE, TELEGRAM_BUS_CAPABILITY_PRE_DISPATCH_REBIND, TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE, TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME, } from "./bus.js";
9
+ import { createTelegramBusLocalServer, createUnauthorizedBusAck, rejectTelegramBusRequest, getTelegramBusEnvelopeTrafficClass, getTelegramBusFollowerSocketPath, getTelegramBusProtocolCompatibility, hasTelegramBusCapability, isTelegramBusEnvelopeAuthorized, parseTelegramBusEnvelope, sendTelegramBusLocalEnvelope, stripTelegramBusApiMetadata, TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT, TELEGRAM_BUS_CAPABILITY_DURABLE_FOLLOWER_ADMISSION, TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF, TELEGRAM_BUS_CAPABILITY_CONNECT_RECOVERY, hasTelegramBusSteerQueueHandoffCapability, TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY, TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE, TELEGRAM_BUS_CAPABILITY_PRE_DISPATCH_REBIND, TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE, TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME, } from "./bus.js";
10
10
  import { escapeHtml } from "./rendering.js";
11
11
  import * as Sync from "./sync.js";
12
12
  import { parseTelegramIntegerId as asInteger, } from "./target.js";
@@ -881,6 +881,16 @@ export function createTelegramBusFollowerTargetProvisioner(deps) {
881
881
  if (typeof chatId !== "number")
882
882
  return registration.target;
883
883
  await deps.topicTargetStore.load();
884
+ if (options?.recoverConnection && !options.existingWorkspaceBindingOnly) {
885
+ const epoch = deps.getCurrentLeaderEpoch?.();
886
+ await Threads.resetTelegramConnectionProvisioning({
887
+ store: deps.topicTargetStore, owner: "manual-follower", instanceId: registration.instanceId,
888
+ profileKey: registration.profileKey ?? `manual:${registration.instanceId}`,
889
+ isCurrent: () => !deps.getCurrentLeaderEpoch ||
890
+ (epoch !== undefined && deps.getCurrentLeaderEpoch() === epoch),
891
+ recordEvent: deps.recordRuntimeEvent,
892
+ });
893
+ }
884
894
  let restoredPredecessor;
885
895
  if (options?.existingWorkspaceBindingOnly && registration.cwd && registration.sessionId &&
886
896
  !registration.previousInstanceId && deps.getLiveFollowers && deps.isFollowerProcessAlive) {
@@ -917,6 +927,7 @@ export function createTelegramBusFollowerTargetProvisioner(deps) {
917
927
  ? deps.topicTargetStore.claimWorkspaceIdentity(registration.cwd, registration.instanceId, registration.previousInstanceId ?? restoredPredecessor?.instanceId, {
918
928
  existingBindingOnly: options?.existingWorkspaceBindingOnly === true,
919
929
  sessionId: registration.sessionId,
930
+ provisionProfileKey: registration.profileKey ?? `manual:${registration.instanceId}`,
920
931
  onCapacityUnavailable() {
921
932
  capacityUnavailable = true;
922
933
  },
@@ -1129,11 +1140,6 @@ export function createTelegramBusFollowerTargetProvisioner(deps) {
1129
1140
  };
1130
1141
  }
1131
1142
  const probeRequiredRecord = reconnectRecord?.status === "probe-required";
1132
- const exactSessionHandoff = crossSessionReuse &&
1133
- !!requestedTarget &&
1134
- registration.previousInstanceId === reconnectRecord?.instanceId &&
1135
- requestedTarget.chatId === reconnectRecord.target.chatId &&
1136
- requestedTarget.threadId === reconnectRecord.target.threadId;
1137
1143
  const requiresVisibilityProbe = crossSessionReuse ||
1138
1144
  probeRequiredRecord ||
1139
1145
  recoverableTarget !== undefined;
@@ -1151,18 +1157,14 @@ export function createTelegramBusFollowerTargetProvisioner(deps) {
1151
1157
  if (requiresVisibilityProbe && connectedAnnouncement) {
1152
1158
  try {
1153
1159
  assertRestoreRegistration();
1154
- await deps.callApi(exactSessionHandoff ? "sendChatAction" : "sendMessage", exactSessionHandoff
1155
- ? {
1156
- chat_id: connectedAnnouncement.target.chatId,
1157
- message_thread_id: connectedAnnouncement.target.threadId,
1158
- action: "typing",
1159
- }
1160
- : {
1161
- chat_id: connectedAnnouncement.target.chatId,
1162
- message_thread_id: connectedAnnouncement.target.threadId,
1163
- text: connectedAnnouncement.text,
1164
- parse_mode: connectedAnnouncement.parseMode,
1165
- });
1160
+ await deps.callApi(
1161
+ // Typing is presence, not evidence that a retained Thread still accepts messages.
1162
+ "sendMessage", {
1163
+ chat_id: connectedAnnouncement.target.chatId,
1164
+ message_thread_id: connectedAnnouncement.target.threadId,
1165
+ text: connectedAnnouncement.text,
1166
+ parse_mode: connectedAnnouncement.parseMode,
1167
+ });
1166
1168
  assertRestoreRegistration();
1167
1169
  if (recoverableTarget || probeRequiredRecord) {
1168
1170
  const activatedRecord = deps.topicTargetStore.upsert({
@@ -2120,6 +2122,10 @@ export function createTelegramBusLeaderEnvelopeHandler(deps) {
2120
2122
  !hasTelegramBusCapability(recipient?.protocol, TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF)) {
2121
2123
  return rejectTelegramBusRequest(envelope.requestId, "Telegram queue handoff capability was not negotiated.");
2122
2124
  }
2125
+ if (envelope.payload.queueLane === "steer" &&
2126
+ ![deps.protocolIdentity, donor.protocol, recipient?.protocol].every(hasTelegramBusSteerQueueHandoffCapability)) {
2127
+ return rejectTelegramBusRequest(envelope.requestId, "Steer queue handoff capability was not negotiated.");
2128
+ }
2123
2129
  if (!recipient?.registrationGeneration ||
2124
2130
  envelope.recipientRegistrationGeneration !==
2125
2131
  recipient.registrationGeneration) {
@@ -2257,6 +2263,9 @@ export function createTelegramBusLeaderEnvelopeHandler(deps) {
2257
2263
  (hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE) &&
2258
2264
  hasTelegramBusCapability(envelope.registration.protocol, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE));
2259
2265
  const restoringWorkspace = envelope.kind === "follower.restoreWorkspace";
2266
+ const recoverConnection = envelope.kind === "follower.register" && envelope.recoverConnection === true;
2267
+ const supportsConnectionRecovery = [deps.protocolIdentity, envelope.registration.protocol]
2268
+ .every(identity => hasTelegramBusCapability(identity, TELEGRAM_BUS_CAPABILITY_CONNECT_RECOVERY));
2260
2269
  const supportsWorkspaceAutoConnect = hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT) &&
2261
2270
  hasTelegramBusCapability(envelope.registration.protocol, TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT);
2262
2271
  if (!compatibility.compatible ||
@@ -2264,7 +2273,8 @@ export function createTelegramBusLeaderEnvelopeHandler(deps) {
2264
2273
  !readerCompatible() ||
2265
2274
  (envelope.registration.cwd !== undefined &&
2266
2275
  envelope.registration.sessionId === undefined) ||
2267
- (restoringWorkspace && !supportsWorkspaceAutoConnect)) {
2276
+ (restoringWorkspace && !supportsWorkspaceAutoConnect) ||
2277
+ (recoverConnection && !supportsConnectionRecovery)) {
2268
2278
  return {
2269
2279
  kind: "bus.ack",
2270
2280
  requestId: envelope.requestId,
@@ -2305,7 +2315,7 @@ export function createTelegramBusLeaderEnvelopeHandler(deps) {
2305
2315
  if (hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE) &&
2306
2316
  !deps.commitFollowerRegistration)
2307
2317
  throw new Error("Workspace Restore registration publication protection is unavailable.");
2308
- const target = await deps.provisionFollowerTarget?.(envelope.registration, { existingWorkspaceBindingOnly: restoringWorkspace });
2318
+ const target = await deps.provisionFollowerTarget?.(envelope.registration, { existingWorkspaceBindingOnly: restoringWorkspace, ...(recoverConnection ? { recoverConnection: true } : {}) });
2309
2319
  if (restoringWorkspace && !target) {
2310
2320
  return {
2311
2321
  kind: "bus.ack",
@@ -3026,6 +3036,10 @@ export function createTelegramBusLeaderRuntime(deps) {
3026
3036
  !hasTelegramBusCapability(recipient?.protocol, TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF)) {
3027
3037
  return rejectTelegramBusRequest(input.requestId, "Telegram queue handoff capability was not negotiated.");
3028
3038
  }
3039
+ if (input.payload.queueLane === "steer" &&
3040
+ ![deps.protocolIdentity, recipient?.protocol].every(hasTelegramBusSteerQueueHandoffCapability)) {
3041
+ return rejectTelegramBusRequest(input.requestId, "Steer queue handoff capability was not negotiated.");
3042
+ }
3029
3043
  if (!recipient?.registrationGeneration ||
3030
3044
  input.recipientRegistrationGeneration !== recipient.registrationGeneration) {
3031
3045
  return rejectTelegramBusRequest(input.requestId, "Stale Telegram queue handoff recipient registration generation.");
package/dist/lib/bus.d.ts CHANGED
@@ -36,6 +36,9 @@ export declare function createTelegramBusProcessRuntime(input: {
36
36
  export declare function createTelegramBusAuthSecret(): string;
37
37
  export declare const TELEGRAM_BUS_CAPABILITY_DURABLE_FOLLOWER_ADMISSION: "durable-follower-admission-v1";
38
38
  export declare const TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF: "queue-handoff-v1";
39
+ /** Unspent Steer lane codec; independent from baseline receipt handoff support. */
40
+ export declare const TELEGRAM_BUS_CAPABILITY_QUEUE_STEER: "queue-steer-v1";
41
+ export declare const TELEGRAM_BUS_CAPABILITY_CONNECT_RECOVERY: "explicit-connect-recovery-v1";
39
42
  export declare const TELEGRAM_BUS_CAPABILITY_INPUT_CUSTODY_REFERENCE: "input-custody-reference-v1";
40
43
  export declare const TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME: "workspace-thread-rename-v1";
41
44
  export declare const TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE: "thread-display-mode-v1";
@@ -56,6 +59,9 @@ export declare const TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_COMMAND_SET: "live-thre
56
59
  /** Every live rebinding needs the save/apply/settle trio; held command selections additionally need these two. */
57
60
  export declare const TELEGRAM_BUS_LIVE_REBIND_CAPABILITIES: readonly ["live-thread-rebind-save-v1", "live-thread-rebind-apply-v1", "live-thread-rebind-settle-v1"];
58
61
  export declare const TELEGRAM_BUS_HELD_COMMAND_CAPABILITIES: readonly ["live-thread-rebind-command-set-v1", "selected-menu-delivery-v1"];
62
+ /** Start/help are restricted menu aliases, negotiated separately from the original held-command set. */
63
+ export declare const TELEGRAM_BUS_CAPABILITY_HELD_MENU_ALIASES: "held-menu-aliases-v1";
64
+ export declare function getTelegramBusHeldCommandCapabilities(name: string): readonly string[];
59
65
  export interface TelegramBusSelectedCommandInput {
60
66
  name: string;
61
67
  target: TelegramTarget & {
@@ -192,6 +198,7 @@ export declare function createTelegramBusProtocolIdentity(input: {
192
198
  }): TelegramBusProtocolIdentity;
193
199
  export declare function createTelegramCurrentBusProtocolIdentity(capabilities?: readonly string[]): TelegramBusProtocolIdentity;
194
200
  export declare function hasTelegramBusCapability(identity: TelegramBusProtocolIdentity | undefined, capability: string): boolean;
201
+ export declare function hasTelegramBusSteerQueueHandoffCapability(identity: TelegramBusProtocolIdentity | undefined): boolean;
195
202
  export declare function getTelegramInputCustodyPeerReadiness(followers: readonly Pick<TelegramBusFollowerView, "registrationGeneration" | "protocol">[]): ("ready" | "legacy" | "unknown")[];
196
203
  /** Both peers advertise every required capability over a compatible protocol; reads only the two identities. */
197
204
  export declare function hasTelegramBusSharedCapabilities(local: TelegramBusProtocolIdentity, remote: TelegramBusProtocolIdentity | undefined, required: readonly string[]): boolean;
@@ -324,6 +331,7 @@ export declare function createTelegramBusFollowerSourceReferenceDeliveryIdentity
324
331
  export type TelegramBusEnvelope = ({
325
332
  kind: "follower.register";
326
333
  requestId: string;
334
+ recoverConnection?: true;
327
335
  registration: TelegramBusInstanceRegistration;
328
336
  } | {
329
337
  kind: "follower.restoreWorkspace";