@llblab/pi-telegram 0.20.0 → 0.20.2

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
@@ -79,7 +79,7 @@ The core product loop is mobile continuation: start or supervise work in the ter
79
79
  - 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.
80
80
  - 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.
81
81
  - 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. 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.
82
- - 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
+ - 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 may survive only through a bounded leader-reload re-registration grace window, after which records without a live owner leave current `state.json` without implying destructive Telegram thread deletion. When post-grace follower compaction removes the binding represented by `bot.lastSlot`, realign the cursor to the newest-created remaining live binding in the same reconciliation pass; preserve a cursor that still belongs to a live binding and continue skipping unexpired pending/reserved slots. Do not reinterpret an otherwise orphaned cursor outside compaction because normal thread deletion may intentionally preserve ring sequence. The alphabet cap is a feature — compact single-letter tabs are scannable.
83
83
  - 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.
84
84
  - 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`.
85
85
  - 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.
@@ -126,6 +126,7 @@ The core product loop is mobile continuation: start or supervise work in the ter
126
126
 
127
127
  - Keep globally reusable rules as independent bullets in the most general applicable section of `AGENTS.md`; do not hide a cross-cutting rule inside one local feature paragraph just because that feature revealed it.
128
128
  - Local/domain bullets should state how a general rule applies there, or record domain-specific exceptions; they should not be the only source for a rule that applies to multiple tools, transports, domains, or docs surfaces.
129
+ - Markdown tables use compact source formatting: separator cells are exactly `---`, and headers/body cells have one surrounding space without alignment padding. Keep vendored references unchanged.
129
130
  - Pi-facing tool results shown in compact tool rows should start with exactly one leading newline (`\nResult`) so the result is visually separated from the tool-call header by one blank line without excessive vertical spacing.
130
131
  - For non-trivial implementation, release, or architecture-audit work, run an `AGENTS.md` compliance pass: reread the durable principles and relevant project docs, map the changed behavior to the rules it touches, validate code/tests/docs against those rules, and explicitly surface any rule conflict, obsolete rule, or evidence-backed improvement. When the rules themselves drift from the product reality, update `AGENTS.md` or document a deliberate exception in the same pass instead of silently working around it.
131
132
  - Treat Windows Telegram runtime error reports as high-signal evidence even when Linux is the primary development environment. Windows uses different filesystem/IPC behavior and is more likely to reveal lock, heartbeat, named-pipe, atomic-rename, and Threaded Mode edge cases; minimize each report into a regression or a documented platform caveat instead of dismissing it as environment noise.
@@ -133,6 +134,7 @@ The core product loop is mobile continuation: start or supervise work in the ter
133
134
  ## 6.2 Validation Hotspots
134
135
 
135
136
  - Treat queue handling, compaction interaction, and lifecycle-hook state transitions as regression-prone areas; validate them after changing dispatch logic
137
+ - Keep the standard `npm run typecheck` gate strict about unused locals and parameters so stale imports and abandoned adapters fail validation instead of accumulating silently
136
138
  - Route important runtime failures through the recent runtime event recorder so `/telegram-status` remains useful for post-mortem debugging, not just transient status-bar errors
137
139
  - Treat remaining Markdown-to-HTML rendering as Telegram UI/compat output work, not generic Markdown rendering or assistant reply delivery
138
140
  - Preserve literal code content in Telegram rendering
@@ -174,6 +176,7 @@ The canonical detailed ownership map lives in [`docs/architecture.md`](./docs/ar
174
176
  - Keep the local `index.ts` plus `/lib/*.ts` import graph acyclic; `tests/invariants.test.ts` guards this boundary plus shared-bucket bans, empty interface-extension shell regressions, pi SDK centralization, source-only entrypoint Node-runtime/local-adapter/process/direct-pi access avoidance, runtime-domain isolation, structural leaf-domain import isolation, menu/model boundary drift, Telegram API/config default coupling, structural update/media coupling to Telegram API transport shapes, and attachment coupling to queue/inbound media/Telegram API helpers as domains keep evolving
175
177
  - Do not reintroduce shared bucket domains such as `lib/constants.ts`, `lib/types.ts`, `lib/globals.ts`, or broad global-augmentation files; constants, registry keys, state interfaces, and concrete transport shapes should stay in their owning domains, and `index.ts` should not grow new shared magic constants
176
178
  - Keep remaining `index.ts` code focused on cross-domain adapter wiring that needs live extension state, pi callbacks, Telegram API ports, or status updates; do not extract one-off closures solely to reduce line count
179
+ - Treat every meaningful `index.ts` change as a composition-pressure checkpoint: if the touched cluster owns domain-specific mutable state, identity, lifecycle, fallback policy, or a testable port boundary, move that cluster into its owning domain in the same release when safe. Keep inline wiring only when extraction would merely relocate one-off adapters or hide cross-domain state behind a broad dependency bag
177
180
  - Domain-specific queue planning, preview transport/controller behavior, UI/compat rendering, Telegram API transport, menu state, and command behavior should stay in their owning domains instead of moving to `/lib/runtime.ts` solely to shrink `index.ts`
178
181
  - Prefer narrow structural runtime ports in domains that only store or route pi-compatible values; direct pi SDK/model imports should stay centralized in `/lib/pi.ts`, while domains that actively register pi hooks/tools/commands should consume those concrete contracts through the adapter
179
182
 
package/BACKLOG.md CHANGED
@@ -1,28 +1,16 @@
1
1
  # Project Backlog
2
2
 
3
- _This backlog tracks only open release-relevant work: 0.20.0 profile isolation, evidence-gated Telegram client/runtime follow-ups, and upstream Pi API blockers. Completed validation evidence belongs in `CHANGELOG.md`, not in this queue._
3
+ _This backlog tracks only open release-relevant work: live promoted-follower verification, evidence-gated Telegram client/runtime follow-ups, and upstream Pi API blockers. Completed validation evidence belongs in `CHANGELOG.md`, not in this queue._
4
4
 
5
- ## P0Profile Reality Isolation Audit
5
+ ## P1Promoted Follower Reload Evidence
6
6
 
7
- Context: 0.20.0 profiles are parallel bot/runtime realities, not just alternate credentials. Any persistent or observable runtime surface that cannot safely mix bot identities must be explicitly global or profile-scoped. Logs and Threaded Mode state are now profile-aware; the remaining audit lens should check surfaces beyond the validated setup/connect smoke path: bus endpoints, temp artifacts, in-memory registries, queues, ownership caches, and status diagnostics.
7
+ Context: deterministic coverage protects promoted follower thread preservation, and the latest live Linux smoke closed reload routing, follower Active, and reroute/restore regressions. The exact promoted-leader reload path is deliberately outside the 0.20.1 profile IPC hotfix because it is unrelated to profile transport isolation; keep it as an evidence-gated follow-up rather than blocking that release.
8
8
 
9
9
  Open work:
10
10
 
11
- - [ ] Audit remaining non-setup filesystem/runtime observability surfaces and classify each as global shared config, session-local memory, or profile-scoped reality.
12
- - [ ] Profile-scope bus leader/follower IPC endpoints if parallel named profiles can run in the same agent dir concurrently.
13
- - [ ] Add regression coverage for any newly profile-scoped surface, preserving default-profile legacy paths where applicable.
11
+ - [ ] Capture live evidence that leader follower promotes `/reload` preserves the promoted leader's Telegram thread identity.
14
12
 
15
- Done when: parallel named-profile runtimes have no shared logs/state/IPC/diagnostic surfaces except intentionally top-level shared bridge configuration.
16
-
17
- ## P0 — Promoted Follower Reload Smoke
18
-
19
- Context: deterministic coverage protects promoted follower thread preservation, and the latest live Linux smoke closed the reload routing, follower Active, and reroute/restore regressions. Keep one explicit live check for promoted-follower reload identity unless that exact path is covered in the release smoke pass.
20
-
21
- Open work:
22
-
23
- - [ ] Close leader → follower promotes → `/reload` promoted leader preserves the same Telegram thread identity.
24
-
25
- Done when: promoted-follower reload identity has live Telegram evidence or is deliberately deferred out of the hotfix scope.
13
+ Done when: promoted-follower reload identity has direct live Telegram evidence.
26
14
 
27
15
  ## P1 — Native Windows Threaded Mode Follow-Ups
28
16
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.20.2: Live Thread Reality
4
+
5
+ - `[Profiles]` Named `/telegram-connect <profile>` now verifies the persisted profile before stopping polling or changing session-local profile activation. Impact: a mistyped or missing profile leaves the current bot, transport ownership, and polling runtime untouched instead of disconnecting a healthy session.
6
+ - `[Profiles]` Follower target replacement now resolves its fallback binding key from the active profile at replacement time instead of capturing the default-profile key during extension construction. Impact: named-profile thread restore cannot write a missing-record fallback into another bot profile's identity namespace.
7
+ - `[Profiles]` Setup now returns explicit success, cancellation, unavailable/busy, validation-failure, and polling-failure outcomes. Named-profile setup validates in an isolated temporary config and commits/stops the previous runtime only after token validation; `saved and connected` appears only after polling startup succeeds. Impact: cancellation and invalid tokens leave the active profile and transport untouched, while polling failures cannot produce false success copy.
8
+ - `[Diagnostics]` Compact status and `telegram_help` now render the active profile's real `state.<profile>.json` and `logs.<profile>.jsonl` filenames while retaining the legacy unsuffixed paths for the default profile. Impact: named-profile debugging no longer points operators and agents at another bot runtime's evidence.
9
+ - `[Validation]` Removed stale turn-helper imports and enabled TypeScript's `noUnusedLocals` and `noUnusedParameters` checks in the standard typecheck gate. Impact: abandoned imports and adapters now fail local and CI validation instead of silently accumulating.
10
+ - `[Composition]` Moved profile-scoped bus process identity and live endpoint resolution into the existing `bus` domain; moved manual-follower key resolution, promoted-binding transfer, and receiver/recovery/registration assembly into `bus-follower`; and consolidated provisioner, reconciliation, API-proxy, and server runtime assembly inside `bus-leader`. Impact: hotfix-sensitive profile isolation and bus lifecycle wiring stay with established owners instead of repeated construction-time closures and circular initialization in the composition root, reducing `index.ts` from 1,398 to 1,314 lines without adding another domain.
11
+ - `[Validation]` Added direct follower and leader assembly regressions, kept entrypoint/import architecture invariants green, and passed the full test suite (1 platform skip). Impact: composition compression is behavior-preserving and the remaining entrypoint surface stays explicit cross-domain wiring rather than being hidden behind a generic runtime bag.
12
+ - `[Live Linux]` A short leader `/reload` smoke auto-started from the retained lock in 652 ms, reused Dawn at the same thread target and slot `D`, restored the local bus server, and removed 17 historical follower bindings after recovery grace. Impact: lock-driven leader reload, same-thread preservation, and delayed non-destructive roster compaction are confirmed live; the smoke also exposed a surviving historical `lastSlot: "Z"` cursor that is corrected below.
13
+ - `[Thread Slots]` When follower-binding reality reconciliation removes stale records, it now realigns a cursor owned by those removed records to the newest-created remaining live binding in the same persisted compaction. It preserves a cursor that still belongs to live state, leaves pending/reserved slots as allocation guards, and does not reinterpret orphaned cursors from ordinary thread deletion. Impact: current-version compaction cannot recreate the observed historical `Z` cursor beside Dawn-only `D`; the next fresh follower starts from `E`.
14
+ - `[Thread Slots]` Converted `bot.lastSlot` from a max-letter watermark into a true ring cursor that advances through `Z → A`, starts allocation from the latest fresh slot, and ignores higher occupied letters when the cursor points earlier in the ring. Impact: wrapped histories no longer pin allocation at `Z`, and a live `D` cursor can assign the next free follower slot from `E`.
15
+ - `[Thread Identity]` Limited cursor updates to fresh/new-target records while preserving same-target binding reuse without cursor drift. Impact: reconciliation and reload upserts cannot silently rewrite the fresh-allocation sequence.
16
+ - `[Live State]` Replaced historical follower-registry restoration with a bounded leader-reload grace window followed by non-destructive reconciliation against the live bus roster. Followers now carry their last known target while re-registering so live processes reuse the same thread, while dead current-state records and identities leave `state.json` without deleting Telegram tabs. Impact: runtime truth compacts stale slot occupancy after recovery has had time to converge.
17
+ - `[Validation]` Added ring-wrap, cursor-priority, dead-follower reality, delayed reconciliation, and same-target follower re-registration regressions while keeping the existing slot reuse, pending provision, reservation, and alphabet-cap coverage green.
18
+
19
+ ## 0.20.1: Profile IPC Isolation Hotfix
20
+
21
+ - `[Runtime Isolation]` Profile-scoped Threaded Mode leader and follower IPC endpoints on Unix and Windows while preserving the default profile's legacy socket and named-pipe paths. Impact: parallel named-profile runtimes no longer contend for, unlink, or connect to another bot profile's local bus transport.
22
+ - `[Profile Switching]` Resolved leader and follower endpoints from the active profile when servers start, follower calls are sent, diagnostics render, and follower registration publishes its receiver address. Impact: changing profiles after process start cannot retain stale IPC identity from the previously active profile.
23
+ - `[Validation]` Added Unix and Windows endpoint-isolation regressions plus runtime restart coverage, and documented global, profile-scoped, and session-local runtime surfaces. Impact: profile reality boundaries are explicit and deterministic without making scratch attachments or extension registries routing authority.
24
+
3
25
  ## 0.20.0: Pi-Compatible Multi-Profile Runtime
4
26
 
5
27
  - `[Profiles]` Added named Telegram bot/session profiles under `telegram.json` `profiles` while preserving the top-level default profile and legacy default paths. Profile activation is session-local, bot/session fields are profile-scoped, shared bridge settings remain global, and setup/connect accept explicit profile names. Impact: separate bots can run from the same agent directory without `telegram-bots.json`, persisted active-profile drift, or default-profile migration risk.
package/README.md CHANGED
@@ -41,7 +41,7 @@ Run this inside Pi:
41
41
  /telegram-setup
42
42
  ```
43
43
 
44
- Paste the bot token. If `~/.pi/agent/telegram.json` already contains a saved token, setup offers it as the default. If no saved token exists, setup can prefill from `TELEGRAM_BOT_TOKEN`, `TELEGRAM_BOT_KEY`, `TELEGRAM_TOKEN`, or `TELEGRAM_KEY`.
44
+ Paste the bot token. If `~/.pi/agent/telegram.json` already contains a saved token, setup offers it as the default. If no saved token exists, setup can prefill from `TELEGRAM_BOT_TOKEN`, `TELEGRAM_BOT_KEY`, `TELEGRAM_TOKEN`, or `TELEGRAM_KEY`. Named profiles are optional; the ordinary `/telegram-setup` and `/telegram-connect` flow keeps using the default profile. Use `/telegram-setup <name>` only when you want an additional bot profile. Cancelling or failing named-profile token validation leaves the currently active profile and polling runtime unchanged; setup reports the profile as saved and connected only after polling startup succeeds.
45
45
 
46
46
  ### 3. Connect this Pi session
47
47
 
@@ -49,7 +49,7 @@ Paste the bot token. If `~/.pi/agent/telegram.json` already contains a saved tok
49
49
  /telegram-connect
50
50
  ```
51
51
 
52
- The connected Pi instance owns Telegram polling. In classic mode this is a singleton lock. When Telegram private-chat Threaded Mode is available, one live instance becomes the leader and later visible Pi instances register as followers.
52
+ The connected Pi instance owns Telegram polling. Use `/telegram-connect <name>` to activate a named profile. Each profile is a parallel bot runtime with isolated polling, diagnostics, Threaded Mode state, and local bus transport; the unnamed default profile keeps legacy paths. In classic mode each profile uses a singleton lock. When Telegram private-chat Threaded Mode is available, one live instance becomes the profile's leader and later visible Pi instances register as followers.
53
53
 
54
54
  ### 4. Pair your Telegram account
55
55
 
@@ -69,44 +69,45 @@ The first Telegram user to message the bot becomes the allowed owner. Other user
69
69
  - Send voice, images, files, replies, edits, or media groups; the bridge turns them into Pi context.
70
70
  - Ask for an artifact; `telegram_attach` returns it through the active reply or direct Telegram delivery.
71
71
  - In Threaded Mode, run multiple visible Pi instances through one bot, each with its own Telegram thread.
72
+ - Configure named profiles to run independent Telegram bots from the same Pi agent directory without sharing transport or routing state.
72
73
 
73
74
  ## Product Model
74
75
 
75
- | Lens | What `pi-telegram` owns |
76
- | ----------------------- | -------------------------------------------------------------------------------------------------- |
77
- | Operator companion | A phone-width control surface for a live Pi session |
78
- | Runtime adapter | Telegram turns mapped into Pi lifecycle, queueing, previews, final replies, and artifacts |
79
- | Telegram UI harness | Menus, settings, callbacks, Rich Markdown, drafts, active status, buttons, voice, and files |
80
- | Multi-instance organism | One leader plus explicit visible followers routed through Telegram private-chat threads |
81
- | Extension platform | Commands, sections, status rows, update handlers, inbound/outbound handlers, and voice providers |
82
- | Safety boundary | No hidden Pi processes, no fake terminal, no PTY tricks, no arbitrary TUI slash-command forwarding |
76
+ | Lens | What `pi-telegram` owns |
77
+ | --- | --- |
78
+ | Operator companion | A phone-width control surface for a live Pi session |
79
+ | Runtime adapter | Telegram turns mapped into Pi lifecycle, queueing, previews, final replies, and artifacts |
80
+ | Telegram UI harness | Menus, settings, callbacks, Rich Markdown, drafts, active status, buttons, voice, and files |
81
+ | Multi-instance organism | One leader plus explicit visible followers routed through Telegram private-chat threads |
82
+ | Extension platform | Commands, sections, status rows, update handlers, inbound/outbound handlers, and voice providers |
83
+ | Safety boundary | No hidden Pi processes, no fake terminal, no PTY tricks, no arbitrary TUI slash-command forwarding |
83
84
 
84
85
  ## Feature Showcase
85
86
 
86
87
  `pi-telegram` is intentionally broad: it is a Telegram-shaped runtime surface, not only a message relay. This catalogue keeps the practical feature surface visible while detailed contracts stay in `/docs`.
87
88
 
88
- | Surface | What you can do | Why it matters |
89
- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
90
- | Prompt intake | Send text, replies, edits, images, files, albums, voice notes, and handler output into Pi. | Telegram becomes a real mobile input surface with file/context references, not just a text tunnel. |
91
- | Queue control | Inspect waiting turns, delete stale work, promote important prompts, continue, abort, stop, or force the next queued item. | Long Pi tasks keep running while new mobile prompts stay visible and controllable instead of interrupting or disappearing. |
92
- | Operator menu | Use `/start` for status, prompt templates, model, thinking, settings, queue, extension sections, and diagnostics. | The bot is an operator panel, not a command cheat sheet. |
93
- | Prompt templates | Run Pi prompt templates as Telegram-safe commands such as `/fix_tests`. | Reusable local workflows become phone-accessible without exposing arbitrary terminal commands. |
94
- | Model and thinking | Switch model or reasoning level from Telegram through safe continuation flows. | Mobile control can adjust execution strategy without tearing down the current session. |
95
- | Compaction | Confirm `/compact`, show native active status during compaction, and preserve Telegram-owned turn semantics. | Context maintenance is visible and safe from the phone. |
96
- | Draft previews | Show native active status by default, or enable Telegram Rich Draft previews while the agent is still working. | The operator can hide/show in-progress text independently from final rendering. |
97
- | Assistant rendering | Choose Native Rich Markdown or legacy Markdown-to-HTML for final assistant replies. | Renderer compatibility is explicit instead of being conflated with draft previews. |
98
- | Bridge UI rendering | Render tool rows, reasoning/technical steps, menus, queue controls, status, settings, diagnostics, and sections through explicit Telegram HTML/plain UI. | Harness-owned surfaces remain operationally predictable and visually distinct from model-authored answers. |
99
- | Inbound files | Download inbound files to the Pi agent temp directory with size limits. | Screenshots, PDFs, datasets, and artifacts enter Pi as inspectable local files. |
100
- | Outbound artifacts | Return generated files through `telegram_attach` during active turns or explicit direct delivery. | Agents send real artifacts as files, not pasted blobs. |
101
- | Voice input | Route audio through configured command-template handlers, programmatic handlers, or STT providers. | Voice notes become usable prompt context. |
102
- | Voice output | Use `telegram_voice`, reply modes, configured voice handlers, and TTS providers. | Replies can become Telegram voice messages when the workflow calls for it. |
103
- | Buttons | Turn top-level `telegram_button` comments into inline buttons. | Assistant-authored choices become native Telegram interactions. |
104
- | Callback routing | Route known callbacks to the owner extension and unknown callbacks back into Pi. | Companion extensions can build UI without polling Telegram themselves. |
105
- | 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. |
106
- | Reroute and restore | Preserve unknown threads and offer explicit target choices. | Telegram client state can be repaired without silently deleting or hijacking prompts. |
107
- | 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. |
108
- | 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. |
109
- | Safety and ownership | Pair one owner, lock transport, scope targets, and reject fake terminal behavior. | Remote access remains explicit, bounded, and understandable. |
89
+ | Surface | What you can do | Why it matters |
90
+ | --- | --- | --- |
91
+ | Prompt intake | Send text, replies, edits, images, files, albums, voice notes, and handler output into Pi. | Telegram becomes a real mobile input surface with file/context references, not just a text tunnel. |
92
+ | Queue control | Inspect waiting turns, delete stale work, promote important prompts, continue, abort, stop, or force the next queued item. | Long Pi tasks keep running while new mobile prompts stay visible and controllable instead of interrupting or disappearing. |
93
+ | Operator menu | Use `/start` for status, prompt templates, model, thinking, settings, queue, extension sections, and diagnostics. | The bot is an operator panel, not a command cheat sheet. |
94
+ | Prompt templates | Run Pi prompt templates as Telegram-safe commands such as `/fix_tests`. | Reusable local workflows become phone-accessible without exposing arbitrary terminal commands. |
95
+ | Model and thinking | Switch model or reasoning level from Telegram through safe continuation flows. | Mobile control can adjust execution strategy without tearing down the current session. |
96
+ | Compaction | Confirm `/compact`, show native active status during compaction, and preserve Telegram-owned turn semantics. | Context maintenance is visible and safe from the phone. |
97
+ | Draft previews | Show native active status by default, or enable Telegram Rich Draft previews while the agent is still working. | The operator can hide/show in-progress text independently from final rendering. |
98
+ | Assistant rendering | Choose Native Rich Markdown or legacy Markdown-to-HTML for final assistant replies. | Renderer compatibility is explicit instead of being conflated with draft previews. |
99
+ | Bridge UI rendering | Render tool rows, reasoning/technical steps, menus, queue controls, status, settings, diagnostics, and sections through explicit Telegram HTML/plain UI. | Harness-owned surfaces remain operationally predictable and visually distinct from model-authored answers. |
100
+ | Inbound files | Download inbound files to the Pi agent temp directory with size limits. | Screenshots, PDFs, datasets, and artifacts enter Pi as inspectable local files. |
101
+ | Outbound artifacts | Return generated files through `telegram_attach` during active turns or explicit direct delivery. | Agents send real artifacts as files, not pasted blobs. |
102
+ | Voice input | Route audio through configured command-template handlers, programmatic handlers, or STT providers. | Voice notes become usable prompt context. |
103
+ | Voice output | Use `telegram_voice`, reply modes, configured voice handlers, and TTS providers. | Replies can become Telegram voice messages when the workflow calls for it. |
104
+ | Buttons | Turn top-level `telegram_button` comments into inline buttons. | Assistant-authored choices become native Telegram interactions. |
105
+ | Callback routing | Route known callbacks to the owner extension and unknown callbacks back into Pi. | Companion extensions can build UI without polling Telegram themselves. |
106
+ | 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. |
107
+ | Reroute and restore | Preserve unknown threads and offer explicit target choices. | Telegram client state can be repaired without silently deleting or hijacking prompts. |
108
+ | 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. |
109
+ | 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. |
110
+ | Safety and ownership | Pair one owner, lock transport, scope targets, and reject fake terminal behavior. | Remote access remains explicit, bounded, and understandable. |
110
111
 
111
112
  ## Core Loop
112
113
 
@@ -126,14 +127,14 @@ The bridge keeps Telegram responsive without stealing Pi's runtime model. Queuei
126
127
 
127
128
  Use these in the bot DM.
128
129
 
129
- | Command | Purpose |
130
- | ----------- | ------------------------------------------------------- |
131
- | `/start` | Pair when needed and open the main operator menu |
132
- | `/compact` | Confirm and run session compaction when safe |
133
- | `/next` | Dispatch the next queued turn, aborting first if needed |
134
- | `/continue` | Enqueue a priority continuation prompt |
135
- | `/abort` | Abort the active run while preserving the queue |
136
- | `/stop` | Abort the active run and clear waiting Telegram turns |
130
+ | Command | Purpose |
131
+ | --- | --- |
132
+ | `/start` | Pair when needed and open the main operator menu |
133
+ | `/compact` | Confirm and run session compaction when safe |
134
+ | `/next` | Dispatch the next queued turn, aborting first if needed |
135
+ | `/continue` | Enqueue a priority continuation prompt |
136
+ | `/abort` | Abort the active run while preserving the queue |
137
+ | `/stop` | Abort the active run and clear waiting Telegram turns |
137
138
 
138
139
  Hidden compatibility shortcuts: `/help`, `/status`, `/model`, `/thinking`, `/queue`, and `/settings` jump into the same menu system.
139
140
 
@@ -141,12 +142,14 @@ Hidden compatibility shortcuts: `/help`, `/status`, `/model`, `/thinking`, `/que
141
142
 
142
143
  Run these inside Pi.
143
144
 
144
- | Command | Purpose |
145
- | ---------------------- | ------------------------------------------------------------------ |
146
- | `/telegram-setup` | Save or update the bot token |
147
- | `/telegram-connect` | Start polling and acquire Telegram transport ownership |
148
- | `/telegram-disconnect` | Stop polling and release ownership |
149
- | `/telegram-status` | Inspect connection, mode, queue, transport, and recent diagnostics |
145
+ | Command | Purpose |
146
+ | --- | --- |
147
+ | `/telegram-setup` | Save or update the default bot token |
148
+ | `/telegram-setup <profile>` | Save or update a named-profile bot token |
149
+ | `/telegram-connect` | Activate the default profile and acquire its transport ownership |
150
+ | `/telegram-connect <profile>` | Activate a named profile and acquire its transport ownership |
151
+ | `/telegram-disconnect` | Stop polling and release ownership |
152
+ | `/telegram-status` | Inspect connection, mode, queue, transport, and recent diagnostics |
150
153
 
151
154
  ## Main Surfaces
152
155
 
@@ -184,23 +187,23 @@ Classic private DM mode is the base product mode. When Telegram private-chat Thr
184
187
  - Unknown threads are preserved and offered explicit reroute/restore choices.
185
188
  - Telegram never launches hidden Pi processes.
186
189
 
187
- | Mode | Best for | Runtime shape |
188
- | ------------- | -------------------------------------------------------- | ----------------------------------------------------------------------------- |
189
- | Classic DM | One live Pi session controlled from one private bot chat | One polling owner, one queue/runtime surface |
190
- | Threaded Mode | Several visible Pi terminals sharing one bot | One leader owns transport; followers route through named private-chat threads |
190
+ | Mode | Best for | Runtime shape |
191
+ | --- | --- | --- |
192
+ | Classic DM | One live Pi session controlled from one private bot chat | One polling owner, one queue/runtime surface |
193
+ | Threaded Mode | Several visible Pi terminals sharing one bot | One leader owns transport; followers route through named private-chat threads |
191
194
 
192
195
  ## Environment Configuration
193
196
 
194
197
  Most controls live in Pi commands or the Telegram menu. Environment variables remain for bootstrap and transport boundaries:
195
198
 
196
- | Area | Variables |
197
- | ------------------------- | ---------------------------------------------------------------------------------------------- |
198
- | Bot token bootstrap | `TELEGRAM_BOT_TOKEN`, `TELEGRAM_BOT_KEY`, `TELEGRAM_TOKEN`, `TELEGRAM_KEY` |
199
- | HTTP proxy | `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY`, plus `NODE_USE_ENV_PROXY=1` or Node `--use-env-proxy` |
200
- | Telegram network family | `PI_TELEGRAM_NETWORK_FAMILY=auto`, `ipv4`, `ipv6`, or `ipv4-fallback` |
201
- | Agent data root | `PI_CODING_AGENT_DIR` |
202
- | Inbound file limit | `PI_TELEGRAM_INBOUND_FILE_MAX_BYTES`, `TELEGRAM_MAX_FILE_SIZE_BYTES` |
203
- | Outbound attachment limit | `PI_TELEGRAM_OUTBOUND_ATTACHMENT_MAX_BYTES`, `TELEGRAM_MAX_ATTACHMENT_SIZE_BYTES` |
199
+ | Area | Variables |
200
+ | --- | --- |
201
+ | Bot token bootstrap | `TELEGRAM_BOT_TOKEN`, `TELEGRAM_BOT_KEY`, `TELEGRAM_TOKEN`, `TELEGRAM_KEY` |
202
+ | HTTP proxy | `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY`, plus `NODE_USE_ENV_PROXY=1` or Node `--use-env-proxy` |
203
+ | Telegram network family | `PI_TELEGRAM_NETWORK_FAMILY=auto`, `ipv4`, `ipv6`, or `ipv4-fallback` |
204
+ | Agent data root | `PI_CODING_AGENT_DIR` |
205
+ | Inbound file limit | `PI_TELEGRAM_INBOUND_FILE_MAX_BYTES`, `TELEGRAM_MAX_FILE_SIZE_BYTES` |
206
+ | Outbound attachment limit | `PI_TELEGRAM_OUTBOUND_ATTACHMENT_MAX_BYTES`, `TELEGRAM_MAX_ATTACHMENT_SIZE_BYTES` |
204
207
 
205
208
  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.
206
209
 
package/docs/README.md CHANGED
@@ -6,7 +6,7 @@ Living index of project documentation in `/docs`.
6
6
 
7
7
  ## Documents
8
8
 
9
- - [architecture.md](./architecture.md) — Overview of the Telegram bridge runtime, queueing model, native Rich Markdown delivery, UI/compat rendering, and interactive controls
9
+ - [architecture.md](./architecture.md) — Overview of the Telegram bridge runtime, optional profile isolation, queueing model, native Rich Markdown delivery, UI/compat rendering, and interactive controls
10
10
  - [public-api.md](./public-api.md) — Stable public API map: package entrypoints, commands, config, assistant markup, extension APIs, smoke examples, and compatibility boundaries
11
11
  - [../.agents/skills/telegram-bot/SKILL.md](../.agents/skills/telegram-bot/SKILL.md) — Agent-facing Telegram Bot API lookup skill backed by a local full Bot API reference
12
12
  - [../.agents/skills/domain-dag/SKILL.md](../.agents/skills/domain-dag/SKILL.md) — Project-local Domain DAG architecture skill and validator guidance
@@ -15,7 +15,7 @@ Living index of project documentation in `/docs`.
15
15
  - [outbound.md](./outbound.md) — Local `pi-telegram` outbound-handler config, text/voice/button behavior, voice synthesis provider fallback priority, artifact outputs, and callback routing
16
16
  - [callback-namespaces.md](./callback-namespaces.md) — Shared Telegram `callback_data` namespace standard for layered extensions
17
17
  - [updates.md](./updates.md) — Update classification and runtime handler registry that lets layered extensions observe and consume Telegram updates without owning their own polling connection
18
- - [multi-instance-bus.md](./multi-instance-bus.md) — Optional multi-instance Telegram bus architecture: leader/follower routing, thread targets, instance slots, manual follower registration, and recovery semantics
18
+ - [multi-instance-bus.md](./multi-instance-bus.md) — Optional multi-instance Telegram bus architecture: profile-scoped transport, leader/follower routing, thread targets, instance slots, manual follower registration, and recovery semantics
19
19
  - [sections.md](./sections.md) — Telegram Extension Sections Standard: registration contract, context ports, callback routing, navigation hierarchy, and demo reference for pi extensions that want Telegram UI surfaces
20
20
  - [voice.md](./voice.md) — Voice integration guide: detection, reply policy, STT/TTS provider registration, provider-owned conversion, and transparent interception
21
21
  - [locks.md](./locks.md) — Shared `locks.json` standard for singleton extension ownership
@@ -52,7 +52,7 @@ The repository uses a **Flat Domain DAG**:
52
52
  - `api`: Bot API helpers, retries, uploads/downloads, temp cleanup, byte limits, chat actions, lazy token clients, and API error recording.
53
53
  - `config` / `setup`: `telegram.json`, bot token setup, named bot/session profiles, first-user pairing, authorization, env fallback, atomic persistence, effective config views, and live config accessors.
54
54
  - `locks` / `polling`: singleton lock storage and status labels, lock-aware polling lifecycle/takeover/follower-registration orchestration, classic-vs-Threaded polling switching, Threaded Mode capability probes/monitoring, long-poll controller state, offset persistence, and poll-loop wiring.
55
- - `bus` / `bus-api` / `bus-leader` / `bus-follower` / `ownership` / `target`: Threaded Mode multi-instance bus contracts, local leader/follower IPC, leader-only orchestration, follower-side manual registration/session runtime, follower-routed Bot API calls, live message ownership, and `{ chatId, threadId? }` target identity. `bus` owns shared protocol and local IPC primitives; `bus-leader` owns leader runtime, leader envelope handling, activation scheduling, and leader polling/server/prune orchestration; `bus-follower` owns this Pi instance's follower-side registration, heartbeat, forwarded-update receiver, and routed API caller without any process spawning.
55
+ - `bus` / `bus-api` / `bus-leader` / `bus-follower` / `ownership` / `target`: Threaded Mode multi-instance bus contracts, profile-scoped process/endpoint identity, local leader/follower IPC, leader-only orchestration, follower-side manual registration/session runtime, follower-routed Bot API calls, live message ownership, and `{ chatId, threadId? }` target identity. `bus` owns shared protocol, process identity, profile-aware local endpoints, and IPC primitives; `bus-leader` owns leader runtime, leader envelope handling, activation scheduling, and leader polling/server/prune orchestration; `bus-follower` owns process-stable manual-follower keys plus this Pi instance's follower-side registration, heartbeat, forwarded-update receiver, and routed API caller without any process spawning.
56
56
  - `sync`: demand-driven Telegram reconciliation and local assumption policy. It does not own a complete Telegram bot read-model; Bot API lacks a complete topic/thread listing surface. It owns sync slices, invalidation triggers, observation intake, status/debug freshness, and reconciliation scheduling across bot identity, pairing assumptions, live target bindings, reservations, and transport health after meaningful observable signals. It should call narrower domain primitives rather than letting `index.ts`, `threads`, or `status` accumulate cross-cutting reconciliation policy.
57
57
  - `thread-reconciler`: Threaded Mode control-plane planning for Telegram thread/tab lifecycle. It owns the reconciliation state machine (`stable`, `provisioning`, `sync-required`, `cleanup-required`), pure plans, proof-before-delete rules, pending-provision protection, fresh-creation grace windows, leader-epoch checks, and the single policy authority for destructive thread cleanup actions. It excludes live Telegram API calls, inbound routing, menu rendering, and direct persistence.
58
58
  - `threads`: Telegram UI thread/tab binding state mapped to Bot API `message_thread_id` / `ForumTopic` transport. Owns current thread target state, slot allocation from the current extension state, baked compact thread-name selection, current binding persistence, and primitive thread provision helpers. It should not persist stale/offline/failed target history, own destructive cleanup policy, grow into the general Telegram synchronization domain, or expose a rename tool.
@@ -120,7 +120,9 @@ Deleting `locks.json` resets runtime ownership without deleting Telegram configu
120
120
 
121
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
- Named Telegram profiles are orthogonal to Threaded Mode. The selected profile chooses the bot/session slice (`botToken`, `botId`, `botUsername`, `allowedUserId`, `lastUpdateId`) and scopes singleton locks, diagnostics logs, state files, and thread/bus owner keys; it must not change the Threaded Mode rules. Within one selected profile, leader/follower election, bus transport, thread provisioning, routing, ownership forwarding, cleanup, and runtime diagnostics behave exactly as they do for the default profile. A different selected profile is a parallel bot runtime: its locks, `tmp/telegram/state.<profile>.json`, `tmp/telegram/logs.<profile>.jsonl`, previous log, and thread bindings are isolated from the default profile and from other named profiles while shared bridge settings remain top-level/global. The default profile preserves legacy `state.json`, `logs.jsonl`, and `logs.previous.jsonl` paths for compatibility.
123
+ Named Telegram profiles are orthogonal to Threaded Mode. The selected profile chooses the bot/session slice (`botToken`, `botId`, `botUsername`, `allowedUserId`, `lastUpdateId`) and scopes singleton locks, diagnostics logs, state files, thread/bus owner keys, and leader/follower IPC endpoints; it must not change the Threaded Mode rules. Within one selected profile, leader/follower election, bus transport, thread provisioning, routing, ownership forwarding, cleanup, and runtime diagnostics behave exactly as they do for the default profile. A different selected profile is a parallel bot runtime: its locks, `tmp/telegram/state.<profile>.json`, `tmp/telegram/logs.<profile>.jsonl`, previous log, thread bindings, Unix sockets, and Windows named pipes are isolated from the default profile and from other named profiles while shared bridge settings remain top-level/global. The default profile preserves legacy state, log, socket, and named-pipe paths for compatibility.
124
+
125
+ Profile reality follows three explicit storage classes. `telegram.json` shared settings and extension registries are process-global platform configuration; profile bot/session fields and observable transport/routing authority are profile-scoped; queues, active turns, ownership caches, menu state, and runtime controllers are session-local memory. Downloaded attachments use UUID-prefixed names in the shared Telegram scratch directory and are session artifacts rather than identity or routing authority, so cross-profile cleanup is limited to stale scratch files and cannot redirect live traffic.
124
126
 
125
127
  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.
126
128
 
@@ -136,7 +138,7 @@ The routing identity split is deliberate:
136
138
 
137
139
  - Live routing owner: `instanceId` from the currently registered follower/leader runtime. A live instance may have only one active bound thread; provisioning a new target removes older current-state bindings for the same `instanceId` and closes duplicate Telegram threads when possible.
138
140
  - Current binding owner: explicit `owner` metadata (`leader`, `manual-follower`, or API-level pending thread creation) plus cwd/thread-name metadata; string compatibility keys are derived internally and must not be the persisted source of ownership truth.
139
- - Instance slot: extension-owned single-letter `A`-`Z` ordering metadata. New instances advance monotonically through the alphabet and wrap after `Z` only to a free slot; closed lower slots are not backfilled out of order, and live concurrent instances are capped to available alphabet slots rather than duplicating occupied letters. The compact `bot.lastSlot` cursor is durable across reloads/live-test history, so after it reaches `Z` a later new thread may intentionally become `A` again if `A` is currently free.
141
+ - Instance slot: extension-owned single-letter `A`-`Z` ordering metadata. New instances advance through the alphabet and wrap after `Z` only to a free slot; live concurrent instances are capped to available alphabet slots rather than duplicating occupied letters. The compact `bot.lastSlot` cursor persists while its binding remains live/recovering, including true `Z → A` wraparound. When post-grace follower compaction removes the binding represented by the cursor, the same reconciliation pass realigns it to the newest-created remaining live binding so removed historical followers cannot dictate fresh allocation; unexpired pending provisions and reservations remain collision guards. Other thread deletion paths may intentionally preserve an orphaned cursor to continue ring sequence.
140
142
  - Instance thread name: durable human-facing identity metadata that replaces slot-only thread titles. Fresh threads choose one baked 4-6 letter Latin-word name from the assigned slot's curated palette using provisioning timestamp entropy and create the Telegram thread with that title immediately. Telegram-originated prompt prefixes expose this thread identity label, never follower/leader roles or generic seeds. Bare slot letters are fallback/legacy labels only; agents are not asked to name or rename threads.
141
143
  - Telegram destination: `TelegramTarget` as `{ chatId, threadId? }`, where `threadId` is Telegram `message_thread_id` for UI thread targets.
142
144
 
@@ -63,13 +63,13 @@ Implementations may expand `~` in command position and may resolve relative comm
63
63
 
64
64
  Supported forms:
65
65
 
66
- | Form | Meaning |
67
- | ---------------- | ------------------------------------------------ |
68
- | `{name}` | Required value from runtime values or `defaults` |
69
- | `{name=default}` | Inline default when no value is provided |
70
- | `{items[index]}` | Array item selected by literal or repeat index |
71
- | `{value??fallback}` | Fallback when the value is absent or falsy |
72
- | `{flag?yes:no}` | Conditional text selected by flag truthiness |
66
+ | Form | Meaning |
67
+ | --- | --- |
68
+ | `{name}` | Required value from runtime values or `defaults` |
69
+ | `{name=default}` | Inline default when no value is provided |
70
+ | `{items[index]}` | Array item selected by literal or repeat index |
71
+ | `{value??fallback}` | Fallback when the value is absent or falsy |
72
+ | `{flag?yes:no}` | Conditional text selected by flag truthiness |
73
73
 
74
74
  Resolution order is runtime values → `defaults` → inline default → error. Default values that are themselves a single placeholder, such as `{prompt}` resolving to `{prompts[index]}`, are resolved recursively with a small depth guard. A repeat node may set `repeat` to `{items.length}` when an array arg should determine fanout width.
75
75
 
package/docs/inbound.md CHANGED
@@ -59,11 +59,11 @@ Handlers match by optional `type`, `mime`, or `match`. `mime` and `type` are ind
59
59
 
60
60
  Built-in placeholders for text handlers:
61
61
 
62
- | Placeholder | Value |
63
- | ----------- | ------------ |
64
- | `{text}` | Current text |
65
- | `{mime}` | `text/plain` |
66
- | `{type}` | `text` |
62
+ | Placeholder | Value |
63
+ | --- | --- |
64
+ | `{text}` | Current text |
65
+ | `{mime}` | `text/plain` |
66
+ | `{type}` | `text` |
67
67
 
68
68
  ## Media/File Handlers
69
69
 
@@ -71,12 +71,12 @@ Media/file handlers keep the legacy attachment-handler behavior: downloaded file
71
71
 
72
72
  Built-in placeholders for media/file handlers:
73
73
 
74
- | Placeholder | Value |
75
- | ----------- | ---------------------------------------------- |
76
- | `{file}` | Downloaded file path |
77
- | `{mime}` | MIME type if known |
78
- | `{type}` | Kind: `voice`, `audio`, `document`, or `photo` |
79
- | `{text}` | Empty string |
74
+ | Placeholder | Value |
75
+ | --- | --- |
76
+ | `{file}` | Downloaded file path |
77
+ | `{mime}` | MIME type if known |
78
+ | `{type}` | Kind: `voice`, `audio`, `document`, or `photo` |
79
+ | `{text}` | Empty string |
80
80
 
81
81
  If a top-level one-step media handler template has no `{file}` placeholder, the downloaded file path is appended as the last command arg as a one-step handler convenience. Composition steps are plain command templates and do not receive implicit file-path args; include `{file}` explicitly where needed.
82
82
 
@@ -8,6 +8,8 @@ This document uses **thread** as the canonical product term because Telegram cli
8
8
 
9
9
  This document supersedes the narrower API-topic framing. Telegram threads are a UI/routing substrate, but the deeper design problem is multi-instance coordination: one bot token has one Telegram API update bus, while multiple live Pi agent instances may want to expose their own Telegram workspace through that bus.
10
10
 
11
+ Named bot profiles are optional and orthogonal to this design. The ordinary unnamed profile keeps the existing setup, connect, lock, state, log, Unix socket, and Windows named-pipe paths. When operators configure additional profiles, each profile is an independent bot runtime with its own lock key, observable state, thread ownership, and leader/follower IPC endpoints; leader election and follower routing never cross profile boundaries.
12
+
11
13
  ## Problem
12
14
 
13
15
  Classic `pi-telegram` mode binds one private Telegram DM to one live Pi instance through one bot token and one singleton polling owner. The lock currently answers: "which Pi instance owns Telegram control/polling?"
@@ -153,7 +155,7 @@ Leader election is heartbeat-gated and lock-backed:
153
155
  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
156
  5. If several followers detect stale leadership, atomic compare/write lock acquisition ensures only one becomes leader.
155
157
 
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.
158
+ Followers first try to re-register after leader reload or unknown-heartbeat responses, carrying their last known target so the new leader can reuse the same binding, then promote only after the grace window expires. The leader does not restore persisted followers into the live registry speculatively: after a bounded re-registration grace window it removes follower records that still lack a live bus owner from current `state.json`, without deleting Telegram tabs. This preserves real thread bindings through transient reload gaps without allowing historical records or competing pollers to masquerade as live state.
157
159
 
158
160
  ## Leader/Follower Communication
159
161
 
@@ -222,7 +224,7 @@ In Telegram private-chat Threaded Mode:
222
224
  - The private bot DM becomes the operator's multi-instance dashboard.
223
225
  - Each live bound instance gets one visible thread.
224
226
  - Each instance has a durable single-letter slot (`A`-`Z`) assigned by the extension and a bridge-authored `threadName`.
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.
227
+ - New slots advance through the alphabet and wrap after `Z` only to a free slot, intentionally capping concurrent visible instances to the alphabet without duplicating occupied letters. The compact `bot.lastSlot` cursor persists while its binding remains live/recovering, including true `Z → A` wraparound. When post-grace follower compaction removes the cursor's binding, that same reconciliation pass realigns it to the newest-created remaining live binding; pending provisions and reservations still block collisions. Other explicit thread deletion paths may preserve the cursor to continue ring sequence.
226
228
  - A follower that later becomes leader keeps its existing slot and thread name; leadership changes are transport role changes, not identity resets.
227
229
  - 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.
228
230
  - A thread-local `/start` opens that instance's menu.
@@ -368,9 +370,10 @@ All files containing routing, chat ids, thread ids, or process details use priva
368
370
 
369
371
  ### Follower heartbeat is missed
370
372
 
371
- - Leader prunes the follower from the live registry after missed heartbeats, but heartbeat pruning is only liveness bookkeeping.
373
+ - Leader prunes the follower from the live registry after missed heartbeats, but heartbeat pruning is only immediate liveness bookkeeping.
372
374
  - 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.
373
- - Followers treat rejected/missing heartbeat acknowledgements as registration loss: clear local registered truth, try to re-register with the current leader, wait a short leader-reload grace window, retry, and then promote themselves if the leader still cannot route them.
375
+ - 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, retry, and then promote themselves if the leader still cannot route them.
376
+ - After leader reload grace expires, persisted manual-follower records without a matching live registry owner leave current `state.json`; this compaction is non-destructive and does not close/delete the Telegram thread.
374
377
  - Every successful follower registration/re-registration sends a compact connected notice in the assigned thread so recovery and reconnection are visible during live testing without confusing heartbeat suspicion with real disconnect.
375
378
  - Successful forwarded updates and follower-originated API calls refresh liveness, so active followers are not pruned only because the interval heartbeat tick lagged.
376
379
  - Destructive follower thread teardown belongs to explicit `/telegram-disconnect` or confirmed reconciliation actions, not generic heartbeat pruning.
package/docs/outbound.md CHANGED
@@ -10,11 +10,11 @@ Text handlers use the portable [Command Template Standard](./command-templates.m
10
10
 
11
11
  An outbound handler is selected by `type`. Text replies and assistant markup map to handler types:
12
12
 
13
- | Source | Handler | Action |
14
- | ----------------- | ----------------------------- | ----------------------- |
15
- | Final text | `outboundHandlers[type=text]` | Transform before render |
16
- | `telegram_voice` | Voice pipeline | OGG/Opus `sendVoice` |
17
- | `telegram_button` | Built-in | Attach inline button |
13
+ | Source | Handler | Action |
14
+ | --- | --- | --- |
15
+ | Final text | `outboundHandlers[type=text]` | Transform before render |
16
+ | `telegram_voice` | Voice pipeline | OGG/Opus `sendVoice` |
17
+ | `telegram_button` | Built-in | Attach inline button |
18
18
 
19
19
  The voice pipeline is detailed below: configured `type: "voice"` handlers first, then programmatic handlers, then registered synthesis providers.
20
20