@llblab/pi-telegram 0.52.2 → 0.53.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 (204) hide show
  1. package/AGENTS.md +9 -7
  2. package/BACKLOG.md +1 -18
  3. package/CHANGELOG.md +15 -0
  4. package/README.md +7 -11
  5. package/api/commands.ts +3 -0
  6. package/dist/api/commands.d.ts +1 -1
  7. package/dist/lib/activity-verbosity.d.ts +1 -5
  8. package/dist/lib/activity-verbosity.js +15 -42
  9. package/dist/lib/activity.d.ts +9 -0
  10. package/dist/lib/activity.js +39 -10
  11. package/dist/lib/agent-messages.js +7 -3
  12. package/dist/lib/bindings.d.ts +43 -5
  13. package/dist/lib/bindings.js +163 -20
  14. package/dist/lib/bus-api.d.ts +12 -0
  15. package/dist/lib/bus-api.js +182 -79
  16. package/dist/lib/bus-follower.d.ts +111 -31
  17. package/dist/lib/bus-follower.js +1324 -359
  18. package/dist/lib/bus-leader.d.ts +41 -91
  19. package/dist/lib/bus-leader.js +969 -637
  20. package/dist/lib/bus-transport.js +16 -9
  21. package/dist/lib/bus.d.ts +215 -18
  22. package/dist/lib/bus.js +1048 -327
  23. package/dist/lib/channel-posts.d.ts +5 -0
  24. package/dist/lib/channel-posts.js +434 -261
  25. package/dist/lib/command-templates.d.ts +26 -5
  26. package/dist/lib/command-templates.js +92 -176
  27. package/dist/lib/commands.d.ts +168 -82
  28. package/dist/lib/commands.js +1409 -291
  29. package/dist/lib/config.d.ts +1 -8
  30. package/dist/lib/config.js +34 -76
  31. package/dist/lib/delivery.d.ts +2 -2
  32. package/dist/lib/delivery.js +16 -16
  33. package/dist/lib/extension.js +639 -171
  34. package/dist/lib/generative-app-worker.mjs +19 -7
  35. package/dist/lib/generative-apps.d.ts +0 -5
  36. package/dist/lib/generative-apps.js +61 -49
  37. package/dist/lib/inbound.d.ts +5 -0
  38. package/dist/lib/inbound.js +35 -59
  39. package/dist/lib/journal.d.ts +33 -3
  40. package/dist/lib/journal.js +2063 -782
  41. package/dist/lib/lifecycle.d.ts +2 -6
  42. package/dist/lib/lifecycle.js +75 -58
  43. package/dist/lib/locks.d.ts +29 -3
  44. package/dist/lib/locks.js +289 -108
  45. package/dist/lib/logging.js +50 -14
  46. package/dist/lib/media.d.ts +22 -1
  47. package/dist/lib/media.js +46 -28
  48. package/dist/lib/menu-model.d.ts +19 -5
  49. package/dist/lib/menu-model.js +53 -11
  50. package/dist/lib/menu-queue.d.ts +5 -4
  51. package/dist/lib/menu-queue.js +45 -63
  52. package/dist/lib/menu-settings.d.ts +5 -4
  53. package/dist/lib/menu-settings.js +114 -78
  54. package/dist/lib/menu-status.d.ts +1 -1
  55. package/dist/lib/menu-status.js +8 -25
  56. package/dist/lib/menu-thinking.d.ts +6 -1
  57. package/dist/lib/menu-thinking.js +14 -21
  58. package/dist/lib/menu.d.ts +8 -61
  59. package/dist/lib/menu.js +92 -164
  60. package/dist/lib/model.d.ts +1 -1
  61. package/dist/lib/model.js +2 -16
  62. package/dist/lib/outbound-attachments.d.ts +6 -27
  63. package/dist/lib/outbound-attachments.js +45 -38
  64. package/dist/lib/outbound-buttons.js +22 -16
  65. package/dist/lib/outbound-markup.d.ts +0 -2
  66. package/dist/lib/outbound-markup.js +51 -64
  67. package/dist/lib/outbound-voice.js +6 -3
  68. package/dist/lib/outbound.d.ts +6 -6
  69. package/dist/lib/outbound.js +65 -51
  70. package/dist/lib/ownership.js +6 -2
  71. package/dist/lib/paths.d.ts +5 -0
  72. package/dist/lib/paths.js +37 -21
  73. package/dist/lib/pi.d.ts +2 -0
  74. package/dist/lib/pi.js +8 -1
  75. package/dist/lib/polling.d.ts +21 -37
  76. package/dist/lib/polling.js +59 -50
  77. package/dist/lib/preview.js +39 -19
  78. package/dist/lib/process-identity.d.ts +24 -0
  79. package/dist/lib/process-identity.js +133 -0
  80. package/dist/lib/prompts.js +2 -7
  81. package/dist/lib/queue.d.ts +14 -16
  82. package/dist/lib/queue.js +252 -127
  83. package/dist/lib/recovery.d.ts +2 -83
  84. package/dist/lib/recovery.js +20 -274
  85. package/dist/lib/replies.d.ts +16 -24
  86. package/dist/lib/replies.js +99 -116
  87. package/dist/lib/routing.d.ts +120 -7
  88. package/dist/lib/routing.js +4667 -1152
  89. package/dist/lib/runtime.d.ts +1 -11
  90. package/dist/lib/sections.d.ts +4 -29
  91. package/dist/lib/sections.js +7 -20
  92. package/dist/lib/skills.js +4 -3
  93. package/dist/lib/status.js +86 -57
  94. package/dist/lib/sync.d.ts +1 -3
  95. package/dist/lib/sync.js +67 -43
  96. package/dist/lib/target.d.ts +4 -0
  97. package/dist/lib/target.js +18 -0
  98. package/dist/lib/telegram-api.d.ts +18 -13
  99. package/dist/lib/telegram-api.js +148 -53
  100. package/dist/lib/text-groups.js +4 -39
  101. package/dist/lib/thread-cleanup-manager.d.ts +6 -27
  102. package/dist/lib/thread-cleanup-manager.js +369 -186
  103. package/dist/lib/thread-display.d.ts +1 -2
  104. package/dist/lib/thread-display.js +58 -38
  105. package/dist/lib/thread-naming.d.ts +28 -8
  106. package/dist/lib/thread-naming.js +175 -66
  107. package/dist/lib/thread-reconciler.d.ts +42 -16
  108. package/dist/lib/thread-reconciler.js +90 -50
  109. package/dist/lib/threads.d.ts +82 -20
  110. package/dist/lib/threads.js +2247 -855
  111. package/dist/lib/time-injection.js +1 -2
  112. package/dist/lib/turns.js +1 -19
  113. package/dist/lib/updates.d.ts +169 -80
  114. package/dist/lib/updates.js +2418 -641
  115. package/dist/lib/voice.d.ts +7 -22
  116. package/dist/lib/voice.js +7 -22
  117. package/dist/lib/wire.js +1 -1
  118. package/dist/lib/workspace-admission.d.ts +3 -1
  119. package/dist/lib/workspace-admission.js +425 -211
  120. package/dist/lib/workspace-identity.js +8 -13
  121. package/dist/lib/workspace-retirement.d.ts +1 -1
  122. package/dist/lib/workspace-retirement.js +328 -140
  123. package/dist/lib/workspace-slots.js +8 -4
  124. package/dist/package.json +1 -1
  125. package/docs/architecture.md +31 -29
  126. package/docs/inbound.md +1 -1
  127. package/docs/multi-instance-bus.md +15 -3
  128. package/docs/outbound.md +1 -1
  129. package/docs/public-api.md +29 -1
  130. package/docs/ui-style.md +25 -8
  131. package/lib/activity-verbosity.ts +59 -82
  132. package/lib/activity.ts +146 -102
  133. package/lib/agent-messages.ts +12 -5
  134. package/lib/bindings.ts +389 -82
  135. package/lib/bus-api.ts +263 -102
  136. package/lib/bus-follower.ts +2440 -601
  137. package/lib/bus-leader.ts +2209 -1290
  138. package/lib/bus-transport.ts +46 -12
  139. package/lib/bus.ts +1673 -444
  140. package/lib/channel-posts.ts +848 -327
  141. package/lib/command-templates.ts +152 -265
  142. package/lib/commands.ts +2398 -555
  143. package/lib/config.ts +177 -170
  144. package/lib/delivery.ts +41 -29
  145. package/lib/extension.ts +1206 -433
  146. package/lib/generative-app-worker.mjs +19 -7
  147. package/lib/generative-apps.ts +324 -211
  148. package/lib/inbound.ts +95 -97
  149. package/lib/journal.ts +5311 -1763
  150. package/lib/keyboard.ts +1 -3
  151. package/lib/lifecycle.ts +128 -78
  152. package/lib/locks.ts +677 -203
  153. package/lib/logging.ts +147 -58
  154. package/lib/media.ts +82 -43
  155. package/lib/menu-model.ts +85 -18
  156. package/lib/menu-queue.ts +88 -129
  157. package/lib/menu-settings.ts +260 -155
  158. package/lib/menu-status.ts +20 -60
  159. package/lib/menu-thinking.ts +41 -56
  160. package/lib/menu.ts +112 -285
  161. package/lib/model.ts +8 -22
  162. package/lib/outbound-attachments.ts +156 -125
  163. package/lib/outbound-buttons.ts +76 -49
  164. package/lib/outbound-markup.ts +96 -94
  165. package/lib/outbound-voice.ts +33 -27
  166. package/lib/outbound.ts +128 -125
  167. package/lib/ownership.ts +19 -10
  168. package/lib/paths.ts +148 -49
  169. package/lib/pi.ts +35 -15
  170. package/lib/polling.ts +144 -147
  171. package/lib/preview.ts +93 -40
  172. package/lib/process-identity.ts +164 -0
  173. package/lib/prompts.ts +4 -11
  174. package/lib/queue.ts +516 -301
  175. package/lib/recovery.ts +50 -454
  176. package/lib/rendering.ts +1 -3
  177. package/lib/replies.ts +248 -209
  178. package/lib/routing.ts +8509 -2186
  179. package/lib/runtime.ts +1 -12
  180. package/lib/sections.ts +24 -89
  181. package/lib/setup.ts +1 -3
  182. package/lib/skills.ts +9 -4
  183. package/lib/status.ts +215 -110
  184. package/lib/sync.ts +448 -361
  185. package/lib/target.ts +22 -3
  186. package/lib/telegram-api.ts +302 -127
  187. package/lib/text-groups.ts +5 -39
  188. package/lib/thread-cleanup-manager.ts +833 -331
  189. package/lib/thread-display.ts +213 -97
  190. package/lib/thread-naming.ts +232 -87
  191. package/lib/thread-reconciler.ts +155 -72
  192. package/lib/threads.ts +5318 -1845
  193. package/lib/time-injection.ts +1 -4
  194. package/lib/turns.ts +15 -28
  195. package/lib/updates.ts +5184 -1612
  196. package/lib/voice.ts +7 -35
  197. package/lib/wire.ts +1 -1
  198. package/lib/workspace-admission.ts +996 -416
  199. package/lib/workspace-identity.ts +14 -17
  200. package/lib/workspace-retirement.ts +878 -376
  201. package/lib/workspace-slots.ts +22 -13
  202. package/package.json +1 -1
  203. package/scripts/check-downgrade.mjs +0 -472
  204. package/scripts/measure-workspace.mjs +0 -108
