@llblab/pi-telegram 0.11.1 → 0.12.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 (45) hide show
  1. package/AGENTS.md +17 -12
  2. package/BACKLOG.md +0 -10
  3. package/CHANGELOG.md +33 -2
  4. package/README.md +42 -25
  5. package/api/inbound.ts +14 -0
  6. package/api/keyboard.ts +10 -0
  7. package/api/outbound.ts +11 -0
  8. package/api/sections.ts +17 -0
  9. package/api/updates.ts +11 -0
  10. package/api/voice.ts +24 -0
  11. package/docs/README.md +7 -5
  12. package/docs/architecture.md +176 -135
  13. package/docs/callback-namespaces.md +3 -3
  14. package/docs/command-templates.md +81 -24
  15. package/docs/{inbound-handlers.md → inbound.md} +13 -10
  16. package/docs/locks.md +3 -3
  17. package/docs/{outbound-handlers.md → outbound.md} +13 -10
  18. package/docs/public-api.md +266 -0
  19. package/docs/{extension-sections.md → sections.md} +31 -27
  20. package/docs/ui-style.md +165 -0
  21. package/docs/{external-handlers.md → updates.md} +33 -31
  22. package/docs/voice.md +17 -14
  23. package/index.ts +86 -261
  24. package/lib/bindings.ts +301 -0
  25. package/lib/command-templates.ts +163 -32
  26. package/lib/commands.ts +114 -1
  27. package/lib/config.ts +45 -4
  28. package/lib/{inbound-handlers.ts → inbound.ts} +5 -4
  29. package/lib/lifecycle.ts +122 -1
  30. package/lib/menu-model.ts +3 -3
  31. package/lib/menu-queue.ts +1 -1
  32. package/lib/menu-settings.ts +63 -32
  33. package/lib/menu-status.ts +1 -1
  34. package/lib/menu.ts +1 -1
  35. package/lib/{outbound-handlers.ts → outbound.ts} +21 -11
  36. package/lib/pi.ts +4 -0
  37. package/lib/polling.ts +4 -3
  38. package/lib/preview.ts +1 -1
  39. package/lib/routing.ts +45 -13
  40. package/lib/{extension-sections.ts → sections.ts} +37 -8
  41. package/lib/time-injection.ts +1 -1
  42. package/lib/updates.ts +121 -1
  43. package/lib/voice.ts +33 -14
  44. package/package.json +11 -1
  45. package/lib/external-handlers.ts +0 -166
@@ -7,7 +7,7 @@
7
7
  import {
8
8
  getTelegramExtensionSettingsRows,
9
9
  type TelegramSectionRegistry,
10
- } from "./extension-sections.ts";
10
+ } from "./sections.ts";
11
11
  import type { TelegramTimeMode } from "./config.ts";
12
12
  import type { TelegramInlineKeyboardMarkup } from "./keyboard.ts";
13
13
  import type { TelegramModelMenuState } from "./menu-model.ts";
@@ -115,7 +115,8 @@ export interface TelegramSettingsMenuRuntimeDeps<
115
115
 
116
116
  export const SETTINGS_MENU_TITLE = "<b>⚙️ Settings:</b>";
117
117
  export const PROACTIVE_PUSH_SETTINGS_TITLE = "<b>📌 Proactive push:</b>";
118
- export const TIME_INJECTION_MODE_SETTINGS_TITLE = "<b>🕒 Time injection mode:</b>";
118
+ export const TIME_INJECTION_MODE_SETTINGS_TITLE =
119
+ "<b>🕒 Time injection mode:</b>";
119
120
  export const VOICE_REPLY_MODE_SETTINGS_TITLE = "<b>👄 Voice reply mode:</b>";
120
121
 
121
122
  type TelegramVoiceReplyModeSetting = TelegramVoiceReplyMode | "hidden";
@@ -124,6 +125,10 @@ function getVoiceReplyModeLabel(mode: TelegramVoiceReplyModeSetting): string {
124
125
  return mode;
125
126
  }
