@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
package/lib/config.ts CHANGED
@@ -15,20 +15,20 @@ import {
15
15
  unlinkSync,
16
16
  writeFileSync,
17
17
  } from "node:fs";
18
- import { chmod, mkdir, rename, writeFile } from "node:fs/promises";
18
+
19
+ import type { CommandTemplateObjectConfig } from "./command-templates.ts";
20
+ import type { TelegramInboundHandlerConfig } from "./inbound.ts";
21
+ import { withTelegramFileTransaction } from "./locks.ts";
19
22
  import {
20
23
  resolveAgentDir,
21
24
  resolveTelegramConfigPath,
22
25
  TELEGRAM_DEFAULT_PROFILE_NAME,
23
26
  } from "./paths.ts";
24
- export { TELEGRAM_DEFAULT_PROFILE_NAME } from "./paths.ts";
25
-
26
- import type { CommandTemplateObjectConfig } from "./command-templates.ts";
27
+ import { isPiStaleContextError } from "./pi.ts";
27
28
  import { isWireRecord as isPlainConfigRecord } from "./wire.ts";
28
- import type { TelegramInboundHandlerConfig } from "./inbound.ts";
29
- import { withTelegramFileTransaction } from "./locks.ts";
30
29
 
31
- const CONFIG_RUNTIME_KEY = "__piTelegramConfigRuntime__";
30
+ export { TELEGRAM_DEFAULT_PROFILE_NAME } from "./paths.ts";
31
+
32
32
  const CONFIG_REPLACE_RETRY_ATTEMPTS = 5;
33
33
  const CONFIG_REPLACE_RETRY_DELAY_MS = 25;
34
34
 
@@ -136,21 +136,22 @@ export interface ResolvedTelegramTimeConfig {
136
136
  }
137
137
 
138
138
  export type TelegramThreadDisplayMode =
139
- | "letters"
140
- | "names"
141
- | "directories"
142
- | "directory-snake"
143
- | "directory-title";
139
+ "letters" | "names" | "directories" | "directory-snake" | "directory-title";
144
140
 
145
141
  const TELEGRAM_THREAD_DISPLAY_MODES: readonly TelegramThreadDisplayMode[] = [
146
- "letters", "names", "directory-snake", "directory-title",
142
+ "letters",
143
+ "names",
144
+ "directory-snake",
145
+ "directory-title",
147
146
  ];
148
147
 
149
148
  export function resolveTelegramThreadDisplayMode(
150
149
  config: Pick<TelegramConfig, "threadDisplayMode">,
151
150
  ): TelegramThreadDisplayMode {
152
- return TELEGRAM_THREAD_DISPLAY_MODES.includes(config.threadDisplayMode as TelegramThreadDisplayMode)
153
- ? config.threadDisplayMode as TelegramThreadDisplayMode
151
+ return TELEGRAM_THREAD_DISPLAY_MODES.includes(
152
+ config.threadDisplayMode as TelegramThreadDisplayMode,
153
+ )
154
+ ? (config.threadDisplayMode as TelegramThreadDisplayMode)
154
155
  : "letters";
155
156
  }
156
157
 
@@ -164,21 +165,27 @@ export async function setTelegramThreadDisplayMode(
164
165
  }
165
166
  const profile = store.getActiveProfileName();
166
167
  const current = () => isCurrent() && store.getActiveProfileName() === profile;
167
- if (!current()) throw new Error("Telegram Thread display setting lost authority.");
168
+ if (!current())
169
+ throw new Error("Telegram Thread display setting lost authority.");
168
170
  await store.load();
169
171
  if (!current() || !store.hasBotToken()) {
170
- throw new Error("Telegram Thread display setting lost its configured profile.");
172
+ throw new Error(
173
+ "Telegram Thread display setting lost its configured profile.",
174
+ );
171
175
  }
