@llblab/pi-telegram 0.18.0 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -74,19 +74,19 @@ The core product loop is mobile continuation: start or supervise work in the ter
74
74
  - The bridge is session-local, paired to one allowed Telegram user, and owns a local queue aligned with Pi lifecycle hooks
75
75
  - The Telegram lock owns only external control/polling. Local extension state and queue runtime are per Pi instance: losing `/telegram-connect` ownership stops live Telegram control here, but must not clear, silence, or stop active/queued preview, final delivery, or dispatch in this instance. 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.
76
76
  - The Telegram lock is always the transport ownership lock. In classic/private-chat mode, transport ownership is the whole bridge ownership and `/telegram-connect` from another live Pi should offer the classic takeover confirmation. In Threaded Mode, transport ownership is the bus leader role and `/telegram-connect` from another live Pi should register as follower instead of offering takeover while the leader is live. If follower registration is not applicable because Threaded Mode is disabled or unavailable, fall back to the classic takeover path.
77
- - BotFather 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 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. 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.
77
+ - 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. 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.
78
78
  - 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. 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 itself and force-acquire transport leadership so the organism degrades forward instead of staying split-brain. 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 explicit disconnect 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` is a manual thread teardown: a leader deletes its own bound thread before releasing transport ownership, while a follower asks the leader to delete its assigned thread through scoped bus API before unregistering. 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.
79
79
  - Runtime routing binds to a live `instanceId`; current thread identity should use explicit owner records (`leader`, `manual-follower`, `pending-topic`) rather than hiding ownership direction in legacy string keys. Compatibility keys may be derived internally, but 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.
80
80
  - 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. 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.
81
81
  - Instance slots are extension-owned ordering metadata. Allocate new slots monotonically `A` → `B` → ... → `Z` → `A` when a new Telegram thread is truly needed. Reclaim only current pending bindings; do not reclaim inactive stale/offline/failed history because it may point at closed/deleted Telegram tabs. Preserve a slot on current binding/thread reuse. If all current bindings disappear, continue from the compact `bot.lastSlot` cursor in `state.json` instead of retaining old session/thread records. TTL-pruned `pendingProvisions` are scratchpad state: they reserve in-flight provisioning slots and protect cleanup planning, not durable routing history. The alphabet cap is a feature — compact single-letter tabs are scannable.
82
82
  - Instance thread names are provisioner-authored identity metadata, not model-authored output. Fresh Threaded Mode threads select one of five curated compact 4-6 letter Latin names for the assigned slot letter using provisioning timestamp entropy, then create the Telegram thread with that thread name immediately. The title is the thread name itself, not `{slot} {threadName}` or `{slot} — {threadName}`. Do not register or reintroduce an agent-facing thread rename tool; removing the extra rename prompt/tool turn is an intentional simplicity constraint. Thread names must mirror into terminal status and live diagnostics from the bound thread record; do not hide a valid baked/custom thread name behind old slot-prefix validation. A follower that later becomes leader keeps its existing name; leadership changes are transport role changes, not identity resets.
83
- - BotFather private-chat Threaded Mode treats `All` as an aggregate/control surface, not a durable `General` thread and not a process launcher. Leader activation creates/reuses a leader thread in the private bot DM (chat = allowedUserId) proactively. If all threads are closed and the owner writes from `All`, a first new unbound thread should be reclaimed by the leader when the leader lacks an active bound thread. Later unknown threads require explicit routing or restore; creating another Pi instance is a manual terminal action followed by `/telegram-connect`.
83
+ - Telegram private-chat Threaded Mode treats `All` as an aggregate/control surface, not a durable `General` thread and not a process launcher. Leader activation creates/reuses a leader thread in the private bot DM (chat = allowedUserId) proactively. If all threads are closed and the owner writes from `All`, a first new unbound thread should be reclaimed by the leader when the leader lacks an active bound thread. Later unknown threads require explicit routing or restore; creating another Pi instance is a manual terminal action followed by `/telegram-connect`.
84
84
  - Unbound thread detection: when a message arrives from the owner in an unknown `message_thread_id` while Threaded Mode is active, the default runtime first reclaims it for the leader if the leader has no active bound thread and routes the prompt locally. Later unknown threads are preserved by default and receive an in-thread reroute chooser that can send the captured prompt to a live thread or restore a stale leader/follower binding through explicit restore actions; destructive cleanup happens only after explicit user confirmation and through `thread-reconciler`. If the operator wants another Pi instance, they start Pi in a terminal and run `/telegram-connect`; Telegram-originated hidden auto-spawn and `/thread`-style process launch flows are intentionally absent. If Threaded Mode is unavailable, the message is processed normally through classic routing.
85
85
  - 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.
86
86
  - 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. 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.
87
87
  - 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 (owner writing in `All`) are preserved until the user explicitly chooses a reroute/restore/new-instance action that makes cleanup eligible; any destructive cleanup must go through `thread-reconciler`. Unknown `forum_topic_created` service events are observations, not destructive cleanup proof.
88
- - The multi-instance bus uses private bot DMs with Topics enabled (BotFather Threaded Mode). 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 BotFather private-chat Threaded Mode.
89
- - 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 private BotFather 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.
88
+ - 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.
89
+ - 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.
90
90
  - 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.
91
91
  - Queue admission is explicit and validated: immediate commands, control lane, priority lane, and default lane must preserve allowed kind/lane pairings
92
92
  - Dispatch is gated by active turns, pending dispatch, unsettled control work, compaction, `ctx.isIdle()`, and Pi pending messages; dispatched prompts remain queued until `agent_start` consumes them
package/CHANGELOG.md CHANGED
@@ -1,25 +1,31 @@
1
1
  # Changelog
2
2
 
3
- ## 0.18.0: Thread Mode
3
+ ## 0.18.1: Windows setup transport hotfix
4
4
 
5
- - `[Thread Mode]` Added BotFather private-chat Thread Mode while keeping classic private DM as the base mode. BotFather Thread Mode is now the bus switch: in ordinary private-DM mode, one Pi session owns the bot; when Telegram exposes Threads, one live leader owns polling/Bot API transport and visible follower Pi processes join explicitly with `/telegram-connect`. Impact: operators can choose either a simple single-instance bot or a multi-thread development workspace without competing pollers or hidden process spawning.
6
- - `[Capability Switching]` Reworked Thread Mode availability around private-bot evidence: `getMe.has_topics_enabled`, incoming `message_thread_id`, and private-thread operation success/failure. A 5-second monitor can hot-upgrade classic mode to Thread Mode and hot-downgrade back to classic without `/reload`. Impact: BotFather Thread Mode behaves as a progressive enhancement instead of a startup-only assumption.
5
+ - `[Setup]` `/telegram-setup` token validation now uses the same fallback-aware Telegram transport as normal API calls. Impact: Windows/QEMU hosts that fail native `fetch` during bot-token validation can retry through IPv4 fallback instead of failing before config is saved.
6
+ - `[Setup]` Token validation transport failures now report a setup error notification instead of escaping as a command failure. Impact: operators get a clear retryable setup failure when Telegram is unreachable.
7
+ - `[Docs]` Normalized Threaded Mode language across README, architecture docs, prompt guidance, and changelog: `Threaded Mode` is the mode name, `thread` is the user-visible Telegram surface, and BotFather is only the bot configuration tool. Impact: release docs no longer imply BotFather owns runtime Threaded Mode behavior.
8
+
9
+ ## 0.18.0: Threaded Mode
10
+
11
+ - `[Threaded Mode]` Added Telegram private-chat Threaded Mode while keeping classic private DM as the base mode. Threaded Mode is now the bus switch: in ordinary private-DM mode, one Pi session owns the bot; when Telegram exposes threads for the bot, one live leader owns polling/Bot API transport and visible follower Pi processes join explicitly with `/telegram-connect`. Impact: operators can choose either a simple single-instance bot or a multi-thread development workspace without competing pollers or hidden process spawning.
12
+ - `[Capability Switching]` Reworked Threaded Mode availability around private-bot evidence: `getMe.has_topics_enabled`, incoming `message_thread_id`, and private-thread operation success/failure. A 5-second monitor can hot-upgrade classic mode to Threaded Mode and hot-downgrade back to classic without `/reload`. Impact: Threaded Mode behaves as a progressive enhancement instead of a startup-only assumption.
7
13
  - `[Thread Routing]` Preserved per-instance ownership for queues, active turns, model/tool state, previews, menus, lifecycle hooks, replies, files, voice, buttons, reactions, and command/control replies. Impact: leader and follower threads behave like scoped Telegram workspaces rather than views over one shared queue.
8
14
  - `[Thread Lifecycle]` Added durable current-state thread bindings with explicit owners, stable slots, compact baked thread names, reservations, and proof-before-delete cleanup through `thread-reconciler`. Impact: reloads, reconnects, stale tabs, restores, and cleanup avoid duplicate live threads and avoid deleting uncertain targets.
9
- - `[Leader/Follower Recovery]` Added recovery for reloads, reconnects, follower heartbeat loss, promotion, explicit disconnect, stale thread cleanup, and same-profile follower resume. Impact: Thread Mode degrades forward across local process churn while preserving the intended thread binding where possible.
15
+ - `[Leader/Follower Recovery]` Added recovery for reloads, reconnects, follower heartbeat loss, promotion, explicit disconnect, stale thread cleanup, and same-profile follower resume. Impact: Threaded Mode degrades forward across local process churn while preserving the intended thread binding where possible.
10
16
  - `[Native Activity Status]` Kept Telegram work indication native-only through Bot API `sendChatAction(typing)` / Telegram client `...active` status. Custom progress/activity/thinking message surfaces are not part of the release path. Impact: long-running work uses the Telegram-native affordance instead of adding extra chat noise.
11
17
  - `[Compaction Activity]` Scoped typing to real work: active Telegram turns, tool/reasoning/message activity during those turns, confirmed manual `/compact`, and automatic compaction only when it belongs to an active Telegram turn. Thread-scoped typing preserves the concrete thread target and mirrors to `All` as aggregate activity. Impact: compaction shows native activity when it should, without reviving startup/connect/reload typing.
12
18
  - `[Thread-Scoped Delivery]` Propagated `{ chatId, threadId? }` across inbound routing, replies, previews/drafts, typing, voice, attachments, menus, sections, callbacks, reactions, media groups, slash commands, and follower-routed Bot API calls. Impact: classic DM delivery and thread delivery share one target-scoped transport contract.
13
- - `[Replies And Drafts]` Anchored the first assistant reply block to the prompting message in Thread Mode and kept later chunks sequential; preview rollover preserves the active turn's source reply metadata. Telegram Desktop may still omit the visual reply header despite the same correct payload that mobile renders. Impact: thread conversations keep local reply context without stacking headers on every chunk.
14
- - `[Thread UX]` Improved unbound-thread reroute/restore flows, cleanup of temporary chooser surfaces, connected/offline notices, status/menu role indicators, and routing controls. Impact: operator-facing Thread Mode surfaces are more recognizable and less likely to leave stale controls behind.
15
- - `[Proactive Push]` Allowed Thread Mode followers to send successful local non-Telegram final replies through the leader transport when `proactivePush` is enabled. Impact: follower-owned local results can reach the assigned thread without granting unrelated non-owner processes Telegram delivery.
19
+ - `[Replies And Drafts]` Anchored the first assistant reply block to the prompting message in Threaded Mode and kept later chunks sequential; preview rollover preserves the active turn's source reply metadata. Telegram Desktop may still omit the visual reply header despite the same correct payload that mobile renders. Impact: thread conversations keep local reply context without stacking headers on every chunk.
20
+ - `[Thread UX]` Improved unbound-thread reroute/restore flows, cleanup of temporary chooser surfaces, connected/offline notices, status/menu role indicators, and routing controls. Impact: operator-facing Threaded Mode surfaces are more recognizable and less likely to leave stale controls behind.
21
+ - `[Proactive Push]` Allowed Threaded Mode followers to send successful local non-Telegram final replies through the leader transport when `proactivePush` is enabled. Impact: follower-owned local results can reach the assigned thread without granting unrelated non-owner processes Telegram delivery.
16
22
  - `[Agent Responsiveness]` Moved successful Telegram turn follow-up delivery work into extension-owned background tasks. Impact: Pi can leave the working state sooner after producing the final answer while preserving Telegram delivery ordering.
17
23
  - `[Telegram API Transport]` Added `PI_TELEGRAM_NETWORK_FAMILY=auto|ipv4|ipv6|ipv4-fallback`; the default `ipv4-fallback` retries transport-level dual-stack failures through Node's IPv4-only HTTPS path after native `fetch`. JSON calls, multipart uploads, and file downloads now share one transport helper with richer redacted diagnostics. Impact: hosts with broken IPv6 can keep polling, typing, drafts, downloads, uploads, and direct tools working without global DNS changes.
18
24
  - `[Security]` Hardened local bus authorization with leader-minted secrets, derived private endpoints, allowed-user checks, target-scoped follower Bot API allowlists, and liveness refreshes around forwarded updates/API calls. Impact: followers can use their assigned thread without gaining arbitrary bot control.
19
- - `[Windows IPC]` Added native Windows named-pipe path selection for the local leader/follower bus alongside Unix socket support. Impact: Thread Mode has an intended native Windows transport path, with live Windows smoke still tracked separately in the backlog.
20
- - `[Diagnostics]` Expanded `/telegram-status`, `tmp/telegram/state.json`, and redacted `tmp/telegram/logs.jsonl` around runtime role, roster, capability state, reservations, thread reconciliation, and transport health, without making diagnostics files routing authority. Impact: operators and agents can inspect Thread Mode health without depending on stale history.
21
- - `[Validation]` Split Thread Mode coverage across focused bus, leader, follower, runtime, integration, routing, lifecycle, and delivery tests. Impact: the release behavior is pinned by domain-owned regressions rather than one oversized integration bucket.
22
- - `[Architecture]` Moved Thread Mode logic out of `index.ts` into focused domains for bus protocol, leader runtime, follower runtime, thread bindings, synchronization, polling/capability switching, and reconciliation. The separate public `telegram.json` bus switch was removed from the product model and implementation. Impact: the public surface stays small, classic mode remains the base mode, BotFather Thread Mode is the multi-instance switch, and product language consistently says Thread while Bot API topics remain implementation details.
25
+ - `[Windows IPC]` Added native Windows named-pipe path selection for the local leader/follower bus alongside Unix socket support. Impact: Threaded Mode has an intended native Windows transport path, with live Windows smoke still tracked separately in the backlog.
26
+ - `[Diagnostics]` Expanded `/telegram-status`, `tmp/telegram/state.json`, and redacted `tmp/telegram/logs.jsonl` around runtime role, roster, capability state, reservations, thread reconciliation, and transport health, without making diagnostics files routing authority. Impact: operators and agents can inspect Threaded Mode health without depending on stale history.
27
+ - `[Validation]` Split Threaded Mode coverage across focused bus, leader, follower, runtime, integration, routing, lifecycle, and delivery tests. Impact: the release behavior is pinned by domain-owned regressions rather than one oversized integration bucket.
28
+ - `[Architecture]` Moved Threaded Mode logic out of `index.ts` into focused domains for bus protocol, leader runtime, follower runtime, thread bindings, synchronization, polling/capability switching, and reconciliation. The separate public `telegram.json` bus switch was removed from the product model and implementation. Impact: the public surface stays small, classic mode remains the base mode, Telegram private-chat Threaded Mode is the multi-instance switch, and product language consistently says thread while Bot API topics remain implementation details.
23
29
 
24
30
  ## 0.17.5: Screenshot Refresh
25
31
 
package/README.md CHANGED
@@ -15,7 +15,7 @@ This repository is an actively maintained fork of [`badlogic/pi-telegram`](https
15
15
  - **Mobile supervision**: continue a live Pi session from Telegram without turning Telegram into a fake terminal.
16
16
  - **Telegram-native controls**: menus, settings, queue controls, native active status, Rich Markdown replies, drafts, buttons, voice, files, and artifacts.
17
17
  - **Safe runtime mapping**: Telegram turns map into Pi lifecycle, queueing, model switching, compaction, previews, final replies, and ownership rules.
18
- - **Optional Threaded Mode**: one leader and visible follower Pi processes can share one bot through named Telegram Threads.
18
+ - **Optional Threaded Mode**: one leader and visible follower Pi processes can share one bot through named Telegram threads.
19
19
  - **Extension platform**: companion extensions can add Telegram-native commands, sections, status rows, update handlers, handlers, and voice providers without owning polling.
20
20
 
21
21
  Use this README for the product shape. Follow the docs for exact contracts.
@@ -59,7 +59,7 @@ Paste your bot token when prompted. If a bot token is already saved in `~/.pi/ag
59
59
  /telegram-connect
