@llblab/pi-telegram 0.34.1 → 0.35.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  > Each release keeps at most 8 outcome records of at most 512 characters.
4
4
 
5
+ ## 0.35.1: Semantic Ragged Control Layouts
6
+
7
+ - `Row Composition`: Teaches Generated Control Surface to model Telegram controls as an ordered ragged sequence of independently sized semantic rows rather than filling a rectangular matrix: compact rows hold genuine peers, singleton rows isolate independent actions, and symmetry is admitted only when equal relationships or real spatial topology provide evidence for it.
8
+ - `Layout Catalog`: Adds an adaptable shape vocabulary for singleton, binary peer, asymmetric staged, navigation-plus-collection, repeated text-pair, and true rectangular surfaces; agents infer task relationships first and must not force work into a preset shape.
9
+ - `Layout Hierarchy`: Limits word-, phrase-, and icon-plus-text controls to two columns and moves additional choices into more semantic rows; denser rows remain reserved for short position-bearing glyphs or codes. Row widths vary intentionally, preserve structural reading order, forbid uniformity padding, and use rectangular grids only when spatial topology carries real meaning.
10
+
11
+ ## 0.35.0: Compact Matrix Literal
12
+
13
+ - `Compact Wire Format`: Adds portable Compact Matrix Literal alongside JSON and attributes: `{value}` copies label to prompt, `{label|prompt}` separates them, top-level cells remain full-width, nested rows preserve horizontal grouping, and atom boundaries trim automatically.
14
+ - `Literal Fidelity`: Preserves non-structural printable Unicode—including brackets, quotes, commas, colons, paths, and spaces—while decoding only `\|`, `\}`, and `\\`; unknown/trailing escapes, empty values, repeated separators, controls, malformed delimiters, empty rows, and deeper nesting fail closed before callback registration.
15
+ - `Renderer-Owned Width`: Removes the artificial parser-level row-width cap because Telegram documents none, while bundled Skills keep five columns as the proven default, admit six through eight only for very short position-bearing labels, and direct wider surfaces toward regrouping. A dedicated portable standard owns grammar, JSON-first routing, conformance, and versioning.
16
+
5
17
  ## 0.34.1: Compact Stateful Control Surfaces
6
18
 
7
19
  - `Prompt Compression`: Teaches Generated Control Surface to encode the smallest sufficient action delta—such as a coordinate, symbol, identifier, or short verb—when visible conversation establishes unambiguous state, while retaining stable identity when delivery or routing could separate action from context.
package/README.md CHANGED
@@ -192,7 +192,7 @@ Voice notes, audio, images, PDFs, and other media can pass through configured in
192
192
 
193
193
  ### Buttons And Callbacks
194
194
 
195
- Assistant replies can include top-level hidden `telegram_button` comments using a JSON object, JSON matrix, or compact double-quoted attributes; `telegram_buttons` is a plural alias. In a matrix, top-level objects become full-width rows while nested arrays group one to five buttons horizontally. Prefer one array 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.
195
+ Assistant replies can include top-level hidden `telegram_button` comments using a JSON object, JSON matrix, Compact Matrix Literal (CML), or compact double-quoted attributes; `telegram_buttons` is a plural alias. In JSON and CML matrices, 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}` or `{label|prompt}`, 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.
196
196
 
197
197
  ### Threaded Mode And Multi-Instance Bus
198
198
 
package/docs/README.md CHANGED
@@ -15,6 +15,7 @@ Living index of project documentation in `/docs`.
15
15
  - [command-templates.md](./command-templates.md) — Portable command-template standard core
16
16
  - [inbound.md](./inbound.md) — Local `pi-telegram` inbound text/media handler bus, programmatic inbound handlers, registered STT provider fallbacks, legacy `attachmentHandlers` compatibility, placeholders, and fallbacks
17
17
  - [outbound.md](./outbound.md) — Local `pi-telegram` outbound-handler config, text/voice/button behavior, single-artifact Rich results, voice synthesis provider fallback priority, artifact outputs, and callback routing
18
+ - [compact-matrix-literal.md](./compact-matrix-literal.md) — Portable Compact Matrix Literal v1 standard for trimmed key-value cells, minimal escapes, bounded-depth rows, JSON coexistence, and renderer-owned width policy
18
19
  - [callback-namespaces.md](./callback-namespaces.md) — Shared Telegram `callback_data` namespace standard for layered extensions
19
20
  - [updates.md](./updates.md) — Update classification and runtime handler registry that lets layered extensions observe and consume Telegram updates without owning their own polling connection
20
21
  - [multi-instance-bus.md](./multi-instance-bus.md) — Optional multi-instance Telegram bus architecture: profile-scoped transport, leader/follower routing, thread targets, instance slots, manual follower registration, and recovery semantics
@@ -327,7 +327,7 @@ Outbound files staged during an active Telegram turn are delivered after that tu
327
327
  Assistant-authored final-message actions use hidden top-level comments with colon-free markers; colon-prefixed payloads are rejected:
328
328
 
329
329
  - `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.
