@llblab/pi-kit 0.2.0 → 0.3.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.
Files changed (29) hide show
  1. package/AGENTS.md +1 -0
  2. package/CHANGELOG.md +9 -0
  3. package/README.md +1 -1
  4. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +18 -0
  5. package/node_modules/@llblab/pi-telegram/README.md +3 -3
  6. package/node_modules/@llblab/pi-telegram/docs/architecture.md +6 -6
  7. package/node_modules/@llblab/pi-telegram/docs/compact-matrix-literal.md +29 -23
  8. package/node_modules/@llblab/pi-telegram/docs/inbound.md +1 -1
  9. package/node_modules/@llblab/pi-telegram/docs/outbound.md +12 -21
  10. package/node_modules/@llblab/pi-telegram/docs/public-api.md +4 -4
  11. package/node_modules/@llblab/pi-telegram/lib/activity.ts +7 -0
  12. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +3 -0
  13. package/node_modules/@llblab/pi-telegram/lib/commands.ts +16 -6
  14. package/node_modules/@llblab/pi-telegram/lib/media.ts +12 -2
  15. package/node_modules/@llblab/pi-telegram/lib/outbound-buttons.ts +9 -8
  16. package/node_modules/@llblab/pi-telegram/lib/outbound-markup.ts +218 -83
  17. package/node_modules/@llblab/pi-telegram/lib/routing.ts +78 -36
  18. package/node_modules/@llblab/pi-telegram/lib/status.ts +13 -4
  19. package/node_modules/@llblab/pi-telegram/lib/turns.ts +10 -1
  20. package/node_modules/@llblab/pi-telegram/lib/updates.ts +1 -1
  21. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  22. package/node_modules/@llblab/pi-telegram/skills/generated-control-surface/SKILL.md +61 -207
  23. package/node_modules/@llblab/pi-telegram/skills/generated-control-surface/references/capability-adapters.md +27 -0
  24. package/node_modules/@llblab/pi-telegram/skills/generated-control-surface/references/layout-and-state.md +35 -0
  25. package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/SKILL.md +71 -110
  26. package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/configuration.md +15 -0
  27. package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/delivery-and-threads.md +27 -0
  28. package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/diagnosis.md +14 -0
  29. package/package.json +2 -2
@@ -153,63 +153,158 @@ 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 label ? { value: label } : undefined;
280
+ if (!prompt) return undefined;
281
+ const action = label ? { label, prompt } : { prompt };
282
+ if (atoms.length === 2) return action;
283
+ if (
284
+ selectedStyle !== "primary" &&
285
+ selectedStyle !== "success" &&
286
+ selectedStyle !== "danger"
287
+ ) return undefined;
288
+ return { ...action, selected_style: selectedStyle };
289
+ }
290
+
291
+ function parseTelegramVoiceCompactActionPayload(
292
+ atoms: readonly string[],
293
+ ): Record<string, unknown> | undefined {
294
+ const [text, lang, rate] = atoms;
295
+ if (!text) return undefined;
296
+ if (atoms.length === 1) return { text };
297
+ if (!lang) return undefined;
298
+ if (atoms.length === 2) return { text, lang };
299
+ if (!rate) return undefined;
300
+ return { text, lang, rate };
301
+ }
302
+
210
303
  function parseTelegramAdaptiveActionPayloadRows(
211
304
  source: string,
212
- ): Record<string, unknown>[][] | undefined {
305
+ parseCompactPayload: TelegramCompactActionPayloadParser,
306
+ options: { allowTrailing?: boolean } = {},
307
+ ): { rows: Record<string, unknown>[][]; end: number } | undefined {
213
308
  let offset = 0;
214
309
  const isStructuralWhitespace = (character: string | undefined): boolean =>
215
310
  character === " " ||
@@ -224,11 +319,11 @@ function parseTelegramAdaptiveActionPayloadRows(
224
319
  if (source[offset] !== ",") return true;
225
320
  offset += 1;
226
321
  skipWhitespace();
227
- return source[offset] !== "," && source[offset] !== "]";
322
+ return source[offset] !== ",";
228
323
  };
229
324
  const normalizeAtom = (value: string): string | undefined => {
230
325
  const normalized = value.trim();
231
- return normalized && !TELEGRAM_COMPACT_ACTION_CONTROL_PATTERN.test(normalized)
326
+ return !TELEGRAM_COMPACT_ACTION_CONTROL_PATTERN.test(normalized)
232
327
  ? normalized
233
328
  : undefined;
234
329
  };
@@ -259,15 +354,7 @@ function parseTelegramAdaptiveActionPayloadRows(
259
354
  normalizeAtom(atom.join("")),
260
355
  );
261
356
  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 };
