@llblab/pi-telegram 0.27.12 โ†’ 0.29.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.
package/README.md CHANGED
@@ -65,7 +65,7 @@ Open the bot DM and send:
65
65
  /start
66
66
  ```
67
67
 
68
- The first Telegram user to message the bot becomes the allowed owner. Other users are ignored.
68
+ The first Telegram user to message the bot becomes the allowed owner. Other users are ignored. After required pairing state is persisted, `/start` is admitted independently from best-effort menu rendering and BotFather command-list synchronization, so either Telegram side effect can fail or remain in flight without stopping later inbound updates.
69
69
 
70
70
  ### 5. Enable optional bot capabilities in BotFather
71
71
 
@@ -73,7 +73,7 @@ Enable the optional capabilities the bridge needs in [@BotFather](https://t.me/B
73
73
 
74
74
  1. Enable guest mode so the bot can answer mentions and replies in chats where it is not a member.
75
75
  2. Enable private-chat Threaded Mode; when it is available, one live instance becomes the profile's leader and later visible Pi instances register as followers. Without it, the bridge stays in classic single-owner DM mode.
76
- 3. Make the bot an administrator in any chat where the queue reaction shortcuts (๐Ÿ‘ promote, ๐Ÿ‘Ž remove) should work. Reaction updates require admin rights, so the shortcuts silently do nothing in non-admin chats; private chats deliver reactions without admin rights.
76
+ 3. Make the bot an administrator in any chat where the queue reaction shortcuts (๐Ÿ‘ prioritize, ๐Ÿ‘Ž suppress) should work. Reaction updates require admin rights, so the shortcuts silently do nothing in non-admin chats; private chats deliver reactions without admin rights.
77
77
 
78
78
  ## What It Feels Like
79
79
 
@@ -121,7 +121,7 @@ Enable the optional capabilities the bridge needs in [@BotFather](https://t.me/B
121
121
  | 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. |
122
122
  | 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 removes only the replaced old tab, so Telegram client state repairs without orphan controls. |
123
123
  | 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. |
124
- | Runtime diagnostics | Use `/telegram-status` and recent runtime events for connection, role, queue, transport, and failure evidence. | Debugging lives in the operator surface instead of hidden logs only. |
124
+ | 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. |
125
125
  | Safety and ownership | Pair one owner, lock transport, scope targets, and reject fake terminal behavior. | Remote access remains explicit, bounded, and understandable. |
126
126
 
127
127
  ## Core Loop
@@ -163,8 +163,8 @@ Run these inside Pi.
163
163
  | `/telegram-setup <profile>` | Save or update a named-profile bot token |
164
164
  | `/telegram-connect` / `/telegram-connect default` | Activate `profiles.default` and acquire its transport ownership |
165
165
  | `/telegram-connect <profile>` | Activate a named profile and acquire its transport ownership |
166
- | `/telegram-disconnect` | Confirm, then stop polling, release ownership, and delete this instance's Threaded Mode tab; graceful Pi quit does the same without prompting when automatic cleanup is enabled |
167
- | `/telegram-status` | Inspect connection, mode, queue, transport, and recent diagnostics |
166
+ | `/telegram-disconnect` | Confirm, then stop polling, release ownership, and delete this instance's Threaded Mode tab; graceful Pi quit always preserves restart ownership and independently deletes the tab only when automatic cleanup is enabled |
167
+ | `/telegram-status` | Inspect connection, mode, separate polling/worker progress, journal depth, queue, transport, automatic retry state, and recent diagnostics |
168
168
 
169
169
  Named profile identifiers contain only lowercase ASCII letters and digits (maximum 32 characters); `default`, `main`, and `active` remain reserved. If graceful thread deletion was interrupted, a same-profile replacement reuses its still-active thread and cancels the superseded cleanup instead of deleting and recreating the tab during startup.
170
170
 
@@ -176,7 +176,7 @@ Named profile identifiers contain only lowercase ASCII letters and digits (maxim
176
176
 
177
177
  ### Queue Runtime
178
178
 
179
- Messages sent while Pi is busy become queued turns. Priority lanes support control actions and model-switch continuations. Queue controls let you inspect, delete, promote, and dispatch work from Telegram without touching the terminal. 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.
179
+ Messages sent while Pi is busy become queued turns. Priority lanes support control actions and model-switch continuations. Queue controls let you inspect, delete, promote, and dispatch work from Telegram without touching the terminal. Reaction shortcuts are reversible while a turn is waiting: priority reactions move it ahead, removal reactions suppress it, and changing or removing those reactions restores the corresponding priority or default state. 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.
180
180
 
181
181
  ### Native Rich Markdown
182
182
 
@@ -201,6 +201,7 @@ Classic private DM mode is the base product mode. When Telegram private-chat Thr
201
201
  - One live leader owns `getUpdates`.
202
202
  - Followers are visible Pi processes started by the operator.
203
203
  - Each connected instance gets a Telegram thread target.
204
+ - Queued work for a live follower transfers through authenticated exact-journal handoff rather than replaying under the transport owner.
204
205
  - Follower session replacement automatically reconnects the new session context to the same thread instead of requiring another manual connect.
205
206
  - Unknown threads are preserved and offered explicit reroute/restore choices.
206
207
  - Telegram never launches hidden Pi processes.
@@ -243,6 +244,8 @@ Stable public entrypoints are documented in [Public API](./docs/public-api.md),
243
244
 
244
245
  ## Safety Boundaries
245
246
 
247
+ Durable inbound admission is a **process-crash recovery** guarantee. Atomic private-file replacement preserves acknowledged journal authority 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. Keep `~/.pi/agent` on appropriately managed storage and backups if that stronger operational guarantee is required. Before downgrading below `0.28.0`, run `node scripts/check-downgrade.mjs`; a blocked result means `0.28.x` must drain the retained journal first. See [Durable Admission And Recovery](./docs/architecture.md#durable-admission-and-recovery).
248
+
246
249
  `pi-telegram` intentionally does not:
247
250
 
248
251
  - Spawn hidden Pi follower processes.
@@ -254,7 +257,7 @@ Stable public entrypoints are documented in [Public API](./docs/public-api.md),
254
257
 
255
258
  Telegram is a companion surface around a live Pi runtime, not a second runtime. It can compact the current session, but it cannot create, resume, fork, browse, or switch sessions until Pi exposes safe public extension APIs for those operations.
256
259
 
257
- A Telegram prompt is a normal model turn in the active Pi session and therefore inherits that session's active post-compaction context; the bridge does not make token cost proportional only to the new mobile message. Current releases keep per-turn guidance small and transient, with detailed bridge instructions available on demand through `telegram_help` instead of persisted in every user turn. Disconnecting removes pi-telegram's model tools and prompt guidance from later requests until direct ownership or follower registration returns, without changing other active Pi tools. Pi session JSONL contains model history; profile-scoped pi-telegram `logs*.jsonl` contains redacted operational events and is never model context.
260
+ A Telegram prompt is a normal model turn in the active Pi session and therefore inherits that session's active post-compaction context; the bridge does not make token cost proportional only to the new mobile message. The bundled `telegram-bridge` Skill owns agent operation and `button-console` provides optional generated-button CLI navigation. Disconnecting removes pi-telegram's delivery tools and transient routing guidance from later requests until direct ownership or follower registration returns, without changing other active Pi tools. Pi session JSONL contains model history; profile-scoped pi-telegram `logs*.jsonl` contains redacted operational events and is never model context.
258
261
 
259
262
  ## Documentation Map
260
263
 
package/api/updates.ts CHANGED
@@ -5,7 +5,12 @@
5
5
  */
6
6
 
7
7
  export {
8
+ assertTelegramUpdateExecutionCurrent,
9
+ carryTelegramUpdateExecutionFence,
10
+ createTelegramUpdateExecutionFenceGuard,
11
+ getTelegramUpdateExecutionFence,
8
12
  registerTelegramUpdateHandler,
13
+ type TelegramUpdateExecutionFence,
9
14
  type TelegramUpdateHandler,
10
15
  type TelegramUpdateHandlerVerdict,
11
16
  } from "../lib/updates.ts";
@@ -47,7 +47,7 @@ A Telegram destination follows a Pi instance, not an immutable Pi session file.
47
47
 
48
48
  Pi session JSONL and pi-telegram runtime JSONL serve different purposes. Pi session files contain model conversation, tool, usage, branch, and compaction entries. Profile-scoped `logs.jsonl` / `logs.<profile>.jsonl` contain redacted bridge operations from one or more instances and never become model context. Sharing a Telegram profile or working directory does not by itself merge Pi session identities or model histories.
49
49
 
50
- A Telegram prompt is a normal Pi model turn. It inherits the active post-compaction context just like a TUI prompt in the same session; pi-telegram does not promise context isolation or token cost proportional only to the new message. Current prompt guidance uses a small transient system note and the on-demand `telegram_help` tool instead of persisting the former large guidance suffix in every user turn. Existing session files created by older versions may still contain those historical repeated suffixes until session replacement or compaction removes them from active context.
50
+ A Telegram prompt is a normal Pi model turn. It inherits the active post-compaction context just like a TUI prompt in the same session; pi-telegram does not promise context isolation or token cost proportional only to the new message. The bundled `telegram-bridge` Skill owns agent operation, while a small authority-aware system note routes applicable turns to it. Existing session files created by older versions may still contain historical repeated guidance until session replacement or compaction removes it from active context.
51
51
 
52
52
  The repository uses a **Flat Domain DAG**:
53
53
 
@@ -60,17 +60,18 @@ The repository uses a **Flat Domain DAG**:
60
60
 
61
61
  ### Domain Ownership Map
62
62
 
63
- - `index.ts`: composition root for live ports, session-state ports, transport adapters, and lifecycle registration. It exposes cross-domain wiring but does not own mutable domain state or reusable adapters.
63
+ - `index.ts`: composition root for live ports, domain runtime construction, cross-domain port wiring, and lifecycle registration. It exposes wiring but owns no process identity, journal-binding selection, mutable late-binding state, admission lifecycle selection, reusable policy, or low-level adapter mechanics; those belong to `bus`, `journal`, `prompts`, `activity-verbosity`, `updates`, and other named domains.
64
64
  - `api`: Bot API helpers, retries, uploads/downloads, temp cleanup, byte limits, chat actions, lazy token clients, and API error recording.
65
65
  - `config` / `setup`: `telegram.json`, bot token setup, named bot/session profiles, first-user pairing, authorization, env fallback, atomic persistence, effective config views, and live config accessors.
66
- - `locks` / `polling`: extension-local transport owner storage, exact-owner epoch exposure, process-global reload generations, owner-aware polling lifecycle/takeover/follower registration, and the cohesive classic-vs-Threaded capability state/monitor/observation/polling orchestration. Polling also owns long-poll controller state, offset admission/persistence, and poll-loop wiring.
67
- - `bus` / `bus-api` / `bus-leader` / `bus-follower` / `ownership` / `target`: Threaded Mode multi-instance bus contracts, profile-scoped process/endpoint identity, local leader/follower IPC, leader-only orchestration, follower-side manual registration/session runtime, follower-routed Bot API calls, live message ownership, and `{ chatId, threadId? }` target identity. `bus` owns shared protocol, process identity, profile-aware local endpoints, and IPC primitives; `bus-leader` owns leader runtime, leader envelope handling, activation scheduling, and leader polling/server/prune orchestration; `bus-follower` owns process-stable manual-follower keys plus this Pi instance's follower-side registration, active leader-auth/transient-election control state, heartbeat, one-sequence authenticated client assembly, forwarded-update adaptation/receiving, recovery retry defaults, and routed API caller without any process spawning.
68
- - `sync`: demand-driven Telegram reconciliation, mutable sync-slice state, nested provisioning activity, and local assumption policy. It does not own a complete Telegram bot read-model; Bot API lacks a complete topic/thread listing surface. It owns sync slices, invalidation triggers, config-persist invalidation sequencing, stale-topic API recovery adaptation, observation intake, status/debug freshness, and reconciliation scheduling across bot identity, pairing assumptions, live target bindings, reservations, and transport health after meaningful observable signals. It should call narrower domain primitives rather than letting `index.ts`, `threads`, or `status` accumulate cross-cutting reconciliation policy.
66
+ - `locks` / `polling`: extension-local transport owner storage, exact-owner epoch exposure, process-global reload generations, owner-aware polling lifecycle/takeover/follower registration, and classic-vs-Threaded capability orchestration. Polling owns long-poll state, worker-before-poller startup, strict batch validation, journal-before-offset admission, one offset commit per response, and non-awaited worker signaling.
67
+ - `journal`: private profile/bot-scoped raw-update authority with strict v1 identity/schema validation, exact deduplication, bounded transaction-serialized `0600` publication, process/session/acquisition-bound prompt/control receipts, owner-fenced completion, durable retry state, and generic removal that rejects queued or legacy failed authority. Runtime/recovery identity separates token rotation from future proof-gated queue-owner recovery.
68
+ - `bus` / `bus-api` / `bus-leader` / `bus-follower` / `ownership` / `target`: Threaded Mode multi-instance bus contracts, profile-scoped process/endpoint identity, local leader/follower IPC, leader-only orchestration, follower-side manual registration/session runtime, follower-routed Bot API calls, live message ownership, and `{ chatId, threadId? }` target identity. `bus` owns protocol v1 independently from package build, canonical capabilities, compatibility, process identity, profile-aware endpoints, and IPC primitives. Registration rejects missing/mismatched protocol before provisioning while preserving compatible package skew; negotiated identity reaches status/state. Leader runtime, leader envelope handling, follower assembly, and follower registration construction require explicit protocol identity, preventing identity-less composition at both production and low-level runtime boundaries. `bus-leader` owns leader envelope handling and polling/server/prune orchestration; `bus-follower` owns registration/ack negotiation, active leader-auth/election state, heartbeat, authenticated clients, forwarded receiving, and recovery.
69
+ - `sync`: demand-driven Telegram reconciliation, mutable sync-slice state, nested provisioning activity, and local assumption policy. It does not own a complete Telegram bot read-model; Bot API lacks a complete topic/thread listing surface. It owns sync slices, invalidation triggers, config-persist invalidation sequencing, stale-topic API recovery adaptation, observation intake, status/debug freshness, paired manual-disconnect/session-restart cleanup assembly, and reconciliation scheduling across bot identity, pairing assumptions, live target bindings, reservations, and transport health after meaningful observable signals. It should call narrower domain primitives rather than letting `index.ts`, `threads`, or `status` accumulate cross-cutting reconciliation policy.
69
70
  - `thread-reconciler`: Threaded Mode control-plane planning for Telegram thread/tab lifecycle. It owns the reconciliation state machine (`stable`, `provisioning`, `sync-required`, `cleanup-required`), pure plans, proof-before-delete rules, pending-provision protection, fresh-creation grace windows, leader-epoch checks, and the single policy authority for destructive thread cleanup actions. It excludes live Telegram API calls, inbound routing, menu rendering, and direct persistence.
70
- - `threads`: Telegram UI thread/tab binding state mapped to Bot API `message_thread_id` / `ForumTopic` transport. Owns leader and current-instance identity state, profile-bound same-process leader session handoff, status projections, slot allocation from the current extension state, baked compact thread-name selection, current binding persistence, and primitive thread provision helpers. It should not persist stale/offline/failed target history, own destructive cleanup policy, grow into the general Telegram synchronization domain, or expose a rename tool.
71
- - `updates` / `routing`: update classification, authorization planning, callbacks, edited messages, reactions, target-owner forwarding, inbound bus ownership/live-target/local-label projection, and inbound route composition.
72
- - `media` / `text-groups` / `time-injection` / `turns` / `inbound`: inbound text/media/file extraction, rich-message reply-context plaintext recovery, media-group debounce, long-text coalescing, optional `[time]` context, handler execution, and prompt-turn assembly/editing.
73
- - `queue`: queue item contracts, profile/token transport-generation stamping, lane admission/order, readiness gates, mutations, dispatch runtime, prompt/control enqueueing, and session/agent/tool lifecycle sequencing.
71
+ - `threads`: Telegram UI thread/tab binding state mapped to Bot API `message_thread_id` / `ForumTopic` transport. Owns leader and current-instance identity state, active-turn โ†’ follower โ†’ leader target preference, matching status projection assembly, profile-bound same-process leader session handoff, slot allocation from current extension state, baked compact thread-name selection, current binding persistence, and primitive thread provision helpers. It should not persist stale/offline/failed target history, own destructive cleanup policy, grow into the general Telegram synchronization domain, or expose a rename tool.
72
+ - `updates` / `routing`: update classification, authorization, callbacks, edits, reactions, forwarding, and inbound composition. `updates` owns production journal workers, leader/follower admission lifecycle construction, binding and settlement selection, queue-handoff projection across recipient journals/admission/IPC/live queue state, process/session queue-owner projection, post-public source binding, exact-signal late settlement, durable receipt readiness, same-process claim reconstruction, and structural worker state. `routing` converts message, callback, guest, section, reroute, and control admissions into exact receipts.
73
+ - `media` / `text-groups` / `time-injection` / `turns` / `inbound`: inbound extraction, rich reply plaintext, grouped debounce, split-text coalescing, optional time context, handlers, and prompt assembly/editing. Group replay replaces stale generation-local message/report bindings without duplicating content.
74
+ - `queue`: queue contracts, transport stamps, lanes, readiness, mutations, dispatch, enqueueing, and lifecycle sequencing. Durable admission uses deterministic receipts, canonical source sets, replay dedupe, multiple folded-history receipts, append-before-dispatch reporting, exact handoff/control/discard settlement, and a readiness gate. Receipt-bearing inactive-profile work is preserved after current-profile work rather than dropped.
74
75
  - `runtime`: session-local coordination primitives: counters, flags, setup guard, abort handler, typing timers, dispatch flags, and reset binding.
75
76
  - `model` / `menu-model` / `menu-thinking` / `menu-status` / `menu-queue` / `menu-settings` / `menu` / `commands`: model identity, thinking levels, scoped model handling, menu render/callback behavior, slash commands, bot commands, and interactive controls.
76
77
  - `sections`: Telegram menu-section registry, opaque section callback tokens, render/callback dispatch, safe section ports, and diagnostics.
@@ -82,7 +83,7 @@ The repository uses a **Flat Domain DAG**:
82
83
  - `outbound`: outbound text transformations, voice/button artifact delivery, and generated callback actions.
83
84
  - `outbound-attachments`: `telegram_attach`, queued outbound files, stat/limit checks, ordinary photo/document delivery, and narrow single-artifact Rich Message planning/sending for probe-confirmed photo/video/audio formats. It owns known-failure fallback eligibility and ambiguous-send no-replay classification through structural error contracts without importing Bot API helpers.
84
85
  - `status` / `logs`: status bar/status-message rendering, queue-lane summaries, the structural redacted event ring, profile-aware JSONL scope/reset/append behavior, exact-owner destructive commits, fail-soft synchronous and queued diagnostics persistence, status snapshot scheduling, and grouped diagnostics. `status` remains a structural leaf; `logs` composes filesystem evidence with status projections and contains every persistence failure so diagnostics cannot terminate or poison the runtime queue.
85
- - `bindings` / `lifecycle` / `prompts` / `prompt-templates` / `pi`: Pi-facing command/tool/hook registration and cohesive cross-domain binding assembly; session-generation fencing and start/shutdown sequencing across Queue, grouped input, Delivery, polling, capability monitor, watchdog, follower refresh, and assistant-output projection; Telegram prompt guidance; prompt-template discovery/expansion; and centralized direct Pi SDK imports.
86
+ - `bindings` / `lifecycle` / `prompts` / `prompt-templates` / `pi`: Pi-facing command/tool/hook registration and cohesive cross-domain binding assembly, including queue mutation/dispatch/watchdog composition over admission and transport ports; session-generation fencing and start/shutdown sequencing across Queue, grouped input, Delivery, polling, capability monitor, follower refresh, and assistant-output projection; Telegram prompt guidance; prompt-template discovery/expansion; and centralized direct Pi SDK imports.
86
87
  - `command-templates`: shell-free command-template helpers, composition expansion, placeholder substitution, executable resolution, warnings, and retry/timeout semantics.
87
88
 
88
89
  ### Guarded Invariants
@@ -119,8 +120,11 @@ Telegram configuration lives in `~/.pi/agent/telegram.json`. Bot/session identit
119
120
 
120
121
  ### Runtime Ownership
121
122
 
122
- - `/telegram-connect` acquires or moves the active profile's owner slot before polling starts. `/telegram-disconnect` keeps its destructive confirmation, then stops polling and releases only that exact slot. In Threaded Mode it tears down the disconnecting instance's bound Telegram thread: leaders delete their own thread directly, and followers send an authenticated exact-generation disconnect envelope and wait for confirmed leader cleanup before unregistering. Graceful Pi `quit` invokes the same teardown without a shutdown confirmation when `threads.automaticCleanup` is enabled (the default); disabling it preserves the tab through normal replacement-style suspension. Failed automatic cleanup records diagnostics and falls back to safe suspension so remaining lifecycle cleanup still runs.
123
+ - `/telegram-connect` acquires or moves the active profile's owner slot before polling starts. `/telegram-disconnect` keeps its destructive confirmation, then stops polling and releases only that exact slot. In Threaded Mode it tears down the disconnecting instance's bound Telegram thread: leaders delete their own thread directly, and followers send an authenticated exact-generation disconnect envelope and wait for confirmed leader cleanup before unregistering. Graceful Pi `quit` always preserves the owner slot as restart intent, allowing a reopened same-`cwd` session to reclaim the stale lease. When `threads.automaticCleanup` is enabled (the default), quit also deletes the bound Telegram tab without releasing that restart intent; disabling it preserves the tab through replacement-style suspension. Failed automatic cleanup records diagnostics and falls back to safe suspension so remaining lifecycle cleanup still runs.
123
124
  - Session start schedules polling resume asynchronously only when the owner slot already points at the current `pid`/`cwd`, or when a stale same-`cwd` owner can be safely replaced after process restart. Startup and `/resume` do not wait on leader election, Bot API probes, poller handoff, or thread reconciliation before restoring the Pi session.
125
+ - The polling owner alone bounds `getUpdates`: each request derives its cancellation budget from Telegram's declared long-poll timeout plus 10 seconds of transport grace (10 seconds for the zero-timeout initial sync and 40 seconds for the normal 30-second poll). The request-local controller inherits poller cancellation, rejects its owner at the budget, and fences any late transport result. Ordinary Bot API and media operations do not receive speculative blanket deadlines. Existing caller signals remain authoritative through API retry waits, only retry-safe methods replay explicit retryable responses, and non-idempotent sends preserve commit-unknown evidence instead of risking duplicate mutation.
126
+ - `pollingActive` reports only whether this runtime still owns an unresolved polling lifecycle; it is not health evidence. A separate observable state records `starting`, `long-poll`, `persisting-journal`, `persisting-offset`, `retrying`, or `stopped`, together with phase start, current update id, last successful response time/count, and terminal stop reason. This distinguishes a stuck HTTP poll from downstream update work without a wall-clock stale heuristic.
127
+ - Built-in read-only menu commands return after required local mutation and schedule context-fenced rendering and command synchronization independently, so those effects cannot withhold the next inbound offset.
124
128
  - Pi `print`/`json` run modes stay passive. Inherited child sessions that share `telegram.json` but do not own the exact `pid`/`cwd` slot must not poll or call `getUpdates` unless the operator force-takes ownership.
125
129
  - Session replacement through `reload`, `new`, `resume`, or `fork` suspends polling/watchers without releasing ownership so the next session in the same process can resume. A registered follower snapshots its assigned target into a short-lived same-process handoff, stops the old receiver/heartbeat, and re-registers through the live leader without marking or replacing its Telegram thread. Hard process termination cannot run graceful teardown, so stale recovery retains its restart-hint path.
126
130
  - Live external owners require explicit takeover confirmation. Long-lived timers compare against snapshotted owner identity and stop local transport work when the slot no longer matches.
@@ -134,7 +138,7 @@ Telegram configuration lives in `~/.pi/agent/telegram.json`. Bot/session identit
134
138
  The three runtime files have different authority and write pressure. Preserve that distinction when optimizing them:
135
139
 
136
140
  - `owners.json` is safety-critical transport authority. Acquire, release, takeover, stale recovery, and two-second leader lease refresh mutate it. The steady-state baseline is one cached atomic rewrite every two seconds per active profile, or 43,200 refreshes/day; one-second ownership checks are read-only. Every mutation serializes the full cross-process read/check/write through `owners.json.transaction`.
137
- - `state.json` combines recovery-critical thread/capability state with observational runtime projections. Every explicit thread-store `persist()` builds a semantic snapshot, but an unchanged payload skips temporary-file creation and rename after ignoring `writtenAtMs`; changed snapshots retain the full atomic replacement path. Diagnostics scheduling coalesces requests across a bounded 100 ms window. Only the exact transport owner commits; non-owners reload current disk state instead of publishing.
141
+ - `state.json` combines recovery-critical thread/capability state with observational runtime projections. Every explicit thread-store `persist()` builds a semantic snapshot, but an unchanged payload skips temporary-file creation and rename after ignoring `writtenAtMs`; changed snapshots retain the full atomic replacement path. Diagnostics scheduling coalesces requests across a bounded 100 ms window. Polling phase transitions and successful `getUpdates` responses schedule this observational projection, so an idle leader normally produces one changed polling snapshot per completed long-poll cycle. Only the exact transport owner commits; non-owners reload current disk state instead of publishing.
138
142
  - `logs.jsonl` is fail-soft observational evidence, never routing authority. Runtime events admitted in one JavaScript turn batch by captured profile path into one size check, one profile-wide file transaction, and one append while preserving event order. Batching adds no timer or shutdown-loss window; separate profiles remain isolated, and one failed group does not drop another. Scope reset and rotation retain their serialized copy/replace path. The 5 MiB value is a rotation threshold: an authorized writer rotates between batched records before the next record crosses it, so overshoot is bounded to one admitted record plus reset metadata; a writer without reset authority defers rotation to the owner.
139
143
 
140
144
  This baseline counts write-producing code paths rather than filesystem implementation details that vary between ext4, APFS, NTFS, and network-backed home directories. Optimization evidence should compare these deterministic triggers first, then use platform smoke evidence for rename, named-pipe, crash, and cleanup behavior. Recovery-critical `state.json` fields are `bot`, `identities`, `reservations`, `pendingProvisions`, `syncObservations`, and `threads`; `runtime`, `liveRoster`, `diagnostics`, and `writtenAtMs` are observational and may use bounded coalescing when authority checks remain unchanged.
@@ -143,13 +147,13 @@ Version `0.24.0` intentionally does not read or migrate the former agent-level `
143
147
 
