@llblab/pi-telegram 0.16.3 → 0.16.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -7,12 +7,15 @@
7
7
  - `Boundary Clarity`: Separate Telegram transport concerns, π integration concerns, rendering behavior, and release/documentation state
8
8
  - `Progressive Enhancement + Graceful Degradation`: Prefer behavior that upgrades automatically when richer runtime context exists, but always preserves a useful fallback path when it does not
9
9
  - `Runtime Safety`: Prefer queue and rendering behavior that fails predictably over clever behavior that can desynchronize the Telegram bridge from π session state
10
+ - `Mobile Companion Boundary`: `pi-telegram` extends a live Pi session for phone use; it is not a remote terminal, PTY supervisor, or process launcher. Do not add terminal-control tricks to make Telegram pretend to be the TUI.
10
11
  - `Pi-Native Extensibility`: `pi-telegram` should inherit π's own extension philosophy. It is not only a Telegram adapter; it should become a small, convenient, composable Telegram shell for π extensions, where new capabilities plug into stable contracts instead of forking polling, transport, or menu ownership.
11
12
 
12
13
  ## 1. Concept
13
14
 
14
15
  `pi-telegram` is a Telegram runtime adapter for π: a session-local operator console that turns a private Telegram DM into a runtime surface for prompt intake, streaming previews, queue management, model/thinking/settings controls, inbound/outbound handler pipelines, voice/buttons, artifacts, and extension callback interop. Treat it as a Telegram membrane around π, not a narrow message pipe.
15
16
 
17
+ The core product loop is mobile continuation: start or supervise work in the terminal, then continue from Telegram while away from the keyboard. Telegram controls should be a safe extension-facing subset of the live session, not a replacement for Pi's interactive TUI.
18
+
16
19
  ## 2. Identity & Naming Contract
17
20
 
18
21
  - `Telegram turn`: One unit of Telegram input processed by π; this may represent one message or a coalesced media group
@@ -67,6 +70,7 @@
67
70
  - Command/menu emoji are fixed UI adornments owned by the `commands` map; do not add a persisted emoji toggle or Settings menu until there is a real setting to own
68
71
  - Telegram `reply_to_message` context is prompt-only and must not affect slash-command parsing
69
72
  - Long-lived timers, pollers, watchers, and deferred queue dispatch must be session-bound and avoid stale live π contexts after session replacement
73
+ - Do not add Telegram commands that imitate Pi interactive session replacement, navigation, or TUI rendering through private internals, ANSI terminal clearing, raw TTY injection, or a shadow `pi` subprocess. Features such as a real Telegram `/new` require a public Pi API that runs the same session-replacement path as the terminal command.
70
74
  - In-flight `/model` switching is limited to Telegram-owned active turns; if a tool call is active, abort is delayed until the tool finishes
71
75
 
72
76
  ## 5.3 Telegram Delivery Semantics
@@ -75,6 +79,7 @@
75
79
  - Real code blocks must stay literal and escaped
76
80
  - `telegram_attach` is the canonical outbound file-delivery path for Telegram-originated requests; outside active Telegram turns it may send immediately to the paired/default chat for explicit local/TUI delivery requests only when this π instance owns `/telegram-connect`. `telegram_message` is the first-class direct Telegram Markdown text tool for local/TUI prompts and is also gated by `/telegram-connect`; neither direct tool replaces normal active-turn replies. It reuses top-level `telegram_button` comments for inline buttons; buttons must be attached to a text message, never sent as standalone actions
77
81
  - Telegram delivery strips top-level HTML comments from preview/final text; column-zero top-level `<!-- telegram_voice ... -->` and `<!-- telegram_button ... -->` blocks are special outbound comments handled after `agent_end` without requiring agent-side transport tool calls, while comments inside code, quotes, lists, or indented examples stay literal
82
+ - Telegram prompt guidance is layered: unconfigured sessions receive no bridge suffix, local/TUI prompts receive only explicit direct-delivery guidance, and Telegram-originated turns receive the full inbound/phone-width/output-action contract
78
83
  - `telegram_voice` and `telegram_button` are not π tools; keep prompts/docs explicit that agents should author markup while voice synthesis provider extensions own TTS/OGG conversion, and pi-telegram owns button routing plus Telegram delivery
79
84
  - Voice reply policy and prompt context are owned by pi-telegram's `telegram.json` `voice.replyMode`: missing/invalid config behaves as `manual` but does not add a `[voice]` prompt-context block; only an explicit valid `voice.replyMode` renders context. Render a single voice field as `[voice] reply mode: manual|mirror|always`, and render multiple fields as a `[voice]` list; place voice context after `[outputs]` when handler output exists, otherwise after `[attachments]`; provider prompt contributions are optional provider-specific additions, not the default policy channel
80
85
  - Optional `telegram.json` `time` may add `[time] YYYY-MM-DD HH:mm:ss <timezone>` to Telegram-originated prompts for wall-clock context. It is hidden by default, uses `time.injectionMode` values `hidden|always|interval`, stores `time.interval` in milliseconds, uses the system timezone, and should render last after `[attachments]`, `[outputs]`, and `[voice]` sections. The Settings row `🕒 Time injection: hidden|always|interval` controls `time.injectionMode` only.
package/BACKLOG.md CHANGED
@@ -1,3 +1,6 @@
1
1
  # Project Backlog
2
2
 
