@llblab/pi-telegram 0.18.4 → 0.18.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -74,7 +74,7 @@ The core product loop is mobile continuation: start or supervise work in the ter
74
74
  - The bridge is session-local, paired to one allowed Telegram user, and owns a local queue aligned with Pi lifecycle hooks
75
75
  - The Telegram lock owns only external control/polling. Local extension state and queue runtime are per Pi instance: losing `/telegram-connect` ownership stops live Telegram control here, but must not clear, silence, or stop active/queued preview, final delivery, or dispatch in this instance. Bus leader lock entries carry a stable `leaderEpoch` minted on acquisition and preserved across heartbeat refreshes; leader-owned Thread Reconciler cleanup/provisioning actions stamp that epoch and skip destructive side effects if current leadership differs.
76
76
  - The Telegram lock is always the transport ownership lock. In classic/private-chat mode, transport ownership is the whole bridge ownership and `/telegram-connect` from another live Pi should offer the classic takeover confirmation. In Threaded Mode, transport ownership is the bus leader role and `/telegram-connect` from another live Pi should register as follower instead of offering takeover while the leader is live. If follower registration is not applicable because Threaded Mode is disabled or unavailable, fall back to the classic takeover path.
77
- - Telegram private-chat Threaded Mode is the public switch for the local multi-instance bus. There is no separate public `telegram.json` bus switch: classic/private-chat singleton DM behavior is the base product mode, and when Telegram reports Threaded Mode available for the bot the bridge enables leader/follower bus behavior. If Telegram reports Threaded Mode unavailable or thread operations prove it unavailable, the bridge returns to classic singleton behavior as a first-class mode. Do not inject slot/thread rename guidance in classic mode. Blocked instances should not silently register as followers unless the effective runtime mode is Threaded Mode.
77
+ - Telegram private-chat Threaded Mode is the public switch for the local multi-instance bus. There is no separate public `telegram.json` bus switch: classic/private-chat singleton DM behavior is the base product mode, and when Telegram reports Threaded Mode available for the bot the bridge enables leader/follower bus behavior. If Telegram reports Threaded Mode unavailable or thread operations prove it unavailable, the bridge returns to classic singleton behavior as a first-class mode. During a live Threaded Mode downgrade, the current transport leader keeps the singleton classic polling role and followers disconnect instead of taking over or offering takeover while active thread bindings prove this is a downgrade from a live bus organism. Do not inject slot/thread rename guidance in classic mode. Blocked instances should not silently register as followers unless the effective runtime mode is Threaded Mode.
78
78
  - In Threaded Mode, exactly one live leader owns `getUpdates` and direct Telegram Bot API calls for one bot token. `/telegram-connect` means join the local Telegram organism: if no live leader exists the instance becomes leader; if a live leader exists the terminal-visible instance registers as follower; ordinary connect must not offer manual takeover while the leader heartbeat is live. Leadership is an ephemeral transport role, not permanent ownership by the first terminal-visible Pi instance. Followers must be started by the operator as real Pi processes, register over the local bus, heartbeat, receive only their routed inbound updates, and send outbound replies/previews/files/chat-actions through the leader transport; Telegram must not spawn hidden Pi processes, launch follower subprocesses, or expose a slash command that creates a new local Pi instance. If a leader reloads while followers stay alive, followers must treat rejected/unknown/unauthorized heartbeats as a registration-loss signal and optimistically re-register with the currently active leader before considering promotion; after a short grace window without successful re-registration, a live follower may promote itself and force-acquire transport leadership so the organism degrades forward instead of staying split-brain. Terminal status must not remain `follower` while the leader registry can no longer route that follower's thread. Heartbeat pruning is liveness bookkeeping, not permission to delete, mark offline, or send a Telegram-visible disconnected notice for a follower's thread; preserve active follower tabs/bindings across transient reload/reconnect incidents and reserve destructive teardown/offline transitions for explicit disconnect or confirmed reconciliation cases. If the leader dies, a live follower should eventually be able to take over through stale-leader election/recovery so the local multi-instance organism keeps running. Explicit `/telegram-disconnect` is a manual thread teardown: a leader deletes its own bound thread before releasing transport ownership, while a follower asks the leader to delete its assigned thread through scoped bus API before unregistering. Status is reactive to the effective runtime role and bot capability: classic/non-thread mode shows ordinary `telegram connected`, active bus leadership shows `telegram leader`, and registered followers show `telegram follower`; stable multi-instance truth belongs in the bus role (`leader`/`follower`), while lifecycle is reserved for transient recovery state such as `electing` and must not duplicate stable roles with labels like `leader-active`. When leadership or Threaded Mode availability changes at runtime, the status line and `/telegram-status` must converge promptly instead of preserving a stale role label.
79
79
  - Runtime routing binds to a live `instanceId`; current thread identity should use explicit owner records (`leader`, `manual-follower`, `pending-topic`) rather than hiding ownership direction in legacy string keys. Compatibility keys may be derived internally, but stale session records must not be treated as live owners. A live instance owns at most one active bound thread; provisioning a new target for the same `instanceId` must remove older current-state bindings and close duplicate Telegram threads when possible. A reload creates a new runtime `instanceId`; if the previous leader slot is still known/alive, keep it occupied in reservations and assign the new leader the next free slot instead of creating a duplicate same-letter tab or blocking startup on Telegram UI convergence.
80
80
  - Follower registration readiness should stay on the smallest critical path that makes the follower routable: create or reuse the Telegram target, persist the active binding, mark target-bindings fresh, and return the target to the follower. Telegram-visible connected notices, duplicate/replaced thread cleanup, and other reconciliation work are important but non-critical; run them after registration and record slow/failing background steps as runtime events rather than delaying follower usability.
@@ -126,6 +126,7 @@ The core product loop is mobile continuation: start or supervise work in the ter
126
126
  - Keep globally reusable rules as independent bullets in the most general applicable section of `AGENTS.md`; do not hide a cross-cutting rule inside one local feature paragraph just because that feature revealed it.
