@llblab/pi-telegram 0.18.3 → 0.18.5

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.
@@ -144,6 +144,7 @@ The core product loop is mobile continuation: start or supervise work in the ter
144
144
  - 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
145
  - 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
146
  - 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
147
+ - 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
148
  - 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
149
  - 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
150
 
@@ -161,8 +162,9 @@ The canonical detailed ownership map lives in [`docs/architecture.md`](./docs/ar
161
162
  ## 6.5 Entrypoint And Import Boundaries
162
163
 
163
164
  - 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.
165
+ - 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.
166
+ - 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.
167
+ - 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
168
  - 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
169
  - 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
170
  - 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
package/BACKLOG.md CHANGED
@@ -1,6 +1,6 @@
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, core live Threaded Mode smoke, and native Windows classic↔Threaded Mode upgrade/downgrade 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
5
  ## P0 — Live Threaded Mode Regression Sweep
6
6
 
@@ -8,19 +8,8 @@ Context: live Linux testing exposed regressions around prompt dispatch readiness
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
11
  - [ ] 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
12
  - [ ] 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
13
  - [ ] leader reload recovers without duplicate visible thread renames;
25
14
  - [ ] prompts dispatch without a second command;
26
15
  - [ ] status remains stable around thread name and role while active turns start/end.
@@ -28,18 +17,16 @@ Open work:
28
17
 
29
18
  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
19
 
31
- ## P0 — Native Windows Threaded Mode Support
20
+ ## P1 — Native Windows Threaded Mode Follow-Ups
32
21
 
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.
22
+ 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
23
 
35
24
  Open work:
36
25
 
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
- - 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.
40
- - [ ] If Windows live smoke exposes pipe-specific behavior, add a minimized regression at the bus transport boundary before changing higher-level Threaded Mode logic.
26
+ - [ ] Capture text diagnostics if Windows classic restore/status convergence repeatedly exceeds the intended 5–15 second fallback window.
27
+ - [ ] 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.
41
28
 
42
- 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.
29
+ 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.
43
30
 
44
31
  ## P1 — Evidence-Backed Telegram Client Follow-Ups
45
32
 
package/CHANGELOG.md CHANGED
@@ -1,6 +1,18 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 0.18.5: Windows Threaded Mode stabilization hotfix
4
+
5
+ - `[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.
6
+ - `[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.
7
+ - `[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.
8
+ - `[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.
9
+ - `[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.
10
+ - `[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.
11
+
12
+ ## 0.18.4: Windows Threaded Mode hotfix
13
+
14
+ - `[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
+ - `[Queue]` A session-bound queue dispatch watchdog now retries dispatch while Telegram work remains queued. Impact: if a platform drops the one-shot deferred dispatch wakeup, queued Telegram messages can resume without waiting for a manual `/reload`.
4
16
 
5
17
  ## 0.18.3: Threaded Mode live hotfix
6
18
 
@@ -10,7 +22,7 @@
10
22
  - `[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.
11
23
  - `[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.
12
24
 
13
- ## 0.18.2: setup pairing start hotfix
25
+ ## 0.18.2: Setup pairing start hotfix
14
26
 
15
27
  - `[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.
16
28
 
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
 
@@ -190,7 +190,7 @@ Manual smoke checklist:
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
 
@@ -311,7 +311,7 @@ Typical config remains just bot identity and authorization:
311
311
 
312
312
  Rules:
313
313
 
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.
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. 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.
315
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.
316
316
  - 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
317
  - 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.
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
  },
@@ -454,6 +470,12 @@ export default function (pi: Pi.ExtensionAPI) {
454
470
  ...promptDispatchRuntime,
455
471
  sendUserMessage,
456
472
  }).dispatchNext;
473
+ const queueDispatchWatchdogRuntime =
474
+ Queue.createTelegramQueueDispatchWatchdogRuntime({
475
+ hasQueuedItems: telegramQueueStore.hasQueuedItems,
476
+ dispatchNextQueuedTelegramTurn,
477
+ recordRuntimeEvent,
478
+ });
457
479
  const nativeMarkdownDraftSender =
458
480
  TelegramApi.createTelegramNativeMarkdownDraftSender({
459
481
  sendMessageDraft,
@@ -641,7 +663,12 @@ export default function (pi: Pi.ExtensionAPI) {
641
663
  },
642
664
  getMessageOwnership: messageOwnershipStore.get,
643
665
  getTargetOwnership(target) {
644
- return telegramBusFollowerRegistry.getByTarget(target);
666
+ return Bus.getTelegramFollowerTargetOwnership({
667
+ target,
668
+ followers: telegramBusFollowerRegistry.list(),
669
+ activeThreadRecords: threadStore.list(),
670
+ currentInstanceId: telegramInstanceId,
671
+ });
645
672
  },
646
673
  getLiveThreadTargets() {
647
674
  return Bus.listTelegramBusLiveThreadTargets({
@@ -1168,8 +1195,10 @@ export default function (pi: Pi.ExtensionAPI) {
1168
1195
  async onSessionStart(event, ctx) {
1169
1196
  await lockedPollingRuntime.onSessionStart(event, ctx);
1170
1197
  telegramThreadCapabilityMonitor.start(ctx);
1198
+ queueDispatchWatchdogRuntime.start(ctx);
1171
1199
  },
1172
1200
  async onSessionShutdown() {
1201
+ queueDispatchWatchdogRuntime.stop();
1173
1202
  telegramThreadCapabilityMonitor.stop();
1174
1203
  },
1175
1204
  },
@@ -19,9 +19,17 @@ import {
19
19
  sendTelegramBusLocalEnvelope,
20
20
  type TelegramBusEnvelope,
21
21
  } from "./bus.ts";
22
+ import {
23
+ getTelegramBusTransportRetryPolicy,
24
+ TELEGRAM_BUS_REGISTRATION_RETRY,
25
+ } from "./bus-transport.ts";
22
26
 
23
27
  export const TELEGRAM_BUS_FOLLOWER_PROMOTION_GRACE_MS = 2_500;
24
28
  export const TELEGRAM_FOLLOWER_SESSION_HANDOFF_TTL_MS = 30_000;
29
+ export const TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_ATTEMPTS =
30
+ TELEGRAM_BUS_REGISTRATION_RETRY.attempts;
31
+ export const TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_DELAY_MS =
32
+ TELEGRAM_BUS_REGISTRATION_RETRY.delayMs;
25
33
 
26
34
  const TELEGRAM_FOLLOWER_SESSION_HANDOFF_KEY =
27
35
  "__piTelegramFollowerSessionHandoff";
@@ -150,6 +158,8 @@ export interface TelegramBusFollowerRegistrationRuntimeDeps<
150
158
  getPid?: () => number;
151
159
  timeoutMs?: number;
152
160
  registrationTimeoutMs?: number;
161
+ registrationRetryAttempts?: number;
162
+ registrationRetryDelayMs?: number;
153
163
  heartbeatMs?: number;
154
164
  recordRuntimeEvent?: (
155
165
  category: string,
@@ -337,6 +347,10 @@ export function createTelegramBusFollowerApiCaller(
337
347
  const response = await sendTelegramBusLocalEnvelope({
338
348
  socketPath: deps.socketPath,
339
349
  timeoutMs,
350
+ retry: getTelegramBusTransportRetryPolicy({
351
+ endpoint: deps.socketPath,
352
+ operation: "operation",
353
+ }),
340
354
  envelope: {
341
355
  kind: "follower.callApi",
342
356
  requestId: deps.createRequestId(),
@@ -588,6 +602,12 @@ export function createTelegramBusFollowerRegistrationRuntime<
588
602
  const heartbeatMs = deps.heartbeatMs ?? 1000;
589
603
  const registrationTimeoutMs =
590
604
  deps.registrationTimeoutMs ?? deps.timeoutMs ?? 30000;
605
+ const registrationRetryAttempts =
606
+ deps.registrationRetryAttempts ??
607
+ TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_ATTEMPTS;
608
+ const registrationRetryDelayMs =
609
+ deps.registrationRetryDelayMs ??
610
+ TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_DELAY_MS;
591
611
  let heartbeatInterval: ReturnType<typeof setInterval> | undefined;
592
612
  let activeLeaderSocketPath: string | undefined;
593
613
  let activeAuthSecret: string | undefined;
@@ -611,6 +631,10 @@ export function createTelegramBusFollowerRegistrationRuntime<
611
631
  const response = await sendTelegramBusLocalEnvelope({
612
632
  socketPath: activeLeaderSocketPath,
613
633
  timeoutMs: deps.timeoutMs,
634
+ retry: getTelegramBusTransportRetryPolicy({
635
+ endpoint: activeLeaderSocketPath,
636
+ operation: "operation",
637
+ }),
614
638
  envelope: {
615
639
  kind: "follower.heartbeat",
616
640
  requestId: deps.createRequestId(),
@@ -646,28 +670,46 @@ export function createTelegramBusFollowerRegistrationRuntime<
646
670
  await deps.startReceiving?.();
647
671
  activeAuthSecret = deps.getLeaderAuthSecret?.(leader);
648
672
  deps.setActiveAuthSecret?.(activeAuthSecret);
673
+ const createRegistrationEnvelope = (): Extract<
674
+ TelegramBusEnvelope,
675
+ { kind: "follower.register" }
676
+ > => ({
677
+ kind: "follower.register",
678
+ requestId: deps.createRequestId(),
679
+ auth: activeAuthSecret,
680
+ registration: {
681
+ instanceId: deps.instanceId,
682
+ profileKey:
683
+ deps.getProfileKey?.(ctx) ??
684
+ (ctx.cwd ? `cwd:${ctx.cwd}` : undefined),
685
+ threadName:
686
+ deps.getThreadName?.(ctx) ??
687
+ (ctx.cwd ? basename(ctx.cwd) : undefined),
688
+ cwd: ctx.cwd,
689
+ pid: getPid(),
690
+ busSocketPath: deps.followerBusSocketPath,
691
+ connectedAtMs: getNowMs(),
692
+ },
693
+ });
649
694
  let response: TelegramBusEnvelope | undefined;
650
695
  try {
651
696
  response = await sendTelegramBusLocalEnvelope({
652
697
  socketPath: leaderSocketPath,
653
698
  timeoutMs: registrationTimeoutMs,
654
- envelope: {
655
- kind: "follower.register",
656
- requestId: deps.createRequestId(),
657
- auth: activeAuthSecret,
658
- registration: {
659
- instanceId: deps.instanceId,
660
- profileKey:
661
- deps.getProfileKey?.(ctx) ??
662
- (ctx.cwd ? `cwd:${ctx.cwd}` : undefined),
663
- threadName:
664
- deps.getThreadName?.(ctx) ??
665
- (ctx.cwd ? basename(ctx.cwd) : undefined),
666
- cwd: ctx.cwd,
667
- pid: getPid(),
668
- busSocketPath: deps.followerBusSocketPath,
669
- connectedAtMs: getNowMs(),
699
+ envelope: createRegistrationEnvelope(),
700
+ retry: getTelegramBusTransportRetryPolicy({
701
+ endpoint: leaderSocketPath,
702
+ operation: "registration",
703
+ overrides: {
704
+ attempts: registrationRetryAttempts,
705
+ delayMs: registrationRetryDelayMs,
670
706
  },
707
+ }),
708
+ recordTransportEvent(phase, details) {
709
+ deps.recordRuntimeEvent?.("bus", `Telegram bus ${phase}`, {
710
+ phase: `follower-register-${phase}`,
711
+ ...details,
712
+ });
671
713
  },
672
714
  });
673
715
  } catch (error) {
@@ -734,6 +776,12 @@ export function createTelegramBusForwardedUpdateReceiverRuntime<
734
776
  ): TelegramBusForwardedUpdateReceiverRuntime {
735
777
  const server = createTelegramBusLocalServer({
736
778
  socketPath: deps.socketPath,
779
+ recordTransportEvent(phase, details) {
780
+ deps.recordRuntimeEvent?.("bus", `Telegram bus ${phase}`, {
781
+ phase: `follower-receiver-${phase}`,
782
+ ...details,
783
+ });
784
+ },
737
785
  async handleEnvelope(envelope) {
738
786
  const authSecret = deps.getAuthSecret?.();
739
787
  if (deps.getAuthSecret && (!authSecret || envelope.auth !== authSecret)) {
package/lib/bus-leader.ts CHANGED
@@ -21,6 +21,7 @@ import {
21
21
  type TelegramBusFollowerView,
22
22
  type TelegramBusInstanceRegistration,
23
23
  } from "./bus.ts";
24
+ import { getTelegramBusTransportRetryPolicy } from "./bus-transport.ts";
24
25
 
25
26
  export interface TelegramBusLeaderRuntime<TContext> {
26
27
  startPolling: (ctx: TContext) => Promise<void>;
@@ -598,31 +599,22 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
598
599
  >,
599
600
  ): Promise<TelegramBusEnvelope> => {
600
601
  const follower = deps.followerRegistry.get(envelope.recipientInstanceId);
601
- if (!follower) {
602
- return {
603
- kind: "bus.ack",
604
- requestId: envelope.requestId,
605
- ok: false,
606
- message: "Unknown Telegram bus follower instance.",
607
- };
608
- }
609
- if (!follower.busSocketPath) {
610
- return {
611
- kind: "bus.ack",
612
- requestId: envelope.requestId,
613
- ok: false,
614
- message: "Telegram bus follower does not expose a receiver socket.",
615
- };
616
- }
617
- deps.followerRegistry.heartbeat(follower.instanceId, getNowMs());
602
+ const followerSocketPath =
603
+ follower?.busSocketPath ??
604
+ getTelegramBusFollowerSocketPath(envelope.recipientInstanceId);
605
+ if (follower) deps.followerRegistry.heartbeat(follower.instanceId, getNowMs());
618
606
  try {
619
607
  const response = await sendTelegramBusLocalEnvelope({
620
- socketPath: follower.busSocketPath,
608
+ socketPath: followerSocketPath,
621
609
  envelope,
622
610
  timeoutMs: deps.timeoutMs,
611
+ retry: getTelegramBusTransportRetryPolicy({
612
+ endpoint: followerSocketPath,
613
+ operation: "operation",
614
+ }),
623
615
  });
624
616
  if (response?.kind === "bus.ack" && response.ok) {
625
- deps.followerRegistry.heartbeat(follower.instanceId, getNowMs());
617
+ if (follower) deps.followerRegistry.heartbeat(follower.instanceId, getNowMs());
626
618
  return { kind: "bus.ack", requestId: envelope.requestId, ok: true };
627
619
  }
628
620
  const message =
@@ -842,7 +834,7 @@ export function createTelegramBusLeaderRuntime<TContext>(
842
834
  ): TelegramBusLeaderRuntime<TContext> {
843
835
  const getNowMs = deps.getNowMs ?? Date.now;
844
836
  const followerPruneIntervalMs = deps.followerPruneIntervalMs ?? 1000;
845
- const followerStaleAfterMs = deps.followerStaleAfterMs ?? 2000;
837
+ const followerStaleAfterMs = deps.followerStaleAfterMs ?? 5000;
846
838
  let pruneInterval: ReturnType<typeof setInterval> | undefined;
847
839
  const stopPruning = () => {
848
840
  if (!pruneInterval) return;
@@ -878,6 +870,12 @@ export function createTelegramBusLeaderRuntime<TContext>(
878
870
  };
879
871
  const localServer = createTelegramBusLocalServer({
880
872
  socketPath: deps.socketPath,
873
+ recordTransportEvent(phase, details) {
874
+ deps.recordRuntimeEvent?.("bus", `Telegram bus ${phase}`, {
875
+ phase: `leader-${phase}`,
876
+ ...details,
877
+ });
878
+ },
881
879
  handleEnvelope: createTelegramBusLeaderEnvelopeHandler({
882
880
  followerRegistry: deps.followerRegistry,
883
881
  authSecret: deps.authSecret,
@@ -917,6 +915,7 @@ export function createTelegramBusLeaderRuntime<TContext>(
917
915
  .catch((error) =>
918
916
  deps.recordRuntimeEvent?.("bus", error, { phase: "stop" }),
919
917
  );
918
+ deps.followerRegistry.clear();
920
919
  }
921
920
  },
922
921
  };