@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 +1 -0
- package/BACKLOG.md +5 -17
- package/CHANGELOG.md +6 -0
- package/README.md +61 -58
- package/docs/README.md +2 -2
- package/docs/architecture.md +3 -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/index.ts +28 -14
- package/lib/bus-follower.ts +10 -5
- package/lib/bus-leader.ts +2 -1
- package/lib/bus-transport.ts +28 -4
- package/lib/bus.ts +49 -23
- package/package.json +1 -1
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:
|
|
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:
|
|
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
|
-
- [ ]
|
|
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:
|
|
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
|
|
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
|
|
76
|
-
|
|
|
77
|
-
| Operator companion
|
|
78
|
-
| Runtime adapter
|
|
79
|
-
| Telegram UI harness
|
|
80
|
-
| Multi-instance organism | One leader plus explicit visible followers routed through Telegram private-chat threads
|
|
81
|
-
| Extension platform
|
|
82
|
-
| Safety boundary
|
|
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
|
|
89
|
-
|
|
|
90
|
-
| Prompt intake
|
|
91
|
-
| Queue control
|
|
92
|
-
| Operator menu
|
|
93
|
-
| Prompt templates
|
|
94
|
-
| Model and thinking
|
|
95
|
-
| Compaction
|
|
96
|
-
| Draft previews
|
|
97
|
-
| Assistant rendering
|
|
98
|
-
| Bridge UI rendering
|
|
99
|
-
| Inbound files
|
|
100
|
-
| Outbound artifacts
|
|
101
|
-
| Voice input
|
|
102
|
-
| Voice output
|
|
103
|
-
| Buttons
|
|
104
|
-
| Callback routing
|
|
105
|
-
| Threaded Mode
|
|
106
|
-
| Reroute and restore
|
|
107
|
-
| Extension sections
|
|
108
|
-
| Runtime diagnostics
|
|
109
|
-
| Safety and ownership | Pair one owner, lock transport, scope targets, and reject fake terminal behavior.
|
|
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
|
|
130
|
-
|
|
|
131
|
-
| `/start`
|
|
132
|
-
| `/compact`
|
|
133
|
-
| `/next`
|
|
134
|
-
| `/continue` | Enqueue a priority continuation prompt
|
|
135
|
-
| `/abort`
|
|
136
|
-
| `/stop`
|
|
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
|
|
145
|
-
|
|
|
146
|
-
| `/telegram-setup`
|
|
147
|
-
| `/telegram-
|
|
148
|
-
| `/telegram-
|
|
149
|
-
| `/telegram-
|
|
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
|
|
188
|
-
|
|
|
189
|
-
| Classic DM
|
|
190
|
-
| Threaded Mode | Several visible Pi terminals sharing one bot
|
|
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
|
|
197
|
-
|
|
|
198
|
-
| Bot token bootstrap
|
|
199
|
-
| HTTP proxy
|
|
200
|
-
| Telegram network family
|
|
201
|
-
| Agent data root
|
|
202
|
-
| Inbound file limit
|
|
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
|
package/docs/architecture.md
CHANGED
|
@@ -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,
|
|
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
|
|
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/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
|
|
81
|
-
|
|
82
|
-
|
|
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:
|
|
304
|
-
leaderTransport:
|
|
305
|
-
|
|
306
|
-
|
|
315
|
+
leaderSocketPath: getTelegramBusSocketPath(),
|
|
316
|
+
leaderTransport: BusTransport.getTelegramBusTransportKind(
|
|
317
|
+
getTelegramBusSocketPath(),
|
|
318
|
+
),
|
|
319
|
+
followerSocketPath: getTelegramBusFollowerSocketPath(),
|
|
307
320
|
followerTransport: BusTransport.getTelegramBusTransportKind(
|
|
308
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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:
|
|
984
|
+
socketPath: getTelegramBusSocketPath,
|
|
971
985
|
followerRegistry: telegramBusFollowerRegistry,
|
|
972
986
|
authSecret: telegramBusAuthSecret,
|
|
973
987
|
startPolling: pollingRuntime.start,
|
package/lib/bus-follower.ts
CHANGED
|
@@ -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:
|
|
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:
|
|
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
|
|
362
|
+
socketPath,
|
|
359
363
|
timeoutMs,
|
|
360
364
|
retry: getTelegramBusTransportRetryPolicy({
|
|
361
|
-
endpoint:
|
|
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:
|
|
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:
|
|
180
|
+
socketPath: TelegramBusSocketPathSource;
|
|
180
181
|
followerRegistry: TelegramBusFollowerRegistry;
|
|
181
182
|
authSecret?: string;
|
|
182
183
|
startPolling: (ctx: TContext) => void | Promise<void>;
|
package/lib/bus-transport.ts
CHANGED
|
@@ -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({
|
|
108
|
-
|
|
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:
|
|
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
|
-
|
|
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({
|
|
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:
|
|
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:
|
|
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
|
|
421
|
+
socketPath,
|
|
406
422
|
envelope,
|
|
407
423
|
timeoutMs: deps.timeoutMs,
|
|
408
424
|
retry: getTelegramBusTransportRetryPolicy({
|
|
409
|
-
endpoint:
|
|
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
|
|
586
|
+
const socketPath = resolveTelegramBusSocketPath(deps.socketPath);
|
|
587
|
+
activeSocketPath = socketPath;
|
|
588
|
+
const usesWindowsPipe = isTelegramBusPipePath(socketPath);
|
|
570
589
|
deps.recordTransportEvent?.(
|
|
571
590
|
"server-start",
|
|
572
|
-
getTelegramBusEndpointDiagnostics(
|
|
591
|
+
getTelegramBusEndpointDiagnostics(socketPath),
|
|
573
592
|
);
|
|
574
593
|
if (!usesWindowsPipe) {
|
|
575
|
-
const socketDir = dirname(
|
|
594
|
+
const socketDir = dirname(socketPath);
|
|
576
595
|
mkdirSync(socketDir, { recursive: true, mode: 0o700 });
|
|
577
596
|
chmodSync(socketDir, 0o700);
|
|
578
|
-
if (existsSync(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
629
|
+
server?.listen(socketPath, resolve);
|
|
611
630
|
});
|
|
612
631
|
deps.recordTransportEvent?.(
|
|
613
632
|
"server-started",
|
|
614
|
-
getTelegramBusEndpointDiagnostics(
|
|
633
|
+
getTelegramBusEndpointDiagnostics(socketPath),
|
|
615
634
|
);
|
|
616
635
|
} catch (error) {
|
|
636
|
+
server = undefined;
|
|
637
|
+
activeSocketPath = undefined;
|
|
617
638
|
deps.recordTransportEvent?.("server-start-failed", {
|
|
618
|
-
...getTelegramBusEndpointDiagnostics(
|
|
639
|
+
...getTelegramBusEndpointDiagnostics(socketPath),
|
|
619
640
|
...classifyTelegramBusTransportError(error),
|
|
620
641
|
});
|
|
621
642
|
throw error;
|
|
622
643
|
}
|
|
623
|
-
if (!usesWindowsPipe) chmodSync(
|
|
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
|
-
|
|
636
|
-
|
|
658
|
+
socketPath &&
|
|
659
|
+
!isTelegramBusPipePath(socketPath) &&
|
|
660
|
+
existsSync(socketPath)
|
|
637
661
|
) {
|
|
638
|
-
unlinkSync(
|
|
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
|
}
|