357
+ return parseCompactPayload(atoms as string[]);
271
358
  }
272
359
  atomSources.at(-1)!.push(character);
273
360
  offset += 1;
@@ -304,14 +391,10 @@ function parseTelegramAdaptiveActionPayloadRows(
304
391
  ) return undefined;
305
392
  if (stack.length > 0) continue;
306
393
  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
- }
394
+ const value = parseTelegramJsonObjectCandidate(candidate);
395
+ if (!value) return undefined;
396
+ offset = index + 1;
397
+ return value;
315
398
  }
316
399
  return undefined;
317
400
  };
@@ -320,6 +403,7 @@ function parseTelegramAdaptiveActionPayloadRows(
320
403
  const jsonCell = parseJsonObjectCell();
321
404
  if (jsonCell) return jsonCell;
322
405
  offset = start;
406
+ if (looksLikeTelegramNamedJsonObject(source, start)) return undefined;
323
407
  return parseCompactCell();
324
408
  };
325
409
  const parseRow = (): Record<string, unknown>[] | undefined => {
@@ -377,42 +461,92 @@ function parseTelegramAdaptiveActionPayloadRows(
377
461
  }
378
462
  if (!rows) return undefined;
379
463
  skipWhitespace();
380
- return offset === source.length ? rows : undefined;
464
+ if (!options.allowTrailing && offset !== source.length) return undefined;
465
+ return { rows, end: offset };
466
+ }
467
+
468
+ function findTelegramStructuredPayloadEnd(
469
+ source: string,
470
+ start: number,
471
+ ): number | undefined {
472
+ const stack: string[] = [source[start]!];
473
+ let inString = false;
474
+ let escaped = false;
475
+ for (let offset = start + 1; offset < source.length; offset += 1) {
476
+ const character = source[offset]!;
477
+ if (inString) {
478
+ if (escaped) escaped = false;
479
+ else if (character === "\\") escaped = true;
480
+ else if (character === '"') inString = false;
481
+ continue;
482
+ }
483
+ if (character === '"') {
484
+ inString = true;
485
+ continue;
486
+ }
487
+ if (character === "[" || character === "{") {
488
+ stack.push(character);
489
+ continue;
490
+ }
491
+ if (character !== "]" && character !== "}") continue;
492
+ const expected = character === "]" ? "[" : "{";
493
+ if (stack.at(-1) === expected) stack.pop();
494
+ if (stack.length === 0) return offset + 1;
495
+ }
496
+ return undefined;
497
+ }
498
+
499
+ function isPlausibleTelegramMatrixStart(
500
+ source: string,
501
+ start: number,
502
+ ): boolean {
503
+ let offset = start + 1;
504
+ while (/\s/u.test(source[offset] ?? "")) offset += 1;
505
+ return (
506
+ source[offset] === "{" ||
507
+ source[offset] === "[" ||
508
+ source[offset] === "]"
509
+ );
381
510
  }
382
511
 
383
512
  export function parseTelegramActionPayloadRows(
384
513
  comment: TelegramTopLevelHtmlComment,
385
514
  command: string,
386
515
  ): 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);