60
60
  ```
61
61
 
62
- The adapter is session-local: only one Pi instance polls Telegram at a time. In classic mode, `/telegram-connect` records external control/polling ownership in `~/.pi/agent/locks.json`. When BotFather Threaded Mode is available, `/telegram-connect` uses the local Telegram organism automatically: the first live instance becomes leader, later live instances register as followers instead of taking over while the leader heartbeat is healthy. Local queue and reply state stay per Pi instance, so an instance that loses Telegram control still finishes work it already accepted.
62
+ The adapter is session-local: only one Pi instance polls Telegram at a time. In classic mode, `/telegram-connect` records external control/polling ownership in `~/.pi/agent/locks.json`. When Telegram private-chat Threaded Mode is available for the bot, `/telegram-connect` uses the local Telegram organism automatically: the first live instance becomes leader, later live instances register as followers instead of taking over while the leader heartbeat is healthy. Local queue and reply state stay per Pi instance, so an instance that loses Telegram control still finishes work it already accepted.
63
63
 
64
64
  ### 4. Pair your Telegram account
65
65
 
@@ -130,11 +130,11 @@ Send files or images directly to the bot. Inbound downloads are saved under `<ag
130
130
 
131
131
  If you ask Pi for a generated file, Pi can call `telegram_attach`: during a Telegram-originated turn the adapter sends it with the next Telegram reply, and during local/TUI work it sends directly to the paired/default chat, a registered follower's assigned thread, or explicit `chat_id` plus optional `thread_id`. Local work can also use `telegram_message` when you explicitly ask the agent to push a Markdown text message to Telegram; embedded `telegram_button` comments are parsed and attached to that message. Direct local/TUI delivery requires the current Pi instance to own `/telegram-connect`, or to be registered with an explicitly enabled multi-instance bus so it can route through the leader; if neither is true, take over or enable/register with the bus before sending. Outbound attachments default to a 50 MiB limit. Environment variables for both limits are listed in [Environment-only configuration](#environment-only-configuration).