@@ -7,16 +7,21 @@
7
7
  import type {
8
8
  TelegramActivityVerbosity,
9
9
  TelegramAssistantRenderingMode,
10
- TelegramTimeMode,
11
10
  TelegramThreadDisplayMode,
11
+ TelegramTimeMode,
12
12
  } from "./config.ts";
13
13
  import type { TelegramInlineKeyboardMarkup } from "./keyboard.ts";
14
- import type { TelegramModelMenuState } from "./menu-model.ts";
14
+ import {
15
+ createTelegramMenuDelivery,
16
+ type TelegramMenuMessageRuntimeDeps,
17
+ type TelegramModelMenuState,
18
+ } from "./menu-model.ts";
15
19
  import type { MenuModel } from "./model.ts";
16
20
  import {
17
21
  getTelegramExtensionSettingsRows,
18
22
  type TelegramSectionRegistry,
19
23
  } from "./sections.ts";
24
+ import type { TelegramApiCallOptions } from "./telegram-api.ts";
20
25
  import type { TelegramVoiceReplyMode } from "./voice.ts";
21
26
 
22
27
  export type TelegramSettingsMenuReplyMarkup = TelegramInlineKeyboardMarkup;
@@ -39,18 +44,22 @@ export interface TelegramSettingsMutationDeps extends TelegramSettingsStateDeps
39
44
  setAssistantRenderingMode: (
40
45
  mode: TelegramAssistantRenderingMode,
41
46
  ) => Promise<void>;
