@llblab/pi-telegram 0.26.11 → 0.26.13

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
@@ -89,7 +89,7 @@ The core product loop is mobile continuation: start or supervise work in the ter
89
89
  - Instance thread names are provisioner-authored identity metadata, not model-authored output. Fresh Threaded Mode threads select one of five curated compact 4-6 letter Latin names for the assigned slot letter using provisioning timestamp entropy, then create the Telegram thread with that thread name immediately. The title is the thread name itself, not `{slot} {threadName}` or `{slot} — {threadName}`. Do not register or reintroduce an agent-facing thread rename tool; removing the extra rename prompt/tool turn is an intentional simplicity constraint. Thread names must mirror into terminal status, live diagnostics, and `[telegram|thread:name]` through one target-aware current-instance identity resolver. Registered follower/leader metadata takes precedence over a stale shared record for the same target, with the record used only as fallback; do not hide a valid baked/custom thread name behind old slot-prefix validation. A follower that later becomes leader keeps its existing name; leadership changes are transport role changes, not identity resets.
90
90
  - Telegram private-chat Threaded Mode treats `All` as an aggregate/control surface, not a durable `General` thread or process launcher, and leader activation proactively creates or reuses its private-DM thread. When the owner writes in an unknown thread, the leader reclaims and routes the first one only if it lacks an active binding; later unknown threads remain intact and receive an in-thread chooser to reroute the captured prompt or restore a stale binding. Destructive cleanup requires explicit confirmation through `thread-reconciler`; another Pi instance still requires a manually started process followed by `/telegram-connect`. Without Threaded Mode, process the message through classic routing.
91
91
  - Telegram extension work should not hold Pi's core agent lifecycle hostage once Pi has produced the semantic result. For Telegram-originated turns, final Telegram delivery, attachment upload, and transport cleanup are extension-owned side effects; schedule them off the critical `agent_end` path when ordering can still be preserved, record failures as runtime events, and keep dispatch of the next Telegram turn behind the delivery task when needed to avoid interleaving previews/finals. Treat a low-level error `agent_end` as retryable while its Telegram turn remains active; only a later successful `agent_end` or Pi's `agent_settled` boundary may finalize and release that turn. Public activity handlers run through isolated asynchronous per-handler queues; lifecycle hooks enqueue normalized events and never await consumer work. Proactive push defaults enabled and requires explicit `assistant.proactivePush: false` to opt out. While enabled, core delivery projects each completed Pi-visible assistant text block from local/autonomous work once and in order; bind admitted blocks to the exact target, profile/token transport stamp, direct leader epoch or follower registration generation, and session generation. Token deltas, hidden reasoning, tool traffic, stale authority, and Telegram-owned turns never enter that projection.
92
- - Core thinking/tool visibility is controlled by file-backed `assistant.activity: "quiet" | "thinking" | "tools" | "verbose"`; every Pi instance reloads the shared config at `agent-start` before admitting activity so process-local cache cannot broaden the selected class. It defaults to `verbose` when absent and remains separate from proactive public prose and assistant Markdown: provider-exposed thinking uses only a durable ordinary HTML expandable blockquote, while completed executed tools use native Rich Messages whose closed `<Tool>: <status>` root details preserve sentence case while uppercasing an initial two- or three-letter repeated prefix, then contain an open-by-default `arguments` child plus closed `update`, `result`, or `error` children with plain lowercase labels and JSON pre blocks; treat these labels like quote-free outer JSON keys and add no list marker. Thinking renders inline Markdown as Telegram HTML without spending a separate header row; `verbose` enables both classes, and every activity send/edit disables Telegram link previews while technical evidence also neutralizes HTTP(S) auto-link detection as a client-safe fallback. Keep it compact by default, exact-target/transport-generation fenced, start-order preserving, boundary-aware, memory/transport bounded, and non-blocking; final delivery waits for admitted technical activity only inside the extension-owned background task, and session replacement abandons queued old-generation work without waiting on old transport. Read legacy `assistant.activityVerbosity` only when `assistant.activity` is absent and remove it on the next Settings write. Never invent unavailable provider-private thinking or replay a possibly committed technical message.
92
+ - Core thinking/tool visibility is controlled by file-backed `assistant.activity: "quiet" | "thinking" | "tools" | "verbose"`; every Pi instance reloads the shared config at `agent-start` before admitting activity so process-local cache cannot broaden the selected class. It defaults to `verbose` when absent and remains separate from proactive public prose and assistant Markdown: provider-exposed thinking uses only a durable ordinary HTML expandable blockquote, while completed executed tools use native Rich Messages whose closed `<Tool>: <status>` root details preserve sentence case while uppercasing an initial two- or three-letter repeated prefix, then contain an open-by-default `arguments` child plus closed `update`, `result`, or `error` children with lowercase monospaced labels and JSON pre blocks; treat each label as one quote-free outer JSON key and add no icon, list marker, or heading emphasis. Thinking renders inline Markdown as Telegram HTML without spending a separate header row; `verbose` enables both classes, and every activity send/edit disables Telegram link previews while technical evidence also neutralizes HTTP(S) auto-link detection as a client-safe fallback. Keep it compact by default, exact-target/transport-generation fenced, start-order preserving, boundary-aware, memory/transport bounded, and non-blocking; final delivery waits for admitted technical activity only inside the extension-owned background task, and session replacement abandons queued old-generation work without waiting on old transport. Read legacy `assistant.activityVerbosity` only when `assistant.activity` is absent and remove it on the next Settings write. Never invent unavailable provider-private thinking or replay a possibly committed technical message.
93
93
  - Telegram runtime state should be treated as event-driven reconciliation of extension assumptions against observable Telegram signals, not as a full Telegram bot read-model and not as a reason to query Telegram on every action. Keep a local assumption model for bot identity/capabilities, pairing assumptions, thread support, known thread lifecycle, bound instance targets, reservations, and transport health. Invalidate and reconcile the relevant slice on meaningful events: startup/reload, lifecycle service messages, stale-send/API errors, setup/pairing changes, follower register/prune, explicit status/debug requests, and bounded low-frequency leader health ticks. `lib/sync.ts` owns sync slices, observation intake, invalidation triggers, status/debug freshness, and reconciliation scheduling; it must not promise complete bot-state mirroring because Bot API lacks a complete topic listing surface. `lib/thread-reconciler.ts` owns pure Threaded Mode lifecycle planning and should become the only policy authority for destructive thread cleanup decisions. `threads` owns current Telegram UI thread/tab binding primitives and thread-name helpers mapped to Bot API topic transport, `status` renders projections, and `index.ts` only wires ports. `tmp/telegram/logs.jsonl` is a session-local redacted runtime evidence stream for race debugging and resets on extension start / runtime scope changes; it is diagnostics only, not routing/provisioning authority. `state.json` should be an observable/debug snapshot aligned with `/telegram-status` (extension runtime, bot capabilities including `bot.lastSlot`, runtime role, live roster projection, reservations, diagnostics), not routing/provisioning authority. Because every process on one Telegram profile shares this file, only the active transport lock owner may persist it; followers read current state and gain write authority only after promotion. Status-only writes must refresh disk-backed bindings before serialization so a stale loaded snapshot cannot erase newer leader state. Live bus/runtime state is authoritative; file reservations and fresh capability observations may be startup hints/collision guards only; stale snapshots must re-probe before suppressing bus/topic behavior. Do not persist stale/offline/failed thread target history as source of truth.