144
148
  ### Threaded Mode Multi-Instance Bus
145
149
 
146
- Telegram private-chat Threaded Mode is the public switch for multi-instance Telegram operation. Classic single-DM polling is the base mode. When Telegram private-chat threads are available for the bot, the bridge enables the local leader/follower bus automatically; when threads are unavailable or later disabled, the bridge returns to classic single-DM polling as a first-class mode.
150
+ Telegram private-chat Threaded Mode is the public switch for multi-instance Telegram operation. Classic single-DM polling is the base mode. When Telegram private-chat threads are available for the bot, the bridge enables the local leader/follower bus automatically; when threads are unavailable or later disabled, the bridge returns to classic single-DM polling as a first-class mode. Before a non-owner `/telegram-connect` chooses follower registration or singleton takeover, it discards process-local status/capability projections and reads the current owner-published mode: `enabled` registers a follower without a takeover prompt, while `disabled` uses the classic confirmation flow.
147
151
 
148
152
  Named Telegram profiles are orthogonal to Threaded Mode. The selected profile chooses the bot/session slice (`botToken`, `botId`, `botUsername`, `allowedUserId`, `lastUpdateId`) and scopes the `owners.json` slot, diagnostics logs, state files, thread/bus ownership, and leader/follower IPC endpoints; it must not change the Threaded Mode rules. Within one selected profile, leader/follower election, bus transport, thread provisioning, routing, ownership forwarding, cleanup, and runtime diagnostics behave exactly as they do for the `default` slot. A different selected profile is a parallel bot runtime: its owner slot, `tmp/telegram/state.<profile>.json`, `tmp/telegram/logs.<profile>.jsonl`, `tmp/telegram/logs.<profile>._prev.jsonl`, thread bindings, Unix sockets, and Windows named pipes are isolated from the default profile and other named profiles while shared bridge settings remain top-level/global.
