@llblab/pi-kit 0.1.13 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/AGENTS.md +1 -0
  2. package/CHANGELOG.md +10 -0
  3. package/README.md +2 -2
  4. package/node_modules/@llblab/pi-actors/AGENTS.md +5 -3
  5. package/node_modules/@llblab/pi-actors/BACKLOG.md +1 -1
  6. package/node_modules/@llblab/pi-actors/CHANGELOG.md +15 -0
  7. package/node_modules/@llblab/pi-actors/README.md +5 -3
  8. package/node_modules/@llblab/pi-actors/dist/index.js +4 -1
  9. package/node_modules/@llblab/pi-actors/dist/lib/async-runs.d.ts +2 -2
  10. package/node_modules/@llblab/pi-actors/dist/lib/async-runs.js +42 -19
  11. package/node_modules/@llblab/pi-actors/dist/lib/extension-runtime.d.ts +2 -1
  12. package/node_modules/@llblab/pi-actors/dist/lib/extension-runtime.js +7 -2
  13. package/node_modules/@llblab/pi-actors/dist/lib/limits.d.ts +9 -0
  14. package/node_modules/@llblab/pi-actors/dist/lib/limits.js +9 -0
  15. package/node_modules/@llblab/pi-actors/dist/lib/observability.d.ts +10 -11
  16. package/node_modules/@llblab/pi-actors/dist/lib/observability.js +81 -56
  17. package/node_modules/@llblab/pi-actors/dist/lib/pi.d.ts +31 -0
  18. package/node_modules/@llblab/pi-actors/dist/lib/pi.js +180 -0
  19. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery.d.ts +115 -0
  20. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery.js +623 -0
  21. package/node_modules/@llblab/pi-actors/dist/lib/run-ui-runtime.d.ts +3 -0
  22. package/node_modules/@llblab/pi-actors/dist/lib/run-ui-runtime.js +341 -13
  23. package/node_modules/@llblab/pi-actors/dist/lib/runs-trace.d.ts +1 -1
  24. package/node_modules/@llblab/pi-actors/dist/lib/runs-trace.js +5 -3
  25. package/node_modules/@llblab/pi-actors/dist/lib/session-evidence.d.ts +16 -0
  26. package/node_modules/@llblab/pi-actors/dist/lib/session-evidence.js +143 -0
  27. package/node_modules/@llblab/pi-actors/dist/lib/temp.js +1 -1
  28. package/node_modules/@llblab/pi-actors/dist/lib/tools-inspect.js +3 -1
  29. package/node_modules/@llblab/pi-actors/dist/scripts/async-runner.mjs +5 -19
  30. package/node_modules/@llblab/pi-actors/dist/skills/actors/SKILL.md +2 -2
  31. package/node_modules/@llblab/pi-actors/dist/skills/actors/references/runs.md +1 -1
  32. package/node_modules/@llblab/pi-actors/dist/skills/swarm/SKILL.md +1 -1
  33. package/node_modules/@llblab/pi-actors/docs/README.md +1 -0
  34. package/node_modules/@llblab/pi-actors/docs/async-runs.md +8 -4
  35. package/node_modules/@llblab/pi-actors/docs/coordinator-delivery.md +207 -0
  36. package/node_modules/@llblab/pi-actors/index.ts +4 -1
  37. package/node_modules/@llblab/pi-actors/lib/async-runs.ts +42 -21
  38. package/node_modules/@llblab/pi-actors/lib/extension-runtime.ts +8 -3
  39. package/node_modules/@llblab/pi-actors/lib/limits.ts +9 -0
  40. package/node_modules/@llblab/pi-actors/lib/observability.ts +97 -78
  41. package/node_modules/@llblab/pi-actors/lib/pi.ts +210 -0
  42. package/node_modules/@llblab/pi-actors/lib/run-delivery.ts +800 -0
  43. package/node_modules/@llblab/pi-actors/lib/run-ui-runtime.ts +370 -18
  44. package/node_modules/@llblab/pi-actors/lib/runs-trace.ts +6 -4
  45. package/node_modules/@llblab/pi-actors/lib/session-evidence.ts +153 -0
  46. package/node_modules/@llblab/pi-actors/lib/temp.ts +1 -1
  47. package/node_modules/@llblab/pi-actors/lib/tools-inspect.ts +4 -1
  48. package/node_modules/@llblab/pi-actors/package.json +3 -3
  49. package/node_modules/@llblab/pi-actors/scripts/async-runner.mjs +5 -19
  50. package/node_modules/@llblab/pi-actors/skills/actors/SKILL.md +2 -2
  51. package/node_modules/@llblab/pi-actors/skills/actors/references/runs.md +1 -1
  52. package/node_modules/@llblab/pi-actors/skills/swarm/SKILL.md +1 -1
  53. package/node_modules/@llblab/pi-telegram/AGENTS.md +4 -4
  54. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +25 -0
  55. package/node_modules/@llblab/pi-telegram/README.md +4 -4
  56. package/node_modules/@llblab/pi-telegram/docs/activity.md +4 -4
  57. package/node_modules/@llblab/pi-telegram/docs/architecture.md +10 -10
  58. package/node_modules/@llblab/pi-telegram/docs/compact-matrix-literal.md +16 -14
  59. package/node_modules/@llblab/pi-telegram/docs/inbound.md +1 -1
  60. package/node_modules/@llblab/pi-telegram/docs/outbound.md +13 -22
  61. package/node_modules/@llblab/pi-telegram/docs/public-api.md +6 -7
  62. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +4 -5
  63. package/node_modules/@llblab/pi-telegram/docs/voice.md +12 -2
  64. package/node_modules/@llblab/pi-telegram/index.ts +0 -1
  65. package/node_modules/@llblab/pi-telegram/lib/activity.ts +10 -5
  66. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +16 -17
  67. package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +1 -1
  68. package/node_modules/@llblab/pi-telegram/lib/commands.ts +16 -6
  69. package/node_modules/@llblab/pi-telegram/lib/config.ts +25 -25
  70. package/node_modules/@llblab/pi-telegram/lib/media.ts +12 -2
  71. package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +10 -73
  72. package/node_modules/@llblab/pi-telegram/lib/outbound-buttons.ts +9 -8
  73. package/node_modules/@llblab/pi-telegram/lib/outbound-markup.ts +212 -82
  74. package/node_modules/@llblab/pi-telegram/lib/routing.ts +78 -36
  75. package/node_modules/@llblab/pi-telegram/lib/status.ts +23 -4
  76. package/node_modules/@llblab/pi-telegram/lib/turns.ts +10 -1
  77. package/node_modules/@llblab/pi-telegram/lib/updates.ts +1 -1
  78. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  79. package/node_modules/@llblab/pi-telegram/skills/generated-control-surface/SKILL.md +61 -205
  80. package/node_modules/@llblab/pi-telegram/skills/generated-control-surface/references/capability-adapters.md +27 -0
  81. package/node_modules/@llblab/pi-telegram/skills/generated-control-surface/references/layout-and-state.md +35 -0
  82. package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/SKILL.md +70 -110
  83. package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/configuration.md +15 -0
  84. package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/delivery-and-threads.md +27 -0
  85. package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/diagnosis.md +14 -0
  86. package/package.json +3 -3