330
- - `telegram_button` accepts a JSON object, JSON matrix, or double-quoted attributes; `telegram_buttons` aliases the same parser. Each top-level matrix object creates one full-width row, while a nested array groups one to five buttons horizontally; every callback enqueues its configured prompt text as a normal Telegram prompt turn.
330
+ - `telegram_button` accepts a JSON object, JSON matrix, Compact Matrix Literal, or double-quoted attributes; `telegram_buttons` aliases the same parser. Each top-level matrix cell creates one full-width row, while a nested row groups one or more buttons horizontally without an artificial parser-width cap; every callback enqueues its configured prompt text as a normal Telegram prompt turn. CML's portable grammar, trim/escape rules, JSON-first routing, and renderer-owned width policy are specified in [Compact Matrix Literal](./compact-matrix-literal.md).
331
331
 
332
332
  Preview delivery strips top-level action comments before streaming draft Markdown. Comments inside code fences, quotes, lists, or indented examples stay literal.
333
333
 
@@ -362,7 +362,7 @@ Complete intermediate assistant text blocks from Telegram-originated activity ar
362
362
 
363
363
  `assistant.activity` is an independent bridge-owned projection over normalized Activity events. Each process reloads the shared file-backed setting at `agent-start` before activity admission, so multi-instance mode cannot continue projecting a stale broader process-local selection. Omitted values resolve to `verbose`, while invalid values fail closed to `quiet`; `thinking` and `tools` select one technical class, while `verbose` enables both. Provider-exposed thinking uses persistent ordinary HTML containing only a standard expandable blockquote with a bounded redacted latest-text window and inline Markdown rendered as Telegram HTML. Completed executed tools use native Rich Messages: each closed `<Tool>: <status>` root details node renders snake-case names as title words while preserving an uppercase two- or three-letter repeated prefix per word, then the native disclosure chevron reveals an open-by-default `arguments` child plus closed retained `update N` and `result`/`error` child details with lowercase monospaced, marker-free summaries and JSON pre blocks; known-safe Rich rejections fall back to the previous HTML disclosure. The projection captures the exact target and transport stamp at activity admission, serializes updates, preserves tool-start order, closes coalescing across assistant/thinking boundaries, bounds retained text/update memory plus edit frames and message/tool size, disables previews and HTTP(S) auto-link recognition inside technical evidence, and never replays a possibly committed send. Session generations own independent queues, so replacement drops queued old work without waiting on an old call. Both proactive prose and active-turn final delivery wait for the admitted activity queue inside their extension-owned delivery tasks, preserving technical-before-semantic ordering without delaying Pi lifecycle completion. Settlement, replacement, disconnect, failure, or stale authority clears only local ownership; already-sent activity messages remain in chat.
364
364
 
