@llblab/pi-kit 0.37.0 → 0.37.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 +5 -0
- package/README.md +1 -1
- package/node_modules/@llblab/pi-telegram/AGENTS.md +1 -1
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +8 -10
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +10 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-buttons.js +20 -18
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.d.ts +6 -6
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.js +23 -65
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound.js +3 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/prompts.d.ts +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/prompts.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/runtime.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/runtime.js +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +4 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +24 -13
- package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/dist/skills/telegram-bridge/SKILL.md +5 -5
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +5 -5
- package/node_modules/@llblab/pi-telegram/docs/compact-matrix-literal.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/generative-apps.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/outbound.md +8 -8
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +2 -2
- package/node_modules/@llblab/pi-telegram/lib/outbound-buttons.ts +17 -15
- package/node_modules/@llblab/pi-telegram/lib/outbound-markup.ts +28 -68
- package/node_modules/@llblab/pi-telegram/lib/outbound.ts +3 -3
- package/node_modules/@llblab/pi-telegram/lib/prompts.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/runtime.ts +2 -1
- package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +25 -12
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/SKILL.md +5 -5
- package/package.json +2 -2
|
@@ -106,7 +106,7 @@ Only the live cleanup origin uses the approved historical-protection reduction.
|
|
|
106
106
|
- `preview` / `replies` / `rendering`: throttled native Rich Markdown draft delivery, native final reply delivery, reply parameters, transport-limit chunking, and remaining Telegram HTML rendering (bold, italic, strikethrough, spoilers, code, links) for bridge-owned UI/compatibility surfaces.
|
|
107
107
|
- `delivery`: public extension operational-view delivery, active-turn/instance/aggregate/authorized target policy, logical chunk handles, per-target ordering, runtime generation fencing, and the process-local runtime membrane. Its bridge adapter composes the established UI/compat reply renderer with narrow bus-aware Telegram API and ownership ports; it never exposes bot clients or Pi contexts.
|
|
108
108
|
- `activity`: public normalized Pi lifecycle registration, activity/source identity, assistant segment and reasoning normalization, executed-tool events, non-blocking per-handler queues, delivery contexts, compatibility adapters, and shutdown fencing. Internal publication/command-work and handler drain notifications share the lifecycle-owned captured-session/transport observer with companion queues and API target counters; bindings/root wire those neutral ports into routing's existing leader cleanup scheduler, never into deletion authority. Routing retains busy/unproven-idle delivery hints without early admission, preserves ordinary timers, and lets semantic settlement or post-settlement output drain request fresh checks. The same domain extends assistant-output observation for connected companion projection: eligible completed local/autonomous public segments retain source order and deduplicate event identity. `bindings` assembles observation, authority, sender, and failure-projection ports; routing owns exact delivery authority, outbound composes established transformations and reply delivery, and Bot API domains implement transport. No separate proactive state-machine domain exists.
|
|
109
|
-
- `outbound-markup`:
|
|
109
|
+
- `outbound-markup`: delimiter-based assistant action comments, top-level button fence parsing, shared JSON/CML grammar, attribute parsing, voice reply planning, and preview/delivery stripping.
|
|
110
110
|
- `outbound`: outbound text transformations, voice/button artifact delivery, and generated callback actions.
|
|
111
111
|
- `generative-apps`: managed deterministic application identity, canonical installation and explicit replacement, content-addressed module loading, state timelines, cross-process transition serialization, bounded executable-plus-argv adaptation, `telegram_bind`, and pre-model-queue `app::method` invocation. It does not own Telegram transport, arbitrary shell execution, or the external application adapted by one Generative App.
|
|
112
112
|
- `outbound-attachments`: `telegram_attach`, queued outbound files, stat/limit checks, ordinary photo/document delivery, and narrow single-artifact Rich Message planning/sending for probe-confirmed photo/video/audio formats. It owns known-failure fallback eligibility and ambiguous-send no-replay classification through structural error contracts without importing Bot API helpers.
|
|
@@ -685,7 +685,7 @@ Native typing during compaction follows connected-instance activity rather than
|
|
|
685
685
|
- Thread-targeted typing is sent only to the concrete thread. Aggregate `All` mirroring is intentionally omitted because duplicating every keepalive multiplies shared-chat flood pressure. Compaction completion, failure, or timeout stops only a loop actually started by the compaction observer; a pre-existing agent-owned loop remains active. Ownership is sticky across a repeated compaction start, whose shared-loop start reports `false` for the observer's own running loop. Authority loss and shutdown still stop the keyed loop.
|
|
686
686
|
- Pi `ui_prompt_start` pauses typing while an extension-owned local prompt waits for the operator; `ui_prompt_end` emits the matching Activity boundary and resumes typing whenever agent or compaction work remains unsettled.
|
|
687
687
|
|
|
688
|
-
At every connected instance `agent_start`, the lifecycle binding starts Telegram's native `…typing` indicator in that instance's assigned target, whether the run came from Telegram, the local TUI, or an autonomous continuation such as Grow Loop. Terminal `Active` remains Telegram-turn-specific; the native indicator answers the separate question of whether the instance is doing agent work. Streaming keepalives reuse a valid typing owner with the same session generation, transport and target rather than revoking its pending HTTP/IPC action. Fresh context wrappers may reuse that owner within a proven generation; without generation evidence, context identity must match. Explicit queue dispatch and Telegram prompt handoff still replace the owner. Queue dispatch carries the queued turn's exact target into typing before `agent_start`, rather than reducing it to a chat ID. In Threaded Mode, missing or invalid Thread targets are refused before starting a loop or invoking the API; mode changes also invalidate a captured Classic chat-only loop. A rejected start never replaces an existing valid Thread loop. The starter also requires the captured Pi context and session generation to remain current at start, queued send, tick and late failure boundaries, even if the transport epoch is unchanged; replacing a context object in place cannot revive old activity. Message activity hooks capture generation across awaited preview work, and agent-start bindings recheck it before post-await activity/typing, so an obsolete finally or continuation cannot acquire replacement ownership. The typing sender carries its captured loop/context/generation/transport assertion as process-local call authority: direct API issuance rechecks it after queued work, and the follower caller rechecks it after registration waits and immediately before socket issuance without putting callbacks in IPC data. Automatic typing uses one HTTP attempt with no transport-family fallback or delayed replay, including forwarded calls; a future keepalive is a fresh owner-checked attempt. An already issued HTTP/RPC cannot be retroactively unsent. Each loop refreshes its exact target every three seconds and keeps one action in flight. The leader API runtime coalesces identical chat/thread/action calls for two seconds, admits different Threads independently, and shares a real Telegram 429 `retry_after` fence across every Thread key in that chat. A pending-request count retains that shared state until every concurrent action settles, so completion of a faster peer cannot orphan a late 429; expired gates prune opportunistically and each gate family retains at most 256 active keys. Suppression never schedules a delayed retry. A failed typing action remains a structured diagnostic but cannot project `error` onto an otherwise healthy connected/leader/follower status. Assistant message start/update hooks still re-arm typing during Telegram-owned turns so transient provider/model errors do not leave a continuing run without activity feedback, and agent/session completion stops it.
|
|
688
|
+
At every connected instance `agent_start`, the lifecycle binding starts Telegram's native `…typing` indicator in that instance's assigned target, whether the run came from Telegram, the local TUI, or an autonomous continuation such as Grow Loop. Terminal `Active` remains Telegram-turn-specific; the native indicator answers the separate question of whether the instance is doing agent work. Streaming keepalives reuse a valid typing owner with the same session generation, transport and target rather than revoking its pending HTTP/IPC action. Fresh context wrappers may reuse that owner within a proven generation; without generation evidence, context identity must match. Explicit queue dispatch and Telegram prompt handoff still replace the owner. Queue dispatch carries the queued turn's exact target into typing before `agent_start`, rather than reducing it to a chat ID. In Threaded Mode, missing or invalid Thread targets are refused before starting a loop or invoking the API; mode changes also invalidate a captured Classic chat-only loop. A rejected start never replaces an existing valid Thread loop. The starter also requires the captured Pi context and session generation to remain current at start, queued send, tick and late failure boundaries, even if the transport epoch is unchanged; replacing a context object in place cannot revive old activity. Message activity hooks capture generation across awaited preview work, and agent-start bindings recheck it before post-await activity/typing, so an obsolete finally or continuation cannot acquire replacement ownership. The typing sender carries its captured loop/context/generation/transport assertion as process-local call authority: direct API issuance rechecks it after queued work, and the follower caller rechecks it after registration waits and immediately before socket issuance without putting callbacks in IPC data. Automatic typing uses one HTTP attempt with no transport-family fallback or delayed replay, including forwarded calls; a future keepalive is a fresh owner-checked attempt. An already issued HTTP/RPC cannot be retroactively unsent. An ended loop supplies a process-local cancellation observation: only a typed API authority refusal with no underlying failure and paired with that proof is omitted from API failure logs, while the call still rejects without success or retry. If authority expires while handling a real HTTP/network failure, the authority error retains that failure as its cause and the API recorder logs the original failure. Active-owner refusals and HTTP/network failures keep their diagnostics; the cancellation observation never crosses IPC. Each loop refreshes its exact target every three seconds and keeps one action in flight. The leader API runtime coalesces identical chat/thread/action calls for two seconds, admits different Threads independently, and shares a real Telegram 429 `retry_after` fence across every Thread key in that chat. A pending-request count retains that shared state until every concurrent action settles, so completion of a faster peer cannot orphan a late 429; expired gates prune opportunistically and each gate family retains at most 256 active keys. Suppression never schedules a delayed retry. A failed typing action remains a structured diagnostic but cannot project `error` onto an otherwise healthy connected/leader/follower status. Assistant message start/update hooks still re-arm typing during Telegram-owned turns so transient provider/model errors do not leave a continuing run without activity feedback, and agent/session completion stops it.
|
|
689
689
|
|
|
690
690
|
### Rendering And Delivery
|
|
691
691
|
|
|
@@ -714,14 +714,14 @@ Outbound files staged during an active Telegram turn are delivered after that tu
|
|
|
714
714
|
|
|
715
715
|
The channel-post journal records only this agent path's own publication intent. `prepare` is exact and idempotent; `beginPublication` durably changes one prepared operation to outcome-unknown before any future non-idempotent send; only `confirmPublished` records the returned numeric channel/message identity. A retry never regains issuance from outcome-unknown or published state. The store validates profile and token fingerprint, refuses malformed/duplicate/over-capacity state, and lists newest retained records without claiming Telegram history. The public-`@username` direct-leader sender now uses the tool-call ID to prepare and begin this journal transition before `sendRichMessage`, then records the returned numeric channel/message identity plus bounded username/title observed by `getChat`. Retained outcome-unknown state refuses automatic replay. Bounded agent-facing listing is available locally. Exact edit/delete journal transitions fence each tool-call mutation as outcome-unknown before direct-leader `editMessageText` or `deleteMessage`, and require the same mutation ID to confirm edited or deleted state. Numeric-channel publication requires explicit `channel: true`; direct-leader `getChat` must prove the exact negative ID and channel type before issuance, and the send response must preserve that identity. Cross-process regressions prove one winner for concurrent publication/edit/delete issuance and no restart regrant. Reads fail closed before parsing links, foreign/loose files, unsupported no-follow platforms, identity races, or oversized input. No legacy post schema is migrated: absent state starts empty and unknown versions/fields fail closed. The 0.44 downgrade checker leaves this inert journal untouched because old code cannot issue its effects. Only an explicit successful list returns retained authored Markdown. Channel publication/list/mutation failures and runtime events use fixed messages without retained content, token, path, or arbitrary transport detail. Production-helper regressions model a lost successful caller ACK and an ambiguous send response: the same operation returns retained success or refuses outcome-unknown without a second transport call. The retry uses a freshly opened replacement store, proving durable behavior across helper replacement/restart. A native entrypoint regression disconnects/reconnects direct ownership and proves both retained success and ambiguous outcome avoid a second `sendRichMessage`. Sender-rights and live rollout evidence remain separate 0.45.0 work. Channel media posts upload one local `.jpg`/`.jpeg`/`.png`/`.webp` photo or `.mp4` video through the multipart transport with `text` as the HTML caption; kind, byte size (photo ≤ 10 MiB, video ≤ 50 MiB), and 1024 visible caption characters are validated before issuance, and unsupported types or albums are rejected rather than downgraded to links. The journal binds the inspected kind/file name/byte size/SHA-256 media identity and caption, so duplicate requests and lost acknowledgements never re-upload. A media-post edit replaces the caption through `editMessageCaption` with the same Markdown-to-HTML rendering, where `||spoiler||` now renders as `<tg-spoiler>`.
|
|
716
716
|
|
|
717
|
-
Assistant-authored final-message actions use hidden
|
|
717
|
+
Assistant-authored final-message actions use hidden HTML comments, with an additional fenced wrapper for in-body buttons:
|
|
718
718
|
|
|
719
719
|
- `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.
|
|
720
720
|
- `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).
|
|
721
721
|
|
|
722
|
-
Standalone
|
|
722
|
+
Standalone `telegram_button` fenced blocks reuse the same cell/matrix grammar and callback store, including adjacent complete cells and matrices whose normalized rows concatenate in source order, and compile to native Rich Markdown button rows between paragraphs. Rendering validates each complete block before registering any callbacks, escapes label text, preserves disabled cells, and leaves enclosing fenced examples literal. Opening and closing lines allow spaces/tabs; openings accept three or more backticks or tildes, optional whitespace before the exact `telegram_button` name, and trailing whitespace. Closures must use the same marker and at least the opening length, with no other text. Previews hide complete and unfinished action fences. HTML mode projects those controls into the footer. In-body callbacks acknowledge without rewriting the Rich message; selected-style highlighting remains footer-only.
|
|
723
723
|
|
|
724
|
-
|
|
724
|
+
Complete HTML action comments activate solely by their delimiters and accepted payload, independent of whitespace, adjacent text, lists, quotes or code fences. Same-line comments are independent. Escape the opening delimiter as `<!--` to show a literal example; backticks and backslashes do not disable an action comment. Exact-name button fences retain their separate top-level recognition rules. The Telegram surface independently strips every complete assistant-authored HTML comment from previews and final delivery regardless of Markdown position or comment owner; an unclosed comment is withheld through the remaining tail, and a comment-only result sends no text message. Pi's terminal transcript and model context remain unchanged.
|
|
725
725
|
|
|
726
726
|
Unknown callback data outside owned prefixes is forwarded as `[callback] <data>` only after built-in and extension handlers decline it.
|
|
727
727
|
|
|
@@ -154,7 +154,7 @@ For `telegram_button` hidden comments and fenced blocks:
|
|
|
154
154
|
- Nested rows become horizontal keyboard rows; horizontal intent still requires the explicit nested array.
|
|
155
155
|
- Invalid payloads are stripped with their recognized action comment and register no callbacks.
|
|
156
156
|
|
|
157
|
-
The wrapper selects placement without changing cell semantics. A hidden `telegram_button` HTML comment builds the footer keyboard. A standalone
|
|
157
|
+
The wrapper selects placement without changing cell semantics. A hidden `telegram_button` HTML comment builds the footer keyboard. A standalone fenced block opened by at least three backticks or tildes plus the exact name `telegram_button` renders rows between paragraphs in Native Rich Markdown. Delimiter lines accept horizontal indentation, optional whitespace before the name, and trailing spaces/tabs. Close on a separate line using the same marker and at least the opening length, with no other text. Enclosing code fences keep button-block examples literal. A singleton JSON/CML object needs no array in either wrapper. Fenced content must be one complete group sequence without trailing envelope text; any invalid group rejects the entire block before callback registration. Larger outer fences and ordinary code blocks remain literal examples; unclosed action fences are withheld. Native Rich rows support at most eight buttons and must fit one message chunk; these are renderer constraints, not grammar limits. HTML compatibility places fenced controls in the footer. See [Outbound](./outbound.md) for delivery and callback behavior.
|
|
158
158
|
|
|
159
159
|
````markdown
|
|
160
160
|
Description.
|
|
@@ -49,7 +49,7 @@ An app name is a unique lowercase ASCII identifier accepted by the runtime's pat
|
|
|
49
49
|
|
|
50
50
|
## Inference-Bypass Syntax
|
|
51
51
|
|
|
52
|
-
Compact Matrix Literal and full JSON buttons keep their existing `label + prompt` contract. App output may use hidden `telegram_button` comments for footer controls or standalone
|
|
52
|
+
Compact Matrix Literal and full JSON buttons keep their existing `label + prompt` contract. App output may use hidden `telegram_button` comments for footer controls or standalone `telegram_button` fenced blocks between paragraphs (three or more backticks or tildes, horizontal indentation/whitespace tolerated, same-marker closure no shorter than the opening); both singleton cells and mixed matrices retain the same generation/revision-bound routing. In-body controls acknowledge clicks without recoloring the message; HTML compatibility moves them to the footer. A bound action is encoded entirely in the prompt string:
|
|
53
53
|
|
|
54
54
|
```ebnf
|
|
55
55
|
bound-action = app "::" method [ "(" json-value ")" ]
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
`pi-telegram` maps hidden assistant-authored HTML comments to Telegram-native outbound actions.
|
|
4
4
|
|
|
5
|
-
Normal Telegram-turn replies are intentionally prompt-driven: the agent writes Markdown plus small hidden
|
|
5
|
+
Normal Telegram-turn replies are intentionally prompt-driven: the agent writes Markdown plus small hidden HTML comments, and the bridge performs transport after `agent_end`. `telegram_voice` and `telegram_button` are not Pi tools. Complete action comments activate by their delimiters and accepted payload regardless of surrounding whitespace or Markdown, including code fences; Telegram preview and final delivery remove every assistant-authored `<!-- … -->` block regardless of Markdown position or owning extension. Unclosed comment tails are withheld, comment-only text plans send no message, and the Pi terminal transcript remains unchanged. For local/TUI-initiated work where the user explicitly asks to send something to Telegram, the bridge also exposes direct tools: `telegram_message` for Markdown text and `telegram_attach` for file delivery when no Telegram turn is active. In classic mode, direct local/TUI delivery requires this Pi instance to own `/telegram-connect`; in Threaded Mode, a registered follower may route direct-tool sends through the leader-owned bus transport. During an active Telegram turn, `telegram_message` rejects an implicit or same-turn target so ordinary final delivery remains the sole current-target response. Its `thread` argument accepts a case-insensitive live thread name or numeric id: the bridge preflights one live owner, sends visibly, then admits the text as a source-attributed turn in that instance. Unknown, ambiguous, same-target, offline, and cross-chat destinations fail before sending. Existing `chat_id` plus `thread_id` targeting stays compatible; registered followers use authenticated, generation-fenced bus routing. Outbound behavior combines assistant prompt markup, text command-template handlers, registered voice synthesis providers, generated artifacts, direct Telegram tools, and reply delivery. Direct `telegram_message` text is planned through the same reply markup path, so embedded `telegram_button` comments become buttons attached to that text message.
|
|
6
6
|
|
|
7
7
|
Text handlers use the portable [Command Template Standard](./command-templates.md). Programmatic outbound handlers use `registerTelegramOutboundHandler(kind, handler)`. Voice replies can use configured command-template handlers or the provider API described in [Voice Integration](./voice.md).
|
|
8
8
|
|
|
@@ -163,11 +163,11 @@ Full text answer stays here.
|
|
|
163
163
|
<!-- telegram_voice {"text":"First line.\nSecond line.","lang":"ru"} -->
|
|
164
164
|
```
|
|
165
165
|
|
|
166
|
-
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.
|
|
166
|
+
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. Complete voice comments activate anywhere, including inline, indented, quoted and fenced text; surrounding Markdown is not an action boundary. Escape the opening delimiter as `<!--` for literal examples.
|
|
167
167
|
|
|
168
168
|
## Buttons Markup
|
|
169
169
|
|
|
170
|
-
Assistant replies can include one or many button actions through a
|
|
170
|
+
Assistant replies can include one or many button actions through a `telegram_button` HTML comment:
|
|
171
171
|
|
|
172
172
|
```md
|
|
173
173
|
I can continue.
|
|
@@ -181,15 +181,15 @@ Rules:
|
|
|
181
181
|
|
|
182
182
|
- 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}`, `{|prompt}`, or the corresponding three-atom form with `selected_style`; the optional third atom requires a non-empty prompt and accepts only `primary`, `success`, or `danger`. A fourth atom accepts `1` or `true` (disabled), and `0` or `false` (enabled), with exact lowercase spelling, permitting an empty style atom in this form: `{|Next||1}`. An omitted fourth position stays enabled. JSON uses boolean `disabled`; legacy attributes accept `disabled="true"` or `disabled="false"`. Invalid disabled values reject the candidate matrix atomically. Disabled cells require no prompt or selected style: `{Next|||1}` is label-only and `{|||1}` is blank, equivalent to JSON `{"label":"Next","disabled":true}` and `{"disabled":true}`. They retain their row and label, serialize as `{ text, disabled: {} }` (non-breaking-space text for a blank cell), and register no callback, prompt, or bound app action. It trims every atom, preserves non-structural printable text, and decodes only `\|`, `\}`, and `\\`.
|
|
183
183
|
- Use `label` plus `prompt`, or the compact `value` key when both strings are identical. If only `label`, only `prompt`, one-field `{value}`, or prompt-only `{|prompt}` 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.
|
|
184
|
-
-
|
|
184
|
+
- Complete HTML comments activate by their delimiters and accepted payload, regardless of indentation, surrounding text, lists, quotes or code fences. Multiple comments on one line are independent. Escape the opening delimiter as `<!--` for literal examples; backticks and backslashes do not disable action comments.
|
|
185
185
|
- 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 fourth unescaped CML separator, empty enabled-cell prompts or three-atom styles, invalid disabled flags, empty one-atom cells, unknown selected styles, and deeper nesting are rejected atomically. Multi-atom cells may omit the label; four-atom cells may additionally omit the selected style, and disabled cells may omit the prompt. Generated surfaces default to five columns and expand to six through eight only for short position-bearing labels. Repeated singular comments remain valid.
|
|
186
186
|
- Button actions are stored in memory with short `callback_data`; Telegram never sees the full prompt in the button payload.
|
|
187
|
-
- Standalone
|
|
187
|
+
- Standalone fences opened by at least three backticks or tildes and the exact name `telegram_button` use the same singleton/matrix JSON/CML grammar. Delimiter lines accept horizontal indentation, optional spaces/tabs before the name, and trailing whitespace. A closing line must use the same marker with at least the opening length and only surrounding horizontal whitespace; inline, quoted/list-prefixed delimiters and name suffixes do not activate a block. Button wrappers accept adjacent complete JSON/CML cells and matrices with optional commas between groups. Their normalized rows concatenate in source order: `[{A|a}] [{B|b}]` equals `[{A|a}{B|b}]`; a singleton cell is one vertical row and horizontal peers still require a row nested inside a matrix. Any invalid group in a fence rejects the entire block before callback registration. Complete valid blocks become native `<tg-button-row>` elements in `InputRichMessage.markdown`, at their original positions. Each row supports at most eight buttons and must fit a Rich Message chunk; validation precedes callback registration for the entire block. Labels are escaped, prompts never enter the native markup, and disabled cells have no callback. Missing closures, trailing payload garbage, and invalid blocks produce no controls. Other language names and enclosing code examples stay literal. Draft previews withhold complete and unfinished action fences without registering actions. HTML compatibility projects fenced rows into the bottom keyboard before comment-authored footer rows; source ordering within each group is retained.
|
|
188
188
|
- In-body controls share prompt/app dispatch, TTL, generation/revision checks, and sent-message target ownership with footer controls. Their acknowledgement is the selection feedback; `selected_style` does not recolor the Rich body. No callback-triggered body rewrite is introduced, preventing a stale click from overwriting newer app output. The operator confirmed the post-reload current-client smoke: singleton CML and mixed JSON/CML in-body rows, their ordinary prompt callbacks, a labeled disabled control, and the separate footer callback work as intended. Client type was not recorded. The operator also confirmed that HTML mode projects the in-body groups into the bottom keyboard as intended. Second-client coverage, follower routing, blank cells, and app-method dispatch/revision rejection remain live-gated. Bot API evidence: `InputRichBlockButtons` / Rich Markdown `tg-button-row` and `RichMessageButton` use ordinary `callback_data` and support disabled controls.
|
|
189
189
|
- After Telegram accepts a generated footer 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.
|
|
190
190
|
- 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.
|
|
191
191
|
|
|
192
|
-
|
|
192
|
+
Prefer separate comment lines for readability; inline comments also activate. Do not use tool calls for ordinary Telegram-turn buttons. The agent writes Markdown plus hidden comments; the bridge strips comments and attaches Telegram `reply_markup` after `agent_end`. For local/TUI-originated direct sends, put the same Markdown and `telegram_button` comments in `telegram_message(text)`.
|
|
193
193
|
|
|
194
194
|
Buttons are built in and do not need a command template because they are pure Telegram reply markup plus callback routing.
|
|
195
195
|
|
|
@@ -203,8 +203,8 @@ The extension injects prompt guidance by context:
|
|
|
203
203
|
- For Telegram-originated turns, the compact note routes the agent to `telegram-bridge` for voice/button/direct-delivery/Threaded Mode/formatting/debug guidance and to `show-me` when the user needs a visual explanation of work or behavior.
|
|
204
204
|
- For ordinary Telegram-originated answers, write the full technical answer as phone-width Markdown. `show-me` may instead deliver a focused self-contained HTML artifact when explicitly requested or when spatial density earns it, while keeping the immediate reply useful.
|
|
205
205
|
- 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.
|
|
206
|
-
- Add `telegram_button` with a JSON object, JSON matrix, or positional CML. Use a hidden comment for footer placement or a standalone
|
|
207
|
-
- 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
|
|
206
|
+
- Add `telegram_button` with a JSON object, JSON matrix, or positional CML. Use a hidden comment for footer placement or a standalone `telegram_button` fenced block for placement between paragraphs. Prefer one matrix for multiple controls. Use `label` plus `prompt`, or `value` when they are identical; `selected_style` and boolean `disabled` are optional. Keep at least one useful enabled action on an interactive surface. A button-only reply may omit parent text because the bridge supplies `☑️ **Choose an option:**` automatically.
|
|
207
|
+
- 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 `telegram_button` comments in its Markdown text because those buttons are attached to that text message.
|
|
208
208
|
- 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.
|
|
209
209
|
|
|
210
210
|
This keeps the agent focused on semantics, prevents Telegram action syntax from leaking into normal local replies, and lets the bridge handle low-latency Telegram adaptation.
|
|
@@ -68,7 +68,7 @@ An unbound Thread chooser offers **⛔️ Cancel routing** to the current paired
|
|
|
68
68
|
|
|
69
69
|
### Tools and assistant-authored actions
|
|
70
70
|
|
|
71
|
-
Every assistant-authored HTML comment is transport-private on Telegram: previews and final replies remove `<!-- … -->` blocks regardless of Markdown nesting or which extension owns the comment,
|
|
71
|
+
Every assistant-authored HTML comment is transport-private on Telegram: previews and final replies remove `<!-- … -->` blocks regardless of Markdown nesting or which extension owns the comment, and complete recognized comments activate voice or buttons independently of surrounding whitespace or Markdown, including code fences. Escape the opening delimiter as `<!--` for literal examples. Unclosed comment tails are withheld and a comment-only result sends no text message; Pi's terminal transcript remains unchanged.
|
|
72
72
|
|
|
73
73
|
- `telegram_bind({ app, script, argument? } | { app, method, argument? })` installs and initializes one canonical managed Generative App module under `<agent-dir>/genapps/<app>/<app>.mjs`, or invokes one named method on an installed app. Installation rejects silent replacement and noncanonical/symlink sources. Methods receive immutable JSON state, one optional JSON argument, cancellation, revision, and a bounded non-shell process port; successful state changes commit to `state.json` plus `states.jsonl`, while output-only methods leave history unchanged. After one-shot `tgbtn` resolution, a complete `app::method` or `app::method(<strict JSON>)` prompt invokes the installed app before Pi queue admission and sends its planned Markdown/buttons directly; malformed or failed bound actions never fall back to a model prompt. Direct app-output buttons retain hidden source revisions and stale actions fail before method execution; sibling processes serialize transitions and recover dead lock owners. Bound actions send a fresh message by default and retain the clicked button's selected state on its prior surface. A result may opt into `viewMode: "edit"` to replace the callback message and keyboard in place, with one fresh-send fallback only for that explicit action. Agent-mediated initial-surface revisions, process-birth lock proof, automatic refresh, and voice output are not implemented.
|
|
74
74
|
- `telegram_attach(paths, chat_id?, thread_id?, caption?)` is the stable artifact delivery tool for generated files. During Telegram turns it queues files before any separate final text; with `assistant.rendering: "rich"`, exactly one PNG/JPEG, MP4, or MP3 artifact plus non-empty final Markdown can become one reply-anchored Rich Message with media first. 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.
|
|
@@ -77,7 +77,7 @@ Every assistant-authored HTML comment is transport-private on Telegram: previews
|
|
|
77
77
|
- `telegram_message(text, chat_id?, media?, channel?, thread_id?, thread?)` sends a direct Telegram Markdown message when this Pi instance owns `/telegram-connect` or is registered with the multi-instance bus. A public `@username`, or an exact negative numeric channel ID with `channel: true`, is passed as `chat_id` without a local registry; channel delivery requires the direct leader, and Telegram enforces whether the bot has channel posting permission. Channel delivery accepts `media` as one local `.jpg`/`.jpeg`/`.png`/`.webp` photo or `.mp4` video (photo ≤ 10 MiB, video ≤ 50 MiB), uploaded through multipart `sendPhoto`/`sendVideo` with `text` as its HTML caption (≤ 1024 visible characters); unsupported media types and albums are rejected before issuance, and the durable channel-post journal binds media identity and caption so duplicate or lost-acknowledgement retries never re-upload. `thread` accepts a live numeric Thread id or its current acknowledged display title; name matching is case-insensitive and fails closed when absent or ambiguous, while delivery captures the numeric target. 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. 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.
|
|
78
78
|
- The bundled `telegram-bridge` Skill owns action syntax, target routing, Threaded Mode, formatting, Generative App operation, and profile-specific debugging guidance. The bundled `show-me` Skill owns portable evidence-honest explanations and adapts them to phone-width Markdown or self-contained HTML artifacts when Telegram is the active surface. The regular prompt routes applicable turns to these and the other bundled Skills. Compiled npm/git installations expose them through the package `pi.skills` manifest so Pi resource filters remain authoritative; a raw TypeScript checkout under Pi's `extensions` directory contributes its source Skill root at runtime instead. `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.
|
|
79
79
|
- `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.
|
|
80
|
-
- `telegram_button` hidden comments create footer buttons; standalone
|
|
80
|
+
- `telegram_button` hidden comments create footer buttons; standalone `telegram_button` fenced blocks create button rows between paragraphs in Native Rich Markdown. Delimiter lines tolerate horizontal indentation and whitespace before the exact name. Open with at least three backticks or tildes; close on a separate line with the same marker and no shorter a run, allowing only surrounding spaces/tabs. Enclosing code fences keep button-block examples literal. Both accept the same singleton or mixed JSON/CML matrix and share prompt/app routing; both wrappers also accept adjacent complete JSON/CML cells and matrices with optional separators, concatenating their normalized rows in source order. Cells remain singleton rows and nested horizontal rows retain their grouping. Native rows allow at most eight buttons and must fit one Rich Message chunk; invalid or incomplete blocks register nothing. Drafts hide action fences. HTML compatibility projects fenced controls into the footer. In-body clicks acknowledge without recoloring the Rich body; selected-style highlighting remains footer-only. 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}`, prompt-only `{|prompt}`, or the corresponding three-atom form with `selected_style`; an omitted label uses the existing prompt-as-label fallback, and the optional third atom requires a non-empty prompt and accepts only `primary`, `success`, or `danger`. A fourth atom accepts `1` or `true` (disabled), and `0` or `false` (enabled), with exact lowercase spelling; an omitted fourth position stays enabled, and the third atom may be empty in this form (`{|Next||1}`). JSON uses boolean `disabled`. Disabled cells need no prompt or selected style: `{Next|||1}` is label-only and `{|||1}` is blank (JSON `{"label":"Next","disabled":true}` and `{"disabled":true}`). The Telegram renderer supplies a non-breaking space only when the label is empty. Disabled cells stay visible but carry `disabled: {}` instead of callback data and register no prompt or bound action; invalid disabled values reject the candidate matrix. 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. HTML action comments activate regardless of surrounding Markdown; escape their opening delimiter for literal examples. Button fences require standalone delimiter lines outside enclosing code examples and quote/list prefixes, but allow horizontal indentation. Bare JSON/CML in prose never activates.
|
|
81
81
|
|
|
82
82
|
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.
|
|
83
83
|
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
parseTelegramActionPayloadRows,
|
|
16
16
|
parseTelegramButtonPayloadRows,
|
|
17
17
|
replaceTelegramButtonFences,
|
|
18
|
-
|
|
18
|
+
replaceTelegramHtmlComments,
|
|
19
19
|
} from "./outbound-markup.ts";
|
|
20
20
|
import {
|
|
21
21
|
type PendingTelegramTurn,
|
|
@@ -237,8 +237,21 @@ export function planTelegramButtonReply(
|
|
|
237
237
|
),
|
|
238
238
|
);
|
|
239
239
|
};
|
|
240
|
+
const footerKeyboard: TelegramOutboundButtonMarkup["inline_keyboard"] = [];
|
|
241
|
+
const stripped = replaceTelegramHtmlComments(markdown, (comment) => {
|
|
242
|
+
const command = "telegram_button";
|
|
243
|
+
const normalizedContent = comment.content
|
|
244
|
+
.replace(/^\s+/, "")
|
|
245
|
+
.replace(/^!/, "");
|
|
246
|
+
if (!normalizedContent.startsWith(command)) return comment.raw;
|
|
247
|
+
const payloadRows = parseTelegramActionPayloadRows(comment, command);
|
|
248
|
+
if (!payloadRows) return "";
|
|
249
|
+
const rows = buildRows(payloadRows, false);
|
|
250
|
+
if (rows) footerKeyboard.push(...rows);
|
|
251
|
+
return "";
|
|
252
|
+
});
|
|
240
253
|
const withRichButtons = replaceTelegramButtonFences(
|
|
241
|
-
|
|
254
|
+
stripped,
|
|
242
255
|
(payload, closed) => {
|
|
243
256
|
if (!closed) return "";
|
|
244
257
|
const payloadRows = parseTelegramButtonPayloadRows(payload);
|
|
@@ -253,19 +266,8 @@ export function planTelegramButtonReply(
|
|
|
253
266
|
return `\n${rows.map(renderTelegramRichButtonRow).join("\n\n")}\n`;
|
|
254
267
|
},
|
|
255
268
|
);
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
const normalizedContent = comment.content
|
|
259
|
-
.replace(/^\s+/, "")
|
|
260
|
-
.replace(/^!/, "");
|
|
261
|
-
if (!normalizedContent.startsWith(command)) return comment.raw;
|
|
262
|
-
const payloadRows = parseTelegramActionPayloadRows(comment, command);
|
|
263
|
-
if (!payloadRows) return "";
|
|
264
|
-
const rows = buildRows(payloadRows, false);
|
|
265
|
-
if (rows) keyboard.push(...rows);
|
|
266
|
-
return "";
|
|
267
|
-
});
|
|
268
|
-
const visibleMarkdown = normalizeMarkdownAfterButtonExtraction(stripped);
|
|
269
|
+
keyboard.push(...footerKeyboard);
|
|
270
|
+
const visibleMarkdown = normalizeMarkdownAfterButtonExtraction(withRichButtons);
|
|
269
271
|
return {
|
|
270
272
|
markdown:
|
|
271
273
|
keyboard.length > 0 && !visibleMarkdown
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Telegram outbound markup parsing helpers
|
|
3
3
|
* Zones: telegram outbound, assistant markup
|
|
4
|
-
* Owns
|
|
4
|
+
* Owns delimiter-based assistant action comment extraction, attribute parsing, and markup stripping shared by voice and outbound delivery
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { isWireRecord as isTelegramActionPayload } from "./wire.ts";
|
|
8
8
|
|
|
9
|
-
export interface
|
|
9
|
+
export interface TelegramHtmlComment {
|
|
10
10
|
raw: string;
|
|
11
11
|
content: string;
|
|
12
12
|
start: number;
|
|
@@ -34,7 +34,7 @@ function getMarkdownLineText(
|
|
|
34
34
|
function getTopLevelOpeningFence(
|
|
35
35
|
line: string,
|
|
36
36
|
): TelegramTopLevelFenceState | undefined {
|
|
37
|
-
const match = line.match(/^
|
|
37
|
+
const match = line.match(/^[ \t]*(`{3,}|~{3,})/);
|
|
38
38
|
const sequence = match?.[1];
|
|
39
39
|
if (!sequence) return undefined;
|
|
40
40
|
return {
|
|
@@ -47,7 +47,7 @@ function isTopLevelClosingFence(
|
|
|
47
47
|
line: string,
|
|
48
48
|
fence: TelegramTopLevelFenceState,
|
|
49
49
|
): boolean {
|
|
50
|
-
const match = line.match(/^
|
|
50
|
+
const match = line.match(/^[ \t]*(`{3,}|~{3,})[ \t]*$/);
|
|
51
51
|
const sequence = match?.[1];
|
|
52
52
|
return (
|
|
53
53
|
!!sequence &&
|
|
@@ -56,38 +56,21 @@ function isTopLevelClosingFence(
|
|
|
56
56
|
);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
export function
|
|
60
|
-
comments:
|
|
59
|
+
export function collectTelegramHtmlComments(markdown: string): {
|
|
60
|
+
comments: TelegramHtmlComment[];
|
|
61
61
|
openCommentStart?: number;
|
|
62
62
|
} {
|
|
63
|
-
const comments:
|
|
63
|
+
const comments: TelegramHtmlComment[] = [];
|
|
64
64
|
let offset = 0;
|
|
65
|
-
let fence: TelegramTopLevelFenceState | undefined;
|
|
66
65
|
while (offset < markdown.length) {
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (nextFence) {
|
|
76
|
-
fence = nextFence;
|
|
77
|
-
offset = lineEnd;
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
if (line.startsWith("<!--")) {
|
|
81
|
-
const closeIndex = markdown.indexOf("-->", offset + 4);
|
|
82
|
-
if (closeIndex === -1) return { comments, openCommentStart: offset };
|
|
83
|
-
const end = closeIndex + 3;
|
|
84
|
-
const raw = markdown.slice(offset, end);
|
|
85
|
-
const content = raw.slice(4, -3);
|
|
86
|
-
comments.push({ raw, content, start: offset, end });
|
|
87
|
-
offset = getMarkdownLineEnd(markdown, end);
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
offset = lineEnd;
|
|
66
|
+
const start = markdown.indexOf("<!--", offset);
|
|
67
|
+
if (start === -1) break;
|
|
68
|
+
const closeIndex = markdown.indexOf("-->", start + 4);
|
|
69
|
+
if (closeIndex === -1) return { comments, openCommentStart: start };
|
|
70
|
+
const end = closeIndex + 3;
|
|
71
|
+
const raw = markdown.slice(start, end);
|
|
72
|
+
comments.push({ raw, content: raw.slice(4, -3), start, end });
|
|
73
|
+
offset = end;
|
|
91
74
|
}
|
|
92
75
|
return { comments };
|
|
93
76
|
}
|
|
@@ -122,7 +105,7 @@ export function replaceTelegramButtonFences(
|
|
|
122
105
|
continue;
|
|
123
106
|
} else {
|
|
124
107
|
fence = getTopLevelOpeningFence(line);
|
|
125
|
-
if (fence &&
|
|
108
|
+
if (fence && /^[ \t]*(?:`{3,}|~{3,})[ \t]*telegram_button[ \t]*$/.test(line)) {
|
|
126
109
|
actionStart = offset;
|
|
127
110
|
contentStart = end;
|
|
128
111
|
}
|
|
@@ -136,11 +119,11 @@ export function replaceTelegramButtonFences(
|
|
|
136
119
|
return result + markdown.slice(copied);
|
|
137
120
|
}
|
|
138
121
|
|
|
139
|
-
export function
|
|
122
|
+
export function replaceTelegramHtmlComments(
|
|
140
123
|
markdown: string,
|
|
141
|
-
replacer: (comment:
|
|
124
|
+
replacer: (comment: TelegramHtmlComment) => string,
|
|
142
125
|
): string {
|
|
143
|
-
const { comments } =
|
|
126
|
+
const { comments } = collectTelegramHtmlComments(markdown);
|
|
144
127
|
if (comments.length === 0) return markdown;
|
|
145
128
|
let result = "";
|
|
146
129
|
let offset = 0;
|
|
@@ -152,32 +135,9 @@ export function replaceTopLevelHtmlComments(
|
|
|
152
135
|
return result + markdown.slice(offset);
|
|
153
136
|
}
|
|
154
137
|
|
|
155
|
-
function
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
let offset = 0;
|
|
159
|
-
let fence: TelegramTopLevelFenceState | undefined;
|
|
160
|
-
while (offset < markdown.length) {
|
|
161
|
-
const lineEnd = getMarkdownLineEnd(markdown, offset);
|
|
162
|
-
const line = getMarkdownLineText(markdown, offset, lineEnd);
|
|
163
|
-
const isLastLine = lineEnd >= markdown.length;
|
|
164
|
-
if (fence) {
|
|
165
|
-
if (isTopLevelClosingFence(line, fence)) fence = undefined;
|
|
166
|
-
offset = lineEnd;
|
|
167
|
-
continue;
|
|
168
|
-
}
|
|
169
|
-
const nextFence = getTopLevelOpeningFence(line);
|
|
170
|
-
if (nextFence) {
|
|
171
|
-
fence = nextFence;
|
|
172
|
-
offset = lineEnd;
|
|
173
|
-
continue;
|
|
174
|
-
}
|
|
175
|
-
if (isLastLine && (line === "<" || line === "<!" || line === "<!-")) {
|
|
176
|
-
return offset;
|
|
177
|
-
}
|
|
178
|
-
offset = lineEnd;
|
|
179
|
-
}
|
|
180
|
-
return -1;
|
|
138
|
+
function findOpenOrPartialHtmlCommentIndex(markdown: string): number {
|
|
139
|
+
const start = markdown.indexOf("<!--");
|
|
140
|
+
return start >= 0 ? start : (markdown.match(/<(?:!-?)?$/u)?.index ?? -1);
|
|
181
141
|
}
|
|
182
142
|
|
|
183
143
|
function parseTolerantTelegramAttributes(
|
|
@@ -263,7 +223,7 @@ function blankTelegramActionSpan(
|
|
|
263
223
|
* with every structured span blanked; implausible `[` starts are skipped without a parse attempt.
|
|
264
224
|
*/
|
|
265
225
|
function scanTelegramActionComment<T>(
|
|
266
|
-
comment:
|
|
226
|
+
comment: TelegramHtmlComment,
|
|
267
227
|
command: string,
|
|
268
228
|
parseStructured: (content: string, offset: number) => T | undefined,
|
|
269
229
|
): { payload: T } | { attributeEnvelope: string } | undefined {
|
|
@@ -287,7 +247,7 @@ function scanTelegramActionComment<T>(
|
|
|
287
247
|
}
|
|
288
248
|
|
|
289
249
|
function parseTelegramActionPayload(
|
|
290
|
-
comment:
|
|
250
|
+
comment: TelegramHtmlComment,
|
|
291
251
|
command: string,
|
|
292
252
|
): Record<string, unknown> | undefined {
|
|
293
253
|
const scanned = scanTelegramActionComment(
|
|
@@ -594,7 +554,7 @@ export function parseTelegramButtonPayloadRows(
|
|
|
594
554
|
}
|
|
595
555
|
|
|
596
556
|
export function parseTelegramActionPayloadRows(
|
|
597
|
-
comment:
|
|
557
|
+
comment: TelegramHtmlComment,
|
|
598
558
|
command: string,
|
|
599
559
|
): Record<string, unknown>[][] | undefined {
|
|
600
560
|
const scanned = scanTelegramActionComment(
|
|
@@ -665,7 +625,7 @@ export function stripTelegramCommentMarkupForPreview(markdown: string): string {
|
|
|
665
625
|
replaceTelegramButtonFences(markdown, () => ""),
|
|
666
626
|
);
|
|
667
627
|
const openBlockIndex =
|
|
668
|
-
|
|
628
|
+
findOpenOrPartialHtmlCommentIndex(withoutClosedBlocks);
|
|
669
629
|
const previewMarkdown =
|
|
670
630
|
openBlockIndex >= 0
|
|
671
631
|
? withoutClosedBlocks.slice(0, openBlockIndex)
|
|
@@ -678,7 +638,7 @@ export function stripTelegramCommentMarkupForDelivery(
|
|
|
678
638
|
): string {
|
|
679
639
|
const withoutClosedBlocks = stripTelegramHtmlCommentBlocks(markdown);
|
|
680
640
|
const openBlockIndex =
|
|
681
|
-
|
|
641
|
+
findOpenOrPartialHtmlCommentIndex(withoutClosedBlocks);
|
|
682
642
|
const deliveryMarkdown =
|
|
683
643
|
openBlockIndex >= 0
|
|
684
644
|
? withoutClosedBlocks.slice(0, openBlockIndex)
|
|
@@ -716,7 +676,7 @@ export function planTelegramVoiceReply(
|
|
|
716
676
|
const voiceReplies: TelegramVoiceReplyItem[] = [];
|
|
717
677
|
let lang: string | undefined;
|
|
718
678
|
let rate: string | undefined;
|
|
719
|
-
const stripped =
|
|
679
|
+
const stripped = replaceTelegramHtmlComments(markdown, (comment) => {
|
|
720
680
|
const command = "telegram_voice";
|
|
721
681
|
const normalizedContent = comment.content
|
|
722
682
|
.replace(/^\s+/, "")
|
|
@@ -849,11 +849,11 @@ export function createTelegramOutboundReplyPlanner(
|
|
|
849
849
|
...(options?.binding ? { binding: options.binding } : {}),
|
|
850
850
|
});
|
|
851
851
|
|
|
852
|
-
//
|
|
853
|
-
const voiceReply = planTelegramVoiceReply(
|
|
852
|
+
// Extract voice from the original text before button-fence rendering consumes it.
|
|
853
|
+
const voiceReply = planTelegramVoiceReply(markdown);
|
|
854
854
|
|
|
855
855
|
return {
|
|
856
|
-
markdown:
|
|
856
|
+
markdown: stripTelegramCommentMarkupForDelivery(buttonReply.markdown),
|
|
857
857
|
...(buttonReply.replyMarkup
|
|
858
858
|
? { replyMarkup: buttonReply.replyMarkup }
|
|
859
859
|
: {}),
|
|
@@ -34,7 +34,7 @@ export const TELEGRAM_MESSAGE_PROMPT_GUIDELINES = [
|
|
|
34
34
|
"For an explicitly requested channel post, pass its exact numeric id or public @username as chat_id; no local channel registry is required, and Telegram remains the authority on the bot's posting permission.",
|
|
35
35
|
"For an explicitly requested channel media post, pass one local .jpg/.jpeg/.png/.webp photo or .mp4 video as media; the text becomes its caption (max 1024 characters), and albums or other media types are rejected.",
|
|
36
36
|
"For a live Pi thread target, provide thread as its case-insensitive name or numeric id; the bridge sends visibly and admits one attributed turn to that live instance. Unknown, ambiguous, same, or offline targets fail before sending.",
|
|
37
|
-
"Add buttons by embedding the same
|
|
37
|
+
"Add buttons by embedding the same telegram_button HTML comments used in normal Telegram replies; Telegram does not support standalone buttons.",
|
|
38
38
|
"During an active Telegram turn, omit telegram_message for the current target and answer normally; use thread only when the user requests delivery to a different live Pi thread.",
|
|
39
39
|
] as const;
|
|
40
40
|
|
|
@@ -360,7 +360,7 @@ export interface TelegramTypingLoopStarterDeps<
|
|
|
360
360
|
getDefaultChatId: () => number | undefined;
|
|
361
361
|
sendTypingAction: (
|
|
362
362
|
chatId: number,
|
|
363
|
-
options?: { message_thread_id?: number; assertAuthority?: () => void },
|
|
363
|
+
options?: { message_thread_id?: number; assertAuthority?: () => void; isCancelled?: () => boolean },
|
|
364
364
|
) => Promise<unknown>;
|
|
365
365
|
updateStatus: (ctx: TContext, error?: string) => void;
|
|
366
366
|
isContextActive?: (ctx: TContext) => boolean;
|
|
@@ -454,6 +454,7 @@ export function createTelegramTypingLoopStarter<TContext>(
|
|
|
454
454
|
try {
|
|
455
455
|
await deps.sendTypingAction(targetChatId, {
|
|
456
456
|
...actionOptions,
|
|
457
|
+
isCancelled: () => !active,
|
|
457
458
|
// This stays process-local; adapters recheck it before HTTP/IPC issuance, not just loop ticks.
|
|
458
459
|
assertAuthority() {
|
|
459
460
|
if (!active || !canContinue()) throw new Error("Typing context authority changed.");
|
|
@@ -407,8 +407,8 @@ export interface TelegramApiCallOptions {
|
|
|
407
407
|
|
|
408
408
|
export class TelegramApiAuthorityError extends Error {
|
|
409
409
|
readonly requestIssued: boolean;
|
|
410
|
-
constructor(requestIssued: boolean) {
|
|
411
|
-
super("Telegram API call authority is unavailable.");
|
|
410
|
+
constructor(requestIssued: boolean, options?: ErrorOptions) {
|
|
411
|
+
super("Telegram API call authority is unavailable.", options);
|
|
412
412
|
this.name = "TelegramApiAuthorityError";
|
|
413
413
|
this.requestIssued = requestIssued;
|
|
414
414
|
}
|
|
@@ -418,11 +418,12 @@ export class TelegramApiAuthorityError extends Error {
|
|
|
418
418
|
export function assertTelegramApiCallAuthority(
|
|
419
419
|
assertAuthority: (() => void) | undefined,
|
|
420
420
|
requestIssued: boolean,
|
|
421
|
+
errorOptions?: ErrorOptions,
|
|
421
422
|
): void {
|
|
422
423
|
try {
|
|
423
424
|
assertAuthority?.();
|
|
424
425
|
} catch {
|
|
425
|
-
throw new TelegramApiAuthorityError(requestIssued);
|
|
426
|
+
throw new TelegramApiAuthorityError(requestIssued, errorOptions);
|
|
426
427
|
}
|
|
427
428
|
}
|
|
428
429
|
|
|
@@ -847,6 +848,8 @@ export interface TelegramBridgeApiRuntimeDeps {
|
|
|
847
848
|
/** Typing issuance guard is local authority, never Bot API body or IPC data. */
|
|
848
849
|
export interface TelegramTypingActionOptions extends Pick<TelegramApiCallOptions, "assertAuthority"> {
|
|
849
850
|
message_thread_id?: number;
|
|
851
|
+
/** Diagnostic observation only: an ended loop does not grant issuance, success or retry. */
|
|
852
|
+
isCancelled?: () => boolean;
|
|
850
853
|
}
|
|
851
854
|
|
|
852
855
|
export interface TelegramBridgeApiRuntime extends Pick<
|
|
@@ -1649,7 +1652,7 @@ async function callTelegramWithRetry<TResponse>(
|
|
|
1649
1652
|
} catch (error) {
|
|
1650
1653
|
// Authority refusal is never a transport failure, retry grant or acknowledged success.
|
|
1651
1654
|
if (error instanceof TelegramApiAuthorityError) throw error;
|
|
1652
|
-
assertTelegramApiCallAuthority(assertAuthority, requestIssued);
|
|
1655
|
+
assertTelegramApiCallAuthority(assertAuthority, requestIssued, { cause: error });
|
|
1653
1656
|
const retryable =
|
|
1654
1657
|
isRetryableTelegramApiError(error) &&
|
|
1655
1658
|
!(
|
|
@@ -2189,11 +2192,14 @@ export function createTelegramBridgeApiRuntime(
|
|
|
2189
2192
|
);
|
|
2190
2193
|
return true as TResponse;
|
|
2191
2194
|
}
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2195
|
+
const diagnosticError = error instanceof TelegramApiAuthorityError && Object.hasOwn(error, "cause")
|
|
2196
|
+
? error.cause : error;
|
|
2197
|
+
if (!options?.isExpectedError?.(error))
|
|
2198
|
+
deps.recordRuntimeEvent(
|
|
2199
|
+
"api",
|
|
2200
|
+
diagnosticError,
|
|
2201
|
+
withTelegramTransportDiagnostics(diagnosticError, { method }),
|
|
2202
|
+
);
|
|
2197
2203
|
throw error;
|
|
2198
2204
|
})
|
|
2199
2205
|
.finally(() => {
|
|
@@ -2311,14 +2317,21 @@ export function createTelegramBridgeApiRuntime(
|
|
|
2311
2317
|
: {}),
|
|
2312
2318
|
}),
|
|
2313
2319
|
sendTypingAction: createTelegramChatActionSender(
|
|
2314
|
-
(chatId, action, options) =>
|
|
2315
|
-
|
|
2320
|
+
(chatId, action, options) => {
|
|
2321
|
+
const isCancelled = options?.isCancelled;
|
|
2322
|
+
return callRecorded<boolean>("sendChatAction", {
|
|
2316
2323
|
chat_id: chatId,
|
|
2317
2324
|
action,
|
|
2318
2325
|
...(options?.message_thread_id !== undefined
|
|
2319
2326
|
? { message_thread_id: options.message_thread_id }
|
|
2320
2327
|
: {}),
|
|
2321
|
-
}, {
|
|
2328
|
+
}, {
|
|
2329
|
+
assertAuthority: options?.assertAuthority,
|
|
2330
|
+
...(isCancelled ? {
|
|
2331
|
+
isExpectedError: (error: unknown) => error instanceof TelegramApiAuthorityError && !Object.hasOwn(error, "cause") && isCancelled() === true,
|
|
2332
|
+
} : {}),
|
|
2333
|
+
});
|
|
2334
|
+
},
|
|
2322
2335
|
"typing",
|
|
2323
2336
|
),
|
|
2324
2337
|
sendRecordVoiceAction: createTelegramChatActionSender(
|