3
- No open work.
3
+ - [ ] Track a public Pi session-replacement hook before adding Telegram `/new`.
4
+ - Context: `pi-telegram` is an extension/mobile companion, not a PTY supervisor. A soft `/new` that mutates session internals or filters context without TUI/runtime parity breaks the product boundary.
5
+ - Requirement: only add Telegram `/new` when Pi exposes a safe public API that invokes the same session-replacement path as terminal `/new`, including lifecycle, active-run handling, and TUI rerender semantics.
6
+ - Rejected for this extension: raw TTY injection, ANSI terminal clearing, private TUI container mutation, or running a shadow `pi` subprocess to control the current session.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ## 0.16.5: Context-Aware Prompt Guidance Hotfix
6
+
7
+ - `[Prompt Guidance]` Made before-agent-start Telegram guidance context-aware: unconfigured sessions receive no bridge suffix, local/TUI prompts receive only explicit direct-delivery guidance, and Telegram-originated turns keep the full inbound, phone-width, voice, and button contract. Impact: ordinary local replies no longer get raw Telegram action-comment syntax unless the current turn actually comes from Telegram.
8
+ - `[Docs]` Recorded the product boundary that `pi-telegram` is a mobile companion for a live Pi session, not a remote terminal, PTY supervisor, or process launcher. Telegram controls should stay within Pi's extension-facing APIs; true session replacement such as Telegram `/new` should wait for a public Pi hook that preserves interactive runtime and TUI semantics.
9
+
10
+ ## 0.16.4: Follow-Up And Runtime Mode Hotfix
11
+
12
+ - `[Runtime]` Feature-detect Pi `ctx.mode` and keep `print`/`json` runs passive by blocking polling start/resume in those modes. Impact: CLI/headless sessions can finish local work without inheriting Telegram polling, while `tui`/`rpc` and older Pi runtimes keep existing behavior.
13
+ - `[Queue]` Forward queued Telegram prompts and unknown callback fallbacks to Pi with explicit `followUp` delivery semantics. Impact: Telegram input keeps the existing non-steering queue contract even when Pi's native streaming-message API requires an explicit busy-run policy.
14
+
3
15
  ## 0.16.3: Ownership And Shutdown Hotfix
4
16
 
5
17
  - `[Ownership]` Lock-gated proactive local/headless final-result push so only the current `/telegram-connect` owner can send non-Telegram agent-end replies to the paired chat, while accepted Telegram turns and queued work still finalize session-locally after polling ownership moves away. Impact: child/headless/non-owner instances no longer leak unrelated local results into Telegram.
@@ -18,7 +30,7 @@
18
30
 
19
31
  ## 0.16.0: Telegram Extension Commands
20
32
 
21
- - `[API]` Added `registerTelegramCommand()` on the public `/commands` subpath so companion extensions can explicitly provide Telegram-native slash commands without adding workflow-specific commands to core. Built-in bridge commands stay reserved, extension command names must be Bot API safe, duplicate extension names are rejected, commands stay hidden unless `showInMenu` is enabled, visible commands must provide an emoji used in `/start` help and Bot API descriptions, extension-command descriptions are shown in `/start`, visible extension commands are inserted after `/compact` before queue-control commands, prompt-template commands remain separated in `/start`, handler failures are isolated with runtime diagnostics, and routing precedence is built-ins → extension commands → prompt-template aliases. Impact: commands such as fresh-session controls can live in companion extensions while `pi-telegram` remains a lightweight Telegram shell.
33
+ - `[API]` Added `registerTelegramCommand()` on the public `/commands` subpath so companion extensions can explicitly provide Telegram-native slash commands without adding workflow-specific commands to core. Built-in bridge commands stay reserved, extension command names must be Bot API safe, duplicate extension names are rejected, commands stay hidden unless `showInMenu` is enabled, visible commands must provide an emoji used in `/start` help and Bot API descriptions, extension-command descriptions are shown in `/start`, visible extension commands are inserted after `/compact` before queue-control commands, prompt-template commands remain separated in `/start`, handler failures are isolated with runtime diagnostics, and routing precedence is built-ins → extension commands → prompt-template aliases. Impact: workflow-specific controls can live in companion extensions while `pi-telegram` remains a lightweight Telegram shell.
22
34
 
23
35
  ## 0.15.1: Typing Keepalive Cadence
24
36
 
package/README.md CHANGED
@@ -6,6 +6,8 @@
6
6
 
7
7
  `pi-telegram` turns a private Telegram DM into a session-local operator console for π. It admits work, preserves context, streams readable replies, keeps busy sessions usable through queues, lets other extensions share one bot, and turns assistant-authored intent into native Telegram artifacts. It is also a voice-provider platform: companion extensions can supply Telegram transcription and synthesis providers while `pi-telegram` keeps ownership of transport, queueing, and reply policy.
8
8
 