127
127
  - 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.
128
128
  - Pi-facing tool results shown in compact tool rows should start with exactly one leading newline (`\nResult`) so the result is visually separated from the tool-call header by one blank line without excessive vertical spacing.
129
+ - 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.
129
130
 
130
131
  ## 6.2 Validation Hotspots
131
132
 
@@ -144,6 +145,7 @@ The core product loop is mobile continuation: start or supervise work in the ter
144
145
  - Name extracted `/lib` modules and mirrored `/tests` suites by bare domain when the repository already supplies the Telegram scope; prefer `queue.ts`, `updates.ts`, and `queue.test.ts` over redundant `telegram-*` filename prefixes. Exception: the concrete Bot API transport domain is named `telegram-api.ts` / `telegram-api.test.ts` to avoid ambiguity with the public `/api/*.ts` package membranes
145
146
  - Keep test helpers with the mirrored domain suite by default because test files mirror module-domain boundaries; introduce shared `tests/fixtures` only when multiple domain suites truly reuse the same setup. A `tests/<domain>.test.ts` file should primarily protect `lib/<domain>.ts`; this includes `tests/runtime.test.ts`, which should protect `lib/runtime.ts` rather than act as a general integration bucket. Cross-domain runtime flows belong in the single shared `tests/integration.test.ts` suite. Do not create narrow `*.integration.test.ts` files unless a future repeated setup boundary becomes large enough to justify splitting the one shared integration suite
146
147
  - Prefer targeted edits, keeping `index.ts` as the orchestration layer and moving reusable logic into flat `/lib` domain modules when a subsystem becomes large enough to earn extraction
148
+ - Do not atomize cohesive domains just because a file is large. A 1k+ line module can remain healthy when it owns one coherent mechanism and its subparts change together; extract only when there is real pressure to isolate, substitute, independently test, independently mutate, break cycles, or reduce repeated cross-domain coupling.
147
149
  - Keep composition wiring DRY with small local adapters or owning-domain contracts when repetition appears, but do not hide live mutable session state behind broad facades just to reduce repeated closures
148
150
  - Keep interface contracts consistent for the same runtime entity: prefer the owning domain's exported contract when multiple modules mean the same entity, and use local structural `*Like`/view contracts only for deliberate narrow projections that avoid real coupling without duplicating source-of-truth shapes
149
151
 