172
- await store.persist({ ...store.get(), threadDisplayMode: mode }, { isCurrent: current });
173
- if (!current()) throw new Error("Telegram Thread display setting changed during persistence.");
176
+ await store.persist(
177
+ { ...store.get(), threadDisplayMode: mode },
178
+ { isCurrent: current },
179
+ );
180
+ if (!current())
181
+ throw new Error(
182
+ "Telegram Thread display setting changed during persistence.",
183
+ );
174
184
  }
175
185
 
176
186
  export type TelegramAssistantRenderingMode = "rich" | "html";
177
187
  export type TelegramActivityVerbosity =
178
- | "quiet"
179
- | "thinking"
180
- | "tools"
181
- | "verbose";
188
+ "quiet" | "thinking" | "tools" | "verbose";
182
189
 
183
190
  export interface TelegramConfig {
184
191
  /** @deprecated persisted identity belongs in profiles.default; retained for effective/legacy views */
@@ -304,7 +311,10 @@ export interface TelegramConfigStore {
304
311
  ) => { admitted: false } | { admitted: true; value: T };
305
312
  load: () => Promise<void>;
306
313
  didLastLoadRecoverInvalidConfig: () => boolean;
307
- persist: (config?: TelegramConfig, options?: { isCurrent?: () => boolean }) => Promise<void>;
314
+ persist: (
315
+ config?: TelegramConfig,
316
+ options?: { isCurrent?: () => boolean },
317
+ ) => Promise<void>;
308
318
  }
309
319
 
310
320
  export function createTelegramConfigBotIdGetter(
@@ -338,29 +348,6 @@ export interface TelegramInvalidConfigRecovery {
338
348
  error: unknown;
339
349
  }
340
350
 
341
- export interface TelegramConfigRuntime {
342
- updateVoiceConfig: (voice: NonNullable<TelegramConfig["voice"]>) => void;
343
- }
344
-
345
- export function setGlobalTelegramConfigRuntime(
346
- runtime: TelegramConfigRuntime | undefined,
347
- ): void {
348
- const globals = globalThis as Record<string, unknown>;
349
- if (runtime) globals[CONFIG_RUNTIME_KEY] = runtime;
350
- else delete globals[CONFIG_RUNTIME_KEY];
351
- }
352
-
353
- export function updateTelegramVoiceConfig(
354
- voice: NonNullable<TelegramConfig["voice"]>,
355
- ): boolean {
356
- const runtime = (globalThis as Record<string, unknown>)[
357
- CONFIG_RUNTIME_KEY
358
- ] as TelegramConfigRuntime | undefined;
359
- if (!runtime || typeof runtime.updateVoiceConfig !== "function") return false;
360
- runtime.updateVoiceConfig(voice);
361
- return true;
362
- }
363
-
364
351
  type TelegramMutableConfigStore = Pick<
365
352
  TelegramConfigStore,
366
353
  "get" | "set" | "persist"
@@ -387,22 +374,6 @@ async function loadLatestTelegramConfig(
387
374
  }
388
375
  }
389
376
 
390
- export function bindGlobalTelegramConfigRuntime(
391
- configStore: TelegramMutableConfigStore,
392
- ): void {
393
- setGlobalTelegramConfigRuntime({
394
- updateVoiceConfig(voice) {
395
- const current = configStore.get();
396
- const next = {
397
- ...current,
398
- voice: { ...(current.voice ?? {}), ...voice },
399
- };
400
- configStore.set(next);
401
- void configStore.persist(next);
402
- },
403
- });
404
- }
405
-
406
377
  function getInvalidTelegramConfigRecoveryPath(configPath: string): string {
407
378
  return `${configPath}.invalid-${process.pid}-${Date.now()}`;
408
379
  }
@@ -448,22 +419,6 @@ export async function readTelegramConfig(
448
419
  }
449
420
  }
450
421
 
