@llblab/pi-telegram 0.24.10 β†’ 0.24.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -124,8 +124,9 @@ The core product loop is mobile continuation: start or supervise work in the ter
124
124
  - Agent activity status is native-only for the release path: Telegram uses `sendChatAction(typing)` and renders its native `…typing` indicator as the automatic work signal. When a connected instance starts any real agent run, send that indicator to the instance's own thread (and aggregate `All` surface), including Telegram turns, local/TUI prompts, and autonomous continuations such as Grow Loop; stop it at agent/session completion. Keep at most one chat action in flight per loop, coalesce identical leader-side actions across follower traffic, and treat Telegram 429 `retry_after` as a shared suppression window rather than a delayed retry or persistent terminal error. This signal is independent from terminal `Active`, which remains scoped to Telegram-owned turns. Confirmed manual `/compact` and automatic compaction tied to an active Telegram turn also use native activity. Do not send activity merely for startup/connect/reload/recovery or add extra in-chat work surfaces without a new explicit design decision.
125
125
  - Voice reply mode Settings UI standard: the top-level Settings row is `πŸ‘„ Voice reply: hidden|mirror|always`; `hidden` is the true default, persists no `voice.replyMode`, adds no voice context, and still allows explicit agent-authored `telegram_voice`; `mirror` activates automatic voice context only for voice/audio input, and `always` activates it for every Telegram turn. The submenu title is `πŸ‘„ Voice reply mode:`; choice buttons use lowercase labels with a model-style active dot (`🟒 hidden`, `🟒 mirror`) rather than per-mode emoji; the explanatory submenu body uses compact HTML-code bullets such as `<code>-</code> <code>hidden</code> (default): ...`. Preserve this wording/icons unless the operator explicitly asks to redesign it
126
126
  - Outbound voice delivery is one fallback pipeline: configured `outboundHandlers` with `type: "voice"` run first in `telegram.json` order, then programmatic voice handlers, then registered voice synthesis providers as zero-config progressive fallbacks; provider extensions must not override operator-configured handlers
127
- - `telegram_voice` text is arbitrary TTS-target text and the prompt should show only forms that include text to speak: `<!-- telegram_voice text="Short summary" -->` or `<!-- telegram_voice: Short summary -->`. Avoid raw Markdown/code/tables in voice text.
128
- - `telegram_button` has three canonical forms: `<!-- telegram_button: OK -->` for label-only buttons, `<!-- telegram_button label=Continue prompt="Continue with the current plan." -->` for one-line prompts, or `<!-- telegram_button label="Show risks"\nList the main risks first.\n-->` for multiline prompts. The bridge preserves agent-authored label text and emoji, and owns post-selection state without changing that label: optional `selected_style="primary|success|danger"` selects blue, green, or red after queue admission, with `primary` as the default, and color never suppresses the prompt. Do not author JSON button specs, inline-after-text comments, standalone button tools, or comments inside code/quotes/lists/indented examples; write normal Markdown plus top-level hidden comments, and let the bridge supply the standard `β˜‘οΈ **Choose an option:**` Rich Markdown heading when buttons are the only output
127
+ - `telegram_voice` and `telegram_button` action comments have exactly two payload forms: a JSON object or double-quoted HTML-like attributes. An optional colon after the action name never selects or changes the payload format; the parser removes it before detecting `{...}` JSON or attributes. JSON is the universal form for long or escaped content, while attributes are the compact form. Keep each complete action in one top-level comment and encode JSON line breaks as `\n`; shorthand, body, paired-comment, unquoted-attribute, and single-quoted-attribute forms are invalid.
128
+ - `telegram_voice` accepts equivalent `text` or `value` keys, with explicit `text` taking precedence when both appear; optional `lang` and `rate` use the same payload object or attributes. Voice text is arbitrary TTS-target text; avoid raw Markdown, code, and tables.
129
+ - `telegram_button` accepts `label` plus `prompt`, or the compact `value` key when both strings are identical; explicitly supplied `label` or `prompt` takes precedence over the corresponding `value` fallback. Optional `selected_style="primary|success|danger"` selects blue, green, or red after queue admission, with `primary` as the default. The bridge preserves agent-authored label text and emoji, and color never suppresses the prompt. Keep comments out of code, quotes, lists, and indented examples; write normal Markdown plus top-level hidden comments, and let the bridge supply the standard `β˜‘οΈ **Choose an option:**` Rich Markdown heading when buttons are the only output
129
130
 
130
131
  ## 6. Engineering Conventions
131
132
 
package/CHANGELOG.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.24.11: Assistant Action Markup Hotfix
6
+
7
+ - `Assistant Action Markup`: Standardized `telegram_button` and `telegram_voice` on two model-facing payload forms: JSON objects and compact double-quoted attributes. The separator colon after either action name is optional and format-neutral, so mixed model output such as colon-prefixed attributes or colonless JSON parses identically. Buttons may use compact `value` when `label` and `prompt` are identical, while voice treats `text` and `value` as equivalent payload keys; explicit fields take precedence. Removed shorthand, body, paired-comment, unquoted-attribute, and single-quoted-attribute parsing so one complete comment owns each action. Impact: models use one shared action grammar without duplicating equal button text or navigating hidden legacy variants.
8
+ - `Composition Root Cooling`: Moved current-process bus identity defaults into `bus`, moved generated-button reply-markup edits into the direct/bus-aware Telegram API runtimes, and removed the always-true bus-configuration callback from Threaded Mode orchestration. Impact: `index.ts` loses low-level process/time/API payload work and dormant policy branching while retaining visible high-level port wiring.
9
+
5
10
  ## 0.24.10: Compact Thread Role Hotfix
6
11
 
7
12
  - `Status`: Threaded Mode status again appends only `@leader` or `@follower` to the execution status instead of repeating the visible Telegram tab name on a separate `Thread` row. Impact: compact `/status` output avoids redundant thread identity while retaining the transport role.
package/README.md CHANGED
@@ -179,11 +179,11 @@ Inbound files land under `<agent-dir>/tmp/telegram` and default to a 50 MiB limi
179
179
 
180
180
  ### Voice And Media
181
181
 