149
153
 
150
154
  Profile reality follows three explicit storage classes. `telegram.json` shared settings and extension registries are process-global platform configuration; `profiles.default` and `profiles.<name>` bot/session fields plus observable transport/routing authority are profile-scoped; queues, active turns, ownership caches, menu state, and runtime controllers are session-local memory. Config persistence serializes cross-process writers and applies each recursive mutation delta to the latest disk snapshot, so named-profile offsets and unrelated global/profile updates do not stale-replace one another. Polling publishes only its monotonic `lastUpdateId` into the current config-store snapshot; it never persists the detached full config object captured at poll start, which would interpret newer Settings fields as deletions. Runtime profile switching follows stop-old/commit-new ordering: reload keeps the selected identity stable, old polling/lock/bus teardown finishes while all dynamic resolvers still point at the old profile, and only then may activation expose the new token, lock key, state path, target namespace, and IPC endpoint. Downloaded attachments use UUID-prefixed names in the shared Telegram scratch directory and are session artifacts rather than identity or routing authority, so cross-profile cleanup is limited to stale scratch files and cannot redirect live traffic.
151
155
 
152
- When Threaded Mode is active, the current polling owner is also the Telegram bus leader. The leader owns the local bus endpoint (Unix-domain socket on Unix-like platforms, named pipe on native Windows), polls `getUpdates`, performs direct Bot API calls, records follower heartbeats, prunes stale followers, and provisions Telegram UI thread targets through live runtime/bus state. Follower liveness is intentionally fast because heartbeat traffic is local IPC: followers heartbeat every `1s`, the leader treats them as stale after `2s`, and the prune loop runs every `1s` so stopped followers are detected promptly while active forwarded updates/API calls still refresh liveness. Heartbeat pruning is silent liveness bookkeeping and does not send a Telegram-visible disconnected notice, because the common cause may be leader reload or IPC handoff rather than a dead follower. Pruning alone preserves the binding; when Thread cleanup is enabled, only a subsequent OS check that confirms the exact registered PID absent may create fenced cleanup intent, and that cleanup serializes ahead of replacement registration. Successful follower target reuse refreshes the binding's recovery timestamp. Absent follower bindings remain durable restoration hints until explicit stale, deleted, offline, or reconciliation evidence invalidates them; process absence and heartbeat pruning alone do not remove them. If an authenticated live follower carries an exact target that is absent from current bindings, the leader recovers it only behind a synchronous visibility probe: success activates it, explicit stale evidence provisions a replacement, and ambiguous failure rejects registration. A carried slot is restored only when it is not already occupied. `tmp/telegram/logs.jsonl` is a session-local redacted runtime evidence stream for race debugging; it resets on extension start and runtime scope changes, and must not become routing/provisioning authority. `tmp/telegram/state.json` is an extension+bot observable/debug snapshot aligned with status diagnostics: `source: "snapshot"` and `writtenAtMs` mark it as observational, not authoritative. All instances on one Telegram profile read the same snapshot, but only the active transport lock owner persists it; followers become writers only after promotion. Status-only persistence reloads current disk bindings before serialization, preventing a stale follower/status view from erasing newer leader-owned targets. Fresh capability observations may skip redundant startup probes, but stale snapshots re-probe before suppressing bus/thread behavior. Top-level `bot` mirrors bot-wide capabilities such as thread mode, `runtime` describes process role/status, `liveRoster` mirrors followers/current targets/reservations, `diagnostics` mirrors recent status/debug signals including the latest thread-reconciler phase/counts, `threads` stores current routeable bindings, TTL-bounded reservations explain short-lived slot collision guards, and TTL-pruned `pendingProvisions` protects in-flight topic creation slots from cleanup/allocation races. Fresh provisioning writes pending state before the Bot API create call, adds the returned target to the pending record, persists a `starting` binding, then promotes it to `active` and clears pending state. If final binding persistence fails after Telegram returns a thread id, the targeted pending provision remains as cleanup/retry evidence. Once targeted pending provisions expire, they are retained for `thread-reconciler` close/delete cleanup and pending scratchpad removal after a successful cleanup apply; untargeted expired pending records can prune without cleanup because no Telegram thread id exists. Runtime events coalesce status-snapshot writes so transient bus/API/update failures remain inspectable even when the operator has not opened `/telegram-status`. The bridge must not keep a durable `telegram-targets.json` target history; stale/offline/failed thread observations are pruned instead of reused. Previous-process leader bindings that still probe alive become reservations/collision guards, not routeable active threads, so a reloaded leader can take the next free slot without duplicating the same visible tab name. The thread chat is always the private bot DM with the paired owner (`allowedUserId`). In Telegram private-chat Threaded Mode, the leader creates/reuses its own thread before polling โ€” it is a real bound instance, not a dispatcher. Followers authenticate bus envelopes with the leader-minted capability secret stored in the active lock entry. Leader lock entries also carry a stable `leaderEpoch` minted on acquisition and preserved across heartbeat refreshes; leader-owned cleanup/provisioning plans stamp that epoch, and Thread Reconciler apply skips destructive work if leadership has moved on before side effects run. Followers own their own Pi session state, queue, active turns, previews, menus, and lifecycle hooks, but route allowlisted, target-scoped Telegram API calls through the leader. When a follower promotes after heartbeat loss, status/state diagnostics expose only the transient `electing` lifecycle phase; stable `leader`/`follower` identity stays in the bus role so diagnostics do not duplicate role state. The TUI status bar and `/telegram-status` report `leader` or `follower` role so a registered follower is not shown as generically disconnected. Terminal status identity and the `[telegram|thread:name]` prompt label use the same target-aware current-instance resolver: registered local metadata wins over a stale shared binding for the matching target, while the binding remains a fallback for partial metadata.
156
+ When Threaded Mode is active, the current polling owner is also the Telegram bus leader. The leader owns the local bus endpoint (Unix-domain socket on Unix-like platforms, named pipe on native Windows), polls `getUpdates`, performs direct Bot API calls, records follower heartbeats, prunes stale followers, and provisions Telegram UI thread targets through live runtime/bus state. Follower liveness is intentionally fast because heartbeat traffic is local IPC: followers heartbeat every `1s`, the leader treats them as stale after `2s`, and the prune loop runs every `1s` so stopped followers are detected promptly while active forwarded updates/API calls still refresh liveness. Heartbeat pruning is silent liveness bookkeeping and does not send a Telegram-visible disconnected notice, because the common cause may be leader reload or IPC handoff rather than a dead follower. Pruning alone preserves the binding; when Thread cleanup is enabled, only a subsequent OS check that confirms the exact registered PID absent may create fenced cleanup intent, and that cleanup serializes ahead of replacement registration. Successful follower target reuse refreshes the binding's recovery timestamp. Absent follower bindings remain durable restoration hints until explicit stale, deleted, offline, or reconciliation evidence invalidates them; process absence and heartbeat pruning alone do not remove them. If an authenticated live follower carries an exact target that is absent from current bindings, the leader recovers it only behind a synchronous visibility probe: success activates it, explicit stale evidence provisions a replacement, and ambiguous failure rejects registration. A carried slot is restored only when it is not already occupied. `tmp/telegram/logs.jsonl` is a session-local redacted runtime evidence stream for race debugging; it resets on extension start and runtime scope changes, and must not become routing/provisioning authority. `tmp/telegram/state.json` is an extension+bot observable/debug snapshot aligned with status diagnostics: `source: "snapshot"` and `writtenAtMs` mark it as observational, not authoritative. All instances on one Telegram profile read the same snapshot, but only the active transport lock owner persists it; followers become writers only after promotion. Status-only persistence reloads current disk bindings before serialization, preventing a stale follower/status view from erasing newer leader-owned targets. Fresh capability observations may skip redundant startup probes, but stale snapshots re-probe before suppressing bus/thread behavior. Top-level `bot` mirrors bot-wide capabilities such as thread mode, `runtime` describes process role/status, `liveRoster` mirrors followers/current targets/reservations, `diagnostics` mirrors recent status/debug signals including the latest thread-reconciler phase/counts, `threads` stores current routeable bindings, TTL-bounded reservations explain short-lived slot collision guards, and TTL-pruned `pendingProvisions` protects in-flight topic creation slots from cleanup/allocation races. Fresh provisioning writes pending state before the Bot API create call, adds the returned target to the pending record, persists a `starting` binding, then promotes it to `active` and clears pending state. If final binding persistence fails after Telegram returns a thread id, the targeted pending provision remains as cleanup/retry evidence. Once targeted pending provisions expire, they are retained for `thread-reconciler` close/delete cleanup and pending scratchpad removal after a successful cleanup apply; untargeted expired pending records can prune without cleanup because no Telegram thread id exists. Runtime events coalesce status-snapshot writes so transient bus/API/update failures remain inspectable even when the operator has not opened `/telegram-status`. The bridge must not keep a durable `telegram-targets.json` target history; stale/offline/failed thread observations are pruned instead of reused. Previous-process leader bindings that still probe alive become reservations/collision guards, not routeable active threads, so a reloaded leader can take the next free slot without duplicating the same visible tab name. The thread chat is always the private bot DM with the paired owner (`allowedUserId`). In Telegram private-chat Threaded Mode, the leader creates/reuses its own thread before polling โ€” it is a real bound instance, not a dispatcher. Followers authenticate bus envelopes with the leader-minted capability secret stored in the active lock entry. Bot capability monitoring does not probe through the bus until the process either owns that direct lock or has completed authenticated follower registration. Leader lock entries also carry a stable `leaderEpoch` minted on acquisition and preserved across heartbeat refreshes; leader-owned cleanup/provisioning plans stamp that epoch, and Thread Reconciler apply skips destructive work if leadership has moved on before side effects run. Followers own their own Pi session state, queue, active turns, previews, menus, and lifecycle hooks, but route allowlisted, target-scoped Telegram API calls through the leader. When a follower promotes after heartbeat loss, status/state diagnostics expose only the transient `electing` lifecycle phase; stable `leader`/`follower` identity stays in the bus role so diagnostics do not duplicate role state. The TUI status bar and `/telegram-status` report `leader` or `follower` role so a registered follower is not shown as generically disconnected. Terminal status identity and the `[telegram|thread:name]` prompt label use the same target-aware current-instance resolver: registered local metadata wins over a stale shared binding for the matching target, while the binding remains a fallback for partial metadata.
153
157
 