9
+ The product shape is a mobile companion for a live Pi session: start work in the terminal, then continue from Telegram on the couch or outside. It is not a remote terminal, PTY supervisor, or process launcher, and it intentionally avoids pretending to own Pi's interactive TUI.
10
+
9
11
  This repository is an actively maintained fork of [`badlogic/pi-telegram`](https://github.com/badlogic/pi-telegram). It started from upstream commit [`cb34008`](https://github.com/badlogic/pi-telegram/commit/cb34008460b6c1ca036d92322f69d87f626be0fc) and has since diverged substantially.
10
12
 
11
13
  ## Install
@@ -47,7 +49,7 @@ Paste your bot token when prompted. If a bot token is already saved in `~/.pi/ag
47
49
  /telegram-connect
48
50
  ```
49
51
 
50
- The adapter is session-local: only one π instance polls Telegram at a time. `/telegram-connect` records only external control/polling ownership in `~/.pi/agent/locks.json`; live ownership moves require confirmation, inherited child sessions do not start polling unless they take ownership, and `/new` plus same-`cwd` restarts resume automatically. Local queue and reply state stay per Pi instance, so an instance that loses Telegram control still finishes work it already accepted.
52
+ The adapter is session-local: only one π instance polls Telegram at a time. `/telegram-connect` records only external control/polling ownership in `~/.pi/agent/locks.json`; live ownership moves require confirmation, inherited child sessions do not start polling unless they take ownership, and same-`cwd` restarts resume automatically. Local queue and reply state stay per Pi instance, so an instance that loses Telegram control still finishes work it already accepted.
51
53
 
52
54
  ### 4. Pair your Telegram account
53
55
 
@@ -72,6 +74,7 @@ Once paired, chat with your bot in Telegram. Text, images, files, replies, edits
72
74
 
73
75
  What it feels like:
74
76
 
77
+ - Start work in the terminal, walk away, and keep supervising the same live π session from Telegram.
75
78
  - Open `/start` and get a Telegram control panel for the running π session: status, prompt templates, model, thinking, settings, and queue.
76
79
  - Fire off three tasks while π is busy. They become visible queue items instead of terminal noise.
77
80
  - Open Queue from the menu, inspect waiting work, delete stale prompts, or move important work forward.
@@ -82,7 +85,7 @@ What it feels like:
82
85
 
83
86
  ### Telegram controls
84
87
 
85
- Use these inside the Telegram DM with your bot. The main entrypoint is `/start`: it opens the operator menu and exposes many of the important agent controls that normally live in the CLI, adapted for Telegram.
88
+ Use these inside the Telegram DM with your bot. The main entrypoint is `/start`: it opens the operator menu and exposes many of the important agent controls that can be safely adapted through Pi's extension APIs. The bot does not forward arbitrary terminal slash commands or emulate TUI-only session controls.
86
89
 
87
90
  - **`/start`**: Pair the first Telegram user when needed, register bot commands, and open the inline application menu with command help, prompt-template commands, status rows, model controls, thinking controls, settings, and queue controls.
88
91
  - **`/compact`**: Ask for inline confirmation, then start session compaction when the session is idle; Telegram shows the native typing indicator while manual or automatic compaction is running.
@@ -170,6 +173,8 @@ A practical voice setup is simple: Telegram `.ogg` arrives, STT runs locally or
170
173
 
171
174
  Assistant replies can include hidden outbound blocks. `telegram_voice` and `telegram_button` are not π tools; they are assistant-authored HTML comments that the adapter removes from Telegram text and handles after `agent_end`. Recognized blocks must start at column zero on a top-level line outside fenced code, quotes, lists, and indented examples. Do not use JSON button specs, inline comments after visible text, or standalone button tool calls; write normal Markdown plus hidden comments, and add visible parent text if buttons would otherwise be the only output.
172
175
 
176
+ Prompt guidance is context-aware: unconfigured sessions receive no Telegram suffix, local/TUI prompts only get explicit direct-delivery guidance, and Telegram-originated turns get the full phone-width output and action-comment contract.
177
+
173
178
  ```md
174
179
  Full technical answer stays readable as text.
175
180
 
@@ -209,7 +214,7 @@ Unknown inline-button callbacks are forwarded to π as `[callback] <data>` when
209
214
 
210
215
  ### Extension Sections
211
216
 
212
- Ordinary pi extensions can register Telegram-native slash commands, structured UI sections, and compact status lines without owning a second polling loop. Slash commands use explicit opt-in registration from `@llblab/pi-telegram/commands`, so workflow-specific commands such as a fresh-session control can live in companion extensions instead of expanding the core bridge command set. UI sections appear in the main Telegram menu and Settings submenu, while status lines allow widgets such as quota indicators to appear beside Status, Usage, Cost, and Context only when relevant to the active model. Each section gets a narrow typed context with `edit`, `open`, `enqueuePrompt`, `answerCallback`, and `callbackData()` — enough to build interactive Telegram-native surfaces while `pi-telegram` owns transport, callback routing, navigation hierarchy, and diagnostics.
217
+ Ordinary pi extensions can register Telegram-native slash commands, structured UI sections, and compact status lines without owning a second polling loop. Slash commands use explicit opt-in registration from `@llblab/pi-telegram/commands`, so workflow-specific commands can live in companion extensions instead of expanding the core bridge command set. UI sections appear in the main Telegram menu and Settings submenu, while status lines allow widgets such as quota indicators to appear beside Status, Usage, Cost, and Context only when relevant to the active model. Each section gets a narrow typed context with `edit`, `open`, `enqueuePrompt`, `answerCallback`, and `callbackData()` — enough to build interactive Telegram-native surfaces while `pi-telegram` owns transport, callback routing, navigation hierarchy, and diagnostics.
213
218
 
214
219
  Import `registerTelegramSection()` from `@llblab/pi-telegram/sections` and return a disposer on shutdown. Sections can send interactive messages directly into the chat via `ctx.open()` — confirmation dialogs, approve/deny gates, and multi-step forms live outside the menu hierarchy while callbacks route through the same typed handler. See [`@llblab/pi-telegram-extension-demo`](https://github.com/llblab/pi-telegram-extension-demo) for a working reference and the [Extension Sections Standard](./docs/sections.md) for the full contract.
215
220
 
@@ -9,6 +9,8 @@
9
9
  - Stream previews and deliver final π responses back to Telegram.
10
10
  - Provide Telegram-native controls for queueing, model/thinking/settings menus, compaction, abort/stop, prompt templates, reactions, and outbound artifacts.
11
11
 
12
+ The bridge is a mobile companion for a live Pi session, not a remote terminal. It should let an operator start work in the TUI and continue supervising from Telegram, while staying inside Pi's extension-facing contracts.
13
+
12
14
  This document is the architectural map. Focused behavior standards live in sibling docs:
13
15
 
14
16
  - [Public API](./public-api.md) — stable commands, config, package entrypoints, assistant markup, extension APIs, and compatibility boundaries.
@@ -23,6 +25,17 @@ This document is the architectural map. Focused behavior standards live in sibli
23
25
 
24
26
  `index.ts` is the only composition root. It wires live π ports, Telegram Bot API ports, session-local stores, lifecycle hooks, and domain runtimes. Reusable logic lives in flat `/lib/*.ts` domain modules rather than a deep local module tree.
25
27
 
28
+ ### Extension Boundary Vs Supervisor Control
29
+
30
+ `pi-telegram` runs inside the current Pi process as an extension. That gives it safe access to public extension APIs such as aborting work, compacting, sending follow-up prompts, observing lifecycle events, and rendering Telegram-native controls. It does not own the terminal, the interactive-mode chat transcript, or the process lifecycle.
31
+
32
+ Keep this boundary explicit:
33
+
34
+ - Do not use raw TTY injection, ANSI terminal clearing, private TUI container mutation, or a shadow `pi` subprocess to simulate interactive commands.
35
+ - Do not treat Telegram as a generic remote shell for every Pi slash command.
36
+ - Commands that require interactive session replacement or TUI rerendering, such as a true Telegram `/new`, need a public Pi API that invokes the same runtime path as the terminal command.
37
+ - A separate PTY supervisor or daemon could choose to own those risks, but that would be a different product mode rather than this extension's runtime contract.
38
+
26
39
  The repository uses a **Flat Domain DAG**:
27
40
 
28
41
  - Local imports must form a directed acyclic graph.
@@ -88,6 +101,7 @@ Telegram configuration lives in `~/.pi/agent/telegram.json`. Polling ownership l
88
101
  - `/telegram-connect` acquires or moves singleton polling ownership before polling starts.
89
102
  - `/telegram-disconnect` stops polling and releases ownership.
90
103
  - Session start resumes polling only when the existing lock already points at the current `pid`/`cwd`, or when a stale same-`cwd` lock can be safely replaced after process restart.
104
+ - Pi `print`/`json` run modes stay passive: they do not start or resume Telegram polling even if a lock is present. Older Pi runtimes without `ctx.mode` keep the previous compatibility behavior.
91
105
  - Inherited child sessions that see the same `telegram.json` but do not own the `pid`/`cwd` lock must not auto-start polling or call `getUpdates` unless the operator force-takes ownership.
92
106
  - Session replacement suspends polling/watchers without releasing ownership so the next session-start hook in the same process can resume.
93
107
  - Live polling owners require explicit takeover confirmation.
@@ -140,7 +154,7 @@ Dispatch requires:
140
154
 
141
155
  A dispatched prompt remains queued until `agent_start` consumes it. This keeps the active Telegram turn bound for previews, attachments, aborts, and final replies.
142
156
 
143
- Post-agent-end queue dispatch uses a session-bound deferred dispatcher. It is activated on session start, clears timers on shutdown, and skips callbacks from older generations before touching `ExtensionContext`. Dispatch stays session-bound after polling ownership moves elsewhere.
157
+ Post-agent-end queue dispatch uses a session-bound deferred dispatcher. It is activated on session start, clears timers on shutdown, and skips callbacks from older generations before touching `ExtensionContext`. Dispatch stays session-bound after polling ownership moves elsewhere. When a queued Telegram prompt is forwarded into Pi, it uses Pi's explicit `followUp` delivery option so Telegram input preserves the existing non-steering queue contract even if Pi is still settling active work.
144
158
 
145
159
  ### Controls And Menus
146
160
 
@@ -229,7 +243,7 @@ Queue reactions are shortcut controls for waiting turns. Promotion reactions (`
229
243
 
230
244
  When proactive push is enabled and this instance owns the Telegram lock, successful local non-Telegram final replies are sent to the paired chat. Non-owners skip proactive delivery and record a runtime diagnostic. Local prompt text is not mirrored because the bot does not own terminal user messages.
231
245
 
232
- Telegram prompt guidance asks assistants to keep dense mobile-visible text around 37 display cells where possible, because emoji and wide Unicode make raw character counts misleading.
246
+ Telegram prompt guidance is context-aware. Unconfigured sessions receive no bridge suffix. Local/TUI prompts receive only explicit direct-delivery guidance so ordinary terminal replies do not learn raw Telegram action-comment syntax. Telegram-originated turns receive the full inbound context, phone-width output, and native action contract, including the 37-display-cell mobile readability hint.
233
247
 
234
248
  ## In-Flight Model Switching
235
249
 
package/docs/outbound.md CHANGED
@@ -132,12 +132,14 @@ Buttons are built in and do not need a command template because they are pure Te
132
132
 
133
133
  ## Prompt Contract
134
134
 
135
- The extension injects Telegram-specific system prompt guidance so agents know the fast path:
135
+ The extension injects prompt guidance by context:
136
136
 
137
- - Write the full technical answer as normal Markdown.
137
+ - If no bot token is configured, no Telegram bridge suffix is injected.
138
+ - For ordinary local/TUI prompts, the agent only sees explicit direct-delivery guidance: use `telegram_attach` or `telegram_message` when the user asks to send something to Telegram, and otherwise answer locally as normal.
139
+ - For Telegram-originated turns, write the full technical answer as normal Markdown.
138
140
  - Add `telegram_voice` when a Telegram-native voice message is useful; use body text, `text="..."`, or colon shorthand for the text to synthesize. A companion summary is optional, no specific summary format is required.
139
141
  - Add `telegram_button: ...` when label equals prompt, `telegram_button label="..." prompt="..."` for one-line prompts, or `telegram_button label="..."` with a body for multiline prompts. If the reply contains only button/voice comment blocks, add a short visible marker (for example `Choose one:`) before them so Telegram always has a visible parent message for attachment.
140
142
  - For ordinary Telegram-turn replies, do not call transport tools for voice or buttons; the bridge owns delivery, while registered voice synthesis providers own TTS and OGG/Opus conversion. For explicit local/TUI direct sends, `telegram_message` may include top-level `telegram_button` comments in its Markdown text because those buttons are attached to that text message.
141
143
  - Never send buttons without visible parent text. If the answer would contain only hidden comments, add a compact line such as `Choose one:` first.
142
144
 
143
- This keeps the agent focused on semantics and lets the bridge handle low-latency Telegram adaptation.
145
+ This keeps the agent focused on semantics, prevents Telegram action syntax from leaking into normal local replies, and lets the bridge handle low-latency Telegram adaptation.
@@ -53,6 +53,8 @@ Stable commands inside the paired Telegram DM:
53
53
 
54
54
  Hidden compatibility shortcuts may open sections directly: `/help`, `/status`, `/model`, `/thinking`, `/queue`, and `/settings`.
55
55
 
56
+ This command surface is a mobile companion subset, not a raw terminal-command bridge. Commands that depend on Pi's interactive runtime owning session replacement, TUI transcript clearing, or arbitrary slash-command dispatch stay out of the stable Telegram API unless Pi exposes a safe public extension hook for them.
57
+
56
58
  ### Tools and assistant-authored actions
57
59
 
58
60
  - `telegram_attach(paths, chat_id?, caption?)` is the stable artifact delivery tool for generated files. During Telegram turns it queues files for the active reply; outside Telegram turns it sends files directly to the paired/default chat or explicit `chat_id` when this π instance owns `/telegram-connect`.
@@ -60,6 +62,8 @@ Hidden compatibility shortcuts may open sections directly: `/help`, `/status`, `
60
62
  - `telegram_voice` hidden comments request Telegram-native voice delivery.
61
63
  - `telegram_button` hidden comments create inline buttons whose taps enqueue prompts. Use top-level column-zero comments outside code, quotes, lists, and indented examples; do not emit JSON button specs or standalone button actions.
62
64
 
65
+ Prompt guidance is context-aware: local/TUI prompts see only explicit direct-delivery guidance, while Telegram-originated turns receive the full action-comment syntax and phone-width output contract.
66
+
63
67
  See [Outbound Handlers](./outbound.md) for exact markup forms.
64
68
 
65
69
  ## Configuration API
@@ -143,12 +147,12 @@ Import from `@llblab/pi-telegram/commands`. This registers Telegram slash comman
143
147
 
144
148
  ```ts
145
149
  const off = registerTelegramCommand({
146
- name: "new",
147
- description: "Start fresh",
150
+ name: "review",
151
+ description: "Review queued work",
148
152
  showInMenu: true,
149
- emoji: "🆕",
153
+ emoji: "🧩",
150
154
  handler: async (ctx) => {
151
- await ctx.reply("Starting a fresh session is handled by my extension.");
155
+ await ctx.enqueuePrompt(`Review this work: ${ctx.args}`);
152
156
  },
153
157
  });
154
158
  ```
package/index.ts CHANGED
@@ -386,6 +386,8 @@ export default function (pi: Pi.ExtensionAPI) {
386
386
  const lockedPollingRuntime = Locks.createTelegramLockedPollingRuntime({
387
387
  lock: lockRuntime,
388
388
  hasBotToken: configStore.hasBotToken,
389
+ canStartPolling: Pi.canStartPollingInExtensionContext,
390
+ formatStartBlockedMessage: Pi.formatPollingStartBlockedByRunMode,
389
391
  startPolling: pollingRuntime.start,
390
392
  stopPolling: pollingRuntime.stop,
391
393
  updateStatus,
package/lib/bindings.ts CHANGED
@@ -113,7 +113,10 @@ interface TelegramLifecycleBindingDeps {
113
113
  Lifecycle.TelegramLifecycleRegistrationDeps,
114
114
  "onSessionStart" | "onSessionShutdown" | "onModelSelect"
115
115
  >;
116
- configStore: Pick<Config.TelegramConfigStore, "getOutboundHandlers">;
116
+ configStore: Pick<
117
+ Config.TelegramConfigStore,
118
+ "getOutboundHandlers" | "hasBotToken"
119
+ >;
117
120
  abort: Runtime.TelegramRuntimeAbortPort;
118
121
  typing: Runtime.TelegramRuntimeTypingPort;
119
122
  lifecycle: Runtime.TelegramRuntimeLifecyclePort;
@@ -317,6 +320,7 @@ export function registerTelegramLifecycleRuntimeHooks({
317
320
  onSessionCompact: compactionObserver.onSessionCompact,
318
321
  onAgentStart: agentStartWithDedupReset,
319
322
  onBeforeAgentStart: Prompts.createTelegramProactiveBeforeAgentStartHook({
323
+ isConfigured: configStore.hasBotToken,
320
324
  isProactivePushEnabled,
321
325
  isCurrentOwner: lockOwnershipGuard.ownsContext,
322
326
  }),
package/lib/locks.ts CHANGED
@@ -101,6 +101,8 @@ export interface TelegramLockedPollingRuntimeDeps<
101
101
  > {
102
102
  lock: TelegramLockRuntime<TContext>;
103
103
  hasBotToken: () => boolean;
104
+ canStartPolling?: (ctx: TContext) => boolean;
105
+ formatStartBlockedMessage?: (ctx: TContext) => string;
104
106
  startPolling: (ctx: TContext) => void | Promise<void>;
105
107
  stopPolling: () => Promise<void>;
106
108
  updateStatus: (ctx: TContext) => void;
@@ -309,10 +311,18 @@ export function createTelegramLockedPollingRuntime<
309
311
  }, ownershipCheckMs);
310
312
  ownershipInterval.unref?.();
311
313
  };
314
+ const canStartPolling = (ctx: TContext): boolean =>
315
+ deps.canStartPolling?.(ctx) ?? true;
316
+ const formatStartBlockedMessage = (ctx: TContext): string =>
317
+ deps.formatStartBlockedMessage?.(ctx) ??
318
+ "Telegram polling is unavailable in this π run mode.";
312
319
  return {
313
320
  start: async (ctx, options = {}) => {
314
321
  if (!deps.hasBotToken())
315
322
  return { ok: false, message: "Telegram bot is not configured." };
323
+ if (!canStartPolling(ctx)) {
324
+ return { ok: false, message: formatStartBlockedMessage(ctx) };
325
+ }
316
326
  const acquired = deps.lock.acquire(ctx, options);
317
327
  if (!acquired.ok) {
318
328
  return {
@@ -341,6 +351,7 @@ export function createTelegramLockedPollingRuntime<
341
351
  suspend: suspendPolling,
342
352
  onSessionStart: async (_event, ctx) => {
343
353
  if (!deps.hasBotToken()) return;
354
+ if (!canStartPolling(ctx)) return;
344
355
  const ownsCurrentLock = deps.lock.owns(ctx);
345
356
  const state = ownsCurrentLock ? undefined : deps.lock.getState();
346
357
  const canResumeStaleSameCwd =
package/lib/pi.ts CHANGED
@@ -41,6 +41,44 @@ export interface PiSettingsManager {
41
41
  }
42
42
 
43
43
  export type PiSlashCommandInfo = SlashCommandInfo;
44
+ export type PiRunMode = "tui" | "rpc" | "json" | "print";
45
+
46
+ function isPiRunMode(value: unknown): value is PiRunMode {
47
+ return (
48
+ value === "tui" ||
49
+ value === "rpc" ||
50
+ value === "json" ||
51
+ value === "print"
52
+ );
53
+ }
54
+
55
+ export function getExtensionContextMode(ctx: unknown): PiRunMode | undefined {
56
+ const mode =
57
+ typeof ctx === "object" && ctx !== null
58
+ ? (ctx as { mode?: unknown }).mode
59
+ : undefined;
60
+ return isPiRunMode(mode) ? mode : undefined;
61
+ }
62
+
63
+ export function isExtensionContextPassiveRunMode(ctx: unknown): boolean {
64
+ const mode = getExtensionContextMode(ctx);
65
+ return mode === "print" || mode === "json";
66
+ }
67
+
68
+ export function canStartPollingInExtensionContext(ctx: unknown): boolean {
69
+ return !isExtensionContextPassiveRunMode(ctx);
70
+ }
71
+
72
+ export function formatPollingStartBlockedByRunMode(ctx: unknown): string {
73
+ const mode = getExtensionContextMode(ctx);
74
+ return mode
75
+ ? `Telegram polling is unavailable in π ${mode} mode. Use /telegram-connect from a long-lived π session.`
76
+ : "Telegram polling is unavailable in this π run mode.";
77
+ }
78
+
79
+ export type PiSendUserMessageOptions = NonNullable<
80
+ Parameters<ExtensionAPI["sendUserMessage"]>[1]
81
+ >;
44
82
 
45
83
  export interface PiExtensionApiRuntimePorts {
46
84
  sendUserMessage: ExtensionAPI["sendUserMessage"];
@@ -63,7 +101,7 @@ export function createExtensionApiRuntimePorts(
63
101
  >,
64
102
  ): PiExtensionApiRuntimePorts {
65
103
  return {
66
- sendUserMessage: (content) => api.sendUserMessage(content),
104
+ sendUserMessage: (content, options) => api.sendUserMessage(content, options),
67
105
  exec: (command, args, options) => api.exec(command, args, options),
68
106
  getCommands: () => api.getCommands(),
69
107
  getThinkingLevel: () => api.getThinkingLevel(),
package/lib/prompts.ts CHANGED
@@ -7,11 +7,19 @@
7
7
  import type { BeforeAgentStartEvent } from "./pi.ts";
8
8
  import { TELEGRAM_PREFIX } from "./turns.ts";
9
9
 
10
- const SYSTEM_PROMPT_SUFFIX = `
10
+ const LOCAL_SYSTEM_PROMPT_SUFFIX = `
11
11
 
12
- Telegram bridge extension is active.
12
+ Telegram bridge extension is available.
13
13
 
14
- Inbound context:
14
+ Local/TUI Telegram delivery:
15
+ - Answer ordinary local prompts normally; do not add Telegram action comments unless the user explicitly asks for Telegram delivery.
16
+ - For explicit Telegram file delivery, call \`telegram_attach(local_path)\`. For explicit Telegram text delivery, call \`telegram_message(...)\`.
17
+ - Direct local/TUI Telegram delivery requires this π instance to own \`/telegram-connect\`; if ownership is elsewhere, connect/take over first instead of bypassing the lock.
18
+ `;
19
+
20
+ const TELEGRAM_TURN_SYSTEM_PROMPT_SUFFIX = `
21
+
22
+ Telegram-originated turn context:
15
23
  - \`[telegram]\` marks Telegram-originated messages. Suffixes \`|from:user\` (sender) and \`|guest:group\` (guest mode — message from another chat where the bot is not a member) may be present; the bot sees the message as if forwarded from that user/chat.
16
24
  - \`[reply]\` is quoted context from the replied-to message, not a new instruction by itself. Suffix \`|from:user\` identifies the original author in guest-mode replies. Use it to resolve references like "this", "it", or "that message"; the actual instruction is before [reply] unless it explicitly asks to act on the quote.
17
25
  - \`[attachments]\` gives a base directory plus relative local files; resolve and read them as needed. \`[outputs]\` contains inbound-handler stdout such as transcriptions or extracted text for those attachments.
@@ -27,7 +35,7 @@ Telegram-visible output:
27
35
 
28
36
  Native outbound actions:
29
37
  - Use normal Markdown for visible text. Use top-level column-zero hidden Markdown comments outside code, quotes, and lists only for native actions; the bridge strips them after agent_end and turns them into Telegram-native artifacts/reply_markup. Do not render button JSON, do not invent standalone button tools, and do not call/register transport/TTS/text-to-OGG tools for ordinary Telegram-turn voice/buttons.
30
- - \`telegram_voice\`: text is synthesized by the registered voice synthesis provider and delivered by pi-telegram. Use body text for multiline voice, \`<!-- telegram_voice text="Short summary" -->\` for explicit one-line voice, or \`<!-- telegram_voice: Short summary -->\` for one-line voice with no attributes. A companion summary is optional, no specific summary format is required. Keep it TTS-friendly; avoid raw Markdown, code, formulas, tables, or long lists.
38
+ - \`telegram_voice\`: text is synthesized by the registered voice synthesis provider and delivered by pi-telegram. Use body text for multiline voice, \`<!-- telegram_voice text="Short summary" -->\` for explicit one-line text, or \`<!-- telegram_voice: Short summary -->\` for one-line text with no attributes. A companion summary is optional, no specific summary format is required. Keep it TTS-friendly; avoid raw Markdown, code, formulas, tables, or long lists.
31
39
  - \`telegram_button\`: callback prompt is routed back as a normal Telegram turn. Use \`<!-- telegram_button: OK -->\` when prompt equals label, \`<!-- telegram_button label=Continue prompt="Continue with the current plan." -->\` for one-line prompts, or body form \`<!-- telegram_button label="Show risks"\nList the main risks first.\n-->\` for multiline prompts. Do not put button comments inline after visible text, inside code fences, block quotes, lists, or indented examples; those are literal Markdown, not buttons.
32
40
  - If only hidden action comments would remain, add visible parent text like "Choose one:" so Telegram has a message to attach buttons to.
33
41
  `;
@@ -36,19 +44,25 @@ export function buildTelegramBridgeSystemPrompt(options: {
36
44
  prompt: string;
37
45
  systemPrompt: string;
38
46
  telegramPrefix?: string;
39
- systemPromptSuffix: string;
47
+ localSystemPromptSuffix: string;
48
+ telegramTurnSystemPromptSuffix: string;
40
49
  }): { systemPrompt: string } {
41
50
  const telegramPrefix = options.telegramPrefix ?? TELEGRAM_PREFIX;
42
- const suffix = options.prompt.trimStart().startsWith(telegramPrefix)
43
- ? `${options.systemPromptSuffix}\n- The current user message came from Telegram.`
44
- : options.systemPromptSuffix;
45
- return { systemPrompt: options.systemPrompt + suffix };
51
+ const telegramTurn = options.prompt.trimStart().startsWith(telegramPrefix);
52
+ const telegramSuffix = telegramTurn
53
+ ? `${options.telegramTurnSystemPromptSuffix}\n- The current user message came from Telegram.`
54
+ : "";
55
+ return {
56
+ systemPrompt:
57
+ options.systemPrompt + options.localSystemPromptSuffix + telegramSuffix,
58
+ };
46
59
  }
47
60
 
48
61
  export function createTelegramBeforeAgentStartHook(
49
62
  options: {
50
63
  telegramPrefix?: string;
51
- systemPromptSuffix?: string;
64
+ localSystemPromptSuffix?: string;
65
+ telegramTurnSystemPromptSuffix?: string;
52
66
  } = {},
53
67
  ): (event: BeforeAgentStartEvent) => { systemPrompt: string } {
54
68
  return (event) =>
@@ -56,12 +70,17 @@ export function createTelegramBeforeAgentStartHook(
56
70
  prompt: event.prompt,
57
71
  systemPrompt: event.systemPrompt,
58
72
  telegramPrefix: options.telegramPrefix,
59
- systemPromptSuffix: options.systemPromptSuffix ?? SYSTEM_PROMPT_SUFFIX,
73
+ localSystemPromptSuffix:
74
+ options.localSystemPromptSuffix ?? LOCAL_SYSTEM_PROMPT_SUFFIX,
75
+ telegramTurnSystemPromptSuffix:
76
+ options.telegramTurnSystemPromptSuffix ??
77
+ TELEGRAM_TURN_SYSTEM_PROMPT_SUFFIX,
60
78
  });
61
79
  }
62
80
 
63
81
  export interface TelegramProactivePromptHookDeps<TContext> {
64
82
  baseHook?: (event: BeforeAgentStartEvent) => { systemPrompt: string };
83
+ isConfigured: () => boolean;
65
84
  isProactivePushEnabled: () => boolean;
66
85
  isCurrentOwner: (ctx: TContext) => boolean;
67
86
  }
@@ -74,6 +93,7 @@ export function createTelegramProactiveBeforeAgentStartHook<TContext>(
74
93
  ) => Promise<{ systemPrompt: string }> {
75
94
  const baseHook = deps.baseHook ?? createTelegramBeforeAgentStartHook();
76
95
  return async function onBeforeAgentStart(event, ctx) {
96
+ if (!deps.isConfigured()) return { systemPrompt: event.systemPrompt };
77
97
  const result = baseHook(event);
78
98
  if (!deps.isProactivePushEnabled()) return result;
79
99
  if (!deps.isCurrentOwner(ctx)) return result;
package/lib/queue.ts CHANGED
@@ -1820,6 +1820,14 @@ export function createTelegramDeferredQueueDispatchRuntime<TContext = unknown>(
1820
1820
 
1821
1821
  // --- Dispatch Runtime ---
1822
1822
 
1823
+ export interface TelegramPromptDeliveryOptions {
1824
+ deliverAs: "followUp";
1825
+ }
1826
+
1827
+ export const TELEGRAM_PROMPT_FOLLOW_UP_DELIVERY = {
1828
+ deliverAs: "followUp",
1829
+ } as const satisfies TelegramPromptDeliveryOptions;
1830
+
1823
1831
  export interface TelegramDispatchRuntimeDeps<TContext = unknown> {
1824
1832
  executeControlItem: (
1825
1833
  item: Extract<
@@ -1833,6 +1841,7 @@ export interface TelegramDispatchRuntimeDeps<TContext = unknown> {
1833
1841
  TelegramQueueDispatchAction,
1834
1842
  { kind: "prompt" }
1835
1843
  >["item"]["content"],
1844
+ options?: TelegramPromptDeliveryOptions,
1836
1845
  ) => void;
1837
1846
  onPromptDispatchFailure: (message: string) => void;
1838
1847
  onIdle: () => void;
@@ -1870,7 +1879,7 @@ export function executeTelegramQueueDispatchPlan<TContext = unknown>(
1870
1879
  }
1871
1880
  deps.onPromptDispatchStart(plan.item.chatId);
1872
1881
  try {
1873
- deps.sendUserMessage(plan.item.content);
1882
+ deps.sendUserMessage(plan.item.content, TELEGRAM_PROMPT_FOLLOW_UP_DELIVERY);
1874
1883
  } catch (error) {
1875
1884
  const message = getTelegramQueueErrorMessage(error);
1876
1885
  deps.onPromptDispatchFailure(message);
package/lib/routing.ts CHANGED
@@ -122,7 +122,10 @@ export interface TelegramInboundRouteRuntimeDeps<
122
122
  ctx: TContext,
123
123
  ) => Promise<void>;
124
124
  setModel: (model: TModel) => Promise<boolean>;
125
- sendUserMessage?: (message: string) => void;
125
+ sendUserMessage?: (
126
+ message: string,
127
+ options?: Queue.TelegramPromptDeliveryOptions,
128
+ ) => void;
126
129
  isIdle: (ctx: TContext) => boolean;
127
130
  hasPendingMessages: (ctx: TContext) => boolean;
128
131
  compact: (
@@ -315,7 +318,10 @@ export function createTelegramInboundRouteRuntime<
315
318
  callbackData &&
316
319
  !isTelegramOwnedCallbackData(callbackData)
317
320
  ) {
318
- deps.sendUserMessage(`[callback] ${callbackData}`);
321
+ deps.sendUserMessage(
322
+ `[callback] ${callbackData}`,
323
+ Queue.TELEGRAM_PROMPT_FOLLOW_UP_DELIVERY,
324
+ );
319
325
  await deps.answerCallbackQuery(query.id);
320
326
  return;
321
327
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.16.3",
3
+ "version": "0.16.5",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"