94
94
  - Thread bindings are bidirectional. From the Pi-instance side, an instance knows and preserves its target, slot, thread name, lifecycle state, and direct-delivery defaults. From the Telegram-client/bot side, the bridge observes thread creation/close/reopen/stale-send signals and reconciles them into instance binding state. Lifecycle transitions that affect operator understanding should be Telegram-visible when a live transport remains: every successful follower registration/re-registration gets a compact connected notice in the assigned thread, while heartbeat pruning stays silent because it is only liveness suspicion. These notices should use the instance thread name when known and fall back to the slot letter only while the thread is still unnamed. New unknown or command-created temporary threads receive the complete forward plus replace/restore control set and remain preserved until the user chooses one. Successful forward deletes the chooser and temporary thread; successful replace/restore always deletes the chooser, rebinds the temporary thread, and deletes only the replaced old thread. Any destructive cleanup must go through `thread-reconciler`. Unknown `forum_topic_created` service events are observations, not destructive cleanup proof.
95
95
  - The multi-instance bus uses private bot DMs with Telegram private-chat Threaded Mode enabled for the bot. No supergroup, group, or forum configuration is needed; the thread chat is always the private chat with the paired owner. Threaded capability checks must use bot/private-chat evidence such as `getMe.has_topics_enabled`, incoming `message_thread_id`, and topic operation success/failure; do not use group chat metadata as the control-plane truth for Telegram private-chat Threaded Mode.
package/BACKLOG.md CHANGED
@@ -2,45 +2,6 @@
2
2
 
3
3
  _This backlog tracks only open release-relevant work: hotfixes, bounded maintenance, live runtime verification, evidence-gated Telegram client follow-ups, and upstream Pi API blockers. Completed outcomes and validation evidence belong in `CHANGELOG.md`, not in this queue._
4
4
 
5
- ## P0 — 0.26.11 Windows Release Gates
6
-
7
- Context: the first final release matrix correctly held publication after loaded Windows scheduling exceeded two integration-only deadlines: queued model-menu completion and graceful shutdown of a full-Pi parent fixture. Neither failure changed the runtime feature contract, but both gates must remain bounded and preserve their exact assertions.
8
-
9
- Open work:
10
-
11
- - [x] Recheck the generic integration predicate at its deadline and give only the model-menu completion assertion a five-second budget.
12
- - [x] Give the full-Pi parent fixture up to ten seconds to complete graceful shutdown after its stop marker, without weakening exit-code or child-poll assertions.
13
- - [ ] Pass two fresh Linux/macOS/Windows matrices after these final changes.
14
-
15
- Done when: both Windows-sensitive evidence paths complete under runner load, genuine hangs remain bounded and diagnostic, and two consecutive release matrices pass before merge.
16
-
17
- ## P0 — Settings Description Choice Ordering
18
-
19
- Context: Settings detail copy historically tended to place the default value first. The default is metadata, not an ordering rule. Value descriptions should mirror the actual choice controls so operators can scan explanatory text and buttons in the same sequence.
20
-
21
- Open work:
22
-
23
- - [x] Order every core Settings value-description list exactly like its selectable controls: keyboard rows top-to-bottom and values within a shared row left-to-right; never move a value merely because it carries `(default)`.
24
- - [x] Keep `(default)` attached to the actual default wherever that value falls in control order; for example, Draft previews should describe `on` before `off (default)` because the buttons display On then Off, while Activity should retain `quiet`, `thinking`, `tools`, `verbose (default)`.
25
- - [x] Make the option/control order the explicit source of truth or protect parity with focused regressions so description and keyboard order cannot drift independently.
26
- - [x] Live-smoke the Settings detail ordering before the guarded release flow.
27
-
28
- Done when: every Settings description follows its visible chooser rather than default-first history, default annotations remain truthful, automated parity coverage passes, and live Telegram screens confirm scan order.
29
-
30
- ## P0 — Nested Tool Evidence List Styling
31
-
32
- Context: nested Rich tool evidence labels should read like quote-free outer JSON keys rather than peer headings or list items. Plain lowercase labels distinguish evidence children from the root without adding a redundant marker. Root labels remain sentence-case, but tool names beginning with a two- or three-character run of the same ASCII letter should preserve that recognizable prefix as uppercase instead of flattening it to one initial capital.
33
-
34
- Open work:
35
-
36
- - [x] Render each child summary as a marker-free plain lowercase label: `arguments`, `update N (K earlier omitted)`, `result`, or `error`.
37
- - [x] At the root level only, uppercase a leading repeated-letter run when its length is two or three, then retain ordinary label casing for the remainder; examples: `ffgrep` → `FFgrep`, `fffind` → `FFFind`, and `bash` → `Bash`.
38
- - [x] Keep Arguments open-by-default behavior intact, secondary nodes collapsed, and all JSON evidence, bounds, ordering, coalescing, redaction, fallback, and fencing guarantees unchanged.
39
- - [x] Update focused Rich block regressions, durable UI documentation, changelog, and package metadata.
40
- - [x] Live-smoke `FFFind` and `Bash` roots plus the nested hierarchy, then remove the redundant minus from the final design in response to operator feedback.
41
-
42
- Done when: opening a tool root reveals compact quote-free evidence keys without adding height, all evidence behavior remains intact, automated validation passes, live clients confirm the hierarchy, and the hotfix clears the guarded release flow.
43
-
44
5
  ## P1 — Windows Graceful Disconnect Test Budget