132
132
 
133
- ### BotFather Threads and multi-instance bus
133
+ ### Telegram Threaded Mode and multi-instance bus
134
134
 
135
- BotFather Threaded Mode is the switch. Classic single-DM polling is the base mode. When Telegram reports private-chat Threads are available, the adapter enables the local leader/follower bus automatically; when Threads are unavailable or later disabled, it uses classic single-DM polling as the ordinary private-bot mode.
135
+ Telegram private-chat Threaded Mode is the switch. Classic single-DM polling is the base mode. When Telegram reports private-chat threads are available for the bot, the adapter enables the local leader/follower bus automatically; when threads are unavailable or later disabled, it uses classic single-DM polling as the ordinary private-bot mode.
136
136
 
137
- Only the leader calls `getUpdates`; followers authenticate to the local bus and route allowlisted, target-scoped Telegram work through the leader. When BotFather Threads are available, they become the UI targets:
137
+ Only the leader calls `getUpdates`; followers authenticate to the local bus and route allowlisted, target-scoped Telegram work through the leader. When private-chat threads are available, they become the UI targets:
138
138
 
139
139
  - The leader owns one thread;
140
140
  - Each explicitly connected follower gets one visible thread;
@@ -143,7 +143,7 @@ Only the leader calls `getUpdates`; followers authenticate to the local bus and
143
143
  - Unknown owner-created threads preserve the original prompt and offer a target-thread chooser instead of spawning work invisibly;
144
144
  - Stale follower tabs receive compact lifecycle notices before cleanup when the leader can prove ownership.
145
145
 