365
- Telegram prompt guidance is context- and authority-aware. The package and source-checkout extension both contribute `telegram-bridge` plus the optional `generated-control-surface` Skill through Pi resource discovery. The latter treats `interface = f(state, capabilities, intent)` as a renderer-neutral primitive, compiling transient evidence-backed controls over domain-owned workflows, systems, navigation, supervision, and decisions without creating parallel application state. Repeated controls carry the smallest sufficient action delta when visible conversation is unambiguous; larger or error-prone state moves to a deterministic task-owned Markdown artifact, correctness-sensitive transitions move to a small domain-owned transition implementation, and repeated clicks are adjudicated against current state rather than stale button appearance. Its filesystem adapter reserves the first full-width row for parent traversal outside root, places available Previous/Next controls together in one compact row immediately afterward, orders visible directories, hidden directories, visible files, and hidden files alphabetically within each category before fixed ten-entry pagination, renders path/range metadata as stacked status-style key-value rows instead of middle-dot prose, emits the complete Telegram control set through one JSON-matrix action, suppresses duplicate plain/monospaced listings and default Refresh unless user preference overrides presentation, and retains an ordinary numbered fallback when buttons are unavailable. Only an exact direct owner or live registered follower exposes the two pi-telegram delivery tools, their active-tool metadata, and the compact routing suffix. Disconnect or authority loss removes those tool surfaces for subsequent requests without touching foreign tools; reconnect/recovery restores only the pi-telegram subset that was active before suspension, including across same-process reload. Telegram-originated turns route to the stable Skill contract and retain dynamic blocks such as `[voice] delivery: automatic voice`; the Skill and public documentation own syntax, target routing, Threaded Mode behavior, and diagnostics.
365
+ Telegram prompt guidance is context- and authority-aware. The package and source-checkout extension both contribute `telegram-bridge` plus the optional `generated-control-surface` Skill through Pi resource discovery. The latter treats `interface = f(state, capabilities, intent)` as a renderer-neutral primitive, compiling transient evidence-backed controls over domain-owned workflows, systems, navigation, supervision, and decisions without creating parallel application state. It composes an ordered ragged sequence of independently sized semantic rows rather than filling a rectangular grid: compact rows contain genuine peers, singleton rows isolate structurally independent actions, and rectangular layouts remain reserved for genuinely spatial state. Text-bearing controls use at most two columns and flow into additional rows, while denser rows are reserved for short position-bearing glyphs or codes. Symmetry is treated as an evidence claim about equal relationships or real spatial topology; an abstract layout catalog supplies adaptable singleton, peer, staged, navigational, repeated-pair, and rectangular shapes without forcing tasks into preset grids. Repeated controls carry the smallest sufficient action delta when visible conversation is unambiguous; larger or error-prone state moves to a deterministic task-owned Markdown artifact, correctness-sensitive transitions move to a small domain-owned transition implementation, and repeated clicks are adjudicated against current state rather than stale button appearance. Its filesystem adapter reserves the first full-width row for parent traversal outside root, places available Previous/Next controls together in one compact row immediately afterward, orders visible directories, hidden directories, visible files, and hidden files alphabetically within each category before fixed ten-entry pagination, renders path/range metadata as stacked status-style key-value rows instead of middle-dot prose, emits the complete Telegram control set through one JSON-matrix action, suppresses duplicate plain/monospaced listings and default Refresh unless user preference overrides presentation, and retains an ordinary numbered fallback when buttons are unavailable. Only an exact direct owner or live registered follower exposes the two pi-telegram delivery tools, their active-tool metadata, and the compact routing suffix. Disconnect or authority loss removes those tool surfaces for subsequent requests without touching foreign tools; reconnect/recovery restores only the pi-telegram subset that was active before suspension, including across same-process reload. Telegram-originated turns route to the stable Skill contract and retain dynamic blocks such as `[voice] delivery: automatic voice`; the Skill and public documentation own syntax, target routing, Threaded Mode behavior, and diagnostics.
366
366
 
367
367
  ## In-Flight Model Switching
368
368
 