154
158
  Follower binding is manual and process-first: the operator starts another Pi process, then runs `/telegram-connect`; only then does that process register as a follower with an instance-scoped internal binding identity and cause the leader to create/reuse a thread for it. Telegram does not expose `/thread`, auto-spawn arbitrary unbound threads, or launch hidden follower subprocesses. In Threaded Mode, `/telegram-connect` does not offer manual takeover while a live leader exists; takeover is reserved for stale-leader election/recovery. Leadership remains an ephemeral transport role that another live follower can take over after stale heartbeat detection.
155
159
 
@@ -175,17 +179,58 @@ All inbound updates are gated by the configured authorized user id.
175
179
 
176
180
  ### Inbound Turn Flow
177
181
 
178
- 1. Poll updates through `getUpdates`.
179
- 2. Persist update offsets only after successful handling; repeated handler failures are bounded.
180
- 3. Filter to the paired private user; guest-mode updates require an existing paired user and cannot establish first pairing.
181
- 4. Dispatch owned callbacks and controls before fallback prompt forwarding.
182
+ 1. Poll updates through `getUpdates` under the polling owner's request budget.
183
+ 2. Validate and atomically journal each complete response batch before advancing its offset once.
184
+ 3. Signal the independent source-bound worker and begin the next poll without awaiting semantic execution.
185
+ 4. Run stable public raw-update handlers in registration order, then authorize and route retained built-in traffic.
182
186
  5. Coalesce media groups, likely split long text, and one adjacent forward-plus-comment pair in either order when needed.