@@ -161,8 +163,9 @@ The canonical detailed ownership map lives in [`docs/architecture.md`](./docs/ar
161
163
  ## 6.5 Entrypoint And Import Boundaries
162
164
 
163
165
  - Keep the preview domain as a thin streaming lifecycle controller only: draft ids, safe-prefix selection for `sendRichMessageDraft`, voice suppression, serialized flushes, diagnostics, and finalization state. Do not reintroduce assistant preview rendering there; keep `rendering.ts` scoped to bridge-owned UI/compat regular-message rendering rather than assistant or guest Markdown delivery. Drafts must only send structurally closed Markdown prefixes; draft failures are not proof that drafts are globally unsupported, so record the failure and skip that preview frame. Do not add raw plain-message fallback previews for assistant Markdown
164
- - Preview/final delivery ordering is release-critical: finalization must wait for active preview flushes, persisted final delivery should not be followed by a post-final draft-clear call that creates transient draft UI, and regressions should cover in-flight draft flush serialization plus final reply ordering
165
- - Live Rich Draft observation: `sendMessageDraft(..., undefined)` after a persisted final Rich Message can appear in Telegram clients as a separate animated three-dot draft block before dissolving. Do not use post-final draft-clear for assistant finalization; let the persisted final `sendRichMessage` replace/complete the user-visible lifecycle and reset local preview state only.
166
+ - Preview/final delivery ordering is release-critical: finalization waits for active preview flushes, persisted final delivery should not be followed by a post-final draft-clear call that creates transient draft UI, and regressions should cover in-flight draft flush serialization plus final reply ordering.
167
+ - Live Rich Draft observation: `sendMessageDraft(..., undefined)` after a persisted final Rich Message can appear in Telegram clients as a separate animated three-dot draft block before dissolving. Do not use post-final draft-clear or pre-final draft-clear for assistant finalization; let the persisted final `sendRichMessage` replace/complete the user-visible lifecycle and reset local preview state only.
168
+ - Remove unused code immediately when discovered. Do not keep dormant config knobs, disabled policy branches, speculative fallback paths, or "maybe later" implementation hooks in the product path; reintroduce them only with an active caller, tests, and live evidence.
166
169
  - Keep Telegram prompt guidance compact and operational. Do not add format-specific steering for native Rich Markdown features unless the model needs a real bridge-specific rule such as formula delimiters or hidden outbound action syntax.
167
170
  - Keep direct `node:*` file-operation dependencies out of `index.ts` when an owning domain exists; the entrypoint should compose ports while domains own local filesystem details such as temp-dir preparation, attachment stats, and turn image reads
168
171
  - In `index.ts`, prefer namespace imports for local bridge domains so orchestration reads as domain-scoped calls such as `Queue.*`, `Turns.*`, and `Rendering.*` instead of long flat import lists
@@ -219,14 +222,14 @@ The canonical detailed ownership map lives in [`docs/architecture.md`](./docs/ar
219
222
  - `Section domain ownership`: `lib/sections.ts` owns the registry, token mapping, callback dispatch, and context building. `lib/menu.ts` dispatches `section:` callbacks before built-in handling. `lib/menu-status.ts` injects section rows. `lib/menu-settings.ts` injects settings rows and passes `sectionRegistry` through callback deps
220
223
  - `Callback routing order`: button actions → compact confirmations → queue menu → settings menu → section callbacks → built-in menu handling → `[callback]` fallback. Settings menu callbacks always pass `sectionRegistry` to `updateTelegramSettingsMenuMessage` and `handleTelegramSettingsMenuCallbackAction`
221
224
 
222
- ## 9. Pre-Task Preparation Protocol
225
+ ## 10. Pre-Task Preparation Protocol
223
226
 
224
227
  - Read `README.md` for current user-facing behavior and fork positioning
225
228
  - Read `BACKLOG.md` before changing runtime behavior or documentation so open work stays truthful
226
229
  - Read `/docs/architecture.md` before restructuring queue, preview, rendering, or command-handling logic
227
230
  - Inspect the relevant `index.ts` section before editing because most bridge behavior is stateful and cross-linked
228
231
 
229
- ## 10. Task Completion Protocol
232
+ ## 11. Task Completion Protocol
230
233
 
231
234
  - Run the smallest meaningful validation for the touched area; `npm test` is the default regression suite once rendering or queue logic changes
232
235
  - For rendering changes, ensure regressions still cover nested lists, code blocks, underscore-heavy text, and long-message chunking
package/BACKLOG.md CHANGED
@@ -1,48 +1,27 @@
1
1
  # Project Backlog
2
2
 
3
- _Current deterministic status: Threaded Mode implementation, native typing/activity status, regression coverage, docs/context reconciliation, typecheck, full tests, pack check, audit, Domain DAG validation, context validation, and core live Threaded Mode smoke are green. This backlog intentionally tracks only release-relevant remaining work: native Windows smoke, evidence-gated Telegram client follow-ups, and upstream Pi API blockers._
3
+ _Current deterministic status: Threaded Mode implementation, native typing/activity status, regression coverage, docs/context reconciliation, typecheck, full tests, pack check, audit, Domain DAG validation, context validation, native Windows classic↔Threaded Mode upgrade/downgrade smoke, and live post-reload leader/follower prompt routing, follower Active parity, and unbound reroute/restore smoke are green. This backlog intentionally tracks only release-relevant remaining work: evidence-gated Telegram client/runtime follow-ups and upstream Pi API blockers._
4
4
 
5
- ## P0 — Live Threaded Mode Regression Sweep
5
+ ## P0 — Promoted Follower Reload Smoke
6
6
 
7
- Context: live Linux testing exposed regressions around prompt dispatch readiness, visible thread rename noise during automatic leader reclaim, status thread-name fallback flicker, and follower voice/update forwarding health. These are local runtime correctness issues and must be validated before returning to Windows smoke.
7
+ 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.
8
8
 
9
9
  Open work:
10
10
 
11
- - [x] Hide minimal model-menu one-page pagination and keep scope tabs progressive.
12
- - [x] Add immediate-plus-deferred inbound prompt dispatch so queued prompts do not wait for a later `/reload` or command.
13
- - [x] Stop automatic leader reclaim/reconciliation paths from visibly calling `editForumTopic` for internal identity restoration.
14
- - [x] Prevent leader auto-claim of an unknown unbound thread while another live thread target exists, covering same-directory leader/follower smear risk.
15
- - [x] Prefer local live leader/follower target labels over stale shared thread-store records when building prompt prefixes.
16
- - [x] Permit follower-safe bot identity reads and own-chat native activity through the leader API proxy without granting cross-thread message/file/topic writes.
17
- - [x] Preserve leader thread-name fallback in live status state to reduce `Dune`/generic `Telegram` flicker.
18
- - [ ] Live smoke on Linux with one leader and one follower:
19
- - [x] clean-state pass after removing `tmp/telegram` so stale diagnostic snapshots do not obscure live behavior;
20
- - [ ] dirty-state pass with old `state.json`/`logs.jsonl` present to prove live locks, bus registration, target ownership, and reconciliation override stale diagnostics;
21
- - [x] follower thread receives raw voice/message updates through the leader bus;
22
- - [x] follower-local handlers from that instance's `telegram.json` process voice independently;
23
- - [x] same-directory leader/follower sessions keep distinct thread bindings;
24
- - [ ] leader reload recovers without duplicate visible thread renames;
25
- - [ ] prompts dispatch without a second command;
26
- - [ ] status remains stable around thread name and role while active turns start/end.
27
- - [ ] Add/keep proactive race protections so `state.json` never becomes authoritative over live lock ownership, bus registration, or current target identity.
28
-
29
- Done when: local Linux live Threaded Mode smoke is stable for leader reload, follower connect, prompt dispatch, voice forwarding, and status naming without visible rename noise.
30
-
31
- ## P0 — Native Windows Threaded Mode Support
32
-
33
- Context: Threaded Mode uses a local leader/follower IPC bus. Unix-like platforms use Node `net` over Unix sockets; native Windows uses Node `net` named-pipe paths. The product expectation is identical behavior across both transports: leader/follower registration, heartbeats, forwarded Telegram API calls, thread target preservation, lifecycle cleanup, and shutdown semantics should not depend on socket-vs-pipe transport. This remains after the local Linux regression sweep is green.
11
+ - [ ] Close leader follower promotes `/reload` promoted leader preserves the same Telegram thread identity.
12
+
13
+ Done when: promoted-follower reload identity has live Telegram evidence or is deliberately deferred out of the hotfix scope.
14
+
15
+ ## P1 Native Windows Threaded Mode Follow-Ups
16
+
17
+ Context: Native Windows smoke on the WIP `dev` build now passes for classic mode, classic ownership handoff, hot upgrade to Threaded Mode, leader/follower registration and delivery, and hot downgrade back to classic with follower disconnect. The observed downgrade status convergence can take around 10 seconds, which is acceptable for the current retry-based safety model but should remain evidence-gated if it becomes user-visible friction.
34
18
 
35
19
  Open work:
36
20
 
37
- - [ ] Live smoke Threaded Mode on native Windows without WSL.
38
- - Scope: leader/follower `/telegram-connect`, follower heartbeat, forwarded Bot API calls, restore flows, lifecycle announcements, shutdown cleanup, and reconnect/reload behavior.
39
- - Observed: a same-directory follower can see a live leader lock but fail registration with `connect ENOENT \\.\\pipe\\...`, leaving the follower disconnected during leader reload/hot activation timing.
40
- - Observed: Windows/QEMU live polling/dispatch can lag until reload; inbound messages appear to increase the extension queue count, but the next queued item is not dispatched promptly.
41
- - Baseline: deterministic path tests run everywhere, and a Windows-only named-pipe roundtrip regression runs when the suite executes on `win32`. Live Windows smoke remains unavailable in this environment.
42
- - [x] Add a minimized registration-boundary regression for transient leader endpoint startup races.
43
- - [x] Add a session-bound queue dispatch watchdog so queued Telegram work can recover if a one-shot wakeup/timer is missed.
21
+ - [ ] Capture text diagnostics if Windows classic restore/status convergence repeatedly exceeds the intended 5–15 second fallback window.
22
+ - [ ] Add a focused regression or transport/status adjustment only if new Windows evidence shows a repeatable named-pipe, lock, heartbeat, queue, or status-convergence issue.
44
23
 
45
- Done when: Threaded Mode leader/follower operation works on native Windows with the same safety guarantees as Unix-like systems, and unsupported transport assumptions are covered by tests/docs.
24
+ Done when: new Windows-specific runtime issues are either fixed with targeted coverage or left out of the backlog because the native smoke remains green.
46
25
 
47
26
  ## P1 — Evidence-Backed Telegram Client Follow-Ups
48
27
 
package/CHANGELOG.md CHANGED
@@ -2,6 +2,29 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.18.6: Threaded Mode parity hotfix
6
+
7
+ - `[Threaded Mode]` Follower-routed prompts now record message ownership when the leader forwards them by thread target, and edited messages can route by stored message ownership when Telegram omits thread identity. Impact: later queue-control reactions and queued-prompt edits on the original Telegram message route back to the owning follower instead of being handled by the leader.
8
+ - `[Native Activity Status]` Follower typing/activity startup now sends one thread-scoped action plus one aggregate action, and the default keepalive cadence is back to 2.5 seconds. Impact: follower `...active` status avoids duplicate chat-action bursts that can hit Telegram rate limits.
9
+ - `[Threaded Mode]` Follower `/start` can now register the visible bot command menu through the leader bus without a warning before opening the local menu, follower-sent Bot API messages are recorded in the leader's ownership map, and follower message edit/delete calls used by menu surfaces are allowed as validated same-chat message operations. Impact: follower menu callbacks and interactive cleanup route back to the owning follower even when Telegram callback payloads omit thread identity, without false bus-allowlist failures.
10
+ - `[Threaded Mode]` Follower promotion now snapshots the current follower thread binding before clearing registration state and converts that binding into the leader profile before forced lock acquisition. Impact: an elected follower keeps its existing thread identity across promotion and later reload instead of becoming disconnected or provisioning a fresh leader thread.
11
+ - `[Threaded Mode]` Follower registration now probes a reused same-profile thread with the connected notice before reporting it as reusable, and recreates the thread when Telegram reports the old target as stale. Impact: reconnecting a follower after its Telegram tab was closed no longer leaves the terminal showing a follower thread name while no matching Telegram thread is visible.
12
+ - `[Threaded Mode]` Unbound-thread reroute controls now expose replace/restore for the current live bus roster instead of requiring prior leader reroute confirmation. Impact: when a new unbound Telegram thread appears, the operator can choose any currently live leader/follower instance to route to or replace/restore into that source thread.
13
+ - `[Status]` Follower terminal status now gives active/compacting processing labels precedence over the stable follower role, matching leader behavior. Impact: a follower shows its thread name plus `active` while processing a Telegram-originated prompt, then returns to `follower` when idle.
14
+ - `[Threaded Mode]` Reload-time target ownership now classifies only follower-owned records as follower targets and replaces stale same-profile/same-target follower registry entries on registration. Impact: after leader or follower reload, Telegram prompts and reroutes no longer get forwarded to dead instance ids or report a live thread as unavailable.
15
+ - `[Validation]` Live Linux Threaded Mode smoke now passes for post-reload leader/follower prompt routing, unbound reroute/restore, and leader/follower terminal Active status. Impact: the hotfix candidate has Telegram-client evidence for the regressions fixed after `0.18.5`.
16
+ - `[Docs]` Added a leader/follower capability parity matrix covering prompts, queue reactions, edits, callbacks, replies, previews, attachments, native activity, command/menu bootstrap, and diagnostics. Impact: follower behavior now has an explicit parity checklist instead of being inferred from leader behavior.
17
+ - `[Engineering]` Formalized an `AGENTS.md` compliance pass for non-trivial work and cleaned unused runtime/lifecycle test surfaces surfaced by strict no-unused diagnostics. Impact: future changes have an explicit project-rule audit step, and dead-code diagnostics stay actionable.
18
+
19
+ ## 0.18.5: Windows Threaded Mode stabilization hotfix
20
+
21
+ - `[Bus Transport]` Introduced an explicit local bus transport boundary for endpoint derivation, socket-vs-pipe detection, operation-aware retry policy, timeout/transient IPC error classification, endpoint reachability probes, request-scoped server/client transport events, and handler-failure ACKs instead of silent client timeouts. Impact: Unix socket behavior remains the stable baseline while Windows named-pipe readiness and retry behavior are contained in the transport layer instead of leaking into routing.
22
+ - `[Windows IPC]` Leader-side forwarding now tolerates a pruned follower registry entry by using the follower's deterministic receiver endpoint, the default follower prune window is more conservative, and `/telegram-status --debug` identifies local bus endpoints as pipes or sockets. Impact: follower threads that already connected are less likely to fall back to `not connected to the Telegram bus yet` during Windows named-pipe heartbeat jitter, and diagnostics expose the active transport contour directly.
23
+ - `[Capability Switching]` Live Threaded Mode downgrade now blocks follower takeover while active thread bindings prove the bot is degrading from a live leader/follower organism, confirms disabled thread capability after two 2.5-second monitor probes, retries classic polling restore after transient failures, and clears the in-memory follower registry when bus leadership stops. Impact: when BotFather disables private-chat threads, the current bus leader keeps the classic singleton polling role and followers disconnect instead of stealing ownership or lingering as a live bus roster.
24
+ - `[Diagnostics]` Runtime JSONL reset now preserves the previous session log as `logs.previous.jsonl` and records that path in the new reset line. Impact: `/reload` no longer destroys the best evidence for long-running `/start`, menu, polling, queue, or bus stalls immediately before restart.
25
+ - `[Previews]` Native rich Markdown previews no longer send syntax-only prefixes such as a bare opening `**`, while keeping the native draft/final lifecycle otherwise unchanged and removing unused throttle/manual-clear branches. Impact: Telegram avoids malformed early preview fragments without extra delivery policy that can create duplicate, stalled, or placeholder draft artifacts.
26
+ - `[Validation]` Native Windows smoke passed for classic mode, classic ownership handoff, hot upgrade to Threaded Mode, leader/follower registration and delivery, and hot downgrade back to classic with follower disconnect. Impact: the named-pipe and capability-switching fixes have live evidence across both directions, with classic restore/status convergence inside the intended 5–15 second fallback window.
27
+
5
28
  ## 0.18.4: Windows Threaded Mode hotfix
6
29
 
7
30
  - `[Windows IPC]` Follower registration now retries transient local bus connection failures while the leader named pipe/socket is still coming online. Impact: a same-directory Windows follower is less likely to fail `/telegram-connect` with `connect ENOENT \\.\\pipe\\...` during leader reload or hot Threaded Mode activation.
@@ -15,7 +38,7 @@
15
38
  - `[Status]` Leader target assignment now carries the live thread name into status fallback state. Impact: the status bar is less likely to flicker from `Dune Leader/Active` to generic `Telegram Leader` when the current active turn or thread-store lookup changes.
16
39
  - `[Model Menu]` The Telegram model menu now hides one-page pagination controls and keeps scope tabs hidden unless scoped models exist. Impact: the minimal model menu shows only main-menu navigation and the available models.
17
40
 
18
- ## 0.18.2: setup pairing start hotfix
41
+ ## 0.18.2: Setup pairing start hotfix
19
42
 
20
43
  - `[Setup]` `/telegram-setup` now updates the live in-memory config immediately after persisting the validated bot token and before starting polling. Impact: first-time setup no longer shows `Send /start...` followed by `Telegram bot is not configured`, and `/start` can be received without restarting Pi.
21
44
 
package/README.md CHANGED
@@ -225,7 +225,7 @@ The agent writes intent; providers or voice handlers own TTS and format conversi
225
225
 
226
226
  ### Voice reply policies
227
227
 
228
- The bridge can automatically convert agent text replies into Telegram voice messages without requiring explicit `<!-- telegram_voice -->` markup in every response. Configure this from Settings → `👄 Voice reply` or by setting `voice.replyMode` in `telegram.json`:
228
+ The bridge can automatically convert agent text replies into Telegram voice messages without requiring explicit `<!-- telegram_voice -->` markup in every response. Configure this from Settings → `Voice reply` or by setting `voice.replyMode` in `telegram.json`:
229
229
 
230
230
  - `hidden` (default): no `voice.replyMode` is stored. Behavior is manual, but prompt context stays silent.
231
231
  - `manual`: agent-authored `<!-- telegram_voice -->` markup is required for voice replies; no automatic conversion. Unlike `hidden`, this explicit mode adds `[voice] reply mode: manual` context.
@@ -161,7 +161,7 @@ Implemented transport:
161
161
 
162
162
  ### Local IPC endpoint under agent temp dir
163
163
 
164
- Leader opens a local Node `net` endpoint: a Unix-domain socket under the agent temp directory on Unix-like platforms, or a deterministic Windows named pipe (`\\.\pipe\pi-telegram-...`) on native Windows. Followers register, heartbeat, and exchange routed events. Follower registration uses a longer registration-specific response timeout than ordinary heartbeat/forwarding calls because the leader may need to provision a Telegram thread before it can return the assigned target; timing out that handshake leaves a visible tab with no follower heartbeat. Keep this handshake to the true critical path: create/reuse the target, persist the live binding, and return it. Connected notices and replaced-thread reconciliation cleanup are non-critical and should run after registration so a follower becomes routable before Telegram client/server UI convergence work finishes.
164
+ Leader opens a local Node `net` endpoint: a Unix-domain socket under the agent temp directory on Unix-like platforms, or a deterministic Windows named pipe (`\\.\pipe\pi-telegram-...`) on native Windows. Followers register, heartbeat, and exchange routed events. The transport boundary owns endpoint derivation, socket-vs-pipe detection, bounded operation-aware retry policy, timeout/transient IPC error classification, endpoint reachability probes, and request-scoped transport events. Follower registration uses a longer registration-specific response timeout than ordinary heartbeat/forwarding calls because the leader may need to provision a Telegram thread before it can return the assigned target; timing out that handshake leaves a visible tab with no follower heartbeat. Keep this handshake to the true critical path: create/reuse the target, persist the live binding, and return it. Connected notices and replaced-thread reconciliation cleanup are non-critical and should run after registration so a follower becomes routable before Telegram client/server UI convergence work finishes.
165
165
 
166
166
  Pros:
167
167
 
@@ -184,13 +184,13 @@ Manual smoke checklist:
184
184
 
185
185
  1. Enable Telegram private-chat Threaded Mode for the paired bot.
186
186
  2. Start Pi in one Windows terminal and run `/telegram-connect`; verify it becomes the leader and gets a named Telegram thread.
187
- 3. Start Pi in a second Windows terminal and run `/telegram-connect`; verify it registers as follower rather than offering takeover, creates/uses its assigned thread, and terminal status shows `<ThreadName> Follower`.
187
+ 3. Start Pi in a second Windows terminal and run `/telegram-connect`; verify it registers as follower rather than offering takeover, creates/uses its assigned thread, terminal status shows `<ThreadName> Follower` while idle, and a follower prompt flips it to `<ThreadName> Active` while work is running.
188
188
  4. From the follower thread, send a prompt that requests inline buttons; tap a button and verify the follow-up prompt queues in the follower instance.
189
189
  5. From the follower thread, request a voice reply and/or attachment; verify upload routes through the leader transport into the follower thread.
190
190
  6. Close the follower terminal; verify heartbeat pruning, disconnected notice, and cleanup behavior match Unix-like behavior.
191
191
  7. Reload the leader and verify status/debug output does not expose raw pipe internals except in explicit diagnostics.
192
192
 
193
- If any step fails, capture `telegram-status --debug`, `tmp/telegram/state.json`, and `tmp/telegram/logs.jsonl` before retrying.
193
+ If any step fails, capture `telegram-status --debug`, `tmp/telegram/state.json`, `tmp/telegram/logs.jsonl`, and, after a reload, `tmp/telegram/logs.previous.jsonl`. Debug status prints local leader/follower endpoints with their active transport kind (`pipe` or `socket`), while the runtime log records request-scoped transport failures with envelope kind, request id, retry attempt, endpoint, and classified IPC error. Reloads preserve the prior JSONL log as `logs.previous.jsonl` so the evidence that caused the reload is not immediately overwritten.
194
194
 
195
195
  ### Native Windows Assumption Audit
196
196
 
@@ -283,6 +283,27 @@ This preserves one API bus and one set of rate-limit/retry diagnostics. The curr
283
283
 
284
284
  Every outbound request carries its target. The leader injects `message_thread_id` when `target.threadId` exists.
285
285
 
286
+ ## Leader/Follower Capability Parity Matrix
287
+
288
+ Threaded Mode should make follower threads behave like normal Telegram instance surfaces, with the leader acting only as transport owner. Any feature in the matrix below that works for the leader must either work for followers or have an explicit documented exception.
289
+
290
+ | Surface | Leader behavior | Follower requirement | Routing/ownership invariant | Regression evidence |
291
+ | --- | --- | --- | --- | --- |
292
+ | Prompt intake | Thread prompt queues locally | Thread prompt is forwarded and queued by the owning follower | Target ownership routes by `{ chatId, threadId }` before local handling | Routing tests for foreign target message forwarding |
293
+ | Queued-message removal reactions | 👎/👻/💔/💩/🗑 removes pending prompt/media turn | Same reaction on a queued follower prompt removes that follower's pending turn before dispatch | When the leader forwards a prompt to a follower, it records `chatId/messageId -> follower instance` because Bot API reaction updates expose chat/message but not thread id | Update runtime regression records forwarded message ownership and forwards the later reaction |
294
+ | Queue priority reactions | 👍/⚡/❤/🕊/🔥 prioritizes queued prompts | Same reactions prioritize follower queued prompts | Reaction forwarding uses stored message ownership, then follower mutates its local queue | Reaction mutation tests plus forwarded-reaction coverage |
295
+ | Message edits | Edits update matching queued prompt text | Edits in a follower thread update that follower's queued prompt | Message target ownership forwards edits to the owning instance; stored message ownership is the fallback when Telegram edit payloads omit thread id | Update routing tests for foreign target and message-owned edited-message forwarding |
296
+ | Callbacks/buttons/menus | Callback handled by the owning instance/menu state | Follower callbacks are forwarded to the owning follower; follower menu sends/edits/deletes route through leader transport | Leader records ownership for follower-sent Bot API messages so callbacks can route by message id even when Telegram omits thread id; Bot API edit/delete lacks thread id, so follower bus allows validated same-chat message operations | Callback forwarding, generated-button target, bus follower-sent ownership, and bus edit/delete allowlist tests |
297
+ | Replies/finals | Final replies land in the same thread | Follower finals go through leader transport into follower thread | Outbound calls carry target and inject `message_thread_id` | Reply delivery and bus API tests |
298
+ | Previews/Rich Drafts | Draft previews use the active thread target | Follower previews use the same native draft lifecycle through the leader | Preview transport preserves target and draft id | Preview thread-target tests |
299
+ | Attachments/voice | Files and voice upload in the instance thread | Follower uploads route through leader multipart transport | Multipart calls are target-scoped and follower-authorized | Bus allowlist and outbound delivery tests |
300
+ | Native activity status | `sendChatAction(typing)` shows thread Active, mirrors aggregate `All`, and terminal status flips from role to `active` during work | Follower work sends one thread action and one aggregate action through leader transport, and terminal status follows the same idle-role → active transition as leaders | Typing loop targets the active turn and avoids duplicate aggregate sends/rate-limit pressure; status rendering gives processing labels precedence over stable bus role labels | Runtime typing loop starter and status bar parity regressions |
301
+ | Leader election / promotion | Current leader keeps its thread across reload | A promoted follower keeps its existing thread, slot, and name when elected and after later reload | Promotion converts the current follower binding into the leader profile before forced lock acquisition, so leader startup reuses it instead of provisioning a new thread | Follower heartbeat recovery passes binding snapshot into promotion; own-topic provisioner reuses promoted bindings |
302
+ | `/start` command/menu bootstrap | Registers visible bot commands and opens the menu | Follower `/start` can refresh the bot command menu through the leader and open its local menu without warnings | Bot command registration is a validated global Bot API call allowed through trusted follower bus transport | Bus allowlist regression for `setMyCommands` |
303
+ | Follower reconnect | Existing leader binding is reused only when still usable | Follower reconnect that points at a closed/stale Telegram tab recreates a visible thread before reporting success, and session replacement replaces any old same-profile/same-target registry entry | Same-profile reuse is probed with a connected notice; stale Bot API errors mark the old target stale and provision a fresh target; live follower target ownership never falls back to leader records | Bus leader stale reused follower-thread provisioner and bus registry/ownership regressions |
304
+ | Unbound thread reroute/restore | New unbound thread can route to a live instance or replace the selected instance thread | Same chooser exposes all currently live bus leader/follower targets; restore is offered from concrete unbound threads, not historical snapshots | Live bus roster plus active target bindings define the selectable set; history/state snapshots are not authority | Routing chooser regressions for live target filtering and restore rows |
305
+ | Status/menu diagnostics | Status reflects leader role, queue, and target | Follower status reflects follower role, thread name, queue, and bus health | Status is local runtime truth plus bus registration state, not leader queue state | Status and bus diagnostics tests |
306
+
286
307
  ## Queue And State Scoping
287
308
 
288
309
  Each instance owns its own queue and active turn state. The leader does not become a central queue scheduler for all agents; that would be a separate daemon-mode architecture.
@@ -311,8 +332,8 @@ Typical config remains just bot identity and authorization:
311
332
 
312
333
  Rules:
313
334
 
314
- - Classic mode is selected by Telegram capability: when private-chat threads are unavailable or disabled, the polling owner uses ordinary single-DM behavior and blocked instances do not register as followers.
315
- - Telegram private-chat Threaded Mode enables local leader/follower behavior automatically. The leader owns `getUpdates`; registered followers route Telegram API work through the leader. `/telegram-connect` registers as follower when a live leader exists and does not offer manual takeover in that state. The TUI status bar reports `telegram leader` or `telegram follower` so transport role is visible without opening diagnostics.
335
+ - Classic mode is selected by Telegram capability: when private-chat threads are unavailable or disabled, the polling owner uses ordinary single-DM behavior and blocked instances do not register as followers. During a live downgrade from Threaded Mode, the current bus leader becomes the classic polling owner after two 2.5-second capability-monitor probes and followers disconnect; if classic polling restore fails transiently, later monitor ticks retry the restore instead of allowing a follower takeover. Followers must not turn the downgrade into a takeover while active thread bindings prove the singleton owner was already established by the bus leader.
336
+ - Telegram private-chat Threaded Mode enables local leader/follower behavior automatically. The leader owns `getUpdates`; registered followers route Telegram API work through the leader. `/telegram-connect` registers as follower when a live leader exists and does not offer manual takeover in that state. The TUI status bar reports `telegram leader` or `telegram follower` while idle so transport role is visible without opening diagnostics, and both roles switch to `active`/`compacting` processing labels during local Telegram work. Follower registration is unique by live profile/target: a reload or session replacement must replace stale registry entries rather than leaving multiple routable ids for one Telegram thread, and fallback target ownership must not classify leader records as followers.
316
337
  - The thread chat is the owner's private bot DM (`allowedUserId`); no `topics.chatId` config is needed. Thread names are assigned by the bridge from a baked compact per-slot palette. There is no agent-facing `telegram_rename_thread` tool and no separate user-facing slash command for manual thread renames.
317
338
  - Thread reuse is extension-owned through current live binding identity; there is no separate `topics` config surface in the active private-chat thread model. Manual followers use instance-scoped internal keys by default so multiple terminal processes in the same cwd can receive separate threads.
318
339
  - Thread cleanup remains conservative and centralized: destructive close/delete actions are planned and applied through `thread-reconciler` with proof-before-delete checks, leader-epoch fencing, and retry-preserving failure semantics.
package/index.ts CHANGED
@@ -9,6 +9,7 @@ import * as BusApi from "./lib/bus-api.ts";
9
9
  import * as Bus from "./lib/bus.ts";
10
10
  import * as BusFollower from "./lib/bus-follower.ts";
11
11
  import * as BusLeader from "./lib/bus-leader.ts";
12
+ import * as BusTransport from "./lib/bus-transport.ts";
12
13
  import * as CommandTemplates from "./lib/command-templates.ts";
13
14
  import * as Commands from "./lib/commands.ts";
14
15
  import * as Config from "./lib/config.ts";
@@ -249,6 +250,21 @@ export default function (pi: Pi.ExtensionAPI) {
249
250
  records: threadStore.list(),
250
251
  });
251
252
  },
