@llblab/pi-telegram 0.35.2 → 0.36.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.
@@ -1,189 +1,176 @@
1
- # Compact Matrix Literal
1
+ # Adaptive Button Literal
2
2
 
3
- > Status: Portable v1 standard implemented by the `pi-telegram` 0.35.0 release candidate.
3
+ > Status: Portable CML v3 standard implemented by the unreleased `pi-telegram` control-surface parser.
4
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.
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
 
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
7
+ ```text
8
+ full named JSON → comma-optional adjacency → mixed named/positional cells → compact CML
9
+ ```
10
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.
11
+ The compact form is not JSON: `{label|prompt|variant}` assigns meaning by position. The formats share semantics and topology, not syntax.
17
12
 
18
- ## Non-Goals
13
+ ## Goals
19
14
 
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.
15
+ - Preserve one ordered button matrix across multiple representation densities.
16
+ - Let producers compress individual cells without converting the whole surface.
17
+ - Preserve valid strict JSON behavior unchanged.
18
+ - Admit deterministic linear-time parsing without evaluation or partial recovery.
19
+ - Keep rendering, callback ownership, and application state outside the notation.
24
20
 
25
21
  ## Data Model
26
22
 
27
- A decoded payload is an ordered non-empty list of non-empty rows:
23
+ Every accepted payload normalizes to a non-empty ordered list of non-empty rows:
28
24
 
29
25
  ```text
30
- Cell = { key: string, value: string }
26
+ Cell = { label?: string, prompt?: string, value?: string, selected_style?: string }
31
27
  Rows = Cell[][]
32
28
  ```
33
29
 
34
- A top-level cell normalizes to a singleton row. A nested row preserves its compact grouping.
30
+ A top-level cell becomes a singleton row. A nested row preserves horizontal grouping.
35
31
 
36
- A cell with one atom copies its key into its value:
32
+ These representations are semantically equivalent:
37
33
 
38
34
  ```text
39
- {7} == { key: "7", value: "7" }
35
+ [[{"label":"Pause","prompt":"music::pause"},{"value":"Next"}],{"value":"Status"}]
36
+ [[{"label":"Pause","prompt":"music::pause"}{"value":"Next"}]{"value":"Status"}]
37
+ [[{"label":"Pause","prompt":"music::pause"},{Next}],{Status}]
38
+ [[{Pause|music::pause}{Next}]{Status}]
40
39
  ```
41
40
 
42
- A cell with two atoms separates key and value with one unescaped vertical bar:
41
+ Named JSON objects and positional cells may coexist within the same horizontal row:
43
42
 
44
43
  ```text
45
- {🟥|2,5} == { key: "🟥", value: "2,5" }
44
+ [[{"label":"Open","prompt":"/tmp"}{Back|/}]]
46
45
  ```
47
46
 
48
- ## Grammar
47
+ ## Positional Cells
49
48
 
50
- The normative structural grammar is:
49
+ A one-atom cell copies its value into label and prompt through the existing button contract:
51
50
 
52
51
  ```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)
52
+ {Next}
62
53
  ```
63
54
 
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:
55
+ A two-atom cell separates label and prompt:
67
56
 
68
57
  ```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\}}
58
+ {Pause|music::pause}
74
59
  ```
75
60
 
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:
61
+ A three-atom cell adds the selected style:
83
62
 
84
63
  ```text
85
- \| → literal |
86
- \} → literal }
87
- \\ → literal \
64
+ {Stop|music::stop|danger}
88
65
  ```
89
66
 
90
- Unknown escapes and a trailing backslash are invalid. No character is silently dropped.
67
+ The style atom is accepted only as `primary`, `success`, or `danger`.
91
68
 
92
- Every other printable character is literal inside a cell, including:
69
+ ## Adaptive Grammar
70
+
71
+ The structural grammar is:
93
72
 
94
73
  ```text
95
- { [ ] " : , / emoji and ordinary spaces
74
+ payload := json-object | matrix | positional-cell
75
+ matrix := "[" ws element (boundary element)* ws "]"
76
+ element := cell | row
77
+ row := "[" ws cell (boundary cell)* ws "]"
78
+ cell := json-object | positional-cell
79
+ boundary := ws [","] ws
80
+ positional-cell := "{" atom "}"
81
+ | "{" atom "|" atom "}"
82
+ | "{" atom "|" atom "|" atom "}"
83
+ atom := atom-unit+
84
+ atom-unit := ordinary | "\|" | "\}" | "\\"
85
+ ws := *(SP | HTAB | CR | LF)
96
86
  ```