183
- 6. Download files into `~/.pi/agent/tmp/telegram` with size limits and partial-download cleanup.
184
- 7. Run configured/programmatic inbound handlers in order, appending successful stdout under `[outputs]`.
185
- 8. Add local attachments under `[attachments]`, optional voice context, and optional final `[time]` context.
186
- 9. Build a `PendingTelegramTurn` and append it to the bridge queue.
187
- 10. Handle `edited_message` updates separately while the original turn is still queued.
188
- 11. Dispatch only when all safety gates are clear.
187
+ 6. Download files with size limits and partial-download cleanup, then run configured/programmatic inbound handlers.
188
+ 7. Build a prompt or control queue item carrying an exact durable receipt for every contributing update id.
189
+ 8. Remove journal authority only after prompt handoff, control settlement, or durable follower acknowledgement; execution failures remain durable until automatic replay succeeds.
190
+ 9. Handle `edited_message` updates separately while the original turn is still queued and dispatch only when all safety gates are clear.
191
+
192
+ #### Durable Admission And Recovery
193
+
194
+ Here, **durable** means recovery across ordinary process exit, crash, kill, and replacement after a successful atomic rename is visible to the filesystem. It does not promise survival across host, kernel, filesystem, storage-device, or power failure: journal and offset publication do not call `fsync`/`fdatasync`, and parent directories are not flushed. A host-level failure may therefore lose a recently acknowledged rename despite correct process-level ordering. Operators requiring that stronger boundary must place the agent directory on storage with an independently managed durability/backup policy; `0.28.0` must not be described as power-loss durable.
195
+
196
+ The profile-scoped journal separates transport progress from semantic progress. Leader/classic snapshots live at `tmp/telegram/inbox[.<profile>].json`; follower paths add a stable target-binding hash. The selected post-v1 storage design is one revisioned compacted snapshot plus immutable atomic transaction segments beside it. Existing v1 files load as implicit revision `0`, while positive snapshot revisions are explicit. Immutable revision segments publish privately and atomically under the existing journal transaction lock; exact repeats are idempotent, while gaps and conflicting duplicate revisions fail closed. Each segment carries one complete mutation, and readers reconstruct ordered upserts, removals, and operator-disposition state only from revisions newer than the snapshot. Malformed or gapped segments, filename/revision disagreement, and foreign journal identity fail closed. After the initial snapshot, append, batch completion, queue receipt/owner/handoff, retry/terminal, recovery, and operator dispositions publish only changed upserts/removals and disposition replacement in one segment. This avoids rewriting retained raw updates during completion-heavy drains without splitting exact queue, failure, recovery, or disposition transactions.
197
+
198
+ Compaction runs under the journal transaction lock when either 256 unapplied segments or 4 MiB of segment bytes is reached. It publishes the complete private (`0600`) snapshot at revision `R` before best-effort deletion of segments `<= R`; failed cleanup leaves redundant segments that readers ignore. Interrupted cleanup therefore leaves either an older snapshot plus newer authoritative segments or a newer snapshot plus harmless redundant older segments. Revision gaps, conflicting duplicates, malformed segments, and identity mismatches fail closed. The logical reconstructed journal and aggregate unapplied segment bytes are independently bounded at 10,000 entries and 32 MiB as applicable; rejected growth publishes neither snapshot nor segment bytes. Compaction may temporarily require exactly one private complete snapshot of at most 32 MiB. Capacity pauses polling and authority files are never automatically deleted, reset, or quarantined.
199
+
200
+ `pending` entries remain immediately executable while raw interception, routing, or grouping is incomplete. Every execution failure becomes `retry-wait` with durable attempt count, next eligible time, failure class, bounded summary, and latest failure time. The `failed` state remains schema-compatible only for legacy candidate journals and is converted to automatic retry during lifecycle startup. `queued` entries carry exact prompt/control receipts plus the acquiring Pi runtime instance, OS pid/birth identity, session generation, acquisition id, and acquisition time. Queueing alone is never completion.
201
+
202
+ Queue receipt ownership is independent from the Telegram transport lock. A same-instance, same-process generation may reconstruct its local receipt across a fenced session replacement and may settle it after transport ownership moves. A different process reports the receipt as foreign, never republishes it into its local queue, and cannot complete it even if it reads the acquisition id. Startup no longer treats process replacement as proof that an owner died: foreign and legacy unowned receipts remain durable.
203
+
204
+ Recovery and live handoff are compare-and-set under the journal transaction. Queue discard during exact queue-lifecycle cancellation requires the exact local owner/acquisition and removes all receipt sources atomically. Before admission worker start, the lifecycle groups each foreign receipt and asks the journal to recheck OS pid liveness plus process-birth identity under the same transaction; a live owner returns `owner-alive` and a live owner without stable birth proof returns `owner-unverifiable`, both without mutation, while exact negative proof converts the complete receipt back to `pending`. Replacement registration carries its exact pid/process-birth before this check; when registration and recovery race, that live identity wins the liveness proof and the queued receipt remains untouched. Only then does the replacement worker start, replay, and acquire a fresh receipt id/acquisition, fencing every stale owner.
205
+
206
+ Authenticated live handoff uses journal CAS plus bounded local IPC. The donor creates a one-time high-entropy token and durably offers the complete receipt to one exact recipient runtime/process/session identity; the journal stores only a digest bound to queue kind, receipt sources, donor acquisition, and recipient identity. While offered, donor completion/discard and dead-owner recovery fail closed, so authority cannot disappear during payload transfer. Prompt payloads carry all queue fields; control payloads carry only their stable `status`/`model` identity and rebuild executable closures locally. The separately negotiated `queue-handoff-v1` capability gates this envelope for leader and both peer generations. Each receipt carries its exact source journal binding; the donor derives the recipient follower-journal binding from the authenticated stable follower profile before routing. The bus validates payload shape/size and exact donor/recipient registration generations, and the recipient selects only that matching active lifecycle, stages one complete receipt idempotently, accepts the journal CAS, and returns the exact receipt plus newly minted owner in its ACK. Malformed, legacy-unbound, inactive-generation, or unavailable bindings fail closed.
207
+
208
+ During recipient staging, presenting the token atomically replaces the journal owner with a fresh acquisition carrying the handoff digest, removes the offer, and permanently fences donor settlement. The donor treats only an ACK carrying that exact accepted owner as success and never repeats acceptance against a donor-bound journal runtime. The recipient can repeat the same acceptance idempotently; a different token cannot claim an already accepted receipt. The coordinator contract orders offer โ†’ stage/accept exact receipt-and-owner ACK โ†’ donor removal โ†’ recipient readiness for direct leaderโ†’follower and followerโ†’follower routing. Before acceptance, negative or mismatched acknowledgement exactly cancels the offer and keeps donor work. After acceptance, a lost acknowledgement cannot roll authority back: cancellation fails closed and donor memory remains frozen until exact accepted-owner reconciliation removes it. Recipient registration carries exact process-birth/session identity, and staged payloads remain outside the live dispatch store until accepted journal authority has been reconstructed. Production advertises `queue-handoff-v1` only with this exact role/journal selection and uses the same coordinator ordering for direct leaderโ†’follower and followerโ†’follower routes.
209
+
210
+ Queued semantic authority has no elapsed-time lease. A timeout cannot prove either owner death or effect quiescence, so it cannot safely recover a receipt. Resolution is limited to authenticated live handoff, exact owner discard/settlement, or transaction-rechecked negative PID plus process-birth evidence. Live or unverifiable owners remain queued indefinitely rather than risking duplicate execution.
211
+
212
+ The initial `offset: -1` cursor bootstrap is allowed only when both cursor and journal are absent or empty. Thereafter process-level ordering is journal atomic rename โ†’ one monotonic offset atomic rename โ†’ worker signal. Failure before journal publication leaves the offset unchanged; failure after journal publication but before offset publication permits Telegram redelivery and journal dedupe; failure after offset publication but before worker signal replays from the journal on restart. Queue-owner, retry, terminal, handoff, and completion transitions use the same journal publication primitive and therefore share this process-crash boundary. The final completion window is at-least-once, so replay-sensitive external effects must use `update_id` or the stable delivery id as an idempotency key.
213
+
214
+ Threaded Mode forwarding is a two-journal handoff. Only peers that mutually advertise protocol v1 and `durable-follower-admission-v1` may route or become election-eligible. The follower validates its exact binding and registration generation, durably appends the source-bound delivery, and only then returns the exact receipt. The leader classifies each attempt as `accepted`, `retryable`, or `terminal-rejected` with its delivery identity and failure class; only `accepted` with the expected `deliveryId` and `sourceUpdateId` may complete leader journal authority. Missing, negative, stale-generation, or mismatched-receipt acknowledgements remain durable, and a callback error answer is only an operator-facing side effect.
215
+
216
+ Delivery ids derive only from envelope kind, source `update_id`, and stable recipient binding. Live registration generation remains a separate attempt fence, while callback/reaction message ownership carries that stable binding and rebinds to its current authenticated follower registration after replacement. Lost acknowledgements therefore replay idempotently into the same follower journal identity without changing the delivery id. Package build skew is allowed only while protocol version and capabilities remain compatible.
217
+
218
+ Worker execution ownership is per `update_id` across same-runtime generations. Aborting a generation ends its authority but does not prove its handler settled; replacement replay remains blocked on that exact settlement. Late success and failure are both diagnostic events. Public and built-in handlers receive the same optional execution fence (`signal`, generation/update identity, and pre-effect assertion); the runtime binds it non-enumerably to every internal update carrier and checks it before routing-plan effects. Prompt construction rechecks after downloads and inbound handlers before queue mutation, pairing rechecks around persistence, command/menu and extension-command delegation retain the source fence across detached effects, lifecycle sync rechecks after store load before reconciliation, and reroute clones carry the source fence through forwarding, thread replacement, cleanup, persistence, and Bot API rename boundaries. Legacy handlers remain source-compatible but must not commit unfenced late effects.
219
+
220
+ The canonical update transition contract is:
221
+
222
+ - `pending โ†’ executing`: the generation-local worker selects an unclaimed source; `executing` is a runtime phase, not a separately persisted entry state.
223
+ - `executing โ†’ completed | queued | pending | retry-wait`: exact local completion removes the entry, queue admission persists its receipt, deferred grouping retains replay authority, and every execution failure persists retry evidence.
224
+ - `retry-wait โ†’ executing`: only after `nextRetryAtMs`; repeated signals before eligibility do not execute the entry. Automatic retries continue indefinitely with exponential `1s โ†’ 2s โ†’ 4s โ†’ 8s โ†’ 16s โ†’ 32s โ†’ 60s` delay capped at `60s`, while later independent updates continue draining.
225
+ - Legacy `failed โ†’ retry-wait`: startup atomically resumes terminal entries written by earlier `0.28.0` candidates. Runtime policy never silently discards durable inbound authority and exposes no Pi command for manual retry/discard.
226
+ - `queued โ†’ offered โ†’ staged โ†’ queued`: only the exact persisted donor may offer or cancel a live handoff; an offer preserves donor ownership but freezes ordinary settlement and recovery. Authenticated bounded IPC stages one exact payload/receipt outside the live queue. Exact recipient acceptance mints a fresh acquisition, reconstructs local ownership, removes donor work, then publishes recipient dispatch readiness.
227
+ - `queued โ†’ completed | pending`: only the exact persisted owner receipt may complete or discard queued sources; generic completion rejects queued state. Process-birth-proven owner death may atomically recover the complete unoffered receipt to `pending`; live, unverifiable, or offered owners remain queued.
228
+
229
+ The worker executes at most 64 eligible entries from one validated journal snapshot, commits ordinary completions through one journal transaction, then yields through a generation-checked event-loop boundary. Retry, queue, or prior-generation boundaries first flush completed ids and force a fresh snapshot, preserving exact state-transition atomicity without per-entry parse/rewrite churn. A deterministic 2,048-entry stress gate requires exactly 32 completion publications, 33 reads including the final empty snapshot, continued 1ms timer progress, and less than 250ms maximum observed heartbeat delay. Byte-capacity tests cover failed and retry-wait diagnostics, queue receipt/owner and handoff metadata, and operator dispositions; every rejected growth leaves the prior authority bytes unchanged. It still scans later independent entries after retry or terminal persistence. An unresolved reaction remains a queue-mutation dependency even in `retry-wait` or `failed`, but dispatch checks that dependency against the candidate queue item's exact chat and source message ids instead of globally blocking unrelated targets. Successful replay or an exact discard disposition releases the dependency. Worker state, debug status, state snapshots, and redacted runtime events expose journal depth, retry/terminal counts, the next retry, latest terminal identity, copyable operator commands, and the exact first foreign queued owner identity (instance, PID/birth, session, and acquisition) when semantic authority belongs to another process.
230
+
231
+ Upgrades create journals lazily before the first post-upgrade offset advance. A bot/profile identity change with unresolved authority fails closed. Once reconstructed authority is empty, the next read atomically rebinds profile and bot identity under the journal transaction and removes redundant old-identity segments best-effort; stable-`botId` token rotation remains valid even with entries. Downgrading below `0.28.0` with a non-empty journal is unsafe because the older runtime cannot drain updates whose Telegram offsets already advanced. Run `node scripts/check-downgrade.mjs [agent-dir]`; a blocked result requires draining with a compatible `0.28.x` runtime, while a safe result confirms all reconstructed journal authority is empty before downgrade. Runtime state from an older release must recover without deleting `telegram.json`, ownership state, or journal authority.
232
+
233
+ Polling and inbound-worker diagnostics remain separate so an executing, deferred, locally queued, foreign-queued, or blocked journal head cannot masquerade as a stalled `getUpdates` request.
189
234
 
190
235
  Long-text split recovery remains conservative: only human text at or above the near-limit threshold opens its debounce window. Forward annotation has two semantic layers: the forward owns its source text/caption/media, while an optional separate owner-authored annotation normally precedes it. A bounded one-second pairing window joins that annotation and adjacent forward in either transport order, including a media-only forward without source caption text; the matching opposite-kind message flushes immediately. Same-kind rapid messages, commands, bots, ordinary non-forward captions, media groups, different senders/targets, reversed ids, and distant message ids do not enter this pairing path. Prompt construction always places the owner annotation first, followed by `[forward|from:...]` with the forward's own source text/caption, then source-attributed forwarded attachments, regardless of arrival order.
191
236
 
@@ -222,7 +267,7 @@ For a configured Rich response with final text and exactly one supported queued
222
267
 
223
268
  ### Controls And Menus
224
269
 
225
- Telegram controls execute through command/callback domains, not by entering the normal prompt queue unless they intentionally create a prompt turn.
270
+ Telegram controls execute through command/callback domains, not by entering the normal prompt queue unless they intentionally create a prompt turn. Built-in read-only menu commands are admitted once required local state mutation finishes: first-user pairing still persists before `/start` is accepted, while menu rendering and BotFather command synchronization run as context-fenced best-effort effects with diagnostic failure sinks. Their unresolved Telegram calls therefore cannot retain the durable polling offset or prevent the next `getUpdates` request. Detached effects, deferred dispatch/watchdog, typing, and diagnostics callbacks contain primary and diagnostic failure; stale typing context is ignored, while snapshot publication serializes one write plus one retained coalesced rerun. Raw companion handlers still run before durable built-in routing and should return quickly even though their execution no longer retains polling.
226
271
 
227
272
  Immediate controls:
228
273
 
@@ -309,15 +354,15 @@ The bridge does not mirror arbitrary `ctx.ui.confirm/input/select/custom` prompt
309
354
 
310
355
  Status rendering distinguishes connected, active, dispatching, queued, tool-running, model-switching, and compacting states. If a queue mutation removes the last waiting item while Telegram-owned work still has running tools, status remains active instead of degrading to connected.