146
- Threaded input is still authorized by `allowedUserId`. There is no separate public `telegram.json` switch for the bus: Telegram capability detection is the runtime source of truth. Native Windows Threaded Mode smoke remains tracked in `BACKLOG.md`; the intended transport is the same local bus over Windows named pipes instead of Unix sockets.
146
+ Thread input is still authorized by `allowedUserId`. There is no separate public `telegram.json` switch for the bus: Telegram capability detection is the runtime source of truth. Native Windows Threaded Mode smoke remains tracked in `BACKLOG.md`; the intended transport is the same local bus over Windows named pipes instead of Unix sockets.
147
147
 
148
148
  ## Core features
149
149
 
@@ -118,9 +118,9 @@ Deleting `locks.json` resets runtime ownership without deleting Telegram configu
118
118
 
119
119
  ### Threaded Mode Multi-Instance Bus
120
120
 
121
- BotFather 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, 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.
121
+ 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.
122
122
 
123
- 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: it preserves the follower thread binding 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. `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. 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 BotFather 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.
123
+ 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: it preserves the follower thread binding 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. `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. 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.
124
124
 
125
125
  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.
126
126
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Status
4
4
 
5
- Implemented behind BotFather private-chat Threaded Mode capability. Classic/private-chat mode remains the default whenever Telegram Threads are unavailable, and live Telegram client smoke remains the release gate for client-visible thread UX.
5
+ Implemented for paired bots where Telegram exposes private-chat Threaded Mode. Classic/private-chat mode remains the default whenever Telegram threads are unavailable, and live Telegram client smoke remains the release gate for client-visible thread UX.
6
6
 
7
7
  This document uses **thread** as the canonical product term because Telegram clients present the tabbed UI as threads. The Bot API calls the underlying primitive a `Topic` / `ForumTopic`, but project language follows user-perceived client reality rather than API naming. Use **topic** only when discussing Bot API method names, service-message names, or transport-level evidence. User/operator UX and product docs should say thread.
8
8
 
@@ -37,7 +37,7 @@ A practical Telegram UI can then use threads:
37
37
  one private bot chat -> one thread per live Pi instance
38
38
  ```
39
39
 
40
- The operator experience should feel like:
40
+ The operator experience:
41
41
 
42
42
  1. Start one Pi instance; it becomes the Telegram bus leader and polls Telegram.
43
43
  2. Start another Pi instance with `pi-telegram` and run `/telegram-connect`; the follower registers instead of fighting for `getUpdates`.
@@ -57,9 +57,9 @@ The operator experience should feel like:
57
57
  ## Terms
58
58
 
59
59
  - `Telegram bus`: The singleton local capability to poll Telegram updates and send Telegram API calls for one bot token.
60
- - `Leader`: The live Pi instance that currently owns the Telegram bus and calls `getUpdates`; this is an ephemeral role that should be transferable after stale heartbeat detection.
60
+ - `Leader`: The live Pi instance that currently owns the Telegram bus and calls `getUpdates`; this is an ephemeral role transferable after stale heartbeat detection.
61
61
  - `Follower`: A live Pi instance that wants Telegram presence but routes Telegram API access through the leader.
62
- - `Bus lifecycle`: Transient recovery state only. Stable identity is the bus role (`leader` / `follower`); lifecycle should surface exceptional handoff states such as `electing`, not duplicate roles with labels like `leader-active`.
62
+ - `Bus lifecycle`: Transient recovery state only. Stable identity is the bus role (`leader` / `follower`); lifecycle surfaces exceptional handoff states such as `electing`, not duplicate roles with labels like `leader-active`.
63
63
  - `Agent instance`: A running Pi process/session with its own extension state, queue, active turn, model, tools, and lifecycle hooks.
64
64
  - `Telegram target`: The concrete Telegram destination for an instance, represented as `{ chatId, threadId? }`.
65
65
  - `Thread target`: A Telegram UI thread destination, represented as `{ chatId, threadId: message_thread_id }` over Bot API topic transport.
@@ -67,15 +67,15 @@ The operator experience should feel like:
67
67
 
68
68
  ## Core Shift
69
69
 
70
- The lock should evolve from "this instance is the only usable Telegram extension" to "this instance is the current Telegram bus leader".
70
+ In Threaded Mode, the lock means "this instance is the current Telegram bus leader" rather than "this instance is the only usable Telegram extension".
71
71
 
72
- Current meaning:
72
+ Classic lock meaning:
73
73
 
74
74
  ```text
75
75
  locks.json / @llblab/pi-telegram -> polling/control owner
76
76
  ```
77
77
 
78
- Proposed meaning:
78
+ Threaded Mode meaning:
79
79
 
80
80
  ```text
81
81
  locks.json / @llblab/pi-telegram -> bus leader identity + heartbeat
@@ -85,7 +85,7 @@ Followers do not poll. They register with the leader and receive routed inbound
85
85
 
86
86
  ## Target Abstraction
87
87
 
88
- Introduce a first-class target abstraction:
88
+ The bridge uses a first-class target abstraction:
89
89
 
90
90
  ```ts
91
91
  type TelegramTarget = {
@@ -96,9 +96,9 @@ type TelegramTarget = {
96
96
 
97
97
  Private-chat mode uses `{ chatId: allowedUserId }`.
98
98
 
99
- Thread instance mode uses `{ chatId: topicChatId, threadId: messageThreadId }` over Bot API topic transport.
99
+ Threaded Mode uses `{ chatId: privateChatId, threadId: messageThreadId }` over Bot API topic transport.
100
100
 
101
- Every session/instance-scoped path should eventually carry a target:
101
+ Every session/instance-scoped path carries or preserves a target:
102
102
 
103
103
  - Inbound update routing.
104
104
  - Queue item identity.
@@ -112,56 +112,20 @@ Every session/instance-scoped path should eventually carry a target:
112
112
  - Typing/record-voice chat actions.
113
113
  - Direct local/TUI Telegram delivery.
114
114
 
115
- ## Instance Binding Vs Session Binding
115
+ ## Binding Model
116
116
 
117
- The user's proposed instance binding is stronger than the original session binding.
118
-
119
- ### Session binding
120
-
121
- A thread maps to a Pi session id/session file/cwd.
122
-
123
- Pros:
124
-
125
- - Durable history can survive process restarts.
126
- - A session transcript can be reopened if Pi exposes a stable session identity.
127
- - Instance-thread identity is conceptually tied to work history.
128
-
129
- Cons:
130
-
131
- - A thread may point at a dead session with no live agent to answer.
132
- - `/new`, compaction, session replacement, and session file behavior depend on Pi internals.
133
- - Multi-instance liveness still needs a separate routing layer.
134
-
135
- ### Instance binding
136
-
137
- A thread maps to a currently running Pi instance.
138
-
139
- Pros:
140
-
141
- - Thread liveness is honest: if the instance is registered, there is a live owner.
142
- - Routing can use process identity, heartbeat, cwd, model, and current status directly.
143
- - Leader election and target registration naturally operate over live instances.
144
-
145
- Cons:
146
-
147
- - Threads become ephemeral unless the instance identity has a durable resume key.
148
- - Closing a Pi instance can leave an orphan thread/history unless cleanup/status rules are clear.
149
- - Restarting the same project may create a new thread unless reuse is based on cwd/profile/name.
150
-
151
- ### Recommended stance
152
-
153
- Use instance binding as the runtime truth, with an optional durable `instanceProfileKey` for thread reuse.
117
+ A thread maps to a currently running Pi instance, not to a historical session file. `instanceId` is the live routing owner, while `instanceProfileKey` is a reuse hint for reclaiming a compatible current thread across process replacement.
154
118
 
155
119
  ```text
