@llblab/pi-telegram 0.24.3 → 0.24.4
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/AGENTS.md +4 -4
- package/CHANGELOG.md +6 -0
- package/README.md +6 -4
- package/docs/architecture.md +5 -5
- package/docs/multi-instance-bus.md +5 -5
- package/docs/public-api.md +7 -3
- package/index.ts +37 -4
- package/lib/bindings.ts +21 -10
- package/lib/bus-follower.ts +5 -1
- package/lib/config.ts +51 -0
- package/lib/locks.ts +7 -0
- package/lib/menu-settings.ts +94 -6
- package/lib/outbound-attachments.ts +10 -15
- package/lib/pi.ts +6 -0
- package/lib/prompts.ts +126 -8
- package/lib/routing.ts +301 -256
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -79,9 +79,9 @@ The core product loop is mobile continuation: start or supervise work in the ter
|
|
|
79
79
|
- The Telegram lock owns external Telegram transport, not local Pi execution. Local extension state and queue runtime are per Pi instance: losing exact `/telegram-connect` ownership must not clear accepted queue state or stop local Pi dispatch, but preview, final delivery, attachments, menus, and other Bot API mutations fail closed once another runtime owns transport. A replacement owner must never receive stale direct mutations merely to preserve old-runtime Telegram output. Bus leader lock entries carry a stable `leaderEpoch` minted on acquisition and preserved across heartbeat refreshes; leader-owned Thread Reconciler cleanup/provisioning actions stamp that epoch and skip destructive side effects if current leadership differs.
|
|
80
80
|
- Telegram transport ownership lives only in the extension-local `tmp/telegram/owners.json` store under `default` or named profile slots. Every mutation must serialize the complete cross-process read/check/write transaction through `owners.json.transaction` and fail closed on contention; atomic payload replacement alone does not make ownership linearizable. Refresh, release, takeover, and irreversible leader work additionally fence against the exact acquired owner and leader epoch. In classic/private-chat mode, transport ownership is the whole bridge ownership and `/telegram-connect` from another live Pi offers classic takeover confirmation. In Threaded Mode, transport ownership is the bus leader role and another live Pi registers as follower instead of offering takeover while the leader is live. If follower registration is unavailable, fall back to classic takeover.
|
|
81
81
|
- Polling checks exact `owners.json` ownership every second but refreshes the durable owner heartbeat only every two seconds; an eight-second stale threshold preserves bounded recovery headroom without turning every ownership check into an atomic disk rewrite. Ownership transactions publish a fully initialized private guard directory containing one exact generation-specific `owner.<generation>.json` through same-parent atomic rename. The owner filename and payload generation must match so stale observation cannot rename replacement metadata. Release verifies the owner and atomically renames the stable guard away before cleanup. Directory recovery first claims owner metadata inside the still-occupied guard so a crashed recoverer can itself be replaced without exposing the stable path; transient guard moves and rollback both retry, while inactive process-global claim generations remain recoverable after failed restoration. If secondary recovery cleanup fails after publishing a replacement main guard, release that exact replacement before propagating the failure. Guards must not depend on hard links, direct file rename-overwrite, incomplete create-then-write state, or age-only recovery; malformed or unsupported guards fail closed.
|
|
82
|
-
- Telegram profiles are parallel bot/runtime realities, not merely alternate credentials. Persist every bot/session field (`botToken`, `botUsername`, `botId`, `allowedUserId`, `lastUpdateId`) under `telegram.json` `profiles.<name>`, including `profiles.default`; keep shared handlers and assistant/voice/time settings top-level. Bare and explicit `default` setup/connect commands address the same profile. Legacy root identity may normalize atomically only when unambiguous; conflicts fail closed without mutation. Any owner slot, IPC endpoint, state snapshot, diagnostics log, thread/bus binding, runtime cache, ownership map, or operator-visible status surface that could mix bot identities must be deliberately classified as global, session-local, or profile-scoped. Use the `default` owner slot for the default profile and validated profile names for named slots; give named profiles isolated observable surfaces unless there is an explicit top-level shared-config reason not to.
|
|
82
|
+
- Telegram profiles are parallel bot/runtime realities, not merely alternate credentials. Persist every bot/session field (`botToken`, `botUsername`, `botId`, `allowedUserId`, `lastUpdateId`) under `telegram.json` `profiles.<name>`, including `profiles.default`; keep shared handlers and assistant/voice/time/thread-lifecycle settings top-level. Bare and explicit `default` setup/connect commands address the same profile. Legacy root identity may normalize atomically only when unambiguous; conflicts fail closed without mutation. Any owner slot, IPC endpoint, state snapshot, diagnostics log, thread/bus binding, runtime cache, ownership map, or operator-visible status surface that could mix bot identities must be deliberately classified as global, session-local, or profile-scoped. Use the `default` owner slot for the default profile and validated profile names for named slots; give named profiles isolated observable surfaces unless there is an explicit top-level shared-config reason not to.
|
|
83
83
|
- Telegram private-chat Threaded Mode is the public switch for the local multi-instance bus. There is no separate public `telegram.json` bus switch: classic/private-chat singleton DM behavior is the base product mode, and when Telegram reports Threaded Mode available for the bot the bridge enables leader/follower bus behavior. If Telegram reports Threaded Mode unavailable or thread operations prove it unavailable, the bridge returns to classic singleton behavior as a first-class mode. During a live Threaded Mode downgrade, the current transport leader keeps the singleton classic polling role and followers disconnect instead of taking over or offering takeover while active thread bindings prove this is a downgrade from a live bus organism. Do not inject slot/thread rename guidance in classic mode. Blocked instances should not silently register as followers unless the effective runtime mode is Threaded Mode.
|
|
84
|
-
- In Threaded Mode, exactly one live leader owns `getUpdates` and direct Telegram Bot API calls for one bot token. `/telegram-connect` means join the local Telegram organism: if no live leader exists the instance becomes leader; if a live leader exists the terminal-visible instance registers as follower; ordinary connect must not offer manual takeover while the leader heartbeat is live. Leadership is an ephemeral transport role, not permanent ownership by the first terminal-visible Pi instance. Heartbeat acknowledgements carry the authenticated live follower-slot roster: on leader loss, the lowest observed live slot attempts the atomic lock first, while higher slots defer for one bounded grace and then re-check so a missing lower follower cannot block recovery indefinitely. Followers must be started by the operator as real Pi processes, register over the local bus, heartbeat, receive only their routed inbound updates, and send outbound replies/previews/files/chat-actions through the leader transport; Telegram must not spawn hidden Pi processes, launch follower subprocesses, or expose a slash command that creates a new local Pi instance. If a leader reloads while followers stay alive, followers must treat rejected/unknown/unauthorized heartbeats as a registration-loss signal and optimistically re-register with the currently active leader before considering promotion; after a short grace window without successful re-registration, a live follower may promote only through an atomic expected-owner transition after the exact observed leader lease becomes stale or inactive. An unreachable IPC endpoint does not authorize replacing a still-live lock owner; remain disconnected/retrying rather than creating split-brain polling. Terminal status must not remain `follower` while the leader registry can no longer route that follower's thread. Heartbeat pruning is liveness bookkeeping, not permission to delete, mark offline, or send a Telegram-visible disconnected notice for a follower's thread; preserve active follower tabs/bindings across transient reload/reconnect incidents and reserve destructive teardown/offline transitions for
|
|
84
|
+
- In Threaded Mode, exactly one live leader owns `getUpdates` and direct Telegram Bot API calls for one bot token. `/telegram-connect` means join the local Telegram organism: if no live leader exists the instance becomes leader; if a live leader exists the terminal-visible instance registers as follower; ordinary connect must not offer manual takeover while the leader heartbeat is live. Leadership is an ephemeral transport role, not permanent ownership by the first terminal-visible Pi instance. Heartbeat acknowledgements carry the authenticated live follower-slot roster: on leader loss, the lowest observed live slot attempts the atomic lock first, while higher slots defer for one bounded grace and then re-check so a missing lower follower cannot block recovery indefinitely. Followers must be started by the operator as real Pi processes, register over the local bus, heartbeat, receive only their routed inbound updates, and send outbound replies/previews/files/chat-actions through the leader transport; Telegram must not spawn hidden Pi processes, launch follower subprocesses, or expose a slash command that creates a new local Pi instance. If a leader reloads while followers stay alive, followers must treat rejected/unknown/unauthorized heartbeats as a registration-loss signal and optimistically re-register with the currently active leader before considering promotion; after a short grace window without successful re-registration, a live follower may promote only through an atomic expected-owner transition after the exact observed leader lease becomes stale or inactive. An unreachable IPC endpoint does not authorize replacing a still-live lock owner; remain disconnected/retrying rather than creating split-brain polling. Terminal status must not remain `follower` while the leader registry can no longer route that follower's thread. Heartbeat pruning is liveness bookkeeping, not permission to delete, mark offline, or send a Telegram-visible disconnected notice for a follower's thread; preserve active follower tabs/bindings across transient reload/reconnect incidents and reserve destructive teardown/offline transitions for confirmed manual disconnect, graceful Pi quit, or confirmed reconciliation cases. If the leader dies, a live follower should eventually be able to take over through stale-leader election/recovery so the local multi-instance organism keeps running. Explicit `/telegram-disconnect` retains destructive confirmation; graceful Pi `quit` invokes the same teardown without prompting when top-level `threads.automaticCleanup` is enabled (default), while disabling it and `reload`, `new`, `resume`, or `fork` preserve the binding through safe handoff/suspension. A leader deletes its own bound thread before releasing transport ownership, while a follower sends an authenticated exact-registration-generation disconnect envelope and waits for the leader to delete its assigned thread, mark the durable binding offline, and remove live routing authority before unregistering. Cleanup must be confirmed by successful deletion or explicit already-gone evidence; incomplete cleanup preserves binding/routing state, keeps the session open, and returns actionable retry guidance. A promoted leader authorizes inherited follower-thread cleanup by its current leader epoch rather than the historical owner label. Status is reactive to the effective runtime role and bot capability: classic/non-thread mode shows ordinary `telegram connected`, active bus leadership shows `telegram leader`, and registered followers show `telegram follower`; stable multi-instance truth belongs in the bus role (`leader`/`follower`), while lifecycle is reserved for transient recovery state such as `electing` and must not duplicate stable roles with labels like `leader-active`. When leadership or Threaded Mode availability changes at runtime, the status line and `/telegram-status` must converge promptly instead of preserving a stale role label.
|
|
85
85
|
- Runtime routing binds to a live `instanceId` plus its exact follower registration generation; current thread identity should use explicit owner records (`leader`, `manual-follower`, `pending-topic`) rather than hiding ownership direction in legacy string keys. Persisted manual-follower records may act as stable restart hints, but only an authenticated live registry entry authorizes inbound forwarding or outbound transport; stale session records must not be treated as live owners. A live instance owns at most one active bound thread; provisioning a new target for the same `instanceId` must remove older current-state bindings and close duplicate Telegram threads when possible. A reload creates a new runtime `instanceId`; if the previous leader slot is still known/alive, keep it occupied in reservations and assign the new leader the next free slot instead of creating a duplicate same-letter tab or blocking startup on Telegram UI convergence.
|
|
86
86
|
- Follower registration readiness should stay on the smallest critical path that makes the follower routable: create or reuse the Telegram target, persist the active binding, mark target-bindings fresh, and return the target to the follower. Registration requires a present exact generation. Cross-session target reuse and any carried target absent from current bindings require one synchronous visibility probe: explicit stale evidence may provision a replacement, while ambiguous/non-stale failure must reject registration without replay, binding invalidation, or speculative replacement. Persist an ambiguously observed absent target only as non-routable `probe-required` restoration evidence so targetless retries and leader reloads must probe the exact target again before activation or replacement. Registration and explicit disconnect mutations for one durable follower profile must serialize across awaited cleanup so an old runtime's disconnect cannot delete or unregister a replacement generation. Manual follower identity must survive a Pi process reload in the same terminal so reload is not treated as a new follower/thread; do not key durable follower ownership only by the short-lived Pi process id. Telegram-visible connected notices, duplicate/replaced thread cleanup, and other reconciliation work are important but non-critical; run them after registration and record slow/failing background steps as runtime events rather than delaying follower usability.
|
|
87
87
|
- Instance slots are extension-owned ring-order metadata. Allocate fresh slots from the `bot.lastSlot` cursor as `A` → `B` → ... → `Z` → `A`, allowing the cursor to move to lower letters after wraparound. Only current live/recovering bindings plus unexpired pending provisions and explicit reservations occupy slots; historical records must not block allocation indefinitely. Preserve a slot on ordinary current binding/thread reuse without advancing or rewinding the fresh-allocation cursor. Follower bindings survive process absence as stable restoration hints until explicit stale/deleted/offline evidence invalidates them. Successful target reuse must refresh the binding timestamp and replacement runtime identity; generic leader startup or heartbeat pruning must not erase the target merely because no follower registration is currently live. An authenticated live follower whose carried exact target is missing from current persisted bindings should recover that target instead of creating another Telegram thread; preserve a carried slot only when it does not collide with another current record. Preserve a cursor that belongs to a live or recoverable binding and continue skipping retained restart hints plus unexpired pending/reserved slots. Explicit stale/deleted/offline reconciliation may release that slot; generic process absence may not. The alphabet cap is a feature — compact single-letter tabs are scannable.
|
|
@@ -89,7 +89,7 @@ The core product loop is mobile continuation: start or supervise work in the ter
|
|
|
89
89
|
- Telegram private-chat Threaded Mode treats `All` as an aggregate/control surface, not a durable `General` thread or process launcher, and leader activation proactively creates or reuses its private-DM thread. When the owner writes in an unknown thread, the leader reclaims and routes the first one only if it lacks an active binding; later unknown threads remain intact and receive an in-thread chooser to reroute the captured prompt or restore a stale binding. Destructive cleanup requires explicit confirmation through `thread-reconciler`; another Pi instance still requires a manually started process followed by `/telegram-connect`. Without Threaded Mode, process the message through classic routing.
|
|
90
90
|
- Telegram extension work should not hold Pi's core agent lifecycle hostage once Pi has produced the semantic result. For Telegram-originated turns, final Telegram delivery, attachment upload, and transport cleanup are extension-owned side effects; schedule them off the critical `agent_end` path when ordering can still be preserved, record failures as runtime events, and keep dispatch of the next Telegram turn behind the delivery task when needed to avoid interleaving previews/finals. Treat a low-level error `agent_end` as retryable while its Telegram turn remains active; only a later successful `agent_end` or Pi's `agent_settled` boundary may finalize and release that turn. Public activity handlers run through isolated asynchronous per-handler queues; lifecycle hooks enqueue normalized events and never await consumer work. Proactive push defaults enabled and requires explicit `assistant.proactivePush: false` to opt out. While enabled, core delivery projects each completed Pi-visible assistant text block from local/autonomous work once and in order; bind admitted blocks to the exact target, profile/token transport stamp, direct leader epoch or follower registration generation, and session generation. Token deltas, hidden reasoning, tool traffic, stale authority, and Telegram-owned turns never enter that projection.
|
|
91
91
|
- Telegram runtime state should be treated as event-driven reconciliation of extension assumptions against observable Telegram signals, not as a full Telegram bot read-model and not as a reason to query Telegram on every action. Keep a local assumption model for bot identity/capabilities, pairing assumptions, thread support, known thread lifecycle, bound instance targets, reservations, and transport health. Invalidate and reconcile the relevant slice on meaningful events: startup/reload, lifecycle service messages, stale-send/API errors, setup/pairing changes, follower register/prune, explicit status/debug requests, and bounded low-frequency leader health ticks. `lib/sync.ts` owns sync slices, observation intake, invalidation triggers, status/debug freshness, and reconciliation scheduling; it must not promise complete bot-state mirroring because Bot API lacks a complete topic listing surface. `lib/thread-reconciler.ts` owns pure Threaded Mode lifecycle planning and should become the only policy authority for destructive thread cleanup decisions. `threads` owns current Telegram UI thread/tab binding primitives and thread-name helpers mapped to Bot API topic transport, `status` renders projections, and `index.ts` only wires ports. `tmp/telegram/logs.jsonl` is a session-local redacted runtime evidence stream for race debugging and resets on extension start / runtime scope changes; it is diagnostics only, not routing/provisioning authority. `state.json` should be an observable/debug snapshot aligned with `/telegram-status` (extension runtime, bot capabilities including `bot.lastSlot`, runtime role, live roster projection, reservations, diagnostics), not routing/provisioning authority. Because every process on one Telegram profile shares this file, only the active transport lock owner may persist it; followers read current state and gain write authority only after promotion. Status-only writes must refresh disk-backed bindings before serialization so a stale loaded snapshot cannot erase newer leader state. Live bus/runtime state is authoritative; file reservations and fresh capability observations may be startup hints/collision guards only; stale snapshots must re-probe before suppressing bus/topic behavior. Do not persist stale/offline/failed thread target history as source of truth.
|
|
92
|
-
- Thread bindings are bidirectional. From the Pi-instance side, an instance knows and preserves its target, slot, thread name, lifecycle state, and direct-delivery defaults. From the Telegram-client/bot side, the bridge observes thread creation/close/reopen/stale-send signals and reconciles them into instance binding state. Lifecycle transitions that affect operator understanding should be Telegram-visible when a live transport remains: every successful follower registration/re-registration gets a compact connected notice in the assigned thread, while heartbeat pruning stays silent because it is only liveness suspicion. These notices should use the instance thread name when known and fall back to the slot letter only while the thread is still unnamed. New unknown threads
|
|
92
|
+
- Thread bindings are bidirectional. From the Pi-instance side, an instance knows and preserves its target, slot, thread name, lifecycle state, and direct-delivery defaults. From the Telegram-client/bot side, the bridge observes thread creation/close/reopen/stale-send signals and reconciles them into instance binding state. Lifecycle transitions that affect operator understanding should be Telegram-visible when a live transport remains: every successful follower registration/re-registration gets a compact connected notice in the assigned thread, while heartbeat pruning stays silent because it is only liveness suspicion. These notices should use the instance thread name when known and fall back to the slot letter only while the thread is still unnamed. New unknown or command-created temporary threads receive the complete forward plus replace/restore control set and remain preserved until the user chooses one. Successful forward deletes the chooser and temporary thread; successful replace/restore always deletes the chooser, rebinds the temporary thread, and deletes only the replaced old thread. Any destructive cleanup must go through `thread-reconciler`. Unknown `forum_topic_created` service events are observations, not destructive cleanup proof.
|
|
93
93
|
- The multi-instance bus uses private bot DMs with Telegram private-chat Threaded Mode enabled for the bot. No supergroup, group, or forum configuration is needed; the thread chat is always the private chat with the paired owner. Threaded capability checks must use bot/private-chat evidence such as `getMe.has_topics_enabled`, incoming `message_thread_id`, and topic operation success/failure; do not use group chat metadata as the control-plane truth for Telegram private-chat Threaded Mode.
|
|
94
94
|
- Target-scoped behavior must preserve `{ chatId, threadId? }` through inbound messages/edits/callbacks/reactions, thread lifecycle service messages, media and split-text grouping, queue mutations, active-turn cleanup, previews, reply deduplication, menus, sections, generated buttons, voice, attachments, and direct delivery. Threaded Mode replies must keep Telegram's reply affordance to the triggering message just like classic DM mode. In Telegram private-chat Threaded Mode, mobile Telegram has been live-verified to render `sendMessage` replies that include top-level `message_thread_id` plus same-chat `reply_parameters.message_id`; Telegram Desktop may fail to show the visual reply header for the same correct payload, so do not treat Desktop-only absence as bridge failure without mobile/payload evidence.
|
|
95
95
|
- Local bus IPC is a local-only trust boundary: private socket directories/files are necessary but not the whole authorization model; never let arbitrary local processes inject prompts, callbacks, API sends, or artifact routes.
|
|
@@ -114,7 +114,7 @@ The core product loop is mobile continuation: start or supervise work in the ter
|
|
|
114
114
|
- Formula guidance belongs in the Telegram-turn prompt contract: use `$...$` for inline math and `$$...$$` for block math; backticks intentionally render formulas as literal code
|
|
115
115
|
- Real code blocks must stay literal and escaped
|
|
116
116
|
- `telegram_attach` is the canonical outbound file-delivery path for Telegram-originated requests; outside active Telegram turns it may send immediately to the paired/default chat, a registered follower's assigned target, or explicit `chat_id` plus optional `thread_id` for explicit local/TUI delivery requests only when this Pi instance owns `/telegram-connect` or is registered with the Threaded Mode multi-instance bus. `telegram_message` is the first-class direct Telegram Markdown text tool for local/TUI prompts and follows the same ownership-or-bus-registration gate; neither direct tool replaces normal active-turn replies. It reuses top-level `telegram_button` comments for inline buttons; buttons must be attached to a text message, never sent as standalone actions
|
|
117
|
-
- Telegram prompt guidance is layered and context-frugal:
|
|
117
|
+
- Telegram prompt guidance is layered, authority-aware, and context-frugal: only a current direct owner or registered follower exposes `telegram_attach`, `telegram_message`, `telegram_help`, their active-tool prompt metadata, and the compact bridge suffix. Successful disconnect or transport-authority loss removes those surfaces for subsequent requests while preserving every foreign tool; reconnect/recovery restores only the pi-telegram tool subset that was active before suspension. Telegram-originated turns receive only the minimal per-turn contract, and detailed voice/button/direct-delivery/Threaded Mode/formatting/debug guidance belongs in `telegram_help` instead of every prompt.
|
|
118
118
|
- Telegram inbound prompt topology keeps current user material contiguous: `[outputs]` produced from current attachments follows `[attachments]` immediately, then independent `[reply]` or forwarded-source context follows the complete current-user block. Forwarded Rich Message media belongs under source-attributed `[attachments|from:<identity>]` immediately after its `[forward|from:<identity>]` block and remains available as model image content. Do not place source context between an attachment and its transcription or handler output.
|
|
119
119
|
- Forward-annotation grouping distinguishes two semantic layers: the forward owns its source text/caption/media, while an optional separate owner-authored annotation normally arrives immediately before it. Use a bounded one-second candidate window in both transport orders: ordinary short human text may wait for an adjacent same-sender/chat/thread forward, and a forwarded message—including a media-only forward with no source caption—may wait for one adjacent owner annotation. A matching opposite-kind message flushes immediately; same-kind messages, commands, bots, ordinary non-forward captions, media groups, target/sender mismatches, reversed ids, and distant ids stay separate. Preserve the candidate across session replacement and follower forwarding without weakening exact routing-generation authority.
|
|
120
120
|
- `telegram_voice` and `telegram_button` are not Pi tools; keep prompts/docs explicit that agents should author markup while voice synthesis provider extensions own TTS/OGG conversion, and pi-telegram owns button routing plus Telegram delivery
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.24.4: Context And Thread Lifecycle Hotfix
|
|
6
|
+
|
|
7
|
+
- `Disconnect Context Availability`: Reconciled `telegram_attach`, `telegram_message`, `telegram_help`, their active-tool prompt metadata, and the compact bridge suffix against effective direct-owner or registered-follower authority. Successful disconnect or authority loss removes only pi-telegram surfaces for subsequent requests; reconnect/recovery restores only the operator's previously active pi-telegram subset across same-process reload, while execute-time transport guards remain. Impact: disconnected sessions stop spending context on unavailable Telegram capabilities or inviting calls that must fail.
|
|
8
|
+
- `Graceful Instance Teardown`: Graceful Pi quit now performs the same generation-fenced thread deletion and authority release as confirmed `/telegram-disconnect`, without prompting during shutdown when `🧹 Auto thread cleanup` is enabled (default). The compact Settings control and `threads.automaticCleanup` config key can preserve tabs as restart hints. Manual disconnect keeps its destructive confirmation, while reload, new-session, resume, and fork replacement preserve same-process bindings. Impact: closing a Pi instance normally removes its Telegram tab instead of leaving an offline restart hint; hard kills remain recoverable through stale binding reconciliation.
|
|
9
|
+
- `Temporary Thread Routing`: Unified commands sent from `All`-created temporary threads with the complete unbound-thread forward and replace/restore controls. Successful forward deletes the chooser and temporary thread; successful restore always deletes the chooser, rebinds the temporary thread, and removes only the replaced old thread. Partial foreign delivery retries only remaining messages, while incomplete thread or chooser deletion retains a cleanup-only retry control without redispatching the command or prompt or leaving an expired visible button. Impact: `/start` and other routed commands no longer leave orphan command tabs, omit recovery controls, or duplicate work while cleanup recovers.
|
|
10
|
+
|
|
5
11
|
## 0.24.3: Queue And Typing Reliability Hotfix
|
|
6
12
|
|
|
7
13
|
- `Typing Backpressure`: Serialized each native activity loop to one in-flight action and coalesced identical leader-side chat actions across direct and follower traffic. Telegram 429 `retry_after` now opens a shared suppression window without delayed retry fan-out or terminal-status projection, while non-rate-limit retries and diagnostics remain intact. Impact: concurrent Threaded Mode activity no longer amplifies `sendChatAction` throttling into repeated `Telegram Error` states.
|
package/README.md
CHANGED
|
@@ -10,6 +10,8 @@ It is a **runtime adapter**, not a remote terminal. Start or supervise work in t
|
|
|
10
10
|
|
|
11
11
|
Proactive push is enabled by default. `assistant.proactivePush` projects every completed public assistant text block from local or autonomous work—including visible checkpoints and the final answer—to the authorized Telegram target once and in order; set it explicitly to `false` to disable projection. It never mirrors local prompts, hidden reasoning, tool traffic, token deltas, Telegram-owned turns, or stale-generation work. See [Outbound](docs/outbound.md#proactive-public-output) and the [configuration reference](docs/public-api.md#configuration-api).
|
|
12
12
|
|
|
13
|
+
Automatic thread cleanup is enabled by default. Graceful Pi quit deletes that instance's Threaded Mode tab; use `🧹 Auto thread cleanup` in Telegram Settings or set `threads.automaticCleanup` to `false` to preserve the tab as a restart hint. This option never changes manual `/telegram-disconnect`, which still asks for confirmation and deletes the tab.
|
|
14
|
+
|
|
13
15
|
This repository is an actively maintained fork of [`badlogic/pi-telegram`](https://github.com/badlogic/pi-telegram). It started from upstream commit [`cb34008`](https://github.com/badlogic/pi-telegram/commit/cb34008460b6c1ca036d92322f69d87f626be0fc) and has since diverged substantially.
|
|
14
16
|
|
|
15
17
|
## Install
|
|
@@ -108,7 +110,7 @@ The first Telegram user to message the bot becomes the allowed owner. Other user
|
|
|
108
110
|
| Buttons | Turn top-level `telegram_button` comments into inline buttons. | Assistant-authored choices become native Telegram interactions. |
|
|
109
111
|
| Callback routing | Route known callbacks to the owner extension and unknown callbacks back into Pi. | Companion extensions can build UI without polling Telegram themselves. |
|
|
110
112
|
| 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. |
|
|
111
|
-
| Reroute and restore |
|
|
113
|
+
| 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. |
|
|
112
114
|
| 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. |
|
|
113
115
|
| 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. |
|
|
114
116
|
| Safety and ownership | Pair one owner, lock transport, scope targets, and reject fake terminal behavior. | Remote access remains explicit, bounded, and understandable. |
|
|
@@ -152,7 +154,7 @@ Run these inside Pi.
|
|
|
152
154
|
| `/telegram-setup <profile>` | Save or update a named-profile bot token |
|
|
153
155
|
| `/telegram-connect` / `/telegram-connect default` | Activate `profiles.default` and acquire its transport ownership |
|
|
154
156
|
| `/telegram-connect <profile>` | Activate a named profile and acquire its transport ownership |
|
|
155
|
-
| `/telegram-disconnect` |
|
|
157
|
+
| `/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 |
|
|
156
158
|
| `/telegram-status` | Inspect connection, mode, queue, transport, and recent diagnostics |
|
|
157
159
|
|
|
158
160
|
Named profile identifiers contain only lowercase ASCII letters and digits (maximum 32 characters); `default`, `main`, and `active` remain reserved.
|
|
@@ -212,7 +214,7 @@ Most controls live in Pi commands or the Telegram menu. Environment variables re
|
|
|
212
214
|
| Inbound file limit | `PI_TELEGRAM_INBOUND_FILE_MAX_BYTES`, `TELEGRAM_MAX_FILE_SIZE_BYTES` |
|
|
213
215
|
| Outbound attachment limit | `PI_TELEGRAM_OUTBOUND_ATTACHMENT_MAX_BYTES`, `TELEGRAM_MAX_ATTACHMENT_SIZE_BYTES` |
|
|
214
216
|
|
|
215
|
-
Defaults are chosen for ordinary private-bot use: saved config in `~/.pi/agent`, inbound temp files in `~/.pi/agent/tmp/telegram`, `assistant: { rendering: "rich", draftPreviews: false }` for assistant answer output, and native Telegram active status for long-running turns.
|
|
217
|
+
Defaults are chosen for ordinary private-bot use: saved config in `~/.pi/agent`, inbound temp files in `~/.pi/agent/tmp/telegram`, `assistant: { rendering: "rich", draftPreviews: false }` for assistant answer output, `threads.automaticCleanup: true` for graceful Threaded Mode teardown, and native Telegram active status for long-running turns.
|
|
216
218
|
|
|
217
219
|
## Extension Platform
|
|
218
220
|
|
|
@@ -243,7 +245,7 @@ Stable public entrypoints are documented in [Public API](./docs/public-api.md),
|
|
|
243
245
|
|
|
244
246
|
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.
|
|
245
247
|
|
|
246
|
-
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. Pi session JSONL contains model history; profile-scoped pi-telegram `logs*.jsonl` contains redacted operational events and is never model context.
|
|
248
|
+
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.
|
|
247
249
|
|
|
248
250
|
## Documentation Map
|
|
249
251
|
|
package/docs/architecture.md
CHANGED
|
@@ -119,10 +119,10 @@ Telegram configuration lives in `~/.pi/agent/telegram.json`. Bot/session identit
|
|
|
119
119
|
|
|
120
120
|
### Runtime Ownership
|
|
121
121
|
|
|
122
|
-
- `/telegram-connect` acquires or moves the active profile's owner slot before polling starts. `/telegram-disconnect` stops polling and releases only that exact slot. In Threaded Mode
|
|
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
123
|
- 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.
|
|
124
124
|
- 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
|
-
- Session replacement 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.
|
|
125
|
+
- 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
126
|
- 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.
|
|
127
127
|
- `owners.json` owns only Telegram transport control. Local extension and accepted queue state remain per Pi instance when ownership moves, but previews, final delivery, dispatch transport mutations, and delayed Bot API work fail closed until exact direct or follower authority becomes valid again.
|
|
128
128
|
- Exact ownership remains checked every second, while the durable owner heartbeat refresh runs every two seconds and becomes stale after eight seconds. This keeps replacement detection responsive while halving steady-state atomic `owners.json` rewrites without changing the cross-platform file-transaction authority. Every acquisition, refresh, release, takeover, and stale recovery serializes through the sibling `owners.json.transaction` guard. The guard publishes one private generation-named owner record atomically, validates filename/payload generation agreement, fences stale recovery and delayed release against replacement-owner ABA, and fails closed on malformed state, unverifiable ownership, contention timeout, or unsupported filesystem behavior. The JSON store publishes through a private same-directory temporary file and atomic rename; atomic payload replacement does not replace transaction serialization.
|
|
@@ -154,9 +154,9 @@ Follower binding is manual and process-first: the operator starts another Pi pro
|
|
|
154
154
|
|
|
155
155
|
### Unbound Thread Detection
|
|
156
156
|
|
|
157
|
-
When Threaded Mode is enabled, writing a message in the `All` tab can create a new thread without an existing instance binding. The bridge detects this during update execution: if a message from the owner has a `message_thread_id` that no instance owns, the message is routed to the unbound-thread handler instead of the leader's normal message handler. In the default runtime, this handler first reclaims the thread for the leader when the leader has no active bound thread, assigns the current leader thread identity, persists the active binding, and serves the prompt locally. If the leader already has an active thread, the handler preserves the prompt in the source Telegram thread and shows
|
|
157
|
+
When Threaded Mode is enabled, writing a message in the `All` tab can create a new thread without an existing instance binding. The bridge detects this during update execution: if a message from the owner has a `message_thread_id` that no instance owns, the message is routed to the unbound-thread handler instead of the leader's normal message handler. In the default runtime, this handler first reclaims the thread for the leader when the leader has no active bound thread, assigns the current leader thread identity, persists the active binding, and serves the prompt locally. If the leader already has an active thread, the handler preserves the prompt in the source Telegram thread and shows the complete forward plus replace/restore chooser. Successful forward deletes the chooser and closes/deletes the confirmed temporary source through `thread-reconciler` proof-before-delete planning and stale-epoch fencing. Successful restore always deletes the chooser, rebinds the source thread to the selected Pi instance, and closes/deletes only that instance's replaced old thread. If foreign batch forwarding partially fails, retry sends only the remaining messages before cleanup. If Telegram cannot confirm thread or chooser deletion, the chooser becomes a cleanup-only or deletion-only retry control so already-routed content never dispatches twice and no visible button expires prematurely. Unknown `forum_topic_created` service events are recorded as observations and are not destructive cleanup proof, because Telegram can deliver creation events before local provisioning/binding writes become visible across reloads. If Threaded Mode is unavailable, the message is processed normally through classic routing.
|
|
158
158
|
|
|
159
|
-
Threadless messages from `All` are not routed as prompts once bound threads exist, because `All` cannot identify the owning Pi instance. Known commands open
|
|
159
|
+
Threadless messages from `All` are not routed as prompts once bound threads exist, because `All` cannot identify the owning Pi instance. Known commands open the same complete forward and replace/restore chooser as ordinary unbound content, while threadless ordinary prompts get guidance to use a bound Pi thread. This prevents accidental empty tabs from black-holing prompts or bypassing the manual follower-registration contract above.
|
|
160
160
|
|
|
161
161
|
The routing identity split is deliberate:
|
|
162
162
|
|
|
@@ -314,7 +314,7 @@ Queue reactions are shortcut controls for waiting turns. Promotion reactions (`
|
|
|
314
314
|
|
|
315
315
|
When `assistant.proactivePush` is enabled and this instance has exact direct or follower transport authority, completed public assistant text blocks from local/autonomous work are sent once and in source order to the instance's authorized target. Visible commentary/checkpoints and the final block use the configured Rich or HTML renderer. Hidden reasoning, tool traffic, token deltas, local prompt text, Telegram-owned turns, and stale generations are not mirrored. 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.
|
|
316
316
|
|
|
317
|
-
Telegram prompt guidance is context-aware.
|
|
317
|
+
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.
|
|
318
318
|
|
|
319
319
|
## In-Flight Model Switching
|
|
320
320
|
|
|
@@ -189,7 +189,7 @@ Manual smoke checklist:
|
|
|
189
189
|
3. Start Pi in a second Windows terminal and run `/telegram-connect`; verify it registers as follower rather than offering takeover, creates/uses its assigned thread, terminal status shows `<ThreadName> Follower` while idle, and a follower prompt flips it to `<ThreadName> Active` while work is running.
|
|
190
190
|
4. From the follower thread, send a prompt that requests inline buttons; tap a button and verify the follow-up prompt queues in the follower instance.
|
|
191
191
|
5. From the follower thread, request a voice reply and/or attachment; verify upload routes through the leader transport into the follower thread.
|
|
192
|
-
6.
|
|
192
|
+
6. With `🧹 Auto thread cleanup` enabled (default), quit the follower Pi normally without an explicit disconnect; verify graceful shutdown deletes its current tab through the leader before local suspension. Disable the setting, quit another follower normally, and verify its tab remains as a restart hint. Separately terminate a follower process abruptly; verify heartbeat pruning remains silent and preserves the tab/binding as a restart hint. Reconnect, run `/telegram-disconnect`, confirm the prompt, and verify the leader confirms deletion before local polling stops.
|
|
193
193
|
7. Reload the leader and verify status/debug output does not expose raw pipe internals except in explicit diagnostics.
|
|
194
194
|
|
|
195
195
|
If any step fails, capture `telegram-status --debug`, `tmp/telegram/state.json`, `tmp/telegram/logs.jsonl`, and, after a reload, `tmp/telegram/logs._prev.jsonl`. Debug status prints local leader/follower endpoints with their active transport kind (`pipe` or `socket`), while the runtime log records request-scoped transport failures with envelope kind, request id, retry attempt, endpoint, and classified IPC error. Reloads preserve the prior JSONL log as `logs._prev.jsonl` so the evidence that caused the reload is not immediately overwritten.
|
|
@@ -214,7 +214,7 @@ In Telegram private-chat Threaded Mode:
|
|
|
214
214
|
- The private bot chat is a tabbed instance workspace, not a classic `General + threads` forum.
|
|
215
215
|
- `All` is an aggregate view, not a process launcher. Explicit new instances use live Pi follower registration: the operator starts Pi in a terminal and runs `/telegram-connect`; owner-created empty threads are observed but not treated as a Pi instance until the user chooses a route or restore action.
|
|
216
216
|
- The leader proactively creates or reclaims its own thread on startup/activation when Threaded Mode is available, so the visible leader has the same two-way binding as followers.
|
|
217
|
-
- **Unbound thread detection**: when the owner writes in an unknown `message_thread_id`, the bridge checks effective Threaded Mode state. If the current leader has no active bound thread, that new thread is reclaimed for the leader and the prompt is served locally. Otherwise the bridge preserves
|
|
217
|
+
- **Unbound thread detection**: when the owner writes in an unknown `message_thread_id`, the bridge checks effective Threaded Mode state. If the current leader has no active bound thread, that new thread is reclaimed for the leader and the prompt is served locally. Otherwise the bridge preserves prompts and commands in that Telegram thread and shows the complete forward plus replace/restore chooser. Successful forward deletes the chooser and confirmed temporary source through `thread-reconciler`; successful restore always deletes the chooser, rebinds the source, and deletes only the selected instance's replaced old thread. Partial foreign batch delivery retries only remaining messages; incomplete thread or chooser deletion retains a cleanup-only retry control without redispatching routed content or leaving an expired visible button.
|
|
218
218
|
- Unknown later threads and threadless prompt messages are not silently routed to the leader and never launch hidden Pi processes. The default and only operator path for a new visible instance is starting a visible second Pi process and letting it register as follower through `/telegram-connect`. A manual follower with the same stable binding identity reclaims its current persisted thread across process restart; only an authenticated live registration becomes routing authority. Explicit stale/deleted observations invalidate that restoration hint before a fresh thread is provisioned.
|
|
219
219
|
- Thread lifecycle service messages (`forum_topic_created`, `forum_topic_closed`, `forum_topic_reopened`, deletion/stale send errors) update observations and binding state. Closed/deleted leader or follower threads can be reclaimed or recreated deliberately. Leader startup also probes reused own threads with a non-visible chat action; if Telegram reports the thread closed/deleted, the binding is marked stale and a fresh leader thread is created. Unknown `forum_topic_created` service events are observation-only and are not destructive cleanup proof.
|
|
220
220
|
- Bidirectional binding is a core UX requirement, not an implementation detail: Pi instances actively advertise/remember their thread identity, while the bot observes Telegram-client thread state and reflects it back into instance state. This keeps the system responsive, recognizable, and controllable even when the operator closes tabs, writes from `All`, or a follower later becomes leader.
|
|
@@ -303,7 +303,7 @@ Threaded Mode should make follower threads behave like normal Telegram instance
|
|
|
303
303
|
| Leader election / promotion | Current leader keeps its thread across reload | A promoted follower keeps its existing thread, slot, and name when elected and after later reload | Promotion converts the current follower binding into the leader profile before forced lock acquisition, so leader startup reuses it instead of provisioning a new thread | Follower heartbeat recovery passes binding snapshot into promotion; own-topic provisioner reuses promoted bindings |
|
|
304
304
|
| `/start` command/menu bootstrap | Registers visible bot commands and opens the menu | Follower `/start` can refresh the bot command menu through the leader and open its local menu without warnings | Bot command registration is a validated global Bot API call allowed through trusted follower bus transport | Bus allowlist regression for `setMyCommands` |
|
|
305
305
|
| Follower reconnect | Existing leader binding is reused only when still usable | Same-process `/new` or `/reload` suspends the old follower socket/context and automatically re-registers the new session to the exact prior target; explicit reconnect to a genuinely closed/stale Telegram tab still recreates a visible thread before success | A short-lived handoff carries the assigned target across session replacement, and the leader transfers that binding to the new runtime instance id by stable manual-follower identity; stale Bot API errors remain the proof for fresh provisioning | Session handoff/refresh, leader binding-transfer, persisted leader-reload reuse, and stale-target replacement regressions |
|
|
306
|
-
| Unbound thread reroute/restore |
|
|
306
|
+
| Unbound thread reroute/restore | Prompt- and command-created temporary threads expose forward plus replace/restore; forward deletes the chooser/source, while restore deletes the chooser, rebinds the source, and removes only the replaced old thread | Same complete chooser exposes all currently live bus leader/follower targets; restore is offered from concrete unbound threads, not historical snapshots | Live bus roster plus active target bindings define the selectable set; history/state snapshots are not authority | Routing chooser regressions for live target filtering and restore rows |
|
|
307
307
|
| Status/menu diagnostics | Status reflects leader role, queue, and target | Follower status reflects follower role, thread name, queue, and bus health | Status is local runtime truth plus bus registration state, not leader queue state | Status and bus diagnostics tests |
|
|
308
308
|
|
|
309
309
|
## Queue And State Scoping
|
|
@@ -379,11 +379,11 @@ All files containing routing, chat ids, thread ids, or process details use priva
|
|
|
379
379
|
- Leader prunes the follower from the live registry after missed heartbeats, but heartbeat pruning is only immediate liveness bookkeeping.
|
|
380
380
|
- 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.
|
|
381
381
|
- 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.
|
|
382
|
-
- Persisted current manual-follower bindings survive process absence as restoration hints, but cannot receive inbound or outbound work until the replacement follower authenticates and registers with a fresh generation.
|
|
382
|
+
- Persisted current manual-follower bindings survive abrupt process absence as restoration hints, but cannot receive inbound or outbound work until the replacement follower authenticates and registers with a fresh generation. Graceful Pi quit requests exact-generation thread teardown before lifecycle suspension when automatic thread cleanup is enabled; disabling it deliberately preserves the restart hint.
|
|
383
383
|
- Fresh registration sends a compact connected notice in the assigned thread; cross-session restoration uses that same notice as the visibility probe and follows the stale/ambiguous recovery contract defined above.
|
|
384
384
|
- 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.
|
|
385
385
|
- Successful forwarded updates and follower-originated API calls refresh liveness, so active followers are not pruned only because the interval heartbeat tick lagged.
|
|
386
|
-
- Destructive follower thread teardown belongs to
|
|
386
|
+
- 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 automatic thread cleanup is enabled (default). A registered follower sends an authenticated request fenced by its exact registration generation; the active leader closes and deletes that follower's exact topic, marks its durable binding offline, removes live routing authority, and acknowledges completion before the follower stops. Cleanup counts as confirmed only after successful deletion or explicit already-gone evidence. Incomplete manual cleanup preserves the binding and registration for retry; quit records the failure and completes safe suspension. A promoted leader uses its current owned leader epoch even when the inherited record still carries a historical `manual-follower` owner label.
|
|
387
387
|
- Explicit stale/deleted/offline observations invalidate reuse; mere process absence does not.
|
|
388
388
|
|
|
389
389
|
### Thread is deleted
|
package/docs/public-api.md
CHANGED
|
@@ -41,7 +41,7 @@ Stable commands inside Pi:
|
|
|
41
41
|
|
|
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
|
-
- `/telegram-disconnect` — stop polling and release ownership without deleting or silencing accepted local queue state. In Threaded Mode it
|
|
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
45
|
- `/telegram-status` — show connection, polling, execution, queue, and recent event diagnostics.
|
|
46
46
|
|
|
47
47
|
### Telegram commands
|
|
@@ -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 from local/TUI-initiated work when this Pi instance owns `/telegram-connect` or is registered with the multi-instance bus. 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.
|
|
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.
|
|
67
67
|
- `telegram_voice` hidden comments request Telegram-native voice delivery.
|
|
68
68
|
- `telegram_button` hidden comments create inline buttons whose taps enqueue prompts. Use top-level column-zero comments outside code, quotes, lists, and indented examples; do not emit JSON button specs or standalone button actions.
|
|
69
69
|
|
|
@@ -104,15 +104,19 @@ interface TelegramConfig {
|
|
|
104
104
|
injectionMode?: "hidden" | "always" | "interval";
|
|
105
105
|
interval?: number;
|
|
106
106
|
};
|
|
107
|
+
threads?: {
|
|
108
|
+
automaticCleanup?: boolean;
|
|
109
|
+
};
|
|
107
110
|
}
|
|
108
111
|
```
|
|
109
112
|
|
|
110
|
-
Bot/session identity always persists under `profiles.<name>`. The ordinary setup path uses `profiles.default`; `/telegram-setup default` and `/telegram-connect default` are exact aliases for the bare commands. Named profiles use the same shape. Shared handlers plus `assistant`, `voice`, and `
|
|
113
|
+
Bot/session identity always persists under `profiles.<name>`. The ordinary setup path uses `profiles.default`; `/telegram-setup default` and `/telegram-connect default` are exact aliases for the bare commands. Named profiles use the same shape. Shared handlers plus `assistant`, `voice`, `time`, and `threads` remain top-level. On the first `0.24.0` load, unambiguous legacy root identity moves atomically into `profiles.default`; identical duplicates collapse, complementary fields merge, and conflicting values fail closed without modifying the file.
|
|
111
114
|
|
|
112
115
|
The file is global across Pi instances. Cooperating instances serialize recursive delta merges through `telegram.json.transaction`, preserve unrelated global/profile changes from newer disk snapshots, and merge `lastUpdateId` monotonically. A semantically unchanged merge adopts the latest disk state in memory without replacing the file; later commits win when two deltas intentionally change the same leaf. For manual edits, stop or idle the connected instances, publish a complete valid file atomically, and let them reload. A non-transactional editor racing Pi persistence has no same-leaf conflict guarantee.
|
|
113
116
|
|
|
114
117
|
Hidden/default semantics are represented by absence:
|
|
115
118
|
|
|
119
|
+
- `threads.automaticCleanup` defaults to `true`; graceful Pi quit deletes the instance's bound Threaded Mode tab without prompting. Set it to `false`, or use `🧹 Auto thread cleanup` in Telegram Settings, to preserve the tab as a restart hint. Settings views and graceful quit reload shared config before evaluating this switch, so another live Pi instance's update takes effect without restarting. Invalid-config recovery makes the quit-time setting unresolved and therefore skips destructive automatic cleanup. Manual `/telegram-disconnect` keeps its confirmation and teardown behavior regardless of this setting.
|
|
116
120
|
- `assistant.proactivePush` defaults to `true`; omit it to keep projection enabled, or set it explicitly to `false` to disable it. When enabled, each completed public assistant text block from local or autonomous work is projected to the authorized Telegram target once and in source order. This includes visible intermediate commentary/checkpoints and the final block. It excludes token deltas, hidden reasoning, tool calls/arguments/results, Telegram-owned turns, empty blocks, 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`.
|
|
117
121
|
- 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.
|
|
118
122
|
- Agent activity status is not configurable in this release. Telegram uses native `sendChatAction(typing)` / product `...active` status as the only automatic in-chat work signal before the final reply.
|
package/index.ts
CHANGED
|
@@ -30,6 +30,7 @@ import * as Paths from "./lib/paths.ts";
|
|
|
30
30
|
import * as Pi from "./lib/pi.ts";
|
|
31
31
|
import * as Polling from "./lib/polling.ts";
|
|
32
32
|
import * as Preview from "./lib/preview.ts";
|
|
33
|
+
import * as Prompts from "./lib/prompts.ts";
|
|
33
34
|
import * as PromptTemplates from "./lib/prompt-templates.ts";
|
|
34
35
|
import * as Queue from "./lib/queue.ts";
|
|
35
36
|
import * as Replies from "./lib/replies.ts";
|
|
@@ -53,9 +54,11 @@ type ActivePiModel = NonNullable<Pi.ExtensionContext["model"]>;
|
|
|
53
54
|
export default function (pi: Pi.ExtensionAPI) {
|
|
54
55
|
const piRuntime = Pi.createExtensionApiRuntimePorts(pi);
|
|
55
56
|
const {
|
|
57
|
+
getActiveTools,
|
|
56
58
|
getCommands,
|
|
57
59
|
getThinkingLevel,
|
|
58
60
|
sendUserMessage,
|
|
61
|
+
setActiveTools,
|
|
59
62
|
setModel,
|
|
60
63
|
setThinkingLevel,
|
|
61
64
|
} = piRuntime;
|
|
@@ -85,8 +88,15 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
85
88
|
let telegramBusLifecycleOverridePhase:
|
|
86
89
|
Status.TelegramBridgeBusLifecyclePhase | undefined;
|
|
87
90
|
const telegramBusFollowerRegistry = Bus.createTelegramBusFollowerRegistry();
|
|
91
|
+
let modelContextAvailabilityRuntime:
|
|
92
|
+
| Prompts.TelegramModelContextAvailabilityRuntime
|
|
93
|
+
| undefined;
|
|
88
94
|
const telegramBusFollowerRegistrationState =
|
|
89
|
-
BusFollower.createTelegramBusFollowerRegistrationState(
|
|
95
|
+
BusFollower.createTelegramBusFollowerRegistrationState({
|
|
96
|
+
onAvailabilityChanged() {
|
|
97
|
+
modelContextAvailabilityRuntime?.reconcile();
|
|
98
|
+
},
|
|
99
|
+
});
|
|
90
100
|
const telegramBusLeaderState =
|
|
91
101
|
Threads.createTelegramLeaderThreadStateRuntime();
|
|
92
102
|
const telegramThreadCapabilityState =
|
|
@@ -154,6 +164,21 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
154
164
|
lock: lockRuntime,
|
|
155
165
|
contextStore: telegramSessionContextStore,
|
|
156
166
|
});
|
|
167
|
+
modelContextAvailabilityRuntime =
|
|
168
|
+
Prompts.createTelegramModelContextAvailabilityRuntime({
|
|
169
|
+
getActiveTools,
|
|
170
|
+
setActiveTools,
|
|
171
|
+
isAvailable() {
|
|
172
|
+
return (
|
|
173
|
+
ownsTelegramDirectDelivery() ||
|
|
174
|
+
telegramBusFollowerRegistrationState.isRegistered()
|
|
175
|
+
);
|
|
176
|
+
},
|
|
177
|
+
canReconcile() {
|
|
178
|
+
const ctx = telegramSessionContextStore.get();
|
|
179
|
+
return !ctx || Pi.isExtensionContextIdle(ctx);
|
|
180
|
+
},
|
|
181
|
+
});
|
|
157
182
|
const activeTurnRuntime = Queue.createTelegramActiveTurnStore();
|
|
158
183
|
const proactivePushTargetGetter =
|
|
159
184
|
Config.createTelegramProactivePushTargetGetter({
|
|
@@ -619,6 +644,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
619
644
|
});
|
|
620
645
|
const settingsMenuRuntime = MenuSettings.createTelegramSettingsMenuRuntime(
|
|
621
646
|
{
|
|
647
|
+
reloadConfig: configStore.load,
|
|
622
648
|
getModelMenuState: getQueueMenuState,
|
|
623
649
|
getStoredModelMenuState: modelMenuRuntime.getState,
|
|
624
650
|
storeModelMenuState: modelMenuRuntime.storeState,
|
|
@@ -935,6 +961,8 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
935
961
|
registerFollowerWithOwner:
|
|
936
962
|
threadAwarePollingPorts.registerFollowerWithOwner,
|
|
937
963
|
stopFollowerRegistration: threadAwarePollingPorts.stopFollowerRegistration,
|
|
964
|
+
onTransportAvailabilityChanged:
|
|
965
|
+
modelContextAvailabilityRuntime.reconcile,
|
|
938
966
|
updateStatus,
|
|
939
967
|
recordRuntimeEvent,
|
|
940
968
|
});
|
|
@@ -1021,7 +1049,10 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1021
1049
|
if (!record?.target.threadId) return undefined;
|
|
1022
1050
|
return record.threadName ?? "current Telegram thread";
|
|
1023
1051
|
},
|
|
1024
|
-
onTransportChanged
|
|
1052
|
+
onTransportChanged() {
|
|
1053
|
+
deliveryLifecycleRuntime.onSessionStart();
|
|
1054
|
+
modelContextAvailabilityRuntime.reconcile();
|
|
1055
|
+
},
|
|
1025
1056
|
getStatusLines,
|
|
1026
1057
|
buttonActionStore,
|
|
1027
1058
|
sendMarkdownReply,
|
|
@@ -1058,7 +1089,10 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1058
1089
|
previewRuntime,
|
|
1059
1090
|
promptDispatchRuntime,
|
|
1060
1091
|
deferredQueueDispatchRuntime,
|
|
1061
|
-
|
|
1092
|
+
modelContextAvailabilityRuntime,
|
|
1093
|
+
disconnectOnQuit: disconnectTelegramAndDeleteCurrentThread,
|
|
1094
|
+
resolveAutomaticThreadCleanupEnabled:
|
|
1095
|
+
configControls.resolveAutomaticThreadCleanupEnabled,
|
|
1062
1096
|
buttonActionStore,
|
|
1063
1097
|
callMultipart,
|
|
1064
1098
|
sendChatAction,
|
|
@@ -1071,7 +1105,6 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1071
1105
|
sendGuestReply,
|
|
1072
1106
|
finalizeMarkdownPreview,
|
|
1073
1107
|
proactivePushTargetGetter,
|
|
1074
|
-
isProactivePushEnabled: configControls.isProactivePushEnabled,
|
|
1075
1108
|
getAssistantRenderingMode: configControls.getAssistantRenderingMode,
|
|
1076
1109
|
recordMessageOwnership: messageOwnershipRuntime.recordLocal,
|
|
1077
1110
|
canSendAgentActivity(ctx) {
|
package/lib/bindings.ts
CHANGED
|
@@ -295,10 +295,9 @@ interface TelegramLifecycleBindingDeps {
|
|
|
295
295
|
>;
|
|
296
296
|
promptDispatchRuntime: Runtime.TelegramPromptDispatchRuntime<Pi.ExtensionContext>;
|
|
297
297
|
deferredQueueDispatchRuntime: Queue.TelegramDeferredQueueDispatchRuntime<Pi.ExtensionContext>;
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
>;
|
|
298
|
+
modelContextAvailabilityRuntime: Prompts.TelegramModelContextAvailabilityRuntime;
|
|
299
|
+
disconnectOnQuit?: () => Promise<unknown>;
|
|
300
|
+
resolveAutomaticThreadCleanupEnabled?: () => boolean | Promise<boolean>;
|
|
302
301
|
buttonActionStore: OutboundHandlers.TelegramButtonActionStore;
|
|
303
302
|
callMultipart: OutboundHandlers.TelegramVoiceReplySenderDeps["sendMultipart"];
|
|
304
303
|
sendChatAction: NonNullable<
|
|
@@ -338,7 +337,6 @@ interface TelegramLifecycleBindingDeps {
|
|
|
338
337
|
Keyboard.TelegramInlineKeyboardMarkup
|
|
339
338
|
>["finalizeMarkdownPreview"];
|
|
340
339
|
proactivePushTargetGetter: () => Queue.TelegramQueueTarget | undefined;
|
|
341
|
-
isProactivePushEnabled: () => boolean;
|
|
342
340
|
getAssistantRenderingMode: () => "rich" | "html";
|
|
343
341
|
recordMessageOwnership?: (input: {
|
|
344
342
|
chatId: number;
|
|
@@ -367,7 +365,9 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
367
365
|
previewRuntime,
|
|
368
366
|
promptDispatchRuntime,
|
|
369
367
|
deferredQueueDispatchRuntime,
|
|
370
|
-
|
|
368
|
+
modelContextAvailabilityRuntime,
|
|
369
|
+
disconnectOnQuit,
|
|
370
|
+
resolveAutomaticThreadCleanupEnabled,
|
|
371
371
|
buttonActionStore,
|
|
372
372
|
callMultipart,
|
|
373
373
|
sendChatAction,
|
|
@@ -380,7 +380,6 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
380
380
|
sendGuestReply,
|
|
381
381
|
finalizeMarkdownPreview,
|
|
382
382
|
proactivePushTargetGetter,
|
|
383
|
-
isProactivePushEnabled,
|
|
384
383
|
getAssistantRenderingMode,
|
|
385
384
|
recordMessageOwnership,
|
|
386
385
|
canSendAgentActivity,
|
|
@@ -629,6 +628,7 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
629
628
|
previewRuntime.invalidate();
|
|
630
629
|
assistantOutputRuntime.start();
|
|
631
630
|
activityRuntime.onSessionStart?.();
|
|
631
|
+
modelContextAvailabilityRuntime.reconcile();
|
|
632
632
|
await sessionLifecycleRuntime.onSessionStart(event, ctx);
|
|
633
633
|
},
|
|
634
634
|
async onSessionShutdown(event, ctx) {
|
|
@@ -637,6 +637,17 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
637
637
|
activityRuntime.onSessionShutdown();
|
|
638
638
|
assistantOutputRuntime.stop();
|
|
639
639
|
compactionObserver.onSessionShutdown();
|
|
640
|
+
if (event.reason === "quit" && disconnectOnQuit) {
|
|
641
|
+
try {
|
|
642
|
+
const automaticCleanupEnabled =
|
|
643
|
+
(await resolveAutomaticThreadCleanupEnabled?.()) ?? true;
|
|
644
|
+
if (automaticCleanupEnabled) await disconnectOnQuit();
|
|
645
|
+
} catch (error) {
|
|
646
|
+
recordRuntimeEvent("session", error, {
|
|
647
|
+
phase: "automatic-disconnect-on-quit",
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
}
|
|
640
651
|
await sessionLifecycleRuntime.onSessionShutdown(event, ctx);
|
|
641
652
|
},
|
|
642
653
|
onSessionBeforeCompact(event, ctx) {
|
|
@@ -704,11 +715,11 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
704
715
|
if (!isSessionContextActive(ctx)) return;
|
|
705
716
|
await agentLifecycleHooks.onAgentSettled(event, ctx);
|
|
706
717
|
activityRuntime.onAgentSettled();
|
|
718
|
+
modelContextAvailabilityRuntime.reconcile();
|
|
707
719
|
},
|
|
708
720
|
onBeforeAgentStart: Prompts.createTelegramProactiveBeforeAgentStartHook({
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
isCurrentOwner: lockOwnershipGuard.ownsContext,
|
|
721
|
+
reconcileAvailability: modelContextAvailabilityRuntime.reconcile,
|
|
722
|
+
isAvailable: canSendAgentActivity,
|
|
712
723
|
}),
|
|
713
724
|
});
|
|
714
725
|
}
|
package/lib/bus-follower.ts
CHANGED
|
@@ -841,7 +841,9 @@ export function createTelegramBusFollowerSessionRefreshHook<TContext>(
|
|
|
841
841
|
};
|
|
842
842
|
}
|
|
843
843
|
|
|
844
|
-
export function createTelegramBusFollowerRegistrationState(
|
|
844
|
+
export function createTelegramBusFollowerRegistrationState(
|
|
845
|
+
options: { onAvailabilityChanged?: () => void } = {},
|
|
846
|
+
): TelegramBusFollowerRegistrationState {
|
|
845
847
|
let registered = false;
|
|
846
848
|
let target: TelegramTarget | undefined;
|
|
847
849
|
let slot: string | undefined;
|
|
@@ -861,11 +863,13 @@ export function createTelegramBusFollowerRegistrationState(): TelegramBusFollowe
|
|
|
861
863
|
).sort();
|
|
862
864
|
},
|
|
863
865
|
setRegistered: (next, nextTarget, metadata) => {
|
|
866
|
+
const availabilityChanged = registered !== next;
|
|
864
867
|
registered = next;
|
|
865
868
|
target = next ? (nextTarget ? { ...nextTarget } : undefined) : undefined;
|
|
866
869
|
slot = next ? metadata?.slot : undefined;
|
|
867
870
|
threadName = next ? metadata?.threadName : undefined;
|
|
868
871
|
generation = next ? metadata?.generation : undefined;
|
|
872
|
+
if (availabilityChanged) options.onAvailabilityChanged?.();
|
|
869
873
|
},
|
|
870
874
|
};
|
|
871
875
|
}
|