45
6
 
46
7
  Context: the graceful follower-disconnect integration overrides the production 30-second bus budget with 500 milliseconds even though the path persists cleanup intent, performs close/delete operations, updates binding state, and returns an authenticated response. A loaded Windows runner exceeded that artificial deadline while the same focused path completes promptly under normal scheduling.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.26.13: Entrypoint Compression And BotFather Prerequisites
4
+
5
+ - `Entrypoint Compression`: Moved low-level policy out of `index.ts` into owning domains: the deferred queue dispatch delay defaults in the queue domain, the delivery generation seed is built by the delivery domain, the `target-bindings` sync slice key and the default profile name resolve from their domains, and bus-runtime capability answers come from the thread capability state. Removed a redundant follower client timeout literal that duplicated the domain default. Impact: the composition root wires ports while constants, generations, and capability decisions stay with their owning domains.
6
+ - `Assembly Ownership`: Extracted the follower assembly (receiver, target replacement, recovery, registration) and session-lifecycle assembly (queue, follower, services) dependency structures into builders owned by `bus-follower` and `lifecycle`, normalizing shared ports and moving auth-secret derivation and grouped-input sequencing into domain runtimes. Impact: assembly shape and policy live in the owning domains, and the entrypoint reads as named construction steps instead of nested constructor calls.
7
+ - `Setup Documentation`: Documented the BotFather prerequisites for guest mode, private-chat Threaded Mode, and queue-reaction admin rights in the Quick Start. Impact: operators can enable the capabilities a feature needs instead of discovering that a disabled capability silently never triggers.
8
+
9
+ ## 0.26.12: Monospaced Tool Evidence Keys
10
+
11
+ - `Tool Evidence Typography`: Rendered nested `arguments`, `update`, `result`, and `error` summaries as lowercase inline code, including any dropped-update suffix in the same span. Impact: child labels read as compact quote-free outer JSON keys while root styling, disclosure state, evidence blocks, message height, and delivery guarantees remain unchanged.
12
+
3
13
  ## 0.26.11: Compact Tool Hierarchy And Ordered Settings Copy
4
14
 
5
15
  - `Tool Evidence Hierarchy`: Restyled nested Rich evidence summaries as plain lowercase labels (`arguments`, `update`, `result`, `error`) without icons, list markers, or heading emphasis. Arguments still open with the root and secondary evidence remains collapsed. Impact: child nodes read as quote-free outer JSON keys without adding message height or changing evidence behavior.
package/README.md CHANGED
@@ -67,6 +67,14 @@ Open the bot DM and send:
67
67
 
68
68
  The first Telegram user to message the bot becomes the allowed owner. Other users are ignored.
69
69
 