516
+ let content = comment.content.replace(/^\s+/, "").replace(/^!/, "");
517
+ if (!content.startsWith(command)) return undefined;
518
+ content = content.slice(command.length);
519
+ let attributeEnvelope = content;
520
+ for (let offset = 0; offset < content.length; offset += 1) {
521
+ if (content[offset] !== "[" && content[offset] !== "{") continue;
522
+ if (
523
+ content[offset] === "[" &&
524
+ !isPlausibleTelegramMatrixStart(content, offset)
525
+ ) {
526
+ const noiseEnd = findTelegramStructuredPayloadEnd(content, offset);
527
+ if (noiseEnd !== undefined) {
528
+ attributeEnvelope = `${attributeEnvelope.slice(0, offset)}${" ".repeat(noiseEnd - offset)}${attributeEnvelope.slice(noiseEnd)}`;
529
+ offset = noiseEnd - 1;
408
530
  }
409
- return rows;
410
- } catch {
411
- return parseTelegramAdaptiveActionPayloadRows(payload.source);
531
+ continue;
412
532
  }
533
+ const parsed = parseTelegramAdaptiveActionPayloadRows(
534
+ content.slice(offset),
535
+ parseTelegramButtonCompactActionPayload,
536
+ { allowTrailing: true },
537
+ );
538
+ if (parsed) return parsed.rows;
539
+ const end = findTelegramStructuredPayloadEnd(content, offset);
540
+ if (end === undefined) continue;
541
+ attributeEnvelope = `${attributeEnvelope.slice(0, offset)}${" ".repeat(end - offset)}${attributeEnvelope.slice(end)}`;
542
+ offset = end - 1;
413
543
  }
414
- if (payload.hasBody) return undefined;
415
- const attributes = parseCanonicalTelegramActionAttributes(payload.source);
544
+ const attributes = parseTolerantTelegramAttributes(attributeEnvelope, [
545
+ "label",
546
+ "prompt",
547
+ "value",
548
+ "selected_style",
549
+ ]);
416
550
  return attributes ? [[attributes]] : undefined;
417
551
  }
418
552
 
