@llblab/pi-telegram 0.8.2 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -131,6 +131,7 @@ The canonical detailed ownership map lives in [`docs/architecture.md`](./docs/ar
131
131
  - For `/telegram-setup`, prefer the locally saved bot token over environment variables on repeat setup runs; env vars are the bootstrap path when no local token exists, and persisted `telegram.json` writes must remain atomic plus private because status/setup/polling paths may read it concurrently
132
132
  - Command help plus prompt-template commands and status/model/thinking/queue controls are driven through `/start`'s Telegram inline application menu and callback queries; the Queue button shows the queued-item count, model-menu scope/pagination controls stay at the top under Main menu, the model pagination indicator opens a compact page picker, and thinking-menu text stays a compact heading because the current level is marked by button state; `/status`, `/model`, `/thinking`, and `/queue` are hidden compatibility shortcuts
133
133
  - Shared inline-keyboard structure belongs to `keyboard`; application-control button labels, callback data, and callback behavior stay in `menu`/`menu-model`/`menu-thinking`/`menu-status`/`menu-queue` while core queue mechanics stay in `queue`
134
+ - Telegram `/settings` options should open nested detail submenus by default: checkbox options show a description plus Back, On, and Off; list options show Back plus selectable values. One-shot actions such as syncing may run directly without a submenu when there is no meaningful choice or description step.
134
135
  - Inbound text/media may be transformed through configured `inboundHandlers` before queueing; legacy `attachmentHandlers` are deprecated compatibility aliases appended after `inboundHandlers`; outbound files must flow through `telegram_attach`
135
136
  - Long Telegram text split recovery belongs to `text-groups`: keep it conservative, short-debounced, same chat/user/message-id contiguous, and gated by near-limit human text so normal rapid follow-ups and slash commands stay separate
136
137
  - Inbound handlers and command-backed outbound handlers use command templates as the standard integration contract; built-in outbound buttons use inline keyboards plus callback routing because no external command execution is needed
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.1: Model Detail Hotfix
4
+
5
+ - `[Model Menu]` Detail-mode activation now preserves scoped `thinkingLevel` by resolving the selected scoped entry before falling back to the unscoped model list. Impact: scoped model shortcuts opened through the detail submenu keep their reasoning/thinking level.
6
+ - `[Model Menu]` Activating an already active model from the detail submenu now still runs the refresh path that applies scoped thinking changes while returning to the model list. Impact: tapping Active can still correct the thinking level instead of becoming a no-op.
7
+ - `[Proactive Push]` Removed the unused proactive reply-target store and always sends proactive local-result pushes without `reply_to_message_id`. Impact: the runtime no longer carries dead state for a target-capture behavior that does not exist yet.
8
+ - `[Queue Reactions]` Added `🔥` as a priority reaction and `🗑` as a queue-removal reaction. Impact: the intuitive fire/removal gestures now work alongside the existing reaction controls.
9
+ - `[Docs]` Updated the status-bar example to match the compact active/queued display.
10
+ - `[Package]` Bumped package metadata to `0.9.1` and kept the lockfile in sync.
11
+
12
+ ## 0.9.0: Hidden Settings And Proactive Push
13
+
14
+ - `[Settings Menu]` Added hidden Telegram `/settings` with a proactive push checkbox detail submenu plus `/telegram-settings` in the terminal. Impact: operators can see green/black binary flag state, use green/black/yellow On/Off checkbox controls from Telegram, and toggle the same proactive push flag locally without adding a visible bot-command entry.
15
+ - `[Proactive Push]` `telegram.json` now supports `proactivePush`; when enabled, successful local non-Telegram π final replies are sent to the paired Telegram chat if no Telegram turn is active and the current session still owns the Telegram lock. Local prompt text stays private because the bot does not own or mirror terminal user messages. Impact: long local tasks can notify the phone with result context without leaking from stale bridge owners or failed/aborted turns.
16
+ - `[Queue UI]` Empty queue states now use the bottom-filled `⌛` hourglass while non-empty queue states keep `⏳`. Queue item details now show the selected queue position above the raw prompt preview, preserve reaction-specific priority emoji in the heading, and use side-by-side Priority/Normal tabs that refresh the heading marker immediately. The terminal status bar now stays yellow active while Telegram-owned work still has running tools even if a queued prompt is removed by reaction. Impact: queue emptiness has a small visual easter egg, item submenus stay oriented without changing queue semantics, and queue-removal reactions no longer visually degrade active work to connected.
17
+ - `[Model Menu]` Model rows now open a detail submenu with Back, ☑️ Activate/🟢 Active selection, and yellow/black-marked Scoped/All membership tabs. Impact: model selection remains one tap away while scoped model membership can be managed from Telegram.
18
+ - `[Status Menu]` The main Telegram menu status row now shows `compacting` while a Telegram `/compact` run is active. Impact: the phone UI reflects the same compaction state that already blocks dispatch and appears in terminal status.
19
+ - `[Prompt Guidance]` Telegram prompt injection now asks agents to target 37 visible cells for tables, dense list items, and compact text blocks. Impact: replies better fit narrow mobile Telegram screens, especially when emoji or wide glyphs are present.
20
+
3
21
  ## 0.8.2: Lock-Safe Delivery
4
22
 
5
23
  - `[Lock Safety]` Active Telegram turns now re-check singleton ownership before preview flushes and final agent-end delivery. Impact: an old π instance stays silent after another instance takes the Telegram bridge lock, even if the old instance finishes a long-running prompt later.
@@ -47,7 +65,7 @@
47
65
  ## 0.7.0: Unified App Menu & Command Template Hardening
48
66
 
49
67
  - `[Commands]` Visible Telegram bot command menu now exposes `/start`, `/compact`, `/next`, `/continue`, `/abort`, and `/stop`; `/help`, `/status`, `/model`, `/thinking`, and `/queue` remain hidden compatibility shortcuts. `/start`, `/help`, and `/status` open one unified app menu containing command help, status rows, and inline controls. Command emoji are centralized as fixed adornments in the commands domain and reused by matching menu buttons (`🤖` model, `🧠` thinking); `/next` uses `⏩` and `/continue` uses `▶️`. `/continue` enqueues a priority Telegram-owned `continue` prompt instead of forcing the next queued item or requiring π to be idle. Impact: the visible command surface is cleaner while existing operator muscle memory still works and skills can react to queued `continue` prompts.
50
- - `[Application Menu]` `/start` opens command help plus status rows and the inline application menu; `/queue` opens the queue section directly, the status menu Queue button shows the current queued-item count, all submenus keep Back/Main menu navigation in the top row, and queued items are listed in dispatch order with numeric labels plus `⚡`/`📎` markers. Queue menu message text uses the same HTML heading style as the other inline menus; empty queue menus render bold message text with only the Main menu navigation button instead of a disabled empty-state button. Item submenus support Back, priority toggle, and Cancel, and stale item clicks refresh the live list. Impact: queued Telegram work is inspectable and mutable from the menu control surface without relying only on reactions.
68
+ - `[Application Menu]` `/start` opens command help plus status rows and the inline application menu; `/queue` opens the queue section directly, the status menu Queue button shows the current queued-item count, all submenus keep Back/Main menu navigation in the top row, and queued items are listed in dispatch order with numeric labels plus `⚡`/`📎` markers. Queue menu message text uses the same HTML heading style as the other inline menus; empty queue menus render bold message text with only the Main menu navigation button instead of a disabled empty-state button. Item submenus support Back, Priority/Normal tabs, and Cancel, and stale item clicks refresh the live list. Impact: queued Telegram work is inspectable and mutable from the menu control surface without relying only on reactions.
51
69
  - `[Prompt Templates]` `/start` now shows a separate block for π prompt-template commands, and the Telegram bot command menu registers Telegram-safe prompt-template aliases such as `fix-tests` → `/fix_tests` when they do not conflict with built-in bridge commands or hidden shortcuts. Sending `/template_name args` from Telegram expands the matching π prompt-template file before queueing the turn. Impact: reusable π workflows are available from Telegram without duplicating prompt text manually.
52
70
  - `[Keyboard]` Shared Telegram inline-keyboard reply-markup structure was extracted to `keyboard`, while `menu` owns application-control button semantics and `outbound-handlers` owns assistant-authored button semantics. Impact: inline UI domains share one Bot API shape without centralizing feature behavior.
53
71
  - `[Domain DAG]` Source-module opening comments now include `Zones:` tags for cross-cutting responsibility areas such as Telegram transport, π agent lifecycle, TUI, and shared utilities. Impact: flat files keep folder-like orientation without adding directory nesting.
package/README.md CHANGED
@@ -13,7 +13,7 @@ This repository is an actively maintained fork of [`badlogic/pi-telegram`](https
13
13
 
14
14
  ## Key Features
15
15
 
16
- - **Telegram Controls**: `/start` opens the inline application menu with command help, available π prompt templates, status rows, model, thinking, and queue sections; `/stop`, `/abort`, `/next`, and `/continue` provide queue-clear, queue-preserve, force-next, and queued-resume semantics respectively; model-switch continuation turns still use the control lane when a restart needs to resume safely.
16
+ - **Telegram Controls**: `/start` opens the inline application menu with command help, available π prompt templates, status rows, model, thinking, and queue sections; the Status row reports `compacting` during Telegram `/compact`; `/stop`, `/abort`, `/next`, and `/continue` provide queue-clear, queue-preserve, force-next, and queued-resume semantics respectively; model-switch continuation turns still use the control lane when a restart needs to resume safely.
17
17
  - **Interactive UI**: Manage your session directly from Telegram. Inline buttons expose an application menu for switching models, choosing model pages from the pagination indicator, adjusting reasoning (thinking) levels, and inspecting or mutating the waiting queue; model scope/pagination controls stay at the top of the model menu, the Queue button shows the current item count, and command emoji are reused on matching controls such as model and thinking.
18
18
  - **In-flight Model Switching**: Change the active model mid-generation. The agent gracefully pauses, applies the new model, and restarts its response without losing context.
19
19
  - **Smart Message Queue**: Messages sent while the agent is busy are queued and previewed in the π status bar, and queued turns can be reprioritized or removed with Telegram reactions or the queue section of the inline application menu.
@@ -92,9 +92,9 @@ Use these inside the Telegram DM with your bot:
92
92
 
93
93
  Prompt-template commands: π prompt templates are mapped to Telegram-safe aliases (`fix-tests.md` becomes `/fix_tests`) and shown as compact command-only rows between the built-in commands and status rows in `/start`. They are not registered in the Telegram bot command menu, keeping the bot menu focused on bridge controls. Sending `/template_name args` from Telegram expands the matching π prompt-template file and queues the expanded prompt like normal Telegram work.
94
94
 
95
- Hidden compatibility shortcuts: `/help` and `/status` open the same main application menu, `/model` opens the model section, `/thinking` opens the thinking section, and `/queue` opens the queue section. They are intentionally not shown in the bot command menu.
95
+ Hidden compatibility shortcuts: `/help` and `/status` open the same main application menu, `/model` opens the model section, `/thinking` opens the thinking section, `/queue` opens the queue section, and `/settings` opens hidden bridge settings. Settings rows open detail submenus with Back plus green/black/yellow option controls such as On and Off for checkboxes. These shortcuts are intentionally not shown in the bot command menu.
96
96
 
97
- Telegram command admission is explicit: `/compact`, `/queue`, `/stop`, `/abort`, `/next`, `/help`, `/start`, `/status`, `/model`, and `/thinking` execute immediately. `/continue` is a command shortcut that enqueues a priority Telegram prompt containing `continue`. Prompt-template commands expand before queueing and then follow normal prompt-queue rules. Synthetic model-switch continuation turns still enter the high-priority control lane so they can resume before normal queued prompts when π becomes safe to dispatch.
97
+ Telegram command admission is explicit: `/compact`, `/queue`, `/settings`, `/stop`, `/abort`, `/next`, `/help`, `/start`, `/status`, `/model`, and `/thinking` execute immediately. `/continue` is a command shortcut that enqueues a priority Telegram prompt containing `continue`. Prompt-template commands expand before queueing and then follow normal prompt-queue rules. Synthetic model-switch continuation turns still enter the high-priority control lane so they can resume before normal queued prompts when π becomes safe to dispatch.
98
98
 
99
99
  ### Pi Commands
100
100
 
@@ -102,6 +102,7 @@ Run these inside π, not Telegram:
102
102
 
103
103
  - **`/telegram-setup`**: Configure or update the Telegram bot token.
104
104
  - **`/telegram-status`**: Check bridge status, connection, polling, execution, queue, and recent redacted runtime/API failure events.
105
+ - **`/telegram-settings`**: Open local bridge settings and toggle proactive push using the same `telegram.json` flag as the Telegram `/settings` menu.
105
106
  - **`/telegram-connect`**: Start polling Telegram updates in the current π session, acquire the singleton lock, or interactively move ownership here from another live instance.
106
107
  - **`/telegram-disconnect`**: Stop polling in the current π session and release the singleton lock.
107
108
 
@@ -109,8 +110,8 @@ Run these inside π, not Telegram:
109
110
 
110
111
  - If you send more Telegram messages while π is busy, they enter the default prompt queue and are processed in order.
111
112
  - Very long text messages that Telegram appears to split automatically are coalesced through a short conservative debounce and forwarded to π as one prompt when the first chunk is near Telegram's text limit, currently using a 3600-character threshold. Commands, bot messages, media groups, and normal short follow-ups are not coalesced.
112
- - `👍`, `⚡️`, `❤️`, and `🕊` move a waiting prompt into the priority prompt queue, behind control actions but ahead of default prompts. Removing the last priority reaction sends it back to its normal queue position, and adding a priority reaction again gives it a fresh priority position.
113
- - `👎`, `👻`, `💔`, and `💩` remove a waiting turn from the queue. Telegram Bot API does not expose ordinary DM message-deletion events through the polling path used here, so queue removal is bound to removal reactions.
113
+ - `👍`, `⚡️`, `❤️`, `🕊`, and `🔥` move a waiting prompt into the priority prompt queue, behind control actions but ahead of default prompts. Removing the last priority reaction sends it back to its normal queue position, and adding a priority reaction again gives it a fresh priority position.
114
+ - `👎`, `👻`, `💔`, `💩`, and `🗑` remove a waiting turn from the queue. Telegram Bot API does not expose ordinary DM message-deletion events through the polling path used here, so queue removal is bound to removal reactions.
114
115
  - Reactions apply to any waiting Telegram turn, including text, voice, files, images, and media groups. For media groups, a reaction on any message in the group applies to the whole queued turn.
115
116
  - If you edit a Telegram message while it is still waiting in the queue, the queued turn is updated instead of creating a duplicate prompt. Edits after a turn has already started may not affect the active run.
116
117
  - Telegram replies to earlier text or caption messages are forwarded as `[reply]` context for normal prompts, while slash commands still parse from the new message text only.
@@ -119,6 +120,8 @@ Run these inside π, not Telegram:
119
120
 
120
121
  ### Inbound Handlers
121
122
 
123
+ `telegram.json` can set `proactivePush: true` to send successful local non-Telegram final replies to the paired Telegram chat when no Telegram turn is active. Local prompt text is not sent because the bot does not own or mirror terminal user messages. The mode is off by default, can be toggled from the hidden `/settings` menu, persists across contexts until explicitly disabled or removed from config, is gated by the current Telegram lock owner, and skips aborted or failed turns.
124
+
122
125
  `telegram.json` can define ordered `inboundHandlers` for Telegram → π preprocessing such as text translation, voice transcription, OCR, or PDF extraction. Matching handlers run before the Telegram turn enters the π queue. If a matching media/file handler fails, the next matching handler is tried as a fallback. Legacy `attachmentHandlers` still work as a deprecated compatibility alias and are appended after `inboundHandlers`.
123
126
 
124
127
  ```json
@@ -231,10 +234,10 @@ Rich previews are sent through editable messages because Telegram drafts are tex
231
234
 
232
235
  ## Status bar
233
236
 
234
- The π status bar shows the current bridge state plus queued Telegram turns as compact previews. Busy labels distinguish states such as `active`, `dispatching`, `queued`, `tool running`, `model`, and `compacting`.
237
+ The π status bar shows the current bridge state plus queued Telegram turns as compact previews. Busy labels distinguish states such as `active`, `dispatching`, `queued`, `tool running`, `model`, and `compacting`. Telegram prompt guidance asks agents to keep tables, dense list items, and compact text blocks within about 37 visible cells when possible so mobile replies stay readable.
235
238
 
236
239
  ```text
237
- telegram queued +3: [⚡ write a shell script…, summarize this image…, 📎 2 attachments]
240
+ telegram active +3
238
241
  ```
239
242
 
240
243
  ## Notes
@@ -23,24 +23,24 @@ Naming rule: because the repository already scopes this codebase to Telegram, ex
23
23
 
24
24
  Current runtime areas use these ownership boundaries:
25
25
 
26
- | Domain | Owns |
27
- | ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
28
- | `index.ts` | Single composition root for live π/Telegram ports, session state, API-bound transport adapters, and status updates |
29
- | `api` | Bot API transport shapes/helpers, retries, file download, temp-dir lifecycle, inbound limits, chat actions, lazy bot-token clients, runtime error recording |
30
- | `config` / `setup` | Persisted bot/session pairing state, authorization, first-user pairing, token prompting, env fallback, validation, config persistence |
31
- | `locks` / `polling` | Singleton `locks.json` ownership, takeover/restart semantics, long-poll controller state, update offset persistence, poll-loop runtime wiring |
32
- | `updates` / `routing` | Update classification/execution planning, paired authorization, reactions, edits, callbacks, and inbound route composition |
33
- | `media` / `text-groups` / `turns` / `inbound-handlers` | Text/media extraction, media-group debounce, long-text split coalescing, inbound downloads, inbound text/media handler execution, turn building/editing, image reads, legacy `attachmentHandlers` compatibility |
34
- | `queue` | Queue item contracts, lane admission/order, stores, mutations, dispatch readiness/runtime, prompt/control enqueueing, session and agent/tool lifecycle sequencing |
35
- | `runtime` | Session-local coordination primitives: counters, lifecycle flags, setup guard, abort handler, typing-loop timers, prompt-dispatch flags, agent-end reset binding |
36
- | `model` / `menu-model` / `menu-thinking` / `menu-status` / `menu` / `menu-queue` / `commands` | Model identity/thinking levels, scoped model resolution, in-flight switching, model-menu UI, thinking-menu UI, status-menu UI, inline application callback composition, queue-menu UI, slash commands, bot command registration |
37
- | `keyboard` | Shared Telegram inline-keyboard reply-markup structure; feature domains own callback semantics and button construction |
38
- | `preview` / `replies` / `rendering` | Preview lifecycle/transports, final reply delivery and reply parameters, Telegram HTML Markdown rendering, chunking, stable-preview snapshots |
39
- | `outbound-handlers` | Outbound text transformation, assistant-authored outbound comments, generated reply artifacts, inline-keyboard callbacks, and post-`agent_end` outbound action delivery |
40
- | `outbound-attachments` | `telegram_attach` registration, outbound attachment queueing, stat/limit checks, photo/document delivery classification |
41
- | `status` | Status-bar/status-message rendering, queue-lane status views, redacted runtime event ring, grouped π diagnostics |
42
- | `lifecycle` / `prompts` / `prompt-templates` / `pi` | π hook registration, Telegram-specific before-agent prompt injection, π prompt-template discovery/expansion, centralized direct pi SDK imports and context adapters |
43
- | `command-templates` | Portable shell-free command-template standard helpers, composition expansion, placeholder substitution, and executable resolution |
26
+ | Domain | Owns |
27
+ | --------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
28
+ | `index.ts` | Single composition root for live π/Telegram ports, session state, API-bound transport adapters, and status updates |
29
+ | `api` | Bot API transport shapes/helpers, retries, file download, temp-dir lifecycle, inbound limits, chat actions, lazy bot-token clients, runtime error recording |
30
+ | `config` / `setup` | Persisted bot/session pairing state, authorization, first-user pairing, token prompting, env fallback, validation, config persistence |
31
+ | `locks` / `polling` | Singleton `locks.json` ownership, takeover/restart semantics, long-poll controller state, update offset persistence, poll-loop runtime wiring |
32
+ | `updates` / `routing` | Update classification/execution planning, paired authorization, reactions, edits, callbacks, and inbound route composition |
33
+ | `media` / `text-groups` / `turns` / `inbound-handlers` | Text/media extraction, media-group debounce, long-text split coalescing, inbound downloads, inbound text/media handler execution, turn building/editing, image reads, legacy `attachmentHandlers` compatibility |
34
+ | `queue` | Queue item contracts, lane admission/order, stores, mutations, dispatch readiness/runtime, prompt/control enqueueing, session and agent/tool lifecycle sequencing |
35
+ | `runtime` | Session-local coordination primitives: counters, lifecycle flags, setup guard, abort handler, typing-loop timers, prompt-dispatch flags, agent-end reset binding |
36
+ | `model` / `menu-model` / `menu-thinking` / `menu-status` / `menu` / `menu-queue` / `menu-settings` / `commands` | Model identity/thinking levels, scoped model resolution, in-flight switching, model-menu UI, thinking-menu UI, status-menu UI, inline application callback composition, queue-menu UI, hidden settings UI, slash commands, bot command registration |
37
+ | `keyboard` | Shared Telegram inline-keyboard reply-markup structure; feature domains own callback semantics and button construction |
38
+ | `preview` / `replies` / `rendering` | Preview lifecycle/transports, final reply delivery and reply parameters, Telegram HTML Markdown rendering, chunking, stable-preview snapshots |
39
+ | `outbound-handlers` | Outbound text transformation, assistant-authored outbound comments, generated reply artifacts, inline-keyboard callbacks, and post-`agent_end` outbound action delivery |
40
+ | `outbound-attachments` | `telegram_attach` registration, outbound attachment queueing, stat/limit checks, photo/document delivery classification |
41
+ | `status` | Status-bar/status-message rendering, queue-lane status views, redacted runtime event ring, grouped π diagnostics |
42
+ | `lifecycle` / `prompts` / `prompt-templates` / `pi` | π hook registration, Telegram-specific before-agent prompt injection, π prompt-template discovery/expansion, centralized direct pi SDK imports and context adapters |
43
+ | `command-templates` | Portable shell-free command-template standard helpers, composition expansion, placeholder substitution, and executable resolution |
44
44
 
45
45
  Boundary invariants:
46
46
 
@@ -95,15 +95,15 @@ Queued items now use two explicit dimensions:
95
95
 
96
96
  Admission contract:
97
97
 
98
- | Admission | Examples | Queue shape | Dispatch rank |
99
- | --------------------- | ------------------------------------------------------------ | -------------------------------------------------------------------- | ------------- |
100
- | Immediate execution | `/compact`, `/queue`, `/stop`, `/help`, `/start` | Does not enter the Telegram queue; `/help` opens the same menu as `/start`; `/stop` also clears queued items | N/A |
101
- | Queued prompt command | `/continue`, `/template_name args` | `/continue` enqueues a Telegram-owned `continue` prompt; prompt-template commands expand the matching π template before entering the normal prompt queue | priority for `/continue`, otherwise default |
102
- | Control queue | Model-switch continuation turns and future deferred controls | `queueLane: control`; accepts control items and continuation prompts | 0 |
103
- | Priority prompt queue | A waiting prompt promoted by `👍`, `⚡️`, `❤️`, or `🕊` | `kind: prompt`, `queueLane: priority` | 1 |
104
- | Default prompt queue | Normal Telegram text/media turns | `kind: prompt`, `queueLane: default` | 2 |
98
+ | Admission | Examples | Queue shape | Dispatch rank |
99
+ | --------------------- | ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- |
100
+ | Immediate execution | `/compact`, `/queue`, `/stop`, `/help`, `/start` | Does not enter the Telegram queue; `/help` opens the same menu as `/start`; `/stop` also clears queued items | N/A |
101
+ | Queued prompt command | `/continue`, `/template_name args` | `/continue` enqueues a Telegram-owned `continue` prompt; prompt-template commands expand the matching π template before entering the normal prompt queue | priority for `/continue`, otherwise default |
102
+ | Control queue | Model-switch continuation turns and future deferred controls | `queueLane: control`; accepts control items and continuation prompts | 0 |
103
+ | Priority prompt queue | A waiting prompt promoted by `👍`, `⚡️`, `❤️`, `🕊`, or `🔥` | `kind: prompt`, `queueLane: priority` | 1 |
104
+ | Default prompt queue | Normal Telegram text/media turns | `kind: prompt`, `queueLane: default` | 2 |
105
105
 
106
- The command action itself carries its execution mode, and the queue domain exposes lane contracts for admission mode, dispatch rank, and allowed item kinds. Queue append and planning paths validate lane admission so a malformed control/default or other invalid lane pairing fails predictably instead of silently changing priority. This lets synthetic control actions and Telegram prompts share one stable ordering model while still rendering distinctly in status output. In the π status bar, busy labels distinguish `active`, `dispatching`, `queued`, `tool running`, `model`, and `compacting`; priority prompts and priority control items are marked with `⚡`.
106
+ The command action itself carries its execution mode, and the queue domain exposes lane contracts for admission mode, dispatch rank, and allowed item kinds. Queue append and planning paths validate lane admission so a malformed control/default or other invalid lane pairing fails predictably instead of silently changing priority. This lets synthetic control actions and Telegram prompts share one stable ordering model while still rendering distinctly in status output. In the π status bar, busy labels distinguish `active`, `dispatching`, `queued`, `tool running`, `model`, and `compacting`; priority prompts and priority control items are marked with `⚡`. If a queue mutation removes the last waiting item while Telegram-owned work still has running tools, the status remains yellow `active` instead of degrading to green `connected`.
107
107
 
108
108
  A dispatched prompt remains in the queue until `agent_start` consumes it. That keeps the active Telegram turn bound correctly for previews, attachments, abort handling, and final reply delivery.
109
109
 
@@ -115,9 +115,9 @@ Dispatch is gated by:
115
115
  - `ctx.isIdle()` being true
116
116
  - `ctx.hasPendingMessages()` being false
117
117
 
118
- This prevents queue races around rapid follow-ups, `/compact`, and mixed local plus Telegram activity. Post-agent-end dispatch retries are scheduled through a session-bound deferred dispatcher that activates on session start, cancels timers on session shutdown, and skips callbacks from older generations before they touch `ExtensionContext`. Telegram `/start` and hidden compatibility shortcuts `/status`, `/model`, `/thinking`, and `/queue` execute immediately; the dispatch controller still serializes any deferred control items so a queued control action must settle before the next queued action can dispatch.
118
+ This prevents queue races around rapid follow-ups, `/compact`, and mixed local plus Telegram activity. Post-agent-end dispatch retries are scheduled through a session-bound deferred dispatcher that activates on session start, cancels timers on session shutdown, and skips callbacks from older generations before they touch `ExtensionContext`. Telegram `/start` and hidden compatibility shortcuts `/status`, `/model`, `/thinking`, `/queue`, and `/settings` execute immediately; the dispatch controller still serializes any deferred control items so a queued control action must settle before the next queued action can dispatch.
119
119
 
120
- `/start` opens the main application menu: visible command help, compact command-only prompt-template rows when π exposes Telegram-compatible prompt-template names, status rows (`Status`, `Usage`, `Cost`, `Context`), and top-level buttons for model, thinking, and queue sections. The Queue button includes the current queued-item count. Hidden compatibility shortcuts `/help`, `/status`, `/model`, `/thinking`, and `/queue` jump directly to their corresponding menu screens. Command emoji come from the `commands` domain map so visible command descriptions and matching menu buttons share one fixed adornment source. Prompt-template commands use a fixed `🧩` marker, map π template names to Telegram-safe aliases such as `fix-tests` → `/fix_tests`, stay visible only inside the `/start` menu, and expand before queueing because `ExtensionAPI.sendUserMessage()` intentionally bypasses π prompt-template expansion for extension-originated messages. Every submenu starts with a top Back row so navigation stays anchored near the original user message above the inline keyboard; model-menu scope and pagination controls sit directly under that top row before model choices, and tapping the pagination indicator opens a compact page picker headed by `<b>Choose a page:</b>`. `menu-model` owns model-menu state, scoped model pages, model callback planning, page-picker rendering, and model-menu rendering while `model` owns core model identity/switching semantics. `menu-thinking` owns thinking-menu text, reply markup, callback handling, and message rendering. `menu-status` owns status-menu payloads, status callback handling, and status-message rendering. `menu-queue` owns queue-menu UI only: queue items are rendered under a compact `<b>Queue:</b>` heading, top-to-bottom in dispatch order, numbered, and marked with `⚡` for priority prompts or `📎` for prompts with attachments. An empty queue renders bold message text plus the top Main menu button, not a disabled empty-state button. Selecting an item opens a submenu that displays the full queued prompt text with Back, priority toggle, and Cancel. If a callback targets an item that has already left the queue, the menu refreshes the list instead of applying a stale mutation.
120
+ `/start` opens the main application menu: visible command help, compact command-only prompt-template rows when π exposes Telegram-compatible prompt-template names, status rows (`Status`, `Usage`, `Cost`, `Context`), and top-level buttons for model, thinking, and queue sections. The `Status` row reports `compacting` while a Telegram `/compact` run is active. The Queue button includes the current queued-item count. Hidden compatibility shortcuts `/help`, `/status`, `/model`, `/thinking`, and `/queue` jump directly to their corresponding menu screens, while `/settings` opens the hidden settings menu for bridge toggles such as proactive push. Settings options open detail submenus; checkbox-like settings use Back plus green/black/yellow On and Off controls instead of mutating directly from the list. Command emoji come from the `commands` domain map so visible command descriptions and matching menu buttons share one fixed adornment source. Prompt-template commands use a fixed `🧩` marker, map π template names to Telegram-safe aliases such as `fix-tests` → `/fix_tests`, stay visible only inside the `/start` menu, and expand before queueing because `ExtensionAPI.sendUserMessage()` intentionally bypasses π prompt-template expansion for extension-originated messages. Every submenu starts with a top Back row so navigation stays anchored near the original user message above the inline keyboard; model-menu pagination controls sit near the top, tapping the pagination indicator opens a compact page picker headed by `<b>Choose a page:</b>`, and tapping a model opens a detail submenu with Back, ☑️ Activate/🟢 Active selection, and yellow/black-marked Scoped/All membership tabs. `menu-model` owns model-menu state, scoped model pages, model detail rendering, scoped-list persistence planning, and model-menu rendering while `model` owns core model identity/switching semantics. `menu-thinking` owns thinking-menu text, reply markup, callback handling, and message rendering. `menu-status` owns status-menu payloads, status callback handling, and status-message rendering. `menu-queue` owns queue-menu UI only: queue items are rendered under a compact `<b>Queue:</b>` heading, top-to-bottom in dispatch order, numbered, and marked with `⚡` for priority prompts or `📎` for prompts with attachments. An empty queue renders bold message text with the bottom-filled `⌛` hourglass plus the top Main menu button, while non-empty queue states keep the running `⏳` hourglass. Selecting an item opens a submenu that displays the queue item number above the full queued prompt text with Back, side-by-side Priority/Normal tabs, and Cancel. If a callback targets an item that has already left the queue, the menu refreshes the list instead of applying a stale mutation.
121
121
 
122
122
  ### Abort Behavior
123
123
 
@@ -160,7 +160,9 @@ Telegram prompt responses use explicit delivery context to attach outbound text,
160
160
 
161
161
  Outbound files are sent only after the active Telegram turn completes, must be staged through the `telegram_attach` tool, are staged atomically per tool call, are checked against a default 50 MiB limit configurable through `PI_TELEGRAM_OUTBOUND_ATTACHMENT_MAX_BYTES` or `TELEGRAM_MAX_ATTACHMENT_SIZE_BYTES`, and use file-backed multipart blobs so large sends do not require preloading whole files into memory.
162
162
 
163
- Assistant-authored outbound actions use final-message markup instead of agent tool calls. Preview updates strip closed top-level HTML comments and currently open/partial top-level comment starts before rendering, so users do not see transient metadata even when streaming flushes happen after only `<`, `<!`, or `<!--`. On `agent_end`, the bridge removes top-level comments from the Markdown text reply, but treats column-zero top-level `<!-- telegram_voice ... -->` and `<!-- telegram_button ... -->` blocks specially before delivery; comments inside fenced code, quotes, lists, or indented examples stay literal, including fenced blocks with Markdown-valid indented closing fences. Voice maps to the first matching `outboundHandlers[]` entry with `type: "voice"`, synthesizes body text, `text="..."`, or colon shorthand through command-template execution, and uploads the generated OGG/Opus file via Telegram `sendVoice`; when no outbound voice handler is configured, it silently skips voice delivery. The `template: [...]` form can express TTS plus MP3-to-OGG conversion using configured templates and bridge-provided `{text}`, `{mp3}`, and `{ogg}` placeholders. Top-level `args` and `defaults` apply to all composed steps unless a step defines private values, the default command timeout applies automatically, and each step receives the previous step's stdout on stdin by default, without hard-coded filesystem defaults. Button blocks are built in: each `telegram_button` block becomes one inline-keyboard button on the final text, and callback clicks enqueue the configured prompt text as a normal Telegram prompt turn; the `telegram_button: Label` shorthand uses the same text for label and prompt, `prompt="..."` supports explicit one-line prompts, and body-form buttons use the body as the prompt. Unknown callback data that does not match pi-telegram-owned prefixes (`tgbtn:`, `menu:`, `model:`, `thinking:`, `status:`, `queue:`) is forwarded to π as `[callback] <data>` after built-in handlers decline it, giving layered extensions a simple namespaced button channel without separate polling; layered callback payloads should follow the [Callback Namespace Standard](./callback-namespaces.md). This keeps technical Markdown, code, tables, formulas, and numbered lists in the text channel when appropriate while allowing TTS-friendly voice messages and tappable continuations without invoking `telegram_attach` or extra transport tools.
163
+ Assistant-authored outbound actions use final-message markup instead of agent tool calls. Preview updates strip closed top-level HTML comments and currently open/partial top-level comment starts before rendering, so users do not see transient metadata even when streaming flushes happen after only `<`, `<!`, or `<!--`. On `agent_end`, the bridge removes top-level comments from the Markdown text reply, but treats column-zero top-level `<!-- telegram_voice ... -->` and `<!-- telegram_button ... -->` blocks specially before delivery; comments inside fenced code, quotes, lists, or indented examples stay literal, including fenced blocks with Markdown-valid indented closing fences. Voice maps to the first matching `outboundHandlers[]` entry with `type: "voice"`, synthesizes body text, `text="..."`, or colon shorthand through command-template execution, and uploads the generated OGG/Opus file via Telegram `sendVoice`; when no outbound voice handler is configured, it silently skips voice delivery. The `template: [...]` form can express TTS plus MP3-to-OGG conversion using configured templates and bridge-provided `{text}`, `{mp3}`, and `{ogg}` placeholders. Top-level `args` and `defaults` apply to all composed steps unless a step defines private values, the default command timeout applies automatically, and each step receives the previous step's stdout on stdin by default, without hard-coded filesystem defaults. Button blocks are built in: each `telegram_button` block becomes one inline-keyboard button on the final text, and callback clicks enqueue the configured prompt text as a normal Telegram prompt turn; the `telegram_button: Label` shorthand uses the same text for label and prompt, `prompt="..."` supports explicit one-line prompts, and body-form buttons use the body as the prompt. Unknown callback data that does not match pi-telegram-owned prefixes (`tgbtn:`, `menu:`, `model:`, `thinking:`, `status:`, `queue:`) is forwarded to π as `[callback] <data>` after built-in handlers decline it, giving layered extensions a simple namespaced button channel without separate polling; layered callback payloads should follow the [Callback Namespace Standard](./callback-namespaces.md). When proactive push is enabled, successful local non-Telegram final replies are sent to the paired chat. Local prompt text is not sent because the bot does not own or mirror terminal user messages. This keeps terminal-originated results visible in Telegram without changing Telegram-originated turn delivery.
164
+
165
+ This keeps technical Markdown, code, tables, formulas, and numbered lists in the text channel when appropriate while allowing TTS-friendly voice messages and tappable continuations without invoking `telegram_attach` or extra transport tools. Telegram prompt guidance targets about 37 visible cells for tables, dense list items, and compact text blocks because emoji and other wide glyphs make raw character counts misleading on mobile screens.
164
166
 
165
167
  ## Interactive Controls
166
168
 
@@ -172,13 +174,14 @@ Current operator controls include:
172
174
  - Inline application-menu buttons for model, thinking, and queue controls, applying idle selections immediately while still respecting busy-run restart rules; model-menu inputs are cached briefly and stored inline-menu states are pruned by TTL/LRU so old keyboards expire predictably
173
175
  - Hidden `/model` and `/thinking` shortcuts for opening the model and thinking sections directly while keeping settings out of the visible bot command menu
174
176
  - `/compact` for Telegram-triggered π session compaction when the bridge is idle
175
- - `/queue` for opening the queue section of the inline application menu; the same section is reachable from the status/main menu and supports top-anchored Back navigation, priority toggling, and cancellation
177
+ - `/queue` for opening the queue section of the inline application menu; the same section is reachable from the status/main menu and supports top-anchored Back navigation, Priority/Normal tabs, and cancellation
176
178
  - `/next` for dispatching the next queued turn, aborting the active run first when π is busy
177
179
  - `/continue` for enqueueing a Telegram-owned `continue` prompt, without aborting the current turn or forcing the next queued item
178
180
  - `/abort` for aborting the active Telegram-owned run while preserving queued items for manual continuation
179
181
  - `/stop` for aborting the active Telegram-owned run and clearing waiting Telegram queue items
180
182
  - `/telegram-status` for π-side diagnostics as grouped line-by-line sections separated by blank lines: connection, polling, execution, queue, and the recent redacted runtime/API event ring. These sections include polling state, last update id, active turn source ids, pending dispatch, compaction state, active tool count, pending model-switch state, total queue depth, and queue-lane counts. The event ring records transport/API, polling/update, prompt-dispatch, control-action, typing, compaction, setup, session-lifecycle, and attachment queue/delivery failures; benign unchanged edit responses and unsupported empty draft-clear attempts are filtered out so expected preview transport noise does not obscure real failures
181
- - Queue reactions apply to waiting text, voice, file, image, and media-group turns by matching the turn's source Telegram message ids: `👍`, `⚡️`, `❤️`, and `🕊` promote waiting prompts, while `👎`, `👻`, `💔`, and `💩` remove waiting turns because ordinary Telegram DM message deletions are not exposed through the Bot API polling path this bridge uses
183
+ - `/telegram-settings` for π-side bridge settings; it currently exposes proactive push as a local toggle backed by the same `telegram.json` flag as the hidden Telegram `/settings` menu
184
+ - Queue reactions apply to waiting text, voice, file, image, and media-group turns by matching the turn's source Telegram message ids: `👍`, `⚡️`, `❤️`, `🕊`, and `🔥` promote waiting prompts, while `👎`, `👻`, `💔`, `💩`, and `🗑` remove waiting turns because ordinary Telegram DM message deletions are not exposed through the Bot API polling path this bridge uses
182
185
 
183
186
  ## In-Flight Model Switching
184
187
 
package/index.ts CHANGED
@@ -5,7 +5,6 @@
5
5
  */
6
6
 
7
7
  import * as Api from "./lib/api.ts";
8
- import * as OutboundAttachments from "./lib/outbound-attachments.ts";
9
8
  import * as CommandTemplates from "./lib/command-templates.ts";
10
9
  import * as Commands from "./lib/commands.ts";
11
10
  import * as Config from "./lib/config.ts";
@@ -15,8 +14,10 @@ import * as Lifecycle from "./lib/lifecycle.ts";
15
14
  import * as Locks from "./lib/locks.ts";
16
15
  import * as Media from "./lib/media.ts";
17
16
  import * as MenuQueue from "./lib/menu-queue.ts";
17
+ import * as MenuSettings from "./lib/menu-settings.ts";
18
18
  import * as Menu from "./lib/menu.ts";
19
19
  import * as Model from "./lib/model.ts";
20
+ import * as OutboundAttachments from "./lib/outbound-attachments.ts";
20
21
  import * as OutboundHandlers from "./lib/outbound-handlers.ts";
21
22
  import * as Pi from "./lib/pi.ts";
22
23
  import * as Polling from "./lib/polling.ts";
@@ -48,10 +49,19 @@ export default function (pi: Pi.ExtensionAPI) {
48
49
  const bridgeRuntime = Runtime.createTelegramBridgeRuntime();
49
50
  const { abort, lifecycle, queue, setup, typing } = bridgeRuntime;
50
51
  const configStore = Config.createTelegramConfigStore();
52
+ const isProactivePushEnabled =
53
+ Config.createTelegramProactivePushChecker(configStore);
54
+ const setProactivePushEnabled =
55
+ Config.createTelegramProactivePushSetter(configStore);
51
56
  const lockRuntime = Locks.createTelegramLockRuntime<Pi.ExtensionContext>();
52
57
  const lockOwnershipGuard =
53
58
  Locks.createTelegramLockOwnershipGuard(lockRuntime);
54
59
  const activeTurnRuntime = Queue.createTelegramActiveTurnStore();
60
+ const proactivePushChatIdGetter =
61
+ Config.createTelegramProactivePushChatIdGetter({
62
+ getActiveTurnChatId: activeTurnRuntime.getChatId,
63
+ getAllowedUserId: configStore.getAllowedUserId,
64
+ });
55
65
  const buttonActionStore = OutboundHandlers.createTelegramButtonActionStore();
56
66
  const pendingModelSwitchStore =
57
67
  Model.createPendingModelSwitchStore<
@@ -249,6 +259,7 @@ export default function (pi: Pi.ExtensionAPI) {
249
259
  buildStatusHtml: Commands.createTelegramAppMenuHtmlBuilder({
250
260
  buildStatusHtml: Status.createTelegramStatusHtmlBuilder({
251
261
  getActiveModel: currentModelRuntime.get,
262
+ isCompactionInProgress: lifecycle.isCompactionInProgress,
252
263
  }),
253
264
  getPromptTemplateCommands,
254
265
  }),
@@ -279,6 +290,16 @@ export default function (pi: Pi.ExtensionAPI) {
279
290
  updateStatusMessage: menuActions.updateStatusMessage,
280
291
  updateStatus,
281
292
  });
293
+ const settingsMenuRuntime = MenuSettings.createTelegramSettingsMenuRuntime({
294
+ getModelMenuState: getQueueMenuState,
295
+ getStoredModelMenuState: modelMenuRuntime.getState,
296
+ storeModelMenuState: modelMenuRuntime.storeState,
297
+ editInteractiveMessage,
298
+ sendInteractiveMessage,
299
+ answerCallbackQuery,
300
+ isProactivePushEnabled,
301
+ setProactivePushEnabled,
302
+ });
282
303
 
283
304
  // --- Polling ---
284
305
 
@@ -300,8 +321,11 @@ export default function (pi: Pi.ExtensionAPI) {
300
321
  currentModelRuntime,
301
322
  modelSwitchController,
302
323
  menuActions,
324
+ updateSettingsMenuMessage: settingsMenuRuntime.updateSettingsMenuMessage,
303
325
  openQueueMenu: queueMenuRuntime.openQueueMenu,
304
326
  queueMenuCallbackHandler: queueMenuRuntime.handleCallbackQuery,
327
+ openSettingsMenu: settingsMenuRuntime.openSettingsMenu,
328
+ settingsMenuCallbackHandler: settingsMenuRuntime.handleCallbackQuery,
305
329
  buttonActionStore,
306
330
  inboundHandlerRuntime,
307
331
  updateStatus,
@@ -313,6 +337,10 @@ export default function (pi: Pi.ExtensionAPI) {
313
337
  downloadFile: downloadTelegramBridgeFile,
314
338
  getThinkingLevel,
315
339
  setThinkingLevel,
340
+ persistScopedModelPatterns: Pi.createScopedModelPatternPersister({
341
+ createSettingsManager: Pi.createSettingsManager,
342
+ clearCachedModelMenuInputs: modelMenuRuntime.clearCachedInputs,
343
+ }),
316
344
  setModel,
317
345
  sendUserMessage,
318
346
  isIdle,
@@ -400,6 +428,8 @@ export default function (pi: Pi.ExtensionAPI) {
400
428
  startPolling: lockedPollingRuntime.start,
401
429
  stopPolling: lockedPollingRuntime.stop,
402
430
  updateStatus,
431
+ isProactivePushEnabled,
432
+ setProactivePushEnabled,
403
433
  });
404
434
 
405
435
  // --- Lifecycle Hooks ---
@@ -463,6 +493,9 @@ export default function (pi: Pi.ExtensionAPI) {
463
493
  planOutboundReply: outboundReplyPlanner,
464
494
  sendOutboundReplyArtifacts: outboundReplyArtifactSender,
465
495
  isCurrentOwner: lockOwnershipGuard.ownsContext,
496
+ getDefaultChatId: proactivePushChatIdGetter,
497
+ isProactivePushEnabled,
498
+ recordRuntimeEvent,
466
499
  getActiveToolExecutions: lifecycle.getActiveToolExecutions,
467
500
  setActiveToolExecutions: lifecycle.setActiveToolExecutions,
468
501
  triggerPendingModelSwitchAbort: modelSwitchController.triggerPendingAbort,
@@ -476,7 +509,10 @@ export default function (pi: Pi.ExtensionAPI) {
476
509
  ...sessionLifecycleRuntime,
477
510
  ...agentLifecycleHooks,
478
511
  onAgentStart: agentStartWithDedupReset,
479
- onBeforeAgentStart: Prompts.createTelegramBeforeAgentStartHook(),
512
+ onBeforeAgentStart: Prompts.createTelegramProactiveBeforeAgentStartHook({
513
+ isProactivePushEnabled,
514
+ isCurrentOwner: lockOwnershipGuard.ownsContext,
515
+ }),
480
516
  onModelSelect: currentModelRuntime.onModelSelect,
481
517
  onMessageStart: previewRuntime.onMessageStart,
482
518
  onMessageUpdate: previewRuntime.onMessageUpdate,
package/lib/commands.ts CHANGED
@@ -135,6 +135,10 @@ export interface TelegramBridgeCommandStartPollingResult {
135
135
  owner?: string;
136
136
  }
137
137
 
138
+ interface TelegramBridgeSettingsSelectUi {
139
+ select?: (title: string, items: string[]) => Promise<string | undefined>;
140
+ }
141
+
138
142
  export interface TelegramBridgeCommandRegistrationDeps {
139
143
  promptForConfig: (ctx: ExtensionCommandContext) => Promise<void>;
140
144
  getStatusLines: () => string[];
@@ -149,6 +153,8 @@ export interface TelegramBridgeCommandRegistrationDeps {
149
153
  | TelegramBridgeCommandStartPollingResult;
150
154
  stopPolling: () => Promise<void | string>;
151
155
  updateStatus: (ctx: ExtensionCommandContext) => void;
156
+ isProactivePushEnabled?: () => boolean;
157
+ setProactivePushEnabled?: (enabled: boolean) => Promise<void>;
152
158
  }
153
159
 
154
160
  function formatTelegramTakeoverTitle(ctx: ExtensionCommandContext): string {
@@ -183,6 +189,36 @@ export function registerTelegramBridgeCommands(
183
189
  ctx.ui.notify(deps.getStatusLines().join("\n"), "info");
184
190
  },
185
191
  });
192
+ pi.registerCommand("telegram-settings", {
193
+ description: "Open Telegram bridge settings",
194
+ handler: async (_args, ctx) => {
195
+ if (!deps.isProactivePushEnabled || !deps.setProactivePushEnabled) {
196
+ ctx.ui.notify("Telegram settings are unavailable.", "warning");
197
+ return;
198
+ }
199
+ await deps.reloadConfig();
200
+ const enabled = deps.isProactivePushEnabled();
201
+ const nextEnabled = !enabled;
202
+ const label = `${enabled ? "🟢" : "⚫️"} Proactive push`;
203
+ const action = `${nextEnabled ? "Enable" : "Disable"} proactive push`;
204
+ const select = (ctx.ui as TelegramBridgeSettingsSelectUi).select;
205
+ if (!select) {
206
+ ctx.ui.notify(
207
+ `${label}\n${action}: /telegram-settings requires interactive mode.`,
208
+ "info",
209
+ );
210
+ return;
211
+ }
212
+ const selected = await select("Telegram settings", [label, "Cancel"]);
213
+ if (selected !== label) return;
214
+ await deps.setProactivePushEnabled(nextEnabled);
215
+ deps.updateStatus(ctx);
216
+ ctx.ui.notify(
217
+ `Proactive push ${nextEnabled ? "enabled" : "disabled"}.`,
218
+ "info",
219
+ );
220
+ },
221
+ });
186
222
  pi.registerCommand("telegram-connect", {
187
223
  description: "Start the Telegram bridge in this π session",
188
224
  handler: async (_args, ctx) => {
@@ -230,6 +266,7 @@ export const TELEGRAM_RESERVED_COMMAND_NAMES = [
230
266
  "compact",
231
267
  "model",
232
268
  "thinking",
269
+ "settings",
233
270
  "help",
234
271
  "start",
235
272
  ] as const;
@@ -261,6 +298,7 @@ export type TelegramCommandAction =
261
298
  | { kind: "status"; executionMode: "immediate" }
262
299
  | { kind: "model"; executionMode: "immediate" }
263
300
  | { kind: "thinking"; executionMode: "immediate" }
301
+ | { kind: "settings"; executionMode: "immediate" }
264
302
  | {
265
303
  kind: "help";
266
304
  commandName: "help" | "start";
@@ -279,6 +317,7 @@ export interface TelegramCommandActionDeps<TMessage, TContext> {
279
317
  handleStatus: (message: TMessage, ctx: TContext) => Promise<void>;
280
318
  handleModel: (message: TMessage, ctx: TContext) => Promise<void>;
281
319
  handleThinking: (message: TMessage, ctx: TContext) => Promise<void>;
320
+ handleSettings?: (message: TMessage, ctx: TContext) => Promise<void>;
282
321
  handleHelp: (
283
322
  message: TMessage,
284
323
  commandName: "help" | "start",
@@ -353,6 +392,11 @@ export interface TelegramCommandTargetRuntimeDeps<TContext> {
353
392
  replyToMessageId: number,
354
393
  ctx: TContext,
355
394
  ) => Promise<void>;
395
+ openSettingsMenu?: (
396
+ chatId: number,
397
+ replyToMessageId: number,
398
+ ctx: TContext,
399
+ ) => Promise<void>;
356
400
  sendTextReply: (
357
401
  chatId: number,
358
402
  replyToMessageId: number,
@@ -373,6 +417,7 @@ export interface TelegramCommandTargetRuntime<
373
417
  ) => void;
374
418
  showStatus: (message: TMessage, ctx: TContext) => Promise<void>;
375
419
  openModelMenu: (message: TMessage, ctx: TContext) => Promise<void>;
420
+ openSettingsMenu: (message: TMessage, ctx: TContext) => Promise<void>;
376
421
  sendTextReply: (message: TMessage, text: string) => Promise<void>;
377
422
  }
378
423
 
@@ -457,6 +502,7 @@ export function createTelegramCommandTargetQueueRuntime<
457
502
  }),
458
503
  showStatus: deps.showStatus,
459
504
  openModelMenu: deps.openModelMenu,
505
+ openSettingsMenu: deps.openSettingsMenu,
460
506
  sendTextReply: deps.sendTextReply,
461
507
  });
462
508
  }
@@ -485,6 +531,18 @@ export function createTelegramCommandTargetRuntime<
485
531
  const target = getTelegramCommandMessageTarget(message);
486
532
  return deps.openModelMenu(target.chatId, target.replyToMessageId, ctx);
487
533
  },
534
+ openSettingsMenu: async (message, ctx) => {
535
+ const target = getTelegramCommandMessageTarget(message);
536
+ if (!deps.openSettingsMenu) {
537
+ await deps.sendTextReply(
538
+ target.chatId,
539
+ target.replyToMessageId,
540
+ "Settings menu is unavailable.",
541
+ );
542
+ return;
543
+ }
544
+ await deps.openSettingsMenu(target.chatId, target.replyToMessageId, ctx);
545
+ },
488
546
  sendTextReply: async (message, text) => {
489
547
  const target = getTelegramCommandMessageTarget(message);
490
548
  await deps.sendTextReply(target.chatId, target.replyToMessageId, text);
@@ -541,6 +599,7 @@ export interface TelegramCommandRuntimeDeps<
541
599
  openModelMenu: (message: TMessage, ctx: TContext) => Promise<void>;
542
600
  openThinkingMenu: (message: TMessage, ctx: TContext) => Promise<void>;
543
601
  openQueueMenu: (message: TMessage, ctx: TContext) => Promise<void>;
602
+ openSettingsMenu?: (message: TMessage, ctx: TContext) => Promise<void>;
544
603
  getAllowedUserId: () => number | undefined;
545
604
  setAllowedUserId: (userId: number) => void;
546
605
  registerBotCommands: () => Promise<void>;
@@ -624,6 +683,7 @@ export const TELEGRAM_COMMAND_ACTIONS = {
624
683
  compact: { kind: "compact", executionMode: "immediate" },
625
684
  model: { kind: "model", executionMode: "immediate" },
626
685
  thinking: { kind: "thinking", executionMode: "immediate" },
686
+ settings: { kind: "settings", executionMode: "immediate" },
627
687
  help: { kind: "help", commandName: "help", executionMode: "immediate" },
628
688
  start: { kind: "help", commandName: "start", executionMode: "immediate" },
629
689
  } as const satisfies Record<TelegramReservedCommandName, TelegramCommandAction>;
@@ -830,6 +890,10 @@ export async function executeTelegramCommandAction<TMessage, TContext>(
830
890
  case "thinking":
831
891
  await deps.handleThinking(message, ctx);
832
892
  return true;
893
+ case "settings":
894
+ if (!deps.handleSettings) return false;
895
+ await deps.handleSettings(message, ctx);
896
+ return true;
833
897
  case "help":
834
898
  await deps.handleHelp(message, action.commandName, ctx);
835
899
  return true;
@@ -846,6 +910,7 @@ export interface TelegramCommandHandlerTargetRuntimeDeps<
846
910
  | "enqueueControlItem"
847
911
  | "showStatus"
848
912
  | "openModelMenu"
913
+ | "openSettingsMenu"
849
914
  | "sendTextReply"
850
915
  | "registerBotCommands"
851
916
  >,
@@ -877,6 +942,7 @@ export function createTelegramCommandHandlerTargetRuntime<
877
942
  dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
878
943
  showStatus: deps.showStatus,
879
944
  openModelMenu: deps.openModelMenu,
945
+ openSettingsMenu: deps.openSettingsMenu,
880
946
  sendTextReply: deps.sendTextReply,
881
947
  });
882
948
  return createTelegramCommandHandler({
@@ -901,6 +967,7 @@ export function createTelegramCommandHandlerTargetRuntime<
901
967
  openModelMenu: commandTargetRuntime.openModelMenu,
902
968
  openThinkingMenu: deps.openThinkingMenu,
903
969
  openQueueMenu: deps.openQueueMenu,
970
+ openSettingsMenu: commandTargetRuntime.openSettingsMenu,
904
971
  getAllowedUserId: deps.getAllowedUserId,
905
972
  setAllowedUserId: deps.setAllowedUserId,
906
973
  registerBotCommands: createTelegramBotCommandRegistrar({
@@ -1056,6 +1123,11 @@ async function handleTelegramCommandRuntime<
1056
1123
  handleThinking: async (nextMessage, commandCtx) => {
1057
1124
  await deps.openThinkingMenu(nextMessage, commandCtx);
1058
1125
  },
1126
+ handleSettings: deps.openSettingsMenu
1127
+ ? async (nextMessage, commandCtx) => {
1128
+ await deps.openSettingsMenu?.(nextMessage, commandCtx);
1129
+ }
1130
+ : undefined,
1059
1131
  handleHelp: async (nextMessage, _nextCommandName, commandCtx) => {
1060
1132
  try {
1061
1133
  await deps.registerBotCommands();
package/lib/config.ts CHANGED
@@ -42,6 +42,7 @@ export interface TelegramConfig {
42
42
  inboundHandlers?: TelegramInboundHandlerConfig[];
43
43
  attachmentHandlers?: TelegramInboundHandlerConfig[];
44
44
  outboundHandlers?: TelegramOutboundHandlerConfig[];
45
+ proactivePush?: boolean;
45
46
  }
46
47
 
47
48
  export interface TelegramConfigStore {
@@ -124,6 +125,29 @@ export function createTelegramConfigStore(
124
125
  };
125
126
  }
126
127
 
128
+ export function createTelegramProactivePushChecker(
129
+ configStore: Pick<TelegramConfigStore, "get">,
130
+ ): () => boolean {
131
+ return () => configStore.get().proactivePush ?? false;
132
+ }
133
+
134
+ export function createTelegramProactivePushSetter(
135
+ configStore: Pick<TelegramConfigStore, "get" | "set" | "persist">,
136
+ ): (enabled: boolean) => Promise<void> {
137
+ return async (enabled) => {
138
+ const config = { ...configStore.get(), proactivePush: enabled };
139
+ configStore.set(config);
140
+ await configStore.persist(config);
141
+ };
142
+ }
143
+
144
+ export function createTelegramProactivePushChatIdGetter(deps: {
145
+ getActiveTurnChatId: () => number | undefined;
146
+ getAllowedUserId: () => number | undefined;
147
+ }): () => number | undefined {
148
+ return () => deps.getActiveTurnChatId() ?? deps.getAllowedUserId();
149
+ }
150
+
127
151
  export type TelegramAuthorizationState =
128
152
  | { kind: "pair"; userId: number }
129
153
  | { kind: "allow" }