253
+ getLocalBus() {
254
+ return {
255
+ leaderSocketPath: telegramBusSocketPath,
256
+ leaderTransport:
257
+ BusTransport.getTelegramBusTransportKind(telegramBusSocketPath),
258
+ followerSocketPath: telegramBusFollowerSocketPath,
259
+ followerTransport: BusTransport.getTelegramBusTransportKind(
260
+ telegramBusFollowerSocketPath,
261
+ ),
262
+ followerRegistered: telegramBusFollowerRegistrationState.isRegistered(),
263
+ followerTarget: telegramBusFollowerRegistrationState.getTarget(),
264
+ followerSlot: telegramBusFollowerRegistrationState.getSlot(),
265
+ followerThreadName: telegramBusFollowerRegistrationState.getThreadName(),
266
+ };
267
+ },
252
268
  getTopicTargets() {
253
269
  return Threads.listTelegramThreadStatusTargets(threadStore.list());
254
270
  },
@@ -646,8 +662,16 @@ export default function (pi: Pi.ExtensionAPI) {
646
662
  return telegramInstanceId;
647
663
  },
648
664
  getMessageOwnership: messageOwnershipStore.get,
665
+ recordMessageOwnership(input) {
666
+ messageOwnershipStore.record(input);
667
+ },
649
668
  getTargetOwnership(target) {
650
- return telegramBusFollowerRegistry.getByTarget(target);
669
+ return Bus.getTelegramFollowerTargetOwnership({
670
+ target,
671
+ followers: telegramBusFollowerRegistry.list(),
672
+ activeThreadRecords: threadStore.list(),
673
+ currentInstanceId: telegramInstanceId,
674
+ });
651
675
  },