@@ -0,0 +1,189 @@
1
+ # Compact Matrix Literal
2
+
3
+ > Status: Portable v1 standard implemented by the `pi-telegram` 0.35.0 release candidate.
4
+
5
+ Compact Matrix Literal (CML) is a bounded-depth text format for ordered key-value cells arranged as singleton or compact rows. It optimizes repeated interactive controls where JSON field names, quotes, and commas dominate the payload.
6
+
7
+ CML is transport-neutral. An embedding maps each decoded cell's `key` and `value` to its own domain. The `pi-telegram` profile maps `key` to button label and `value` to button prompt.
8
+
9
+ ## Goals
10
+
11
+ - Encode common key-value matrices with minimal punctuation.
12
+ - Preserve ordered singleton and compact rows.
13
+ - Preserve non-structural Unicode text literally.
14
+ - Admit deterministic linear-time parsing without evaluation or recovery.
15
+ - Remain deterministic beside an existing JSON form.
16
+ - Fail closed on malformed or deeper structures.
17
+
18
+ ## Non-Goals
19
+
20
+ - Replacing JSON for arbitrary objects, metadata, styles, or extensible schemas.
21
+ - Defining callback ownership, application state, rendering policy, or transport behavior.
22
+ - Recovering partial intent from malformed input.
23
+ - Defining one universal visual row-width limit for every renderer.
24
+
25
+ ## Data Model
26
+
27
+ A decoded payload is an ordered non-empty list of non-empty rows:
28
+
29
+ ```text
30
+ Cell = { key: string, value: string }
31
+ Rows = Cell[][]
32
+ ```
33
+
34
+ A top-level cell normalizes to a singleton row. A nested row preserves its compact grouping.
35
+
36
+ A cell with one atom copies its key into its value:
37
+
38
+ ```text
39
+ {7} == { key: "7", value: "7" }
40
+ ```
41
+
42
+ A cell with two atoms separates key and value with one unescaped vertical bar:
43
+
44
+ ```text
45
+ {🟥|2,5} == { key: "🟥", value: "2,5" }
46
+ ```
47
+
48
+ ## Grammar
49
+
50
+ The normative structural grammar is:
51
+
52
+ ```text
53
+ payload := cell | matrix
54
+ matrix := "[" ws element (ws element)* ws "]"
55
+ element := cell | row
56
+ row := "[" ws cell (ws cell)* ws "]"
57
+ cell := "{" atom "}"
58
+ | "{" atom "|" atom "}"
59
+ atom := atom-unit+
60
+ atom-unit := ordinary | "\\|" | "\\}" | "\\\\"
61
+ ws := *(SP | HTAB | CR | LF)
62
+ ```
63
+
64
+ `ordinary` is any printable Unicode scalar other than unescaped `|`, unescaped `}`, or `\`. No commas separate elements. A matrix and every nested row must contain at least one element. A row cannot contain another row.
65
+
66
+ Examples:
67
+
68
+ ```text
69
+ {Continue}
70
+ {Open|/tmp}
71
+ [{Up|/}[{Prev|page-1}{Next|page-3}]{etc|/etc}]
72
+ [[{1}{2}{3}{4}{5}{6}{7}{8}]]
73
+ {A \| B|C:\\Games\}}
74
+ ```
75
+
76
+ These normalize respectively to one copied singleton cell, one key-value singleton cell, a mixed singleton/compact matrix, one eight-cell row, and `{ key: "A | B", value: "C:\\Games}" }`.
77
+
78
+ ## Atoms, Whitespace, And Escapes
79
+
80
+ Leading and trailing whitespace in each decoded key and value is trimmed. Internal ordinary spaces are preserved. CR, LF, HTAB, C0 controls, DEL, and C1 controls that remain inside an atom after trimming are invalid.
81
+
82
+ Only three escape sequences exist:
83
+
84
+ ```text
85
+ \| → literal |
86
+ \} → literal }
87
+ \\ → literal \
88
+ ```
89
+
90
+ Unknown escapes and a trailing backslash are invalid. No character is silently dropped.
91
+
92
+ Every other printable character is literal inside a cell, including:
93
+
94
+ ```text
95
+ { [ ] " : , / emoji and ordinary spaces
96
+ ```
97
+
98
+ An opening `{` has no structural meaning after a cell has begun. Square brackets are structural only outside a cell. A second unescaped vertical bar is invalid. When multiline text or additional metadata is needed, the producer uses the embedding's JSON or other full-fidelity form.
99
+
100
+ ## Width Policy
101
+
102
+ CML Core does not impose a visual row-width maximum. Width is a renderer and interaction-policy concern, not a property of the key-value matrix wire format.
103
+
104
+ An embedding may enforce a documented host limit. The `pi-telegram` parser does not add an artificial per-row width cap because Telegram Bot API does not document one and existing top-level matrices already admit host-bounded action counts. Its bundled Generated Control Surface Skill owns UX policy: five columns are the proven default for short position-bearing labels, six to eight may be used only when labels remain compact and readable, and wider surfaces should normally be regrouped.
105
+
106
+ ## Parsing Contract
107
+
108
+ A conforming parser:
109
+
110
+ 1. Consumes Unicode text without executing, interpolating, or evaluating it.
111
+ 2. Parses exactly one `payload` and rejects trailing non-whitespace input.
112
+ 3. Rejects empty atoms, empty matrices, empty rows, and nesting deeper than one row inside the top-level matrix.
113
+ 4. Rejects missing, extra, crossed, or mismatched delimiters.
114
+ 5. Rejects a second unescaped vertical bar in a cell.
115
+ 6. Decodes only `\|`, `\}`, and `\\`; unknown or trailing escapes fail.
116
+ 7. Trims atom boundaries, then rejects empty values and remaining control characters.
117
+ 8. Returns no partial rows or cells after any failure.
118
+ 9. Runs in linear time over a host-bounded payload and does not recurse beyond the fixed grammar depth.
119
+
120
+ Implementations may report diagnostics internally, but an invalid payload must not register or execute any action.
121
+
122
+ ## JSON Coexistence
123
+
124
+ An embedding that already accepts JSON uses deterministic routing:
125
+
126
+ 1. Attempt strict JSON parsing first for payloads beginning with `{` or `[`.
127
+ 2. If JSON parsing succeeds, validate only against the embedding's JSON schema. A JSON shape failure must not fall back to CML.
128
+ 3. If JSON parsing fails, attempt CML from the original source.
129
+ 4. Accept CML only after complete grammar and embedding validation.
130
+
131
+ Valid JSON behavior therefore remains unchanged. A malformed JSON-looking source receives no tolerant recovery: it is accepted only when it independently forms a complete valid CML payload.
132
+
133
+ ## `pi-telegram` Profile
134
+
135
+ For `telegram_button` and its exact `telegram_buttons` alias:
136
+
137
+ - `Cell.key` becomes the visible button label.
138
+ - `Cell.value` becomes the queued prompt.
139
+ - A top-level cell becomes one full-width inline-keyboard row.
140
+ - A nested row becomes one horizontal row.
141
+ - `{value}` is equivalent to JSON `{"value":"value"}`.
142
+ - `{label|prompt}` is equivalent to JSON `{"label":"label","prompt":"prompt"}`.
143
+ - JSON and double-quoted attributes remain the full-fidelity forms.
144
+ - `selected_style` and future metadata are not represented by CML v1.
145
+ - Invalid CML is stripped with its enclosing recognized action comment and registers no callbacks, matching existing fail-closed action behavior.
146
+
147
+ Example embedding:
148
+
149
+ ```html
150
+ <!-- telegram_button [{⬆️ Up|/}[{⬅️|page-1}{➡️|page-3}]{📁 etc|/etc}] -->
151
+ ```
152
+
153
+ The enclosing HTML-comment transport still owns its own delimiter boundary; content containing the comment terminator cannot reach the CML parser and must use another supported delivery representation.
154
+
155
+ ## Conformance Classes
156
+
157
+ A conformance suite covers properties rather than incident-specific strings.
158
+
159
+ ### Accepted
160
+
161
+ - Singular copied and key-value cells.
162
+ - Top-level singleton rows.
163
+ - Nested rows at widths one, five, and eight.
164
+ - Mixed singleton and compact rows.
165
+ - Unicode, punctuation, brackets, quotes, commas, colons, and internal spaces.
166
+ - Trimmed atom boundaries.
167
+ - Each defined escape sequence.
168
+ - Structural whitespace between tokens.
169
+ - Semantic equivalence with supported JSON cell forms.
170
+
171
+ ### Rejected
172
+
173
+ - Empty payload, matrix, row, key, or value.
174
+ - Deeper nesting.
175
+ - Missing or mismatched delimiters.
176
+ - A second unescaped separator.
177
+ - Unknown or trailing escapes.
178
+ - Internal control characters.
179
+ - Commas between cells.
180
+ - Trailing garbage.
181
+ - JSON that parses but fails the JSON action schema.
182
+
183
+ Every rejected case proves zero callback registration.
184
+
185
+ ## Versioning
186
+
187
+ This document defines CML v1. Compatible embeddings may impose documented host-level byte, cell-count, or width limits without changing the core grammar, but must preserve bounded-depth and fail-closed semantics.
188
+
189
+ Future versions must not assign new meaning to input rejected by a security or ownership boundary without an explicit version discriminator. Metadata fields, styles, and deeper structures require a revised standard rather than permissive v1 parsing.
package/docs/outbound.md CHANGED
@@ -158,15 +158,17 @@ I can continue.
158
158
  <!-- telegram_button [{"label":"⬆️ Up","prompt":"/"},[{"value":"⬅️ Previous"},{"value":"➡️ Next"}],{"label":"📁 etc","prompt":"/etc"}] -->
159
159
 
160
160
  <!-- telegram_buttons [[{"value":"Approve"},{"value":"Reject"}]] -->
161
+
162
+ <!-- telegram_button [{⬆️ Up|/}[{⬅️|page-1}{➡️|page-3}]{📁 etc|/etc}] -->
161
163
  ```
162
164
 
163
165
  Rules:
164
166
 
165
- - `telegram_button` accepts a JSON object, JSON matrix, or double-quoted HTML-like attributes; `telegram_buttons` is an exact plural alias. Shorthand, body, paired-comment, unquoted-attribute, and single-quoted-attribute forms are rejected.
167
+ - `telegram_button` accepts a JSON object, JSON matrix, [Compact Matrix Literal](./compact-matrix-literal.md), or double-quoted HTML-like attributes; `telegram_buttons` is an exact plural alias. CML uses `{value}` or `{label|prompt}`, trims both atoms, preserves non-structural printable text, and decodes only `\|`, `\}`, and `\\`. Shorthand, body, paired-comment, unquoted-attribute, and single-quoted-attribute forms are rejected.
166
168
  - A colon after either button marker is rejected so every payload form shares one unambiguous action marker.
167
169
  - 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.
168
170
  - 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.
169
- - Prefer one JSON array comment for multiple buttons. Each top-level object becomes one full-width inline-keyboard row in source order; a nested array groups one to five button objects into one horizontal row. Empty rows, rows above five buttons, and deeper nesting are rejected. Repeated singular comments remain valid.
171
+ - 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 second unescaped CML separator, 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.
170
172
  - Button actions are stored in memory with short `callback_data`; Telegram never sees the full prompt in the button payload.
171
173
  - After Telegram accepts a generated button callback as a queued prompt, the bridge changes that exact button to its configured selection style without changing agent-authored text or emoji. Set `selected_style` to `primary` (blue), `success` (green), or `danger` (red); omitted or invalid values fall back to `primary`. The style never suppresses queue admission. Other choices stay visually unchanged and remain available; the callback acknowledgement remains the fallback on clients that do not render button styles.
172
174
  - When generated button markup is the entire assistant reply, the bridge supplies the standard `☑️ **Choose an option:**` heading as visible message text so Telegram has a message to which it can attach the inline keyboard.
@@ -65,7 +65,7 @@ This command surface is a mobile companion subset, not a raw terminal-command br
65
65
  - `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.
66
66
  - The bundled `telegram-bridge` Skill owns action syntax, target routing, Threaded Mode, formatting, and profile-specific debugging guidance. The regular prompt routes applicable turns to that Skill. `telegram_attach` 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.
67
67
  - `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.
68
- - `telegram_button` hidden comments create inline buttons whose taps enqueue prompts. One marker accepts a JSON object, JSON matrix, or double-quoted attributes; `telegram_buttons` is an exact plural alias. Top-level matrix objects become full-width rows, while nested arrays group one to five buttons horizontally. Prefer one array comment for multiple buttons. Use `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_button` hidden comments create inline buttons whose taps enqueue prompts. One marker accepts a JSON object, JSON matrix, [Compact Matrix Literal](./compact-matrix-literal.md), or double-quoted attributes; `telegram_buttons` is an exact plural alias. Top-level matrix cells become full-width rows, while nested rows group one or more buttons horizontally without an artificial parser-width cap. CML uses `{value}` or `{label|prompt}`, 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.
69
69
 
70
70
  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.
71
71
 
@@ -205,6 +205,122 @@ export function parseTelegramActionPayload(
205
205
  return parseCanonicalTelegramActionAttributes(payload.source);
206
206
  }
207
207
 
208
+ const TELEGRAM_COMPACT_ACTION_CONTROL_PATTERN = /[\u0000-\u001f\u007f-\u009f]/u;
209
+
210
+ function parseTelegramCompactActionPayloadRows(
211
+ source: string,
212
+ ): Record<string, unknown>[][] | undefined {
213
+ let offset = 0;
214
+ const isStructuralWhitespace = (character: string | undefined): boolean =>
215
+ character === " " ||
216
+ character === "\t" ||
217
+ character === "\r" ||
218
+ character === "\n";
219
+ const skipWhitespace = (): void => {
220
+ while (isStructuralWhitespace(source[offset])) offset += 1;
221
+ };
222
+ const normalizeAtom = (value: string): string | undefined => {
223
+ const normalized = value.trim();
224
+ return normalized && !TELEGRAM_COMPACT_ACTION_CONTROL_PATTERN.test(normalized)
225
+ ? normalized
226
+ : undefined;
227
+ };
228
+ const parseCell = (): Record<string, unknown> | undefined => {
229
+ if (source[offset] !== "{") return undefined;
230
+ offset += 1;
231
+ const keySource: string[] = [];
232
+ const valueSource: string[] = [];
233
+ let hasSeparator = false;
234
+ while (offset < source.length) {
235
+ const character = source[offset]!;
236
+ if (character === "\\") {
237
+ const escaped = source[offset + 1];
238
+ if (escaped !== "|" && escaped !== "}" && escaped !== "\\") {
239
+ return undefined;
240
+ }
241
+ if (hasSeparator) valueSource.push(escaped);
242
+ else keySource.push(escaped);
243
+ offset += 2;
244
+ continue;
245
+ }
246
+ if (character === "|") {
247
+ if (hasSeparator) return undefined;
248
+ hasSeparator = true;
249
+ offset += 1;
250
+ continue;
251
+ }
252
+ if (character === "}") {
253
+ offset += 1;
254
+ const key = normalizeAtom(keySource.join(""));
255
+ if (!key) return undefined;
256
+ if (!hasSeparator) return { value: key };
257
+ const value = normalizeAtom(valueSource.join(""));
258
+ return value ? { label: key, prompt: value } : undefined;
259
+ }
260
+ if (hasSeparator) valueSource.push(character);
261
+ else keySource.push(character);
262
+ offset += 1;
263
+ }
264
+ return undefined;
265
+ };
266
+ const parseRow = (): Record<string, unknown>[] | undefined => {
267
+ if (source[offset] !== "[") return undefined;
268
+ offset += 1;
269
+ const row: Record<string, unknown>[] = [];
270
+ while (offset < source.length) {
271
+ skipWhitespace();
272
+ if (source[offset] === "]") {
273
+ offset += 1;
274
+ return row.length > 0 ? row : undefined;
275
+ }
276
+ if (source[offset] !== "{") return undefined;
277
+ const cell = parseCell();
278
+ if (!cell) return undefined;
279
+ row.push(cell);
280
+ }
281
+ return undefined;
282
+ };
283
+ const parseMatrix = (): Record<string, unknown>[][] | undefined => {
284
+ if (source[offset] !== "[") return undefined;
285
+ offset += 1;
286
+ const rows: Record<string, unknown>[][] = [];
287
+ while (offset < source.length) {
288
+ skipWhitespace();
289
+ const character = source[offset];
290
+ if (character === "]") {
291
+ offset += 1;
292
+ return rows.length > 0 ? rows : undefined;
293
+ }
294
+ if (character === "{") {
295
+ const cell = parseCell();
296
+ if (!cell) return undefined;
297
+ rows.push([cell]);
298
+ continue;
299
+ }
300
+ if (character === "[") {
301
+ const row = parseRow();
302
+ if (!row) return undefined;
303
+ rows.push(row);
304
+ continue;
305
+ }
306
+ return undefined;
307
+ }
308
+ return undefined;
309
+ };
310
+
311
+ skipWhitespace();
312
+ let rows: Record<string, unknown>[][] | undefined;
313
+ if (source[offset] === "{") {
314
+ const cell = parseCell();
315
+ rows = cell ? [[cell]] : undefined;
316
+ } else {
317
+ rows = parseMatrix();
318
+ }
319
+ if (!rows) return undefined;
320
+ skipWhitespace();
321
+ return offset === source.length ? rows : undefined;
322
+ }
323
+
208
324
  export function parseTelegramActionPayloadRows(
209
325
  comment: TelegramTopLevelHtmlComment,
210
326
  command: string,
@@ -225,7 +341,6 @@ export function parseTelegramActionPayloadRows(
225
341
  if (
226
342
  !Array.isArray(entry) ||
227
343
  entry.length === 0 ||
228
- entry.length > 5 ||
229
344
  !entry.every(isTelegramActionPayload)
230
345
  ) {
231
346
  return undefined;
@@ -234,7 +349,7 @@ export function parseTelegramActionPayloadRows(
234
349
  }
235
350
  return rows;
236
351
  } catch {
237
- return undefined;
352
+ return parseTelegramCompactActionPayloadRows(payload.source);
238
353
  }
239
354
  }
240
355
  if (payload.hasBody) return undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.34.1",
3
+ "version": "0.35.1",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -118,9 +118,36 @@ Re-check mutable targets immediately before execution. Access denial never autho
118
118
 
119
119
  ## Prompt Buttons
120
120
 
121
- Use the transport's canonical prompt-button syntax. For pi-telegram, one top-level `telegram_button` comment accepts one JSON object, double-quoted attributes, or a JSON matrix. A top-level object becomes one full-width row; a nested array groups one to five objects into one compact horizontal row. Prefer one array comment for multiple controls instead of repeating the marker; `telegram_buttons` is a plural alias, not a different format. Use compact rows only when the controls form one coherent peer group such as Previous/Next, approval alternatives, or view modes; keep ordinary actions full-width.
121
+ Use the transport's canonical prompt-button syntax. For pi-telegram, one top-level `telegram_button` comment accepts one JSON object, double-quoted attributes, a JSON matrix, or Compact Matrix Literal (CML). CML uses `{value}` or `{label|prompt}`, trims atom boundaries, preserves other printable text literally, and decodes only `\|`, `\}`, and `\\`; use JSON for multiline prompts, styles, or metadata. A top-level cell becomes one full-width row, while a nested row groups one or more controls horizontally without a parser-level width cap. Prefer one layout comment for multiple controls instead of repeating the marker; `telegram_buttons` is a plural alias, not a different format.
122
122
 
123
- Treat the five-wide matrix as an interaction primitive when spatial position carries meaning and acting through the surface communicates better than prose. Five-wide rows are proven usable with short, distinct labels; use fewer columns or full-width rows when labels need explanation, wrap ambiguously, or lose meaning without prose. Preserve the ordinary admission test: proactively offer an interactive surface even when the user did not request buttons when it materially reduces effort or demonstrates an available capability, but do not manufacture decorative interaction.
123
+ ### Semantic Row Composition
124
+
125
+ Model the control surface as an ordered ragged sequence of independently sized rows, not as a rectangular matrix to fill. Rectangular grids are one specialization for genuinely spatial or coordinate-bearing state; most interfaces should vary row width according to hierarchy, grouping, label pressure, and action priority.
126
+
127
+ - Put controls in one compact row only when they are genuine peers that answer the same local question or form one coherent toolbar/navigation group.
128
+ - Use a singleton full-width row for a structurally independent, pinned, primary, summary, or high-consequence action when separation improves comprehension.
129
+ - Vary row widths intentionally—for example `1 → 2 → 4 → 1 → 2`—and never pad a row with empty, duplicate, or no-op controls merely to produce uniform dimensions.
130
+ - Preserve reading order across rows: orientation and structural navigation first, primary content or choices next, secondary controls afterward, and destructive actions visibly separated when present.
131
+ - Use at most two columns when buttons carry words, phrases, icon-plus-text labels, or other text that must be read; move additional peer choices into more semantic rows rather than compressing four or more textual buttons across a phone-width line. Three through five columns are for short symbols, glyphs, coordinates, or compact codes whose position carries meaning. Six through eight may be used only for single-glyph or similarly minimal position-bearing labels whose grouping materially improves the interaction. Never shorten necessary wording merely to increase row density; regroup or use full-width rows when labels need explanation, wrap ambiguously, or lose meaning without prose.
132
+
133
+ Treat symmetry as an evidence claim about the task. Equal rectangular rows imply equal relationships and stable spatial correspondence; do not make that claim merely because the renderer supports a grid. Infer the task's independent, peer, ordered, hierarchical, and spatial relationships first, then let those relationships determine row boundaries and widths. A non-spatial task should be ragged by default, and its asymmetry should remain visible when that best communicates hierarchy or action weight.
134
+
135
+ ### Layout Catalog
136
+
137
+ Use this catalog as a shape vocabulary, not a fixed menu of demos:
138
+
139
+ - `1`: One independent full-width action, destination, summary drill-down, or consequence boundary.
140
+ - `2`: One binary or tightly coupled peer decision; neither control is visually subordinated.
141
+ - `1 → 2`: One orienting or primary action followed by a local peer pair.
142
+ - `2 → 1`: One peer mode/filter pair followed by an independent continuation or detail action.
143
+ - `1 → 2 → N×1`: Pinned structural navigation, compact traversal, then a vertical collection of independently readable items.
144
+ - `1 → 2 → 3 → 1`: Ragged staged control where context, local alternatives, denser short peers, and a separated terminal action have different semantic weight.
145
+ - Repeated `2`: Text-bearing choices distributed down the screen instead of compressed across it.
146
+ - `R×C`: A rectangular layout only when rows and columns map to genuine coordinates, repeated dimensions, or another stable spatial topology.
147
+
148
+ Adapt a surface by identifying relationships, grouping only genuine peers, ordering groups by task hierarchy, applying label-pressure limits, and then choosing the least regular layout that remains immediately comprehensible. Do not select a catalog shape first and force the task into it.
149
+
150
+ Preserve the ordinary admission test: proactively offer an interactive surface even when the user did not request buttons when it materially reduces effort or demonstrates an available capability, but do not manufacture decorative interaction.
124
151
 
125
152
  ### Interaction State And Prompt Compression
126
153
 
@@ -132,7 +159,7 @@ Treat the five-wide matrix as an interaction primitive when spatial position car
132
159
 
133
160
  ```html
134
161
  <!-- telegram_button {"label":"🔍 Inspect run","prompt":"Inspect Run run:example read-only, summarize its current status and latest material evidence, then regenerate relevant supervision controls."} -->
135
- <!-- telegram_button [{"label":"⬆️ Up","prompt":"/"},[{"value":"⬅️ Previous"},{"value":"➡️ Next"}],{"label":"📁 etc","prompt":"/etc"}] -->
162
+ <!-- telegram_button [{⬆️ Up|/}[{⬅️ Previous}{➡️ Next}]{📁 etc|/etc}] -->
136
163
  ```
137
164
 
138
165
  Button prompts must:
@@ -57,9 +57,10 @@ Button forms:
57
57
  <!-- telegram_button value="Continue" -->
58
58
  <!-- telegram_button [{"label":"⬆️ Up","prompt":"/"},[{"value":"⬅️ Previous"},{"value":"➡️ Next"}],{"label":"📁 etc","prompt":"/etc"}] -->
59
59
  <!-- telegram_buttons [[{"value":"Approve"},{"value":"Reject"}]] -->
60
+ <!-- telegram_button [{⬆️ Up|/}[{⬅️|page-1}{➡️|page-3}]{📁 etc|/etc}] -->
60
61
  ```
61
62
 
62
- - `telegram_button` accepts one JSON object, a JSON matrix, or double-quoted attributes; `telegram_buttons` is an exact plural alias. In a matrix, each top-level object becomes a full-width row and each nested array groups one to five buttons into one horizontal row. Prefer one array comment for multiple buttons. Keep the complete action in one top-level comment and encode JSON line breaks as `\n`.
63
+ - `telegram_button` accepts one JSON object, a JSON matrix, Compact Matrix Literal (CML), or double-quoted attributes; `telegram_buttons` is an exact plural alias. CML uses `{value}` or `{label|prompt}`, trims atom boundaries, preserves other printable text literally, and decodes only `\|`, `\}`, and `\\`. In a matrix, each top-level cell becomes a full-width row and each nested row groups one or more buttons horizontally without a parser-level width cap. Prefer one matrix comment for multiple buttons, normally keep generated rows at five columns or fewer, and use six through eight only for short position-bearing labels. Keep the complete action in one top-level comment and encode multiline content with JSON `\n`.
63
64
  - Use `label` plus a self-contained `prompt`, or non-empty `value` when both are identical.
64
65
  - Optional `selected_style` is `primary` (default), `success`, or `danger`; style never suppresses prompt admission.
65
66
  - If button comments form the whole reply, the bridge supplies the standard choice heading.