42
- setActivityVerbosity: (
43
- verbosity: TelegramActivityVerbosity,
44
- ) => Promise<void>;
47
+ setActivityVerbosity: (verbosity: TelegramActivityVerbosity) => Promise<void>;
45
48
  setVoiceReplyMode: (
46
49
  mode: TelegramVoiceReplyMode | undefined,
47
50
  ) => Promise<void>;
48
51
  setTimeInjectionMode: (mode: TelegramTimeMode) => Promise<void>;
49
52
  setAutomaticThreadCleanupEnabled: (enabled: boolean) => Promise<void>;
50
- reviewInactiveThreads?: () => Promise<{ count: number; operationId?: string }>;
53
+ reviewInactiveThreads?: () => Promise<{
54
+ count: number;
55
+ operationId?: string;
56
+ }>;
51
57
  cleanInactiveThreads?: (operationId: string) => Promise<{
52
- deleted: number; outcomeUnknown: number; blocked?: number;
53
- recovery?: "commit-ready" | "deletion-outcome-unknown" | "authority-blocked";
58
+ deleted: number;
59
+ outcomeUnknown: number;
60
+ blocked?: number;
61
+ recovery?:
62
+ "commit-ready" | "deletion-outcome-unknown" | "authority-blocked";
54
63
  }>;
55
64
  }
56
65
 
@@ -83,6 +92,8 @@ export interface TelegramSettingsMenuRuntime<TContext> {
83
92
  chatId: number,
84
93
  replyToMessageId: number,
85
94
  ctx: TContext,
95
+ threadId?: number,
96
+ options?: Pick<TelegramApiCallOptions, "assertAuthority">,
86
97
  ) => Promise<void>;