311
356
 
312
- Queue reactions are shortcut controls for waiting turns. Promotion reactions (`๐Ÿ‘`, `โšก๏ธ`, `โค๏ธ`, `๐Ÿ•Š`, `๐Ÿ”ฅ`) move prompts to priority; removal reactions (`๐Ÿ‘Ž`, `๐Ÿ‘ป`, `๐Ÿ’”`, `๐Ÿ’ฉ`, `๐Ÿ—‘`) remove waiting turns because ordinary Telegram DM deletions are not exposed through Bot API polling.
357
+ Queue reactions are reversible shortcut controls for waiting turns. The runtime reconciles each complete `MessageReactionUpdated.new_reaction` set: any removal reaction (`๐Ÿ‘Ž`, `๐Ÿ‘ป`, `๐Ÿ’”`, `๐Ÿ’ฉ`, `๐Ÿ—‘`) suppresses the governed prompt without discarding its queue authority; otherwise any promotion reaction (`๐Ÿ‘`, `โšก๏ธ`, `โค๏ธ`, `๐Ÿ•Š`, `๐Ÿ”ฅ`) moves it to priority; otherwise it returns to the default lane. Suppressed prompts remain visible in the queue menu, survive authenticated queue handoff, and do not block unrelated dispatch. Reaction changes first flush a matching delayed text or media group so the governed turn exists before mutation. Once Pi has consumed a prompt, reactions cannot retract it.
313
358
 
314
- `/telegram-status` records grouped diagnostics for transport/API, polling/update, prompt dispatch, controls, typing, compaction, setup, session lifecycle, attachment queue/delivery, and recent redacted runtime events. Expected preview noise such as unchanged edit responses is filtered out. The compact TUI status renders only `error`; detailed failure text remains in diagnostics and profile-scoped logs instead of expanding the status line.
359
+ `/telegram-status` records grouped diagnostics for transport/API, polling/update, prompt dispatch, controls, typing, compaction, setup, session lifecycle, attachment queue/delivery, and recent redacted runtime events. Polling diagnostics expose the exact phase, phase start, current update, last successful `getUpdates` response, and stop reason; outbound success never substitutes for inbound progress. Expected preview noise such as unchanged edit responses is filtered out. The compact TUI status renders only `error`; detailed failure text remains in diagnostics and profile-scoped logs instead of expanding the status line.
315
360
 
316
361
  Complete intermediate assistant text blocks from Telegram-originated activity are sent once to the immutable originating target before active-turn final delivery; final and terminal-partial segments stay with settlement so replies are not duplicated. When `assistant.proactivePush` is enabled and this instance has exact direct or follower transport authority, completed public blocks from local/autonomous work are also sent once and in source order to the instance's authorized target. Both paths use the configured Rich or HTML renderer and exclude reasoning, tool traffic, token deltas, local prompt text, unknown sources, and stale generations. Each admitted block remains fenced to its exact target, profile/token stamp, leader epoch or follower registration generation, and session generation; non-idempotent acknowledgement ambiguity never authorizes replay.
317
362
 
318
363
  `assistant.activity` is an independent bridge-owned projection over normalized Activity events. Each process reloads the shared file-backed setting at `agent-start` before activity admission, so multi-instance mode cannot continue projecting a stale broader process-local selection. Omitted values resolve to `verbose`, while invalid values fail closed to `quiet`; `thinking` and `tools` select one technical class, while `verbose` enables both. Provider-exposed thinking uses persistent ordinary HTML containing only a standard expandable blockquote with a bounded redacted latest-text window and inline Markdown rendered as Telegram HTML. Completed executed tools use native Rich Messages: each closed `<Tool>: <status>` root details node renders snake-case names as title words while preserving an uppercase two- or three-letter repeated prefix per word, then the native disclosure chevron reveals an open-by-default `arguments` child plus closed retained `update N` and `result`/`error` child details with lowercase monospaced, marker-free summaries and JSON pre blocks; known-safe Rich rejections fall back to the previous HTML disclosure. The projection captures the exact target and transport stamp at activity admission, serializes updates, preserves tool-start order, closes coalescing across assistant/thinking boundaries, bounds retained text/update memory plus edit frames and message/tool size, disables previews and HTTP(S) auto-link recognition inside technical evidence, and never replays a possibly committed send. Session generations own independent queues, so replacement drops queued old work without waiting on an old call. Both proactive prose and active-turn final delivery wait for the admitted activity queue inside their extension-owned delivery tasks, preserving technical-before-semantic ordering without delaying Pi lifecycle completion. Settlement, replacement, disconnect, failure, or stale authority clears only local ownership; already-sent activity messages remain in chat.
319
364
 
320
- Telegram prompt guidance is context- and authority-aware. Only an exact direct owner or live registered follower exposes the three pi-telegram model tools, their active-tool metadata, and the compact local bridge suffix. Disconnect or authority loss removes those surfaces for subsequent requests without touching foreign tools; reconnect/recovery restores only the pi-telegram subset that was active before suspension, including across same-process reload. Telegram-originated turns receive a compact pointer to `telegram_help` plus dynamic prompt blocks such as `[voice] delivery: automatic voice`; full voice/button/direct-delivery/Threaded Mode syntax stays in the help tool rather than every system prompt.
365
+ Telegram prompt guidance is context- and authority-aware. The package and source-checkout extension both contribute `telegram-bridge` plus the optional `button-console` Skill through Pi resource discovery. Only an exact direct owner or live registered follower exposes the two pi-telegram delivery tools, their active-tool metadata, and the compact routing suffix. Disconnect or authority loss removes those tool surfaces for subsequent requests without touching foreign tools; reconnect/recovery restores only the pi-telegram subset that was active before suspension, including across same-process reload. Telegram-originated turns route to the stable Skill contract and retain dynamic blocks such as `[voice] delivery: automatic voice`; the Skill and public documentation own syntax, target routing, Threaded Mode behavior, and diagnostics.
321
366
 
322
367
  ## In-Flight Model Switching
323
368
 
@@ -85,6 +85,8 @@ tmp/telegram/owners.json / <profile-slot> -> bus leader identity + heartbeat
85
85
 
86
86
  Followers do not poll. They register with the leader and receive routed inbound updates from it. Followers still own their local queue, active-turn state, previews, final delivery planning, model switches, and Pi lifecycle. The leader owns only Telegram transport and update fanout. Pi session replacement (`new`) changes follower agent context, not bus membership: a registered follower preserves its registration and refreshes the live context instead of disconnecting. The Telegram bus belongs to the local set of cooperating visible Pi instances rather than to the first terminal session forever: if the visible terminal leader exits, a live registered follower can take over leadership.
87
87
 
88
+ A follower may route Bot API work or capability checks only after authenticated registration; an unregistered process that does not own the direct transport lock remains transport-passive. Follower request settlement remains owned by the existing local IPC operation budget rather than being inferred from Bot API method names; followers never invoke `getUpdates`.
89
+
88
90
  ## Target Abstraction
89
91
 
90
92
  The bridge uses a first-class target abstraction:
@@ -159,6 +161,22 @@ Followers first try to re-register after leader reload or unknown-heartbeat resp
159
161
 
160
162
  ## Leader/Follower Communication
161
163
 
164
+ ### Protocol identity and compatibility
165
+
166
+ The local wire contract has protocol version `1`, independent from the npm package version. Follower registration and the leader acknowledgement carry `{ protocolVersion, runtimeBuild, capabilities }`. Capability names are canonical, unique, and sorted. A leader rejects missing or mismatched protocol identity before provisioning a target or publishing the follower into live routing; a strict follower likewise rejects an acknowledgement without compatible leader identity. Different package builds remain compatible when their protocol versions agree. `durable-follower-admission-v1` gates source forwarding, while `queue-handoff-v1` independently gates live semantic queue transfer; every participant in a routed handoff must advertise it.
167
+
168
+ Negotiated identities remain on the live follower registry and appear in `/telegram-status --debug` plus the observational state snapshot. The Threaded Mode capability monitor owns one in-flight probe across lifecycle generations: stop/restart invalidates a late read, and a replacement monitor waits for the previous request to settle instead of creating overlapping transport transitions. Durable follower admission is authorized only when both peers advertise `durable-follower-admission-v1`, never inferred from package version: a capable runtime rejects missing support before provisioning, inbound routing, or election-roster eligibility. Authentication and exact registration generation remain mandatory independently of protocol compatibility. `follower.register` is the sole bootstrap request and must carry a fresh generation before provisioning; every other request is exact-generation-fenced against a live registry entry. `bus.ack` is response-only and is rejected if submitted as a server request. Leader forwarding never synthesizes authority for an unknown recipient and preserves the follower's exact durable receipt end to end.
169
+
170
+ Foreign update forwarding returns an explicit `accepted`, `retryable`, or `terminal-rejected` settlement. Acceptance requires an acknowledgement for the exact request whose receipt contains the expected stable `deliveryId` and source `update_id`; a callback error popup never substitutes for that receipt. Missing or negative acknowledgements, stale registrations, absent follower context, binding rejection, journal admission failure, and missing or mismatched receipts all retain the leader source. Message, edited-message, reaction, and callback paths share this contract.
171
+
172
+ The delivery id excludes the replaceable runtime instance and registration generation: it derives from envelope kind, source `update_id`, and the stable manual-follower binding. Stored message ownership carries that binding and may rebind to the current authenticated registration after follower replacement, preserving one retry identity while still fencing each attempt by the current generation. A lost acknowledgement can therefore replay idempotently into the follower journal and becomes accepted only when the exact durable receipt returns.
173
+
174
+ Transport leadership does not own already-queued semantics. Each queued journal receipt binds the acquiring runtime instance, OS process birth, session generation, and acquisition. A replacement leader or follower process sees another live process's receipt as foreign and cannot replay or settle it; the original process may complete its local Pi queue after transport moves. Startup preserves foreign and legacy unowned receipts. Before a replacement admission worker starts, tri-state pid/process-birth proof: an absent PID or mismatched stable Linux/macOS birth identity permits recovery, while a live matching owner stays `alive` and Windows or inaccessible birth metadata stays `unverifiable`; both non-dead outcomes preserve the receipt may transactionally recover a dead owner's complete receipt to pending, after which replacement replay creates fresh queue authority. Registration publishes the replacement's exact pid/process-birth identity first; a concurrent recovery that observes it returns `owner-alive`, while live or unverifiable owners remain untouched.
175
+
176
+ Live-process handoff combines journal CAS with authenticated bounded bus payloads. A donor-generated one-time token is hashed together with the exact receipt, donor acquisition, and recipient runtime/process/session identity. Offering retains donor ownership but freezes donor completion/discard and dead-owner recovery. Prompt payloads serialize queue data; control payloads serialize only stable `status`/`model` identity and rebuild closures at the recipient. Leader and follower receivers require exact live donor/recipient registration generations, reject malformed/oversized payloads, stage one complete receipt idempotently, accept its journal authority, and acknowledge only that exact receipt plus newly minted owner. Receipts name their source journal binding, while the donor derives the recipient follower-journal binding from the authenticated stable profile. The recipient accepts only that matching active lifecycle. Production advertises `queue-handoff-v1` with this exact role/path composition; legacy-unbound or unavailable bindings fail closed.
177
+
178
+ As part of staging, the exact recipient presents the token and atomically receives a fresh acquisition carrying the handoff digest; donor settlement is then stale, and the donor trusts only an ACK carrying that accepted owner. Registration carries recipient process-birth and session generation so journal authority matches the live runtime exactly. The coordinator contract shares one ordering across leaderโ†’follower and followerโ†’follower paths: offer, route bounded payload, require the exact staged-and-accepted receipt-and-owner ACK, remove donor work, then publish recipient dispatch readiness. Negative or mismatched acknowledgement cancels the still-unaccepted offer and retains donor work. A lost acknowledgement after recipient acceptance cannot revoke the accepted owner; cancellation fails closed and donor memory remains frozen for exact accepted-owner reconciliation. Repeated exact acceptance is idempotent; a different token cannot claim the accepted receipt. Queue authority has no elapsed-time lease: only exact handoff, owner action, or transaction-rechecked PID/process-birth death proof may move it.
179
+
162
180
  Implemented transport:
163
181
 
164
182
  ### Local IPC endpoint with bounded native paths
@@ -207,7 +225,7 @@ Current portability audit:
207
225
  - Ownership/config/state/temp files: path construction uses `path.join`/`path.resolve` under the Pi agent directory. File permission calls remain best-effort private-mode hardening; native Windows may emulate POSIX modes, so broad Windows ACL auditing is outside this extension's current local-bus baseline.
208
226
  - Process liveness: lock ownership uses `process.kill(pid, 0)`, which Node supports on Windows for existence checks. Cross-user permission failures are treated as alive, matching Unix semantics.
209
227
  - Shell/provider commands: outbound handler command templates remain operator-configured and platform-dependent; Threaded Mode bus portability does not guarantee every configured STT/TTS/shell provider is Windows-native.
210
- - Manual follower identity: process ids are local liveness hints, paired with OS process-birth metadata where available rather than treated as cross-machine identifiers. Linux uses `/proc` start ticks and macOS uses the parent process start time; inaccessible metadata falls back to the extension generation. A fresh authenticated session handoff carries the previous runtime identity and exact target through initial registration, allowing fallback identities to migrate without provisioning another thread.
228
+ - Manual follower identity: process ids are local liveness hints, paired with OS process-birth metadata where available rather than treated as cross-machine identifiers. Linux uses `/proc` start ticks and macOS uses the parent process start time. Fallback generation strings identify a runtime but are not independent death proofs: Windows or inaccessible process-birth metadata is `unverifiable` while the PID remains live. A fresh authenticated session handoff carries the previous runtime identity and exact target through initial registration, allowing fallback identities to migrate without provisioning another thread.
211
229
 
212
230
  Remaining risk is live native Windows behavior: named-pipe creation/connect timing, antivirus/firewall/ACL interference, and provider command availability need operator smoke evidence.
213
231
 
@@ -358,7 +376,7 @@ Rules:
358
376
  Current state under the agent dir:
359
377
 
360
378
  - `tmp/telegram/owners.json`: authoritative extension-local transport owners keyed by `default` or named profile. Each owner contains the bus leader identity, capability secret, heartbeat, generation, and cleanup fencing epoch. Mutations serialize through `owners.json.transaction`; followers never write owner slots. The local bus endpoint is derived from the agent directory by default; legacy `busSocketPath` entry fields are tolerated inside current owner records but are not required.
361
- - `tmp/telegram/state.json`: volatile extension+bot observable/debug snapshot, not routing authority. It writes `source: "snapshot"` and `writtenAtMs` so consumers do not confuse it with an authoritative database. Every process on one Telegram profile reads this shared path, but only the active transport owner may persist it; followers become writers only after promotion. Status-only persistence refreshes disk-backed bindings before serialization so an already-loaded stale view cannot erase newer leader records. It mirrors `/telegram-status`-style projections: top-level `bot` stores bot-wide capability state such as `threadMode: "unknown" | "enabled" | "disabled"`, `runtime` identifies leader/follower role and process status, `liveRoster` mirrors followers/current targets/reservations, `diagnostics` mirrors status/debug signals, `threads` stores current routeable bindings, `bot.lastSlot` stores the compact slot cursor used when all current threads are gone, and `reservations` records short-lived slot collision guards.
379
+ - `tmp/telegram/state.json`: volatile extension+bot observable/debug snapshot, not routing authority. It writes `source: "snapshot"` and `writtenAtMs` so consumers do not confuse it with an authoritative database. Every process on one Telegram profile reads this shared path, but only the active transport owner may persist it; followers become writers only after promotion. Status-only persistence refreshes disk-backed bindings before serialization so an already-loaded stale view cannot erase newer leader records. It mirrors `/telegram-status`-style projections: top-level `bot` stores bot-wide capability state such as `threadMode: "unknown" | "enabled" | "disabled"`; `runtime` identifies leader/follower role, lifecycle activity, and the exact polling phase/progress snapshot; `liveRoster` mirrors followers/current targets/reservations; `diagnostics` mirrors status/debug signals; `threads` stores current routeable bindings; `bot.lastSlot` stores the compact slot cursor used when all current threads are gone; and `reservations` records short-lived slot collision guards.
362
380
  - Local bus endpoints: Unix-like platforms expose stable `tmp/telegram/bus.sock` and `tmp/telegram/followers/*` symlinks backed by private generation sockets; native Windows uses deterministic named pipes under `\\.\pipe\pi-telegram-...`. These are transient IPC endpoints, not durable routing state.
363
381
 
364
382
  If an unclean host shutdown truncates `owners.json`, a profile `state*.json`, or the ownership transaction guard, `/telegram-connect` classifies the damage before recovery. With no verifiable live owner, one cross-process recovery winner quarantines only those damaged disposable artifacts and startup retries once; followers or leaders appearing during the final guarded reread stop the reset. `telegram.json`, `logs*.jsonl`, other profiles' valid state, and unrelated extension data remain untouched. A blocked or failed reset reports which Pi must restart instead of emitting repeated raw parse/transaction errors.
@@ -384,14 +402,14 @@ All files containing routing, chat ids, thread ids, or process details use priva
384
402
 
385
403
  ### Follower heartbeat is missed
386
404
 
387
- - Leader prunes the follower from the live registry after missed heartbeats, but heartbeat pruning is only immediate liveness bookkeeping.
405
+ - Leader prunes the follower from the live registry after missed heartbeats, but heartbeat pruning is only immediate liveness bookkeeping. One leader generation owns at most one prune operation; stop makes late endpoint, policy, and cleanup settlement inert, while durable-profile mutation serialization prevents replacement registration from crossing confirmed-dead cleanup.
388
406
  - A missed heartbeat does not delete, close, mark offline, or send a disconnected notice for the follower's Telegram thread binding because the common cause may be leader reload, IPC handoff, or transient reconnect rather than a dead follower.
389
407
  - Followers treat rejected/missing heartbeat acknowledgements as registration loss: retain the last known target locally, clear registered truth, try to re-register with the current leader, wait a short leader-reload grace window, and retry. They promote only after the exact leader lease becomes stale or inactive; a live owner with an unreachable endpoint leaves the follower disconnected/retrying rather than creating a competing poller.
390
408
  - Persisted current manual-follower bindings survive abrupt process absence as restoration hints when Thread cleanup is disabled. When enabled, graceful Pi quit requests exact-generation teardown before lifecycle suspension; if that envelope is missed, stale pruning may delete only after the leader's OS confirms the exact registered PID has exited.
391
409
  - Fresh registration sends one compact connected notice in the assigned thread. An exact immediate session handoff uses a target-scoped `sendChatAction` as its synchronous visibility probe, avoiding a duplicate notice while retaining stale/ambiguous recovery; other cross-session restoration keeps the connected notice as its probe.
392
410
  - Registration requires a present generation, and explicit disconnect requires that same exact live generation. Leader-side registration and disconnect mutations serialize per durable follower profile across old and replacement runtime instance IDs, so a replacement registration cannot overtake awaited destructive cleanup and an old disconnect cannot remove its successor's routing authority.
393
- - Successful forwarded updates and follower-originated API calls refresh liveness, so active followers are not pruned only because the interval heartbeat tick lagged.
394
- - Destructive follower thread teardown belongs to confirmed `/telegram-disconnect`, graceful Pi quit, or confirmed reconciliation actions, not generic heartbeat pruning. Manual disconnect retains its destructive confirmation; quit performs the same action without prompting when Thread cleanup is enabled (default). Confirmed leader/follower teardown first persists an exact target/runtime-generation cleanup intent. The active leader attempts deletion under its current epoch; interruption preserves the intent so that leader or a successor can replay it under current authority, and confirmed deletion removes the binding plus intent in the same persisted state transition. If the graceful request is missed, stale heartbeat plus OS-confirmed absence of the exact registered PID may authorize the same cleanup while enabled; this action serializes ahead of replacement registration. Disabled cleanup, silence, heartbeat expiry alone, IPC/auth failure, and live or unknown process liveness remain non-destructive. Incomplete cleanup preserves durable intent for retry. A promoted leader uses its current owned leader epoch even when the inherited record still carries a historical `manual-follower` owner label.
411
+ - Successful forwarded updates and follower-originated API calls refresh liveness, so active followers are not pruned only because the interval heartbeat tick lagged. Each follower lifecycle owns at most one in-flight heartbeat for its exact registration generation; stop/replacement makes late settlement inert, and recovery or diagnostic failure cannot escape as an unhandled interval Promise.
412
+ - Destructive follower thread teardown belongs to confirmed `/telegram-disconnect`, graceful Pi quit, or confirmed reconciliation actions, not generic heartbeat pruning. Manual disconnect retains its destructive confirmation and clears restart ownership; quit deletes the tab without prompting when Thread cleanup is enabled (default) but preserves the owner slot independently so a same-directory restart can reclaim leadership. Confirmed leader/follower teardown first persists an exact target/runtime-generation cleanup intent. The active leader attempts deletion under its current epoch; interruption preserves the intent so that leader or a successor can replay it under current authority, and confirmed deletion removes the binding plus intent in the same persisted state transition. If the graceful request is missed, stale heartbeat plus OS-confirmed absence of the exact registered PID may authorize the same cleanup while enabled; this action serializes ahead of replacement registration. Disabled cleanup, silence, heartbeat expiry alone, IPC/auth failure, and live or unknown process liveness remain non-destructive. Incomplete cleanup preserves durable intent for retry. A promoted leader uses its current owned leader epoch even when the inherited record still carries a historical `manual-follower` owner label.
395
413
  - Explicit stale/deleted/offline observations invalidate reuse. Process absence affects reuse only through the enabled, exact-PID confirmed-dead cleanup path.
396
414
 
397
415
  ### Thread is deleted
package/docs/outbound.md CHANGED
@@ -176,8 +176,8 @@ Buttons are built in and do not need a command template because they are pure Te
176
176
  The extension injects prompt guidance by context:
177
177
 
178
178
  - If no bot token is configured, no Telegram bridge suffix is injected.