126
127
 
128
+ function getTelegramSettingsStateValueLabel(value: string): string {
129
+ return value.length > 0 ? value[0]!.toUpperCase() + value.slice(1) : value;
130
+ }
131
+
127
132
  function getVoiceReplyModeSetting(
128
133
  mode: TelegramVoiceReplyMode,
129
134
  configured: boolean,
@@ -135,17 +140,24 @@ export function buildTelegramSettingsMenuText(): string {
135
140
  return SETTINGS_MENU_TITLE;
136
141
  }
137
142
 
138
- export function buildProactivePushSettingsText(): string {
143
+ export function buildProactivePushSettingsText(
144
+ proactivePushEnabled: boolean,
145
+ ): string {
139
146
  return [
140
- PROACTIVE_PUSH_SETTINGS_TITLE,
147
+ `${PROACTIVE_PUSH_SETTINGS_TITLE} <code>${proactivePushEnabled ? "on" : "off"}</code>`,
141
148
  "",
142
149
  "Send successful local π task results to Telegram when the bridge is connected.",
143
150
  ].join("\n");
144
151
  }
145
152
 
146
- export function buildVoiceReplyModeSettingsText(): string {
153
+ export function buildVoiceReplyModeSettingsText(
154
+ mode: TelegramVoiceReplyMode,
155
+ configured = true,
156
+ ): string {
147
157
  return [
148
- VOICE_REPLY_MODE_SETTINGS_TITLE,
158
+ `${VOICE_REPLY_MODE_SETTINGS_TITLE} <code>${getVoiceReplyModeLabel(
159
+ getVoiceReplyModeSetting(mode, configured),
160
+ )}</code>`,
149
161
  "",
150
162
  "Controls when pi-telegram converts assistant text replies into Telegram voice messages.",
151
163
  "",
@@ -156,13 +168,15 @@ export function buildVoiceReplyModeSettingsText(): string {
156
168
  ].join("\n");
157
169
  }
158
170
 
159
- export function buildTimeInjectionModeSettingsText(): string {
171
+ export function buildTimeInjectionModeSettingsText(
172
+ mode: TelegramTimeMode,
173
+ ): string {
160
174
  return [
161
- TIME_INJECTION_MODE_SETTINGS_TITLE,
175
+ `${TIME_INJECTION_MODE_SETTINGS_TITLE} <code>${mode}</code>`,
162
176
  "",
163
177
  "Controls whether Telegram-originated prompts include a compact wall-clock [time] line.",
164
178
  "",
165
- "<code>-</code> <code>off</code> (default): no time line is added.",
179
+ "<code>-</code> <code>hidden</code> (default): no time line is added to prompt context.",
166
180
  "<code>-</code> <code>always</code>: add time to every Telegram turn.",
167
181
  "<code>-</code> <code>interval</code>: add time at most once per chat interval (default: 1 hour).",
168
182
  ].join("\n");
@@ -188,21 +202,23 @@ export function buildTelegramSettingsMenuReplyMarkup(
188
202
  rows.push(
189
203
  [
190
204
  {
191
- text: `👄 Voice reply: ${getVoiceReplyModeLabel(
192
- getVoiceReplyModeSetting(voiceReplyMode, voiceReplyModeConfigured),
205
+ text: `👄 Voice reply: ${getTelegramSettingsStateValueLabel(
206
+ getVoiceReplyModeLabel(
207
+ getVoiceReplyModeSetting(voiceReplyMode, voiceReplyModeConfigured),
208
+ ),
193
209
  )}`,
194
210
  callback_data: "settings:open:voice-reply",
195
211
  },
196
212
  ],
197
213
  [
198
214
  {
199
- text: `🕒 Time injection: ${timeInjectionMode}`,
215
+ text: `🕒 Time injection: ${getTelegramSettingsStateValueLabel(timeInjectionMode)}`,
200
216
  callback_data: "settings:open:time-injection",
201
217
  },
202
218
  ],
203
219
  [
204
220
  {
205
- text: `📌 Proactive push: ${proactivePushEnabled ? "on" : "off"}`,
221
+ text: `📌 Proactive push: ${proactivePushEnabled ? "On" : "Off"}`,
206
222
  callback_data: "settings:open:proactive",
207
223
  },
208
224
  ],
@@ -242,11 +258,11 @@ export function buildProactivePushSettingsReplyMarkup(
242
258
  [{ text: "⬆️ Back", callback_data: "settings:list" }],
243
259
  [
244
260
  {
245
- text: proactivePushEnabled ? "🟢 on" : "⚫️ on",
261
+ text: proactivePushEnabled ? "🟢 On" : "⚫️ On",
246
262
  callback_data: "settings:set:proactive:on",
247
263
  },
248
264
  {
249
- text: proactivePushEnabled ? "⚫️ off" : "🟡 off",
265
+ text: proactivePushEnabled ? "⚫️ Off" : "🟡 Off",
250
266
  callback_data: "settings:set:proactive:off",
251
267
  },
252
268
  ],
@@ -257,7 +273,7 @@ export function buildProactivePushSettingsReplyMarkup(
257
273
  export function buildTimeInjectionModeSettingsReplyMarkup(
258
274
  mode: TelegramTimeMode,
259
275
  ): TelegramSettingsMenuReplyMarkup {
260
- const modes: TelegramTimeMode[] = ["off", "always", "interval"];
276
+ const modes: TelegramTimeMode[] = ["hidden", "always", "interval"];
261
277
  return {
262
278
  inline_keyboard: [
263
279
  [{ text: "⬆️ Back", callback_data: "settings:list" }],
@@ -314,30 +330,31 @@ export async function updateTelegramSettingsMenuMessage(
314
330
  export async function updateProactivePushSettingsMessage(
315
331
  deps: TelegramSettingsMenuCallbackDeps,
316
332
  ): Promise<void> {
333
+ const proactivePushEnabled = deps.isProactivePushEnabled();
317
334
  await deps.updateSettingsMessage(
318
- buildProactivePushSettingsText(),
319
- buildProactivePushSettingsReplyMarkup(deps.isProactivePushEnabled()),
335
+ buildProactivePushSettingsText(proactivePushEnabled),
336
+ buildProactivePushSettingsReplyMarkup(proactivePushEnabled),
320
337
  );
321
338
  }
322
339
 
323
340
  export async function updateTimeInjectionModeSettingsMessage(
324
341
  deps: TelegramSettingsMenuCallbackDeps,
325
342
  ): Promise<void> {
343
+ const mode = deps.getTimeInjectionMode();
326
344
  await deps.updateSettingsMessage(
327
- buildTimeInjectionModeSettingsText(),
328
- buildTimeInjectionModeSettingsReplyMarkup(deps.getTimeInjectionMode()),
345
+ buildTimeInjectionModeSettingsText(mode),
346
+ buildTimeInjectionModeSettingsReplyMarkup(mode),
329
347
  );
330
348
  }
331
349
 
332
350
  export async function updateVoiceReplyModeSettingsMessage(
333
351
  deps: TelegramSettingsMenuCallbackDeps,
334
352
  ): Promise<void> {
353
+ const mode = deps.getVoiceReplyMode();
354
+ const configured = deps.isVoiceReplyModeConfigured();
335
355
  await deps.updateSettingsMessage(
336
- buildVoiceReplyModeSettingsText(),
337
- buildVoiceReplyModeSettingsReplyMarkup(
338
- deps.getVoiceReplyMode(),
339
- deps.isVoiceReplyModeConfigured(),
340
- ),
356
+ buildVoiceReplyModeSettingsText(mode, configured),
357
+ buildVoiceReplyModeSettingsReplyMarkup(mode, configured),
341
358
  );
342
359
  }
343
360
 
@@ -362,7 +379,10 @@ export async function handleTelegramSettingsMenuCallbackAction(
362
379
  await deps.answerCallbackQuery(callbackQueryId);
363
380
  return true;
364
381
  }
365
- if (data === "settings:open:time-injection" || data === "settings:open:time") {
382
+ if (
383
+ data === "settings:open:time-injection" ||
384
+ data === "settings:open:time"
385
+ ) {
366
386
  await updateTimeInjectionModeSettingsMessage(deps);
367
387
  await deps.answerCallbackQuery(callbackQueryId);
368
388
  return true;
@@ -391,10 +411,18 @@ export async function handleTelegramSettingsMenuCallbackAction(
391
411
  const mode = data.startsWith("settings:set:time-injection:")
392
412
  ? data.slice("settings:set:time-injection:".length)
393
413
  : data.slice("settings:set:time:".length);
394
- if (mode === "off" || mode === "always" || mode === "interval") {
395
- await deps.setTimeInjectionMode(mode);
414
+ const normalizedMode = mode === "off" ? "hidden" : mode;
415
+ if (
416
+ normalizedMode === "hidden" ||
417
+ normalizedMode === "always" ||
418
+ normalizedMode === "interval"
419
+ ) {
420
+ await deps.setTimeInjectionMode(normalizedMode);
396
421
  await updateTimeInjectionModeSettingsMessage(deps);
397
- await deps.answerCallbackQuery(callbackQueryId, `Time injection: ${mode}`);
422
+ await deps.answerCallbackQuery(
423
+ callbackQueryId,
424
+ `Time injection: ${normalizedMode}`,
425
+ );
398
426
  return true;
399
427
  }
400
428
  }
@@ -488,15 +516,18 @@ export function createTelegramSettingsMenuRuntime<
488
516
  ? query.data.slice("settings:set:time-injection:".length)
489
517
  : query.data.slice("settings:set:time:".length);
490
518
  if (
491
- (hasTimeInjectionPrefix || query.data.startsWith("settings:set:time:")) &&
519
+ (hasTimeInjectionPrefix ||
520
+ query.data.startsWith("settings:set:time:")) &&
492
521
  (timeMode === "off" ||
522
+ timeMode === "hidden" ||
493
523
  timeMode === "always" ||
494
524
  timeMode === "interval")
495
525
  ) {
496
- await deps.setTimeInjectionMode(timeMode);
526
+ const normalizedMode = timeMode === "off" ? "hidden" : timeMode;
527
+ await deps.setTimeInjectionMode(normalizedMode);
497
528
  await deps.answerCallbackQuery(
498
529
  query.id,
499
- `Time injection: ${timeMode}`,
530
+ `Time injection: ${normalizedMode}`,
500
531
  );
501
532
  return true;
502
533
  }
@@ -8,7 +8,7 @@ import { formatTelegramCommandEmojiPrefix } from "./commands.ts";
8
8
  import {
9
9
  getTelegramSectionMainMenuRows,
10
10
  type TelegramSectionRegistry,
11
- } from "./extension-sections.ts";
11
+ } from "./sections.ts";
12
12
  import {
13
13
  formatStatusButtonLabel,
14
14
  type TelegramMenuMessageRuntimeDeps,
package/lib/menu.ts CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  handleTelegramSectionSettingsOpen,
11
11
  parseTelegramSectionCallback,
12
12
  type TelegramSectionRegistry,
13
- } from "./extension-sections.ts";
13
+ } from "./sections.ts";
14
14
  import {
15
15
  createTelegramModelMenuStateBuilder,
16
16
  handleTelegramModelMenuCallbackAction,
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Telegram outbound handler helpers
2
+ * Telegram outbound surface helpers
3
3
  * Zones: telegram outbound, assistant markup, command templates, callback routing
4
- * Owns assistant-authored outbound markup extraction, configured artifact generation, callback actions, and Telegram outbound delivery
4
+ * Owns assistant-authored outbound markup extraction, configured artifact generation, callback actions, runtime-event bridge, and Telegram outbound delivery
5
5
  */
6
6
 
7
7
  import { randomUUID } from "node:crypto";
@@ -61,6 +61,18 @@ const DEFAULT_VOICE_TIMEOUT_MS = 120_000;
61
61
  * diagnostics alongside pi-telegram's own events. Events are silently dropped
62
62
  * when pi-telegram is not loaded.
63
63
  */
64
+ export type TelegramRuntimeEventRecorder = (
65
+ category: string,
66
+ error: unknown,
67
+ details?: Record<string, unknown>,
68
+ ) => void;
69
+
70
+ export function bindTelegramRuntimeEventRecorder(
71
+ recorder: TelegramRuntimeEventRecorder,
72
+ ): void {
73
+ (globalThis as Record<string, unknown>)[VOICE_EVENT_RECORDER_KEY] = recorder;
74
+ }
75
+
64
76
  export function recordTelegramRuntimeEvent(
65
77
  category: string,
66
78
  error: unknown,
@@ -70,13 +82,7 @@ export function recordTelegramRuntimeEvent(
70
82
  VOICE_EVENT_RECORDER_KEY
71
83
  ];
72
84
  if (typeof recorder === "function") {
73
- (
74
- recorder as (
75
- category: string,
76
- error: unknown,
77
- details?: Record<string, unknown>,
78
- ) => void
79
- )(category, error, details);
85
+ (recorder as TelegramRuntimeEventRecorder)(category, error, details);
80
86
  }
81
87
  }
82
88
 
@@ -1028,7 +1034,9 @@ export function createTelegramVoiceReplySender(
1028
1034
  });
1029
1035
  return;
1030
1036
  } catch (error) {
1031
- deps.recordRuntimeEvent?.("voice", error, { phase: "template-handler-send" });
1037
+ deps.recordRuntimeEvent?.("voice", error, {
1038
+ phase: "template-handler-send",
1039
+ });
1032
1040
  }
1033
1041
  }
1034
1042
 
@@ -1045,7 +1053,9 @@ export function createTelegramVoiceReplySender(
1045
1053
  });
1046
1054
  return;
1047
1055
  } catch (error) {
1048
- deps.recordRuntimeEvent?.("voice", error, { phase: "programmatic-handler-send" });
1056
+ deps.recordRuntimeEvent?.("voice", error, {
1057
+ phase: "programmatic-handler-send",
1058
+ });
1049
1059
  }
1050
1060
  }
1051
1061
 
package/lib/pi.ts CHANGED
@@ -11,6 +11,8 @@ import {
11
11
  type ExtensionAPI,
12
12
  type ExtensionCommandContext,
13
13
  type ExtensionContext,
14
+ type SessionBeforeCompactEvent,
15
+ type SessionCompactEvent,
14
16
  type SessionShutdownEvent,
15
17
  type SessionStartEvent,
16
18
  type SlashCommandInfo,
@@ -24,6 +26,8 @@ export type {
24
26
  ExtensionAPI,
25
27
  ExtensionCommandContext,
26
28
  ExtensionContext,
29
+ SessionBeforeCompactEvent,
30
+ SessionCompactEvent,
27
31
  SessionShutdownEvent,
28
32
  SessionStartEvent,
29
33
  SlashCommandInfo,
package/lib/polling.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Telegram polling domain helpers
2
+ * Telegram polling runtime domain helpers
3
3
  * Zones: telegram transport, polling runtime
4
4
  * Owns polling request builders, stop conditions, and the long-poll loop runtime for Telegram updates
5
5
  */
@@ -91,8 +91,9 @@ export function createTelegramPollingActivityReader(
91
91
  return () => isTelegramPollingControllerActive(state);
92
92
  }
93
93
 
94
- export interface TelegramPollingRuntimeDeps<TContext>
95
- extends TelegramRuntimeEventRecorderPort {
94
+ export interface TelegramPollingRuntimeDeps<
95
+ TContext,
96
+ > extends TelegramRuntimeEventRecorderPort {
96
97
  hasBotToken: () => boolean;
97
98
  getPollingPromise: () => Promise<void> | undefined;
98
99
  setPollingPromise: (promise: Promise<void> | undefined) => void;
package/lib/preview.ts CHANGED
@@ -22,7 +22,7 @@ import {
22
22
  } from "./rendering.ts";
23
23
 
24
24
  import { buildTelegramReplyParameters } from "./replies.ts";
25
- import { stripTelegramCommentMarkupForPreview } from "./outbound-handlers.ts";
25
+ import { stripTelegramCommentMarkupForPreview } from "./outbound.ts";
26
26
  import { shouldSuppressPreviewForVoice } from "./voice.ts";
27
27
 
28
28
  const TELEGRAM_PREVIEW_THROTTLE_MS = 750;
package/lib/routing.ts CHANGED
@@ -7,12 +7,12 @@
7
7
  import { readFile } from "node:fs/promises";
8
8
  import * as Commands from "./commands.ts";
9
9
  import type { TelegramConfigStore } from "./config.ts";
10
- import type { TelegramSectionRegistry } from "./extension-sections.ts";
11
- import type { TelegramInboundHandlerRuntime } from "./inbound-handlers.ts";
10
+ import type { TelegramSectionRegistry } from "./sections.ts";
11
+ import type { TelegramInboundHandlerRuntime } from "./inbound.ts";
12
12
  import * as Media from "./media.ts";
13
13
  import * as Menu from "./menu.ts";
14
14
  import * as Model from "./model.ts";
15
- import * as OutboundHandlers from "./outbound-handlers.ts";
15
+ import * as OutboundHandlers from "./outbound.ts";
16
16
  import * as PromptTemplates from "./prompt-templates.ts";
17
17
  import * as Queue from "./queue.ts";
18
18
  import type { TelegramBridgeRuntime } from "./runtime.ts";
@@ -138,6 +138,7 @@ export interface TelegramInboundRouteRuntimeDeps<
138
138
  }
139
139
 
140
140
  const TELEGRAM_OWNED_CALLBACK_PREFIXES = [
141
+ "compact:",
141
142
  "menu:",
142
143
  "model:",
143
144
  "queue:",
@@ -262,6 +263,45 @@ export function createTelegramInboundRouteRuntime<
262
263
  );
263
264
  if (handled) return;
264
265
  }
266
+ const handledByCompact =
267
+ await Commands.handleTelegramCompactConfirmationCallback(query, {
268
+ ctx,
269
+ answerCallbackQuery: deps.answerCallbackQuery,
270
+ editInteractiveMessage: deps.editInteractiveMessage ?? (async () => {}),
271
+ runCompact: async (compactCtx, chatId, replyToMessageId) => {
272
+ await Commands.handleTelegramCompactCommand({
273
+ isIdle: () => deps.isIdle(compactCtx),
274
+ hasPendingMessages: () => deps.hasPendingMessages(compactCtx),
275
+ hasActiveTelegramTurn: deps.activeTurnRuntime.has,
276
+ hasDispatchPending: deps.bridgeRuntime.lifecycle.hasDispatchPending,
277
+ hasQueuedTelegramItems: deps.telegramQueueStore.hasQueuedItems,
278
+ isCompactionInProgress:
279
+ deps.bridgeRuntime.lifecycle.isCompactionInProgress,
280
+ setCompactionInProgress:
281
+ deps.bridgeRuntime.lifecycle.setCompactionInProgress,
282
+ updateStatus: () => deps.updateStatus(compactCtx),
283
+ dispatchNextQueuedTelegramTurn: () =>
284
+ deps.dispatchNextQueuedTelegramTurn(compactCtx),
285
+ requestDeferredDispatchNextQueuedTelegramTurn:
286
+ deps.requestDeferredDispatchNextQueuedTelegramTurn
287
+ ? (dispatch) =>
288
+ deps.requestDeferredDispatchNextQueuedTelegramTurn?.(() =>
289
+ dispatch(),
290
+ )
291
+ : undefined,
292
+ compact: (callbacks) => deps.compact(compactCtx, callbacks),
293
+ startTypingLoop: deps.startTypingLoop
294
+ ? () => deps.startTypingLoop?.(compactCtx, chatId)
295
+ : undefined,
296
+ stopTypingLoop: deps.stopTypingLoop,
297
+ sendTextReply: (text) =>
298
+ deps.sendTextReply(chatId, replyToMessageId, text).then(() => {}),
299
+ suppressStartNotice: true,
300
+ recordRuntimeEvent: deps.recordRuntimeEvent,
301
+ });
302
+ },
303
+ });
304
+ if (handledByCompact) return;
265
305
  const handledByQueue = await deps.queueMenuCallbackHandler(query, ctx);
266
306
  if (handledByQueue) return;
267
307
  const handledBySettings = await deps.settingsMenuCallbackHandler?.(
@@ -302,28 +342,19 @@ export function createTelegramInboundRouteRuntime<
302
342
  message: TMessage,
303
343
  ctx: TContext,
304
344
  ): Promise<void> => {
305
- const enqueuePlan = Queue.planTelegramPromptEnqueue(
306
- deps.telegramQueueStore.getQueuedItems(),
307
- deps.bridgeRuntime.lifecycle.shouldPreserveQueuedTurnsAsHistory(),
308
- );
309
345
  deps.bridgeRuntime.lifecycle.setPreserveQueuedTurnsAsHistory(false);
310
346
  const continueMessage = {
311
347
  ...message,
312
348
  text: "continue",
313
349
  caption: undefined,
314
350
  } as TMessage;
315
- const turn = await promptTurnBuilder(
316
- [continueMessage],
317
- enqueuePlan.historyTurns,
318
- ctx,
319
- );
351
+ const turn = await promptTurnBuilder([continueMessage], [], ctx);
320
352
  const continueTurn = {
321
353
  ...turn,
322
354
  queueLane: "priority" as const,
323
355
  laneOrder: Number.MIN_SAFE_INTEGER + turn.queueOrder,
324
356
  statusSummary: "continue",
325
357
  };
326
- deps.telegramQueueStore.setQueuedItems(enqueuePlan.remainingItems);
327
358
  deps.queueMutationRuntime.append(continueTurn, ctx);
328
359
  deps.dispatchNextQueuedTelegramTurn(ctx);
329
360
  };
@@ -381,6 +412,7 @@ export function createTelegramInboundRouteRuntime<
381
412
  getPromptTemplateCommands,
382
413
  persistConfig: deps.configStore.persist,
383
414
  sendTextReply: deps.sendTextReply,
415
+ sendInteractiveMessage: deps.sendInteractiveMessage,
384
416
  recordRuntimeEvent: deps.recordRuntimeEvent,
385
417
  });
386
418
  const promptEnqueue = Queue.createTelegramPromptEnqueueController<
@@ -1,12 +1,13 @@
1
1
  /**
2
2
  * Telegram Extension Sections registry and callback routing
3
3
  * Zones: telegram ui, extension platform, callback routing
4
- * Owns section registration, token mapping, main-menu/settings row injection, and section callback dispatch
4
+ * Owns section registration, global registry binding, token mapping, main-menu/settings row injection, and section callback dispatch
5
5
  */
6
6
 
7
7
  import type { TelegramInlineKeyboardMarkup } from "./keyboard.ts";
8
8
 
9
9
  const SECTION_REGISTRY_KEY = "__piTelegramSectionRegistry__";
10
+ const TELEGRAM_CALLBACK_DATA_MAX_BYTES = 64;
10
11
 
11
12
  // --- Core Types ---
12
13
 
@@ -181,9 +182,7 @@ function buildTelegramSectionContext(
181
182
  .then(() => {}),
182
183
  enqueuePrompt: deps.enqueuePrompt,
183
184
  callbackData: (action, payload) =>
184
- payload
185
- ? `section:${token}:${action}:${payload}`
186
- : `section:${token}:${action}`,
185
+ buildTelegramSectionCallbackData(token, action, payload),
187
186
  deleteMessage: () =>
188
187
  messageId !== undefined
189
188
  ? deps.deleteMessage(chatId, messageId)
@@ -231,9 +230,7 @@ function buildTelegramSectionCallbackContext(
231
230
  .then(() => {}),
232
231
  enqueuePrompt: deps.enqueuePrompt,
233
232
  callbackData: (action, payload) =>
234
- payload
235
- ? `section:${token}:${action}:${payload}`
236
- : `section:${token}:${action}`,
233
+ buildTelegramSectionCallbackData(token, action, payload),
237
234
  deleteMessage: () =>
238
235
  messageId !== undefined
239
236
  ? deps.deleteMessage(chatId, messageId)
@@ -250,6 +247,13 @@ export function setGlobalTelegramSectionRegistry(
250
247
  (globalThis as Record<string, unknown>)[SECTION_REGISTRY_KEY] = registry;
251
248
  }
252
249
 
250
+ /** @internal */
251
+ export function createAndBindTelegramSectionRegistry(): TelegramSectionRegistry {
252
+ const registry = createTelegramExtensionSectionRegistry();
253
+ setGlobalTelegramSectionRegistry(registry);
254
+ return registry;
255
+ }
256
+
253
257
  /**
254
258
  * Register a Telegram Extension Section from any pi extension.
255
259
  * Returns a disposer. Throws if no section registry is active.
@@ -271,8 +275,8 @@ export function registerTelegramSection(
271
275
 
272
276
  /**
273
277
  * Get current section diagnostics. Returns empty array when registry is absent.
278
+ * @internal
274
279
  */
275
- /** @internal */
276
280
  export function getTelegramSectionDiagnostics(): TelegramSectionDiagnostic[] {
277
281
  const registry = (globalThis as Record<string, unknown>)[
278
282
  SECTION_REGISTRY_KEY
@@ -291,6 +295,27 @@ const BACK_NAV_ROW = {
291
295
  text: "⬆️ Back",
292
296
  } as const;
293
297
 
298
+ function getUtf8ByteLength(value: string): number {
299
+ return new TextEncoder().encode(value).byteLength;
300
+ }
301
+
302
+ function buildTelegramSectionCallbackData(
303
+ token: TelegramSectionToken,
304
+ action: string,
305
+ payload?: string,
306
+ ): string {
307
+ const data = payload
308
+ ? `section:${token}:${action}:${payload}`
309
+ : `section:${token}:${action}`;
310
+ const byteLength = getUtf8ByteLength(data);
311
+ if (byteLength > TELEGRAM_CALLBACK_DATA_MAX_BYTES) {
312
+ throw new Error(
313
+ `Telegram section callback_data exceeds ${TELEGRAM_CALLBACK_DATA_MAX_BYTES} bytes (${byteLength}). Use a shorter action/payload or store state behind a compact key.`,
314
+ );
315
+ }
316
+ return data;
317
+ }
318
+
294
319
  function prependBackRow(
295
320
  replyMarkup: TelegramInlineKeyboardMarkup | undefined,
296
321
  backCallback: string,
@@ -319,6 +344,10 @@ export function createTelegramExtensionSectionRegistry(): TelegramSectionRegistr
319
344
  let nextToken = 0;
320
345
 
321
346
  function register(section: TelegramSectionRegistration): () => void {
347
+ const duplicate = [...sections.values()].find((s) => s.id === section.id);
348
+ if (duplicate) {
349
+ throw new Error(`Telegram section id already registered: ${section.id}`);
350
+ }
322
351
  const token = String(nextToken++);
323
352
  const registered: RegisteredTelegramSection = {
324
353
  id: section.id,
@@ -52,7 +52,7 @@ export function createTimeInjectionRuntime(
52
52
  return {
53
53
  resolveLine: (chatId, now = new Date()) => {
54
54
  const config = deps.getConfig();
55
- if (config.injectionMode === "off") return null;
55
+ if (config.injectionMode === "hidden") return null;
56
56
  let line: string;
57
57
  try {
58
58
  line = formatTelegramTimeInjectionLine(now, config.timezone);