156
120
  runtime owner: live instance id
157
- reuse key: cwd/profile/user-chosen alias/session id when available
121
+ reuse hint: cwd/profile/user-chosen alias/session id when available
158
122
  ```
159
123
 
160
- This avoids dead-thread routing while still allowing a restarted project to reclaim a previous thread when the operator wants stable workspace history.
124
+ This keeps thread liveness honest: if an instance is registered, there is a live owner to answer. It also avoids coupling `/new`, compaction, and session-file internals to Telegram routing. Restarted projects may still reclaim previous current bindings through profile-aware reuse when that does not conflict with live ownership.
161
125
 
162
126
  ## Instance Identity
163
127
 
164
- A registered instance should expose:
128
+ A registered instance exposes:
165
129
 
166
130
  ```json
167
131
  {
@@ -171,7 +135,7 @@ A registered instance should expose:
171
135
  "startedAt": "2026-05-20T10:00:00.000Z",
172
136
  "owner": { "kind": "leader", "cwd": "/home/user/project" },
173
137
  "threadName": "<valid-instance-identity>",
174
- "target": { "chatId": -1001234567890, "threadId": 42 },
138
+ "target": { "chatId": 123456789, "threadId": 42 },
175
139
  "status": "idle|active|queued|compacting|disconnected",
176
140
  "lastHeartbeatAt": "2026-05-20T10:00:05.000Z"
177
141
  }
@@ -181,28 +145,21 @@ A registered instance should expose:
181
145
 
182
146
  ## Leader Election
183
147
 
184
- Minimum viable election:
148
+ Leader election is heartbeat-gated and lock-backed:
185
149
 
186
150
  1. On startup, read the Telegram lock.
187
151
  2. If no leader exists, acquire leadership and start polling.
188
152
  3. If a live leader exists, register as follower.
189
- 4. If the leader heartbeat is stale, attempt an atomic leadership takeover; do not treat ordinary `/telegram-connect` on a follower as a leadership move while the leader is live.
190
- 5. If several followers detect stale leadership, use deterministic tie-break or atomic lock write so only one wins.
191
-
192
- Possible tie-breakers:
153
+ 4. If the leader heartbeat is stale, attempt an atomic leadership takeover; ordinary `/telegram-connect` on a follower is not a leadership move while the leader is live.
154
+ 5. If several followers detect stale leadership, atomic compare/write lock acquisition ensures only one becomes leader.
193
155
 
194
- - Oldest live follower wins: stable and predictable.
195
- - Lowest pid wins: simple on one host, weak across machines.
196
- - Random backoff before takeover: reduces stampede, less deterministic.
197
- - Highest priority role wins: future config-driven choice.
198
-
199
- Recommended first pass: stale heartbeat + random jitter + atomic compare/write lock. Later, add a deterministic priority if needed.
156
+ Followers first try to re-register after leader reload or unknown-heartbeat responses, then promote only after the grace window expires. This preserves thread bindings through transient reload gaps without allowing competing pollers.
200
157
 
201
158
  ## Leader/Follower Communication
202
159
 
203
- Open implementation choices:
160
+ Implemented transport:
204
161
 
205
- ### Option A: Local IPC endpoint under agent temp dir
162
+ ### Local IPC endpoint under agent temp dir
206
163
 
207
164
  Leader opens a local Node `net` endpoint: a Unix-domain socket under the agent temp directory on Unix-like platforms, or a deterministic Windows named pipe (`\\.\pipe\pi-telegram-...`) on native Windows. Followers register, heartbeat, and exchange routed events. Follower registration uses a longer registration-specific response timeout than ordinary heartbeat/forwarding calls because the leader may need to provision a Telegram thread before it can return the assigned target; timing out that handshake leaves a visible tab with no follower heartbeat. Keep this handshake to the true critical path: create/reuse the target, persist the live binding, and return it. Connected notices and replaced-thread reconciliation cleanup are non-critical and should run after registration so a follower becomes routable before Telegram client/server UI convergence work finishes.
208
165
 
@@ -217,35 +174,7 @@ Cons:
217
174
  - Adds IPC lifecycle and security concerns.
218
175
  - Cross-machine workers need tunneling or a different transport.
219
176
 
220
- ### Option B: File-backed mailbox plus wakeups
221
-
222
- Followers write registrations and outbound requests to files; leader scans/watches.
223
-
224
- Pros:
225
-
226
- - Simple local persistence and debugging.
227
- - No socket protocol initially.
228
-
229
- Cons:
230
-
231
- - Harder to do low-latency streaming previews and backpressure.
232
- - File locking and cleanup become subtle.
233
-
234
- ### Option C: External daemon
235
-
236
- A dedicated Telegram bus daemon owns polling and all Pi instances connect to it.
237
-
238
- Pros:
239
-
240
- - Cleanest conceptual bus owner.
241
- - Best long-term fit for multi-host or always-on operation.
242
-
243
- Cons:
244
-
245
- - Bigger installation/product boundary than an extension.
246
- - More operational burden.
247
-
248
- Recommended path: keep local IPC as the default internal bus, while keeping the public design compatible with a future daemon if deployment needs outgrow one host.
177
+ Alternative transports such as file-backed mailboxes or an external daemon remain out of the current product boundary. Local IPC is the default internal bus while the public design stays compatible with a future daemon if deployment needs outgrow one host.
249
178
 
250
179
  ## Native Windows Smoke Plan
251
180
 
@@ -253,7 +182,7 @@ Native Windows support should not require WSL. The baseline transport uses Windo
253
182
 
254
183
  Manual smoke checklist:
255
184
 
256
- 1. Enable BotFather Threaded Mode for the paired bot.
185
+ 1. Enable Telegram private-chat Threaded Mode for the paired bot.
257
186
  2. Start Pi in one Windows terminal and run `/telegram-connect`; verify it becomes the leader and gets a named Telegram thread.
258
187
  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, and terminal status shows `<ThreadName> Follower`.
259
188
  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.
@@ -278,24 +207,24 @@ Remaining risk is live native Windows behavior: named-pipe creation/connect timi
278
207
 
279
208
  ## Telegram Thread UX
280
209
 