179
- - For ordinary local/TUI prompts, the agent only sees compact direct-delivery guidance: use `telegram_attach` or `telegram_message` when the user asks to send something to Telegram, and otherwise answer locally as normal.
180
- - For Telegram-originated turns, the prompt carries only minimal mobile/reply/file guidance; agents can call `telegram_help()` for full voice/button/direct-delivery/Threaded Mode/formatting/debug details.
179
+ - For ordinary local/TUI prompts, the compact routing note points to the bundled `telegram-bridge` Skill and forbids Telegram use unless explicitly requested.
180
+ - For Telegram-originated turns, the compact note routes the agent to `telegram-bridge`, which owns voice/button/direct-delivery/Threaded Mode/formatting/debug guidance.
181
181
  - For Telegram-originated turns, write the full technical answer as normal Markdown.
182
182
  - Add `telegram_voice` with either a JSON object or double-quoted attributes when a Telegram-native voice message is useful. A companion summary is optional, no specific summary format is required.
183
183
  - Add `telegram_button` with either a JSON object or double-quoted attributes. Use `label` plus `prompt`, or `value` when they are identical; `selected_style` is optional. A button-only reply may omit parent text because the bridge supplies `โ˜‘๏ธ **Choose an option:**` automatically.
@@ -42,7 +42,7 @@ Stable commands inside Pi:
42
42
  - `/telegram-setup` โ€” configure/update the bot token.
43
43
  - `/telegram-connect` โ€” start polling here and acquire external Telegram control ownership. Accepted queue/reply state stays local if ownership later moves elsewhere.
44
44
  - `/telegram-disconnect` โ€” after destructive confirmation, stop polling and release ownership without deleting or silencing accepted local queue state. In Threaded Mode it deletes this instance's current Telegram thread; a follower waits for its active leader to confirm generation-fenced cleanup before stopping. Graceful Pi `quit` performs the same teardown without prompting, while `reload`, `new`, `resume`, and `fork` preserve same-process handoff.
45
- - `/telegram-status` โ€” show connection, polling, execution, queue, and recent event diagnostics.
45
+ - `/telegram-status` โ€” show connection, polling, execution, queue, and recent event diagnostics; debug output separates poller and worker progress, durable automatic-retry state, exact foreign queued-owner identity, and negotiated protocol/build/capabilities.
46
46
 
47
47
  ### Telegram commands
48
48
 
@@ -63,7 +63,7 @@ This command surface is a mobile companion subset, not a raw terminal-command br
63
63
 
64
64
  - `telegram_attach(paths, chat_id?, thread_id?, caption?)` is the stable artifact delivery tool for generated files. During Telegram turns it queues files for the active reply; with `assistant.rendering: "rich"`, exactly one PNG/JPEG, MP4, or MP3 artifact plus non-empty final Markdown can become one reply-anchored Rich Message. HTML mode, multiple/unsupported files, Guest Mode, and voice outputs retain their established paths. Outside Telegram turns the tool sends files directly to the paired/default chat, the registered follower's assigned thread, or an explicit `chat_id` plus optional `thread_id` when this Pi instance owns `/telegram-connect` or is registered with the multi-instance bus.
65
65
  - `telegram_message(text, chat_id?, thread_id?)` sends a direct Telegram Markdown message when this Pi instance owns `/telegram-connect` or is registered with the multi-instance bus. During an active Telegram turn, omitted targeting and an explicit target equal to that turn are rejected so the ordinary final-reply path remains the sole current-target response; an explicit different chat/thread target remains allowed for requested cross-target delivery. Outside active turns, paired/default local/TUI delivery remains unchanged. Top-level `telegram_button` comments inside `text` are parsed with the same planner used for normal replies and attached to that message; buttons are never standalone Telegram messages.
66
- - `telegram_help()` returns detailed agent-facing guidance for pi-telegram delivery actions, Threaded Mode, formatting, and debugging. The regular prompt only points agents at this tool instead of repeating the full guidance on every turn. `telegram_attach`, `telegram_message`, and `telegram_help` remain registered but are model-active only while this instance owns direct transport or holds a live follower registration; disconnect/loss suppresses their schemas and prompt metadata, and recovery restores only the operator's previously active pi-telegram subset.
66
+ - The bundled `telegram-bridge` Skill owns action syntax, target routing, Threaded Mode, formatting, and profile-specific debugging guidance. The regular prompt routes applicable turns to that Skill. `telegram_attach` and `telegram_message` remain registered but are model-active only while this instance owns direct transport or holds a live follower registration; disconnect/loss suppresses their schemas and prompt metadata, and recovery restores only the operator's previously active pi-telegram subset.
67
67
  - `telegram_voice` hidden comments request Telegram-native voice delivery through either a JSON object or double-quoted attributes. Equivalent `text` or `value` supplies the spoken payload; explicit `text` takes precedence.
68
68
  - `telegram_button` hidden comments create inline buttons whose taps enqueue prompts through the same two payload forms. Use `label` plus `prompt`, or `value` when both strings are identical. The colon after either action name is optional and does not select the format. Use top-level column-zero comments outside code, quotes, lists, and indented examples; do not emit standalone button actions.
69
69
 
@@ -117,7 +117,7 @@ The file is global across Pi instances. Cooperating instances serialize recursiv
117
117
 
118
118
  Hidden/default semantics are represented by absence:
119
119
 
120
- - `threads.automaticCleanup` defaults to `true`; graceful Pi quit deletes the instance's bound Threaded Mode tab without prompting. Set it to `false`, or use `๐Ÿงน Thread cleanup` in Telegram Settings, to preserve the tab as a restart hint. Settings views and cleanup reload shared config before evaluating this switch, so another live Pi instance's update takes effect without restarting. Confirmed leader/follower teardown persists an exact target/runtime-generation cleanup intent before Telegram deletion; an interrupted attempt remains retryable by the current or successor leader under current authority and clears only after confirmed deletion. A same-profile replacement leader first adopts any still-active binding and cancels its superseded cleanup, so startup never deletes and recreates a reusable thread. If a follower's graceful envelope is missed, the leader may create the same fenced cleanup only after its heartbeat is stale, the OS confirms the exact registered PID no longer exists, cleanup remains enabled, and no replacement registration can overtake deletion. Heartbeat loss alone, live/unknown process liveness, IPC failure, and auth failure remain non-destructive. Invalid-config recovery makes the setting unresolved and therefore skips destructive cleanup. Manual `/telegram-disconnect` keeps its confirmation and teardown behavior regardless of this setting.
120
+ - `threads.automaticCleanup` defaults to `true`; graceful Pi quit deletes the instance's bound Threaded Mode tab without prompting but preserves the owner slot as independent restart intent. Set it to `false`, or use `๐Ÿงน Thread cleanup` in Telegram Settings, to preserve the tab too. A confirmed `/telegram-disconnect`, unlike quit, clears restart ownership. Settings views and cleanup reload shared config before evaluating this switch, so another live Pi instance's update takes effect without restarting. Confirmed leader/follower teardown persists an exact target/runtime-generation cleanup intent before Telegram deletion; an interrupted attempt remains retryable by the current or successor leader under current authority and clears only after confirmed deletion. A same-profile replacement leader first adopts any still-active binding and cancels its superseded cleanup, so startup never deletes and recreates a reusable thread. If a follower's graceful envelope is missed, the leader may create the same fenced cleanup only after its heartbeat is stale, the OS confirms the exact registered PID no longer exists, cleanup remains enabled, and no replacement registration can overtake deletion. Heartbeat loss alone, live/unknown process liveness, IPC failure, and auth failure remain non-destructive. Invalid-config recovery makes the setting unresolved and therefore skips destructive cleanup. Manual `/telegram-disconnect` keeps its confirmation and teardown behavior regardless of this setting.
121
121
  - Every complete intermediate assistant text block from a Telegram-originated turn is delivered once to its immutable target before the existing final reply. This active-turn commentary path remains enabled when `assistant.proactivePush` is `false`; final and terminal-partial segments stay with settlement to prevent duplicate replies. `assistant.proactivePush` defaults to `true` only for local/autonomous work: omit it to project every completed public block, including commentary and the final block, or set it explicitly to `false` to disable that projection. Both paths exclude token deltas, hidden reasoning, tool calls/arguments/results, empty blocks, unknown sources, and stale authority. Projection uses the configured Rich or HTML assistant renderer and binds admitted work to the exact target, profile/token transport generation, direct leader epoch or follower registration generation, and session generation. The old top-level `proactivePush` key is ignored; move the setting manually under `assistant`.
122
122
  - `assistant.activity` accepts exactly `"quiet"`, `"thinking"`, `"tools"`, or `"verbose"`; omitted values default to `"verbose"`, explicit values remain unchanged, and invalid values fail closed to `"quiet"`. Each Pi process reloads the shared file-backed value at `agent-start`, so multi-instance activity isolation never relies on a stale process-local config snapshot. `thinking` shows only provider-exposed thinking, `tools` shows only completed tool activity, and `verbose` shows both. Thinking uses persistent ordinary HTML `sendMessage`/`editMessageText` disclosure with a standard expandable blockquote, a `๐Ÿง ` header carrying the current Pi thinking level, and bounded redacted text whose inline Markdown renders as Telegram HTML. Tools use native Rich Messages with one header followed by separate closed details and JSON pre blocks for bounded redacted arguments, retained updates, and results/errors. Thinking disables link previews on every HTML send/edit and neutralizes HTTP(S) auto-link detection; Rich tool output disables automatic entity detection, with the same protections retained by its HTML fallback. Consecutive tools coalesce only inside the same ordered activity segment and bounded message. Legacy `assistant.activityVerbosity` is read only when `assistant.activity` is absent and is removed by the next Activity Settings write.
123
123
  - Voice Reply `hidden`: no `voice.replyMode` key is persisted; legacy `manual` resolves to this silent default. `mirror` adds `[voice] delivery: automatic voice` only to voice/audio-input turns, while `always` adds the same effective line to every Telegram turn.
@@ -158,7 +158,7 @@ Low-level stable buses:
158
158
 
159
159
  - `registerTelegramUpdateHandler()`
160
160
  - Identity: no id.
161
- - Purpose: observe or consume raw Telegram updates before default routing.
161
+ - Purpose: observe or consume raw Telegram updates before default routing. Its optional execution fence supplies cancellation and a required pre-effect authority check for long-running handlers.
162
162
  - `registerTelegramInboundHandler()`
163
163
  - Identity: no id.
164
164
  - Purpose: generic Telegram-to-Pi transforms.
@@ -342,14 +342,15 @@ Contract:
342
342
 
343
343
  ## Updates
344
344
 
345
- Import from `@llblab/pi-telegram/updates`.
345
+ Import `registerTelegramUpdateHandler`, `TelegramUpdateExecutionFence`, and the advanced `getTelegramUpdateExecutionFence`, `createTelegramUpdateExecutionFenceGuard`, `carryTelegramUpdateExecutionFence`, and `assertTelegramUpdateExecutionCurrent` helpers from `@llblab/pi-telegram/updates`.
346
346
 
347
347
  ```ts
348
- const off = registerTelegramUpdateHandler(async (update) => {
348
+ const off = registerTelegramUpdateHandler(async (update, execution) => {
349
349
  const data = (update as { callback_query?: { data?: string } }).callback_query
350
350
  ?.data;
351
351
  if (!data?.startsWith("myext:")) return "pass";
352
- await handleMyCallback(data);
352
+ await handleMyCallback(data, execution?.signal);
353
+ execution?.assertCurrent();
353
354
  return "consume";
354
355
  });
355
356
  ```