97
87
 
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.
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.
99
89
 
100
- ## Width Policy
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.
101
91
 
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.
92
+ Rows cannot contain rows. The grammar never recurses beyond one row inside the top-level matrix.
103
93
 
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.
94
+ ## Atoms, Whitespace, And Escapes
105
95
 
106
- ## Parsing Contract
96
+ Leading and trailing whitespace in positional atoms is trimmed. Internal ordinary spaces are preserved. CR, LF, HTAB, C0 controls, DEL, and C1 controls remaining inside an atom after trimming are invalid.
107
97
 
108
- A conforming parser:
98
+ Only three positional-cell escapes exist:
109
99
 
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.
100
+ ```text
101
+ \| → literal |
102
+ \} → literal }
103
+ \\ → literal \
104
+ ```
119
105
 
120
- Implementations may report diagnostics internally, but an invalid payload must not register or execute any action.
106
+ Every other printable character is literal inside a positional cell, including commas, colons, quotes, square brackets, emoji, and ordinary spaces. A comma inside `{label|prompt}` is data; a comma after the closing `}` is an optional element separator.
121
107
 
122
- ## JSON Coexistence
108
+ ## Deterministic Parsing
123
109
 
124
- An embedding that already accepts JSON uses deterministic routing:
110
+ A conforming parser:
125
111
 
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.
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
+ 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.
117
+ 6. Rejects empty atoms, matrices, rows, and nesting deeper than one row.
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.
130
122
 
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.
123
+ Malformed JSON-looking input receives no generic recovery. It is accepted only if it independently forms a complete valid adaptive literal.
132
124
 
133
- ## `pi-telegram` Profile
125
+ ## Telegram Profile
134
126
 
135
- For `telegram_button` and its exact `telegram_buttons` alias:
127
+ For `telegram_button` and the exact `telegram_buttons` alias:
136
128
 
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.
129
+ - JSON `value` keeps its existing label/prompt fallback semantics.
130
+ - Positional `{value}` is equivalent to JSON `{"value":"value"}`.
131
+ - Positional `{label|prompt}` is equivalent to JSON `{"label":"label","prompt":"prompt"}`.
132
+ - Positional `{label|prompt|selected_style}` is equivalent to the corresponding three-field JSON object.
133
+ - Top-level cells become full-width rows.
134
+ - Nested rows become horizontal keyboard rows.
135
+ - Invalid payloads are stripped with their recognized action comment and register no callbacks.
146
136
 
147
- Example embedding:
137
+ Example:
148
138
 
149
139
  ```html
150
- <!-- telegram_button [{⬆️ Up|/}[{⬅️|page-1}{➡️|page-3}]{📁 etc|/etc}] -->
140
+ <!-- telegram_button [{⬆️ Up|/},[{⬅️|page-1}{➡️|page-3}],{"label":"📁 etc","prompt":"/etc"}] -->
151
141
  ```
152
142
 
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.
143
+ The enclosing HTML-comment transport owns its own delimiter boundary. Content containing the comment terminator must use another supported representation.
154
144
 
155
- ## Conformance Classes
145
+ ## Width Policy
146
+
147
+ The grammar imposes no visual row-width maximum. Renderer and interaction policy own width. The bundled Generated Control Surface Skill defaults to at most five short position-bearing controls per row, permits six to eight only when labels remain compact, and treats eight as the phone-width UX maximum.
156
148
 
157
- A conformance suite covers properties rather than incident-specific strings.
149
+ ## Conformance
158
150
 
159
- ### Accepted
151
+ Accepted classes include:
160
152
 
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.
153
+ - Strict JSON objects and matrices.
154
+ - Positional singleton, two-atom, and styled cells.
155
+ - Matrices and rows with commas, without commas, or a mixture of boundaries.
156
+ - Named JSON and positional cells mixed in one matrix or row.
157
+ - Literal commas inside positional atoms and strict JSON strings.
158
+ - Unicode, defined escapes, structural whitespace, and rows at supported renderer widths.
159
+ - Semantic equivalence across every progressive-compression step.
170
160
 
