@llblab/pi-telegram 0.52.2 → 0.53.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 (204) hide show
  1. package/AGENTS.md +9 -7
  2. package/BACKLOG.md +1 -18
  3. package/CHANGELOG.md +11 -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 +61 -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 +249 -126
  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 +125 -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 +511 -300
  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
@@ -3,10 +3,10 @@
3
3
  * Zones: telegram diagnostics, filesystem, session observability
4
4
  * Owns bounded JSONL runtime evidence files, previous-log preservation, and profile-aware log paths without becoming routing state
5
5
  */
6
- import { copyFileSync, existsSync, mkdirSync, statSync, writeFileSync, appendFileSync, } from "node:fs";
6
+ import { appendFileSync, copyFileSync, existsSync, mkdirSync, statSync, writeFileSync, } from "node:fs";
7
7
  import { basename, dirname, join } from "node:path";
8
- import { resolveAgentDir, resolveTelegramProfileTempFilePath, resolveTelegramRuntimeLogPath, resolveTelegramPreviousSharedRuntimeLogPath, } from "./paths.js";
9
8
  import { withTelegramFileTransaction } from "./locks.js";
9
+ import { resolveAgentDir, resolveTelegramPreviousSharedRuntimeLogPath, resolveTelegramProfileTempFilePath, resolveTelegramRuntimeLogPath, } from "./paths.js";
10
10
  import * as Status from "./status.js";
11
11
  const DEFAULT_MAX_LOG_BYTES = 5 * 1024 * 1024;
12
12
  export function getTelegramRuntimeLogPath(agentDir = resolveAgentDir(), profileName) {
@@ -27,7 +27,9 @@ function safeJsonLine(value) {
27
27
  });
28
28
  }
