@llblab/pi-telegram 0.19.3 → 0.20.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 +6 -3
- package/BACKLOG.md +7 -6
- package/CHANGELOG.md +14 -2
- package/README.md +8 -5
- package/docs/README.md +2 -2
- package/docs/architecture.md +6 -1
- package/docs/command-templates.md +7 -7
- package/docs/inbound.md +11 -11
- package/docs/multi-instance-bus.md +2 -0
- package/docs/outbound.md +5 -5
- package/docs/public-api.md +1 -1
- package/index.ts +95 -26
- package/lib/bindings.ts +61 -11
- package/lib/bus-follower.ts +37 -13
- package/lib/bus-leader.ts +28 -43
- package/lib/bus-transport.ts +28 -4
- package/lib/bus.ts +66 -43
- package/lib/commands.ts +41 -8
- package/lib/config.ts +165 -26
- package/lib/locks.ts +78 -25
- package/lib/{runtime-log.ts → logs.ts} +58 -27
- package/lib/media.ts +97 -4
- package/lib/outbound-buttons.ts +1 -1
- package/lib/outbound.ts +5 -8
- package/lib/paths.ts +77 -0
- package/lib/prompt-templates.ts +3 -1
- package/lib/prompts.ts +1 -1
- package/lib/queue.ts +32 -1
- package/lib/routing.ts +106 -41
- package/lib/status.ts +6 -1
- package/lib/sync.ts +22 -17
- package/lib/telegram-api.ts +13 -14
- package/lib/threads.ts +142 -55
- package/lib/turns.ts +47 -13
- package/package.json +3 -3
- /package/{banner.png → screenshot.png} +0 -0
package/AGENTS.md
CHANGED
|
@@ -74,10 +74,11 @@ 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
|
+
- Telegram profiles are parallel bot/runtime realities, not merely alternate credentials. Shared bridge settings remain global, but any lock, IPC endpoint, state snapshot, diagnostics log, thread/bus binding, runtime cache, ownership map, or operator-visible status surface that could mix bot identities must be deliberately classified as global, session-local, or profile-scoped. Preserve default-profile legacy paths/keys for compatibility, and give named profiles isolated observable surfaces unless there is an explicit top-level shared-config reason not to.
|
|
77
78
|
- Telegram private-chat Threaded Mode is the public switch for the local multi-instance bus. There is no separate public `telegram.json` bus switch: classic/private-chat singleton DM behavior is the base product mode, and when Telegram reports Threaded Mode available for the bot the bridge enables leader/follower bus behavior. If Telegram reports Threaded Mode unavailable or thread operations prove it unavailable, the bridge returns to classic singleton behavior as a first-class mode. During a live Threaded Mode downgrade, the current transport leader keeps the singleton classic polling role and followers disconnect instead of taking over or offering takeover while active thread bindings prove this is a downgrade from a live bus organism. Do not inject slot/thread rename guidance in classic mode. Blocked instances should not silently register as followers unless the effective runtime mode is Threaded Mode.
|
|
78
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.
|
|
79
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.
|
|
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
|
+
- 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.
|
|
81
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
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.
|
|
83
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`.
|
|
@@ -96,8 +97,9 @@ The core product loop is mobile continuation: start or supervise work in the ter
|
|
|
96
97
|
- `/start`, `/help`, and `/status` open the unified command-help/status-row/control menu; `/model`, `/thinking`, and `/queue` jump to sections directly; visible bot commands are `/start`, `/compact`, `/next`, `/continue`, `/abort`, `/stop`
|
|
97
98
|
- Command/menu emoji are fixed UI adornments owned by the `commands` map; do not add a persisted emoji toggle or Settings menu until there is a real setting to own
|
|
98
99
|
- UI emoji semantics live in `docs/ui-style.md` as the project-level emoji registry. Before adding a new UI emoji, reuse an existing registered meaning or update the registry in the same change. Use `🧵` consistently as the canonical Telegram/Pi thread marker in thread chooser headings, thread lifecycle copy, and target-routing status text; do not decorate every concrete thread-target button with it when the button label is already the thread name or slot. Operator-facing Threaded Mode routing copy should use the `Pi` glyph for Pi instance/thread/session concepts; reserve `Pi` spelling for product prose, package/API names, and surrounding docs where the wordmark is clearer.
|
|
100
|
+
- Telegram prompt context metadata uses `[tag|key:value|key:value]` without spaces around `:`. `[telegram...]` identifies the current inbound surface only: owner/current turns use `[telegram]` or `[telegram|thread:<name>]`, while guest-mode turns use `[telegram|guest:<group-title-or-peer-username-or-id>]`. In private guest replies authored by the owner, `guest:<...>` must name the replied guest peer, not the owner. Alternate/source authors belong on source-specific blocks such as `[reply|from:<username-or-id>]`, `[forward|from:<username-or-id>]`, and `[attachments|from:<username-or-id>]`; plain `[attachments]` remains current-turn/current-owner attachments and should render immediately after the `[telegram...]` current text before reply/forward/source attachment context.
|
|
99
101
|
- Telegram `reply_to_message` context is prompt-only and must not affect slash-command parsing; when Telegram includes `rich_message` blocks on a quoted rich reply, extract plain text from those blocks before falling back to raw `text`/`caption` so prompt context does not leak raw Rich Markdown source
|
|
100
|
-
- Long-lived timers, pollers, watchers, and deferred queue dispatch must be session-bound and avoid stale live Pi contexts after session replacement. Pi session replacement
|
|
102
|
+
- Long-lived timers, pollers, watchers, and deferred queue dispatch must be session-bound and avoid stale live Pi contexts after session replacement. Pi session replacement/reload changes agent context; Threaded Mode followers must disconnect their bus registration and receiver cleanly rather than preserving a stale follower socket across reload. After reload, a follower remains `Telegram disconnected` until the operator runs `Telegram Connect` again; its old Telegram thread binding is marked stale so the next explicit connect can provision a fresh routable thread instead of silently rebinding to an unroutable stale tab, and status must not report a live follower while registration is absent.
|
|
101
103
|
- Do not add Telegram commands that imitate Pi interactive session replacement, navigation, or TUI rendering through private internals, ANSI terminal clearing, raw TTY injection, or a shadow `pi` subprocess. Features such as a real Telegram `/new` require a public Pi API that runs the same session-replacement path as the terminal command.
|
|
102
104
|
- In-flight `/model` switching is limited to Telegram-owned active turns; if a tool call is active, abort is delayed until the tool finishes
|
|
103
105
|
|
|
@@ -108,7 +110,6 @@ The core product loop is mobile continuation: start or supervise work in the ter
|
|
|
108
110
|
- Formula guidance belongs in the Telegram-turn prompt contract: use `$...$` for inline math and `$$...$$` for block math; backticks intentionally render formulas as literal code
|
|
109
111
|
- Real code blocks must stay literal and escaped
|
|
110
112
|
- `telegram_attach` is the canonical outbound file-delivery path for Telegram-originated requests; outside active Telegram turns it may send immediately to the paired/default chat, a registered follower's assigned target, or explicit `chat_id` plus optional `thread_id` for explicit local/TUI delivery requests only when this Pi instance owns `/telegram-connect` or is registered with the Threaded Mode multi-instance bus. `telegram_message` is the first-class direct Telegram Markdown text tool for local/TUI prompts and follows the same ownership-or-bus-registration gate; neither direct tool replaces normal active-turn replies. It reuses top-level `telegram_button` comments for inline buttons; buttons must be attached to a text message, never sent as standalone actions
|
|
111
|
-
- Telegram delivery strips top-level HTML comments from preview/final text; column-zero top-level `<!-- telegram_voice ... -->` and `<!-- telegram_button ... -->` blocks are special outbound comments handled after `agent_end` without requiring agent-side transport tool calls, while comments inside code, quotes, lists, or indented examples stay literal
|
|
112
113
|
- Telegram prompt guidance is layered and context-frugal: unconfigured sessions receive no bridge suffix, configured sessions receive compact direct-delivery/help guidance, and Telegram-originated turns receive only the minimal per-turn contract. Detailed voice/button/direct-delivery/Threaded Mode/formatting/debug guidance belongs in the agent-facing `telegram_help` tool instead of being repeated in every prompt.
|
|
113
114
|
- `telegram_voice` and `telegram_button` are not Pi tools; keep prompts/docs explicit that agents should author markup while voice synthesis provider extensions own TTS/OGG conversion, and pi-telegram owns button routing plus Telegram delivery
|
|
114
115
|
- Voice reply policy and prompt context are owned by pi-telegram's `telegram.json` `voice.replyMode`: missing/invalid config behaves as `manual` but does not add a `[voice]` prompt-context block; only an explicit valid `voice.replyMode` renders context. Render a single voice field as `[voice] reply mode: manual|mirror|always`, and render multiple fields as a `[voice]` list; place voice context after `[outputs]` when handler output exists, otherwise after `[attachments]`; provider prompt contributions are optional provider-specific additions, not the default policy channel
|
|
@@ -125,8 +126,10 @@ The core product loop is mobile continuation: start or supervise work in the ter
|
|
|
125
126
|
|
|
126
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.
|
|
127
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.
|
|
128
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.
|
|
129
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.
|
|
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.
|
|
130
133
|
|
|
131
134
|
## 6.2 Validation Hotspots
|
|
132
135
|
|
package/BACKLOG.md
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
# Project Backlog
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
##
|
|
5
|
+
## P1 — Promoted Follower Reload Evidence
|
|
6
6
|
|
|
7
|
-
Context: deterministic coverage protects promoted follower thread preservation, and the latest live Linux smoke closed
|
|
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
|
-
- [ ]
|
|
11
|
+
- [ ] Capture live evidence that leader → follower promotes → `/reload` preserves the promoted leader's Telegram thread identity.
|
|
12
12
|
|
|
13
|
-
Done when: promoted-follower reload identity has live Telegram evidence
|
|
13
|
+
Done when: promoted-follower reload identity has direct live Telegram evidence.
|
|
14
14
|
|
|
15
15
|
## P1 — Native Windows Threaded Mode Follow-Ups
|
|
16
16
|
|
|
@@ -20,12 +20,13 @@ Open work:
|
|
|
20
20
|
|
|
21
21
|
- [ ] Capture text diagnostics if Windows classic restore/status convergence repeatedly exceeds the intended 5–15 second fallback window.
|
|
22
22
|
- [ ] Add a focused regression or transport/status adjustment only if new Windows evidence shows a repeatable named-pipe, lock, heartbeat, queue, or status-convergence issue.
|
|
23
|
+
- [ ] For every Windows connect/runtime crash report, classify the failing boundary (`locks.json` atomic write, named pipe, heartbeat, polling, queue, or status), ensure `logs.jsonl` captures enough redacted evidence before shutdown, and add a minimized regression when the failure can be simulated deterministically.
|
|
23
24
|
|
|
24
25
|
Done when: new Windows-specific runtime issues are either fixed with targeted coverage or left out of the backlog because the native smoke remains green.
|
|
25
26
|
|
|
26
27
|
## P1 — Evidence-Backed Telegram Client Follow-Ups
|
|
27
28
|
|
|
28
|
-
Context: The release should avoid speculative live-test matrices.
|
|
29
|
+
Context: The release should avoid speculative live-test matrices. Future Telegram-client quirks should be handled only when there is concrete evidence or a minimized fixture.
|
|
29
30
|
|
|
30
31
|
Open work:
|
|
31
32
|
|
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.
|
|
3
|
+
## 0.20.1: Profile IPC Isolation Hotfix
|
|
4
4
|
|
|
5
|
-
- `[
|
|
5
|
+
- `[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.
|
|
6
|
+
- `[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.
|
|
7
|
+
- `[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.
|
|
8
|
+
|
|
9
|
+
## 0.20.0: Pi-Compatible Multi-Profile Runtime
|
|
10
|
+
|
|
11
|
+
- `[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.
|
|
12
|
+
- `[Runtime Isolation]` Scoped Telegram lock ownership, Threaded Mode owner keys, runtime logs, previous logs, and Threaded Mode state by selected profile. Impact: named profiles keep independent polling ownership and observable bot realities, while the default profile remains backward-compatible.
|
|
13
|
+
- `[Pi Compatibility]` Centralized agent-dir/path resolution with `PI_CODING_AGENT_DIR` support and moved queued Telegram prompt dispatch back under pi-telegram's scheduler as normal `sendUserMessage(content)` turns. Impact: Pi-compatible runtimes such as OMP can share the bridge without stranded follow-up work or duplicated path contracts.
|
|
14
|
+
- `[Prompt Context]` Normalized Telegram prompt metadata for forwards, replies, source attachments, and guest surfaces. Impact: `[telegram...]` identifies only the inbound surface, while `[reply|from:...]`, `[forward|from:...]`, and `[attachments|from:...]` carry source provenance without conflating owner-authored prompts with quoted or forwarded evidence.
|
|
15
|
+
- `[Threaded Mode]` Hardened manual follower lifecycle around reloads, reconnects, disconnects, and registration races. Followers now disconnect cleanly on session replacement, reconnect only on explicit `Telegram Connect`, provision one fresh routable tab with monotonic slot allocation, and never show stale previous thread names while disconnected. Impact: follower churn is predictable across reload/election paths without duplicate-tab spam or unroutable preserved bindings.
|
|
16
|
+
- `[Reliability]` Hardened Windows lock heartbeat writes for transient `EPERM` / `EBUSY` / `EACCES` failures, skipped virtual prompt-template commands without source paths, and treats `createForumTopic` as non-idempotent by avoiding retry on topic creation. Impact: common runtime and Telegram-client edge cases degrade to diagnosable state instead of crashes, menu failures, or duplicate visible topics.
|
|
17
|
+
- `[Validation]` Added regressions for profile persistence/activation, setup/connect profile behavior, profile-scoped locks/state/logs, OMP-style dispatch, path resolution, prompt context, follower reconnect/provisioning, status projection, and Domain DAG invariants. Live Linux smoke confirmed independent named-profile setup/connect, hot Threaded Mode upgrade, parallel leaders for separate bots, explicit follower reconnect, monotonic follower slot allocation including Z→A wraparound, and single-topic provisioning.
|
|
6
18
|
|
|
7
19
|
## 0.19.2: Draft And Rendering Isolation Hotfix
|
|
8
20
|
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pi-telegram
|
|
2
2
|
|
|
3
|
-

|
|
4
4
|
|
|
5
5
|
**A Telegram companion hub for live Pi sessions.**
|
|
6
6
|
|
|
@@ -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.
|
|
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
|
|
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,6 +69,7 @@ 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
|
|
|
@@ -143,8 +144,10 @@ Run these inside Pi.
|
|
|
143
144
|
|
|
144
145
|
| Command | Purpose |
|
|
145
146
|
| --- | --- |
|
|
146
|
-
| `/telegram-setup` | Save or update the bot token |
|
|
147
|
-
| `/telegram-
|
|
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 |
|
|
148
151
|
| `/telegram-disconnect` | Stop polling and release ownership |
|
|
149
152
|
| `/telegram-status` | Inspect connection, mode, queue, transport, and recent diagnostics |
|
|
150
153
|
|
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
|
package/docs/architecture.md
CHANGED
|
@@ -50,7 +50,7 @@ The repository uses a **Flat Domain DAG**:
|
|
|
50
50
|
|
|
51
51
|
- `index.ts`: composition root for live ports, session state, transport adapters, and lifecycle registration.
|
|
52
52
|
- `api`: Bot API helpers, retries, uploads/downloads, temp cleanup, byte limits, chat actions, lazy token clients, and API error recording.
|
|
53
|
-
- `config` / `setup`: `telegram.json`, bot token setup, first-user pairing, authorization, env fallback, atomic persistence, and live config accessors.
|
|
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
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.
|
|
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.
|
|
@@ -120,6 +120,10 @@ 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, 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.
|
|
126
|
+
|
|
123
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.
|
|
124
128
|
|
|
125
129
|
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.
|
|
@@ -229,6 +233,7 @@ Assistant delivery guarantees:
|
|
|
229
233
|
|
|
230
234
|
- Model-authored Markdown is the source of truth; the bridge does not pre-render assistant Markdown to HTML unless the operator selects `assistant.rendering: "html"` for compatibility.
|
|
231
235
|
- Before native Rich Markdown delivery, the bridge normalizes known Bot-API-fragile source forms without changing visible meaning, including space-after-marker blockquotes and dollar-prefixed ticker atoms that Telegram may otherwise treat as unterminated math.
|
|
236
|
+
- Prompt context blocks use compact metadata (`[tag|key:value]`) as the stable inbound contract. `[telegram...]` names the current surface only: owner/current turns use `[telegram]` or `[telegram|thread:<name>]`; guest-mode turns use `[telegram|guest:<group-title-or-peer-username-or-id>]`. Source authors for quoted/forwarded material and their files are carried by `[reply|from:<username-or-id>]`, `[forward|from:<username-or-id>]`, and `[attachments|from:<username-or-id>]`, while plain `[attachments]` remains current-turn attachments and is ordered before reply/forward/source context.
|
|
232
237
|
- Quoted rich replies use Telegram `rich_message` blocks as the prompt-context source when available, so `[reply]` context receives rendered plain text instead of raw `InputRichMessage.markdown` fallback text.
|
|
233
238
|
- Long native Markdown replies are split only at Telegram Rich Message transport limits; oversized fenced code, display-math, and fully wrapped inline-formatting blocks are rewrapped per chunk so persisted Rich Markdown chunks remain structurally valid.
|
|
234
239
|
- When Draft previews are enabled, streaming previews pass structurally closed assistant Markdown prefixes through to `sendRichMessageDraft` with ownership checks, voice suppression, and serialized flushes. Unclosed inline spans, links, fenced code, comments, and display-math blocks are held back until a safe boundary exists. Draft failures are recorded and the failing frame is skipped instead of degrading to raw plain-message previews, because partial Markdown can be invalid while the final message remains valid.
|
|
@@ -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
|
|
67
|
-
|
|
|
68
|
-
| `{name}`
|
|
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}`
|
|
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}`
|
|
65
|
-
| `{mime}`
|
|
66
|
-
| `{type}`
|
|
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}`
|
|
77
|
-
| `{mime}`
|
|
78
|
-
| `{type}`
|
|
79
|
-
| `{text}`
|
|
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?"
|
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
|
|
14
|
-
|
|
|
15
|
-
| Final text
|
|
16
|
-
| `telegram_voice`
|
|
17
|
-
| `telegram_button` | Built-in
|
|
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
|
|
package/docs/public-api.md
CHANGED
|
@@ -458,7 +458,7 @@ async function synthesizeDemoOgg(_text: string): Promise<string> {
|
|
|
458
458
|
|
|
459
459
|
### Smoke Checklist
|
|
460
460
|
|
|
461
|
-
- The extension imports only `@llblab/pi-telegram
|
|
461
|
+
- The extension imports only public package membranes: `@llblab/pi-telegram`, `/commands`, `/sections`, `/status`, `/updates`, `/inbound`, `/outbound`, `/voice`, or `/keyboard`.
|
|
462
462
|
- It does not import `@llblab/pi-telegram/lib/*`.
|
|
463
463
|
- It registers on `session_start` and disposes on `session_shutdown`.
|
|
464
464
|
- Stable high-level registrations use durable ids.
|