171
- ### Rejected
161
+ Rejected classes include:
172
162
 
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.
163
+ - Empty payloads, matrices, rows, labels, prompts, or style atoms.
164
+ - Leading, repeated, or trailing element commas.
165
+ - Missing commas between properties inside a JSON object.
166
+ - Deeper row nesting.
167
+ - Missing, crossed, or mismatched delimiters.
168
+ - A third positional separator, unknown style, unknown escape, or trailing backslash.
169
+ - Internal control characters and trailing garbage.
170
+ - Valid JSON that fails the existing JSON action schema.
182
171
 
183
172
  Every rejected case proves zero callback registration.
184
173
 
185
174
  ## Versioning
186
175
 
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.
176
+ This document defines CML v3. V3 extends the v2 positional grammar with strict JSON object cells, mixed representation, and optional element-boundary commas. It does not make JSON object internals permissive and does not add deeper structures. Future versions must preserve strict-JSON-first routing, bounded depth, atomic rejection, and an explicit discriminator for any new meaning at a security or ownership boundary.
@@ -0,0 +1,310 @@
1
+ # Generative Apps Runtime For Telegram
2
+
3
+ _Status: incremental implementation. Canonical installation and explicit transactional replacement, agent-side method invocation, state/history commits, partial-tail recovery, cross-process transition locking with dead-owner recovery, installation-generation plus revision rejection for direct app-output controls, lifecycle-cancelled worker-isolated methods, the bounded non-shell process port, strict bound-action parsing, pre-model-queue `tgbtn` dispatch, new-message default views, and opt-in in-place bound-action edits with explicit-action send fallback are implemented locally. Agent-mediated initial-surface revision capture, process-birth lock proof, voice delivery, automatic refresh scheduling, removal, and complete lifecycle diagnostics remain open in the backlog._
4
+
5
+ ## Purpose
6
+
7
+ This document specifies the concrete Generative App runtime implemented by `pi-telegram`. The transport-independent concept, vocabulary, application shapes, hybrid action model, and agent operating workflow belong to the bundled [`generative-apps` Skill](../skills/generative-apps/SKILL.md).
8
+
9
+ The Telegram implementation provides managed installation, method execution, persistence, button binding, callback routing, and message delivery:
10
+
11
+ ```text
12
+ Telegram control → bound method → state/capability owner
13
+ Telegram view ← rendered output ← fresh result
14
+ ```
15
+
16
+ This runtime coexists with ordinary prompt buttons, companion-extension callbacks, Sections, and the Delivery API. It ships no application catalog or `examples/` tree; reusable domain scripts remain with their capability owners.
17
+
18
+ ## Ownership Split
19
+
20
+ - The bundled [`generative-apps` Skill](../skills/generative-apps/SKILL.md) owns the general concept and agent operation: category definition, `generated` versus `generative`, application shapes, hybrid method/prompt surfaces, selection, authorship, review, workflow, safety, and validation judgment.
21
+ - [`architecture.md`](./architecture.md#generative-apps) owns this runtime's place inside the Telegram bridge and its domain boundaries.
22
+ - This document owns only `pi-telegram` implementation contracts: canonical managed identity, executable ABI, Telegram wire syntax, state timeline, installation/replacement, bounded ports, callback routing, delivery, lifecycle, and current limitations.
23
+ - [`generated-control-surface`](../skills/generated-control-surface/SKILL.md) owns the separate ephemeral control-surface operating protocol.
24
+
25
+ Keep conceptual guidance out of this document and Telegram runtime mechanics out of the Generative Apps Skill.
26
+
27
+ ## Canonical Layout And Identity
28
+
29
+ Generative Apps live under the active Pi agent directory, never in package installation files or temporary storage:
30
+
31
+ ```text
32
+ <agent-dir>/genapps/
33
+ └── poker/
34
+ ├── poker.mjs
35
+ ├── state.json
36
+ └── states.jsonl
37
+ ```
38
+
39
+ Identity is structural:
40
+
41
+ ```text
42
+ app = directory name = module stem
43
+ poker = poker = poker.mjs
44
+ ```
45
+
46
+ No app manifest, per-app `package.json`, duplicated `name`, class registration, or default export is required. The `.mjs` extension supplies ESM semantics directly.
47
+
48
+ An app name is a unique lowercase ASCII identifier accepted by the runtime's path-safe validation. It must not contain path separators, `..`, `::`, or a native callback namespace delimiter.
49
+
50
+ ## Inference-Bypass Syntax
51
+
52
+ Compact Matrix Literal and full JSON buttons keep their existing `label + prompt` contract. A bound action is encoded entirely in the prompt string:
53
+
54
+ ```ebnf
55
+ bound-action = app "::" method [ "(" json-value ")" ]
56
+ ```
57
+
58
+ Examples:
59
+
60
+ ```text
61
+ poker::fold
62
+ poker::call(18)
63
+ poker::init({"seed":"abc"})
64
+ media::seek("+30s")
65
+ ```
66
+
67
+ No argument means no decorative empty parentheses. One optional argument is a strict JSON value; the runtime never evaluates JavaScript source from the argument.
68
+
69
+ CML:
70
+
71
+ ```text
72
+ [{Fold|poker::fold}{Call 18|poker::call(18)}]
73
+ ```
74
+
75
+ Equivalent JSON:
76
+
77
+ ```json
78
+ [
79
+ [
80
+ { "label": "Fold", "prompt": "poker::fold" },
81
+ { "label": "Call 18", "prompt": "poker::call(18)" }
82
+ ]
83
+ ]
84
+ ```
85
+
86
+ `app` is not a button property. Both representations normalize to the same prompt string, and routing happens afterward.
87
+
88
+ The double colon is the inference-bypass operator: it routes a generated prompt control to a registered deterministic owner before Pi queue admission. Native extension callbacks retain their existing single-colon grammar:
89
+
90
+ ```text
91
+ myext:action:payload native callback_data namespace
92
+ poker::call(18) generated prompt routed to a Generative App
93
+ ```
94
+
95
+ These routes do not conflict. Native callbacks are direct by construction. `::` exists only because an ordinary generated button prompt would otherwise enter the model queue.
96
+
97
+ An absent, stale, or invalid bound app fails closed and never degrades into an accidental model prompt.
98
+
99
+ ## `telegram_bind` Tool
100
+
101
+ One agent Tool owns installation and deliberate invocation through two mutually exclusive shapes.
102
+
103
+ Install an external self-contained module and initialize it:
104
+
105
+ ```ts
106
+ telegram_bind({
107
+ app: "poker",
108
+ script: "/path/to/poker.mjs",
109
+ argument: { seed: "abc" }
110
+ })
111
+ ```
112
+
113
+ The runtime copies the module to `<agent-dir>/genapps/poker/poker.mjs`, validates the canonical identity and required exports, transactionally invokes `init(argument)`, and initializes state. Existing installation is never overwritten without explicit replacement authority. Installed canonical modules are discovered directly when a bound action resolves; there is no separate app registry.
114
+
115
+ Explicitly replace an installed app after editing its script:
116
+
117
+ ```ts
118
+ telegram_bind({
119
+ app: "poker",
120
+ script: "/path/to/poker.mjs",
121
+ replace: true,
122
+ argument: { seed: "abc" }
123
+ })
124
+ ```
125
+
126
+ Replacement validates and initializes a complete staging app before publishing it under the existing app name. A failed `init` preserves the installed module, state, and timeline. Omitting `replace: true` keeps duplicate installation fail-closed, while setting it for an absent app also fails instead of silently changing replacement into installation.
127
+
128
+ Discover or reuse an app already written at its canonical path and invoke a named method:
129
+
130
+ ```ts
131
+ telegram_bind({
132
+ app: "poker",
133
+ method: "init",
134
+ argument: { seed: "abc" }
135
+ })
136
+ ```
137
+
138
+ Agent-side diagnostic invocation uses the same shape:
139
+
140
+ ```ts
141
+ telegram_bind({ app: "poker", method: "inspect" })
142
+ ```
143
+
144
+ `script` and `method` are mutually exclusive, and `replace` is valid only with `script`. Script installation or replacement implicitly invokes mandatory `init`; existing-app invocation names its method explicitly. Folder presence supplies durable discoverability across runtime replacement without introducing a manifest.
145
+
146
+ During an active Telegram turn, `telegram_bind` displays successful app output directly through the current outbound planner and exact turn target by default, including initial `init` output; its Tool result tells the agent not to repeat or reformat the delivered view. Set `display: false` for agent-only diagnosis. Outside an active Telegram turn, the Tool returns bounded output for exact caller-owned presentation rather than choosing a Telegram target implicitly. The same method invoked through `app::method(argument)` routes its rendered output directly to the owning Telegram surface.
147
+
148
+ ## Module Contract
149
+
150
+ A Generative App exports plain named async or synchronous functions. `init` is mandatory. Classes and default exports are outside the contract.
151
+
152
+ ```js
153
+ export async function init({ argument, run, signal }) {
154
+ const seed = argument?.seed ?? "default";
155
+ return {
156
+ state: { seed, turn: 0 },
157
+ output: "**Ready**\n\n<!-- telegram_button {Start|poker::start} -->"
158
+ };
159
+ }
160
+
161
+ export async function start({ state }) {
162
+ const nextState = { ...state, turn: state.turn + 1 };
163
+ return {
164
+ state: nextState,
165
+ output: `**Turn:** \`${nextState.turn}\``
166
+ };
167
+ }
168
+
169
+ export async function inspect({ state }) {
170
+ return { output: JSON.stringify(state) };
171
+ }
172
+ ```
173
+
174
+ The runtime context may contain only bounded capabilities required by the contract:
175
+
176
+ - Current immutable app state, absent for first initialization.
177
+ - Parsed optional JSON argument.
178
+ - Cancellation signal and current app revision.
179
+ - A bounded non-shell process port for coherent CLI adapters.
180
+ - Redacted app/target metadata needed for diagnostics and rendering ownership.
181
+
182
+ The runtime does not pass a raw Telegram client, bot token, Pi extension context, arbitrary transport operation, or mutable queue/session state.
183
+
184
+ A method result contains:
185
+
186
+ ```ts
187
+ interface GenerativeAppResult {
188
+ state?: JsonValue;
189
+ output: string;
190
+ viewMode?: "new" | "edit";
191
+ }
192
+ ```
193
+
194
+ `output` is ordinary assistant Markdown plus existing top-level voice/button markup. It passes through the established outbound planner rather than defining a second rendering language. Omitted `viewMode` defaults to `"new"`: the result arrives as a fresh message and the clicked button remains visibly selected on its prior surface. `viewMode: "edit"` opts one result into replacing the callback message and keyboard in place when Telegram permits it; edit failure after that explicit action may fall back to one new message.
195
+
196
+ Returning `state` requests a committed transition. Omitting `state` makes the method output-only, which supports inspection and live refresh without appending duplicate history. Invalid, oversized, non-serializable, or malformed results fail before state or Telegram effects commit.
197
+
198
+ ## Current State And State Timeline
199
+
200
+ `state.json` is the compact current projection read by the runtime and, when useful, by the agent:
201
+
202
+ ```json
203
+ {
204
+ "seed": "abc",
205
+ "turn": 2
206
+ }
207
+ ```
208
+
209
+ `states.jsonl` is the committed-state timeline. Its first line is the successful initial state; each later state-changing method appends one complete snapshot envelope:
210
+
211
+ ```jsonl
212
+ {"revision":0,"method":"init","argument":{"seed":"abc"},"state":{"seed":"abc","turn":0}}
213
+ {"revision":1,"method":"start","state":{"seed":"abc","turn":1}}
214
+ {"revision":2,"method":"start","state":{"seed":"abc","turn":2}}
215
+ ```
216
+
217
+ The state in `state.json` equals the state in the latest complete journal line. Runtime-owned locking, revision checks, complete-line append, atomic replacement, and recovery preserve that relation across concurrent clicks and interruption. A partial final JSONL line is never treated as committed state.
218
+
219
+ A successful `init` is a hard new-run boundary. It transactionally clears current state and prior history, writes the new initial snapshot as revision zero, and publishes the initial output. Initialization failure preserves the previous working state and journal unchanged.
220
+
221
+ Application state is the app's complete persistent checkpoint: it includes interaction/configuration state plus the latest normalized external projection needed to render, diagnose, or reconstruct the current view. An agent reading `state.json` should be able to identify material app reality such as selected track, playback state, queue position, backend, and last observation without executing the app first. This projection is explicitly a last-observed cache, not the external domain authority: before a mutation, explicit status, or refresh, a CLI-backed app re-reads the actual owner, then commits a new complete snapshot only when retained app state materially changes.
222
+
223
+ ## CLI Capability Adapters
224
+
225
+ A Generative App may compose several existing CLI tools when they belong to one coherent domain or user journey:
226
+
227
+ ```text
228
+ media Generative App → playerctl + mpv + local media library
229
+ git Generative App → git + gh
230
+ actors Generative App → documented Actor runtime capabilities
231
+ ```
232
+
233
+ A bounded process port uses executable plus argument arrays, an explicit working directory, output limits, timeout, cancellation, and redacted evidence. Shell interpolation is not the default contract.
234
+
235
+ ```js
236
+ const result = await run({
237
+ command: "playerctl",
238
+ args: ["metadata", "--format", "{{artist}} — {{title}}"],
239
+ timeoutMs: 10_000
240
+ });
241
+ ```
242
+
243
+ A generic `exec(arbitrary-shell-command)` Generative App is forbidden. It would turn Telegram into a remote terminal, bypass bounded capability ownership, and violate the mobile companion boundary. A direct button click authorizes only the installed app method and its validated argument, never arbitrary process execution.
244
+
245
+ ## Live Views
246
+
247
+ A Generative App sends a new message after a successful bound user action by default. This simple mode preserves prior surfaces and their visibly selected buttons, is robust across ordinary Telegram constraints, and remains a first-class behavior rather than a fallback to eliminate. A method may opt into `viewMode: "edit"` to replace the callback message and keyboard in place; if that explicit action cannot edit a deleted or otherwise unavailable message, it may send one fresh view because the click itself supplies recreation authority.
248
+
249
+ Automatic refresh is not implemented in the current runtime. The intended future contract uses an exported `refresh` method and a bounded scheduling hint; applications must not return or rely on that hint until the backlog item is complete:
250
+
251
+ ```js
252
+ export async function refresh({ state, run }) {
253
+ return {
254
+ output: await renderPlayer(state.player, run),
255
+ refreshAfterMs: 5000
256
+ };
257
+ }
258
+ ```
259
+
260
+ The runtime contract is:
261
+
262
+ - Missing `refreshAfterMs` stops automatic refresh.
263
+ - Values below two seconds clamp to two seconds.
264
+ - The next interval starts only after the prior refresh and Telegram edit settle; calls never overlap or accumulate.
265
+ - One refresh schedule exists per app, profile, target, and logical surface.
266
+ - An unchanged normalized frame digest causes no Telegram edit.
267
+ - Telegram `retry_after`, bounded backoff, lifecycle cancellation, target authority, and execution generation remain authoritative.
268
+ - Refresh is session-bound and does not silently resume after process replacement until the surface is opened again.
269
+ - Output-only refresh does not change `state.json` or append `states.jsonl`.
270
+
271
+ The runtime retains the latest `TelegramDeliveryHandle` in memory for each live app surface. The first frame sends a logical view; later app actions and refreshes edit that same view rather than creating message traffic.
272
+
273
+ Telegram does not reliably report deletion of every ordinary private bot message. When a supported deletion update identifies the handle, the runtime invalidates it immediately. When edit returns a known message-not-found result, the runtime forgets the handle and stops refresh. It never recreates a user-deleted view automatically; the next explicit user action or app opening may create a fresh view.
274
+
275
+ ## Lifecycle And Safety
276
+
277
+ Generative Apps are trusted local code and therefore an explicit capability grant, not a sandbox promise. The runtime still narrows accidental authority and operational failure:
278
+
279
+ - Installation validates canonical paths and rejects traversal, symlinks outside the managed root, identity mismatch, and silent replacement; explicit replacement stages and initializes the new app before swapping it under the same app.
280
+ - App execution cannot own Telegram polling, credentials, raw transport, Pi queue state, or another app's files through the provided contract.
281
+ - Per-app transitions serialize and compare immutable installation generation plus state revision so stale buttons cannot cross replacement or mutate newer state.
282
+ - State commit and Telegram effect ordering are explicit; ambiguous non-idempotent transport outcomes never replay blindly.
283
+ - Time, output, state-size, refresh-rate, and process bounds prevent one app from monopolizing the extension.
284
+ - Session/profile/target generation replacement makes old scheduled work inert.
285
+ - Diagnostics redact secrets and preserve app name, method, revision, failure class, and bounded stderr/result evidence.
286
+ - Removal cancels refresh, invalidates the live binding, and keeps destructive state deletion as a separate explicit operation.
287
+
288
+ ## Application Roles
289
+
290
+ The runtime supports two ownership roles without shipping application templates:
291
+
292
+ - A standalone deterministic app owns its complete application state and transition rules. Poker-like games are the reference shape for app-owned state, not a bundled catalog entry.
293
+ - A view/controller adapter owns only validated adapter configuration and a last-observed projection. A music-player remote is the reference shape: the Actor remains authoritative, while the Generative App samples structured status, invokes bounded controls, and renders the next view.
294
+
295
+ Both roles use the same module, state, bound-action, and safety contracts. Selection and authoring procedure belong to the bundled `generative-apps` Skill.
296
+
297
+ ## Validation Contract
298
+
299
+ Implementation is not complete until evidence covers:
300
+
301
+ - Canonical path identity, direct app discovery, copy/install, explicit replacement with failure preservation, removal, and traversal rejection.
302
+ - Mandatory `init`, named method dispatch, no-argument and strict-JSON argument parsing, missing exports, and result validation.
303
+ - Transactional initialization, state/history equality, concurrent/stale actions, partial journal recovery, and output-only methods.
304
+ - CML and full JSON button equivalence, inference bypass before Pi queue admission, absent-owner failure, and unchanged native callback routing.
305
+ - Direct classic, leader, and follower target delivery with generation fencing and no model turn.
306
+ - CLI process timeout, cancellation, output bounds, stderr diagnostics, and arbitrary-shell rejection.
307
+ - Live-view handle retention, unchanged-frame suppression, two-second minimum, non-overlap, coalescing, Telegram backoff, deletion invalidation, message-not-found handling, and lifecycle cancellation.
308
+ - Poker-style internal state and media-style external-state reference applications.
309
+
310
+ The canonical open implementation work remains in [`../BACKLOG.md`](../BACKLOG.md). This document owns the proposed subsystem contract and its architectural boundaries.
@@ -316,7 +316,7 @@ Threaded Mode should make follower threads behave like normal Telegram instance
316
316
  | Surface | Leader behavior | Follower requirement | Routing/ownership invariant | Regression evidence |
317
317
  | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
318
318
  | Prompt intake | Thread prompt queues locally | Thread prompt is forwarded and queued by the owning follower | Target ownership routes by `{ chatId, threadId }` before local handling | Routing tests for foreign target message forwarding |
319
- | Queued-message removal reactions | 👎/👻/💔/💩/🗑 removes pending prompt/media turn | Same reaction on a queued follower prompt removes that follower's pending turn before dispatch | When the leader forwards a prompt to a follower, it records `chatId/messageId -> follower instance` because Bot API reaction updates expose chat/message but not thread id | Update runtime regression records forwarded message ownership and forwards the later reaction |
319
+ | Queued-message removal reactions | 👎/👻/💔/💩/🗑 marks a pending prompt/media turn for deletion when it reaches dispatch | Same reaction on a queued follower prompt marks that follower's pending turn for deletion before model dispatch | When the leader forwards a prompt to a follower, it records `chatId/messageId -> follower instance` because Bot API reaction updates expose chat/message but not thread id | Update runtime regression records forwarded message ownership and forwards the later reaction |
320
320
  | Queue priority reactions | 👍/⚡/❤/🕊/🔥 prioritizes queued prompts | Same reactions prioritize follower queued prompts | Reaction forwarding uses stored message ownership, then follower mutates its local queue | Reaction mutation tests plus forwarded-reaction coverage |
321
321
  | Message edits | Edits update matching queued prompt text | Edits in a follower thread update that follower's queued prompt | Message target ownership forwards edits to the owning instance; stored message ownership is the fallback when Telegram edit payloads omit thread id | Update routing tests for foreign target and message-owned edited-message forwarding |
322
322
  | Callbacks/buttons/menus | Callback handled by the owning instance/menu state | Follower callbacks are forwarded to the owning follower; follower menu sends/edits/deletes route through leader transport | Leader records ownership for follower-sent Bot API messages so callbacks can route by message id even when Telegram omits thread id; Bot API edit/delete lacks thread id, so follower bus allows validated same-chat message operations | Callback forwarding, generated-button target, bus follower-sent ownership, and bus edit/delete allowlist tests |