29
29
  export function createTelegramRuntimeJsonlLog(options = {}) {
30
- const shared = options.sharedProfiles ? { ...options.sharedProfiles } : undefined;
30
+ const shared = options.sharedProfiles
31
+ ? { ...options.sharedProfiles }
32
+ : undefined;
31
33
  const pathSource = options.path, previousSource = options.previousPath;
32
34
  const canReset = options.canReset, commitReset = options.commitReset;
33
35
  const resolvePath = () => typeof pathSource === "function"
@@ -38,7 +40,8 @@ export function createTelegramRuntimeJsonlLog(options = {}) {
38
40
  return previousSource();
39
41
  if (previousSource)
40
42
  return previousSource;
41
- return shared ? join(dirname(resolvePath()), "logs", `${basename(resolvePath(), ".jsonl")}._prev.jsonl`)
43
+ return shared
44
+ ? join(dirname(resolvePath()), "logs", `${basename(resolvePath(), ".jsonl")}._prev.jsonl`)
42
45
  : resolvePath().replace(/\.jsonl$/u, "._prev.jsonl");
43
46
  };
44
47
  const maxBytes = options.maxBytes ?? DEFAULT_MAX_LOG_BYTES;
@@ -48,12 +51,17 @@ export function createTelegramRuntimeJsonlLog(options = {}) {
48
51
  let appendScheduled = false;
49
52
  let queuedAppends = [];
50
53
  const getProfile = () => shared?.getProfileName() ?? "default";
51
- const transactionPath = (path) => shared ? join(dirname(path), "runtime", `${basename(path)}.transaction`) : `${path}.transaction`;
54
+ const transactionPath = (path) => shared
55
+ ? join(dirname(path), "runtime", `${basename(path)}.transaction`)
56
+ : `${path}.transaction`;
52
57
  const captureRotation = (path, previousPath, profile) => {
53
58
  if (!shared)
54
59
  return undefined;
55
60
  const authority = shared.captureAuthority();
56
- return () => authority?.() === true && resolvePath() === path && resolvePreviousPath() === previousPath && getProfile() === profile;
61
+ return () => authority?.() === true &&
62
+ resolvePath() === path &&
63
+ resolvePreviousPath() === previousPath &&
64
+ getProfile() === profile;
57
65
  };
58
66
  const scopeStorageKey = (path, profile) => shared ? JSON.stringify([path, profile]) : path;
59
67
  const ensureParent = (path) => {
@@ -94,11 +102,18 @@ export function createTelegramRuntimeJsonlLog(options = {}) {
94
102
  if (shared && !isCurrent?.())
95
103
  return;
96
104
  if (shared) {
97
- const line = safeJsonLine({ at: getNowMs(), kind: "reset", reason, scope, profile }) + "\n";
105
+ const line = safeJsonLine({
106
+ at: getNowMs(),
107
+ kind: "reset",
108
+ reason,
109
+ scope,
110
+ profile,
111
+ }) + "\n";
98
112
  if (!isCurrent?.())
99
113
  return;
100
114
  ensureParent(path);
101
- if (existsSync(path) && statSync(path).size + Buffer.byteLength(line) > maxBytes &&
115
+ if (existsSync(path) &&
116
+ statSync(path).size + Buffer.byteLength(line) > maxBytes &&
102
117
  !writeResetLocked(path, previousPath, "max-bytes", { maxBytes }, profile, isCurrent))
103
118
  return;
104
119
  if (!isCurrent?.())
@@ -121,8 +136,17 @@ export function createTelegramRuntimeJsonlLog(options = {}) {
121
136
  const appendEvent = (event) => {
122
137
  const path = resolvePath(), previousPath = resolvePreviousPath(), profile = getProfile();
123
138
  const rotationCurrent = captureRotation(path, previousPath, profile);
124
- const line = safeJsonLine({ kind: "event", ...event, ...(shared ? { profile } : {}) }) + "\n";
125
- queuedAppends.push({ path, previousPath, line, ...(shared ? { profile, rotationCurrent } : {}) });
139
+ const line = safeJsonLine({
140
+ kind: "event",
141
+ ...event,
142
+ ...(shared ? { profile } : {}),
143
+ }) + "\n";
144
+ queuedAppends.push({
145
+ path,
146
+ previousPath,
147
+ line,
148
+ ...(shared ? { profile, rotationCurrent } : {}),
149
+ });
126
150
  if (appendScheduled)
127
151
  return;
128
152
  appendScheduled = true;
@@ -244,10 +268,20 @@ export function createTelegramRuntimeDiagnosticsRuntime(options = {}) {
244
268
  getBotToken: () => getBotToken(),
245
269
  });
246
270
  const jsonl = createTelegramRuntimeJsonlLog({
247
- path: () => sharedFile ? resolveTelegramRuntimeLogPath() : getTelegramRuntimeLogPath(undefined, getProfileName()),
248
- previousPath: () => sharedFile ? resolveTelegramPreviousSharedRuntimeLogPath()
271
+ path: () => sharedFile
272
+ ? resolveTelegramRuntimeLogPath()
273
+ : getTelegramRuntimeLogPath(undefined, getProfileName()),
274
+ previousPath: () => sharedFile
275
+ ? resolveTelegramPreviousSharedRuntimeLogPath()
249
276
  : getTelegramPreviousRuntimeLogPath(undefined, getProfileName()),
250
- ...(sharedFile ? { sharedProfiles: { getProfileName: () => getProfileName(), captureAuthority: () => captureAuthority() } } : {}),
277
+ ...(sharedFile
278
+ ? {
279
+ sharedProfiles: {
280
+ getProfileName: () => getProfileName(),
281
+ captureAuthority: () => captureAuthority(),
282
+ },
283
+ }
284
+ : {}),
251
285
  canReset: () => canReset(),
252
286
  commitReset: (commit) => commitReset(commit),
253
287
  });
@@ -283,7 +317,9 @@ export function createTelegramRuntimeDiagnosticsRuntime(options = {}) {
283
317
  await ports.persistSnapshot(snapshot);
284
318
  },
285
319
  recordError(error) {
286
- events.record("telegram", error, { phase: "runtime-diagnostics-snapshot-persist" });
320
+ events.record("telegram", error, {
321
+ phase: "runtime-diagnostics-snapshot-persist",
322
+ });
287
323
  },
288
324
  });
289
325
  const updateRuntimeLogScope = function (reason) {
@@ -180,7 +180,6 @@ export declare function guessMediaType(path: string): string | undefined;
180
180
  export declare function extractTelegramMessageText(message: TelegramMediaMessage): string;
181
181
  export declare function extractTelegramForwardContextText(message: TelegramMediaMessage, allowedUserId?: number): string;
182
182
  export declare function buildTelegramReplyContextBlock(message: TelegramMediaMessage, replyFiles?: Pick<DownloadedTelegramFile, "path">[], replyOutputs?: readonly string[]): string;
183
- export declare function extractTelegramMessagePromptText(message: TelegramMediaMessage): string;
184
183
  export declare function extractTelegramMessagesText(messages: TelegramMediaMessage[]): string;
185
184
  export declare function extractTelegramMessagesPromptText(messages: TelegramMediaMessage[]): string;
186
185
  export declare function extractFirstTelegramMessageText(messages: TelegramMediaMessage[]): string;
@@ -197,8 +196,30 @@ export declare function queueTelegramMediaGroupMessage<TMessage extends Telegram
197
196
  clearTimer: (timer: ReturnType<typeof setTimeout>) => void;
198
197
  dispatchMessages: (messages: TMessage[], ctx?: TContext) => unknown | Promise<unknown>;
199
198
  }): boolean;
199
+ /** Timer and dispatch lifecycle shared by delayed media-group and text-group coalescing. */
200
+ export interface TelegramPendingGroupState<TMessage, TContext> {
201
+ messages: TMessage[];
202
+ context?: TContext;
203
+ flushTimer?: ReturnType<typeof setTimeout>;
204
+ dispatching?: boolean;
205
+ suspended?: boolean;
206
+ reschedule?: () => void;
207
+ dispatchNow?: () => Promise<void>;
208
+ }
209
+ export interface TelegramPendingGroupLifecycle<TContext> {
210
+ flushMessage: (messageId: number) => Promise<boolean>;
211
+ suspend: () => void;
212
+ resume: (context: TContext) => void;
213
+ clear: () => void;
214
+ }
215
+ export declare function createTelegramPendingGroupLifecycle<TMessage extends {
216
+ message_id: number;
217
+ }, TContext>(groups: Map<string, TelegramPendingGroupState<TMessage, TContext>>, clearTimer: (timer: ReturnType<typeof setTimeout>) => void): TelegramPendingGroupLifecycle<TContext>;
200
218
  export declare function createTelegramMediaGroupController<TMessage extends TelegramMediaGroupMessage, TContext = unknown>(options?: TelegramMediaGroupControllerOptions): TelegramMediaGroupController<TMessage, TContext>;
201
219
  export declare function createTelegramMediaGroupDispatchRuntime<TMessage extends TelegramMediaGroupMessage, TContext>(deps: TelegramMediaGroupDispatchRuntimeDeps<TMessage, TContext>): TelegramMediaGroupDispatchRuntime<TMessage, TContext>;
220
+ /** Pure `[title]` bullet section appended after a blank line; empty items leave text unchanged. */
221
+ export declare function appendTelegramListSection(text: string, title: string, items: string[]): string;
222
+ export declare function appendTelegramAttachmentSection(text: string, files: Pick<DownloadedTelegramFile, "path">[]): string;
202
223
  export declare function formatTelegramHistoryText(rawText: string, files: DownloadedTelegramFile[], handlerOutputs?: string[]): string;
203
224
  export declare function downloadTelegramMessageFiles(messages: TelegramMediaMessage[], deps: DownloadTelegramMessageFilesDeps): Promise<DownloadedTelegramMessageFile[]>;
204
225
  export declare function collectTelegramFileInfos(messages: TelegramMediaMessage[]): TelegramFileInfo[];
package/dist/lib/media.js CHANGED
@@ -200,9 +200,6 @@ function appendTelegramReplyContext(text, replyContext) {
200
200
  return text;
201
201
  return text ? `${text}\n\n${replyContext}` : `_\n\n${replyContext}`;
202
202
  }
203
- export function extractTelegramMessagePromptText(message) {
204
- return appendTelegramReplyContext(extractTelegramMessageText(message), buildTelegramReplyContextBlock(message));
205
- }
206
203
  export function extractTelegramMessagesText(messages) {
207
204
  return messages.map(extractTelegramMessageText).filter(Boolean).join("\n\n");
208
205
  }
@@ -317,23 +314,8 @@ export function queueTelegramMediaGroupMessage(options) {
317
314
  options.groups.set(key, existing);
318
315
  return true;
319
316
  }
320
- export function createTelegramMediaGroupController(options = {}) {
321
- const groups = new Map();
322
- const debounceMs = options.debounceMs ?? TELEGRAM_MEDIA_GROUP_DEBOUNCE_MS;
323
- const setTimer = options.setTimer ??
324
- ((callback, ms) => setTimeout(callback, ms));
325
- const clearTimer = options.clearTimer ?? clearTimeout;
317
+ export function createTelegramPendingGroupLifecycle(groups, clearTimer) {
326
318
  return {
327
- queueMessage: ({ message, context, dispatchMessages }) => queueTelegramMediaGroupMessage({
328
- message,
329
- context,
330
- groups,
331
- debounceMs,
332
- setTimer,
333
- clearTimer,
334
- dispatchMessages,
335
- }),
336
- removeMessages: (messageIds) => removePendingTelegramMediaGroupMessages(groups, messageIds, clearTimer),
337
319
  async flushMessage(messageId) {
338
320
  for (const state of groups.values()) {
339
321
  if (!state.messages.some((message) => message.message_id === messageId)) {
@@ -376,6 +358,26 @@ export function createTelegramMediaGroupController(options = {}) {
376
358
  },
377
359
  };
378
360
  }
361
+ export function createTelegramMediaGroupController(options = {}) {
362
+ const groups = new Map();
363
+ const debounceMs = options.debounceMs ?? TELEGRAM_MEDIA_GROUP_DEBOUNCE_MS;
364
+ const setTimer = options.setTimer ??
365
+ ((callback, ms) => setTimeout(callback, ms));
366
+ const clearTimer = options.clearTimer ?? clearTimeout;
367
+ return {
368
+ queueMessage: ({ message, context, dispatchMessages }) => queueTelegramMediaGroupMessage({
369
+ message,
370
+ context,
371
+ groups,
372
+ debounceMs,
373
+ setTimer,
374
+ clearTimer,
375
+ dispatchMessages,
376
+ }),
377
+ removeMessages: (messageIds) => removePendingTelegramMediaGroupMessages(groups, messageIds, clearTimer),
378
+ ...createTelegramPendingGroupLifecycle(groups, clearTimer),
379
+ };
380
+ }
379
381
  export function createTelegramMediaGroupDispatchRuntime(deps) {
380
382
  return {
381
383
  handleMessage: async (message, ctx) => {
@@ -394,13 +396,14 @@ export function createTelegramMediaGroupDispatchRuntime(deps) {
394
396
  },
395
397
  };
396
398
  }
397
- function appendTelegramListSection(text, title, items) {
399
+ /** Pure `[title]` bullet section appended after a blank line; empty items leave text unchanged. */
400
+ export function appendTelegramListSection(text, title, items) {
398
401
  if (items.length === 0)
399
402
  return text;
400
403
  const prefix = text.length > 0 ? `${text}\n\n` : "";
401
404
  return `${prefix}[${title}]\n${items.map((item) => `- ${item}`).join("\n")}`;
402
405
  }
403
- function appendTelegramAttachmentSection(text, files) {
406
+ export function appendTelegramAttachmentSection(text, files) {
404
407
  if (files.length === 0)
405
408
  return text;
406
409
  const dirs = [...new Set(files.map((file) => dirname(file.path)))];
@@ -537,7 +540,9 @@ export function collectTelegramFileInfos(messages) {
537
540
  files.push({
538
541
  file_id: message.document.file_id,
539
542
  fileName,
540
- ...(message.document.file_name ? { userFileName: message.document.file_name } : {}),
543
+ ...(message.document.file_name
544
+ ? { userFileName: message.document.file_name }
545
+ : {}),
541
546
  mimeType: message.document.mime_type,
542
547
  kind: "document",
543
548
  isImage: isImageMimeType(message.document.mime_type),
@@ -549,7 +554,9 @@ export function collectTelegramFileInfos(messages) {
549
554
  files.push({
550
555
  file_id: message.video.file_id,
551
556
  fileName,
552
- ...(message.video.file_name ? { userFileName: message.video.file_name } : {}),
557
+ ...(message.video.file_name
558
+ ? { userFileName: message.video.file_name }
559
+ : {}),
553
560
  mimeType: message.video.mime_type,
554
561
  kind: "video",
555
562
  isImage: false,
@@ -562,7 +569,9 @@ export function collectTelegramFileInfos(messages) {
562
569
  files.push({
563
570
  file_id: message.audio.file_id,
564
571
  fileName,
565
- ...(message.audio.file_name ? { userFileName: message.audio.file_name } : {}),
572
+ ...(message.audio.file_name
573
+ ? { userFileName: message.audio.file_name }
574
+ : {}),
566
575
  mimeType: message.audio.mime_type,
567
576
  kind: "audio",
568
577
  isImage: false,
@@ -584,7 +593,9 @@ export function collectTelegramFileInfos(messages) {
584
593
  files.push({
585
594
  file_id: message.animation.file_id,
586
595
  fileName,
587
- ...(message.animation.file_name ? { userFileName: message.animation.file_name } : {}),
596
+ ...(message.animation.file_name
597
+ ? { userFileName: message.animation.file_name }
598
+ : {}),
588
599
  mimeType: message.animation.mime_type,
589
600
  kind: "animation",
590
601
  isImage: false,
@@ -598,16 +609,23 @@ export function collectTelegramFileInfos(messages) {
598
609
  files.push({
599
610
  file_id: message.sticker.file_id,
600
611
  fileName: `sticker-${message.message_id}${extension}`,
601
- mimeType: video ? "video/webm" : animated ? "application/x-tgsticker" : "image/webp",
612
+ mimeType: video
613
+ ? "video/webm"
614
+ : animated
615
+ ? "application/x-tgsticker"
616
+ : "image/webp",
602
617
  kind: "sticker",
603
618
  isImage: !video && !animated,
604
619
  });
605
620
  }
606
621
  for (const file of files.slice(firstOfMessage)) {
607
- file.source = { kind: file.kind, messageId: message.message_id,
622
+ file.source = {
623
+ kind: file.kind,
624
+ messageId: message.message_id,
608
625
  ...(file.index !== undefined ? { index: file.index } : {}),
609
626
  ...(file.userFileName ? { userFileName: file.userFileName } : {}),
610
- ...(message.chat ? { chat: message.chat } : {}) };
627
+ ...(message.chat ? { chat: message.chat } : {}),
628
+ };
611
629
  }
612
630
  }
613
631
  const seenFileIds = new Set();
@@ -5,6 +5,8 @@
5
5
  */
6
6
  import type { TelegramInlineKeyboardMarkup } from "./keyboard.ts";
7
7
  import { type MenuModel, type ScopedTelegramModel, type TelegramModelSwitchContinuationSource, type ThinkingLevel } from "./model.ts";
8
+ import type { TelegramTarget } from "./target.ts";
9
+ import { type TelegramApiCallOptions } from "./telegram-api.ts";
8
10
  export type TelegramModelScope = "all" | "scoped";
9
11
  export interface TelegramModelMenuState<TModel extends MenuModel = MenuModel> {
10
12
  chatId: number;
@@ -79,12 +81,20 @@ export type TelegramReplyMarkup = TelegramInlineKeyboardMarkup;
79
81
  export interface TelegramMenuMessageRuntimeDeps {
80
82
  editInteractiveMessage: (chatId: number, messageId: number, text: string, mode: "markdown" | "html" | "plain", replyMarkup: TelegramReplyMarkup) => Promise<void>;
81
83
  sendInteractiveMessage: (chatId: number, text: string, mode: "markdown" | "html" | "plain", replyMarkup: TelegramReplyMarkup, options?: {
82
- target?: {
83
- chatId: number;
84
- threadId?: number;
85
- };
86
- }) => Promise<number | undefined>;
84
+ target?: TelegramTarget;
85
+ } & Pick<TelegramApiCallOptions, "assertAuthority">) => Promise<number | undefined>;
87
86
  }
87
+ /** Per-open delivery lifetime; never retain this guard in stored callback state. */
88
+ export declare function createTelegramMenuDelivery<TModel extends MenuModel>(target: TelegramTarget, options: Pick<TelegramApiCallOptions, "assertAuthority"> | undefined, deps: Pick<TelegramMenuMessageRuntimeDeps, "sendInteractiveMessage"> & {
89
+ storeModelMenuState: (state: TelegramModelMenuState<TModel>) => void;
90
+ }): {
91
+ assertCurrent: () => void;
92
+ assertAuthority: (() => void) | undefined;
93
+ sendInteractiveMessage: (chatId: number, text: string, mode: "html" | "markdown" | "plain", replyMarkup: TelegramInlineKeyboardMarkup, options?: ({
94
+ target?: TelegramTarget;
95
+ } & Pick<TelegramApiCallOptions, "assertAuthority">) | undefined) => Promise<number | undefined>;
96
+ storeModelMenuState: (state: TelegramModelMenuState<TModel>) => void;
97
+ };
88
98
  export type TelegramModelMenuCallbackDeps<TModel extends MenuModel = MenuModel> = {
89
99
  answerCallbackQuery: (callbackQueryId: string, text?: string) => Promise<void>;
90
100
  updateModelMenuMessage: () => Promise<void>;
@@ -177,6 +187,10 @@ export interface TelegramModelMenuRuntime<TModel extends MenuModel = MenuModel>
177
187
  export declare const TELEGRAM_MODEL_PAGE_SIZE = 6;
178
188
  export declare const MODEL_MENU_TITLE = "<b>\uD83E\uDD16 Choose a model:</b>";
179
189
  export declare const MODEL_PAGE_MENU_TITLE = "<b>Choose a page:</b>";
190
+ /** Apply the payload mode to state, then edit the existing menu message in place. */
191
+ export declare function editTelegramMenuMessage(state: TelegramModelMenuState, payload: TelegramMenuRenderPayload, deps: TelegramMenuMessageRuntimeDeps): Promise<void>;
192
+ /** Apply the payload mode to state, then send a fresh menu message to its exact chat/Thread. */
193
+ export declare function sendTelegramMenuMessage(state: TelegramModelMenuState, payload: TelegramMenuRenderPayload, deps: TelegramMenuMessageRuntimeDeps): Promise<number | undefined>;
180
194
  export declare function formatScopedModelButtonText<TModel extends MenuModel = MenuModel>(entry: ScopedTelegramModel<TModel>, currentModel: TModel | undefined): string;
181
195
  export declare function formatStatusButtonLabel(label: string, value: string): string;
182
196
  export declare function getModelMenuItems<TModel extends MenuModel = MenuModel>(state: TelegramModelMenuState<TModel>): ScopedTelegramModel<TModel>[];
@@ -4,9 +4,49 @@
4
4
  * Owns model-menu state, scoped model pages, model callback planning, and model-menu message rendering
5
5
  */
6
6
  import { getCanonicalModelId, modelsMatch, parseTelegramCliScopedModelPatterns, resolveScopedModelPatterns, sortScopedModels, } from "./model.js";
7
+ import { TelegramApiAuthorityError, } from "./telegram-api.js";
7
8
  const TELEGRAM_MODEL_MENU_CACHE_TTL_MS = 5000;
8
9
  const TELEGRAM_MODEL_MENU_STATE_TTL_MS = 10 * 60 * 1000;
9
10
  const MAX_STORED_TELEGRAM_MODEL_MENUS = 50;
11
+ /** Per-open delivery lifetime; never retain this guard in stored callback state. */
12
+ export function createTelegramMenuDelivery(target, options, deps) {
13
+ const assertAuthority = options?.assertAuthority, capturedTarget = { ...target };
14
+ const sendInteractiveMessage = deps.sendInteractiveMessage, storeModelMenuState = deps.storeModelMenuState;
15
+ let requestIssued = false;
16
+ const assertCurrent = () => {
17
+ try {
18
+ assertAuthority?.();
19
+ }
20
+ catch {
21
+ throw new TelegramApiAuthorityError(requestIssued);
22
+ }
23
+ };
24
+ assertCurrent();
25
+ return {
26
+ assertCurrent,
27
+ assertAuthority,
28
+ sendInteractiveMessage: async (...args) => {
29
+ assertCurrent();
30
+ const [chatId, text, mode, replyMarkup, sendOptions] = args;
31
+ const messageId = await sendInteractiveMessage(assertAuthority ? capturedTarget.chatId : chatId, text, mode, replyMarkup, assertAuthority
32
+ ? { ...sendOptions, target: { ...capturedTarget }, assertAuthority }
33
+ : sendOptions);
34
+ requestIssued = true;
35
+ assertCurrent();
36
+ return messageId;
37
+ },
38
+ storeModelMenuState: (state) => {
39
+ assertCurrent();
40
+ if (assertAuthority &&
41
+ (state.chatId !== capturedTarget.chatId ||
42
+ state.threadId !== capturedTarget.threadId)) {
43
+ throw new TelegramApiAuthorityError(requestIssued);
44
+ }
45
+ storeModelMenuState(state);
46
+ assertCurrent();
47
+ },
48
+ };
49
+ }
10
50
  export const TELEGRAM_MODEL_PAGE_SIZE = 6;
11
51
  const TELEGRAM_MODEL_PAGE_PICKER_ROW_SIZE = 4;
12
52
  export const MODEL_MENU_TITLE = "<b>🤖 Choose a model:</b>";
@@ -42,11 +82,13 @@ function applyTelegramMenuRenderPayload(state, payload) {
42
82
  state.mode = payload.nextMode;
43
83
  return payload;
44
84
  }
45
- async function editTelegramMenuMessage(state, payload, deps) {
85
+ /** Apply the payload mode to state, then edit the existing menu message in place. */
86
+ export async function editTelegramMenuMessage(state, payload, deps) {
46
87
  const appliedPayload = applyTelegramMenuRenderPayload(state, payload);
47
88
  await deps.editInteractiveMessage(state.chatId, state.messageId, appliedPayload.text, appliedPayload.mode, appliedPayload.replyMarkup);
48
89
  }
49
- function sendTelegramMenuMessage(state, payload, deps) {
90
+ /** Apply the payload mode to state, then send a fresh menu message to its exact chat/Thread. */
91
+ export function sendTelegramMenuMessage(state, payload, deps) {
50
92
  const appliedPayload = applyTelegramMenuRenderPayload(state, payload);
51
93
  return deps.sendInteractiveMessage(state.chatId, appliedPayload.text, appliedPayload.mode, appliedPayload.replyMarkup, state.threadId !== undefined
52
94
  ? { target: { chatId: state.chatId, threadId: state.threadId } }
@@ -366,7 +408,7 @@ export function buildTelegramModelCallbackPlan(params) {
366
408
  if (action.action === "open") {
367
409
  const detailResult = applyTelegramModelDetailSelection(params.state, action.value);
368
410
  if (detailResult === "invalid") {
369
- return { kind: "answer", text: "Selected model is no longer available." };
411
+ return { kind: "answer", text: "Selected model is no longer available" };
370
412
  }
371
413
  return { kind: "update-menu" };
372
414
  }
@@ -376,12 +418,12 @@ export function buildTelegramModelCallbackPlan(params) {
376
418
  if (params.state.canMutateScope === false) {
377
419
  return {
378
420
  kind: "answer",
379
- text: "Model scope is controlled by CLI --models.",
421
+ text: "Model scope is controlled by CLI --models",
380
422
  };
381
423
  }
382
424
  const selectionResult = getTelegramSelectedDetailModel(params.state);
383
425
  if (selectionResult.kind !== "selected") {
384
- return { kind: "answer", text: "Selected model is no longer available." };
426
+ return { kind: "answer", text: "Selected model is no longer available" };
385
427
  }
386
428
  const model = selectionResult.selection.model;
387
429
  const enabled = action.action === "scope-toggle"
@@ -406,10 +448,10 @@ export function buildTelegramModelCallbackPlan(params) {
406
448
  ? getTelegramSelectedDetailModel(params.state)
407
449
  : getTelegramModelSelection(params.state, action.value);
408
450
  if (selectionResult.kind === "invalid") {
409
- return { kind: "answer", text: "Invalid model selection." };
451
+ return { kind: "answer", text: "Invalid model selection" };
410
452
  }
411
453
  if (selectionResult.kind === "missing") {
412
- return { kind: "answer", text: "Selected model is no longer available." };
454
+ return { kind: "answer", text: "Selected model is no longer available" };
413
455
  }
414
456
  const selection = selectionResult.selection;
415
457
  if (modelsMatch(selection.model, params.activeModel)) {
@@ -428,7 +470,7 @@ export function buildTelegramModelCallbackPlan(params) {
428
470
  if (!params.canRestartBusyRun) {
429
471
  return {
430
472
  kind: "answer",
431
- text: "Pi is busy. Send /abort, /next, or /stop.",
473
+ text: "Pi is busy. Send /abort, /next, or /stop",
432
474
  };
433
475
  }
434
476
  return {
@@ -481,7 +523,7 @@ export async function handleTelegramModelMenuCallbackAction(callbackQueryId, par
481
523
  }
482
524
  if (plan.kind === "persist-scope") {
483
525
  if (!deps.persistScopedModelPatterns) {
484
- await deps.answerCallbackQuery(callbackQueryId, "Scoped model persistence is unavailable.");
526
+ await deps.answerCallbackQuery(callbackQueryId, "Scoped model persistence is unavailable");
485
527
  return true;
486
528
  }
487
529
  await deps.persistScopedModelPatterns(plan.patterns);
@@ -499,7 +541,7 @@ export async function handleTelegramModelMenuCallbackAction(callbackQueryId, par
499
541
  }
500
542
  const changed = await deps.setModel(plan.selection.model);
501
543
  if (changed === false) {
502
- await deps.answerCallbackQuery(callbackQueryId, "Model is not available.");
544
+ await deps.answerCallbackQuery(callbackQueryId, "Model is not available");
503
545
  return true;
504
546
  }
505
547
  deps.setCurrentModel(plan.selection.model);
@@ -526,7 +568,7 @@ export async function handleTelegramModelMenuCallbackAction(callbackQueryId, par
526
568
  const restarted = await deps.restartInterruptedTelegramTurn(plan.selection, continuationTurn);
527
569
  await deps.updateModelMenuMessage();
528
570
  if (!restarted) {
529
- await deps.answerCallbackQuery(callbackQueryId, "Pi is busy. Send /abort, /next, or /stop.");
571
+ await deps.answerCallbackQuery(callbackQueryId, "Pi is busy. Send /abort, /next, or /stop");
530
572
  return true;
531
573
  }
532
574
  }
@@ -4,9 +4,10 @@
4
4
  * Owns queue-menu rendering, queue item callbacks, and queue-menu runtime adapters while core queue mechanics stay in queue
5
5
  */
6
6
  import type { TelegramInlineKeyboardMarkup } from "./keyboard.ts";
7
- import type { TelegramModelMenuState } from "./menu.ts";
7
+ import { type TelegramMenuMessageRuntimeDeps, type TelegramModelMenuState } from "./menu-model.ts";
8
8
  import type { MenuModel } from "./model.ts";
9
9
  import * as Queue from "./queue.ts";
10
+ import type { TelegramApiCallOptions } from "./telegram-api.ts";
10
11
  type TelegramQueueMenuReplyMarkup = TelegramInlineKeyboardMarkup;
11
12
  interface TelegramQueueMenuCallbackQuery {
12
13
  id: string;
@@ -19,16 +20,16 @@ interface TelegramQueueMenuCallbackQuery {
19
20
  };
20
21
  }
21
22
  interface TelegramQueueMenuRuntime<Context> {
22
- openQueueMenu: (chatId: number, replyToMessageId: number, ctx: Context) => Promise<void>;
23
+ openQueueMenu: (chatId: number, replyToMessageId: number, ctx: Context, threadId?: number, options?: Pick<TelegramApiCallOptions, "assertAuthority">) => Promise<void>;
23
24
  handleCallbackQuery: (query: TelegramQueueMenuCallbackQuery, ctx: Context) => Promise<boolean>;
24
25
  }
25
26
  export declare function createTelegramQueueMenuRuntime<Context, TModel extends MenuModel = MenuModel>(deps: {
26
27
  telegramQueueStore: Queue.TelegramQueueStateStore<Context>;
27
28
  queueMutationRuntime: Queue.TelegramQueueMutationController<Context>;
28
- sendInteractiveMessage: (chatId: number, text: string, mode: "html", replyMarkup: TelegramQueueMenuReplyMarkup) => Promise<number | undefined>;
29
+ sendInteractiveMessage: TelegramMenuMessageRuntimeDeps["sendInteractiveMessage"];
29
30
  editInteractiveMessage: (chatId: number, messageId: number, text: string, mode: "html", replyMarkup: TelegramQueueMenuReplyMarkup) => Promise<void>;
30
31
  answerCallbackQuery: (callbackQueryId: string, text?: string) => Promise<void>;
31
- getModelMenuState: (chatId: number, ctx: Context) => Promise<TelegramModelMenuState<TModel>>;
32
+ getModelMenuState: (chatId: number, ctx: Context, threadId?: number) => Promise<TelegramModelMenuState<TModel>>;
32
33
  getStoredModelMenuState: (messageId: number | undefined, chatId?: number) => TelegramModelMenuState<TModel> | undefined;
33
34
  storeModelMenuState: (state: TelegramModelMenuState<TModel>) => void;
34
35
  updateStatusMessage: (state: TelegramModelMenuState<TModel>, ctx: Context) => Promise<void>;