@@ -481,9 +615,10 @@ export function planTelegramVoiceReply(
481
615
  let lang: string | undefined;
482
616
  let rate: string | undefined;
483
617
  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");
618
+ const command = "telegram_voice";
619
+ const normalizedContent = comment.content.replace(/^\s+/, "").replace(/^!/, "");
620
+ if (!normalizedContent.startsWith(command)) return comment.raw;
621
+ const payload = parseTelegramActionPayload(comment, command);
487
622
  if (!payload) return "";
488
623
  const text =
489
624
  getTelegramActionString(payload, "text") ??
@@ -107,8 +107,9 @@ function appendTelegramSourceAttachmentSection(
107
107
  text: string,
108
108
  from: string | undefined,
109
109
  files: Pick<Media.DownloadedTelegramFile, "path">[],
110
+ outputs: readonly string[] = [],
110
111
  ): string {
111
- if (files.length === 0) return text;
112
+ if (files.length === 0 && outputs.length === 0) return text;
112
113
  const dirs = [...new Set(files.map((file) => dirname(file.path)))];
113
114
  const sameDir = dirs.length === 1;
114
115
  const source = from ? `|from:${from}` : "";
@@ -118,8 +119,15 @@ function appendTelegramSourceAttachmentSection(
118
119
  const items = sameDir
119
120
  ? files.map((file) => `/${basename(file.path)}`)
120
121
  : files.map((file) => file.path);
121
- const prefix = text.length > 0 ? `${text}\n\n` : "";
122
- return `${prefix}${header}\n${items.map((item) => `- ${item}`).join("\n")}`;
122
+ const sections = text ? [text] : [];
123
+ if (items.length > 0) {
124
+ sections.push(`${header}\n${items.map((item) => `- ${item}`).join("\n")}`);
125
+ }
126
+ if (outputs.length > 0) {
127
+ const outputHeader = `[outputs${source}]`;
128
+ sections.push(`${outputHeader}\n${outputs.map((output) => `- ${output}`).join("\n")}`);
129
+ }
130
+ return sections.join("\n\n");
123
131
  }
124
132
 
125
133
  function getContextCwd(ctx: unknown): string | undefined {
@@ -269,7 +277,7 @@ function getTelegramRoutableThreadRecords(
269
277
 
270
278
  function formatTelegramAllTabMenuChooserText(command: string): string {
271
279
  return [
272
- "<b>🧵 Choose target thread</b>",
280
+ "<b>🧵 Choose target thread:</b>",
273
281
  "",
274
282
  `You used <code>/${escapeHtml(command)}</code> from the <b>All</b> tab.`,
275
283
  "Select the Pi thread that should handle it:",
@@ -332,7 +340,7 @@ function buildTelegramUnboundRerouteRestoreChooserMarkup(
332
340
 
333
341
  function formatTelegramUnboundRerouteRestoreChooserText(): string {
334
342
  return [
335
- "<b>🧵 Replace/restore Telegram thread</b>",
343
+ "<b>🧵 Replace/restore Telegram thread:</b>",
336
344
  "",
337
345
  "Choose the Pi instance to move to this new Telegram thread:",
338
346
  ].join("\n");
@@ -340,7 +348,7 @@ function formatTelegramUnboundRerouteRestoreChooserText(): string {
340
348
 
341
349
  function formatTelegramUnboundTopicGuidance(): string {
342
350
  return [
343
- "⚠️ <b>New thread is not a Pi instance</b>",
351
+ "<b>⚠️ New thread is not a Pi instance.</b>",
344
352
  "",
345
353
  "To create a bound Telegram tab:",
346
354
  "<code>1.</code> Start another Pi instance in your terminal.",
@@ -358,7 +366,7 @@ function formatTelegramUnboundRerouteChooserText(
358
366
  options: { includeGuidance?: boolean } = {},
359
367
  ): string {
360
368
  const rerouteText = [
361
- "🧵 <b>Choose target thread</b>",
369
+ "<b>🧵 Choose target thread:</b>",
362
370
  "",
363
371
  "Your message is still in this Telegram thread.",
364
372
  "Select the Pi thread that should handle it:",
@@ -751,6 +759,42 @@ export function createTelegramInboundRouteRuntime<
751
759
  );
752
760
  }
753
761
  };
762
+ const resolveTelegramThreadLabel = (message: {
763
+ chat: { id: number };
764
+ message_thread_id?: number;
765
+ }): string | undefined => {
766
+ const chatId = message.chat.id;
767
+ const threadId = message.message_thread_id;
768
+ if (!threadId) return undefined;
769
+ const localLabel = deps.getLocalThreadLabelForTarget?.({ chatId, threadId });
770
+ if (localLabel) return localLabel;
771
+ if (!deps.threadStore) return undefined;
772
+ const records = deps.threadStore.list();
773
+ const currentInstanceId = deps.getCurrentInstanceId?.();
774
+ for (const record of records) {
775
+ if (
776
+ record.target.chatId !== chatId ||
777
+ record.target.threadId !== threadId
778
+ ) {
779
+ continue;
780
+ }
781
+ if (
782
+ currentInstanceId &&
783
+ record.instanceId &&
784
+ record.instanceId !== currentInstanceId
785
+ ) {
786
+ continue;
787
+ }
788
+ return record.threadName &&
789
+ Threads.isTelegramTopicThreadNameValidForSlot(
790
+ record.threadName,
791
+ record.slot,
792
+ )
793
+ ? record.threadName
794
+ : getRestoredThreadName(record, record.slot ?? "");
795
+ }
796
+ return undefined;
797
+ };
754
798
  const createAdmissionReceipts = (
755
799
  queueKind: Queue.TelegramQueueItemKind,
756
800
  sources: readonly unknown[],
@@ -1620,6 +1664,13 @@ export function createTelegramInboundRouteRuntime<
1620
1664
  replyToMessageId: messageId,
1621
1665
  queueOrder,
1622
1666
  action,
1667
+ telegramPrefix: Turns.createTelegramTurnPrefix({
1668
+ thread: resolveTelegramThreadLabel({
1669
+ chat: { id: chatId },
1670
+ message_thread_id:
1671
+ buttonQuery.message?.message_thread_id,
1672
+ }),
1673
+ }),
1623
1674
  }),
1624
1675
  ...(admissionReceipts.length > 0 ? { admissionReceipts } : {}),
1625
1676
  };
@@ -1762,34 +1813,7 @@ export function createTelegramInboundRouteRuntime<
1762
1813
 
1763
1814
  // Voice policy resolves missing, invalid, and legacy manual config to hidden.
1764
1815
  getVoiceReplyMode: () => getTelegramVoiceReplyMode(deps.configStore.get()),
1765
- getTelegramThreadLabel(message) {
1766
- if (!deps.threadStore) return undefined;
1767
- const chatId = message.chat.id;
1768
- const threadId = message.message_thread_id;
1769
- if (!threadId) return undefined;
1770
- const localLabel = deps.getLocalThreadLabelForTarget?.({
1771
- chatId,
1772
- threadId,
1773
- });
1774
- if (localLabel) return localLabel;
1775
- const records = deps.threadStore.list();
1776
- const currentInstanceId = deps.getCurrentInstanceId?.();
1777
- for (const r of records) {
1778
- if (r.target.chatId !== chatId || r.target.threadId !== threadId)
1779
- continue;
1780
- if (
1781
- currentInstanceId &&
1782
- r.instanceId &&
1783
- r.instanceId !== currentInstanceId
1784
- )
1785
- continue;
1786
- return r.threadName &&
1787
- Threads.isTelegramTopicThreadNameValidForSlot(r.threadName, r.slot)
1788
- ? r.threadName
1789
- : getRestoredThreadName(r, r.slot ?? "");
1790
- }
1791
- return undefined;
1792
- },
1816
+ getTelegramThreadLabel: resolveTelegramThreadLabel,
1793
1817
  });
1794
1818
  const enqueueContinueTurn = async (
1795
1819
  message: TMessage,
@@ -1877,6 +1901,18 @@ export function createTelegramInboundRouteRuntime<
1877
1901
  getPromptTemplateCommands,
1878
1902
  persistConfig: deps.configStore.persist,
1879
1903
  sendTextReply: deps.sendTextReply,
1904
+ getActiveTurnReply: () => {
1905
+ const activeTurn = deps.activeTurnRuntime.get();
1906
+ if (!activeTurn) return undefined;
1907
+ return async (text, options) => {
1908
+ await deps.sendTextReply(
1909
+ activeTurn.chatId,
1910
+ activeTurn.replyToMessageId,
1911
+ text,
1912
+ { target: activeTurn.target, parseMode: options?.parseMode },
1913
+ );
1914
+ };
1915
+ },
1880
1916
  sendInteractiveMessage: deps.sendInteractiveMessage,
1881
1917
  recordRuntimeEvent: deps.recordRuntimeEvent,
1882
1918
  });
@@ -2265,6 +2301,11 @@ export function createTelegramInboundRouteRuntime<
2265
2301
  )
2266
2302
  : [];
2267
2303
  assertExecutionCurrent();
2304
+ const processedReply =
2305
+ replyFiles.length > 0
2306
+ ? await deps.inboundHandlerRuntime.process(replyFiles, "", ctx)
2307
+ : undefined;
2308
+ assertExecutionCurrent();
2268
2309
  const files = await Media.downloadTelegramMessageFiles([guestMsg], {
2269
2310
  downloadFile: deps.downloadFile,
2270
2311
  });
@@ -2285,7 +2326,8 @@ export function createTelegramInboundRouteRuntime<
2285
2326
  sourceContext = appendTelegramSourceAttachmentSection(
2286
2327
  replyBlock,
2287
2328
  replyPeer,
2288
- replyFiles,
2329
+ processedReply?.promptFiles ?? replyFiles,
2330
+ processedReply?.handlerOutputs,
2289
2331
  );
2290
2332
  }
2291
2333
  const promptText = Turns.buildTelegramTurnPrompt({
@@ -1494,17 +1494,22 @@ function buildUsageSummary(stats: TelegramUsageStats): string | undefined {
1494
1494
  const tokenParts: string[] = [];
1495
1495
  if (stats.totalInput) tokenParts.push(`↑${formatTokens(stats.totalInput)}`);
1496
1496
  if (stats.totalOutput) tokenParts.push(`↓${formatTokens(stats.totalOutput)}`);
1497
+ return tokenParts.length > 0 ? tokenParts.join(" ") : undefined;
1498
+ }
1499
+
1500
+ function buildCacheSummary(stats: TelegramUsageStats): string | undefined {
1501
+ const cacheParts: string[] = [];
1497
1502
  if (stats.totalCacheRead)
1498
- tokenParts.push(`R${formatTokens(stats.totalCacheRead)}`);
1503
+ cacheParts.push(`R${formatTokens(stats.totalCacheRead)}`);
1499
1504
  if (stats.totalCacheWrite)
1500
- tokenParts.push(`W${formatTokens(stats.totalCacheWrite)}`);
1505
+ cacheParts.push(`W${formatTokens(stats.totalCacheWrite)}`);
1501
1506
  if (
1502
1507
  (stats.totalCacheRead > 0 || stats.totalCacheWrite > 0) &&
1503
1508
  stats.latestCacheHitRate !== undefined
1504
1509
  ) {
1505
- tokenParts.push(`CH${stats.latestCacheHitRate.toFixed(1)}%`);
1510
+ cacheParts.push(`CH${stats.latestCacheHitRate.toFixed(1)}%`);
1506
1511
  }
1507
- return tokenParts.length > 0 ? tokenParts.join(" ") : undefined;
1512
+ return cacheParts.length > 0 ? cacheParts.join(" ") : undefined;
1508
1513
  }
1509
1514
 
1510
1515
  function buildCostSummary(
@@ -1557,10 +1562,14 @@ export function buildStatusHtml(
1557
1562
  ),
1558
1563
  ];
1559
1564
  const usageSummary = buildUsageSummary(stats);
1565
+ const cacheSummary = buildCacheSummary(stats);
1560
1566
  const costSummary = buildCostSummary(stats, usesSubscription);
1561
1567
  if (usageSummary) {
1562
1568
  lines.push(buildStatusRow("Tokens", usageSummary));
1563
1569
  }
1570
+ if (cacheSummary) {
1571
+ lines.push(buildStatusRow("Cache", cacheSummary));
1572
+ }
1564
1573
  if (costSummary) {
1565
1574
  lines.push(buildStatusRow("Cost", costSummary));
1566
1575
  }
@@ -468,8 +468,17 @@ export function createTelegramPromptTurnRuntimeBuilder<
468
468
  )
469
469
  : [];
470
470
  if (firstMessage) deps.assertExecutionCurrent?.(firstMessage);
471
+ const processedReply =
472
+ deps.processAttachments && replyFiles.length > 0
473
+ ? await deps.processAttachments(replyFiles, "", ctx as TContext)
474
+ : undefined;
475
+ if (firstMessage) deps.assertExecutionCurrent?.(firstMessage);
471
476
  const replyContext = firstMessage
472
- ? buildTelegramReplyContextBlock(firstMessage, replyFiles)
477
+ ? buildTelegramReplyContextBlock(
478
+ firstMessage,
479
+ processedReply?.promptFiles ?? replyFiles,
480
+ processedReply?.handlerOutputs,
481
+ )
473
482
  : "";
474
483
  const forwardEntries = messages.flatMap((message) => {
475
484
  const context = extractTelegramForwardContextText(
@@ -895,7 +895,7 @@ const TELEGRAM_UNAUTHORIZED_DENIAL_COPY = "Access denied.";
895
895
 
896
896
  function formatTelegramUnauthorizedDenial(format: "plain" | "html"): string {
897
897
  return format === "html"
898
- ? `🚫 <b>${TELEGRAM_UNAUTHORIZED_DENIAL_COPY}</b>`
898
+ ? `<b>🚫 ${TELEGRAM_UNAUTHORIZED_DENIAL_COPY}</b>`
899
899
  : `🚫 ${TELEGRAM_UNAUTHORIZED_DENIAL_COPY}`;
900
900
  }
901
901
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.41.0",
3
+ "version": "0.42.1",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"