@@ -21,7 +21,6 @@ import type { TelegramVoiceReplyMode } from "./voice.ts";
21
21
  export type TelegramSettingsMenuReplyMarkup = TelegramInlineKeyboardMarkup;
22
22
 
23
23
  export interface TelegramSettingsStateDeps {
24
- isProactivePushEnabled: () => boolean;
25
24
  areDraftPreviewsEnabled: () => boolean;
26
25
  getAssistantRenderingMode: () => TelegramAssistantRenderingMode;
27
26
  getActivityVerbosity: () => TelegramActivityVerbosity;
@@ -32,7 +31,6 @@ export interface TelegramSettingsStateDeps {
32
31
  }
33
32
 
34
33
  export interface TelegramSettingsMutationDeps extends TelegramSettingsStateDeps {
35
- setProactivePushEnabled: (enabled: boolean) => Promise<void>;
36
34
  setDraftPreviewsEnabled: (enabled: boolean) => Promise<void>;
37
35
  setAssistantRenderingMode: (
38
36
  mode: TelegramAssistantRenderingMode,
@@ -139,7 +137,6 @@ export interface TelegramSettingsMenuRuntimeDeps<
139
137
  export const SETTINGS_MENU_TITLE = "<b>⚙️ Settings:</b>";
140
138
  export const AUTOMATIC_THREAD_CLEANUP_SETTINGS_TITLE =
141
139
  "<b>🧹 Thread cleanup:</b>";
142
- export const PROACTIVE_PUSH_SETTINGS_TITLE = "<b>📌 Proactive push:</b>";
143
140
  export const DRAFT_PREVIEWS_SETTINGS_TITLE = "<b>📝 Draft previews:</b>";
144
141
  export const ASSISTANT_RENDERING_SETTINGS_TITLE =
145
142
  "<b>🧾 Assistant rendering:</b>";
@@ -181,19 +178,6 @@ export function buildAutomaticThreadCleanupSettingsText(
181
178
  ].join("\n");
182
179
  }
183
180
 
184
- export function buildProactivePushSettingsText(
185
- proactivePushEnabled: boolean,
186
- ): string {
187
- return [
188
- `${PROACTIVE_PUSH_SETTINGS_TITLE} <code>${proactivePushEnabled ? "on" : "off"}</code>`,
189
- "",
190
- "Control whether public assistant output from local/autonomous work is projected to Telegram.",
191
- "",
192
- "<code>-</code> <code>on</code> (default): send each completed public block, including visible checkpoints and the final answer, while connected.",
193
- "<code>-</code> <code>off</code>: keep local/autonomous assistant blocks in Pi; Telegram-originated replies still use their normal delivery path.",
194
- ].join("\n");
195
- }
196
-
197
181
  export function buildDraftPreviewsSettingsText(enabled: boolean): string {
198
182
  return [
199
183
  `${DRAFT_PREVIEWS_SETTINGS_TITLE} <code>${enabled ? "on" : "off"}</code>`,
@@ -265,7 +249,6 @@ export function buildTimeInjectionModeSettingsText(
265
249
  }
266
250
 
267
251
  export function buildTelegramSettingsMenuReplyMarkup(
268
- proactivePushEnabled: boolean,
269
252
  draftPreviewsEnabled: boolean,
270
253
  assistantRenderingModeOrVoiceReplyMode:
271
254
  TelegramAssistantRenderingMode | TelegramVoiceReplyMode,
@@ -325,10 +308,6 @@ export function buildTelegramSettingsMenuReplyMarkup(
325
308
  text: `🔬 Activity: ${activityVerbosity}`,
326
309
  callback_data: "settings:open:activity-verbosity",
327
310
  },
328
- {
329
- text: `📌 Proactive push: ${proactivePushEnabled ? "on" : "off"}`,
330
- callback_data: "settings:open:proactive",
331
- },
332
311
  {
333
312
  text: `🕒 Time injection: ${getTelegramSettingsStateValueLabel(timeInjectionMode)}`,
334
313
  callback_data: "settings:open:time-injection",
@@ -362,7 +341,6 @@ export async function openTelegramSettingsMenu<
362
341
  state,
363
342
  buildTelegramSettingsMenuText(),
364
343
  buildTelegramSettingsMenuReplyMarkup(
365
- deps.isProactivePushEnabled(),
366
344
  deps.areDraftPreviewsEnabled(),
367
345
  deps.getAssistantRenderingMode(),
368
346
  deps.getVoiceReplyMode(),
@@ -399,26 +377,6 @@ export function buildAutomaticThreadCleanupSettingsReplyMarkup(
399
377
  };
400
378
  }
401
379
 
402
- export function buildProactivePushSettingsReplyMarkup(
403
- proactivePushEnabled: boolean,
404
- ): TelegramSettingsMenuReplyMarkup {
405
- return {
406
- inline_keyboard: [
407
- [{ text: "⬆️ Back", callback_data: "settings:list" }],
408
- [
409
- {
410
- text: proactivePushEnabled ? "🟢 On" : "⚫️ On",
411
- callback_data: "settings:set:proactive:on",
412
- },
413
- {
414
- text: proactivePushEnabled ? "⚫️ Off" : "🟡 Off",
415
- callback_data: "settings:set:proactive:off",
416
- },
417
- ],
418
- ],
419
- };
420
- }
421
-
422
380
  export function buildDraftPreviewsSettingsReplyMarkup(
423
381
  enabled: boolean,
424
382
  ): TelegramSettingsMenuReplyMarkup {
@@ -521,7 +479,6 @@ export async function updateTelegramSettingsMenuMessage(
521
479
  await deps.updateSettingsMessage(
522
480
  buildTelegramSettingsMenuText(),
523
481
  buildTelegramSettingsMenuReplyMarkup(
524
- deps.isProactivePushEnabled(),
525
482
  deps.areDraftPreviewsEnabled(),
526
483
  deps.getAssistantRenderingMode(),
527
484
  deps.getVoiceReplyMode(),
@@ -544,16 +501,6 @@ export async function updateAutomaticThreadCleanupSettingsMessage(
544
501
  );
545
502
  }
546
503
 
547
- export async function updateProactivePushSettingsMessage(
548
- deps: TelegramSettingsMenuCallbackDeps,
549
- ): Promise<void> {
550
- const proactivePushEnabled = deps.isProactivePushEnabled();
551
- await deps.updateSettingsMessage(
552
- buildProactivePushSettingsText(proactivePushEnabled),
553
- buildProactivePushSettingsReplyMarkup(proactivePushEnabled),
554
- );
555
- }
556
-
557
504
  export async function updateDraftPreviewsSettingsMessage(
558
505
  deps: TelegramSettingsMenuCallbackDeps,
559
506
  ): Promise<void> {
@@ -621,9 +568,16 @@ export async function handleTelegramSettingsMenuCallbackAction(
621
568
  await deps.answerCallbackQuery(callbackQueryId);
622
569
  return true;
623
570
  }
624
- if (data === "settings:open:proactive") {
625
- await updateProactivePushSettingsMessage(deps);
626
- await deps.answerCallbackQuery(callbackQueryId);
571
+ if (
572
+ data === "settings:open:proactive" ||
573
+ data === "settings:set:proactive:on" ||
574
+ data === "settings:set:proactive:off"
575
+ ) {
576
+ await updateTelegramSettingsMenuMessage(deps, deps.sectionRegistry);
577
+ await deps.answerCallbackQuery(
578
+ callbackQueryId,
579
+ "Public assistant output is always delivered while Telegram is connected.",
580
+ );
627
581
  return true;
628
582
  }
629
583
  if (
@@ -751,19 +705,6 @@ export async function handleTelegramSettingsMenuCallbackAction(
751
705
  );
752
706
  return true;
753
707
  }
754
- if (
755
- data === "settings:set:proactive:on" ||
756
- data === "settings:set:proactive:off"
757
- ) {
758
- const enabled = data.endsWith(":on");
759
- await deps.setProactivePushEnabled(enabled);
760
- await updateProactivePushSettingsMessage(deps);
761
- await deps.answerCallbackQuery(
762
- callbackQueryId,
763
- `Proactive push ${enabled ? "enabled" : "disabled"}`,
764
- );
765
- return true;
766
- }
767
708
  await deps.answerCallbackQuery(callbackQueryId);
768
709
  return true;
769
710
  }
@@ -781,7 +722,6 @@ export function createTelegramSettingsMenuRuntime<
781
722
  return openTelegramSettingsMenu(
782
723
  {
783
724
  getModelMenuState: () => deps.getModelMenuState(chatId, ctx),
784
- isProactivePushEnabled: deps.isProactivePushEnabled,
785
725
  areDraftPreviewsEnabled: deps.areDraftPreviewsEnabled,
786
726
  getAssistantRenderingMode: deps.getAssistantRenderingMode,
787
727
  getActivityVerbosity: deps.getActivityVerbosity,
@@ -805,7 +745,6 @@ export function createTelegramSettingsMenuRuntime<
805
745
  await deps.reloadConfig?.();
806
746
  return updateTelegramSettingsMenuMessage(
807
747
  {
808
- isProactivePushEnabled: deps.isProactivePushEnabled,
809
748
  areDraftPreviewsEnabled: deps.areDraftPreviewsEnabled,
810
749
  getAssistantRenderingMode: deps.getAssistantRenderingMode,
811
750
  getActivityVerbosity: deps.getActivityVerbosity,
@@ -849,7 +788,6 @@ export function createTelegramSettingsMenuRuntime<
849
788
  deps.storeModelMenuState(state);
850
789
  }
851
790
  return handleTelegramSettingsMenuCallbackAction(query.id, query.data, {
852
- isProactivePushEnabled: deps.isProactivePushEnabled,
853
791
  areDraftPreviewsEnabled: deps.areDraftPreviewsEnabled,
854
792
  getAssistantRenderingMode: deps.getAssistantRenderingMode,
855
793
  getActivityVerbosity: deps.getActivityVerbosity,
@@ -857,7 +795,6 @@ export function createTelegramSettingsMenuRuntime<
857
795
  isVoiceReplyModeConfigured: deps.isVoiceReplyModeConfigured,
858
796
  getTimeInjectionMode: deps.getTimeInjectionMode,
859
797
  isAutomaticThreadCleanupEnabled: deps.isAutomaticThreadCleanupEnabled,
860
- setProactivePushEnabled: deps.setProactivePushEnabled,
861
798
  setDraftPreviewsEnabled: deps.setDraftPreviewsEnabled,
862
799
  setAssistantRenderingMode: deps.setAssistantRenderingMode,
863
800
  setActivityVerbosity: deps.setActivityVerbosity,
@@ -12,7 +12,6 @@ import type {
12
12
  } from "./keyboard.ts";
13
13
  import {
14
14
  parseTelegramActionPayloadRows,
15
- parseTopLevelTelegramComment,
16
15
  replaceTopLevelHtmlComments,
17
16
  } from "./outbound-markup.ts";
18
17
  import {
@@ -113,8 +112,10 @@ function parseTelegramButtonAction(
113
112
  payload: Record<string, unknown>,
114
113
  ): TelegramOutboundButtonAction | undefined {
115
114
  const value = getTelegramButtonString(payload, "value");
116
- const label = getTelegramButtonString(payload, "label") ?? value;
117
- const prompt = getTelegramButtonString(payload, "prompt") ?? value;
115
+ const explicitLabel = getTelegramButtonString(payload, "label");
116
+ const explicitPrompt = getTelegramButtonString(payload, "prompt");
117
+ const label = explicitLabel ?? value ?? explicitPrompt;
118
+ const prompt = explicitPrompt ?? value ?? explicitLabel;
118
119
  if (!label || !prompt) return undefined;
119
120
  const selectedStyle = payload.selected_style;
120
121
  return {
@@ -179,10 +180,9 @@ export function planTelegramButtonReply(
179
180
  ): TelegramButtonReplyPlan {
180
181
  const keyboard: TelegramOutboundButtonMarkup["inline_keyboard"] = [];
181
182
  const stripped = replaceTopLevelHtmlComments(markdown, (comment) => {
182
- const command = ["telegram_button", "telegram_buttons"].find((candidate) =>
183
- parseTopLevelTelegramComment(comment, candidate),
184
- );
185
- if (!command) return comment.raw;
183
+ const command = "telegram_button";
184
+ const normalizedContent = comment.content.replace(/^\s+/, "").replace(/^!/, "");
185
+ if (!normalizedContent.startsWith(command)) return comment.raw;
186
186
  const payloadRows = parseTelegramActionPayloadRows(comment, command);
187
187
  if (!payloadRows) return "";
188
188
  const actionRows = payloadRows.map((payloadRow) =>
@@ -225,8 +225,9 @@ export function createTelegramButtonPromptTurn(options: {
225
225
  queueOrder: number;
226
226
  action: TelegramOutboundButtonAction;
227
227
  target?: TelegramQueueTarget;
228
+ telegramPrefix?: string;
228
229
  }): PendingTelegramTurn {
229
- const prompt = `[telegram] ${options.action.prompt}`;
230
+ const prompt = `${options.telegramPrefix ?? "[telegram]"} ${options.action.prompt}`;
230
231
  return {
231
232
  kind: "prompt",
232
233
  chatId: options.chatId,
@@ -153,63 +153,153 @@ export function parseTopLevelTelegramComment(
153
153
  };
154
154
  }
155
155
 
156
- function parseCanonicalTelegramActionAttributes(
156
+ function parseTolerantTelegramAttributes(
157
157
  source: string,
158
+ names: readonly string[],
158
159
  ): Record<string, string> | undefined {
159
160
  const attributes: Record<string, string> = {};
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;
161
+ const namePattern = names.join("|");
162
+ const pattern = new RegExp(
163
+ `\\b(${namePattern})\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s]+))`,
164
+ "gu",
165
+ );
166
+ for (const match of source.matchAll(pattern)) {
167
+ const value = (match[2] ?? match[3] ?? match[4] ?? "").trim();
168
+ if (value) attributes[match[1]!] = value;
169
169
  }
170
170
  return Object.keys(attributes).length > 0 ? attributes : undefined;
171
171
  }
172
172
 
173
- function getTelegramActionPayloadSource(
174
- comment: TelegramTopLevelHtmlComment,
175
- command: string,
176
- ): { source: string; hasBody: boolean } | undefined {
177
- const parsed = parseTopLevelTelegramComment(comment, command);
178
- if (!parsed || parsed.head.trimStart().startsWith(":")) return undefined;
179
- const source = [parsed.head, parsed.body]
180
- .filter((part): part is string => part !== undefined)
181
- .join("\n")
182
- .trim();
183
- return source ? { source, hasBody: parsed.body !== undefined } : undefined;
184
- }
185
-
186
173
  function isTelegramActionPayload(value: unknown): value is Record<string, unknown> {
187
174
  return value !== null && typeof value === "object" && !Array.isArray(value);
188
175
  }
189
176
 
177
+ function removeTelegramJsonTrailingCommas(source: string): string {
178
+ let normalized = "";
179
+ let inString = false;
180
+ let escaped = false;
181
+ for (let offset = 0; offset < source.length; offset += 1) {
182
+ const character = source[offset]!;
183
+ if (inString) {
184
+ normalized += character;
185
+ if (escaped) escaped = false;
186
+ else if (character === "\\") escaped = true;
187
+ else if (character === '"') inString = false;
188
+ continue;
189
+ }
190
+ if (character === '"') {
191
+ inString = true;
192
+ normalized += character;
193
+ continue;
194
+ }
195
+ if (character === ",") {
196
+ let next = offset + 1;
197
+ while (/\s/u.test(source[next] ?? "")) next += 1;
198
+ if (source[next] === "}" || source[next] === "]") continue;
199
+ }
200
+ normalized += character;
201
+ }
202
+ return normalized;
203
+ }
204
+
205
+ function parseTelegramJsonObjectCandidate(
206
+ source: string,
207
+ ): Record<string, unknown> | undefined {
208
+ const normalized = removeTelegramJsonTrailingCommas(source);
209
+ for (const candidate of normalized === source ? [source] : [source, normalized]) {
210
+ try {
211
+ const value: unknown = JSON.parse(candidate);
212
+ if (isTelegramActionPayload(value)) return value;
213
+ } catch {
214
+ // Try the bounded trailing-comma normalization before rejecting JSON.
215
+ }
216
+ }
217
+ return undefined;
218
+ }
219
+
220
+ function looksLikeTelegramNamedJsonObject(
221
+ source: string,
222
+ offset: number,
223
+ ): boolean {
224
+ return /^\{\s*"(?:[^"\\]|\\.)*"\s*:/u.test(source.slice(offset));
225
+ }
226
+
190
227
  export function parseTelegramActionPayload(
191
228
  comment: TelegramTopLevelHtmlComment,
192
229
  command: string,
193
230
  ): Record<string, unknown> | undefined {
194
- const payload = getTelegramActionPayloadSource(comment, command);
195
- if (!payload) return undefined;
196
- if (payload.source.startsWith("{")) {
197
- try {
198
- const value: unknown = JSON.parse(payload.source);
199
- return isTelegramActionPayload(value) ? value : undefined;
200
- } catch {
201
- return undefined;
231
+ let content = comment.content.replace(/^\s+/, "").replace(/^!/, "");
232
+ if (!content.startsWith(command)) return undefined;
233
+ content = content.slice(command.length);
234
+ let attributeEnvelope = content;
235
+ for (let offset = 0; offset < content.length; offset += 1) {
236
+ if (content[offset] !== "{" && content[offset] !== "[") continue;
237
+ if (
238
+ content[offset] === "[" &&
239
+ !isPlausibleTelegramMatrixStart(content, offset)
240
+ ) {
241
+ const noiseEnd = findTelegramStructuredPayloadEnd(content, offset);
242
+ if (noiseEnd !== undefined) {
243
+ attributeEnvelope = `${attributeEnvelope.slice(0, offset)}${" ".repeat(noiseEnd - offset)}${attributeEnvelope.slice(noiseEnd)}`;
244
+ offset = noiseEnd - 1;
245
+ }
246
+ continue;
247
+ }
248
+ if (content[offset] === "{") {
249
+ const parsed = parseTelegramAdaptiveActionPayloadRows(
250
+ content.slice(offset),
251
+ parseTelegramVoiceCompactActionPayload,
252
+ { allowTrailing: true },
253
+ );
254
+ if (parsed) return parsed.rows[0]![0];
202
255
  }
256
+ const end = findTelegramStructuredPayloadEnd(content, offset);
257
+ if (end === undefined) continue;
258
+ attributeEnvelope = `${attributeEnvelope.slice(0, offset)}${" ".repeat(end - offset)}${attributeEnvelope.slice(end)}`;
259
+ offset = end - 1;
203
260
  }
204
- if (payload.hasBody) return undefined;
205
- return parseCanonicalTelegramActionAttributes(payload.source);
261
+ return parseTolerantTelegramAttributes(attributeEnvelope, [
262
+ "text",
263
+ "value",
264
+ "lang",
265
+ "rate",
266
+ ]);
206
267
  }
207
268
 
208
269
  const TELEGRAM_COMPACT_ACTION_CONTROL_PATTERN = /[\u0000-\u001f\u007f-\u009f]/u;
209
270
 
271
+ type TelegramCompactActionPayloadParser = (
272
+ atoms: readonly string[],
273
+ ) => Record<string, unknown> | undefined;
274
+
275
+ function parseTelegramButtonCompactActionPayload(
276
+ atoms: readonly string[],
277
+ ): Record<string, unknown> | undefined {
278
+ const [label, prompt, selectedStyle] = atoms;
279
+ if (atoms.length === 1) return { value: label };
280
+ if (atoms.length === 2) return { label, prompt };
281
+ if (
282
+ selectedStyle !== "primary" &&
283
+ selectedStyle !== "success" &&
284
+ selectedStyle !== "danger"
285
+ ) return undefined;
286
+ return { label, prompt, selected_style: selectedStyle };
287
+ }
288
+
289
+ function parseTelegramVoiceCompactActionPayload(
290
+ atoms: readonly string[],
291
+ ): Record<string, unknown> | undefined {
292
+ const [text, lang, rate] = atoms;
293
+ if (atoms.length === 1) return { text };
294
+ if (atoms.length === 2) return { text, lang };
295
+ return { text, lang, rate };
296
+ }
297
+
210
298
  function parseTelegramAdaptiveActionPayloadRows(
211
299
  source: string,
212
- ): Record<string, unknown>[][] | undefined {
300
+ parseCompactPayload: TelegramCompactActionPayloadParser,
301
+ options: { allowTrailing?: boolean } = {},
302
+ ): { rows: Record<string, unknown>[][]; end: number } | undefined {
213
303
  let offset = 0;
214
304
  const isStructuralWhitespace = (character: string | undefined): boolean =>
215
305
  character === " " ||
@@ -224,7 +314,7 @@ function parseTelegramAdaptiveActionPayloadRows(
224
314
  if (source[offset] !== ",") return true;
225
315
  offset += 1;
226
316
  skipWhitespace();
227
- return source[offset] !== "," && source[offset] !== "]";
317
+ return source[offset] !== ",";
228
318
  };
229
319
  const normalizeAtom = (value: string): string | undefined => {
230
320
  const normalized = value.trim();
@@ -259,15 +349,7 @@ function parseTelegramAdaptiveActionPayloadRows(
259
349
  normalizeAtom(atom.join("")),
260
350
  );
261
351
  if (atoms.some((atom) => atom === undefined)) return undefined;
262
- const [label, prompt, selectedStyle] = atoms as string[];
263
- if (atoms.length === 1) return { value: label };
264
- if (atoms.length === 2) return { label, prompt };
265
- if (
266
- selectedStyle !== "primary" &&
267
- selectedStyle !== "success" &&
268
- selectedStyle !== "danger"
269
- ) return undefined;
270
- return { label, prompt, selected_style: selectedStyle };
352
+ return parseCompactPayload(atoms as string[]);
271
353
  }
272
354
  atomSources.at(-1)!.push(character);
273
355
  offset += 1;
@@ -304,14 +386,10 @@ function parseTelegramAdaptiveActionPayloadRows(
304
386
  ) return undefined;
305
387
  if (stack.length > 0) continue;
306
388
  const candidate = source.slice(start, index + 1);
307
- try {
308
- const value: unknown = JSON.parse(candidate);
309
- if (!isTelegramActionPayload(value)) return undefined;
310
- offset = index + 1;
311
- return value;
312
- } catch {
313
- return undefined;
314
- }
389
+ const value = parseTelegramJsonObjectCandidate(candidate);
390
+ if (!value) return undefined;
391
+ offset = index + 1;
392
+ return value;
315
393
  }
316
394
  return undefined;
317
395
  };
@@ -320,6 +398,7 @@ function parseTelegramAdaptiveActionPayloadRows(
320
398
  const jsonCell = parseJsonObjectCell();
321
399
  if (jsonCell) return jsonCell;
322
400
  offset = start;
401
+ if (looksLikeTelegramNamedJsonObject(source, start)) return undefined;
323
402
  return parseCompactCell();
324
403
  };
325
404
  const parseRow = (): Record<string, unknown>[] | undefined => {
@@ -377,42 +456,92 @@ function parseTelegramAdaptiveActionPayloadRows(
377
456
  }
378
457
  if (!rows) return undefined;
379
458
  skipWhitespace();
380
- return offset === source.length ? rows : undefined;
459
+ if (!options.allowTrailing && offset !== source.length) return undefined;
460
+ return { rows, end: offset };
461
+ }
462
+
463
+ function findTelegramStructuredPayloadEnd(
464
+ source: string,
465
+ start: number,
466
+ ): number | undefined {
467
+ const stack: string[] = [source[start]!];
468
+ let inString = false;
469
+ let escaped = false;
470
+ for (let offset = start + 1; offset < source.length; offset += 1) {
471
+ const character = source[offset]!;
472
+ if (inString) {
473
+ if (escaped) escaped = false;
474
+ else if (character === "\\") escaped = true;
475
+ else if (character === '"') inString = false;
476
+ continue;
477
+ }
478
+ if (character === '"') {
479
+ inString = true;
480
+ continue;
481
+ }
482
+ if (character === "[" || character === "{") {
483
+ stack.push(character);
484
+ continue;
485
+ }
486
+ if (character !== "]" && character !== "}") continue;
487
+ const expected = character === "]" ? "[" : "{";
488
+ if (stack.at(-1) === expected) stack.pop();
489
+ if (stack.length === 0) return offset + 1;
490
+ }
491
+ return undefined;
492
+ }
493
+
494
+ function isPlausibleTelegramMatrixStart(
495
+ source: string,
496
+ start: number,
497
+ ): boolean {
498
+ let offset = start + 1;
499
+ while (/\s/u.test(source[offset] ?? "")) offset += 1;
500
+ return (
501
+ source[offset] === "{" ||
502
+ source[offset] === "[" ||
503
+ source[offset] === "]"
504
+ );
381
505
  }
382
506
 
383
507
  export function parseTelegramActionPayloadRows(
384
508
  comment: TelegramTopLevelHtmlComment,
385
509
  command: string,
386
510
  ): Record<string, unknown>[][] | undefined {
387
- const payload = getTelegramActionPayloadSource(comment, command);
388
- if (!payload) return undefined;
389
- if (payload.source.startsWith("[") || payload.source.startsWith("{")) {
390
- try {
391
- const value: unknown = JSON.parse(payload.source);
392
- if (isTelegramActionPayload(value)) return [[value]];
393
- if (!Array.isArray(value)) return undefined;
394
- const rows: Record<string, unknown>[][] = [];
395
- for (const entry of value) {
396
- if (isTelegramActionPayload(entry)) {
397
- rows.push([entry]);
398
- continue;
399
- }
400
- if (
401
- !Array.isArray(entry) ||
402
- entry.length === 0 ||
403
- !entry.every(isTelegramActionPayload)
404
- ) {
405
- return undefined;
406
- }
407
- rows.push(entry);
511
+ let content = comment.content.replace(/^\s+/, "").replace(/^!/, "");
512
+ if (!content.startsWith(command)) return undefined;
513
+ content = content.slice(command.length);
514
+ let attributeEnvelope = content;
515
+ for (let offset = 0; offset < content.length; offset += 1) {
516
+ if (content[offset] !== "[" && content[offset] !== "{") continue;
517
+ if (
518
+ content[offset] === "[" &&
519
+ !isPlausibleTelegramMatrixStart(content, offset)
520
+ ) {
521
+ const noiseEnd = findTelegramStructuredPayloadEnd(content, offset);
522
+ if (noiseEnd !== undefined) {
523
+ attributeEnvelope = `${attributeEnvelope.slice(0, offset)}${" ".repeat(noiseEnd - offset)}${attributeEnvelope.slice(noiseEnd)}`;
524
+ offset = noiseEnd - 1;
408
525
  }
409
- return rows;
410
- } catch {
411
- return parseTelegramAdaptiveActionPayloadRows(payload.source);
526
+ continue;
412
527
  }
528
+ const parsed = parseTelegramAdaptiveActionPayloadRows(
529
+ content.slice(offset),
530
+ parseTelegramButtonCompactActionPayload,
531
+ { allowTrailing: true },
532
+ );
533
+ if (parsed) return parsed.rows;
534
+ const end = findTelegramStructuredPayloadEnd(content, offset);
535
+ if (end === undefined) continue;
536
+ attributeEnvelope = `${attributeEnvelope.slice(0, offset)}${" ".repeat(end - offset)}${attributeEnvelope.slice(end)}`;
537
+ offset = end - 1;
413
538
  }
414
- if (payload.hasBody) return undefined;
415
- const attributes = parseCanonicalTelegramActionAttributes(payload.source);
539
+ const attributes = parseTolerantTelegramAttributes(attributeEnvelope, [
540
+ "label",
541
+ "prompt",
542
+ "value",
543
+ "selected_style",
544
+ ]);
416
545
  return attributes ? [[attributes]] : undefined;
417
546
  }
418
547
 
@@ -481,9 +610,10 @@ export function planTelegramVoiceReply(
481
610
  let lang: string | undefined;
482
611
  let rate: string | undefined;
483
612
  const stripped = replaceTopLevelHtmlComments(markdown, (comment) => {
484
- const command = parseTopLevelTelegramComment(comment, "telegram_voice");
485
- if (!command) return comment.raw;
486
- const payload = parseTelegramActionPayload(comment, "telegram_voice");
613
+ const command = "telegram_voice";
614
+ const normalizedContent = comment.content.replace(/^\s+/, "").replace(/^!/, "");
615
+ if (!normalizedContent.startsWith(command)) return comment.raw;
616
+ const payload = parseTelegramActionPayload(comment, command);
487
617
  if (!payload) return "";
488
618
  const text =
489
619
  getTelegramActionString(payload, "text") ??