182
- Voice notes, audio, images, PDFs, and other media can pass through configured inbound handlers, programmatic handlers, or registered STT providers. Outbound voice can use configured `outboundHandlers` or registered TTS providers; `pi-telegram` owns reply policy and Telegram transport, while providers own synthesis.
182
+ Voice notes, audio, images, PDFs, and other media can pass through configured inbound handlers, programmatic handlers, or registered STT providers. Outbound voice can use configured `outboundHandlers` or registered TTS providers; `pi-telegram` owns reply policy and Telegram transport, while providers own synthesis. Explicit `telegram_voice` actions accept either a JSON object or compact double-quoted attributes, with equivalent `text` and `value` payload keys and an optional format-neutral colon after the action name.
183
183
 
184
184
  ### Buttons And Callbacks
185
185
 
186
- Assistant replies can include top-level hidden `telegram_button` comments. The bridge strips the comments from visible text, renders inline buttons, and routes callbacks back into Pi as queued prompts or extension-owned callback actions. Button-only replies receive the standard `β˜‘οΈ **Choose an option:**` heading as automatic visible fallback text. Once a generated prompt button is accepted, only that exact button switches to its optional `selected_style` (`primary` blue by default, `success` green, or `danger` red) without altering its agent-authored label or emoji; every style still queues the selected prompt.
186
+ Assistant replies can include top-level hidden `telegram_button` comments using either a JSON object or compact double-quoted attributes. Buttons use `label` plus `prompt`, or the compact `value` key when both are identical. The optional colon after the action name never changes format detection. The bridge strips the comments from visible text, renders inline buttons, and routes callbacks back into Pi as queued prompts or extension-owned callback actions. Button-only replies receive the standard `β˜‘οΈ **Choose an option:**` heading as automatic visible fallback text. Once a generated prompt button is accepted, only that exact button switches to its optional `selected_style` (`primary` blue by default, `success` green, or `danger` red) without altering its agent-authored label or emoji; every style still queues the selected prompt.
187
187
 
188
188
  ### Threaded Mode And Multi-Instance Bus
189
189
 
@@ -279,7 +279,7 @@ Final delivery attaches reply metadata only where requested. Reply parameters ap
279
279
 
280
280
  Outbound files staged during an active Telegram turn are delivered after that turn completes. They use `telegram_attach`, are checked atomically per tool call, and use configurable size limits before photo/document upload. When no Telegram turn is active, `telegram_attach` sends files immediately to the paired/default chat, an assigned follower thread, or an explicit `chat_id` plus optional `thread_id`; `telegram_message` provides direct local/TUI Markdown text delivery for explicit user requests and runs the same `telegram_button` markup planner so buttons attach to that text message. Direct local/TUI delivery is singleton-controlled: classic mode requires this Pi instance to own `/telegram-connect`, while Threaded Mode followers must be registered and route through the leader-owned transport. Already accepted active-turn reply/attachment delivery remains session-local.
281
281
 
282
- Assistant-authored final-message actions use hidden top-level comments:
282
+ Assistant-authored final-message actions use hidden top-level comments. Both actions accept a JSON object or double-quoted HTML-like attributes; an optional colon after the action name is format-neutral and stripped before payload detection:
283
283
 
284
284
  - `telegram_voice` creates voice reply artifacts through configured outbound handlers, programmatic voice handlers, or registered synthesis providers.
285
285
  - `telegram_button` creates inline buttons whose callbacks enqueue the configured prompt text as a normal Telegram prompt turn.
package/docs/outbound.md CHANGED
@@ -106,51 +106,44 @@ Synthesis providers receive the extracted `telegram_voice` text plus optional `l
106
106
 
107
107
  ## Voice Markup
108
108
 
109
- Assistant replies can include a hidden voice block:
109
+ Assistant replies can include hidden voice actions in either supported payload form:
110
110
 
111
111
  ```md
112
112
  Full text answer stays here.
113
113
 
114
- <!-- telegram_voice lang=ru rate=+30%
115
- Text to synthesize as a Telegram voice message.
116
- -->
114
+ <!-- telegram_voice: {"value":"Short spoken companion summary.","lang":"ru","rate":"+30%"} -->
117
115
 
118
- <!-- telegram_voice lang=ru rate=+30% text="Short spoken companion summary." -->
119
-
120
- <!-- telegram_voice: Short spoken companion summary. -->
116
+ <!-- telegram_voice text="Short spoken companion summary." lang="ru" rate="+30%" -->
121
117
  ```
122
118
 
123
- The bridge strips the comment from Telegram text. On `agent_end`, it maps each `telegram_voice` block to a provider call, generates one file per block, and sends each file as an independent Telegram-native voice message. The opening `<!-- telegram_voice` marker must start at column zero on a top-level line outside fenced code, quotes, and lists; otherwise it is rendered as literal Markdown. Body-form comments leave the opening line unclosed until the body-ending `-->`; closed heads can use `text="..."` for explicit one-line spoken text.
119
+ The bridge strips the comment from Telegram text. On `agent_end`, it maps each `telegram_voice` action to a provider call, generates one file per action, and sends each file as an independent Telegram-native voice message. Equivalent `text` or `value` supplies the spoken payload, with explicit `text` taking precedence when both appear; `lang` and `rate` are optional. The colon after `telegram_voice` is optional for both JSON and attributes and never changes format detection. Use JSON for long or escaped text and encode line breaks inside JSON strings as `\n`. The opening marker must start at column zero on a top-level line outside fenced code, quotes, lists, and indented examples; otherwise it remains literal Markdown.
124
120
 
125
121
  ## Buttons Markup
126
122
 
127
- Assistant replies can include independent button blocks. The prompt is sent back to Pi when the user taps the button; use the colon shorthand when the prompt should equal the label, `prompt="..."` for one-line prompts, or the body form for multiline prompts:
123
+ Assistant replies can include independent button actions in the same two payload forms:
128
124
 
129
125
  ```md
130
126
  I can continue.
131
127
 
132
- <!-- telegram_button label=Continue prompt="Continue with the current plan." -->
128
+ <!-- telegram_button: {"label":"Continue","prompt":"Continue with the current plan.","selected_style":"primary"} -->
133
129
 
134
- <!-- telegram_button label="Show risks"
135
- List the main risks first.
136
- -->
130
+ <!-- telegram_button label="Show risks" prompt="List the main risks first." selected_style="danger" -->
137
131
 