281
- In BotFather private-chat Threaded Mode:
210
+ In Telegram private-chat Threaded Mode:
282
211
 
283
212
  - The private bot chat is a tabbed instance workspace, not a classic `General + threads` forum.
284
213
  - `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.
285
- - The leader should proactively create or reclaim its own thread on startup/activation when Threaded Mode is available, so the visible leader has the same two-way binding as followers.
214
+ - 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.
286
215
  - **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 the prompt in that Telegram thread and shows a target-thread chooser; explicit routing may later close/delete only extra confirmed source threads through `thread-reconciler`.
287
216
  - 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`. Manual follower registration creates a fresh visible thread unless the same explicit binding identity already has a live binding; old offline/failed records may point at closed/deleted Telegram tabs and are not silently claimed.
288
217
  - 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.
289
- - Bidirectional binding is a core UX requirement, not an implementation detail: Pi instances should actively advertise/remember their thread identity, while the bot should observe Telegram-client thread state and reflect 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.
218
+ - 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.
290
219
 
291
- In BotFather private-chat Threaded Mode:
220
+ In Telegram private-chat Threaded Mode:
292
221
 
293
222
  - The private bot DM becomes the operator's multi-instance dashboard.
294
223
  - Each live bound instance gets one visible thread.
295
224
  - Each instance has a durable single-letter slot (`A`-`Z`) assigned by the extension and a bridge-authored `threadName`.
296
225
  - New slots advance monotonically through the alphabet and wrap after `Z` only to a free slot; closed earlier slots are not backfilled out of order. This preserves sequence feel and intentionally caps concurrent visible instances to the alphabet without duplicating occupied letters. The compact `bot.lastSlot` cursor persists across reloads and live-test history, so after `Z` the next truly new thread can be `A` again when `A` is currently free.
297
226
  - A follower that later becomes leader keeps its existing slot and thread name; leadership changes are transport role changes, not identity resets.
298
- - Instance-thread names should be short and recognizable. Default provisioning chooses one baked 4-6 letter single-word Latin thread name from the assigned slot's five-name palette using provisioning timestamp entropy and creates the Telegram thread with that title immediately. The slot remains internal ordering metadata and is not redundantly included in the thread name. Bare slot titles are fallback/legacy state only; do not prompt agents to self-name and do not expose a rename tool. Existing human-named threads are preserved across reloads and leadership changes when they remain the current live binding. If reload creates a new runtime instance while the previous leader thread is still alive, the new leader should take the next free slot instead of reusing the old slot immediately.
227
+ - Instance-thread names are short and recognizable. Default provisioning chooses one baked 4-6 letter single-word Latin thread name from the assigned slot's five-name palette using provisioning timestamp entropy and creates the Telegram thread with that title immediately. The slot remains internal ordering metadata and is not redundantly included in the thread name. Bare slot titles are fallback/legacy state only; do not prompt agents to self-name and do not expose a rename tool. Existing human-named threads are preserved across reloads and leadership changes when they remain the current live binding. If reload creates a new runtime instance while the previous leader thread is still alive, the new leader should take the next free slot instead of reusing the old slot immediately.
299
228
  - A thread-local `/start` opens that instance's menu.
300
229
  - Prompts typed in a thread route to the owning instance.
301
230
  - Replies, previews, files, voice, and buttons stay in that thread.
@@ -304,18 +233,18 @@ In BotFather private-chat Threaded Mode:
304
233
  - If the instance disconnects, the leader can post/update a compact status: `Instance offline`.
305
234
  - If the same live binding identity returns, it can reclaim the thread and post a compact reconnect status.
306
235
 
307
- ## Bot API Evidence For BotFather Threaded Mode
236
+ ## Bot API Evidence For Private-Chat Threaded Mode
308
237
 
309
238
  The local Bot API reference in [`../.agents/skills/telegram-bot/api.md`](../.agents/skills/telegram-bot/api.md) supports private bot Threaded Mode through bot capability fields and thread-target transport:
310
239
 
311
- - `User` returned by `getMe` can include `has_topics_enabled` and `allows_users_to_create_topics`; these are the BotFather private-chat Threaded Mode capability fields and are the startup/runtime probe source for this extension.
240
+ - `User` returned by `getMe` can include `has_topics_enabled` and `allows_users_to_create_topics`; these are the private-chat Threaded Mode capability fields and are the startup/runtime probe source for this extension.
312
241
  - `createForumTopic` works in a private chat with a user and returns a `ForumTopic`, so the returned `message_thread_id` is persistable as an instance thread target.
313
- - Private-thread management uses Bot API methods such as `editForumTopic`, `closeForumTopic`, `reopenForumTopic`, `deleteForumTopic`, and related unpin methods. Thread-unavailable errors from these methods are degradation evidence when BotFather Threaded Mode is disabled.
242
+ - Private-thread management uses Bot API methods such as `editForumTopic`, `closeForumTopic`, `reopenForumTopic`, `deleteForumTopic`, and related unpin methods. Thread-unavailable errors from these methods are degradation evidence when Threaded Mode is disabled or unavailable for the bot.
314
243
  - `Message` exposes `message_thread_id` and `is_topic_message`; an incoming private-chat message with `message_thread_id` is a live Threaded Mode observation and can trigger progressive upgrade.
315
244
  - Topic lifecycle service messages include `forum_topic_created`, `forum_topic_edited`, `forum_topic_closed`, `forum_topic_reopened`, `general_forum_topic_hidden`, and `general_forum_topic_unhidden`.
316
245
  - `message_thread_id` is supported by the send/upload methods the bridge uses or may need: `sendMessage`, `sendPhoto`, `sendDocument`, `sendVoice`, `sendMediaGroup`, `sendSticker`, `sendRichMessage`, `sendMessageDraft`, `sendRichMessageDraft`, and `sendChatAction`.
317
246
 
318
- Non-goal: group detection is not the control-plane model for this extension. BotFather Threaded Mode lives in the private bot chat, so startup and runtime switching must not depend on group chat metadata or group admin capability fields.
247
+ Non-goal: group detection is not the control-plane model for this extension. Threaded Mode lives in the private bot chat, so startup and runtime switching must not depend on group chat metadata or group admin capability fields.
319
248
 
320
249
  Remaining live-verification points:
321
250
 
@@ -323,7 +252,7 @@ Remaining live-verification points:
323
252
  - Whether message-reaction updates carry thread identity in the current Bot API shape. The reference exposes chat id and message id for reactions, so routing may need stored message ownership.
324
253
  - Whether every rich draft/final/upload/chat-action path behaves identically in Telegram clients when `message_thread_id` is supplied.
325
254
 
326
- Implementation should start with target plumbing and fixtures, then run a Telegram smoke test before marking Threaded Mode stable.
255
+ Implemented behavior stays evidence-gated: when Telegram client or Bot API behavior differs from the contract above, capture a minimized fixture or documented client caveat before changing routing.
327
256
 
