@llblab/pi-telegram 0.8.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -0
- package/CHANGELOG.md +17 -1
- package/README.md +7 -4
- package/docs/architecture.md +9 -6
- package/index.ts +45 -2
- package/lib/commands.ts +72 -0
- package/lib/config.ts +49 -0
- package/lib/inbound-handlers.ts +15 -2
- package/lib/locks.ts +16 -0
- package/lib/menu-model.ts +291 -19
- package/lib/menu-queue.ts +137 -36
- package/lib/menu-settings.ts +272 -0
- package/lib/menu-status.ts +15 -2
- package/lib/menu-thinking.ts +2 -2
- package/lib/menu.ts +45 -3
- package/lib/pi.ts +16 -0
- package/lib/preview.ts +15 -0
- package/lib/prompts.ts +24 -1
- package/lib/queue.ts +53 -12
- package/lib/routing.ts +26 -0
- package/lib/status.ts +20 -6
- package/package.json +1 -1
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,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.0: Hidden Settings And Proactive Push
|
|
4
|
+
|
|
5
|
+
- `[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.
|
|
6
|
+
- `[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.
|
|
7
|
+
- `[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.
|
|
8
|
+
- `[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.
|
|
9
|
+
- `[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.
|
|
10
|
+
- `[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.
|
|
11
|
+
|
|
12
|
+
## 0.8.2: Lock-Safe Delivery
|
|
13
|
+
|
|
14
|
+
- `[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.
|
|
15
|
+
- `[Inbound Handlers]` The first step of an inbound composition now receives the full configured handler timeout before elapsed-time accounting starts on later steps. Impact: composition timeout behavior is deterministic and avoids one-millisecond test/runtime drift at pipeline start.
|
|
16
|
+
- `[Menu UI]` Model and Thinking submenu headers now include their matching command icons (`🤖` and `🧠`). Impact: submenu headings match the Queue menu's icon-led style.
|
|
17
|
+
- `[Package]` Bumped package metadata to `0.8.2` and kept the lockfile in sync.
|
|
18
|
+
|
|
3
19
|
## 0.8.1: Outbound Voice Translation Hotfix
|
|
4
20
|
|
|
5
21
|
- `[Outbound Voice]` Composed voice handlers now pass the original `telegram_voice` text to the first pipeline step through stdin, then continue piping each step's stdout into the next step. Impact: translate-from-stdin voice pipelines can translate hidden voice text before TTS instead of failing with an empty first-step input.
|
|
@@ -40,7 +56,7 @@
|
|
|
40
56
|
## 0.7.0: Unified App Menu & Command Template Hardening
|
|
41
57
|
|
|
42
58
|
- `[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.
|
|
43
|
-
- `[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,
|
|
59
|
+
- `[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.
|
|
44
60
|
- `[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.
|
|
45
61
|
- `[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.
|
|
46
62
|
- `[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,
|
|
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
|
|
|
@@ -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,7 +234,7 @@ 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
240
|
telegram queued +3: [⚡ write a shell script…, summarize this image…, 📎 2 attachments]
|
package/docs/architecture.md
CHANGED
|
@@ -33,7 +33,7 @@ Current runtime areas use these ownership boundaries:
|
|
|
33
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
34
|
| `queue` | Queue item contracts, lane admission/order, stores, mutations, dispatch readiness/runtime, prompt/control enqueueing, session and agent/tool lifecycle sequencing |
|
|
35
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 |
|
|
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
37
|
| `keyboard` | Shared Telegram inline-keyboard reply-markup structure; feature domains own callback semantics and button construction |
|
|
38
38
|
| `preview` / `replies` / `rendering` | Preview lifecycle/transports, final reply delivery and reply parameters, Telegram HTML Markdown rendering, chunking, stable-preview snapshots |
|
|
39
39
|
| `outbound-handlers` | Outbound text transformation, assistant-authored outbound comments, generated reply artifacts, inline-keyboard callbacks, and post-`agent_end` outbound action delivery |
|
|
@@ -103,7 +103,7 @@ Admission contract:
|
|
|
103
103
|
| Priority prompt queue | A waiting prompt promoted by `👍`, `⚡️`, `❤️`, or `🕊` | `kind: prompt`, `queueLane: priority` | 1 |
|
|
104
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 `/
|
|
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
|
|
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).
|
|
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,12 +174,13 @@ 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,
|
|
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
|
|
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
|
|
181
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
|
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,8 +49,21 @@ 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);
|
|
56
|
+
const proactivePromptTargetStore =
|
|
57
|
+
Config.createTelegramProactivePromptTargetStore();
|
|
51
58
|
const lockRuntime = Locks.createTelegramLockRuntime<Pi.ExtensionContext>();
|
|
59
|
+
const lockOwnershipGuard =
|
|
60
|
+
Locks.createTelegramLockOwnershipGuard(lockRuntime);
|
|
52
61
|
const activeTurnRuntime = Queue.createTelegramActiveTurnStore();
|
|
62
|
+
const proactivePushChatIdGetter =
|
|
63
|
+
Config.createTelegramProactivePushChatIdGetter({
|
|
64
|
+
getActiveTurnChatId: activeTurnRuntime.getChatId,
|
|
65
|
+
getAllowedUserId: configStore.getAllowedUserId,
|
|
66
|
+
});
|
|
53
67
|
const buttonActionStore = OutboundHandlers.createTelegramButtonActionStore();
|
|
54
68
|
const pendingModelSwitchStore =
|
|
55
69
|
Model.createPendingModelSwitchStore<
|
|
@@ -198,6 +212,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
198
212
|
sendDraft: sendMessageDraft,
|
|
199
213
|
sendMessage,
|
|
200
214
|
editMessageText: editTelegramMessageText,
|
|
215
|
+
canSend: lockOwnershipGuard.ownsCurrentProcess,
|
|
201
216
|
...replyTransport,
|
|
202
217
|
});
|
|
203
218
|
const { finalizeMarkdownPreview } =
|
|
@@ -246,6 +261,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
246
261
|
buildStatusHtml: Commands.createTelegramAppMenuHtmlBuilder({
|
|
247
262
|
buildStatusHtml: Status.createTelegramStatusHtmlBuilder({
|
|
248
263
|
getActiveModel: currentModelRuntime.get,
|
|
264
|
+
isCompactionInProgress: lifecycle.isCompactionInProgress,
|
|
249
265
|
}),
|
|
250
266
|
getPromptTemplateCommands,
|
|
251
267
|
}),
|
|
@@ -276,6 +292,16 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
276
292
|
updateStatusMessage: menuActions.updateStatusMessage,
|
|
277
293
|
updateStatus,
|
|
278
294
|
});
|
|
295
|
+
const settingsMenuRuntime = MenuSettings.createTelegramSettingsMenuRuntime({
|
|
296
|
+
getModelMenuState: getQueueMenuState,
|
|
297
|
+
getStoredModelMenuState: modelMenuRuntime.getState,
|
|
298
|
+
storeModelMenuState: modelMenuRuntime.storeState,
|
|
299
|
+
editInteractiveMessage,
|
|
300
|
+
sendInteractiveMessage,
|
|
301
|
+
answerCallbackQuery,
|
|
302
|
+
isProactivePushEnabled,
|
|
303
|
+
setProactivePushEnabled,
|
|
304
|
+
});
|
|
279
305
|
|
|
280
306
|
// --- Polling ---
|
|
281
307
|
|
|
@@ -297,8 +323,11 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
297
323
|
currentModelRuntime,
|
|
298
324
|
modelSwitchController,
|
|
299
325
|
menuActions,
|
|
326
|
+
updateSettingsMenuMessage: settingsMenuRuntime.updateSettingsMenuMessage,
|
|
300
327
|
openQueueMenu: queueMenuRuntime.openQueueMenu,
|
|
301
328
|
queueMenuCallbackHandler: queueMenuRuntime.handleCallbackQuery,
|
|
329
|
+
openSettingsMenu: settingsMenuRuntime.openSettingsMenu,
|
|
330
|
+
settingsMenuCallbackHandler: settingsMenuRuntime.handleCallbackQuery,
|
|
302
331
|
buttonActionStore,
|
|
303
332
|
inboundHandlerRuntime,
|
|
304
333
|
updateStatus,
|
|
@@ -310,6 +339,10 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
310
339
|
downloadFile: downloadTelegramBridgeFile,
|
|
311
340
|
getThinkingLevel,
|
|
312
341
|
setThinkingLevel,
|
|
342
|
+
persistScopedModelPatterns: Pi.createScopedModelPatternPersister({
|
|
343
|
+
createSettingsManager: Pi.createSettingsManager,
|
|
344
|
+
clearCachedModelMenuInputs: modelMenuRuntime.clearCachedInputs,
|
|
345
|
+
}),
|
|
313
346
|
setModel,
|
|
314
347
|
sendUserMessage,
|
|
315
348
|
isIdle,
|
|
@@ -397,6 +430,8 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
397
430
|
startPolling: lockedPollingRuntime.start,
|
|
398
431
|
stopPolling: lockedPollingRuntime.stop,
|
|
399
432
|
updateStatus,
|
|
433
|
+
isProactivePushEnabled,
|
|
434
|
+
setProactivePushEnabled,
|
|
400
435
|
});
|
|
401
436
|
|
|
402
437
|
// --- Lifecycle Hooks ---
|
|
@@ -459,6 +494,11 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
459
494
|
sendQueuedAttachments: queuedAttachmentSender,
|
|
460
495
|
planOutboundReply: outboundReplyPlanner,
|
|
461
496
|
sendOutboundReplyArtifacts: outboundReplyArtifactSender,
|
|
497
|
+
isCurrentOwner: lockOwnershipGuard.ownsContext,
|
|
498
|
+
getDefaultChatId: proactivePushChatIdGetter,
|
|
499
|
+
consumeProactiveReplyToMessageId: proactivePromptTargetStore.consumeForChat,
|
|
500
|
+
isProactivePushEnabled,
|
|
501
|
+
recordRuntimeEvent,
|
|
462
502
|
getActiveToolExecutions: lifecycle.getActiveToolExecutions,
|
|
463
503
|
setActiveToolExecutions: lifecycle.setActiveToolExecutions,
|
|
464
504
|
triggerPendingModelSwitchAbort: modelSwitchController.triggerPendingAbort,
|
|
@@ -472,7 +512,10 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
472
512
|
...sessionLifecycleRuntime,
|
|
473
513
|
...agentLifecycleHooks,
|
|
474
514
|
onAgentStart: agentStartWithDedupReset,
|
|
475
|
-
onBeforeAgentStart: Prompts.
|
|
515
|
+
onBeforeAgentStart: Prompts.createTelegramProactiveBeforeAgentStartHook({
|
|
516
|
+
isProactivePushEnabled,
|
|
517
|
+
isCurrentOwner: lockOwnershipGuard.ownsContext,
|
|
518
|
+
}),
|
|
476
519
|
onModelSelect: currentModelRuntime.onModelSelect,
|
|
477
520
|
onMessageStart: previewRuntime.onMessageStart,
|
|
478
521
|
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,54 @@ 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
|
+
|
|
151
|
+
export interface TelegramProactivePromptTarget {
|
|
152
|
+
chatId: number;
|
|
153
|
+
messageId: number;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface TelegramProactivePromptTargetStore {
|
|
157
|
+
set: (target: TelegramProactivePromptTarget) => void;
|
|
158
|
+
consumeForChat: (chatId: number) => number | undefined;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function createTelegramProactivePromptTargetStore(): TelegramProactivePromptTargetStore {
|
|
162
|
+
let target: TelegramProactivePromptTarget | undefined;
|
|
163
|
+
return {
|
|
164
|
+
set: (nextTarget) => {
|
|
165
|
+
target = nextTarget;
|
|
166
|
+
},
|
|
167
|
+
consumeForChat: (chatId) => {
|
|
168
|
+
if (!target || target.chatId !== chatId) return undefined;
|
|
169
|
+
const messageId = target.messageId;
|
|
170
|
+
target = undefined;
|
|
171
|
+
return messageId;
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
127
176
|
export type TelegramAuthorizationState =
|
|
128
177
|
| { kind: "pair"; userId: number }
|
|
129
178
|
| { kind: "allow" }
|
package/lib/inbound-handlers.ts
CHANGED
|
@@ -258,6 +258,15 @@ function getRemainingTelegramInboundTimeout(
|
|
|
258
258
|
return Math.max(1, timeout - (Date.now() - startedAt));
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
+
function getTelegramInboundInitialCompositionStepTimeout(
|
|
262
|
+
handler: TelegramInboundHandlerConfig,
|
|
263
|
+
step: TelegramInboundCommandTemplateConfig,
|
|
264
|
+
): number {
|
|
265
|
+
const timeout = getTelegramInboundHandlerTimeout(handler);
|
|
266
|
+
const stepTimeout = getTelegramInboundHandlerConfiguredTimeout(step);
|
|
267
|
+
return stepTimeout === undefined ? timeout : Math.min(stepTimeout, timeout);
|
|
268
|
+
}
|
|
269
|
+
|
|
261
270
|
function getTelegramInboundCompositionStepTimeout(
|
|
262
271
|
handler: TelegramInboundHandlerConfig,
|
|
263
272
|
step: TelegramInboundCommandTemplateConfig,
|
|
@@ -421,7 +430,9 @@ async function executeTelegramTextHandler(
|
|
|
421
430
|
output,
|
|
422
431
|
cwd,
|
|
423
432
|
deps,
|
|
424
|
-
|
|
433
|
+
index === 0
|
|
434
|
+
? getTelegramInboundInitialCompositionStepTimeout(handler, step)
|
|
435
|
+
: getTelegramInboundCompositionStepTimeout(handler, step, startedAt),
|
|
425
436
|
);
|
|
426
437
|
} catch (error) {
|
|
427
438
|
if (typeof step === "object" && step.critical) throw error;
|
|
@@ -498,7 +509,9 @@ async function executeTelegramInboundHandler(
|
|
|
498
509
|
cwd,
|
|
499
510
|
deps,
|
|
500
511
|
false,
|
|
501
|
-
|
|
512
|
+
index === 0
|
|
513
|
+
? getTelegramInboundInitialCompositionStepTimeout(handler, step)
|
|
514
|
+
: getTelegramInboundCompositionStepTimeout(handler, step, startedAt),
|
|
502
515
|
index === 0 ? undefined : output,
|
|
503
516
|
);
|
|
504
517
|
} catch (error) {
|
package/lib/locks.ts
CHANGED
|
@@ -61,6 +61,11 @@ export interface TelegramLockRuntime<TContext extends TelegramLockContext> {
|
|
|
61
61
|
owns: (ctx?: TelegramLockContext) => boolean;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
export interface TelegramLockOwnershipGuard<TContext extends TelegramLockContext> {
|
|
65
|
+
ownsCurrentProcess: () => boolean;
|
|
66
|
+
ownsContext: (ctx: TContext) => boolean;
|
|
67
|
+
}
|
|
68
|
+
|
|
64
69
|
export interface TelegramLockRuntimeOptions {
|
|
65
70
|
key?: string;
|
|
66
71
|
locksPath?: string;
|
|
@@ -234,6 +239,17 @@ export function createTelegramLockRuntime<TContext extends TelegramLockContext>(
|
|
|
234
239
|
};
|
|
235
240
|
}
|
|
236
241
|
|
|
242
|
+
export function createTelegramLockOwnershipGuard<
|
|
243
|
+
TContext extends TelegramLockContext,
|
|
244
|
+
>(
|
|
245
|
+
lock: TelegramLockRuntime<TContext>,
|
|
246
|
+
): TelegramLockOwnershipGuard<TContext> {
|
|
247
|
+
return {
|
|
248
|
+
ownsCurrentProcess: () => lock.owns(),
|
|
249
|
+
ownsContext: (ctx) => lock.owns(ctx),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
237
253
|
export function createTelegramLockedPollingRuntime<
|
|
238
254
|
TContext extends TelegramLockContext,
|
|
239
255
|
>(
|