138
- <!-- telegram_button: Done -->
132
+ <!-- telegram_button: {"value":"Done"} -->
139
133
  ```
140
134
 
141
135
  Rules:
142
136
 
143
- - `telegram_button: Label` creates one independent label-only button row whose prompt equals the label.
144
- - `telegram_button label="Label" prompt="Prompt"` creates one independent button row whose prompt is the `prompt` attribute.
145
- - `telegram_button label="Label"` with a body creates one independent button row whose prompt is the block body.
146
- - The opening `<!-- telegram_button` marker must start at column zero on a top-level line outside fenced code, quotes, lists, and indented examples; otherwise it is literal Markdown.
147
- - Keep the canonical body form as `<!-- telegram_button label="Label"` + body + `-->`; closed heads must use `prompt="..."` or the colon shorthand to create a button.
148
- - Use one block per button; this mirrors HTML's singular element model and avoids a nested button DSL inside comments.
137
+ - JSON objects and double-quoted HTML-like attributes are the only accepted payload forms; shorthand, body, paired-comment, unquoted-attribute, and single-quoted-attribute forms are rejected.
138
+ - The colon after `telegram_button` is optional for both forms and never changes format detection.
139
+ - Use `label` plus `prompt`, or the compact `value` key when both strings are identical. Explicit `label` or `prompt` takes precedence over its `value` fallback. Use JSON with `\n` escapes for multiline prompts.
140
+ - The opening marker must start at column zero on a top-level line outside fenced code, quotes, lists, and indented examples; otherwise it remains literal Markdown.
141
+ - Use one comment per button; this mirrors HTML's singular element model and avoids a nested button DSL.
149
142
  - Button actions are stored in memory with short `callback_data`; Telegram never sees the full prompt in the button payload.
150
- - After Telegram accepts a generated button callback as a queued prompt, the bridge changes that exact button to its configured selection style without changing agent-authored text or emoji. Set `selected_style="primary"` (blue), `selected_style="success"` (green), or `selected_style="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.
143
+ - After Telegram accepts a generated button callback as a queued prompt, the bridge changes that exact button to its configured selection style without changing agent-authored text or emoji. Set `selected_style` to `primary` (blue), `success` (green), or `danger` (red); omitted or invalid values fall back to `primary`. The style never suppresses queue admission. Other choices stay visually unchanged and remain available; the callback acknowledgement remains the fallback on clients that do not render button styles.
151
144
  - 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.
152
145
 
153
- Do not emit JSON button specs, inline comments after visible text, standalone button actions, or 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)`.
146
+ Do not emit inline comments after visible text, standalone button actions, or 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)`.
154
147
 
155
148
  Buttons are built in and do not need a command template because they are pure Telegram reply markup plus callback routing.
156
149
 
@@ -162,8 +155,8 @@ The extension injects prompt guidance by context:
162
155
  - For ordinary local/TUI prompts, the agent only sees compact direct-delivery guidance: use `telegram_attach` or `telegram_message` when the user asks to send something to Telegram, and otherwise answer locally as normal.
163
156
  - For Telegram-originated turns, the prompt carries only minimal mobile/reply/file guidance; agents can call `telegram_help()` for full voice/button/direct-delivery/Threaded Mode/formatting/debug details.
164
157
  - For Telegram-originated turns, write the full technical answer as normal Markdown.
165
- - Add `telegram_voice` when a Telegram-native voice message is useful; use body text, `text="..."`, or colon shorthand for the text to synthesize. A companion summary is optional, no specific summary format is required.
166
- - Add `telegram_button: ...` when label equals prompt, `telegram_button label="..." prompt="..."` for one-line prompts, or `telegram_button label="..."` with a body for multiline prompts. Use optional `selected_style="success|danger|primary"` for post-admission color. A button-only reply may omit parent text because the bridge supplies `β˜‘οΈ **Choose an option:**` automatically.
158
+ - Add `telegram_voice` with either a JSON object or double-quoted attributes when a Telegram-native voice message is useful. A companion summary is optional, no specific summary format is required.
159
+ - Add `telegram_button` with either a JSON object or double-quoted attributes. Use `label` plus `prompt`, or `value` when they are identical; `selected_style` is optional. A button-only reply may omit parent text because the bridge supplies `β˜‘οΈ **Choose an option:**` automatically.
167
160
  - For ordinary Telegram-turn replies, do not call transport tools for voice or buttons; the bridge owns delivery, while registered voice synthesis providers own TTS and OGG/Opus conversion. For explicit local/TUI direct sends, `telegram_message` may include top-level `telegram_button` comments in its Markdown text because those buttons are attached to that text message.
168
161
  - 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.
169
162
 
@@ -64,8 +64,8 @@ This command surface is a mobile companion subset, not a raw terminal-command br
64
64
  - `telegram_attach(paths, chat_id?, thread_id?, caption?)` is the stable artifact delivery tool for generated files. During Telegram turns it queues files for the active reply; with `assistant.rendering: "rich"`, exactly one PNG/JPEG, MP4, or MP3 artifact plus non-empty final Markdown can become one reply-anchored Rich Message. HTML mode, multiple/unsupported files, Guest Mode, and voice outputs retain their established paths. Outside Telegram turns the tool sends files directly to the paired/default chat, the registered follower's assigned thread, or an explicit `chat_id` plus optional `thread_id` when this Pi instance owns `/telegram-connect` or is registered with the multi-instance bus.
65
65
  - `telegram_message(text, chat_id?, thread_id?)` sends a direct Telegram Markdown message from local/TUI-initiated work when this Pi instance owns `/telegram-connect` or is registered with the multi-instance bus. Top-level `telegram_button` comments inside `text` are parsed with the same planner used for normal replies and attached to that message; buttons are never standalone Telegram messages.
66
66
  - `telegram_help()` returns detailed agent-facing guidance for pi-telegram delivery actions, Threaded Mode, formatting, and debugging. The regular prompt only points agents at this tool instead of repeating the full guidance on every turn. `telegram_attach`, `telegram_message`, and `telegram_help` remain registered but are model-active only while this instance owns direct transport or holds a live follower registration; disconnect/loss suppresses their schemas and prompt metadata, and recovery restores only the operator's previously active pi-telegram subset.