328
257
  ## Inbound Routing
329
258
 
@@ -340,11 +269,11 @@ Then it dispatches:
340
269
  - If target is unknown but authorized and setup allows provisioning, offer or create a binding.
341
270
  - If target is unknown or unauthorized, ignore or send a safe denial.
342
271
 
343
- Follower instances should receive normalized events, not raw Telegram transport internals where possible. The follower should still run the same queue/routing logic, but Telegram API calls go back through the leader transport port.
272
+ Follower instances receive normalized events, not raw Telegram transport internals where possible. The follower still runs the same queue/routing logic, but Telegram API calls go back through the leader transport port.
344
273
 
345
274
  ## Outbound Routing
346
275
 
347
- Followers should not call Telegram Bot API directly for routed Telegram work. Instead, they call a leader-owned transport port:
276
+ Followers do not call Telegram Bot API directly for routed Telegram work. Instead, they call a leader-owned transport port:
348
277
 
349
278
  ```text
350
279
  follower reply/preview/upload/chat-action/download/callback-answer -> leader IPC -> Telegram API
@@ -356,7 +285,7 @@ Every outbound request carries its target. The leader injects `message_thread_id
356
285
 
357
286
  ## Queue And State Scoping
358
287
 
359
- Each instance owns its own queue and active turn state. The leader should not become a central queue scheduler for all agents unless a future daemon mode deliberately chooses that architecture.
288
+ Each instance owns its own queue and active turn state. The leader does not become a central queue scheduler for all agents; that would be a separate daemon-mode architecture.
360
289
 
361
290
  Target-scoped state requirements:
362
291
 
@@ -369,7 +298,7 @@ Target-scoped state requirements:
369
298
 
370
299
  ## Configuration
371
300
 
372
- There is no public `telegram.json` switch for the bus. BotFather private-chat Threaded Mode is the capability switch: when Telegram exposes Threads, the bridge enables the local bus; when Telegram runs as an ordinary private DM, the bridge uses classic private-chat flow as the base mode.
301
+ There is no public `telegram.json` switch for the bus. Telegram private-chat Threaded Mode is the runtime switch: when Telegram exposes threads for the bot, the bridge enables the local bus; when Telegram runs as an ordinary private DM, the bridge uses classic private-chat flow as the base mode.
373
302
 
374
303
  Typical config remains just bot identity and authorization:
375
304
 
@@ -382,8 +311,8 @@ Typical config remains just bot identity and authorization:
382
311
 
383
312
  Rules:
384
313
 
385
- - Classic mode is selected by Telegram capability: when private-chat Threads are unavailable or disabled, the polling owner uses ordinary single-DM behavior and blocked instances do not register as followers.
386
- - BotFather Threaded Mode enables local leader/follower behavior automatically. The leader owns `getUpdates`; registered followers route Telegram API work through the leader. `/telegram-connect` registers as follower when a live leader exists and does not offer manual takeover in that state. The TUI status bar reports `telegram leader` or `telegram follower` so transport role is visible without opening diagnostics.
314
+ - Classic mode is selected by Telegram capability: when private-chat threads are unavailable or disabled, the polling owner uses ordinary single-DM behavior and blocked instances do not register as followers.
315
+ - Telegram private-chat Threaded Mode enables local leader/follower behavior automatically. The leader owns `getUpdates`; registered followers route Telegram API work through the leader. `/telegram-connect` registers as follower when a live leader exists and does not offer manual takeover in that state. The TUI status bar reports `telegram leader` or `telegram follower` so transport role is visible without opening diagnostics.
387
316
  - The thread chat is the owner's private bot DM (`allowedUserId`); no `topics.chatId` config is needed. Thread names are assigned by the bridge from a baked compact per-slot palette. There is no agent-facing `telegram_rename_thread` tool and no separate user-facing slash command for manual thread renames.
388
317
  - Thread reuse is extension-owned through current live binding identity; there is no separate `topics` config surface in the active private-chat thread model. Manual followers use instance-scoped internal keys by default so multiple terminal processes in the same cwd can receive separate threads.
389
318
  - Thread cleanup remains conservative and centralized: destructive close/delete actions are planned and applied through `thread-reconciler` with proof-before-delete checks, leader-epoch fencing, and retry-preserving failure semantics.
@@ -394,12 +323,12 @@ Rules:
394
323
  Current state under the agent dir:
395
324
 
396
325
  - `locks.json`: current bus leader identity, capability secret, heartbeat, and cleanup fencing epoch. The local bus endpoint is derived from the agent directory by default; legacy `busSocketPath` entries are tolerated but are not required.
