@llblab/pi-telegram 0.41.1 → 0.42.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/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  > Each release keeps at most 8 outcome records of at most 512 characters.
4
4
 
5
+ ## 0.42.0: Compact Action Grammar
6
+
7
+ - `Adaptive Action Grammar`: Extracts the first semantically valid JSON or positional CML payload from noisy `telegram_button` and `telegram_voice` envelopes, tolerates bounded trailing commas and unmatched matrix noise, keeps malformed named JSON on the JSON path, and retains legacy attribute parsing as undocumented compatibility.
8
+ - `Voice Action Cells`: Adds compact `{text}`, `{text|lang}`, and `{text|lang|rate}` voice actions while preserving JSON for multiline, named, and escape-heavy payloads; each voice comment remains one independent artifact and matrix-shaped voice payloads fail closed.
9
+ - `Thread-Aware Buttons`: Tags prompts created by generated-button callbacks with the same resolved Telegram Thread label as ordinary inbound turns, preserving current local labels, persisted bindings, target routing, queue ordering, and callback selection behavior.
10
+ - `Focused Agent Guidance`: Makes generated control surfaces proactively emit useful emoji-led prompt buttons, keeps Telegram action syntax under the bridge owner, and moves specialized delivery, configuration, diagnosis, layout, state, and capability guidance behind conditional references to reduce default context without dropping safety constraints.
11
+
5
12
  ## 0.41.1: Focused Operator Status Hotfix
6
13
 
7
14
  - `Next Turn Feedback`: Replies one Pi-aligned fully bold abort notice to the aborted Telegram turn, falls back to the `/next` command for local work, preserves completed intermediate output, and suppresses pending aborted assistant text so one forced transition no longer produces a contradictory second reply.
package/README.md CHANGED
@@ -10,7 +10,7 @@ It is a **runtime adapter**, not a remote terminal. Start or supervise work in t
10
10
 
