@llblab/pi-telegram 0.20.0 → 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 CHANGED
@@ -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.
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,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.20.1: Profile IPC Isolation Hotfix
4
+
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
+
3
9
  ## 0.20.0: Pi-Compatible Multi-Profile Runtime
4
10
 
5
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.
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.
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
@@ -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
 
@@ -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?"
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
 
package/index.ts CHANGED
@@ -77,9 +77,21 @@ export default function (pi: Pi.ExtensionAPI) {
77
77
  let telegramBusLifecycleOverridePhase:
78
78
  | Status.TelegramBridgeBusLifecyclePhase
79
79
  | undefined;
80
- const telegramBusSocketPath = Bus.getTelegramBusSocketPath();
81
- const telegramBusFollowerSocketPath =
82
- Bus.getTelegramBusFollowerSocketPath(telegramInstanceId);
80
+ const getTelegramBusSocketPath = function (): string {
81
+ return Bus.getTelegramBusSocketPath(
82
+ undefined,
83
+ undefined,
84
+ getActiveTelegramThreadProfile(),
85
+ );
86
+ };
87
+ const getTelegramBusFollowerSocketPath = function (): string {
88
+ return Bus.getTelegramBusFollowerSocketPath(
89
+ telegramInstanceId,
90
+ undefined,
91
+ undefined,
92
+ getActiveTelegramThreadProfile(),
93
+ );
94
+ };
83
95
  let telegramBusRequestSequence = 0;
84
96
  const telegramBusFollowerRegistry = Bus.createTelegramBusFollowerRegistry();
85
97
  const telegramBusFollowerRegistrationState =
@@ -300,12 +312,13 @@ export default function (pi: Pi.ExtensionAPI) {
300
312
  },
301
313
  getLocalBus() {
302
314
  return {
303
- leaderSocketPath: telegramBusSocketPath,
304
- leaderTransport:
305
- BusTransport.getTelegramBusTransportKind(telegramBusSocketPath),
306
- followerSocketPath: telegramBusFollowerSocketPath,
315
+ leaderSocketPath: getTelegramBusSocketPath(),
316
+ leaderTransport: BusTransport.getTelegramBusTransportKind(
317
+ getTelegramBusSocketPath(),
318
+ ),
319
+ followerSocketPath: getTelegramBusFollowerSocketPath(),
307
320
  followerTransport: BusTransport.getTelegramBusTransportKind(
308
- telegramBusFollowerSocketPath,
321
+ getTelegramBusFollowerSocketPath(),
309
322
  ),
310
323
  followerRegistered: telegramBusFollowerRegistrationState.isRegistered(),
311
324
  followerTarget: telegramBusFollowerRegistrationState.getTarget(),
@@ -412,7 +425,7 @@ export default function (pi: Pi.ExtensionAPI) {
412
425
  },
413
426
  getDefaultTarget: proactivePushTargetGetter,
414
427
  callFollowerApi: BusFollower.createTelegramBusFollowerApiCaller({
415
- socketPath: telegramBusSocketPath,
428
+ socketPath: getTelegramBusSocketPath,
416
429
  instanceId: telegramInstanceId,
417
430
  createRequestId() {
418
431
  telegramBusRequestSequence += 1;
@@ -641,7 +654,7 @@ export default function (pi: Pi.ExtensionAPI) {
641
654
  Routing.TelegramRoutedCallbackQuery,
642
655
  Routing.TelegramRoutedMessage
643
656
  >({
644
- socketPath: telegramBusSocketPath,
657
+ socketPath: getTelegramBusSocketPath,
645
658
  createRequestId() {
646
659
  telegramBusRequestSequence += 1;
647
660
  return Bus.createTelegramBusRequestId({
@@ -656,7 +669,7 @@ export default function (pi: Pi.ExtensionAPI) {
656
669
  });
657
670
  const followerTargetController =
658
671
  Bus.createTelegramBusFollowerTargetController({
659
- socketPath: telegramBusSocketPath,
672
+ socketPath: getTelegramBusSocketPath,
660
673
  createRequestId() {
661
674
  telegramBusRequestSequence += 1;
662
675
  return Bus.createTelegramBusRequestId({
@@ -811,7 +824,7 @@ export default function (pi: Pi.ExtensionAPI) {
811
824
  Routing.TelegramRoutedCallbackQuery,
812
825
  Routing.TelegramRoutedMessage
813
826
  >({
814
- socketPath: telegramBusFollowerSocketPath,
827
+ socketPath: getTelegramBusFollowerSocketPath,
815
828
  instanceId: telegramInstanceId,
816
829
  getContext() {
817
830
  return telegramSessionContextStore.get();
@@ -910,7 +923,8 @@ export default function (pi: Pi.ExtensionAPI) {
910
923
  BusFollower.createTelegramBusFollowerRegistrationRuntime<Pi.ExtensionContext>(
911
924
  {
912
925
  instanceId: telegramInstanceId,
913
- followerBusSocketPath: telegramBusFollowerSocketPath,
926
+ getFollowerBusSocketPath: getTelegramBusFollowerSocketPath,
927
+ getLeaderSocketPath: getTelegramBusSocketPath,
914
928
  startReceiving: telegramBusForwardedUpdateReceiver.start,
915
929
  stopReceiving: telegramBusForwardedUpdateReceiver.stop,
916
930
  registrationState: telegramBusFollowerRegistrationState,
@@ -967,7 +981,7 @@ export default function (pi: Pi.ExtensionAPI) {
967
981
  });
968
982
  };
969
983
  const telegramBusLeaderRuntime = BusLeader.createTelegramBusLeaderRuntime({
970
- socketPath: telegramBusSocketPath,
984
+ socketPath: getTelegramBusSocketPath,
971
985
  followerRegistry: telegramBusFollowerRegistry,
972
986
  authSecret: telegramBusAuthSecret,
973
987
  startPolling: pollingRuntime.start,
@@ -16,8 +16,10 @@ import {
16
16
  createTelegramBusLocalServer,
17
17
  createUnauthorizedBusAck,
18
18
  getTelegramBusSocketPath,
19
+ resolveTelegramBusSocketPath,
19
20
  sendTelegramBusLocalEnvelope,
20
21
  type TelegramBusEnvelope,
22
+ type TelegramBusSocketPathSource,
21
23
  } from "./bus.ts";
22
24
  import {
23
25
  getTelegramBusTransportRetryPolicy,
@@ -133,7 +135,7 @@ export interface TelegramBusForwardedUpdateReceiverRuntime {
133
135
  }
134
136
 
135
137
  export interface TelegramBusFollowerApiCallerDeps {
136
- socketPath: string;
138
+ socketPath: TelegramBusSocketPathSource;
137
139
  instanceId: string;
138
140
  createRequestId: () => string;
139
141
  getAuthSecret?: () => string | undefined;
@@ -149,6 +151,7 @@ export interface TelegramBusFollowerRegistrationRuntimeDeps<
149
151
  getLeaderAuthSecret?: (leader: { busSecret?: string }) => string | undefined;
150
152
  setActiveAuthSecret?: (secret: string | undefined) => void;
151
153
  followerBusSocketPath?: string;
154
+ getFollowerBusSocketPath?: () => string;
152
155
  getLeaderSocketPath?: () => string;
153
156
  startReceiving?: () => Promise<void>;
154
157
  stopReceiving?: () => Promise<void> | void;
@@ -238,7 +241,7 @@ export interface TelegramBusForwardedUpdateReceiverRuntimeDeps<
238
241
  TCallbackQuery,
239
242
  TMessage = unknown,
240
243
  > {
241
- socketPath: string;
244
+ socketPath: TelegramBusSocketPathSource;
242
245
  instanceId: string;
243
246
  getAuthSecret?: () => string | undefined;
244
247
  getContext: () => TContext | undefined;
@@ -354,11 +357,12 @@ export function createTelegramBusFollowerApiCaller(
354
357
  const getNowMs = deps.getNowMs ?? Date.now;
355
358
  const timeoutMs = deps.timeoutMs ?? 30000;
356
359
  return async (method, args) => {
360
+ const socketPath = resolveTelegramBusSocketPath(deps.socketPath);
357
361
  const response = await sendTelegramBusLocalEnvelope({
358
- socketPath: deps.socketPath,
362
+ socketPath,
359
363
  timeoutMs,
360
364
  retry: getTelegramBusTransportRetryPolicy({
361
- endpoint: deps.socketPath,
365
+ endpoint: socketPath,
362
366
  operation: "operation",
363
367
  }),
364
368
  envelope: {
@@ -725,7 +729,8 @@ export function createTelegramBusFollowerRegistrationRuntime<
725
729
  (ctx.cwd ? basename(ctx.cwd) : undefined),
726
730
  cwd: ctx.cwd,
727
731
  pid: getPid(),
728
- busSocketPath: deps.followerBusSocketPath,
732
+ busSocketPath:
733
+ deps.getFollowerBusSocketPath?.() ?? deps.followerBusSocketPath,
729
734
  connectedAtMs: getNowMs(),
730
735
  },
731
736
  });
package/lib/bus-leader.ts CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  type TelegramBusFollowerRegistry,
21
21
  type TelegramBusFollowerView,
22
22
  type TelegramBusInstanceRegistration,
23
+ type TelegramBusSocketPathSource,
23
24
  } from "./bus.ts";
24
25
  import { getTelegramBusTransportRetryPolicy } from "./bus-transport.ts";
25
26
 
@@ -176,7 +177,7 @@ export type TelegramBusFollowerMessageOwnershipRecorder = (
176
177
  ) => void;
177
178
 
178
179
  export interface TelegramBusLeaderRuntimeDeps<TContext> {
179
- socketPath: string;
180
+ socketPath: TelegramBusSocketPathSource;
180
181
  followerRegistry: TelegramBusFollowerRegistry;
181
182
  authSecret?: string;
182
183
  startPolling: (ctx: TContext) => void | Promise<void>;
@@ -99,31 +99,55 @@ export function getTelegramBusTransportRetryPolicy(input: {
99
99
  };
100
100
  }
101
101
 
102
+ function normalizeTelegramBusEndpointScope(value: string): string {
103
+ return value.replace(/[^a-zA-Z0-9_.-]/g, "_").slice(0, 80);
104
+ }
105
+
102
106
  export function getTelegramBusLeaderEndpoint(input: {
103
107
  agentDir: string;
104
108
  platform: NodeJS.Platform | string;
109
+ profileName?: string;
105
110
  }): string {
111
+ const profileScope = input.profileName
112
+ ? normalizeTelegramBusEndpointScope(input.profileName)
113
+ : undefined;
106
114
  return input.platform === "win32"
107
- ? getTelegramBusPipePath({ agentDir: input.agentDir, scope: "bus" })
108
- : join(input.agentDir, "tmp", "telegram", "bus.sock");
115
+ ? getTelegramBusPipePath({
116
+ agentDir: input.agentDir,
117
+ scope: profileScope ? `bus-${profileScope}` : "bus",
118
+ })
119
+ : join(
120
+ input.agentDir,
121
+ "tmp",
122
+ "telegram",
123
+ profileScope ? `bus.${profileScope}.sock` : "bus.sock",
124
+ );
109
125
  }
110
126
 
111
127
  export function getTelegramBusFollowerEndpoint(input: {
112
128
  agentDir: string;
113
129
  platform: NodeJS.Platform | string;
114
130
  instanceId: string;
131
+ profileName?: string;
115
132
  }): string {
133
+ const instanceScope = normalizeTelegramBusEndpointScope(input.instanceId);
134
+ const profileScope = input.profileName
135
+ ? normalizeTelegramBusEndpointScope(input.profileName)
136
+ : undefined;
116
137
  return input.platform === "win32"
117
138
  ? getTelegramBusPipePath({
118
139
  agentDir: input.agentDir,
119
- scope: `follower-${input.instanceId}`,
140
+ scope: profileScope
141
+ ? `follower-${profileScope}-${instanceScope}`
142
+ : `follower-${instanceScope}`,
120
143
  })
121
144
  : join(
122
145
  input.agentDir,
123
146
  "tmp",
124
147
  "telegram",
125
148
  "followers",
126
- `${input.instanceId.replace(/[^a-zA-Z0-9_.-]/g, "_")}.sock`,
149
+ ...(profileScope ? [profileScope] : []),
150
+ `${instanceScope}.sock`,
127
151
  );
128
152
  }
129
153
 
package/lib/bus.ts CHANGED
@@ -41,16 +41,23 @@ export function createTelegramBusAuthSecret(): string {
41
41
  export function getTelegramBusSocketPath(
42
42
  agentDir = resolveAgentDir(),
43
43
  platform = getPlatform(),
44
+ profileName?: string,
44
45
  ): string {
45
- return getTelegramBusLeaderEndpoint({ agentDir, platform });
46
+ return getTelegramBusLeaderEndpoint({ agentDir, platform, profileName });
46
47
  }
47
48
 
48
49
  export function getTelegramBusFollowerSocketPath(
49
50
  instanceId: string,
50
51
  agentDir = resolveAgentDir(),
51
52
  platform = getPlatform(),
53
+ profileName?: string,
52
54
  ): string {
53
- return getTelegramBusFollowerEndpoint({ agentDir, platform, instanceId });
55
+ return getTelegramBusFollowerEndpoint({
56
+ agentDir,
57
+ platform,
58
+ instanceId,
59
+ profileName,
60
+ });
54
61
  }
55
62
 
56
63
  export interface TelegramBusInstanceRegistration {
@@ -344,8 +351,16 @@ export interface TelegramBusLocalServer {
344
351
  stop: () => Promise<void>;
345
352
  }
346
353
 
354
+ export type TelegramBusSocketPathSource = string | (() => string);
355
+
356
+ export function resolveTelegramBusSocketPath(
357
+ source: TelegramBusSocketPathSource,
358
+ ): string {
359
+ return typeof source === "function" ? source() : source;
360
+ }
361
+
347
362
  export interface TelegramBusLocalServerDeps {
348
- socketPath: string;
363
+ socketPath: TelegramBusSocketPathSource;
349
364
  handleEnvelope: (
350
365
  envelope: TelegramBusEnvelope,
351
366
  ) =>
@@ -362,7 +377,7 @@ export interface TelegramBusLocalClientOptions {
362
377
  }
363
378
 
364
379
  export interface TelegramBusForeignOwnedForwarderDeps {
365
- socketPath: string;
380
+ socketPath: TelegramBusSocketPathSource;
366
381
  createRequestId: () => string;
367
382
  getNowMs?: () => number;
368
383
  timeoutMs?: number;
@@ -401,12 +416,13 @@ export function createTelegramBusForeignOwnedUpdateForwarder<
401
416
  const getNowMs = deps.getNowMs ?? Date.now;
402
417
  const send = async (envelope: TelegramBusEnvelope): Promise<boolean> => {
403
418
  if (deps.getAuthSecret) envelope.auth = deps.getAuthSecret();
419
+ const socketPath = resolveTelegramBusSocketPath(deps.socketPath);
404
420
  const response = await sendTelegramBusLocalEnvelope({
405
- socketPath: deps.socketPath,
421
+ socketPath,
406
422
  envelope,
407
423
  timeoutMs: deps.timeoutMs,
408
424
  retry: getTelegramBusTransportRetryPolicy({
409
- endpoint: deps.socketPath,
425
+ endpoint: socketPath,
410
426
  operation: "operation",
411
427
  }),
412
428
  });
@@ -558,6 +574,7 @@ export function createTelegramBusLocalServer(
558
574
  deps: TelegramBusLocalServerDeps,
559
575
  ): TelegramBusLocalServer {
560
576
  let server: Server | undefined;
577
+ let activeSocketPath: string | undefined;
561
578
  const sockets = new Set<Socket>();
562
579
  const closeSocket = (socket: Socket) => {
563
580
  sockets.delete(socket);
@@ -566,16 +583,18 @@ export function createTelegramBusLocalServer(
566
583
  return {
567
584
  start: async () => {
568
585
  if (server) return;
569
- const usesWindowsPipe = isTelegramBusPipePath(deps.socketPath);
586
+ const socketPath = resolveTelegramBusSocketPath(deps.socketPath);
587
+ activeSocketPath = socketPath;
588
+ const usesWindowsPipe = isTelegramBusPipePath(socketPath);
570
589
  deps.recordTransportEvent?.(
571
590
  "server-start",
572
- getTelegramBusEndpointDiagnostics(deps.socketPath),
591
+ getTelegramBusEndpointDiagnostics(socketPath),
573
592
  );
574
593
  if (!usesWindowsPipe) {
575
- const socketDir = dirname(deps.socketPath);
594
+ const socketDir = dirname(socketPath);
576
595
  mkdirSync(socketDir, { recursive: true, mode: 0o700 });
577
596
  chmodSync(socketDir, 0o700);
578
- if (existsSync(deps.socketPath)) unlinkSync(deps.socketPath);
597
+ if (existsSync(socketPath)) unlinkSync(socketPath);
579
598
  }
580
599
  server = createServer((socket) => {
581
600
  sockets.add(socket);
@@ -591,14 +610,14 @@ export function createTelegramBusLocalServer(
591
610
  socket,
592
611
  deps.handleEnvelope,
593
612
  deps.recordTransportEvent,
594
- deps.socketPath,
613
+ socketPath,
595
614
  );
596
615
  }
597
616
  });
598
617
  socket.on("close", () => sockets.delete(socket));
599
618
  socket.on("error", (error) => {
600
619
  deps.recordTransportEvent?.("server-socket-error", {
601
- ...getTelegramBusEndpointDiagnostics(deps.socketPath),
620
+ ...getTelegramBusEndpointDiagnostics(socketPath),
602
621
  ...classifyTelegramBusTransportError(error),
603
622
  });
604
623
  closeSocket(socket);
@@ -607,24 +626,28 @@ export function createTelegramBusLocalServer(
607
626
  try {
608
627
  await new Promise<void>((resolve, reject) => {
609
628
  server?.once("error", reject);
610
- server?.listen(deps.socketPath, resolve);
629
+ server?.listen(socketPath, resolve);
611
630
  });
612
631
  deps.recordTransportEvent?.(
613
632
  "server-started",
614
- getTelegramBusEndpointDiagnostics(deps.socketPath),
633
+ getTelegramBusEndpointDiagnostics(socketPath),
615
634
  );
616
635
  } catch (error) {
636
+ server = undefined;
637
+ activeSocketPath = undefined;
617
638
  deps.recordTransportEvent?.("server-start-failed", {
618
- ...getTelegramBusEndpointDiagnostics(deps.socketPath),
639
+ ...getTelegramBusEndpointDiagnostics(socketPath),
619
640
  ...classifyTelegramBusTransportError(error),
620
641
  });
621
642
  throw error;
622
643
  }
623
- if (!usesWindowsPipe) chmodSync(deps.socketPath, 0o600);
644
+ if (!usesWindowsPipe) chmodSync(socketPath, 0o600);
624
645
  },
625
646
  stop: async () => {
626
647
  const activeServer = server;
648
+ const socketPath = activeSocketPath;
627
649
  server = undefined;
650
+ activeSocketPath = undefined;
628
651
  for (const socket of sockets) closeSocket(socket);
629
652
  if (activeServer) {
630
653
  await new Promise<void>((resolve) =>
@@ -632,15 +655,18 @@ export function createTelegramBusLocalServer(
632
655
  );
633
656
  }
634
657
  if (
635
- !isTelegramBusPipePath(deps.socketPath) &&
636
- existsSync(deps.socketPath)
658
+ socketPath &&
659
+ !isTelegramBusPipePath(socketPath) &&
660
+ existsSync(socketPath)
637
661
  ) {
638
- unlinkSync(deps.socketPath);
662
+ unlinkSync(socketPath);
663
+ }
664
+ if (socketPath) {
665
+ deps.recordTransportEvent?.(
666
+ "server-stopped",
667
+ getTelegramBusEndpointDiagnostics(socketPath),
668
+ );
639
669
  }
640
- deps.recordTransportEvent?.(
641
- "server-stopped",
642
- getTelegramBusEndpointDiagnostics(deps.socketPath),
643
- );
644
670
  },
645
671
  };
646
672
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.20.0",
3
+ "version": "0.20.1",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"