397
- - `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. It should mirror `/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.
326
+ - `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. 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.
398
327
  - Local bus endpoints: Unix-like platforms use `tmp/telegram/bus.sock` and `tmp/telegram/followers/*`; native Windows uses deterministic named pipes under `\\.\pipe\pi-telegram-...`. These are transient IPC endpoints, not durable routing state.
399
328
 
400
329
  The bridge must not keep a durable `telegram-targets.json` history. Stale/offline/failed thread entries are reconciliation observations, not reusable source-of-truth state; persisting them increases collision risk. `sync` is event-driven assumption reconciliation, not full Telegram bot-state mirroring, because Bot API does not expose a complete topic/thread listing surface. `state.json` therefore exists for extension+bot observability, diagnostics, startup hints, and explaining reconciliation decisions; live bus/runtime state remains authoritative for routing and provisioning. Non-current routeable thread bindings are pruned during load/persist; old session records must not be retained just to compute the next slot because `bot.lastSlot` is the only durable cursor. Previous-process leader bindings are treated as occupied TTL-bounded reservations until Telegram confirms deletion: reload/startup may close/delete/probe the old thread, known reservations are retried proactively on leader startup, and if Telegram still accepts the old thread id, the new leader should provision the next free slot (`B`, `C`, …) rather than creating a duplicate same-letter tab or blocking startup on Telegram UI convergence. Routing must use live current threads/follower registry, never reservations. The bus leader provisions its own thread during bus startup/connect and provisions follower threads on `follower.register`; registered followers also live in the leader's in-memory registry and communicate over the local bus socket. The live follower registry can resolve a follower by exact `{ chatId, threadId? }`; the leader uses that target ownership to forward message and edited-message updates to followers, and the follower receiver accepts those updates in addition to callbacks and reactions. Media album grouping and split-text coalescing keys include the thread target, queue reaction mutations can scope by chat/thread to avoid cross-target message-id collisions, active-turn target is exposed for lifecycle cleanup and local direct-tool defaults, transport reply dedup is chat/thread-scoped, stored menu state is keyed by chat/message so callback state lookup cannot collide across chats, and generated button turns plus section prompt/open actions preserve the callback thread target. `telegram_message` and immediate `telegram_attach` delivery can also carry an explicit `thread_id` with `chat_id`; when a follower is registered, their default direct-tool target is the assigned thread target and the bus-aware API runtime routes the send through the leader instead of calling Bot API transport locally.
401
330
 
402
- All files containing routing, chat ids, thread ids, or process details should use private permissions and should represent current state rather than historical target caches.
331
+ All files containing routing, chat ids, thread ids, or process details use private permissions and represent current state rather than historical target caches.
403
332
 
404
333
  ## Failure Modes
405
334
 
@@ -440,12 +369,12 @@ All files containing routing, chat ids, thread ids, or process details should us
440
369
 
441
370
  ## Security Boundaries
442
371
 
443
- - Messages, edits, callbacks, and reactions must check user authorization, not only chat/thread membership.
372
+ - Messages, edits, callbacks, and reactions check user authorization, not only chat/thread membership.
444
373
  - Followers authenticate to the local leader IPC with a leader-minted capability secret carried in the active lock entry; registration, heartbeat, forwarded updates, and follower API calls without the secret are rejected. Registration rejections are surfaced verbatim in the follower `/telegram-connect` result, registration waits through leader-side Telegram thread provisioning, and successful registrations send an immediate heartbeat before the interval ticker so the leader does not prune a live follower before its first scheduled heartbeat. The local bus socket is also created under a private `0700` directory with `0600` socket permissions as a first local-only boundary.
445
374
  - Follower Bot API proxying is allowlisted and target-scoped where applicable so a follower can reply in its assigned thread without gaining arbitrary bot control.
446
- - Button and section callbacks must verify authorized `from.id` and owning target/instance.
447
- - Generated artifacts must not leak to the wrong thread after leader failover.
448
- - Diagnostics should redact bot tokens, large prompts, attachment paths, and handler output.
375
+ - Button and section callbacks verify authorized `from.id` and owning target/instance.
376
+ - Generated artifacts stay scoped to the owning thread after leader failover.
377
+ - Diagnostics redact bot tokens, large prompts, attachment paths, and handler output.
449
378
 
450
379
  ## Acceptance Criteria
451
380
 
@@ -460,7 +389,8 @@ All files containing routing, chat ids, thread ids, or process details should us
460
389
  - [x] Leader failover promotes one remaining follower without creating competing pollers.
461
390
  - [x] Queue, active turn, preview, reply deduplication, menu, section, button, reaction, and attachment state are scoped by instance/target. Queue reaction mutations and transport reply dedup are chat/thread-scoped; active-turn target is available to lifecycle cleanup; stored menu state is chat/message-keyed; generated button turns and section prompt/open actions preserve callback targets; preview and attachment delivery already carry targets.
462
391
  - [x] Authorization prevents arbitrary Telegram users or local processes from controlling agents or receiving artifacts.
463
- - [ ] Live smoke still needs operator/client confirmation for restore chooser ordering, leader/follower restore, native active-status scoping, follower attachments/buttons, and close/reopen thread lifecycle. Deterministic docs and tests already cover classic compatibility, single leader/follower registration, target routing, stale leader takeover, follower exit, and wrong-target denial.
392
+
393
+ Live client and native Windows evidence gates are tracked in `BACKLOG.md`; this architecture document records the implemented contract, not the active smoke queue.
464
394
 
465
395
  ## Implemented Shape
466
396
 
@@ -475,9 +405,6 @@ All files containing routing, chat ids, thread ids, or process details should us
475
405
  - Thread cleanup is centralized in `thread-reconciler`, fenced by leader epoch, and requires confirmed delete/stale evidence before state is marked deleted.
476
406
  - Stable docs/UI now describe classic mode, opt-in Threaded Mode, manual follower registration, status/diagnostics, unbound-thread reroute/restore UX, and operator recovery boundaries.
477
407
 
478
- ## Remaining Live Questions
408
+ ## Evidence Gates
479
409
 
480
- - Do Telegram clients consistently render restore chooser ordering, leader/follower restore, native active status, follower attachments/buttons, and close/reopen lifecycle after reload?
481
- - Which Telegram client quirks besides the known Desktop private-thread reply-header issue need documented exceptions?
482
- - Should a future daemon/companion own leadership and fanout for multi-host deployments, or is local IPC sufficient for the supported product shape?
483
- - Should offline instance threads eventually get a user-visible archived/offline status surface, or should current conservative cleanup/reclaim rules stay minimal?
410
+ Open live/client questions belong in `BACKLOG.md` until confirmed. Capture confirmed quirks as focused regressions or documented caveats, not broad speculative matrices.
package/lib/prompts.ts CHANGED
@@ -48,7 +48,7 @@ Local/TUI direct delivery:
48
48
  - Do not use \`telegram_message\` for ordinary Telegram-originated replies; answer normally and let the bridge deliver the active turn reply.
49
49
 
50
50
  Threaded Mode:
51
- - pi-telegram supports private-chat Threaded Mode when BotFather Topics/Threaded Mode are enabled.
51
+ - pi-telegram supports private-chat Threaded Mode when Telegram exposes thread support for the bot.
52
52
  - Product/user language is Thread; Bot API primitive names may still say topic.
53
53
  - Threaded Mode has one leader transport and visible follower Pi processes joined manually through \`/telegram-connect\`.
54
54
  - Thread names are bridge-assigned or preserved identities; do not invent rename prompts or use a rename tool.
package/lib/setup.ts CHANGED
@@ -135,7 +135,14 @@ export async function runTelegramSetup(
135
135
  ...deps.config,
136
136
  botToken: token.trim(),
137
137
  };
138
- const data = await deps.getMe(nextConfig.botToken ?? "");
138
+ let data: Awaited<ReturnType<TelegramSetupDeps["getMe"]>>;
139
+ try {
140
+ data = await deps.getMe(nextConfig.botToken ?? "");
141
+ } catch (error) {
142
+ const message = error instanceof Error ? error.message : String(error);
143
+ deps.notify(`Telegram API check failed: ${message}`, "error");
144
+ return undefined;
145
+ }
139
146
  if (!data.ok || !data.result) {
140
147
  deps.notify(data.description || "Invalid Telegram bot token", "error");
141
148
  return undefined;
@@ -928,7 +928,10 @@ export async function fetchTelegramBotIdentity(
928
928
  botToken: string,
929
929
  fetchImpl: typeof fetch = fetch,
930
930
  ): Promise<TelegramBotIdentityResponse> {
931
- const response = await fetchImpl(`${TELEGRAM_API_BASE}/bot${botToken}/getMe`);
931
+ const url = `${TELEGRAM_API_BASE}/bot${botToken}/getMe`;
932
+ const response = await callTelegramTransportRequest((family) =>
933
+ fetchImpl === fetch ? telegramFetch(url, {}, family) : fetchImpl(url),
934
+ );
932
935
  return response.json() as Promise<TelegramBotIdentityResponse>;
933
936
  }
934
937
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.18.0",
3
+ "version": "0.18.1",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"