11
11
  Every completed intermediate commentary block from a Telegram-originated turn is delivered once as its own message before the existing final reply. While Telegram is connected, local, autonomous, and unclassified extension follow-up work also projects visible checkpoints and the final answer to the authorized Telegram target once and in order, preserving assistant-authored `telegram_button` comments as interactive prompt buttons. This connected companion projection is always active rather than configurable. Neither path mirrors local prompts, thinking, tool traffic, token deltas, or stale-generation work. The separate `Activity` setting defaults to `verbose` so new installations discover collapsed provider-exposed thinking and tool evidence immediately; operators can narrow it to one class or choose `quiet`. See [Outbound](docs/outbound.md#public-assistant-output) and the [configuration reference](docs/public-api.md#configuration-api).
12
12
 
13
- 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.
13
+ This repository is an actively maintained standalone 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.
14
14
 
15
15
  ## Install
16
16
 
@@ -218,11 +218,11 @@ Inbound files land under `<agent-dir>/tmp/telegram` and default to a 50 MiB limi
218
218
 
219
219
  ### Voice And Media
220
220
 
221
- Voice notes, audio, images, PDFs, and other media can pass through configured inbound handlers, programmatic handlers, or registered STT providers. Outbound voice can use configured `outboundHandlers` or registered TTS providers; `pi-telegram` owns reply policy and Telegram transport, while providers own synthesis. Configure provider-neutral local/API pipelines and ordered fallbacks through [`telegram.json` command templates](./docs/voice.md#choose-an-integration-path). The default `manual` reply mode still supports intentional voice delivery through explicit `telegram_voice` actions; `mirror` and `always` add automatic voice policy. Explicit actions accept either a JSON object or compact double-quoted attributes, with equivalent `text` and `value` payload keys and one colon-free action marker.
221
+ Voice notes, audio, images, PDFs, and other media can pass through configured inbound handlers, programmatic handlers, or registered STT providers. Outbound voice can use configured `outboundHandlers` or registered TTS providers; `pi-telegram` owns reply policy and Telegram transport, while providers own synthesis. Configure provider-neutral local/API pipelines and ordered fallbacks through [`telegram.json` command templates](./docs/voice.md#choose-an-integration-path). The default `manual` reply mode still supports intentional voice delivery through explicit `telegram_voice` actions; `mirror` and `always` add automatic voice policy. Explicit actions prefer positional `{text}`, `{text|lang}`, or `{text|lang|rate}` cells and use JSON for multiline content, named fields, or escaping.
222
222
 
223
223
  ### Buttons And Callbacks
224
224
 
225
- Assistant replies can include top-level hidden `telegram_button` comments using a JSON object, adaptive JSON/CML matrix, positional Compact Matrix Literal (CML), or compact double-quoted attributes; `telegram_buttons` is a plural alias. One adaptive matrix may mix named JSON objects with positional CML cells, and commas between completed matrix or row elements are optional while JSON object internals remain strict. Top-level cells become full-width rows while nested rows group one or more buttons horizontally without an artificial parser-level width cap; generated surfaces default to five columns and use six to eight only for short position-bearing labels. CML uses `{value}`, `{label|prompt}`, or `{label|prompt|selected_style}` with `primary`, `success`, or `danger`; the optional style requires an explicit prompt. It trims atom boundaries, preserves non-structural text literally, and decodes only `\|`, `\}`, and `\\`. Prefer one matrix comment for multiple buttons. Buttons use `label` plus `prompt`, or the compact `value` key when both are identical. The action marker is colon-free for every payload form. The bridge strips the comments from visible text, renders inline buttons, and routes callbacks back into Pi as queued prompts or extension-owned callback actions. Button-only replies receive the standard `☑️ **Choose an option:**` heading as automatic visible fallback text. Once a generated prompt button is accepted, only that exact button switches to its optional `selected_style` (`primary` blue by default, `success` green, or `danger` red) without altering its agent-authored label or emoji; every style still queues the selected prompt.
225
+ Assistant replies can include top-level hidden `telegram_button` comments containing a JSON object, adaptive JSON/CML matrix, or positional Compact Matrix Literal (CML). One adaptive matrix may mix named JSON objects with positional CML cells; separators are optional and one trailing comma is tolerated, including inside JSON objects. Top-level cells become full-width rows while nested rows group one or more buttons horizontally without an artificial parser-level width cap; generated surfaces default to five columns and use six to eight only for short position-bearing labels. CML uses `{value}`, `{label|prompt}`, or `{label|prompt|selected_style}` with `primary`, `success`, or `danger`; the optional style requires an explicit prompt. It trims atom boundaries, preserves non-structural text literally, and decodes only `\|`, `\}`, and `\\`. Prefer one matrix comment for multiple buttons. Buttons use `label` plus `prompt`, or the compact `value` key when both are identical. The bridge strips the comments from visible text, renders inline buttons, and routes callbacks back into Pi as queued prompts or extension-owned callback actions. Button-only replies receive the standard `☑️ **Choose an option:**` heading as automatic visible fallback text. Once a generated prompt button is accepted, only that exact button switches to its optional `selected_style` (`primary` blue by default, `success` green, or `danger` red) without altering its agent-authored label or emoji; every style still queues the selected prompt.
226
226
 
227
227
  ### Threaded Mode And Multi-Instance Bus
228
228
 
@@ -352,10 +352,10 @@ Final delivery attaches reply metadata only where requested. Reply parameters ap
352
352
 
353
353
  Outbound files staged during an active Telegram turn are delivered after that turn completes. They use `telegram_attach`, are checked atomically per tool call, and use configurable size limits before photo/document upload. When no Telegram turn is active, `telegram_attach` sends files immediately to the paired/default chat, an assigned follower thread, or an explicit `chat_id` plus optional `thread_id`; `telegram_message` provides direct local/TUI Markdown text delivery for explicit user requests and runs the same `telegram_button` markup planner so buttons attach to that text message. Direct local/TUI delivery is singleton-controlled: classic mode requires this Pi instance to own `/telegram-connect`, while Threaded Mode followers must be registered and route through the leader-owned transport. Already accepted active-turn reply/attachment delivery remains session-local.
354
354
 
355
- Assistant-authored final-message actions use hidden top-level comments with colon-free markers; colon-prefixed payloads are rejected:
355
+ Assistant-authored final-message actions use hidden top-level comments:
356
356
 
357
- - `telegram_voice` accepts a JSON object or double-quoted HTML-like attributes and creates voice reply artifacts through configured outbound handlers, programmatic voice handlers, or registered synthesis providers.
358
- - `telegram_button` accepts a JSON object, adaptive JSON/CML matrix, positional Compact Matrix Literal, or double-quoted attributes; `telegram_buttons` aliases the same parser. Named JSON objects and positional cells may coexist, with commas optional only between completed matrix or row elements. Each top-level cell creates one full-width row, while a nested row groups buttons horizontally without an artificial parser-width cap; every callback enqueues its configured prompt text as a normal Telegram prompt turn. The JSON-first grammar, positional trim/escape rules, atomic rejection, and renderer-owned width policy are specified in [Adaptive Button Literal](./compact-matrix-literal.md).
357
+ - `telegram_voice` accepts one positional compact action cell or JSON object and creates one voice artifact through configured outbound handlers, programmatic voice handlers, or registered synthesis providers.
358
+ - `telegram_button` accepts a JSON object, adaptive JSON/CML matrix, or positional Compact Matrix Literal. Named JSON objects and positional cells may coexist, with commas optional only between completed matrix or row elements. Each top-level cell creates one full-width row, while a nested row groups buttons horizontally without an artificial parser-width cap; every callback enqueues its configured prompt text as a normal Telegram prompt turn. The JSON-first grammar, positional trim/escape rules, atomic rejection, and renderer-owned width policy are specified in [Adaptive Button Literal](./compact-matrix-literal.md).
359
359
 
360
360
  Preview delivery strips top-level action comments before streaming draft Markdown. Comments inside code fences, quotes, lists, or indented examples stay literal.
361
361
 
@@ -1,6 +1,6 @@
1
1
  # Adaptive Button Literal
2
2
 
3
- > Status: Portable CML v3 standard implemented by the unreleased `pi-telegram` control-surface parser.
3
+ > Status: Portable CML v3 button standard; `pi-telegram` also reuses its positional-cell mechanics for compact voice actions.
4
4
 
5
5
  Adaptive Button Literal is one bounded-depth matrix grammar over a shared button AST. It accepts strict JSON button objects, positional Compact Matrix Literal (CML) cells, or both in the same matrix and row. Commas between completed matrix or row elements are optional, so producers can progressively compress representation without changing runtime meaning.
6
6
 
@@ -85,9 +85,9 @@ atom-unit := ordinary | "\|" | "\}" | "\\"
85
85
  ws := *(SP | HTAB | CR | LF)
86
86
  ```
87
87
 
88
- `boundary` occurs only after one complete element and before another. It may contain one comma or no comma. Element delimiters make empty adjacency unambiguous. Leading, repeated, and trailing commas are invalid.
88
+ `boundary` occurs after one complete element. It may contain one comma or no comma; one trailing comma before a closing row or matrix delimiter is also tolerated. Element delimiters keep empty adjacency unambiguous. Leading and repeated commas remain invalid.
89
89
 
90
- A `json-object` is one complete strict JSON object. Its property commas, strings, escaping, nested values, and other internals remain ordinary strict JSON; comma optionality applies only between matrix or row elements.
90
+ A `json-object` is one complete JSON object. Strict JSON is attempted first; a bounded recovery removes commas immediately before `}` or `]` outside strings and retries. Property names, strings, escaping, nested values, and all other internals remain strict; missing property commas are not invented.
91
91
 
92
92
  Rows cannot contain rows. The grammar never recurses beyond one row inside the top-level matrix.
93
93
 
@@ -111,29 +111,31 @@ A conforming parser:
111
111
 
112
112
  1. Attempts strict JSON first for sources beginning with `{` or `[`. Successful JSON is validated only against the existing button matrix schema and never reinterpreted.
113
113
  2. If strict JSON parsing fails, parses the original source with the adaptive grammar.
114
- 3. Tries one complete strict JSON object at each cell boundary before positional interpretation.
115
- 4. Accepts at most one optional comma between completed matrix or row elements.
116
- 5. Rejects leading, repeated, trailing, or property-level omitted commas.
114
+ 3. Tries one complete strict JSON object, then bounded trailing-comma recovery, at each cell boundary before positional interpretation.
115
+ 4. Keeps JSON-shaped named objects on the JSON path when validation fails instead of exposing their source as positional text.
116
+ 5. Accepts at most one comma between elements or immediately before a closing row or matrix delimiter, while rejecting leading, repeated, or property-level omitted commas.
117
117
  6. Rejects empty atoms, matrices, rows, and nesting deeper than one row.
118
118
  7. Decodes only `\|`, `\}`, and `\\` in positional cells.
119
- 8. Consumes exactly one complete payload and rejects trailing content.
120
- 9. Returns no partial rows or cells after any failure.
121
- 10. Runs in linear time over a host-bounded payload with fixed grammar depth.
119
+ 8. Extracts the first complete valid payload from a tolerant comment envelope and ignores unrelated text or isolated unmatched matrix brackets around it.
120
+ 9. Returns no partial rows or cells from a balanced malformed candidate.
121
+ 10. Runs over host-bounded payloads with fixed grammar depth.
122
122
 
123
123
  Malformed JSON-looking input receives no generic recovery. It is accepted only if it independently forms a complete valid adaptive literal.
124
124
 
125
- ## Telegram Profile
125
+ ## Telegram Profiles
126
126
 
127
- For `telegram_button` and the exact `telegram_buttons` alias:
127
+ For `telegram_button` comments:
128
128
 
129
129
  - JSON `value` keeps its existing label/prompt fallback semantics.
130
- - Positional `{value}` is equivalent to JSON `{"value":"value"}`.
130
+ - Positional `{value}` is equivalent to JSON `{"value":"value"}`; a lone JSON `label` or `prompt` has the same both-fields shorthand semantics.
131
131
  - Positional `{label|prompt}` is equivalent to JSON `{"label":"label","prompt":"prompt"}`.
132
132
  - Positional `{label|prompt|selected_style}` is equivalent to the corresponding three-field JSON object.
133
133
  - Top-level cells become full-width rows.
134
134
  - Nested rows become horizontal keyboard rows.
135
135
  - Invalid payloads are stripped with their recognized action comment and register no callbacks.
136
136
 
137
+ For `telegram_voice`, one positional cell maps `{text}`, `{text|lang}`, or `{text|lang|rate}` to one voice artifact. JSON object cells remain available for named fields, escaping, and multiline text. Voice comments do not accept matrix or row composition.
138
+
137
139
  Example:
138
140
 
139
141
  ```html
@@ -161,12 +163,12 @@ Accepted classes include:
161
163
  Rejected classes include:
162
164
 
163
165
  - Empty payloads, matrices, rows, labels, prompts, or style atoms.
164
- - Leading, repeated, or trailing element commas.
166
+ - Leading or repeated element commas.
165
167
  - Missing commas between properties inside a JSON object.
166
168
  - Deeper row nesting.
167
169
  - Missing, crossed, or mismatched delimiters.
168
170
  - A third positional separator, unknown style, unknown escape, or trailing backslash.
169
- - Internal control characters and trailing garbage.
171
+ - Internal control characters.
170
172
  - Valid JSON that fails the existing JSON action schema.
171
173
 
172
174
  Every rejected case proves zero callback registration.
package/docs/outbound.md CHANGED
@@ -129,43 +129,34 @@ Synthesis providers receive the extracted `telegram_voice` text plus optional `l
129
129
 
130
130
  ## Voice Markup
131
131
 
132
- Assistant replies can include hidden voice actions in either supported payload form:
132
+ Assistant replies can include hidden voice actions as a positional compact cell or JSON object:
133
133
 
134
134
  ```md
135
135
  Full text answer stays here.
136
136
 
137
- <!-- telegram_voice {"value":"Short spoken companion summary.","lang":"ru","rate":"+30%"} -->
137
+ <!-- telegram_voice {Short spoken companion summary.|ru|+30%} -->
138
138
 
139
- <!-- telegram_voice text="Short spoken companion summary." lang="ru" rate="+30%" -->
139
+ <!-- telegram_voice {"text":"First line.\nSecond line.","lang":"ru"} -->
140
140
  ```
141
141
 
142
- The bridge strips the comment from Telegram text. On `agent_end`, it maps each `telegram_voice` action to a provider call, generates one file per action, and sends each file as an independent Telegram-native voice message. Equivalent `text` or `value` supplies the spoken payload, with explicit `text` taking precedence when both appear; `lang` and `rate` are optional. A colon after `telegram_voice` is rejected so both payload forms share one unambiguous action marker. Use JSON for long or escaped text and encode line breaks inside JSON strings as `\n`. The opening marker must start at column zero on a top-level line outside fenced code, quotes, lists, and indented examples; otherwise it remains literal Markdown.
142
+ The bridge strips the comment from Telegram text. On `agent_end`, it maps each `telegram_voice` action to a provider call, generates one file per action, and sends each file as an independent Telegram-native voice message. Prefer `{text}`, `{text|lang}`, or `{text|lang|rate}`. Use JSON when multiline content, named fields, or escaping earns the extra syntax; equivalent JSON `text` or `value` supplies the spoken payload, with explicit `text` taking precedence. Each comment creates one voice artifact, so voice cells do not accept button-style matrix composition. The opening marker must start at column zero on a top-level line outside fenced code, quotes, lists, and indented examples; otherwise it remains literal Markdown.
143
143
 
144
144
  ## Buttons Markup
145
145
 
146
- Assistant replies can include one or many button actions through one colon-free marker:
146
+ Assistant replies can include one or many button actions through a top-level `telegram_button` comment:
147
147
 
148
148
  ```md
149
149
  I can continue.
150
150
 
151
- <!-- telegram_button {"label":"Continue","prompt":"Continue with the current plan.","selected_style":"primary"} -->
152
-
153
- <!-- telegram_button label="Show risks" prompt="List the main risks first." selected_style="danger" -->
154
-
155
- <!-- telegram_button {"value":"Done"} -->
156
-
157
- <!-- telegram_button [{"label":"⬆️ Up","prompt":"/"},[{"value":"⬅️ Previous"},{"value":"➡️ Next"}],{"label":"📁 etc","prompt":"/etc"}] -->
158
-
159
- <!-- telegram_buttons [[{"value":"Approve"},{"value":"Reject"}]] -->
160
-
161
151
  <!-- telegram_button [{⬆️ Up|/}[{⬅️|page-1}{➡️|page-3}]{📁 etc|/etc}] -->
152
+
153
+ <!-- telegram_button {"label":"▶️ Continue","prompt":"Continue with the current plan.","selected_style":"primary"} -->
162
154
  ```
163
155
 
164
156
  Rules:
165
157
 
166
- - `telegram_button` accepts a JSON object, adaptive JSON/CML matrix, positional [Compact Matrix Literal](./compact-matrix-literal.md), or double-quoted HTML-like attributes; `telegram_buttons` is an exact plural alias. Named JSON objects and positional cells may coexist in one matrix or row, and commas are optional only between completed elements while JSON object internals remain strict. CML uses `{value}`, `{label|prompt}`, or `{label|prompt|selected_style}`; the optional third atom requires a prompt and accepts only `primary`, `success`, or `danger`. It trims every atom, preserves non-structural printable text, and decodes only `\|`, `\}`, and `\\`. Shorthand, body, paired-comment, unquoted-attribute, and single-quoted-attribute forms are rejected.
167
- - A colon after either button marker is rejected so every payload form shares one unambiguous action marker.
168
- - Use `label` plus `prompt`, or the compact `value` key when both strings are identical. Explicit `label` or `prompt` takes precedence over its `value` fallback. Use JSON with `\n` escapes for multiline prompts.
158
+ - The payload may be a JSON object, adaptive JSON/CML matrix, or positional [Compact Matrix Literal](./compact-matrix-literal.md). Named JSON objects and positional cells may coexist in one matrix or row. Commas are optional between completed elements, and one trailing comma before a closing delimiter is tolerated; JSON object validation likewise tolerates trailing commas but does not invent missing values, property names, or internal separators. CML uses `{value}`, `{label|prompt}`, or `{label|prompt|selected_style}`; the optional third atom requires a prompt and accepts only `primary`, `success`, or `danger`. It trims every atom, preserves non-structural printable text, and decodes only `\|`, `\}`, and `\\`.
159
+ - Use `label` plus `prompt`, or the compact `value` key when both strings are identical. If only `label`, only `prompt`, or one-field `{value}` is present, that string supplies both visible label and queued prompt. An explicit counterpart takes precedence over `value`. Use JSON with `\n` escapes for multiline prompts.
169
160
  - The opening marker must start at column zero on a top-level line outside fenced code, quotes, lists, and indented examples; otherwise it remains literal Markdown.
170
161
  - Prefer one matrix comment for multiple buttons. Each top-level JSON object or CML cell becomes one full-width inline-keyboard row in source order; a nested row groups one or more buttons horizontally. The parser imposes no artificial per-row width cap; empty rows, malformed cells, unknown/trailing CML escapes, a third unescaped CML separator, empty atoms, unknown selected styles, and deeper nesting are rejected atomically. Generated surfaces default to five columns and expand to six through eight only for short position-bearing labels. Repeated singular comments remain valid.
171
162
  - Button actions are stored in memory with short `callback_data`; Telegram never sees the full prompt in the button payload.
@@ -184,8 +175,8 @@ The extension injects prompt guidance by context:
184
175
  - For ordinary local/TUI prompts, the compact routing note points to the bundled `telegram-bridge` Skill and forbids Telegram use unless explicitly requested.
185
176
  - For Telegram-originated turns, the compact note routes the agent to `telegram-bridge`, which owns voice/button/direct-delivery/Threaded Mode/formatting/debug guidance.
186
177
  - For Telegram-originated turns, write the full technical answer as normal Markdown.
187
- - Add `telegram_voice` with either a JSON object or double-quoted attributes when a Telegram-native voice message is useful. A companion summary is optional, no specific summary format is required.
188
- - Add `telegram_button` with a JSON object, JSON matrix, or double-quoted attributes; `telegram_buttons` is an alias. Prefer one array for multiple controls. Use `label` plus `prompt`, or `value` when they are identical; `selected_style` is optional. A button-only reply may omit parent text because the bridge supplies `☑️ **Choose an option:**` automatically.
178
+ - Add `telegram_voice` with positional CML by default or JSON when multiline content, named fields, or escaping requires it. A companion summary is optional, no specific summary format is required.
179
+ - Add `telegram_button` with a JSON object, JSON matrix, or positional CML. Prefer one matrix for multiple controls. Use `label` plus `prompt`, or `value` when they are identical; `selected_style` is optional. A button-only reply may omit parent text because the bridge supplies `☑️ **Choose an option:**` automatically.
189
180
  - 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.
190
181
  - Prefer meaningful visible parent text when it adds context; for a button-only answer, rely on the bridge's automatic `☑️ **Choose an option:**` fallback rather than manufacturing duplicate text.
191
182
 
@@ -65,8 +65,8 @@ This command surface is a mobile companion subset, not a raw terminal-command br
65
65
  - `telegram_attach(paths, chat_id?, thread_id?, caption?)` is the stable artifact delivery tool for generated files. During Telegram turns it queues files for the active reply; with `assistant.rendering: "rich"`, exactly one PNG/JPEG, MP4, or MP3 artifact plus non-empty final Markdown can become one reply-anchored Rich Message. HTML mode, multiple/unsupported files, Guest Mode, and voice outputs retain their established paths. Outside Telegram turns the tool sends files directly to the paired/default chat, the registered follower's assigned thread, or an explicit `chat_id` plus optional `thread_id` when this Pi instance owns `/telegram-connect` or is registered with the multi-instance bus.
66
66
  - `telegram_message(text, chat_id?, thread_id?)` sends a direct Telegram Markdown message when this Pi instance owns `/telegram-connect` or is registered with the multi-instance bus. During an active Telegram turn, omitted targeting and an explicit target equal to that turn are rejected so the ordinary final-reply path remains the sole current-target response; an explicit different chat/thread target remains allowed for requested cross-target delivery. Outside active turns, paired/default local/TUI delivery remains unchanged. Top-level `telegram_button` comments inside `text` are parsed with the same planner used for normal replies and attached to that message; buttons are never standalone Telegram messages.
67
67
  - The bundled `telegram-bridge` Skill owns action syntax, target routing, Threaded Mode, formatting, Generative App operation, and profile-specific debugging guidance. The regular prompt routes applicable turns to that Skill. `telegram_attach`, `telegram_bind`, and `telegram_message` remain registered but are model-active only while this instance owns direct transport or holds a live follower registration; disconnect/loss suppresses their schemas and prompt metadata, and recovery restores only the operator's previously active pi-telegram subset.
68
- - `telegram_voice` hidden comments request Telegram-native voice delivery through either a JSON object or double-quoted attributes. Equivalent `text` or `value` supplies the spoken payload; explicit `text` takes precedence.
69
- - `telegram_button` hidden comments create inline buttons whose taps enqueue prompts. One marker accepts a JSON object, adaptive JSON/CML matrix, positional [Compact Matrix Literal](./compact-matrix-literal.md), or double-quoted attributes; `telegram_buttons` is an exact plural alias. Named JSON objects and positional cells may coexist in one matrix or row, and commas are optional only between completed elements. Top-level cells become full-width rows, while nested rows group one or more buttons horizontally without an artificial parser-width cap. CML uses `{value}`, `{label|prompt}`, or `{label|prompt|selected_style}`; the optional third atom requires a prompt and accepts only `primary`, `success`, or `danger`. It trims atom boundaries and supports only the minimal escapes `\|`, `\}`, and `\\`. Prefer one matrix comment for multiple buttons. Use JSON `label` plus `prompt`, or `value` when both strings are identical. Action markers are colon-free; colon-prefixed payloads are rejected. Use top-level column-zero comments outside code, quotes, lists, and indented examples; do not emit standalone button actions.
68
+ - `telegram_voice` hidden comments request Telegram-native voice delivery through `{text}`, `{text|lang}`, `{text|lang|rate}`, or a JSON object. JSON is the fallback for multiline content, named fields, or escaping; equivalent `text` or `value` supplies the spoken payload, with explicit `text` taking precedence.
69
+ - `telegram_button` hidden comments create inline buttons whose taps enqueue prompts. One marker accepts a JSON object, adaptive JSON/CML matrix, or positional [Compact Matrix Literal](./compact-matrix-literal.md). Named JSON objects and positional cells may coexist in one matrix or row; separators are optional and one trailing comma is tolerated at matrix, row, and JSON-object boundaries. Top-level cells become full-width rows, while nested rows group one or more buttons horizontally without an artificial parser-width cap. CML uses `{value}`, `{label|prompt}`, or `{label|prompt|selected_style}`; the optional third atom requires a prompt and accepts only `primary`, `success`, or `danger`. It trims atom boundaries and supports only the minimal escapes `\|`, `\}`, and `\\`. Prefer one matrix comment for multiple buttons. Use JSON `label` plus `prompt`, or `value` when both strings are identical. Action markers are colon-free; colon-prefixed payloads are rejected. Use top-level column-zero comments outside code, quotes, lists, and indented examples; do not emit standalone button actions.
70
70
 
71
71
  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.
72
72
 
@@ -12,7 +12,6 @@ import type {
12
12
  } from "./keyboard.ts";
13
13
  import {
14
14
  parseTelegramActionPayloadRows,
15
- parseTopLevelTelegramComment,
16
15
  replaceTopLevelHtmlComments,
17
16
  } from "./outbound-markup.ts";
18
17
  import {
@@ -113,8 +112,10 @@ function parseTelegramButtonAction(
113
112
  payload: Record<string, unknown>,
114
113
  ): TelegramOutboundButtonAction | undefined {
115
114
  const value = getTelegramButtonString(payload, "value");
116
- const label = getTelegramButtonString(payload, "label") ?? value;
117
- const prompt = getTelegramButtonString(payload, "prompt") ?? value;
115
+ const explicitLabel = getTelegramButtonString(payload, "label");
116
+ const explicitPrompt = getTelegramButtonString(payload, "prompt");
117
+ const label = explicitLabel ?? value ?? explicitPrompt;
118
+ const prompt = explicitPrompt ?? value ?? explicitLabel;
118
119
  if (!label || !prompt) return undefined;
119
120
  const selectedStyle = payload.selected_style;
120
121
  return {
@@ -179,10 +180,9 @@ export function planTelegramButtonReply(
179
180
  ): TelegramButtonReplyPlan {
180
181
  const keyboard: TelegramOutboundButtonMarkup["inline_keyboard"] = [];
181
182
  const stripped = replaceTopLevelHtmlComments(markdown, (comment) => {
182
- const command = ["telegram_button", "telegram_buttons"].find((candidate) =>
183
- parseTopLevelTelegramComment(comment, candidate),
184
- );
185
- if (!command) return comment.raw;
183
+ const command = "telegram_button";
184
+ const normalizedContent = comment.content.replace(/^\s+/, "").replace(/^!/, "");
185
+ if (!normalizedContent.startsWith(command)) return comment.raw;
186
186
  const payloadRows = parseTelegramActionPayloadRows(comment, command);
187
187
  if (!payloadRows) return "";
188
188
  const actionRows = payloadRows.map((payloadRow) =>
@@ -225,8 +225,9 @@ export function createTelegramButtonPromptTurn(options: {
225
225
  queueOrder: number;
226
226
  action: TelegramOutboundButtonAction;
227
227
  target?: TelegramQueueTarget;
228
+ telegramPrefix?: string;
228
229
  }): PendingTelegramTurn {
229
- const prompt = `[telegram] ${options.action.prompt}`;
230
+ const prompt = `${options.telegramPrefix ?? "[telegram]"} ${options.action.prompt}`;
230
231
  return {
231
232
  kind: "prompt",
232
233
  chatId: options.chatId,