70
+ ### 5. Enable optional bot capabilities in BotFather
71
+
72
+ Enable the optional capabilities the bridge needs in [@BotFather](https://t.me/BotFather). The bridge does not fail loudly when a capability is off; the feature simply never triggers.
73
+
74
+ 1. Enable guest mode so the bot can answer mentions and replies in chats where it is not a member.
75
+ 2. Enable private-chat Threaded Mode; when it is available, one live instance becomes the profile's leader and later visible Pi instances register as followers. Without it, the bridge stays in classic single-owner DM mode.
76
+ 3. Make the bot an administrator in any chat where the queue reaction shortcuts (👍 promote, 👎 remove) should work. Reaction updates require admin rights, so the shortcuts silently do nothing in non-admin chats; private chats deliver reactions without admin rights.
77
+
70
78
  ## What It Feels Like
71
79
 
72
80
  - Start a task in the terminal, walk away, and keep supervising it from your phone.
package/docs/activity.md CHANGED
@@ -180,7 +180,7 @@ Pi provider events use `thinking_*`; the public product term is `reasoning`. The
180
180
 
181
181
  Tool activity uses Pi's executed-tool lifecycle (`tool_execution_start/update/end`), not provider `toolcall_*` payloads. Provider tool-call boundaries are used only to classify preceding assistant prose. This prevents duplicate tool rows and reports actual execution results.
182
182
 
183
- `args`, `update`, and `result` may contain paths, source text, command output, or other sensitive data. They are available to trusted local extension code but must not be rendered wholesale by default. Core `quiet` mode renders none of them. Visible tool modes redact known secret shapes, truncate every evidence field, and place completed tool evidence in native Rich Messages with an open-by-default `arguments` child plus closed `update`/`result`/`error` details; child summaries use plain lowercase labels without list markers, like quote-free outer JSON keys, while their evidence remains in JSON pre blocks.
183
+ `args`, `update`, and `result` may contain paths, source text, command output, or other sensitive data. They are available to trusted local extension code but must not be rendered wholesale by default. Core `quiet` mode renders none of them. Visible tool modes redact known secret shapes, truncate every evidence field, and place completed tool evidence in native Rich Messages with an open-by-default `arguments` child plus closed `update`/`result`/`error` details; child summaries use lowercase monospaced labels without icons, list markers, or heading emphasis, like quote-free outer JSON keys, while their evidence remains in JSON pre blocks.
184
184
 
185
185
  ## Delivery Context
186
186
 
@@ -315,7 +315,7 @@ Queue reactions are shortcut controls for waiting turns. Promotion reactions (`
315
315
 
316
316
  When `assistant.proactivePush` is enabled and this instance has exact direct or follower transport authority, completed public assistant text blocks from local/autonomous work are sent once and in source order to the instance's authorized target. Visible commentary/checkpoints and the final block use the configured Rich or HTML renderer. Proactive projection excludes reasoning, tool traffic, token deltas, local prompt text, Telegram-owned turns, and stale generations. Each admitted block remains fenced to its exact target, profile/token stamp, leader epoch or follower registration generation, and session generation; non-idempotent acknowledgement ambiguity never authorizes replay.
317
317
 
318
- `assistant.activity` is an independent bridge-owned projection over normalized Activity events. Each process reloads the shared file-backed setting at `agent-start` before activity admission, so multi-instance mode cannot continue projecting a stale broader process-local selection. Omitted values resolve to `verbose`, while invalid values fail closed to `quiet`; `thinking` and `tools` select one technical class, while `verbose` enables both. Provider-exposed thinking uses persistent ordinary HTML containing only a standard expandable blockquote with a bounded redacted latest-text window and inline Markdown rendered as Telegram HTML. Completed executed tools use native Rich Messages: each closed `<Tool>: <status>` root details node uses sentence case with a preserved uppercase two- or three-letter repeated prefix, then the native disclosure chevron reveals an open-by-default `arguments` child plus closed retained `update N` and `result`/`error` child details with plain lowercase, marker-free summaries and JSON pre blocks; known-safe Rich rejections fall back to the previous HTML disclosure. The projection captures the exact target and transport stamp at activity admission, serializes updates, preserves tool-start order, closes coalescing across assistant/thinking boundaries, bounds retained text/update memory plus edit frames and message/tool size, disables previews and HTTP(S) auto-link recognition inside technical evidence, and never replays a possibly committed send. Session generations own independent queues, so replacement drops queued old work without waiting on an old call. Both proactive prose and active-turn final delivery wait for the admitted activity queue inside their extension-owned delivery tasks, preserving technical-before-semantic ordering without delaying Pi lifecycle completion. Settlement, replacement, disconnect, failure, or stale authority clears only local ownership; already-sent activity messages remain in chat.
318
+ `assistant.activity` is an independent bridge-owned projection over normalized Activity events. Each process reloads the shared file-backed setting at `agent-start` before activity admission, so multi-instance mode cannot continue projecting a stale broader process-local selection. Omitted values resolve to `verbose`, while invalid values fail closed to `quiet`; `thinking` and `tools` select one technical class, while `verbose` enables both. Provider-exposed thinking uses persistent ordinary HTML containing only a standard expandable blockquote with a bounded redacted latest-text window and inline Markdown rendered as Telegram HTML. Completed executed tools use native Rich Messages: each closed `<Tool>: <status>` root details node uses sentence case with a preserved uppercase two- or three-letter repeated prefix, then the native disclosure chevron reveals an open-by-default `arguments` child plus closed retained `update N` and `result`/`error` child details with lowercase monospaced, marker-free summaries and JSON pre blocks; known-safe Rich rejections fall back to the previous HTML disclosure. The projection captures the exact target and transport stamp at activity admission, serializes updates, preserves tool-start order, closes coalescing across assistant/thinking boundaries, bounds retained text/update memory plus edit frames and message/tool size, disables previews and HTTP(S) auto-link recognition inside technical evidence, and never replays a possibly committed send. Session generations own independent queues, so replacement drops queued old work without waiting on an old call. Both proactive prose and active-turn final delivery wait for the admitted activity queue inside their extension-owned delivery tasks, preserving technical-before-semantic ordering without delaying Pi lifecycle completion. Settlement, replacement, disconnect, failure, or stale authority clears only local ownership; already-sent activity messages remain in chat.
319
319
 
320
320
  Telegram prompt guidance is context- and authority-aware. Only an exact direct owner or live registered follower exposes the three pi-telegram model tools, their active-tool metadata, and the compact local bridge suffix. Disconnect or authority loss removes those surfaces for subsequent requests without touching foreign tools; reconnect/recovery restores only the pi-telegram subset that was active before suspension, including across same-process reload. Telegram-originated turns receive a compact pointer to `telegram_help` plus dynamic prompt blocks such as `[voice] delivery: automatic voice`; full voice/button/direct-delivery/Threaded Mode syntax stays in the help tool rather than every system prompt.
321
321
 
package/docs/outbound.md CHANGED
@@ -17,7 +17,7 @@ Proactive blocks use `assistant.rendering` independently of voice policy. Rich m
17
17
  `assistant.activity` defaults to `verbose` when absent and accepts four modes: `quiet`, `thinking`, `tools`, and `verbose`. Explicit stored values remain unchanged; invalid values fail closed to `quiet`. Every Pi instance reloads this shared file-backed mode at `agent-start`, so a setting changed from one thread applies to subsequent runs in the other live instances without requiring process reload. `thinking` shows only provider-exposed thinking, `tools` shows only completed executed tools, and `verbose` shows both without mixing technical UI into assistant Markdown.
18
18
 
19
19
  - Provider-exposed thinking updates one persistent ordinary HTML message containing only a standard Telegram `<blockquote expandable>` with a bounded redacted latest-text window. Omitting a separate icon/level header saves one chat row while the disclosure's unique shape remains recognizable. Inline Markdown emphasis and code render as Telegram HTML instead of leaking raw markers. The bridge sends the message once, edits it only as thinking grows, and leaves the disclosure in chat; it never uses Rich drafts/Rich Messages. Providers that expose no thinking produce none.
20
- - Completed executed tools use native `sendRichMessage` block objects. Each tool is one closed root details node summarized as bold `<Tool>:` plus monospaced `<status>`; root labels remain sentence-case except that a leading two- or three-character repeated-letter prefix stays uppercase (`fffind` → `FFFind`). The native disclosure chevron identifies the row, and opening it immediately reveals the open `arguments` child plus separate closed retained `update N`, `result`, or `error` child details. Each child summary is a plain lowercase label—visually a quote-free outer JSON key with no list marker—and contains one preformatted `json` block. Updates remain chronological, dropped-update counts appear in the first retained update summary, and arrays of object entries keep the denser `[{ ... }, { ... }]` layout. A known-safe Rich HTTP 400 rejection falls back once to the previous expandable HTML representation; ambiguous non-idempotent outcomes never replay.
20
+ - Completed executed tools use native `sendRichMessage` block objects. Each tool is one closed root details node summarized as bold `<Tool>:` plus monospaced `<status>`; root labels remain sentence-case except that a leading two- or three-character repeated-letter prefix stays uppercase (`fffind` → `FFFind`). The native disclosure chevron identifies the row, and opening it immediately reveals the open `arguments` child plus separate closed retained `update N`, `result`, or `error` child details. Each child summary is one lowercase monospaced label—visually a quote-free outer JSON key with no icon, list marker, or heading emphasis—and contains one preformatted `json` block. Updates remain chronological, dropped-update counts appear in the first retained update summary, and arrays of object entries keep the denser `[{ ... }, { ... }]` layout. A known-safe Rich HTTP 400 rejection falls back once to the previous expandable HTML representation; ambiguous non-idempotent outcomes never replay.
21
21
  - Thinking `sendMessage`/`editMessageText` disables link previews and breaks HTTP(S) auto-link recognition inside evidence. Rich tool messages set `skip_entity_detection: true`, so URL-like arguments, updates, and results remain literal code. Consecutive tools coalesce by editing one message only while target, activity, generation, ordering boundary, tool count, and serialized-size bounds still match. Assistant or thinking content closes the batch. A non-idempotent send with unknown commit state is never replayed; failed or ambiguous edits start no fallback send.
22
22
 
23
23
  Thinking retains only a bounded latest-text window, tool updates retain only a bounded latest-entry window, and a session reset abandons queued old-generation work without making the replacement session wait for an old transport call. Final-answer delivery waits for the admitted activity queue inside the extension-owned background delivery task, so completed technical evidence cannot be overtaken by the semantic answer and Pi lifecycle completion remains non-blocking.
package/index.ts CHANGED
@@ -107,7 +107,7 @@ export default function (pi: Pi.ExtensionAPI) {
107
107
  Ownership.createTelegramBusMessageOwnershipRuntime({
108
108
  instanceId: telegramInstanceId,
109
109
  getProfileKey() {
110
- return getActiveTelegramThreadProfile() ?? "default";
110
+ return getActiveTelegramThreadProfile() ?? Config.TELEGRAM_DEFAULT_PROFILE_NAME;
111
111
  },
112
112
  listFollowers: telegramBusFollowerRegistry.list,
113
113
  });
@@ -117,9 +117,7 @@ export default function (pi: Pi.ExtensionAPI) {
117
117
  const runtimeEvents = runtimeDiagnostics.events;
118
118
  const recordRuntimeEvent = runtimeDiagnostics.recordRuntimeEvent;
119
119
  const configStore = Config.createTelegramConfigStore({ recordRuntimeEvent });
120
- const isTelegramBusRuntimeEnabled = function (): boolean {
121
- return !telegramThreadCapabilityState.isTopicModeUnavailable();
122
- };
120
+ const isTelegramBusRuntimeEnabled = telegramThreadCapabilityState.isBusRuntimeEnabled;
123
121
  Config.bindGlobalTelegramConfigRuntime(configStore);
124
122
  const configControls = Config.createTelegramConfigControls(configStore);
125
123
  const lockRuntime = Locks.createTelegramLockRuntime<Pi.ExtensionContext>({
@@ -225,7 +223,6 @@ export default function (pi: Pi.ExtensionAPI) {
225
223
  );
226
224
  const deferredQueueDispatchRuntime =
227
225
  Queue.createTelegramDeferredQueueDispatchRuntime<Pi.ExtensionContext>({
228
- delayMs: 50,
229
226
  recordRuntimeEvent,
230
227
  });
231
228
  const pollingControllerState = Polling.createTelegramPollingControllerState();
@@ -364,7 +361,6 @@ export default function (pi: Pi.ExtensionAPI) {
364
361
  getForwardCommentBatchPosition:
365
362
  textGroupRuntime.getPreparedForwardingPosition,
366
363
  recordRuntimeEvent,
367
- timeoutMs: 30_000,
368
364
  });
369
365
  const telegramApiRuntime = BusApi.createTelegramBusAwareApiRuntime({
370
366
  directRuntime: directTelegramApiRuntime,
@@ -446,7 +442,8 @@ export default function (pi: Pi.ExtensionAPI) {
446
442
  getActiveTurnTarget: activeTurnRuntime.getTarget,
447
443
  getActiveGuestQueryId: activeTurnRuntime.getGuestQueryId,
448
444
  });
449
- const deliveryGenerationSeed = `${telegramInstanceId}:${Date.now()}`;
445
+ const deliveryGenerationSeed =
446
+ Delivery.createTelegramDeliveryGenerationSeed(telegramInstanceId);
450
447
  const deliveryLifecycleRuntime =
451
448
  Delivery.createTelegramBridgeDeliveryLifecycleHooks({
452
449
  generationSeed: deliveryGenerationSeed,
@@ -687,10 +684,13 @@ export default function (pi: Pi.ExtensionAPI) {
687
684
  followerRegistry: telegramBusFollowerRegistry,
688
685
  followerTargetController,
689
686
  onRestored() {
690
- telegramSyncStateRuntime.markSliceFresh("target-bindings", {
691
- nowMs: Date.now(),
692
- action: "follower-thread-restore",
693
- });
687
+ telegramSyncStateRuntime.markSliceFresh(
688
+ Sync.TELEGRAM_SYNC_SLICE_TARGET_BINDINGS,
689
+ {
690
+ nowMs: Date.now(),
691
+ action: "follower-thread-restore",
692
+ },
693
+ );
694
694
  },
695
695
  });
696
696
  const observedThreadTargetBinding =
@@ -811,58 +811,54 @@ export default function (pi: Pi.ExtensionAPI) {
811
811
  handleAuthorizedReactionUpdate:
812
812
  inboundRouteRuntime.handleAuthorizedReactionUpdate,
813
813
  });
814
- const telegramBusFollowerAssembly: BusFollower.TelegramBusFollowerRuntimeAssembly<Pi.ExtensionContext> =
815
- BusFollower.createTelegramBusFollowerRuntimeAssembly<
814
+ const telegramBusFollowerAssemblyDeps =
815
+ BusFollower.createTelegramBusFollowerRuntimeAssemblyDeps<
816
816
  Pi.ExtensionContext,
817
817
  Updates.TelegramMessageReactionUpdated,
818
818
  Routing.TelegramRoutedCallbackQuery,
819
819
  Routing.TelegramRoutedMessage
820
820
  >({
821
+ instanceId: telegramInstanceId,
822
+ registrationState: telegramBusFollowerRegistrationState,
823
+ recordRuntimeEvent,
821
824
  receiver: {
822
825
  socketPath: getTelegramBusFollowerSocketPath,
823
- instanceId: telegramInstanceId,
824
826
  getContext: telegramSessionContextStore.get,
825
827
  getAuthSecret: telegramBusFollowerControlState.getActiveAuthSecret,
826
828
  ...forwardedRouteHandlers,
827
829
  prepareForwardedMessage: textGroupRuntime.prepareForwardedMessage,
828
- recordRuntimeEvent,
829
830
  },
830
831
  targetReplacement: {
831
832
  topicTargetStore: threadStore,
832
- registrationState: telegramBusFollowerRegistrationState,
833
- instanceId: telegramInstanceId,
834
833
  getManualFollowerProfileKey: getTelegramManualFollowerProfileKey,
835
834
  manualFollowerOwnerId: telegramManualFollowerOwnerId,
836
835
  getSyncState: telegramSyncStateRuntime.getState,
837
836
  setSyncState: telegramSyncStateRuntime.setState,
838
837
  updateStatus,
839
- recordRuntimeEvent,
840
838
  },
841
839
  recovery: {
842
- registrationState: telegramBusFollowerRegistrationState,
843
840
  getLeaderState: lockRuntime.getState,
844
841
  setLifecyclePhase: telegramBusFollowerControlState.setLifecyclePhase,
845
842
  updateStatus,
846
843
  promoteToLeader: promoteTelegramBusFollowerToLeader,
847
844
  getActiveContext: telegramSessionContextStore.get,
848
- recordRuntimeEvent,
849
845
  },
850
846
  registration: {
851
- instanceId: telegramInstanceId,
852
847
  getFollowerBusSocketPath: getTelegramBusFollowerSocketPath,
853
848
  getLeaderSocketPath: getTelegramBusSocketPath,
854
- registrationState: telegramBusFollowerRegistrationState,
855
849
  isContextActive: telegramSessionContextStore.isCurrent,
856
850
  createRequestId: telegramBusFollowerClients.createRequestId,
857
- getLeaderAuthSecret(owner) {
858
- return owner.busSecret;
859
- },
860
- setActiveAuthSecret:
861
- telegramBusFollowerControlState.setActiveAuthSecret,
851
+ setActiveAuthSecret: telegramBusFollowerControlState.setActiveAuthSecret,
862
852
  getProfileKey: getTelegramManualFollowerProfileKey,
863
- recordRuntimeEvent,
864
853
  },
865
854
  });
855
+ const telegramBusFollowerAssembly: BusFollower.TelegramBusFollowerRuntimeAssembly<Pi.ExtensionContext> =
856
+ BusFollower.createTelegramBusFollowerRuntimeAssembly<
857
+ Pi.ExtensionContext,
858
+ Updates.TelegramMessageReactionUpdated,
859
+ Routing.TelegramRoutedCallbackQuery,
860
+ Routing.TelegramRoutedMessage
861
+ >(telegramBusFollowerAssemblyDeps);
866
862
  const telegramBusFollowerRegistration =
867
863
  telegramBusFollowerAssembly.registration;
868
864
  const pollingRuntime = Polling.createTelegramPollingControllerRuntime({
@@ -1001,8 +997,8 @@ export default function (pi: Pi.ExtensionAPI) {
1001
997
  stopPolling: lockedPollingRuntime.stop,
1002
998
  recordRuntimeEvent,
1003
999
  });
1004
- const sessionLifecycleRuntime =
1005
- Lifecycle.createTelegramBridgeSessionLifecycleAssembly({
1000
+ const telegramBridgeSessionLifecycleDeps =
1001
+ Lifecycle.createTelegramBridgeSessionLifecycleDeps({
1006
1002
  contextStore: telegramSessionContextStore,
1007
1003
  queue: {
1008
1004
  getCurrentModel: getContextModel,
@@ -1038,20 +1034,24 @@ export default function (pi: Pi.ExtensionAPI) {
1038
1034
  recordRuntimeEvent,
1039
1035
  },
1040
1036
  services: {
1041
- resumeGroupedInput(ctx) {
1042
- mediaGroupRuntime.resume(ctx);
1043
- textGroupRuntime.resume(ctx);
1037
+ mediaGroup: {
1038
+ resume: mediaGroupRuntime.resume,
1039
+ suspend: mediaGroupRuntime.suspend,
1040
+ },
1041
+ textGroup: {
1042
+ resume: textGroupRuntime.resume,
1043
+ suspend: textGroupRuntime.suspend,
1044
1044
  },
1045
- suspendGroupedInput: TextGroups.createTelegramGroupedInputClearer({
1046
- clearMediaGroups: mediaGroupRuntime.suspend,
1047
- clearTextGroups: textGroupRuntime.suspend,
1048
- }),
1049
1045
  delivery: deliveryLifecycleRuntime,
1050
1046
  polling: lockedPollingRuntime,
1051
1047
  capabilityMonitor: telegramThreadCapabilityMonitor,
1052
1048
  queueWatchdog: queueDispatchWatchdogRuntime,
1053
1049
  },
1054
1050
  });
1051
+ const sessionLifecycleRuntime =
1052
+ Lifecycle.createTelegramBridgeSessionLifecycleAssembly(
1053
+ telegramBridgeSessionLifecycleDeps,
1054
+ );
1055
1055
 
1056
1056
  // --- Extension API Bindings ---
1057
1057
 
@@ -212,7 +212,7 @@ function createToolActivityDetail(
212
212
  ): TelegramInputRichBlock {
213
213
  return {
214
214
  type: "details",
215
- summary,
215
+ summary: { type: "code", text: summary },
216
216
  blocks: [{ type: "pre", text, language: "json" }],
217
217
  ...(isOpen ? { is_open: true as const } : {}),
218
218
  };
@@ -514,6 +514,91 @@ export function createTelegramBusForwardedRouteHandlers<
514
514
  };
515
515
  }
516
516
 
517
+ export interface TelegramBusFollowerRuntimeAssemblyPorts<
518
+ TContext extends { cwd?: string },
519
+ TReactionUpdate,
520
+ TCallbackQuery,
521
+ TMessage = unknown,
522
+ > {
523
+ instanceId: string;
524
+ registrationState: TelegramBusFollowerRegistrationState;
525
+ recordRuntimeEvent: (
526
+ category: string,
527
+ error: unknown,
528
+ details?: Record<string, unknown>,
529
+ ) => void;
530
+ receiver: Omit<
531
+ TelegramBusForwardedUpdateReceiverRuntimeDeps<
532
+ TContext,
533
+ TReactionUpdate,
534
+ TCallbackQuery,
535
+ TMessage
536
+ >,
537
+ "handleReplaceTarget" | "instanceId" | "recordRuntimeEvent"
538
+ >;
539
+ targetReplacement: Omit<
540
+ TelegramBusFollowerTargetReplacementHandlerDeps<TContext>,
541
+ "registrationState" | "instanceId" | "recordRuntimeEvent"
542
+ >;
543
+ recovery: Omit<
544
+ TelegramBusFollowerHeartbeatRecoveryHandlerDeps<TContext>,
545
+ "getRegistrationRuntime" | "registrationState" | "recordRuntimeEvent"
546
+ >;
547
+ registration: Omit<
548
+ TelegramBusFollowerRegistrationRuntimeDeps<TContext>,
549
+ | "startReceiving"
550
+ | "stopReceiving"
551
+ | "onHeartbeatFailure"
552
+ | "instanceId"
553
+ | "registrationState"
554
+ | "recordRuntimeEvent"
555
+ >;
556
+ }
557
+
558
+ export function createTelegramBusFollowerRuntimeAssemblyDeps<
559
+ TContext extends { cwd?: string },
560
+ TReactionUpdate,
561
+ TCallbackQuery,
562
+ TMessage = unknown,
563
+ >(
564
+ ports: TelegramBusFollowerRuntimeAssemblyPorts<
565
+ TContext,
566
+ TReactionUpdate,
567
+ TCallbackQuery,
568
+ TMessage
569
+ >,
570
+ ): TelegramBusFollowerRuntimeAssemblyDeps<
571
+ TContext,
572
+ TReactionUpdate,
573
+ TCallbackQuery,
574
+ TMessage
575
+ > {
576
+ return {
577
+ receiver: {
578
+ ...ports.receiver,
579
+ instanceId: ports.instanceId,
580
+ recordRuntimeEvent: ports.recordRuntimeEvent,
581
+ },
582
+ targetReplacement: {
583
+ ...ports.targetReplacement,
584
+ instanceId: ports.instanceId,
585
+ registrationState: ports.registrationState,
586
+ recordRuntimeEvent: ports.recordRuntimeEvent,
587
+ },
588
+ recovery: {
589
+ ...ports.recovery,
590
+ registrationState: ports.registrationState,
591
+ recordRuntimeEvent: ports.recordRuntimeEvent,
592
+ },
593
+ registration: {
594
+ ...ports.registration,
595
+ instanceId: ports.instanceId,
596
+ registrationState: ports.registrationState,
597
+ recordRuntimeEvent: ports.recordRuntimeEvent,
598
+ },
599
+ };
600
+ }
601
+
517
602
  export function createTelegramBusFollowerRuntimeAssembly<
518
603
  TContext extends { cwd?: string },
519
604
  TReactionUpdate,
@@ -1260,7 +1345,9 @@ export function createTelegramBusFollowerRegistrationRuntime<
1260
1345
  deps.getLeaderSocketPath?.() ??
1261
1346
  getTelegramBusSocketPath();
1262
1347
  await deps.startReceiving?.();
1263
- activeAuthSecret = deps.getLeaderAuthSecret?.(leader);
1348
+ activeAuthSecret = deps.getLeaderAuthSecret
1349
+ ? deps.getLeaderAuthSecret(leader)
1350
+ : leader?.busSecret;
1264
1351
  deps.setActiveAuthSecret?.(activeAuthSecret);
1265
1352
  const registrationGeneration = deps.createRequestId();
1266
1353
  const registrationEnvelope: Extract<
package/lib/delivery.ts CHANGED
@@ -198,6 +198,10 @@ export function createTelegramDeliveryLifecycleHooks(
198
198
  };
199
199
  }
200
200
 
201
+ export function createTelegramDeliveryGenerationSeed(instanceId: string): string {
202
+ return `${instanceId}:${Date.now()}`;
203
+ }
204
+
201
205
  /** @internal */
202
206
  export function createTelegramBridgeDeliveryLifecycleHooks<TTransportStamp>(
203
207
  deps: Omit<
package/lib/lifecycle.ts CHANGED
@@ -6,6 +6,7 @@
6
6
 
7
7
  import * as BusFollower from "./bus-follower.ts";
8
8
  import * as Queue from "./queue.ts";
9
+ import * as TextGroups from "./text-groups.ts";
9
10
  import type {
10
11
  AgentEndEvent,
11
12
  AgentSettledEvent,
@@ -253,6 +254,67 @@ export interface TelegramBridgeSessionLifecycleAssemblyDeps<
253
254
  services: TelegramBridgeSessionServiceRuntime;
254
255
  }
255
256
 
257
+ export interface TelegramBridgeSessionLifecyclePorts<
258
+ TQueueItem,
259
+ TModel = unknown,
260
+ > {
261
+ contextStore: TelegramSessionContextStore<ExtensionContext>;
262
+ queue: Omit<
263
+ Queue.TelegramSessionLifecycleRuntimeDeps<
264
+ ExtensionContext,
265
+ TQueueItem,
266
+ TModel
267
+ >,
268
+ "isSessionActive" | "stopPolling" | "clearPendingMediaGroups"
269
+ >;
270
+ follower: Omit<
271
+ BusFollower.TelegramBusFollowerSessionRefreshHookDeps<ExtensionContext>,
272
+ "isSessionActive"
273
+ > &
274
+ BusFollower.TelegramBusFollowerSessionReplacementSuspenderDeps;
275
+ services: {
276
+ mediaGroup: {
277
+ resume(ctx: ExtensionContext): void;
278
+ suspend(): void;
279
+ };
280
+ textGroup: {
281
+ resume(ctx: ExtensionContext): void;
282
+ suspend(): void;
283
+ };
284
+ delivery: TelegramBridgeSessionServiceRuntime["delivery"];
285
+ polling: TelegramBridgeSessionServiceRuntime["polling"];
286
+ capabilityMonitor: TelegramBridgeSessionServiceRuntime["capabilityMonitor"];
287
+ queueWatchdog: TelegramBridgeSessionServiceRuntime["queueWatchdog"];
288
+ };
289
+ }
290
+
291
+ export function createTelegramBridgeSessionLifecycleDeps<
292
+ TQueueItem,
293
+ TModel = unknown,
294
+ >(
295
+ ports: TelegramBridgeSessionLifecyclePorts<TQueueItem, TModel>,
296
+ ): TelegramBridgeSessionLifecycleAssemblyDeps<TQueueItem, TModel> {
297
+ return {
298
+ contextStore: ports.contextStore,
299
+ queue: ports.queue,
300
+ follower: ports.follower,
301
+ services: {
302
+ resumeGroupedInput(ctx) {
303
+ ports.services.mediaGroup.resume(ctx);
304
+ ports.services.textGroup.resume(ctx);
305
+ },
306
+ suspendGroupedInput: TextGroups.createTelegramGroupedInputClearer({
307
+ clearMediaGroups: ports.services.mediaGroup.suspend,
308
+ clearTextGroups: ports.services.textGroup.suspend,
309
+ }),
310
+ delivery: ports.services.delivery,
311
+ polling: ports.services.polling,
312
+ capabilityMonitor: ports.services.capabilityMonitor,
313
+ queueWatchdog: ports.services.queueWatchdog,
314
+ },
315
+ };
316
+ }
317
+
256
318
  export function createTelegramBridgeSessionLifecycleAssembly<
257
319
  TQueueItem,
258
320
  TModel = unknown,
package/lib/polling.ts CHANGED
@@ -351,6 +351,7 @@ export interface TelegramThreadCapabilityStateRuntime {
351
351
  setBusPollingStarted(started: boolean): void;
352
352
  isTopicModeUnavailable(): boolean;
353
353
  setTopicModeUnavailable(unavailable: boolean): void;
354
+ isBusRuntimeEnabled(): boolean;
354
355
  shouldForceFreshLeaderThread(): boolean;
355
356
  setForceFreshLeaderThread(forceFresh: boolean): void;
356
357
  }
@@ -458,6 +459,7 @@ export function createTelegramThreadCapabilityStateRuntime(): TelegramThreadCapa
458
459
  setTopicModeUnavailable(unavailable) {
459
460
  topicModeUnavailable = unavailable;
460
461
  },
462
+ isBusRuntimeEnabled: () => !topicModeUnavailable,
461
463
  shouldForceFreshLeaderThread: () => forceFreshLeaderThread,
462
464
  setForceFreshLeaderThread(forceFresh) {
463
465
  forceFreshLeaderThread = forceFresh;
package/lib/queue.ts CHANGED
@@ -2133,13 +2133,19 @@ export interface TelegramDeferredQueueDispatchRuntime<TContext = unknown> {
2133
2133
  request: (dispatchNextQueuedTelegramTurn: (ctx: TContext) => void) => void;
2134
2134
  }
2135
2135
 
2136
+ /**
2137
+ * Production debounce for deferred queue dispatch; the factory defaults to this
2138
+ * so the entrypoint wires ports instead of policy constants.
2139
+ */
2140
+ export const TELEGRAM_DEFERRED_DISPATCH_DELAY_MS = 50;
2141
+
2136
2142
  export function createTelegramDeferredQueueDispatchRuntime<TContext = unknown>(
2137
2143
  deps: TelegramDeferredQueueDispatchRuntimeDeps = {},
2138
2144
  ): TelegramDeferredQueueDispatchRuntime<TContext> {
2139
2145
  let boundContext: TContext | undefined;
2140
2146
  let generation = 0;
2141
2147
  const timers = new Set<ReturnType<typeof setTimeout>>();
2142
- const delayMs = deps.delayMs ?? 0;
2148
+ const delayMs = deps.delayMs ?? TELEGRAM_DEFERRED_DISPATCH_DELAY_MS;
2143
2149
  const setTimer =
2144
2150
  deps.setTimer ??
2145
2151
  ((callback: () => void, ms: number): ReturnType<typeof setTimeout> =>
package/lib/sync.ts CHANGED
@@ -486,6 +486,8 @@ export async function ensureTelegramLeaderThreadBinding(
486
486
  return ownTarget;
487
487
  }
488
488
 
489
+ export const TELEGRAM_SYNC_SLICE_TARGET_BINDINGS = "target-bindings";
490
+
489
491
  export const TELEGRAM_SYNC_SLICES = [
490
492
  "bot-identity",
491
493
  "bot-capabilities",
@@ -493,7 +495,7 @@ export const TELEGRAM_SYNC_SLICES = [
493
495
  "allowed-user",
494
496
  "topic-capability",
495
497
  "topic-state",
496
- "target-bindings",
498
+ TELEGRAM_SYNC_SLICE_TARGET_BINDINGS,
497
499
  "reservations",
498
500
  "transport-health",
499
501
  ] as const;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.26.11",
3
+ "version": "0.26.13",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"