451
- export async function writeTelegramConfig(
452
- agentDir: string,
453
- configPath: string,
454
- config: TelegramConfig,
455
- ): Promise<void> {
456
- await mkdir(agentDir, { recursive: true });
457
- const tempConfigPath = `${configPath}.tmp-${process.pid}-${Date.now()}`;
458
- await writeFile(tempConfigPath, JSON.stringify(config, null, "\t") + "\n", {
459
- encoding: "utf8",
460
- mode: 0o600,
461
- });
462
- await chmod(tempConfigPath, 0o600);
463
- await rename(tempConfigPath, configPath);
464
- await chmod(configPath, 0o600);
465
- }
466
-
467
422
  function cloneTelegramConfig<T>(value: T): T {
468
423
  return structuredClone(value);
469
424
  }
@@ -544,10 +499,16 @@ export function getTelegramProfileFields(
544
499
  ): TelegramBotProfile | undefined {
545
500
  const token = config.botToken?.trim();
546
501
  if (!token) return undefined;
502
+ return { botToken: token, ...pickTelegramRootProfileFields(config) };
503
+ }
504
+
505
+ /** Root-level profile fields other than the token, including the retired legacy cursor. */
506
+ function pickTelegramRootProfileFields(
507
+ config: TelegramConfig,
508
+ ): Omit<TelegramBotProfile, "botToken"> & TelegramLegacyCursorCarrier {
547
509
  const legacyCursor = (config as TelegramConfig & TelegramLegacyCursorCarrier)
548
510
  .lastUpdateId;
549
511
  return {
550
- botToken: token,
551
512
  ...(config.botUsername !== undefined
552
513
  ? { botUsername: config.botUsername }
553
514
  : {}),
@@ -620,23 +581,7 @@ export function normalizeTelegramDefaultProfileConfig(config: TelegramConfig): {
620
581
  const legacyProfile: Partial<TelegramBotProfile> &
621
582
  TelegramLegacyCursorCarrier = {
622
583
  ...(legacyToken ? { botToken: legacyToken } : {}),
623
- ...(config.botUsername !== undefined
624
- ? { botUsername: config.botUsername }
625
- : {}),
626
- ...(config.botId !== undefined ? { botId: config.botId } : {}),
627
- ...(config.allowedUserId !== undefined
628
- ? { allowedUserId: config.allowedUserId }
629
- : {}),
630
- ...(config.threadDisplayMode !== undefined
631
- ? { threadDisplayMode: config.threadDisplayMode }
632
- : {}),
633
- ...((config as TelegramConfig & TelegramLegacyCursorCarrier)
634
- .lastUpdateId !== undefined
635
- ? {
636
- lastUpdateId: (config as TelegramConfig & TelegramLegacyCursorCarrier)
637
- .lastUpdateId,
638
- }
639
- : {}),
584
+ ...pickTelegramRootProfileFields(config),
640
585
  };
641
586
  if (!canonicalProfile && !legacyToken) {
642
587
  throw new Error("Legacy Telegram default profile has no bot token");
@@ -724,21 +669,31 @@ export function createTelegramConfigStore(
724
669
  );
725
670
  mutationVersion += 1;
726
671
  };
727
- const adoptPersistedConfig = (merged: TelegramConfig, preserveLocalChanges: boolean) => {
672
+ const adoptPersistedConfig = (
673
+ merged: TelegramConfig,
674
+ preserveLocalChanges: boolean,
675
+ ) => {
728
676
  // Local edits are relative to the latest observation, not a queued write's older request baseline.
729
677
  const nextConfig = preserveLocalChanges
730
- ? mergeTelegramConfigDelta(persistedConfig as Record<string, unknown>, config as Record<string, unknown>,
731
- merged as Record<string, unknown>) as TelegramConfig
678
+ ? (mergeTelegramConfigDelta(
679
+ persistedConfig as Record<string, unknown>,
680
+ config as Record<string, unknown>,
681
+ merged as Record<string, unknown>,
682
+ ) as TelegramConfig)
732
683
  : cloneTelegramConfig(merged);
733
684
  persistedConfig = cloneTelegramConfig(merged);
734
685
  config = nextConfig;
735
686
  };
736
687
  const withPersistedPairingProfile = <T>(
737
- profileName: string, tokenSha256: string,
688
+ profileName: string,
689
+ tokenSha256: string,
738
690
  observe: (latest: TelegramConfig, profile: TelegramBotProfile) => T,
739
691
  ): T => {
740
- if ((profileName !== TELEGRAM_DEFAULT_PROFILE_NAME && !isValidTelegramProfileName(profileName)) ||
741
- !/^[a-f0-9]{64}$/u.test(tokenSha256)) {
692
+ if (
693
+ (profileName !== TELEGRAM_DEFAULT_PROFILE_NAME &&
694
+ !isValidTelegramProfileName(profileName)) ||
695
+ !/^[a-f0-9]{64}$/u.test(tokenSha256)
696
+ ) {
742
697
  throw new Error("Invalid Telegram pairing admission identity.");
743
698
  }
744
699
  return withTelegramFileTransaction(`${configPath}.transaction`, () => {
@@ -748,11 +703,18 @@ export function createTelegramConfigStore(
748
703
  typeof profile?.botToken === "string"
749
704
  ? resolveTelegramBotToken(profile.botToken, env)
750
705
  : undefined;
751
- if (!profile || !resolvedToken ||
752
- createHash("sha256").update(resolvedToken).digest("hex") !== tokenSha256 ||
753
- (profile.allowedUserId !== undefined &&
754
- (!Number.isSafeInteger(profile.allowedUserId) || profile.allowedUserId <= 0))) {
755
- throw new Error("Telegram pairing admission authority is unavailable or changed.");
706
+ if (
707
+ !profile ||
708
+ !resolvedToken ||
709
+ createHash("sha256").update(resolvedToken).digest("hex") !==
710
+ tokenSha256 ||
711
+ (profile.allowedUserId !== undefined &&
712
+ (!Number.isSafeInteger(profile.allowedUserId) ||
713
+ profile.allowedUserId <= 0))
714
+ ) {
715
+ throw new Error(
716
+ "Telegram pairing admission authority is unavailable or changed.",
717
+ );
756
718
  }
757
719
  return observe(latest, profile);
758
720
  });
@@ -788,17 +750,20 @@ export function createTelegramConfigStore(
788
750
  return true;
789
751
  },
790
752
  getActiveProfileName: () => activeProfileName,
791
- getBotToken: () => resolveTelegramBotToken(getEffectiveConfig().botToken, env),
753
+ getBotToken: () =>
754
+ resolveTelegramBotToken(getEffectiveConfig().botToken, env),
792
755
  getBotTokenDiagnostic: () =>
793
756
  getTelegramBotTokenDiagnostic(getEffectiveConfig().botToken, env),
794
- hasBotToken: () => !!resolveTelegramBotToken(getEffectiveConfig().botToken, env),
757
+ hasBotToken: () =>
758
+ !!resolveTelegramBotToken(getEffectiveConfig().botToken, env),
795
759
  getAllowedUserId: () => getEffectiveConfig().allowedUserId,
796
760
  getLegacyPollingCursor: () =>
797
761
  (getEffectiveConfig() as TelegramConfig & TelegramLegacyCursorCarrier)
798
762
  .lastUpdateId,
799
763
  removeLegacyPollingCursor: () => {
800
764
  const next = {
801
- ...(getEffectiveConfig() as TelegramConfig & TelegramLegacyCursorCarrier),
765
+ ...(getEffectiveConfig() as TelegramConfig &
766
+ TelegramLegacyCursorCarrier),
802
767
  };
803
768
  delete next.lastUpdateId;
804
769
  setEffectiveConfig(next);
@@ -817,26 +782,47 @@ export function createTelegramConfigStore(
817
782
  withSourceSerialization: (operation) =>
818
783
  withTelegramFileTransaction(`${configPath}.transaction`, operation),
819
784
  withPairingAdmission: (profileName, tokenSha256, publish) =>
820
- withPersistedPairingProfile(profileName, tokenSha256, (_latest, profile) =>
821
- publish(profile.allowedUserId === undefined)),
822
- withPairedUserAdmission: (profileName, tokenSha256, userId, publish, assertExecutionCurrent) => {
823
- if (!Number.isSafeInteger(userId) || userId <= 0) return { admitted: false };
785
+ withPersistedPairingProfile(
786
+ profileName,
787
+ tokenSha256,
788
+ (_latest, profile) => publish(profile.allowedUserId === undefined),
789
+ ),
790
+ withPairedUserAdmission: (
791
+ profileName,
792
+ tokenSha256,
793
+ userId,
794
+ publish,
795
+ assertExecutionCurrent,
796
+ ) => {
797
+ if (!Number.isSafeInteger(userId) || userId <= 0)
798
+ return { admitted: false };
824
799
  assertExecutionCurrent?.();
825
- return withPersistedPairingProfile(profileName, tokenSha256, (latest, profile) => {
826
- if (profile.allowedUserId !== userId) return { admitted: false };
827
- assertExecutionCurrent?.();
828
- const current = getEffectiveConfig();
829
- const previousOwner = persistedConfig.profiles?.[profileName]?.allowedUserId;
830
- if ((activeProfileName ?? TELEGRAM_DEFAULT_PROFILE_NAME) !== profileName ||
800
+ return withPersistedPairingProfile(
801
+ profileName,
802
+ tokenSha256,
803
+ (latest, profile) => {
804
+ if (profile.allowedUserId !== userId) return { admitted: false };
805
+ assertExecutionCurrent?.();
806
+ const current = getEffectiveConfig();
807
+ const previousOwner =
808
+ persistedConfig.profiles?.[profileName]?.allowedUserId;
809
+ if (
810
+ (activeProfileName ?? TELEGRAM_DEFAULT_PROFILE_NAME) !==
811
+ profileName ||
831
812
  current.botToken !== profile.botToken ||
832
- (current.allowedUserId !== undefined && current.allowedUserId !== userId) ||
833
- (current.allowedUserId === undefined && previousOwner !== undefined)) {
834
- throw new Error("Telegram paired admission lost local profile authority.");
835
- }
836
- // Observation is not a local edit; queued persistence still adopts its own fresh disk result.
837
- adoptPersistedConfig(latest, true);
838
- return { admitted: true, value: publish() };
839
- });
813
+ (current.allowedUserId !== undefined &&
814
+ current.allowedUserId !== userId) ||
815
+ (current.allowedUserId === undefined && previousOwner !== undefined)
816
+ ) {
817
+ throw new Error(
818
+ "Telegram paired admission lost local profile authority.",
819
+ );
820
+ }
821
+ // Observation is not a local edit; queued persistence still adopts its own fresh disk result.
822
+ adoptPersistedConfig(latest, true);
823
+ return { admitted: true, value: publish() };
824
+ },
825
+ );
840
826
  },
841
827
  persistAllowedUserId: (userId, assertExecutionCurrent, commitIfOwned) => {
842
828
  const profileName = activeProfileName;
@@ -845,40 +831,63 @@ export function createTelegramConfigStore(
845
831
  const previousOwner = getEffectiveConfig().allowedUserId;
846
832
  const assertCurrent = () => {
847
833
  assertExecutionCurrent?.();
848
- if (activeProfileName !== profileName || getEffectiveConfig().botToken !== botToken ||
849
- getEffectiveConfig().allowedUserId !== previousOwner) {
850
- throw new Error("Telegram pairing lost its originating profile authority.");
834
+ if (
835
+ activeProfileName !== profileName ||
836
+ getEffectiveConfig().botToken !== botToken ||
837
+ getEffectiveConfig().allowedUserId !== previousOwner
838
+ ) {
839
+ throw new Error(
840
+ "Telegram pairing lost its originating profile authority.",
841
+ );
851
842
  }
852
843
  };
853
844
  const pairing = persistQueue.then(() => {
854
845
  assertCurrent();
855
- if (!Number.isSafeInteger(userId) || userId <= 0) throw new Error("Invalid Telegram pairing user ID.");
846
+ if (!Number.isSafeInteger(userId) || userId <= 0)
847
+ throw new Error("Invalid Telegram pairing user ID.");
856
848
  let merged: TelegramConfig | undefined;
857
849
  const publish = () => {
858
- merged = withTelegramFileTransaction(`${configPath}.transaction`, () => {
859
- const latest = readTelegramConfigForTransaction(configPath);
860
- const profile = latest.profiles?.[profileKey];
861
- if (!botToken || profile?.botToken !== botToken) {
862
- throw new Error("Telegram pairing profile is unavailable or changed.");
863
- }
864
- assertCurrent();
865
- if (profile.allowedUserId !== undefined) return latest;
866
- const next = { ...latest, profiles: { ...latest.profiles,
867
- [profileKey]: { ...profile, allowedUserId: userId } } };
868
- writeTelegramConfigInTransaction(agentDir, configPath, next);
869
- return next;
870
- });
850
+ merged = withTelegramFileTransaction(
851
+ `${configPath}.transaction`,
852
+ () => {
853
+ const latest = readTelegramConfigForTransaction(configPath);
854
+ const profile = latest.profiles?.[profileKey];
855
+ if (!botToken || profile?.botToken !== botToken) {
856
+ throw new Error(
857
+ "Telegram pairing profile is unavailable or changed.",
858
+ );
859
+ }
860
+ assertCurrent();
861
+ if (profile.allowedUserId !== undefined) return latest;
862
+ const next = {
863
+ ...latest,
864
+ profiles: {
865
+ ...latest.profiles,
866
+ [profileKey]: { ...profile, allowedUserId: userId },
867
+ },
868
+ };
869
+ writeTelegramConfigInTransaction(agentDir, configPath, next);
870
+ return next;
871
+ },
872
+ );
871
873
  };
872
874
  if (commitIfOwned) {
873
- if (!commitIfOwned(publish)) throw new Error("Telegram pairing lost transport ownership before publication.");
875
+ if (!commitIfOwned(publish))
876
+ throw new Error(
877
+ "Telegram pairing lost transport ownership before publication.",
878
+ );
874
879
  } else {
875
880
  publish();
876
881
  }
877
- if (!merged) throw new Error("Telegram pairing publication did not execute.");
882
+ if (!merged)
883
+ throw new Error("Telegram pairing publication did not execute.");
878
884
  adoptPersistedConfig(merged, true);
879
885
  return merged.profiles?.[profileKey]?.allowedUserId === userId;
880
886
  });
881
- persistQueue = pairing.then(() => undefined, () => undefined);
887
+ persistQueue = pairing.then(
888
+ () => undefined,
889
+ () => undefined,
890
+ );
882
891
  return pairing;
883
892
  },
884
893
  load: async () => {
@@ -936,7 +945,9 @@ export function createTelegramConfigStore(
936
945
  `${configPath}.transaction`,
937
946
  () => {
938
947
  if (options?.isCurrent && !options.isCurrent()) {
939
- throw new Error("Telegram config update lost its originating authority.");
948
+ throw new Error(
949
+ "Telegram config update lost its originating authority.",
950
+ );
940
951
  }
941
952
  const latestConfig = readTelegramConfigForTransaction(configPath);
942
953
  const merged = mergeTelegramConfigDelta(
@@ -950,7 +961,10 @@ export function createTelegramConfigStore(
950
961
  return merged;
951
962
  },
952
963
  );
953
- adoptPersistedConfig(mergedConfig, mutationVersion !== capturedMutationVersion);
964
+ adoptPersistedConfig(
965
+ mergedConfig,
966
+ mutationVersion !== capturedMutationVersion,
967
+ );
954
968
  });
955
969
  persistQueue = persist.catch(() => undefined);
956
970
  return persist;
@@ -1051,10 +1065,8 @@ function createTelegramActivityVerbositySetter(
1051
1065
  return async (verbosity) => {
1052
1066
  await loadLatestTelegramConfig(configStore);
1053
1067
  const current = configStore.get();
1054
- const {
1055
- activityVerbosity: _legacyActivityVerbosity,
1056
- ...assistant
1057
- } = current.assistant ?? {};
1068
+ const { activityVerbosity: _legacyActivityVerbosity, ...assistant } =
1069
+ current.assistant ?? {};
1058
1070
  const config = {
1059
1071
  ...current,
1060
1072
  assistant: {
@@ -1087,7 +1099,9 @@ export function createTelegramVoiceReplyModeConfiguredChecker(
1087
1099
 
1088
1100
  export function createTelegramVoiceReplyModeSetter(
1089
1101
  configStore: TelegramMutableConfigStore,
1090
- ): (replyMode: "manual" | "hidden" | "mirror" | "always" | undefined) => Promise<void> {
1102
+ ): (
1103
+ replyMode: "manual" | "hidden" | "mirror" | "always" | undefined,
1104
+ ) => Promise<void> {
1091
1105
  return async (replyMode) => {
1092
1106
  await loadLatestTelegramConfig(configStore);
1093
1107
  const current = configStore.get();
@@ -1250,12 +1264,10 @@ export function createTelegramConfigControls(
1250
1264
  createTelegramAssistantRenderingModeGetter(configStore),
1251
1265
  setAssistantRenderingMode:
1252
1266
  createTelegramAssistantRenderingModeSetter(configStore),
1253
- getActivityVerbosity:
1254
- createTelegramActivityVerbosityGetter(configStore),
1267
+ getActivityVerbosity: createTelegramActivityVerbosityGetter(configStore),
1255
1268
  refreshActivityVerbosity:
1256
1269
  createTelegramActivityVerbosityRefresher(configStore),
1257
- setActivityVerbosity:
1258
- createTelegramActivityVerbositySetter(configStore),
1270
+ setActivityVerbosity: createTelegramActivityVerbositySetter(configStore),
1259
1271
  getVoiceReplyMode: createTelegramVoiceReplyModeGetter(configStore),
1260
1272
  isVoiceReplyModeConfigured:
1261
1273
  createTelegramVoiceReplyModeConfiguredChecker(configStore),
@@ -1310,14 +1322,6 @@ export function getTelegramAuthorizationState(
1310
1322
  return { kind: "deny" };
1311
1323
  }
1312
1324
 
1313
- function isTelegramStaleContextError(error: unknown): boolean {
1314
- return (
1315
- error instanceof Error &&
1316
- (error.message.includes("stale after session") ||
1317
- error.message.includes("stale ctx"))
1318
- );
1319
- }
1320
-
1321
1325
  export async function pairTelegramUserIfNeeded<TContext>(
1322
1326
  userId: number,
1323
1327
  deps: TelegramUserPairingDeps<TContext>,
@@ -1328,13 +1332,16 @@ export async function pairTelegramUserIfNeeded<TContext>(
1328
1332
  );
1329
1333
  if (authorization.kind !== "pair") return authorization.kind === "allow";
1330
1334
  deps.assertExecutionCurrent?.();
1331
- const allowed = await deps.persistAllowedUserId(authorization.userId, deps.assertExecutionCurrent);
1335
+ const allowed = await deps.persistAllowedUserId(
1336
+ authorization.userId,
1337
+ deps.assertExecutionCurrent,
1338
+ );
1332
1339
  deps.assertExecutionCurrent?.();
1333
1340
  if (!allowed) return false;
1334
1341
  try {
1335
1342
  deps.updateStatus(deps.ctx);
1336
1343
  } catch (error) {
1337
- if (!isTelegramStaleContextError(error)) throw error;
1344
+ if (!isPiStaleContextError(error)) throw error;
1338
1345
  }
1339
1346
  return true;
1340
1347
  }