652
676
  getLiveThreadTargets() {
653
677
  return Bus.listTelegramBusLiveThreadTargets({
@@ -800,7 +824,29 @@ export default function (pi: Pi.ExtensionAPI) {
800
824
  telegramBusLifecycleOverridePhase = phase;
801
825
  },
802
826
  updateStatus,
803
- async promoteToLeader(ctx) {
827
+ async promoteToLeader(ctx, binding) {
828
+ const promotedRecord =
829
+ await Threads.promoteTelegramFollowerBindingToLeader({
830
+ store: threadStore,
831
+ instanceId: telegramInstanceId,
832
+ cwd: ctx.cwd,
833
+ target: binding.target,
834
+ slot: binding.slot,
835
+ threadName: binding.threadName,
836
+ });
837
+ if (promotedRecord) {
838
+ recordRuntimeEvent(
839
+ "bus",
840
+ "Follower thread binding promoted to leader",
841
+ {
842
+ phase: "follower-promoted-binding",
843
+ chatId: promotedRecord.target.chatId,
844
+ threadId: promotedRecord.target.threadId,
845
+ slot: promotedRecord.slot,
846
+ threadName: promotedRecord.threadName,
847
+ },
848
+ );
849
+ }
804
850
  await lockedPollingRuntime.start(ctx, { force: true });
805
851
  },
806
852
  sleep(ms) {
@@ -877,6 +923,14 @@ export default function (pi: Pi.ExtensionAPI) {
877
923
  startPolling: pollingRuntime.start,
878
924
  stopPolling: pollingRuntime.stop,
879
925
  authorizeFollowerApiCall: Bus.isTelegramFollowerApiCallAllowed,
926
+ recordFollowerMessageOwnership(record) {
927
+ messageOwnershipStore.record({
928
+ chatId: record.chatId,
929
+ messageId: record.messageId,
930
+ target: record.target,
931
+ instanceId: record.follower.instanceId,
932
+ });
933
+ },
880
934
  provisionLeaderTarget:
881
935
  BusLeader.createTelegramBusLeaderTargetProvisioner<Pi.ExtensionContext>({
882
936
  getAllowedUserId: configStore.getAllowedUserId,
@@ -1252,8 +1306,6 @@ export default function (pi: Pi.ExtensionAPI) {
1252
1306
  sendRecordVoiceAction,
1253
1307
  sendMarkdownReply,
1254
1308
  sendTextReply,
1255
- editInteractiveMessage,
1256
- deleteMessage: deleteTelegramMessage,
1257
1309
  dispatchNextQueuedTelegramTurn,
1258
1310
  answerGuestQuery,
1259
1311
  sendGuestReply,
package/lib/bindings.ts CHANGED
@@ -21,7 +21,6 @@ import * as Replies from "./replies.ts";
21
21
  import * as Runtime from "./runtime.ts";
22
22
  import * as Setup from "./setup.ts";
23
23
  import * as Status from "./status.ts";
24
- import * as Target from "./target.ts";
25
24
  import * as TelegramApi from "./telegram-api.ts";
26
25
 
27
26
  type ActivePiModel = NonNullable<Pi.ExtensionContext["model"]>;
@@ -166,14 +165,6 @@ interface TelegramLifecycleBindingDeps {
166
165
  Keyboard.TelegramInlineKeyboardMarkup
167
166
  >["sendTextReply"] &
168
167
  NonNullable<OutboundHandlers.TelegramVoiceReplySenderDeps["sendTextReply"]>;
169
- editInteractiveMessage: (
170
- chatId: number,
171
- messageId: number,
172
- text: string,
173
- mode: "html" | "markdown",
174
- replyMarkup: Keyboard.TelegramInlineKeyboardMarkup,
175
- ) => Promise<void>;
176
- deleteMessage: (chatId: number, messageId: number) => Promise<void>;
177
168
  dispatchNextQueuedTelegramTurn: (ctx: Pi.ExtensionContext) => void;
178
169
  answerGuestQuery: NonNullable<
179
170
  Queue.TelegramAgentEndHookRuntimeDeps<
@@ -225,8 +216,6 @@ export function registerTelegramLifecycleRuntimeHooks({
225
216
  sendRecordVoiceAction,
226
217
  sendMarkdownReply,
227
218
  sendTextReply,
228
- editInteractiveMessage,
229
- deleteMessage,
230
219
  dispatchNextQueuedTelegramTurn,
231
220
  answerGuestQuery,
232
221
  sendGuestReply,
@@ -367,12 +356,12 @@ export function registerTelegramLifecycleRuntimeHooks({
367
356
  onSessionBeforeCompact: compactionObserver.onSessionBeforeCompact,
368
357
  onSessionCompact: compactionObserver.onSessionCompact,
369
358
  onAgentStart: agentStartWithDedupReset,
370
- async onToolExecutionStart(event, _ctx) {
359
+ async onToolExecutionStart(_event, _ctx) {
371
360
  agentLifecycleHooks.onToolExecutionStart();
372
361
  },
373
362
  onToolExecutionUpdate() {},
374
- async onToolExecutionEnd(event, ctx) {
375
- agentLifecycleHooks.onToolExecutionEnd(event, ctx);
363
+ async onToolExecutionEnd(_event, ctx) {
364
+ agentLifecycleHooks.onToolExecutionEnd(_event, ctx);
376
365
  },
377
366
  onAgentEnd: agentLifecycleHooks.onAgentEnd,
378
367
  onBeforeAgentStart: Prompts.createTelegramProactiveBeforeAgentStartHook({