87
98
  handleCallbackQuery: (
88
99
  query: {
@@ -131,12 +142,7 @@ export interface TelegramSettingsMenuRuntimeDeps<
131
142
  mode: "markdown" | "html" | "plain",
132
143
  replyMarkup: TelegramSettingsMenuReplyMarkup,
133
144
  ) => Promise<void>;
134
- sendInteractiveMessage: (
135
- chatId: number,
136
- text: string,
137
- mode: "markdown" | "html" | "plain",
138
- replyMarkup: TelegramSettingsMenuReplyMarkup,
139
- ) => Promise<number | undefined>;
145
+ sendInteractiveMessage: TelegramMenuMessageRuntimeDeps["sendInteractiveMessage"];
140
146
  answerCallbackQuery: (
141
147
  callbackQueryId: string,
142
148
  text?: string,
@@ -144,16 +150,12 @@ export interface TelegramSettingsMenuRuntimeDeps<
144
150
  }
145
151
 
146
152
  const SETTINGS_MENU_TITLE = "<b>⚙️ Settings:</b>";
147
- const AUTOMATIC_THREAD_CLEANUP_SETTINGS_TITLE =
148
- "<b>🧹 Thread cleanup:</b>";
153
+ const AUTOMATIC_THREAD_CLEANUP_SETTINGS_TITLE = "<b>🧹 Thread cleanup:</b>";
149
154
  const INACTIVE_THREAD_REVIEW_TITLE = "<b>🔎 Inactive tabs review:</b>";
150
155
  const DRAFT_PREVIEWS_SETTINGS_TITLE = "<b>📝 Draft previews:</b>";
151
- const ASSISTANT_RENDERING_SETTINGS_TITLE =
152
- "<b>🧾 Assistant rendering:</b>";
153
- const ACTIVITY_VERBOSITY_SETTINGS_TITLE =
154
- "<b>🔬 Activity:</b>";
155
- const TIME_INJECTION_MODE_SETTINGS_TITLE =
156
- "<b>🕒 Time injection mode:</b>";
156
+ const ASSISTANT_RENDERING_SETTINGS_TITLE = "<b>🧾 Assistant rendering:</b>";
157
+ const ACTIVITY_VERBOSITY_SETTINGS_TITLE = "<b>🔬 Activity:</b>";
158
+ const TIME_INJECTION_MODE_SETTINGS_TITLE = "<b>🕒 Time injection mode:</b>";
157
159
  const VOICE_REPLY_MODE_SETTINGS_TITLE = "<b>👄 Voice reply mode:</b>";
158
160
  const THREAD_DISPLAY_SETTINGS_TITLE = "<b>🧵 Thread display:</b>";
159
161
 
@@ -210,7 +212,9 @@ export function buildAutomaticThreadCleanupSettingsText(
210
212
  }
211
213
 
212
214
  function buildInactiveThreadReviewText(count: number): string {
213
- return [INACTIVE_THREAD_REVIEW_TITLE, "",
215
+ return [
216
+ INACTIVE_THREAD_REVIEW_TITLE,
217
+ "",
214
218
  `${count} proven inactive tab${count === 1 ? "" : "s"}.`,
215
219
  "No tabs were deleted.",
216
220
  "Deletion requires a separate confirmed Clean action.",
@@ -221,16 +225,29 @@ export function buildInactiveThreadReviewReplyMarkup(
221
225
  operationId?: string,
222
226
  canCleanInactiveThreads = false,
223
227
  ): TelegramSettingsMenuReplyMarkup {
224
- const validOperationId = typeof operationId === "string" &&
228
+ const validOperationId =
229
+ typeof operationId === "string" &&
225
230
  /^thread-cleanup:[a-f0-9]{32}$/u.test(operationId);
226
- return { inline_keyboard: [
227
- [{ text: "⬆️ Back to Thread cleanup",
228
- callback_data: "settings:open:automatic-thread-cleanup" }],
229
- ...(canCleanInactiveThreads && validOperationId ? [[{
230
- text: "🧹 Clean inactive tabs",
231
- callback_data: `settings:clean:${operationId}`,
232
- }]] : []),
233
- ] };
231
+ return {
232
+ inline_keyboard: [
233
+ [
234
+ {
235
+ text: "⬆️ Back to Thread cleanup",
236
+ callback_data: "settings:open:automatic-thread-cleanup",
237
+ },
238
+ ],
239
+ ...(canCleanInactiveThreads && validOperationId
240
+ ? [
241
+ [
242
+ {
243
+ text: "🧹 Clean inactive tabs",
244
+ callback_data: `settings:clean:${operationId}`,
245
+ },
246
+ ],
247
+ ]
248
+ : []),
249
+ ],
250
+ };
234
251
  }
235
252
 
236
253
  export function buildDraftPreviewsSettingsText(enabled: boolean): string {
@@ -352,13 +369,13 @@ export function buildTelegramSettingsMenuReplyMarkup(
352
369
  },
353
370
  {
354
371
  text: `👄 Voice reply: ${getTelegramSettingsStateValueLabel(
355
- getVoiceReplyModeLabel(
356
- getVoiceReplyModeSetting(
357
- voiceReplyMode,
358
- effectiveVoiceReplyModeConfigured,
359
- ),
372
+ getVoiceReplyModeLabel(
373
+ getVoiceReplyModeSetting(
374
+ voiceReplyMode,
375
+ effectiveVoiceReplyModeConfigured,
360
376
  ),
361
- )}`,
377
+ ),
378
+ )}`,
362
379
  callback_data: "settings:open:voice-reply",
363
380
  },
364
381
  {
@@ -370,16 +387,17 @@ export function buildTelegramSettingsMenuReplyMarkup(
370
387
  callback_data: "settings:open:time-injection",
371
388
  },
372
389
  ];
373
- if (threadDisplayMode) settingsButtons.push(
374
- {
375
- text: `🧹 Thread cleanup: ${automaticThreadCleanupEnabled ? "on" : "off"}`,
376
- callback_data: "settings:open:automatic-thread-cleanup",
377
- },
378
- {
379
- text: `🧵 Thread display: ${threadDisplayCustom ? "custom" : threadDisplayMode}`,
380
- callback_data: "settings:open:thread-display",
381
- },
382
- );
390
+ if (threadDisplayMode)
391
+ settingsButtons.push(
392
+ {
393
+ text: `🧹 Thread cleanup: ${automaticThreadCleanupEnabled ? "on" : "off"}`,
394
+ callback_data: "settings:open:automatic-thread-cleanup",
395
+ },
396
+ {
397
+ text: `🧵 Thread display: ${threadDisplayCustom ? "custom" : threadDisplayMode}`,
398
+ callback_data: "settings:open:thread-display",
399
+ },
400
+ );
383
401
  if (sectionRegistry) {
384
402
  const extensionRows = getTelegramExtensionSettingsRows(sectionRegistry);
385
403
  settingsButtons.push(
@@ -393,9 +411,7 @@ export function buildTelegramSettingsMenuReplyMarkup(
393
411
  return { inline_keyboard: rows };
394
412
  }
395
413
 
396
- async function openTelegramSettingsMenu<
397
- TModel extends MenuModel = MenuModel,
398
- >(
414
+ async function openTelegramSettingsMenu<TModel extends MenuModel = MenuModel>(
399
415
  deps: TelegramSettingsMenuOpenDeps<TModel>,
400
416
  sectionRegistry?: TelegramSectionRegistry,
401
417
  ): Promise<void> {
@@ -403,18 +419,7 @@ async function openTelegramSettingsMenu<
403
419
  const messageId = await deps.sendSettingsMenu(
404
420
  state,
405
421
  buildTelegramSettingsMenuText(),
406
- buildTelegramSettingsMenuReplyMarkup(
407
- deps.areDraftPreviewsEnabled(),
408
- deps.getAssistantRenderingMode(),
409
- deps.getVoiceReplyMode(),
410
- deps.getTimeInjectionMode(),
411
- sectionRegistry,
412
- deps.isVoiceReplyModeConfigured(),
413
- deps.isAutomaticThreadCleanupEnabled(),
414
- deps.getActivityVerbosity(),
415
- deps.getThreadDisplayMode?.(),
416
- deps.isThreadDisplayCustom?.() ?? false,
417
- ),
422
+ buildCurrentTelegramSettingsMenuReplyMarkup(deps, sectionRegistry),
418
423
  );
419
424
  if (messageId === undefined) return;
420
425
  state.messageId = messageId;
@@ -424,11 +429,16 @@ async function openTelegramSettingsMenu<
424
429
 
425
430
  function threadDisplayModeLabel(mode: TelegramThreadDisplayMode): string {
426
431
  switch (mode) {
427
- case "letters": return "letters";
428
- case "names": return "names";
429
- case "directory-title": return "directory-title";
430
- case "directory-snake": return "directory-snake";
431
- default: return "letters";
432
+ case "letters":
433
+ return "letters";
434
+ case "names":
435
+ return "names";
436
+ case "directory-title":
437
+ return "directory-title";
438
+ case "directory-snake":
439
+ return "directory-snake";
440
+ default:
441
+ return "letters";
432
442
  }
433
443
  }
434
444
 
@@ -436,13 +446,19 @@ export function buildThreadDisplaySettingsReplyMarkup(
436
446
  mode: TelegramThreadDisplayMode,
437
447
  custom = false,
438
448
  ): TelegramSettingsMenuReplyMarkup {
439
- return { inline_keyboard: [
440
- [{ text: "⬆️ Back", callback_data: "settings:list" }],
441
- ...(["letters", "names", "directory-title", "directory-snake"] as const).map((value) => [{
442
- text: `${!custom && mode === value ? "🟢 " : ""}${threadDisplayModeLabel(value)}`,
443
- callback_data: `settings:set:thread-display:${value}`,
444
- }]),
445
- ] };
449
+ return {
450
+ inline_keyboard: [
451
+ [{ text: "⬆️ Back", callback_data: "settings:list" }],
452
+ ...(
453
+ ["letters", "names", "directory-title", "directory-snake"] as const
454
+ ).map((value) => [
455
+ {
456
+ text: `${!custom && mode === value ? "🟢 " : ""}${threadDisplayModeLabel(value)}`,
457
+ callback_data: `settings:set:thread-display:${value}`,
458
+ },
459
+ ]),
460
+ ],
461
+ };
446
462
  }
447
463
 
448
464
  export function buildAutomaticThreadCleanupSettingsReplyMarkup(
@@ -462,10 +478,16 @@ export function buildAutomaticThreadCleanupSettingsReplyMarkup(
462
478
  callback_data: "settings:set:automatic-thread-cleanup:off",
463
479
  },
464
480
  ],
465
- ...(canReviewInactiveThreads ? [[{
466
- text: "🔎 Review inactive tabs",
467
- callback_data: "settings:review:inactive-threads",
468
- }]] : []),
481
+ ...(canReviewInactiveThreads
482
+ ? [
483
+ [
484
+ {
485
+ text: "🔎 Review inactive tabs",
486
+ callback_data: "settings:review:inactive-threads",
487
+ },
488
+ ],
489
+ ]
490
+ : []),
469
491
  ],
470
492
  };
471
493
  }
@@ -560,24 +582,45 @@ export function buildVoiceReplyModeSettingsReplyMarkup(
560
582
  };
561
583
  }
562
584
 
585
+ type TelegramSettingsMenuValuePorts = Pick<
586
+ TelegramSettingsMenuMessageUpdateDeps,
587
+ | "areDraftPreviewsEnabled"
588
+ | "getAssistantRenderingMode"
589
+ | "getVoiceReplyMode"
590
+ | "getTimeInjectionMode"
591
+ | "isVoiceReplyModeConfigured"
592
+ | "isAutomaticThreadCleanupEnabled"
593
+ | "getActivityVerbosity"
594
+ | "getThreadDisplayMode"
595
+ | "isThreadDisplayCustom"
596
+ >;
597
+
598
+ /** Settings markup from the current setting values; rendering only, no state mutation. */
599
+ function buildCurrentTelegramSettingsMenuReplyMarkup(
600
+ deps: TelegramSettingsMenuValuePorts,
601
+ sectionRegistry?: TelegramSectionRegistry,
602
+ ) {
603
+ return buildTelegramSettingsMenuReplyMarkup(
604
+ deps.areDraftPreviewsEnabled(),
605
+ deps.getAssistantRenderingMode(),
606
+ deps.getVoiceReplyMode(),
607
+ deps.getTimeInjectionMode(),
608
+ sectionRegistry,
609
+ deps.isVoiceReplyModeConfigured(),
610
+ deps.isAutomaticThreadCleanupEnabled(),
611
+ deps.getActivityVerbosity(),
612
+ deps.getThreadDisplayMode?.(),
613
+ deps.isThreadDisplayCustom?.() ?? false,
614
+ );
615
+ }
616
+
563
617
  async function updateTelegramSettingsMenuMessage(
564
618
  deps: TelegramSettingsMenuMessageUpdateDeps,
565
619
  sectionRegistry?: TelegramSectionRegistry,
566
620
  ): Promise<void> {
567
621
  await deps.updateSettingsMessage(
568
622
  buildTelegramSettingsMenuText(),
569
- buildTelegramSettingsMenuReplyMarkup(
570
- deps.areDraftPreviewsEnabled(),
571
- deps.getAssistantRenderingMode(),
572
- deps.getVoiceReplyMode(),
573
- deps.getTimeInjectionMode(),
574
- sectionRegistry,
575
- deps.isVoiceReplyModeConfigured(),
576
- deps.isAutomaticThreadCleanupEnabled(),
577
- deps.getActivityVerbosity(),
578
- deps.getThreadDisplayMode?.(),
579
- deps.isThreadDisplayCustom?.() ?? false,
580
- ),
623
+ buildCurrentTelegramSettingsMenuReplyMarkup(deps, sectionRegistry),
581
624
  );
582
625
  }
583
626
 
@@ -587,7 +630,10 @@ async function updateAutomaticThreadCleanupSettingsMessage(
587
630
  const enabled = deps.isAutomaticThreadCleanupEnabled();
588
631
  await deps.updateSettingsMessage(
589
632
  buildAutomaticThreadCleanupSettingsText(enabled),
590
- buildAutomaticThreadCleanupSettingsReplyMarkup(enabled, !!deps.reviewInactiveThreads),
633
+ buildAutomaticThreadCleanupSettingsReplyMarkup(
634
+ enabled,
635
+ !!deps.reviewInactiveThreads,
636
+ ),
591
637
  );
592
638
  }
593
639
 
@@ -648,33 +694,58 @@ export async function handleTelegramSettingsMenuCallbackAction(
648
694
  deps: TelegramSettingsMenuCallbackDeps,
649
695
  ): Promise<boolean> {
650
696
  if (!data?.startsWith("settings:")) return false;
651
- if (data === "settings:open:thread-display" || data.startsWith("settings:set:thread-display:")) {
697
+ if (
698
+ data === "settings:open:thread-display" ||
699
+ data.startsWith("settings:set:thread-display:")
700
+ ) {
652
701
  if (!deps.getThreadDisplayMode?.() || !deps.setThreadDisplayMode) {
653
- await deps.answerCallbackQuery(callbackQueryId, "Thread display requires Threaded Mode and a connected instance.");
702
+ await deps.answerCallbackQuery(
703
+ callbackQueryId,
704
+ "Thread display requires Threaded Mode and a connected instance",
705
+ );
654
706
  return true;
655
707
  }
656
708
  if (data.startsWith("settings:set:thread-display:")) {
657
709
  const mode = data.slice("settings:set:thread-display:".length);
658
- if (mode !== "letters" && mode !== "names" &&
659
- mode !== "directory-snake" && mode !== "directory-title") {
660
- await deps.answerCallbackQuery(callbackQueryId, "Unknown Thread display mode.");
710
+ if (
711
+ mode !== "letters" &&
712
+ mode !== "names" &&
713
+ mode !== "directory-snake" &&
714
+ mode !== "directory-title"
715
+ ) {
716
+ await deps.answerCallbackQuery(
717
+ callbackQueryId,
718
+ "Unknown Thread display mode",
719
+ );
661
720
  return true;
662
721
  }
663
722
  try {
664
723
  await deps.setThreadDisplayMode(mode);
665
724
  } catch {
666
- await deps.answerCallbackQuery(callbackQueryId, "Thread display was not fully applied. The preference may be saved; retry after checking the leader.");
725
+ await deps.answerCallbackQuery(
726
+ callbackQueryId,
727
+ "Thread display was not fully applied. The preference may be saved; retry after checking the leader",
728
+ );
667
729
  return true;
668
730
  }
669
731
  }
670
732
  const mode = deps.getThreadDisplayMode();
671
733
  if (!mode) {
672
- await deps.answerCallbackQuery(callbackQueryId, "Threaded Mode is no longer available.");
734
+ await deps.answerCallbackQuery(
735
+ callbackQueryId,
736
+ "Threaded Mode is no longer available",
737
+ );
673
738
  return true;
674
739
  }
675
740
  await deps.updateSettingsMessage(
676
- buildThreadDisplaySettingsText(mode, deps.isThreadDisplayCustom?.() ?? false),
677
- buildThreadDisplaySettingsReplyMarkup(mode, deps.isThreadDisplayCustom?.() ?? false),
741
+ buildThreadDisplaySettingsText(
742
+ mode,
743
+ deps.isThreadDisplayCustom?.() ?? false,
744
+ ),
745
+ buildThreadDisplaySettingsReplyMarkup(
746
+ mode,
747
+ deps.isThreadDisplayCustom?.() ?? false,
748
+ ),
678
749
  );
679
750
  await deps.answerCallbackQuery(callbackQueryId);
680
751
  return true;
@@ -697,7 +768,7 @@ export async function handleTelegramSettingsMenuCallbackAction(
697
768
  await updateTelegramSettingsMenuMessage(deps, deps.sectionRegistry);
698
769
  await deps.answerCallbackQuery(
699
770
  callbackQueryId,
700
- "Public assistant output is always delivered while Telegram is connected.",
771
+ "Public assistant output is always delivered while Telegram is connected",
701
772
  );
702
773
  return true;
703
774
  }
@@ -806,48 +877,77 @@ export async function handleTelegramSettingsMenuCallbackAction(
806
877
  ) {
807
878
  await deps.setActivityVerbosity(verbosity);
808
879
  await updateActivityVerbositySettingsMessage(deps);
809
- await deps.answerCallbackQuery(
810
- callbackQueryId,
811
- `Activity: ${verbosity}`,
812
- );
880
+ await deps.answerCallbackQuery(callbackQueryId, `Activity: ${verbosity}`);
813
881
  return true;
814
882
  }
815
883
  }
816
884
  if (data === "settings:review:inactive-threads") {
817
885
  if (!deps.reviewInactiveThreads) {
818
- await deps.answerCallbackQuery(callbackQueryId, "Inactive tab review is unavailable.");
886
+ await deps.answerCallbackQuery(
887
+ callbackQueryId,
888
+ "Inactive tab review is unavailable",
889
+ );
819
890
  return true;
820
891
  }
821
892
  try {
822
893
  const review = await deps.reviewInactiveThreads();
823
894
  if (review.count === 0) {
824
- await deps.answerCallbackQuery(callbackQueryId, "No proven inactive tabs.");
895
+ await deps.answerCallbackQuery(
896
+ callbackQueryId,
897
+ "No proven inactive tabs",
898
+ );
825
899
  } else {
826
- await deps.updateSettingsMessage(buildInactiveThreadReviewText(review.count),
827
- buildInactiveThreadReviewReplyMarkup(review.operationId, !!deps.cleanInactiveThreads));
828
- await deps.answerCallbackQuery(callbackQueryId, "Review prepared. No tabs were deleted.");
900
+ await deps.updateSettingsMessage(
901
+ buildInactiveThreadReviewText(review.count),
902
+ buildInactiveThreadReviewReplyMarkup(
903
+ review.operationId,
904
+ !!deps.cleanInactiveThreads,
905
+ ),
906
+ );
907
+ await deps.answerCallbackQuery(
908
+ callbackQueryId,
909
+ "Review prepared. No tabs were deleted",
910
+ );
829
911
  }
830
912
  } catch {
831
- await deps.answerCallbackQuery(callbackQueryId, "Could not safely review inactive tabs.");
913
+ await deps.answerCallbackQuery(
914
+ callbackQueryId,
915
+ "Could not safely review inactive tabs",
916
+ );
832
917
  }
833
918
  return true;
834
919
  }
835
920
  if (data.startsWith("settings:clean:")) {
836
921
  const operationId = data.slice("settings:clean:".length);
837
- if (!/^thread-cleanup:[a-f0-9]{32}$/u.test(operationId) || !deps.cleanInactiveThreads) {
838
- await deps.answerCallbackQuery(callbackQueryId, "Cleanup confirmation is unavailable or stale.");
922
+ if (
923
+ !/^thread-cleanup:[a-f0-9]{32}$/u.test(operationId) ||
924
+ !deps.cleanInactiveThreads
925
+ ) {
926
+ await deps.answerCallbackQuery(
927
+ callbackQueryId,
928
+ "Cleanup confirmation is unavailable or stale",
929
+ );
839
930
  return true;
840
931
  }
841
932
  try {
842
933
  const result = await deps.cleanInactiveThreads(operationId);
843
- await deps.answerCallbackQuery(callbackQueryId,
844
- `Deleted: ${result.deleted}. Outcome unknown: ${result.outcomeUnknown}.` +
845
- (result.blocked ? ` Blocked: ${result.blocked}.` : "") +
846
- (result.recovery === "commit-ready" ? " Recovery: safe local commit pending." :
847
- result.recovery === "deletion-outcome-unknown" ? " Recovery: deletion outcome unknown; no retry." :
848
- result.recovery === "authority-blocked" ? " Recovery: cleanup authority unavailable." : ""));
934
+ await deps.answerCallbackQuery(
935
+ callbackQueryId,
936
+ `Deleted: ${result.deleted}. Outcome unknown: ${result.outcomeUnknown}` +
937
+ (result.blocked ? `. Blocked: ${result.blocked}` : "") +
938
+ (result.recovery === "commit-ready"
939
+ ? ". Recovery: safe local commit pending"
940
+ : result.recovery === "deletion-outcome-unknown"
941
+ ? ". Recovery: deletion outcome unknown; no retry"
942
+ : result.recovery === "authority-blocked"
943
+ ? ". Recovery: cleanup authority unavailable"
944
+ : ""),
945
+ );
849
946
  } catch {
850
- await deps.answerCallbackQuery(callbackQueryId, "Cleanup could not be safely completed.");
947
+ await deps.answerCallbackQuery(
948
+ callbackQueryId,
949
+ "Cleanup could not be safely completed",
950
+ );
851
951
  }
852
952
  return true;
853
953
  }
@@ -875,29 +975,46 @@ export function createTelegramSettingsMenuRuntime<
875
975
  deps: TelegramSettingsMenuRuntimeDeps<TContext, TModel>,
876
976
  sectionRegistry?: TelegramSectionRegistry,
877
977
  ): TelegramSettingsMenuRuntime<TContext> {
978
+ // Read per call so later port replacement on deps stays visible.
979
+ const readSettingsState = (): TelegramSettingsStateDeps => ({
980
+ areDraftPreviewsEnabled: deps.areDraftPreviewsEnabled,
981
+ getAssistantRenderingMode: deps.getAssistantRenderingMode,
982
+ getActivityVerbosity: deps.getActivityVerbosity,
983
+ getVoiceReplyMode: deps.getVoiceReplyMode,
984
+ isVoiceReplyModeConfigured: deps.isVoiceReplyModeConfigured,
985
+ getTimeInjectionMode: deps.getTimeInjectionMode,
986
+ isAutomaticThreadCleanupEnabled: deps.isAutomaticThreadCleanupEnabled,
987
+ getThreadDisplayMode: deps.getThreadDisplayMode,
988
+ isThreadDisplayCustom: deps.isThreadDisplayCustom,
989
+ });
878
990
  return {
879
- openSettingsMenu: async (chatId, _replyToMessageId, ctx) => {
991
+ openSettingsMenu: async (
992
+ chatId,
993
+ _replyToMessageId,
994
+ ctx,
995
+ threadId,
996
+ options,
997
+ ) => {
998
+ const delivery = createTelegramMenuDelivery(
999
+ { chatId, threadId },
1000
+ options,
1001
+ deps,
1002
+ );
880
1003
  await deps.reloadConfig?.();
1004
+ delivery.assertCurrent();
881
1005
  return openTelegramSettingsMenu(
882
1006
  {
883
- getModelMenuState: () => deps.getModelMenuState(chatId, ctx),
884
- areDraftPreviewsEnabled: deps.areDraftPreviewsEnabled,
885
- getAssistantRenderingMode: deps.getAssistantRenderingMode,
886
- getActivityVerbosity: deps.getActivityVerbosity,
887
- getVoiceReplyMode: deps.getVoiceReplyMode,
888
- isVoiceReplyModeConfigured: deps.isVoiceReplyModeConfigured,
889
- getTimeInjectionMode: deps.getTimeInjectionMode,
890
- isAutomaticThreadCleanupEnabled: deps.isAutomaticThreadCleanupEnabled,
891
- getThreadDisplayMode: deps.getThreadDisplayMode,
892
- isThreadDisplayCustom: deps.isThreadDisplayCustom,
1007
+ getModelMenuState: () =>
1008
+ deps.getModelMenuState(chatId, ctx, threadId),
1009
+ ...readSettingsState(),
893
1010
  sendSettingsMenu: (state, text, replyMarkup) =>
894
- deps.sendInteractiveMessage(
1011
+ delivery.sendInteractiveMessage(
895
1012
  state.chatId,
896
1013
  text,
897
1014
  "html",
898
1015
  replyMarkup,
899
1016
  ),
900
- storeModelMenuState: deps.storeModelMenuState,
1017
+ storeModelMenuState: delivery.storeModelMenuState,
901
1018
  },
902
1019
  sectionRegistry,
903
1020
  );
@@ -906,15 +1023,7 @@ export function createTelegramSettingsMenuRuntime<
906
1023
  await deps.reloadConfig?.();
907
1024
  return updateTelegramSettingsMenuMessage(
908
1025
  {
909
- areDraftPreviewsEnabled: deps.areDraftPreviewsEnabled,
910
- getAssistantRenderingMode: deps.getAssistantRenderingMode,
911
- getActivityVerbosity: deps.getActivityVerbosity,
912
- getVoiceReplyMode: deps.getVoiceReplyMode,
913
- isVoiceReplyModeConfigured: deps.isVoiceReplyModeConfigured,
914
- getTimeInjectionMode: deps.getTimeInjectionMode,
915
- isAutomaticThreadCleanupEnabled: deps.isAutomaticThreadCleanupEnabled,
916
- getThreadDisplayMode: deps.getThreadDisplayMode,
917
- isThreadDisplayCustom: deps.isThreadDisplayCustom,
1026
+ ...readSettingsState(),
918
1027
  updateSettingsMessage: (text, replyMarkup) =>
919
1028
  deps.editInteractiveMessage(
920
1029
  state.chatId,
@@ -937,7 +1046,7 @@ export function createTelegramSettingsMenuRuntime<
937
1046
  if (typeof messageId !== "number" || typeof chatId !== "number") {
938
1047
  await deps.answerCallbackQuery(
939
1048
  query.id,
940
- "Interactive message expired.",
1049
+ "Interactive message expired",
941
1050
  );
942
1051
  return true;
943
1052
  }
@@ -951,15 +1060,7 @@ export function createTelegramSettingsMenuRuntime<
951
1060
  deps.storeModelMenuState(state);
952
1061
  }
953
1062
  return handleTelegramSettingsMenuCallbackAction(query.id, query.data, {
954
- areDraftPreviewsEnabled: deps.areDraftPreviewsEnabled,
955
- getAssistantRenderingMode: deps.getAssistantRenderingMode,
956
- getActivityVerbosity: deps.getActivityVerbosity,
957
- getVoiceReplyMode: deps.getVoiceReplyMode,
958
- isVoiceReplyModeConfigured: deps.isVoiceReplyModeConfigured,
959
- getTimeInjectionMode: deps.getTimeInjectionMode,
960
- isAutomaticThreadCleanupEnabled: deps.isAutomaticThreadCleanupEnabled,
961
- getThreadDisplayMode: deps.getThreadDisplayMode,
962
- isThreadDisplayCustom: deps.isThreadDisplayCustom,
1063
+ ...readSettingsState(),
963
1064
  setThreadDisplayMode: deps.setThreadDisplayMode,
964
1065
  setDraftPreviewsEnabled: deps.setDraftPreviewsEnabled,
965
1066
  setAssistantRenderingMode: deps.setAssistantRenderingMode,
@@ -967,8 +1068,12 @@ export function createTelegramSettingsMenuRuntime<
967
1068
  setVoiceReplyMode: deps.setVoiceReplyMode,
968
1069
  setTimeInjectionMode: deps.setTimeInjectionMode,
969
1070
  setAutomaticThreadCleanupEnabled: deps.setAutomaticThreadCleanupEnabled,
970
- ...(deps.reviewInactiveThreads ? { reviewInactiveThreads: deps.reviewInactiveThreads } : {}),
971
- ...(deps.cleanInactiveThreads ? { cleanInactiveThreads: deps.cleanInactiveThreads } : {}),
1071
+ ...(deps.reviewInactiveThreads
1072
+ ? { reviewInactiveThreads: deps.reviewInactiveThreads }
1073
+ : {}),
1074
+ ...(deps.cleanInactiveThreads
1075
+ ? { cleanInactiveThreads: deps.cleanInactiveThreads }
1076
+ : {}),
972
1077
  updateSettingsMessage: (text, replyMarkup) =>
973
1078
  deps.editInteractiveMessage(
974
1079
  state.chatId,