67
- - `telegram_voice` hidden comments request Telegram-native voice delivery.
68
- - `telegram_button` hidden comments create inline buttons whose taps enqueue prompts. Use top-level column-zero comments outside code, quotes, lists, and indented examples; do not emit JSON button specs or standalone button actions.
67
+ - `telegram_voice` hidden comments request Telegram-native voice delivery through either a JSON object or double-quoted attributes. Equivalent `text` or `value` supplies the spoken payload; explicit `text` takes precedence.
68
+ - `telegram_button` hidden comments create inline buttons whose taps enqueue prompts through the same two payload forms. Use `label` plus `prompt`, or `value` when both strings are identical. The colon after either action name is optional and does not select the format. Use top-level column-zero comments outside code, quotes, lists, and indented examples; do not emit standalone button actions.
69
69
 
70
70
  Prompt guidance is context-aware: local/TUI prompts see only explicit direct-delivery guidance, while Telegram-originated turns receive the full action-comment syntax and phone-width output contract.
71
71
 
package/index.ts CHANGED
@@ -67,11 +67,8 @@ export default function (pi: Pi.ExtensionAPI) {
67
67
  const getActiveTelegramThreadProfile = function (): string | undefined {
68
68
  return configStore.getActiveProfileName();
69
69
  };
70
- const busProcessRuntime = Bus.createTelegramBusProcessRuntime({
70
+ const busProcessRuntime = Bus.createCurrentTelegramBusProcessRuntime({
71
71
  getActiveProfileName: getActiveTelegramThreadProfile,
72
- pid: process.pid,
73
- parentPid: process.ppid,
74
- createdAtMs: Date.now(),
75
72
  });
76
73
  const {
77
74
  instanceId: telegramInstanceId,
@@ -119,14 +116,8 @@ export default function (pi: Pi.ExtensionAPI) {
119
116
  const runtimeEvents = runtimeDiagnostics.events;
120
117
  const recordRuntimeEvent = runtimeDiagnostics.recordRuntimeEvent;
121
118
  const configStore = Config.createTelegramConfigStore({ recordRuntimeEvent });
122
- const isTelegramBusConfigured = function (): boolean {
123
- return true;
124
- };
125
119
  const isTelegramBusRuntimeEnabled = function (): boolean {
126
- return (
127
- isTelegramBusConfigured() &&
128
- !telegramThreadCapabilityState.isTopicModeUnavailable()
129
- );
120
+ return !telegramThreadCapabilityState.isTopicModeUnavailable();
130
121
  };
131
122
  Config.bindGlobalTelegramConfigRuntime(configStore);
132
123
  const configControls = Config.createTelegramConfigControls(configStore);
@@ -397,6 +388,7 @@ export default function (pi: Pi.ExtensionAPI) {
397
388
  sendRichMessageDraft,
398
389
  downloadFile: downloadTelegramBridgeFile,
399
390
  editMessageText: editTelegramMessageText,
391
+ editMessageReplyMarkup: editTelegramMessageReplyMarkup,
400
392
  answerCallbackQuery,
401
393
  answerGuestQuery,
402
394
  deleteMessage: deleteTelegramMessage,
@@ -741,13 +733,7 @@ export default function (pi: Pi.ExtensionAPI) {
741
733
  stopTypingLoop: typing.stop,
742
734
  answerCallbackQuery,
743
735
  editInteractiveMessage,
744
- async editMessageReplyMarkup(chatId, messageId, replyMarkup) {
745
- await callTelegramApi("editMessageReplyMarkup", {
746
- chat_id: chatId,
747
- message_id: messageId,
748
- reply_markup: replyMarkup,
749
- });
750
- },
736
+ editMessageReplyMarkup: editTelegramMessageReplyMarkup,
751
737
  sendInteractiveMessage,
752
738
  deleteMessage: deleteTelegramMessage,
753
739
  answerGuestQuery,
@@ -932,7 +918,6 @@ export default function (pi: Pi.ExtensionAPI) {
932
918
  getAllowedUserId: configStore.getAllowedUserId,
933
919
  callApi: callTelegramApi,
934
920
  topicTargetStore: threadStore,
935
- isBusConfigured: isTelegramBusConfigured,
936
921
  isBusRuntimeEnabled: isTelegramBusRuntimeEnabled,
937
922
  ownsLock: lockRuntime.owns,
938
923
  startClassicPolling: pollingRuntime.start,
package/lib/bus-api.ts CHANGED
@@ -265,6 +265,24 @@ export function createTelegramBusAwareApiRuntime(
265
265
  await deps.callFollowerApi("call", ["editMessageText", body]);
266
266
  return "edited";
267
267
  },
268
+ async editMessageReplyMarkup(
269
+ chatId: number,
270
+ messageId: number,
271
+ replyMarkup: unknown,
272
+ ): Promise<void> {
273
+ if (deps.ownsDirect()) {
274
+ await deps.directRuntime.editMessageReplyMarkup(
275
+ chatId,
276
+ messageId,
277
+ replyMarkup,
278
+ );
279
+ return;
280
+ }
281
+ await deps.callFollowerApi("call", [
282
+ "editMessageReplyMarkup",
283
+ { chat_id: chatId, message_id: messageId, reply_markup: replyMarkup },
284
+ ]);
285
+ },
268
286
  async answerCallbackQuery(
269
287
  callbackQueryId: string,
270
288
  text?: string,
package/lib/bus.ts CHANGED
@@ -72,6 +72,20 @@ export function getTelegramProcessBirthIdentity(
72
72
  return `${pid}:generation:${fallbackGeneration}`;
73
73
  }
74
74
 
75
+ export function createCurrentTelegramBusProcessRuntime(input: {
76
+ getActiveProfileName: () => string | undefined;
77
+ pid?: number;
78
+ parentPid?: number;
79
+ createdAtMs?: number;
80
+ }): TelegramBusProcessRuntime {
81
+ return createTelegramBusProcessRuntime({
82
+ getActiveProfileName: input.getActiveProfileName,
83
+ pid: input.pid ?? process.pid,
84
+ parentPid: input.parentPid ?? process.ppid,
85
+ createdAtMs: input.createdAtMs ?? Date.now(),
86
+ });
87
+ }
88
+
75
89
  export function createTelegramBusProcessRuntime(input: {
76
90
  getActiveProfileName: () => string | undefined;
77
91
  pid: number;
@@ -11,7 +11,7 @@ import type {
11
11
  TelegramInlineKeyboardMarkup,
12
12
  } from "./keyboard.ts";
13
13
  import {
14
- parseTelegramCommentAttributes,
14
+ parseTelegramActionPayload,
15
15
  parseTopLevelTelegramComment,
16
16
  replaceTopLevelHtmlComments,
17
17
  } from "./outbound-markup.ts";
@@ -87,16 +87,27 @@ function normalizeMarkdownAfterButtonExtraction(markdown: string): string {
87
87
  return markdown.replace(/\n{3,}/g, "\n\n").trim();
88
88
  }
89
89
 
90
- function parseButtonsCommentAttributes(input: string): {
91
- label?: string;
92
- prompt?: string;
93
- selectedStyle?: TelegramInlineKeyboardButtonStyle;
94
- } {
95
- const attributes = parseTelegramCommentAttributes(input);
96
- const selectedStyle = attributes.selected_style;
90
+ function getTelegramButtonString(
91
+ payload: Record<string, unknown>,
92
+ key: string,
93
+ ): string | undefined {
94
+ const value = payload[key];
95
+ if (typeof value !== "string") return undefined;
96
+ const trimmed = value.trim();
97
+ return trimmed || undefined;
98
+ }
99
+
100
+ function parseTelegramButtonAction(
101
+ payload: Record<string, unknown>,
102
+ ): TelegramOutboundButtonAction | undefined {
103
+ const value = getTelegramButtonString(payload, "value");
104
+ const label = getTelegramButtonString(payload, "label") ?? value;
105
+ const prompt = getTelegramButtonString(payload, "prompt") ?? value;
106
+ if (!label || !prompt) return undefined;
107
+ const selectedStyle = payload.selected_style;
97
108
  return {
98
- ...(attributes.label ? { label: attributes.label } : {}),
99
- ...(attributes.prompt ? { prompt: attributes.prompt } : {}),
109
+ text: label,
110
+ prompt,
100
111
  ...(selectedStyle === "success" ||
101
112
  selectedStyle === "danger" ||
102
113
  selectedStyle === "primary"
@@ -105,49 +116,6 @@ function parseButtonsCommentAttributes(input: string): {
105
116
  };
106
117
  }
107
118
 
108
- function parseButtonsCommentRows(
109
- head: string,
110
- body: string | undefined,
111
- ): TelegramOutboundButtonAction[][] {
112
- const trimmedHead = head.trim();
113
-
114
- if (body === undefined) {
115
- if (trimmedHead.startsWith(":")) {
116
- const label = trimmedHead.slice(1).trim();
117
- return label ? [[{ text: label, prompt: label }]] : [];
118
- }
119
- const attributes = parseButtonsCommentAttributes(head);
120
- return attributes.label && attributes.prompt
121
- ? [
122
- [
123
- {
124
- text: attributes.label,
125
- prompt: attributes.prompt,
126
- ...(attributes.selectedStyle
127
- ? { selectedStyle: attributes.selectedStyle }
128
- : {}),
129
- },
130
- ],
131
- ]
132
- : [];
133
- }
134
-
135
- const attributes = parseButtonsCommentAttributes(head);
136
- const prompt = body.trim();
137
- if (!attributes.label || !prompt) return [];
138
- return [
139
- [
140
- {
141
- text: attributes.label,
142
- prompt,
143
- ...(attributes.selectedStyle
144
- ? { selectedStyle: attributes.selectedStyle }
145
- : {}),
146
- },
147
- ],
148
- ];
149
- }
150
-
151
119
  export function createTelegramButtonActionStore(
152
120
  options: { ttlMs?: number } = {},
153
121
  ): TelegramButtonActionStore {
@@ -197,14 +165,15 @@ export function planTelegramButtonReply(
197
165
  const stripped = replaceTopLevelHtmlComments(markdown, (comment) => {
198
166
  const command = parseTopLevelTelegramComment(comment, "telegram_button");
199
167
  if (!command) return comment.raw;
200
- const rows = parseButtonsCommentRows(command.head, command.body);
201
- for (const row of rows) {
202
- keyboard.push(
203
- row.map((button) => ({
204
- text: button.text,
205
- callback_data: deps.registerAction(button),
206
- })),
207
- );
168
+ const payload = parseTelegramActionPayload(comment, "telegram_button");
169
+ const action = payload ? parseTelegramButtonAction(payload) : undefined;
170
+ if (action) {
171
+ keyboard.push([
172
+ {
173
+ text: action.text,
174
+ callback_data: deps.registerAction(action),
175
+ },
176
+ ]);
208
177
  }
209
178
  return "";
210
179
  });
@@ -16,16 +16,6 @@ interface TelegramTopLevelFenceState {
16
16
  length: number;
17
17
  }
18
18
 
19
- function isTelegramActionCommentContent(content: string): boolean {
20
- const normalizedContent = content.replace(/^\s+/, "");
21
- const [head = ""] = normalizedContent.split(/\r?\n/, 1);
22
- return ["telegram_voice", "telegram_button"].some((command) => {
23
- if (!head.startsWith(command)) return false;
24
- const nextChar = head[command.length];
25
- return nextChar === undefined || /\s|:/.test(nextChar);
26
- });
27
- }
28
-
29
19
  function getMarkdownLineEnd(markdown: string, offset: number): number {
30
20
  const newlineIndex = markdown.indexOf("\n", offset);
31
21
  return newlineIndex === -1 ? markdown.length : newlineIndex + 1;
@@ -64,58 +54,6 @@ function isTopLevelClosingFence(
64
54
  );
65
55
  }
66
56
 
67
- function collectPairedTelegramVoiceActionBody(
68
- markdown: string,
69
- bodyStart: number,
70
- commentContent: string,
71
- ): { content: string; end: number } | undefined {
72
- const normalizedContent = commentContent.trim();
73
- if (
74
- !normalizedContent.startsWith("telegram_voice") ||
75
- !isTelegramActionCommentContent(commentContent)
76
- ) {
77
- return undefined;
78
- }
79
- let offset = bodyStart;
80
- while (offset < markdown.length) {
81
- const lineEnd = getMarkdownLineEnd(markdown, offset);
82
- const line = getMarkdownLineText(markdown, offset, lineEnd);
83
- if (line === "<!-- /telegram_voice -->") {
84
- const body = markdown.slice(bodyStart, offset).trim();
85
- if (!body) return undefined;
86
- return {
87
- content: `${commentContent.trimEnd()}\n${body}`,
88
- end: lineEnd,
89
- };
90
- }
91
- if (line.startsWith("<!--")) return undefined;
92
- offset = lineEnd;
93
- }
94
- return undefined;
95
- }
96
-
97
- function collectInlineClosedTelegramActionBody(
98
- markdown: string,
99
- bodyStart: number,
100
- commentContent: string,
101
- ): { content: string; end: number } | undefined {
102
- const bodyLineEnd = getMarkdownLineEnd(markdown, bodyStart);
103
- const bodyLine = getMarkdownLineText(markdown, bodyStart, bodyLineEnd);
104
- const closeLineEnd = getMarkdownLineEnd(markdown, bodyLineEnd);
105
- const closeLine = getMarkdownLineText(markdown, bodyLineEnd, closeLineEnd);
106
- const hasRecoverableBody =
107
- isTelegramActionCommentContent(commentContent) &&
108
- bodyLine.trim() !== "" &&
109
- !bodyLine.startsWith("<!--") &&
110
- !bodyLine.startsWith("-->") &&
111
- closeLine === "-->";
112
- if (!hasRecoverableBody) return undefined;
113
- return {
114
- content: `${commentContent.trimEnd()}\n${bodyLine}`,
115
- end: bodyLineEnd + 3,
116
- };
117
- }
118
-
119
57
  export function collectTopLevelHtmlComments(markdown: string): {
120
58
  comments: TelegramTopLevelHtmlComment[];
121
59
  openCommentStart?: number;
@@ -140,27 +78,9 @@ export function collectTopLevelHtmlComments(markdown: string): {
140
78
  if (line.startsWith("<!--")) {
141
79
  const closeIndex = markdown.indexOf("-->", offset + 4);
142
80
  if (closeIndex === -1) return { comments, openCommentStart: offset };
143
- let end = closeIndex + 3;
144
- let raw = markdown.slice(offset, end);
145
- let content = raw.slice(4, -3);
146
- const closeColumn = closeIndex - offset;
147
- const closesOnOpeningLine = closeIndex < lineEnd;
148
- const hasOnlyWhitespaceAfterClose =
149
- line.slice(closeColumn + 3).trim() === "";
150
- const pairedVoiceBody =
151
- closesOnOpeningLine && hasOnlyWhitespaceAfterClose
152
- ? collectPairedTelegramVoiceActionBody(markdown, lineEnd, content)
153
- : undefined;
154
- const inlineBody =
155
- !pairedVoiceBody && closesOnOpeningLine && hasOnlyWhitespaceAfterClose
156
- ? collectInlineClosedTelegramActionBody(markdown, lineEnd, content)
157
- : undefined;
158
- const recoveredBody = pairedVoiceBody ?? inlineBody;
159
- if (recoveredBody) {
160
- end = recoveredBody.end;
161
- raw = markdown.slice(offset, end);
162
- content = recoveredBody.content;
163
- }
81
+ const end = closeIndex + 3;
82
+ const raw = markdown.slice(offset, end);
83
+ const content = raw.slice(4, -3);
164
84
  comments.push({ raw, content, start: offset, end });
165
85
  offset = getMarkdownLineEnd(markdown, end);
166
86
  continue;
@@ -233,18 +153,47 @@ export function parseTopLevelTelegramComment(
233
153
  };
234
154
  }
235
155
 
236
- export function parseTelegramCommentAttributes(
237
- input: string,
238
- ): Record<string, string> {
156
+ function parseCanonicalTelegramActionAttributes(
157
+ source: string,
158
+ ): Record<string, string> | undefined {
239
159
  const attributes: Record<string, string> = {};
240
- for (const match of input.matchAll(
241
- /([A-Za-z_][A-Za-z0-9_-]*)=(?:"([^"]*)"|'([^']*)'|(\S+))/g,
242
- )) {
243
- const key = match[1];
244
- const value = (match[2] ?? match[3] ?? match[4] ?? "").trim();
245
- if (value) attributes[key] = value;
160
+ const pattern = /\s*([A-Za-z_][A-Za-z0-9_-]*)="([^"]*)"/y;
161
+ let offset = 0;
162
+ while (offset < source.length) {
163
+ pattern.lastIndex = offset;
164
+ const match = pattern.exec(source);
165
+ if (!match) return undefined;
166
+ const value = match[2].trim();
167
+ if (value) attributes[match[1]] = value;
168
+ offset = pattern.lastIndex;
169
+ }
170
+ return Object.keys(attributes).length > 0 ? attributes : undefined;
171
+ }
172
+
173
+ export function parseTelegramActionPayload(
174
+ comment: TelegramTopLevelHtmlComment,
175
+ command: string,
176
+ ): Record<string, unknown> | undefined {
177
+ const parsed = parseTopLevelTelegramComment(comment, command);
178
+ if (!parsed) return undefined;
179
+ const source = [parsed.head, parsed.body]
180
+ .filter((part): part is string => part !== undefined)
181
+ .join("\n")
182
+ .trim()
183
+ .replace(/^:\s*/, "");
184
+ if (!source) return undefined;
185
+ if (source.startsWith("{")) {
186
+ try {
187
+ const value: unknown = JSON.parse(source);
188
+ return value !== null && typeof value === "object" && !Array.isArray(value)
189
+ ? (value as Record<string, unknown>)
190
+ : undefined;
191
+ } catch {
192
+ return undefined;
193
+ }
246
194
  }
247
- return attributes;
195
+ if (parsed.body !== undefined) return undefined;
196
+ return parseCanonicalTelegramActionAttributes(source);
248
197
  }
249
198
 
250
199
  export function normalizeMarkdownAfterVoiceExtraction(
@@ -295,58 +244,14 @@ export interface TelegramVoiceReplyPlan {
295
244
  rate?: string;
296
245
  }
297
246
 
298
- function parseVoiceReplyAttributes(input: string): {
299
- lang?: string;
300
- rate?: string;
301
- text?: string;
302
- } {
303
- const attributes = parseTelegramCommentAttributes(input);
304
- return {
305
- ...(attributes.lang ? { lang: attributes.lang } : {}),
306
- ...(attributes.rate ? { rate: attributes.rate } : {}),
307
- ...(attributes.text ? { text: attributes.text } : {}),
308
- };
309
- }
310
-
311
- function parseVoiceCommentBody(
312
- head: string,
313
- body: string | undefined,
314
- ): {
315
- attrs: string;
316
- text: string;
317
- } {
318
- const trimmedHead = head.trim();
319
- if (body !== undefined) {
320
- return { attrs: trimmedHead.replace(/^:/, "").trim(), text: body.trim() };
321
- }
322
- let colonIndex = -1;
323
- let inQuote = false;
324
- let quoteChar = "";
325
- for (let i = 0; i < trimmedHead.length; i++) {
326
- const char = trimmedHead[i];
327
- if (inQuote) {
328
- if (char === quoteChar) inQuote = false;
329
- } else {
330
- if (char === '"' || char === "'") {
331
- inQuote = true;
332
- quoteChar = char;
333
- } else if (char === ":") {
334
- colonIndex = i;
335
- break;
336
- }
337
- }
338
- }
339
- if (colonIndex > 0) {
340
- const attrsPart = trimmedHead.slice(0, colonIndex).trim();
341
- const textPart = trimmedHead.slice(colonIndex + 1).trim();
342
- const attrs = parseVoiceReplyAttributes(attrsPart);
343
- return { attrs: attrsPart, text: textPart || attrs.text || "", ...attrs };
344
- }
345
- if (trimmedHead.startsWith(":")) {
346
- return { attrs: "", text: trimmedHead.slice(1).trim() };
347
- }
348
- const attrs = parseVoiceReplyAttributes(trimmedHead);
349
- return { attrs: trimmedHead, text: attrs.text ?? "" };
247
+ function getTelegramActionString(
248
+ payload: Record<string, unknown>,
249
+ key: string,
250
+ ): string | undefined {
251
+ const value = payload[key];
252
+ if (typeof value !== "string") return undefined;
253
+ const trimmed = value.trim();
254
+ return trimmed || undefined;
350
255
  }
351
256
 
352
257
  export function planTelegramVoiceReply(
@@ -356,26 +261,24 @@ export function planTelegramVoiceReply(
356
261
  let lang: string | undefined;
357
262
  let rate: string | undefined;
358
263
  const stripped = replaceTopLevelHtmlComments(markdown, (comment) => {
359
- let command = parseTopLevelTelegramComment(comment, "telegram_voice");
360
- if (!command) {
361
- let content = comment.content.replace(/^\s+/, "").replace(/^!/, "");
362
- if (content.startsWith("telegram_voice")) {
363
- const headPart = content.slice("telegram_voice".length).trim();
364
- command = { head: headPart, body: undefined };
365
- }
366
- }
367
- if (!command) return "";
368
- const parsed = parseVoiceCommentBody(command.head, command.body);
369
- const attrs = parseVoiceReplyAttributes(parsed.attrs);
370
- if (parsed.text) {
264
+ const command = parseTopLevelTelegramComment(comment, "telegram_voice");
265
+ if (!command) return comment.raw;
266
+ const payload = parseTelegramActionPayload(comment, "telegram_voice");
267
+ if (!payload) return "";
268
+ const text =
269
+ getTelegramActionString(payload, "text") ??
270
+ getTelegramActionString(payload, "value");
271
+ const itemLang = getTelegramActionString(payload, "lang");
272
+ const itemRate = getTelegramActionString(payload, "rate");
273
+ if (text) {
371
274
  voiceReplies.push({
372
- text: parsed.text,
373
- ...(attrs.lang ? { lang: attrs.lang } : {}),
374
- ...(attrs.rate ? { rate: attrs.rate } : {}),
275
+ text,
276
+ ...(itemLang ? { lang: itemLang } : {}),
277
+ ...(itemRate ? { rate: itemRate } : {}),
375
278
  });
376
279
  }
377
- if (attrs.lang) lang = attrs.lang;
378
- if (attrs.rate) rate = attrs.rate;
280
+ if (itemLang) lang = itemLang;
281
+ if (itemRate) rate = itemRate;
379
282
  return "";
380
283
  });
381
284
  const voiceText = voiceReplies
package/lib/polling.ts CHANGED
@@ -319,7 +319,6 @@ export interface TelegramThreadCapabilityRuntimeDeps<
319
319
  TContext,
320
320
  > extends TelegramThreadCapabilityReaderDeps {
321
321
  topicTargetStore: TelegramThreadCapabilityStore;
322
- isBusConfigured: () => boolean;
323
322
  ownsLock: (ctx: TContext) => boolean;
324
323
  getPollingStartedWithTelegramBus: () => boolean;
325
324
  setPollingStartedWithTelegramBus: (started: boolean) => void;
@@ -394,7 +393,6 @@ export interface TelegramThreadAwarePollingDeps<
394
393
  TContext,
395
394
  TOwner,
396
395
  > extends TelegramStartupThreadCapabilityProbeDeps {
397
- isBusConfigured: () => boolean;
398
396
  isBusRuntimeEnabled: () => boolean;
399
397
  isTopicModeUnavailableError: (error: unknown) => boolean;
400
398
  getPollingStartedWithTelegramBus: () => boolean;
@@ -419,7 +417,6 @@ export interface TelegramThreadCapabilityOrchestrationDeps<
419
417
  > extends TelegramThreadCapabilityReaderDeps {
420
418
  state: TelegramThreadCapabilityStateRuntime;
421
419
  topicTargetStore: TelegramThreadCapabilityStore;
422
- isBusConfigured: () => boolean;
423
420
  isBusRuntimeEnabled: () => boolean;
424
421
  ownsLock: (ctx: TContext) => boolean;
425
422
  startClassicPolling: (ctx: TContext) => MaybePromise<void>;
@@ -475,7 +472,6 @@ export function createTelegramThreadCapabilityOrchestration<TContext, TOwner>(
475
472
  getAllowedUserId: deps.getAllowedUserId,
476
473
  callApi: deps.callApi,
477
474
  topicTargetStore: deps.topicTargetStore,
478
- isBusConfigured: deps.isBusConfigured,
479
475
  ownsLock: deps.ownsLock,
480
476
  getPollingStartedWithTelegramBus: deps.state.isBusPollingStarted,
481
477
  setPollingStartedWithTelegramBus: deps.state.setBusPollingStarted,
@@ -498,7 +494,6 @@ export function createTelegramThreadCapabilityOrchestration<TContext, TOwner>(
498
494
  getAllowedUserId: deps.getAllowedUserId,
499
495
  callApi: deps.callApi,
500
496
  topicTargetStore: deps.topicTargetStore,
501
- isBusConfigured: deps.isBusConfigured,
502
497
  isBusRuntimeEnabled: deps.isBusRuntimeEnabled,
503
498
  isTopicModeUnavailableError: deps.isTopicModeUnavailableError,
504
499
  getPollingStartedWithTelegramBus: deps.state.isBusPollingStarted,
@@ -590,7 +585,6 @@ export async function applyTelegramThreadCapability<TContext>(
590
585
  deps: TelegramThreadCapabilityRuntimeDeps<TContext>,
591
586
  ): Promise<void> {
592
587
  await deps.topicTargetStore.load();
593
- if (!deps.isBusConfigured()) return;
594
588
  const nowMs = (deps.getNowMs ?? Date.now)();
595
589
  const previousBotState = deps.topicTargetStore.getBotState();
596
590
  if (!threadModeEnabled) {
@@ -691,17 +685,14 @@ export function createTelegramThreadAwarePollingPorts<TContext, TOwner>(
691
685
  ctx: TContext,
692
686
  options?: { forceFreshLeaderThread?: boolean },
693
687
  ): Promise<void> => {
694
- if (deps.isBusConfigured()) {
695
- await deps.topicTargetStore.load();
696
- let startupThreadCapability: boolean | undefined;
697
- try {
698
- startupThreadCapability =
699
- await probeTelegramStartupThreadCapability(deps);
700
- } catch (error) {
701
- deps.recordEvent("bus", error, { phase: "startup-thread-mode-probe" });
702
- }
703
- deps.setTopicModeUnavailable(startupThreadCapability !== true);
688
+ await deps.topicTargetStore.load();
689
+ let startupThreadCapability: boolean | undefined;
690
+ try {
691
+ startupThreadCapability = await probeTelegramStartupThreadCapability(deps);
692
+ } catch (error) {
693
+ deps.recordEvent("bus", error, { phase: "startup-thread-mode-probe" });
704
694
  }
695
+ deps.setTopicModeUnavailable(startupThreadCapability !== true);
705
696
  if (deps.isBusRuntimeEnabled()) {
706
697
  deps.setTopicModeUnavailable(false);
707
698
  try {
@@ -886,7 +877,6 @@ export function createTelegramThreadCapabilityMonitor<TContext>(
886
877
  return {
887
878
  start(ctx) {
888
879
  stop();
889
- if (!deps.isBusConfigured()) return;
890
880
  interval = setInterval(() => {
891
881
  check(ctx);
892
882
  }, intervalMs);
package/lib/prompts.ts CHANGED
@@ -138,13 +138,13 @@ How to answer Telegram turns:
138
138
  Assistant-authored Telegram actions:
139
139
  - \`telegram_voice\` and \`telegram_button\` are hidden top-level HTML comments, not Pi tools.
140
140
  - Put action comments at column zero, outside code, quotes, lists, and indented examples.
141
- - Voice forms: \`<!-- telegram_voice text="Short summary" -->\`, \`<!-- telegram_voice: Short summary -->\`, multiline \`<!-- telegram_voice lang=ru\nShort summary.\n-->\`, or paired \`<!-- telegram_voice lang=ru -->\nShort summary.\n<!-- /telegram_voice -->\`.
142
- - Keep the complete action at top level and include a non-empty voice payload.
141
+ - Action payloads use either JSON or double-quoted attributes; the colon after the action name is optional and does not affect parsing. Voice accepts equivalent \`text\` or \`value\`: \`<!-- telegram_voice: {"value":"Short summary","lang":"en"} -->\` or \`<!-- telegram_voice text="Short summary" lang="en" -->\`.
142
+ - Keep the complete action in one top-level comment and include non-empty \`text\` or \`value\`; encode line breaks inside JSON strings as \`\\n\`.
143
143
  - Keep voice text TTS-friendly; avoid raw Markdown, code, and tables in voice text.
144
144
  - Voice delivery generates and attaches OGG automatically; do not also call \`telegram_attach\` for the same audio.
145
145
  - Voice reply modes are compact: \`hidden\` emits no automatic context, \`mirror\` emits it for voice/audio input, and \`always\` emits it for every Telegram turn. Explicit \`telegram_voice\` remains available for an intentionally distinct spoken payload.
146
- - Button forms: \`<!-- telegram_button: OK -->\`, \`<!-- telegram_button label=Continue prompt="Continue with the current plan." -->\`, or multiline \`<!-- telegram_button label="Show risks" selected_style="danger"\nList the main risks first.\n-->\`.
147
- - Optional \`selected_style\` controls the button after queue admission: \`primary\` (default, blue), \`success\` (green), or \`danger\` (red). It never suppresses the prompt.
146
+ - Button payloads use \`label\` plus \`prompt\`, or compact \`value\` when both are identical: \`<!-- telegram_button: {"label":"Continue","prompt":"Continue with the current plan."} -->\` or \`<!-- telegram_button value="Continue" -->\`.
147
+ - Optional \`selected_style\` in either payload form controls the button after queue admission: \`primary\` (default, blue), \`success\` (green), or \`danger\` (red). It never suppresses the prompt.
148
148
  - If hidden button comments form the whole reply, the bridge supplies the standard \`β˜‘οΈ **Choose an option:**\` heading automatically.
149
149
 
150
150
  Local/TUI direct delivery:
@@ -497,6 +497,11 @@ export interface TelegramBridgeApiRuntime {
497
497
  editMessageText: (
498
498
  body: TelegramEditMessageTextBody,
499
499
  ) => Promise<"edited" | "unchanged">;
500
+ editMessageReplyMarkup: (
501
+ chatId: number,
502
+ messageId: number,
503
+ replyMarkup: unknown,
504
+ ) => Promise<void>;
500
505
  answerCallbackQuery: (
501
506
  callbackQueryId: string,
502
507
  text?: string,
@@ -1569,6 +1574,13 @@ export function createTelegramBridgeApiRuntime(
1569
1574
  throw error;
1570
1575
  }
1571
1576
  },
1577
+ editMessageReplyMarkup: async (chatId, messageId, replyMarkup) => {
1578
+ await callRecorded("editMessageReplyMarkup", {
1579
+ chat_id: chatId,
1580
+ message_id: messageId,
1581
+ reply_markup: replyMarkup,
1582
+ });
1583
+ },
1572
1584
  answerCallbackQuery: async (callbackQueryId, text) => {
1573
1585
  try {
1574
1586
  await deps.client.answerCallbackQuery(callbackQueryId, text);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.24.10",
3
+ "version": "0.24.11",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"