@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
package/lib/logging.ts CHANGED
@@ -5,21 +5,22 @@
5
5
  */
6
6
 
7
7
  import {
8
+ appendFileSync,
8
9
  copyFileSync,
9
10
  existsSync,
10
11
  mkdirSync,
11
12
  statSync,
12
13
  writeFileSync,
13
- appendFileSync,
14
14
  } from "node:fs";
15
15
  import { basename, dirname, join } from "node:path";
16
+
17
+ import { withTelegramFileTransaction } from "./locks.ts";
16
18
  import {
17
19
  resolveAgentDir,
20
+ resolveTelegramPreviousSharedRuntimeLogPath,
18
21
  resolveTelegramProfileTempFilePath,
19
22
  resolveTelegramRuntimeLogPath,
20
- resolveTelegramPreviousSharedRuntimeLogPath,
21
23
  } from "./paths.ts";
22
- import { withTelegramFileTransaction } from "./locks.ts";
23
24
  import * as Status from "./status.ts";
24
25
 
25
26
  type TelegramLogPathInput = string | (() => string);
@@ -95,9 +96,13 @@ function safeJsonLine(value: unknown): string {
95
96
  export function createTelegramRuntimeJsonlLog(
96
97
  options: TelegramRuntimeJsonlLogOptions = {},
97
98
  ): TelegramRuntimeJsonlLog {
98
- const shared = options.sharedProfiles ? { ...options.sharedProfiles } : undefined;
99
- const pathSource = options.path, previousSource = options.previousPath;
100
- const canReset = options.canReset, commitReset = options.commitReset;
99
+ const shared = options.sharedProfiles
100
+ ? { ...options.sharedProfiles }
101
+ : undefined;
102
+ const pathSource = options.path,
103
+ previousSource = options.previousPath;
104
+ const canReset = options.canReset,
105
+ commitReset = options.commitReset;
101
106
  const resolvePath = () =>
102
107
  typeof pathSource === "function"
103
108
  ? pathSource()
@@ -105,7 +110,12 @@ export function createTelegramRuntimeJsonlLog(
105
110
  const resolvePreviousPath = () => {
106
111
  if (typeof previousSource === "function") return previousSource();
107
112
  if (previousSource) return previousSource;
108
- return shared ? join(dirname(resolvePath()), "logs", `${basename(resolvePath(), ".jsonl")}._prev.jsonl`)
113
+ return shared
114
+ ? join(
115
+ dirname(resolvePath()),
116
+ "logs",
117
+ `${basename(resolvePath(), ".jsonl")}._prev.jsonl`,
118
+ )
109
119
  : resolvePath().replace(/\.jsonl$/u, "._prev.jsonl");
110
120
  };
111
121
  const maxBytes = options.maxBytes ?? DEFAULT_MAX_LOG_BYTES;
@@ -121,13 +131,25 @@ export function createTelegramRuntimeJsonlLog(
121
131
  rotationCurrent?: () => boolean;
122
132
  }[] = [];
123
133
  const getProfile = () => shared?.getProfileName() ?? "default";
124
- const transactionPath = (path: string) => shared ? join(dirname(path), "runtime", `${basename(path)}.transaction`) : `${path}.transaction`;
125
- const captureRotation = (path: string, previousPath: string, profile: string): (() => boolean) | undefined => {
134
+ const transactionPath = (path: string) =>
135
+ shared
136
+ ? join(dirname(path), "runtime", `${basename(path)}.transaction`)
137
+ : `${path}.transaction`;
138
+ const captureRotation = (
139
+ path: string,
140
+ previousPath: string,
141
+ profile: string,
142
+ ): (() => boolean) | undefined => {
126
143
  if (!shared) return undefined;
127
144
  const authority = shared.captureAuthority();
128
- return () => authority?.() === true && resolvePath() === path && resolvePreviousPath() === previousPath && getProfile() === profile;
145
+ return () =>
146
+ authority?.() === true &&
147
+ resolvePath() === path &&
148
+ resolvePreviousPath() === previousPath &&
149
+ getProfile() === profile;
129
150
  };
130
- const scopeStorageKey = (path: string, profile: string) => shared ? JSON.stringify([path, profile]) : path;
151
+ const scopeStorageKey = (path: string, profile: string) =>
152
+ shared ? JSON.stringify([path, profile]) : path;
131
153
 
132
154
  const ensureParent = (path: string) => {
133
155
  mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
@@ -147,14 +169,15 @@ export function createTelegramRuntimeJsonlLog(
147
169
  profile?: string,
148
170
  isCurrent?: () => boolean,
149
171
  ): boolean => {
150
- const line = safeJsonLine({
151
- at: getNowMs(),
152
- kind: "reset",
153
- reason,
154
- scope,
155
- previousPath,
156
- ...(shared ? { profile } : {}),
157
- }) + "\n";
172
+ const line =
173
+ safeJsonLine({
174
+ at: getNowMs(),
175
+ kind: "reset",
176
+ reason,
177
+ scope,
178
+ previousPath,
179
+ ...(shared ? { profile } : {}),
180
+ }) + "\n";
158
181
  if (shared && !isCurrent?.()) return false;
159
182
  ensureParent(path);
160
183
  preserveCurrentLog(path, previousPath);
@@ -177,11 +200,29 @@ export function createTelegramRuntimeJsonlLog(
177
200
  const commit = () => {
178
201
  if (shared && !isCurrent?.()) return;
179
202
  if (shared) {
180
- const line = safeJsonLine({ at: getNowMs(), kind: "reset", reason, scope, profile }) + "\n";
203
+ const line =
204
+ safeJsonLine({
205
+ at: getNowMs(),
206
+ kind: "reset",
207
+ reason,
208
+ scope,
209
+ profile,
210
+ }) + "\n";
181
211
  if (!isCurrent?.()) return;
182
212
  ensureParent(path);
183
- if (existsSync(path) && statSync(path).size + Buffer.byteLength(line) > maxBytes &&
184
- !writeResetLocked(path, previousPath, "max-bytes", { maxBytes }, profile, isCurrent)) return;
213
+ if (
214
+ existsSync(path) &&
215
+ statSync(path).size + Buffer.byteLength(line) > maxBytes &&
216
+ !writeResetLocked(
217
+ path,
218
+ previousPath,
219
+ "max-bytes",
220
+ { maxBytes },
221
+ profile,
222
+ isCurrent,
223
+ )
224
+ )
225
+ return;
185
226
  if (!isCurrent?.()) return;
186
227
  appendFileSync(path, line, { mode: 0o600 });
187
228
  didReset = true;
@@ -197,10 +238,22 @@ export function createTelegramRuntimeJsonlLog(
197
238
  };
198
239
 
199
240
  const appendEvent = (event: TelegramRuntimeJsonlEvent) => {
200
- const path = resolvePath(), previousPath = resolvePreviousPath(), profile = getProfile();
241
+ const path = resolvePath(),
242
+ previousPath = resolvePreviousPath(),
243
+ profile = getProfile();
201
244
  const rotationCurrent = captureRotation(path, previousPath, profile);
202
- const line = safeJsonLine({ kind: "event", ...event, ...(shared ? { profile } : {}) }) + "\n";
203
- queuedAppends.push({ path, previousPath, line, ...(shared ? { profile, rotationCurrent } : {}) });
245
+ const line =
246
+ safeJsonLine({
247
+ kind: "event",
248
+ ...event,
249
+ ...(shared ? { profile } : {}),
250
+ }) + "\n";
251
+ queuedAppends.push({
252
+ path,
253
+ previousPath,
254
+ line,
255
+ ...(shared ? { profile, rotationCurrent } : {}),
256
+ });
204
257
  if (appendScheduled) return;
205
258
  appendScheduled = true;
206
259
  pending = pending
@@ -234,7 +287,9 @@ export function createTelegramRuntimeJsonlLog(
234
287
  chunk = "";
235
288
  chunkBytes = 0;
236
289
  };
237
- const rotate = (entry: typeof queuedAppends[number]): boolean => {
290
+ const rotate = (
291
+ entry: (typeof queuedAppends)[number],
292
+ ): boolean => {
238
293
  if (canReset && !canReset()) return false;
239
294
  if (shared && !entry.rotationCurrent?.()) return false;
240
295
  let rotated = false;
@@ -280,20 +335,27 @@ export function createTelegramRuntimeJsonlLog(
280
335
  return {
281
336
  getPath: resolvePath,
282
337
  reset(reason, scope) {
283
- const path = resolvePath(), profile = getProfile();
338
+ const path = resolvePath(),
339
+ profile = getProfile();
284
340
  try {
285
341
  if (writeReset(reason, scope, path, profile)) {
286
- scopeKeys.set(scopeStorageKey(path, profile), scope ? safeJsonLine(scope) : undefined);
342
+ scopeKeys.set(
343
+ scopeStorageKey(path, profile),
344
+ scope ? safeJsonLine(scope) : undefined,
345
+ );
287
346
  }
288
347
  } catch {
289
348
  // Diagnostics must never break Telegram runtime behavior.
290
349
  }
291
350
  },
292
351
  resetIfScopeChanged(nextScopeKey, reason, scope) {
293
- const path = resolvePath(), profile = getProfile(), key = scopeStorageKey(path, profile);
352
+ const path = resolvePath(),
353
+ profile = getProfile(),
354
+ key = scopeStorageKey(path, profile);
294
355
  if (scopeKeys.get(key) === nextScopeKey) return;
295
356
  try {
296
- if (writeReset(reason, scope, path, profile)) scopeKeys.set(key, nextScopeKey);
357
+ if (writeReset(reason, scope, path, profile))
358
+ scopeKeys.set(key, nextScopeKey);
297
359
  } catch {
298
360
  // Diagnostics must never break Telegram runtime behavior.
299
361
  }
@@ -312,7 +374,9 @@ interface TelegramRuntimeDiagnosticsStatusPorts<TContext> {
312
374
  instanceId: string;
313
375
  updateStatus(ctx: TContext, error?: string): void;
314
376
  getStatusState(): Status.TelegramBridgeStatusLineState;
315
- persistSnapshot(snapshot: ReturnType<typeof Status.createTelegramStatusSnapshot>): Promise<void>;
377
+ persistSnapshot(
378
+ snapshot: ReturnType<typeof Status.createTelegramStatusSnapshot>,
379
+ ): Promise<void>;
316
380
  session?: {
317
381
  get(): TContext | undefined;
318
382
  getGeneration(): number;
@@ -342,12 +406,17 @@ interface TelegramRuntimeDiagnosticsRuntime<TContext> {
342
406
  scheduleSnapshotPersist(): void;
343
407
  }
344
408
 
345
- export function createTelegramRuntimeDiagnosticsRuntime<
346
- TContext,
347
- >(options: {
348
- sharedFile?: boolean;
349
- snapshotTimer?: Pick<Parameters<typeof Status.createTelegramRuntimeDiagnosticsSnapshotScheduler>[0], "setTimer" | "clearTimer">;
350
- } = {}): TelegramRuntimeDiagnosticsRuntime<TContext> {
409
+ export function createTelegramRuntimeDiagnosticsRuntime<TContext>(
410
+ options: {
411
+ sharedFile?: boolean;
412
+ snapshotTimer?: Pick<
413
+ Parameters<
414
+ typeof Status.createTelegramRuntimeDiagnosticsSnapshotScheduler
415
+ >[0],
416
+ "setTimer" | "clearTimer"
417
+ >;
418
+ } = {},
419
+ ): TelegramRuntimeDiagnosticsRuntime<TContext> {
351
420
  const sharedFile = options.sharedFile === true;
352
421
  let getBotToken = (): string | undefined => undefined;
353
422
  let getProfileName = (): string | undefined => undefined;
@@ -359,10 +428,22 @@ export function createTelegramRuntimeDiagnosticsRuntime<
359
428
  getBotToken: () => getBotToken(),
360
429
  });
361
430
  const jsonl = createTelegramRuntimeJsonlLog({
362
- path: () => sharedFile ? resolveTelegramRuntimeLogPath() : getTelegramRuntimeLogPath(undefined, getProfileName()),
363
- previousPath: () => sharedFile ? resolveTelegramPreviousSharedRuntimeLogPath()
364
- : getTelegramPreviousRuntimeLogPath(undefined, getProfileName()),
365
- ...(sharedFile ? { sharedProfiles: { getProfileName: () => getProfileName(), captureAuthority: () => captureAuthority() } } : {}),
431
+ path: () =>
432
+ sharedFile
433
+ ? resolveTelegramRuntimeLogPath()
434
+ : getTelegramRuntimeLogPath(undefined, getProfileName()),
435
+ previousPath: () =>
436
+ sharedFile
437
+ ? resolveTelegramPreviousSharedRuntimeLogPath()
438
+ : getTelegramPreviousRuntimeLogPath(undefined, getProfileName()),
439
+ ...(sharedFile
440
+ ? {
441
+ sharedProfiles: {
442
+ getProfileName: () => getProfileName(),
443
+ captureAuthority: () => captureAuthority(),
444
+ },
445
+ }
446
+ : {}),
366
447
  canReset: () => canReset(),
367
448
  commitReset: (commit) => commitReset(commit),
368
449
  });
@@ -380,25 +461,33 @@ export function createTelegramRuntimeDiagnosticsRuntime<
380
461
  const ports = statusPorts;
381
462
  if (!ports) return undefined;
382
463
  if (!ports.session) return () => statusPorts === ports;
383
- const session = ports.session, ctx = session.get(), generation = session.getGeneration();
384
- if (ctx === undefined || !session.isCurrent(ctx, generation)) return undefined;
464
+ const session = ports.session,
465
+ ctx = session.get(),
466
+ generation = session.getGeneration();
467
+ if (ctx === undefined || !session.isCurrent(ctx, generation))
468
+ return undefined;
385
469
  return () => statusPorts === ports && session.isCurrent(ctx, generation);
386
470
  };
387
- const requestSnapshotPersist = Status.createTelegramRuntimeDiagnosticsSnapshotScheduler({
388
- ...options.snapshotTimer,
389
- captureScope: captureSnapshotScope,
390
- async persistSnapshot(isCurrent) {
391
- const ports = statusPorts;
392
- // Projection may acquire journal guards or recover a snapshot: fence before reading it.
393
- if (!ports || !isCurrent()) return;
394
- const snapshot = Status.createTelegramStatusSnapshot(ports.getStatusState());
395
- if (!isCurrent()) return;
396
- await ports.persistSnapshot(snapshot);
397
- },
398
- recordError(error) {
399
- events.record("telegram", error, { phase: "runtime-diagnostics-snapshot-persist" });
400
- },
401
- });
471
+ const requestSnapshotPersist =
472
+ Status.createTelegramRuntimeDiagnosticsSnapshotScheduler({
473
+ ...options.snapshotTimer,
474
+ captureScope: captureSnapshotScope,
475
+ async persistSnapshot(isCurrent) {
476
+ const ports = statusPorts;
477
+ // Projection may acquire journal guards or recover a snapshot: fence before reading it.
478
+ if (!ports || !isCurrent()) return;
479
+ const snapshot = Status.createTelegramStatusSnapshot(
480
+ ports.getStatusState(),
481
+ );
482
+ if (!isCurrent()) return;
483
+ await ports.persistSnapshot(snapshot);
484
+ },
485
+ recordError(error) {
486
+ events.record("telegram", error, {
487
+ phase: "runtime-diagnostics-snapshot-persist",
488
+ });
489
+ },
490
+ });
402
491
  const updateRuntimeLogScope = function (reason: string): void {
403
492
  if (!statusPorts) return;
404
493
  const scope = Status.createTelegramRuntimeLogScope({
package/lib/media.ts CHANGED
@@ -428,15 +428,6 @@ function appendTelegramReplyContext(
428
428
  return text ? `${text}\n\n${replyContext}` : `_\n\n${replyContext}`;
429
429
  }
430
430
 
431
- export function extractTelegramMessagePromptText(
432
- message: TelegramMediaMessage,
433
- ): string {
434
- return appendTelegramReplyContext(
435
- extractTelegramMessageText(message),
436
- buildTelegramReplyContextBlock(message),
437
- );
438
- }
439
-
440
431
  export function extractTelegramMessagesText(
441
432
  messages: TelegramMediaMessage[],
442
433
  ): string {
@@ -601,35 +592,37 @@ export function queueTelegramMediaGroupMessage<
601
592
  return true;
602
593
  }
603
594
 
604
- export function createTelegramMediaGroupController<
605
- TMessage extends TelegramMediaGroupMessage,
606
- TContext = unknown,
595
+ /** Timer and dispatch lifecycle shared by delayed media-group and text-group coalescing. */
596
+ export interface TelegramPendingGroupState<TMessage, TContext> {
597
+ messages: TMessage[];
598
+ context?: TContext;
599
+ flushTimer?: ReturnType<typeof setTimeout>;
600
+ dispatching?: boolean;
601
+ suspended?: boolean;
602
+ reschedule?: () => void;
603
+ dispatchNow?: () => Promise<void>;
604
+ }
605
+
606
+ export interface TelegramPendingGroupLifecycle<TContext> {
607
+ flushMessage: (messageId: number) => Promise<boolean>;
608
+ suspend: () => void;
609
+ resume: (context: TContext) => void;
610
+ clear: () => void;
611
+ }
612
+
613
+ export function createTelegramPendingGroupLifecycle<
614
+ TMessage extends { message_id: number },
615
+ TContext,
607
616
  >(
608
- options: TelegramMediaGroupControllerOptions = {},
609
- ): TelegramMediaGroupController<TMessage, TContext> {
610
- const groups = new Map<string, TelegramMediaGroupState<TMessage, TContext>>();
611
- const debounceMs = options.debounceMs ?? TELEGRAM_MEDIA_GROUP_DEBOUNCE_MS;
612
- const setTimer =
613
- options.setTimer ??
614
- ((callback: () => void, ms: number): ReturnType<typeof setTimeout> =>
615
- setTimeout(callback, ms));
616
- const clearTimer = options.clearTimer ?? clearTimeout;
617
+ groups: Map<string, TelegramPendingGroupState<TMessage, TContext>>,
618
+ clearTimer: (timer: ReturnType<typeof setTimeout>) => void,
619
+ ): TelegramPendingGroupLifecycle<TContext> {
617
620
  return {
618
- queueMessage: ({ message, context, dispatchMessages }) =>
619
- queueTelegramMediaGroupMessage({
620
- message,
621
- context,
622
- groups,
623
- debounceMs,
624
- setTimer,
625
- clearTimer,
626
- dispatchMessages,
627
- }),
628
- removeMessages: (messageIds) =>
629
- removePendingTelegramMediaGroupMessages(groups, messageIds, clearTimer),
630
621
  async flushMessage(messageId) {
631
622
  for (const state of groups.values()) {
632
- if (!state.messages.some((message) => message.message_id === messageId)) {
623
+ if (
624
+ !state.messages.some((message) => message.message_id === messageId)
625
+ ) {
633
626
  continue;
634
627
  }
635
628
  if (state.flushTimer) clearTimer(state.flushTimer);
@@ -668,6 +661,36 @@ export function createTelegramMediaGroupController<
668
661
  };
669
662
  }
670
663
 
664
+ export function createTelegramMediaGroupController<
665
+ TMessage extends TelegramMediaGroupMessage,
666
+ TContext = unknown,
667
+ >(
668
+ options: TelegramMediaGroupControllerOptions = {},
669
+ ): TelegramMediaGroupController<TMessage, TContext> {
670
+ const groups = new Map<string, TelegramMediaGroupState<TMessage, TContext>>();
671
+ const debounceMs = options.debounceMs ?? TELEGRAM_MEDIA_GROUP_DEBOUNCE_MS;
672
+ const setTimer =
673
+ options.setTimer ??
674
+ ((callback: () => void, ms: number): ReturnType<typeof setTimeout> =>
675
+ setTimeout(callback, ms));
676
+ const clearTimer = options.clearTimer ?? clearTimeout;
677
+ return {
678
+ queueMessage: ({ message, context, dispatchMessages }) =>
679
+ queueTelegramMediaGroupMessage({
680
+ message,
681
+ context,
682
+ groups,
683
+ debounceMs,
684
+ setTimer,
685
+ clearTimer,
686
+ dispatchMessages,
687
+ }),
688
+ removeMessages: (messageIds) =>
689
+ removePendingTelegramMediaGroupMessages(groups, messageIds, clearTimer),
690
+ ...createTelegramPendingGroupLifecycle(groups, clearTimer),
691
+ };
692
+ }
693
+
671
694
  export function createTelegramMediaGroupDispatchRuntime<
672
695
  TMessage extends TelegramMediaGroupMessage,
673
696
  TContext,
@@ -693,7 +716,8 @@ export function createTelegramMediaGroupDispatchRuntime<
693
716
  };
694
717
  }
695
718
 
696
- function appendTelegramListSection(
719
+ /** Pure `[title]` bullet section appended after a blank line; empty items leave text unchanged. */
720
+ export function appendTelegramListSection(
697
721
  text: string,
698
722
  title: string,
699
723
  items: string[],
@@ -703,7 +727,7 @@ function appendTelegramListSection(
703
727
  return `${prefix}[${title}]\n${items.map((item) => `- ${item}`).join("\n")}`;
704
728
  }
705
729
 
706
- function appendTelegramAttachmentSection(
730
+ export function appendTelegramAttachmentSection(
707
731
  text: string,
708
732
  files: Pick<DownloadedTelegramFile, "path">[],
709
733
  ): string {
@@ -875,7 +899,9 @@ export function collectTelegramFileInfos(
875
899
  files.push({
876
900
  file_id: message.document.file_id,
877
901
  fileName,
878
- ...(message.document.file_name ? { userFileName: message.document.file_name } : {}),
902
+ ...(message.document.file_name
903
+ ? { userFileName: message.document.file_name }
904
+ : {}),
879
905
  mimeType: message.document.mime_type,
880
906
  kind: "document",
881
907
  isImage: isImageMimeType(message.document.mime_type),
@@ -891,7 +917,9 @@ export function collectTelegramFileInfos(
891
917
  files.push({
892
918
  file_id: message.video.file_id,
893
919
  fileName,
894
- ...(message.video.file_name ? { userFileName: message.video.file_name } : {}),
920
+ ...(message.video.file_name
921
+ ? { userFileName: message.video.file_name }
922
+ : {}),
895
923
  mimeType: message.video.mime_type,
896
924
  kind: "video",
897
925
  isImage: false,
@@ -908,7 +936,9 @@ export function collectTelegramFileInfos(
908
936
  files.push({
909
937
  file_id: message.audio.file_id,
910
938
  fileName,
911
- ...(message.audio.file_name ? { userFileName: message.audio.file_name } : {}),
939
+ ...(message.audio.file_name
940
+ ? { userFileName: message.audio.file_name }
941
+ : {}),
912
942
  mimeType: message.audio.mime_type,
913
943
  kind: "audio",
914
944
  isImage: false,
@@ -938,7 +968,9 @@ export function collectTelegramFileInfos(
938
968
  files.push({
939
969
  file_id: message.animation.file_id,
940
970
  fileName,
941
- ...(message.animation.file_name ? { userFileName: message.animation.file_name } : {}),
971
+ ...(message.animation.file_name
972
+ ? { userFileName: message.animation.file_name }
973
+ : {}),
942
974
  mimeType: message.animation.mime_type,
943
975
  kind: "animation",
944
976
  isImage: false,
@@ -952,16 +984,23 @@ export function collectTelegramFileInfos(
952
984
  files.push({
953
985
  file_id: message.sticker.file_id,
954
986
  fileName: `sticker-${message.message_id}${extension}`,
955
- mimeType: video ? "video/webm" : animated ? "application/x-tgsticker" : "image/webp",
987
+ mimeType: video
988
+ ? "video/webm"
989
+ : animated
990
+ ? "application/x-tgsticker"
991
+ : "image/webp",
956
992
  kind: "sticker",
957
993
  isImage: !video && !animated,
958
994
  });
959
995
  }
960
996
  for (const file of files.slice(firstOfMessage)) {
961
- file.source = { kind: file.kind, messageId: message.message_id,
997
+ file.source = {
998
+ kind: file.kind,
999
+ messageId: message.message_id,
962
1000
  ...(file.index !== undefined ? { index: file.index } : {}),
963
1001
  ...(file.userFileName ? { userFileName: file.userFileName } : {}),
964
- ...(message.chat ? { chat: message.chat } : {}) };
1002
+ ...(message.chat ? { chat: message.chat } : {}),
1003
+ };
965
1004
  }
966
1005
  }
967
1006
  const seenFileIds = new Set<string>();