@llblab/pi-kit 0.27.5 → 0.28.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 (194) hide show
  1. package/BACKLOG.md +3 -1
  2. package/CHANGELOG.md +9 -0
  3. package/README.md +4 -4
  4. package/node_modules/@llblab/pi-state-flow/AGENTS.md +1 -1
  5. package/node_modules/@llblab/pi-state-flow/BACKLOG.md +6 -2
  6. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +4 -1
  7. package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.js +4 -3
  8. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +26 -13
  9. package/node_modules/@llblab/pi-state-flow/dist/package.json +1 -1
  10. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +3 -1
  11. package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +2 -2
  12. package/node_modules/@llblab/pi-state-flow/docs/usage.md +2 -1
  13. package/node_modules/@llblab/pi-state-flow/lib/compaction.ts +4 -3
  14. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +25 -12
  15. package/node_modules/@llblab/pi-state-flow/package.json +1 -1
  16. package/node_modules/@llblab/pi-telegram/AGENTS.md +10 -8
  17. package/node_modules/@llblab/pi-telegram/BACKLOG.md +1 -9
  18. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +13 -0
  19. package/node_modules/@llblab/pi-telegram/LICENSE +21 -0
  20. package/node_modules/@llblab/pi-telegram/README.md +15 -5
  21. package/node_modules/@llblab/pi-telegram/dist/lib/activity-verbosity.d.ts +1 -5
  22. package/node_modules/@llblab/pi-telegram/dist/lib/activity-verbosity.js +5 -7
  23. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +5 -1
  24. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +6 -1
  25. package/node_modules/@llblab/pi-telegram/dist/lib/bus-api.js +4 -2
  26. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.d.ts +54 -56
  27. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +206 -139
  28. package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.d.ts +21 -2
  29. package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +181 -25
  30. package/node_modules/@llblab/pi-telegram/dist/lib/bus-transport.d.ts +4 -1
  31. package/node_modules/@llblab/pi-telegram/dist/lib/bus-transport.js +28 -4
  32. package/node_modules/@llblab/pi-telegram/dist/lib/bus.d.ts +52 -56
  33. package/node_modules/@llblab/pi-telegram/dist/lib/bus.js +78 -246
  34. package/node_modules/@llblab/pi-telegram/dist/lib/channel-posts.d.ts +1 -3
  35. package/node_modules/@llblab/pi-telegram/dist/lib/channel-posts.js +23 -26
  36. package/node_modules/@llblab/pi-telegram/dist/lib/command-templates.d.ts +0 -5
  37. package/node_modules/@llblab/pi-telegram/dist/lib/command-templates.js +5 -5
  38. package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +0 -23
  39. package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +15 -15
  40. package/node_modules/@llblab/pi-telegram/dist/lib/config.d.ts +0 -17
  41. package/node_modules/@llblab/pi-telegram/dist/lib/config.js +12 -14
  42. package/node_modules/@llblab/pi-telegram/dist/lib/delivery.d.ts +0 -4
  43. package/node_modules/@llblab/pi-telegram/dist/lib/delivery.js +2 -5
  44. package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +255 -61
  45. package/node_modules/@llblab/pi-telegram/dist/lib/generative-apps.d.ts +0 -19
  46. package/node_modules/@llblab/pi-telegram/dist/lib/inbound.d.ts +0 -2
  47. package/node_modules/@llblab/pi-telegram/dist/lib/inbound.js +2 -2
  48. package/node_modules/@llblab/pi-telegram/dist/lib/journal.d.ts +177 -16
  49. package/node_modules/@llblab/pi-telegram/dist/lib/journal.js +1115 -249
  50. package/node_modules/@llblab/pi-telegram/dist/lib/keyboard.d.ts +0 -2
  51. package/node_modules/@llblab/pi-telegram/dist/lib/keyboard.js +2 -2
  52. package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.js +1 -1
  53. package/node_modules/@llblab/pi-telegram/dist/lib/locks.d.ts +100 -12
  54. package/node_modules/@llblab/pi-telegram/dist/lib/locks.js +380 -14
  55. package/node_modules/@llblab/pi-telegram/dist/lib/logging.d.ts +30 -12
  56. package/node_modules/@llblab/pi-telegram/dist/lib/logging.js +129 -72
  57. package/node_modules/@llblab/pi-telegram/dist/lib/media.d.ts +28 -5
  58. package/node_modules/@llblab/pi-telegram/dist/lib/media.js +26 -8
  59. package/node_modules/@llblab/pi-telegram/dist/lib/menu-model.d.ts +0 -11
  60. package/node_modules/@llblab/pi-telegram/dist/lib/menu-model.js +8 -8
  61. package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.d.ts +0 -18
  62. package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.js +18 -18
  63. package/node_modules/@llblab/pi-telegram/dist/lib/menu-status.d.ts +4 -4
  64. package/node_modules/@llblab/pi-telegram/dist/lib/menu-status.js +12 -7
  65. package/node_modules/@llblab/pi-telegram/dist/lib/menu-thinking.d.ts +1 -1
  66. package/node_modules/@llblab/pi-telegram/dist/lib/menu-thinking.js +1 -1
  67. package/node_modules/@llblab/pi-telegram/dist/lib/menu.d.ts +1 -0
  68. package/node_modules/@llblab/pi-telegram/dist/lib/menu.js +4 -3
  69. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.d.ts +0 -1
  70. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.js +1 -1
  71. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-buttons.js +1 -8
  72. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.d.ts +1 -6
  73. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.js +4 -21
  74. package/node_modules/@llblab/pi-telegram/dist/lib/outbound.d.ts +0 -8
  75. package/node_modules/@llblab/pi-telegram/dist/lib/outbound.js +5 -5
  76. package/node_modules/@llblab/pi-telegram/dist/lib/paths.d.ts +50 -7
  77. package/node_modules/@llblab/pi-telegram/dist/lib/paths.js +155 -17
  78. package/node_modules/@llblab/pi-telegram/dist/lib/pi.d.ts +1 -0
  79. package/node_modules/@llblab/pi-telegram/dist/lib/pi.js +2 -1
  80. package/node_modules/@llblab/pi-telegram/dist/lib/polling.d.ts +0 -5
  81. package/node_modules/@llblab/pi-telegram/dist/lib/polling.js +5 -5
  82. package/node_modules/@llblab/pi-telegram/dist/lib/preview.d.ts +0 -3
  83. package/node_modules/@llblab/pi-telegram/dist/lib/preview.js +3 -3
  84. package/node_modules/@llblab/pi-telegram/dist/lib/prompt-templates.d.ts +0 -1
  85. package/node_modules/@llblab/pi-telegram/dist/lib/prompt-templates.js +1 -1
  86. package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +3 -15
  87. package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +69 -8
  88. package/node_modules/@llblab/pi-telegram/dist/lib/recovery.d.ts +29 -9
  89. package/node_modules/@llblab/pi-telegram/dist/lib/recovery.js +104 -33
  90. package/node_modules/@llblab/pi-telegram/dist/lib/replies.d.ts +0 -4
  91. package/node_modules/@llblab/pi-telegram/dist/lib/replies.js +1 -1
  92. package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +73 -8
  93. package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +2206 -307
  94. package/node_modules/@llblab/pi-telegram/dist/lib/sections.d.ts +0 -2
  95. package/node_modules/@llblab/pi-telegram/dist/lib/sections.js +1 -1
  96. package/node_modules/@llblab/pi-telegram/dist/lib/status.d.ts +44 -9
  97. package/node_modules/@llblab/pi-telegram/dist/lib/status.js +149 -27
  98. package/node_modules/@llblab/pi-telegram/dist/lib/sync.d.ts +0 -5
  99. package/node_modules/@llblab/pi-telegram/dist/lib/sync.js +5 -4
  100. package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +11 -3
  101. package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +34 -23
  102. package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.d.ts +1 -0
  103. package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.js +18 -21
  104. package/node_modules/@llblab/pi-telegram/dist/lib/thread-naming.d.ts +32 -5
  105. package/node_modules/@llblab/pi-telegram/dist/lib/thread-naming.js +191 -7
  106. package/node_modules/@llblab/pi-telegram/dist/lib/thread-reconciler.d.ts +2 -0
  107. package/node_modules/@llblab/pi-telegram/dist/lib/thread-reconciler.js +1 -1
  108. package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +291 -59
  109. package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +1647 -369
  110. package/node_modules/@llblab/pi-telegram/dist/lib/turns.d.ts +0 -1
  111. package/node_modules/@llblab/pi-telegram/dist/lib/turns.js +1 -1
  112. package/node_modules/@llblab/pi-telegram/dist/lib/updates.d.ts +184 -28
  113. package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +1032 -114
  114. package/node_modules/@llblab/pi-telegram/dist/lib/wire.d.ts +12 -0
  115. package/node_modules/@llblab/pi-telegram/dist/lib/wire.js +21 -0
  116. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.d.ts +25 -17
  117. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.js +95 -20
  118. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-identity.d.ts +20 -0
  119. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-identity.js +103 -0
  120. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.d.ts +43 -8
  121. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.js +148 -52
  122. package/node_modules/@llblab/pi-telegram/dist/package.json +6 -6
  123. package/node_modules/@llblab/pi-telegram/dist/skills/generated-control-surface/SKILL.md +3 -3
  124. package/node_modules/@llblab/pi-telegram/dist/skills/telegram-bridge/references/diagnosis.md +3 -3
  125. package/node_modules/@llblab/pi-telegram/docs/README.md +1 -1
  126. package/node_modules/@llblab/pi-telegram/docs/activity.md +1 -1
  127. package/node_modules/@llblab/pi-telegram/docs/architecture.md +152 -34
  128. package/node_modules/@llblab/pi-telegram/docs/callback-namespaces.md +1 -1
  129. package/node_modules/@llblab/pi-telegram/docs/delivery.md +1 -1
  130. package/node_modules/@llblab/pi-telegram/docs/generative-apps.md +2 -2
  131. package/node_modules/@llblab/pi-telegram/docs/inbound.md +1 -1
  132. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +125 -19
  133. package/node_modules/@llblab/pi-telegram/docs/public-api.md +3 -1
  134. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +22 -8
  135. package/node_modules/@llblab/pi-telegram/docs/updates.md +1 -1
  136. package/node_modules/@llblab/pi-telegram/lib/activity-verbosity.ts +5 -9
  137. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +6 -0
  138. package/node_modules/@llblab/pi-telegram/lib/bus-api.ts +5 -2
  139. package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +243 -224
  140. package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +178 -31
  141. package/node_modules/@llblab/pi-telegram/lib/bus-transport.ts +30 -8
  142. package/node_modules/@llblab/pi-telegram/lib/bus.ts +110 -334
  143. package/node_modules/@llblab/pi-telegram/lib/channel-posts.ts +29 -29
  144. package/node_modules/@llblab/pi-telegram/lib/command-templates.ts +5 -5
  145. package/node_modules/@llblab/pi-telegram/lib/commands.ts +15 -15
  146. package/node_modules/@llblab/pi-telegram/lib/config.ts +12 -17
  147. package/node_modules/@llblab/pi-telegram/lib/delivery.ts +2 -8
  148. package/node_modules/@llblab/pi-telegram/lib/extension.ts +257 -72
  149. package/node_modules/@llblab/pi-telegram/lib/generative-apps.ts +0 -22
  150. package/node_modules/@llblab/pi-telegram/lib/inbound.ts +2 -2
  151. package/node_modules/@llblab/pi-telegram/lib/journal.ts +1182 -326
  152. package/node_modules/@llblab/pi-telegram/lib/keyboard.ts +2 -2
  153. package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +1 -1
  154. package/node_modules/@llblab/pi-telegram/lib/locks.ts +402 -23
  155. package/node_modules/@llblab/pi-telegram/lib/logging.ts +151 -101
  156. package/node_modules/@llblab/pi-telegram/lib/media.ts +51 -9
  157. package/node_modules/@llblab/pi-telegram/lib/menu-model.ts +8 -8
  158. package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +18 -18
  159. package/node_modules/@llblab/pi-telegram/lib/menu-status.ts +11 -0
  160. package/node_modules/@llblab/pi-telegram/lib/menu-thinking.ts +2 -2
  161. package/node_modules/@llblab/pi-telegram/lib/menu.ts +5 -1
  162. package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +1 -1
  163. package/node_modules/@llblab/pi-telegram/lib/outbound-buttons.ts +1 -10
  164. package/node_modules/@llblab/pi-telegram/lib/outbound-markup.ts +5 -24
  165. package/node_modules/@llblab/pi-telegram/lib/outbound.ts +5 -5
  166. package/node_modules/@llblab/pi-telegram/lib/paths.ts +177 -17
  167. package/node_modules/@llblab/pi-telegram/lib/pi.ts +5 -2
  168. package/node_modules/@llblab/pi-telegram/lib/polling.ts +5 -5
  169. package/node_modules/@llblab/pi-telegram/lib/preview.ts +3 -3
  170. package/node_modules/@llblab/pi-telegram/lib/prompt-templates.ts +1 -1
  171. package/node_modules/@llblab/pi-telegram/lib/queue.ts +67 -9
  172. package/node_modules/@llblab/pi-telegram/lib/recovery.ts +104 -44
  173. package/node_modules/@llblab/pi-telegram/lib/replies.ts +1 -1
  174. package/node_modules/@llblab/pi-telegram/lib/routing.ts +1955 -375
  175. package/node_modules/@llblab/pi-telegram/lib/sections.ts +1 -1
  176. package/node_modules/@llblab/pi-telegram/lib/status.ts +156 -36
  177. package/node_modules/@llblab/pi-telegram/lib/sync.ts +4 -4
  178. package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +48 -33
  179. package/node_modules/@llblab/pi-telegram/lib/thread-cleanup-manager.ts +24 -21
  180. package/node_modules/@llblab/pi-telegram/lib/thread-naming.ts +267 -9
  181. package/node_modules/@llblab/pi-telegram/lib/thread-reconciler.ts +3 -1
  182. package/node_modules/@llblab/pi-telegram/lib/threads.ts +1697 -488
  183. package/node_modules/@llblab/pi-telegram/lib/turns.ts +1 -1
  184. package/node_modules/@llblab/pi-telegram/lib/updates.ts +1072 -151
  185. package/node_modules/@llblab/pi-telegram/lib/wire.ts +28 -0
  186. package/node_modules/@llblab/pi-telegram/lib/workspace-admission.ts +97 -47
  187. package/node_modules/@llblab/pi-telegram/lib/workspace-identity.ts +147 -0
  188. package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +188 -89
  189. package/node_modules/@llblab/pi-telegram/package.json +6 -6
  190. package/node_modules/@llblab/pi-telegram/scripts/audit-exports.mjs +100 -0
  191. package/node_modules/@llblab/pi-telegram/scripts/check-downgrade.mjs +80 -43
  192. package/node_modules/@llblab/pi-telegram/skills/generated-control-surface/SKILL.md +3 -3
  193. package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/diagnosis.md +3 -3
  194. package/package.json +3 -3
@@ -121,8 +121,6 @@ export interface TelegramSectionRuntimeDeps {
121
121
  deleteMessage: (chatId: number, messageId: number) => Promise<void>;
122
122
  }
123
123
  /** @internal */
124
- export declare function setGlobalTelegramSectionRegistry(registry: TelegramSectionRegistry): void;
125
- /** @internal */
126
124
  export declare function createAndBindTelegramSectionRegistry(): TelegramSectionRegistry;
127
125
  /**
128
126
  * Register a Telegram Extension Section from any pi extension.
@@ -51,7 +51,7 @@ function buildTelegramSectionCallbackContext(sectionId, token, chatId, messageId
51
51
  }
52
52
  // --- GlobalThis Bridge ---
53
53
  /** @internal */
54
- export function setGlobalTelegramSectionRegistry(registry) {
54
+ function setGlobalTelegramSectionRegistry(registry) {
55
55
  globalThis[SECTION_REGISTRY_KEY] = registry;
56
56
  }
57
57
  /** @internal */
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Telegram status rendering helpers
3
3
  * Zones: telegram ui, pi agent diagnostics, tui
4
- * Owns status summaries, redacted runtime diagnostics, and compact connection-failure copy
4
+ * Owns status summaries, redacted diagnostics, projection lifecycle and compact connection-failure copy
5
+ * Excludes canonical state, transport admission and filesystem policy
5
6
  */
6
7
  /** UI copy is allowlisted; raw exception text belongs only in redacted diagnostics. */
7
8
  export declare function formatTelegramConnectionFailure(error: unknown): string;
@@ -378,7 +379,6 @@ export interface TelegramStatusRuntime<TContext extends TelegramStatusRuntimeCon
378
379
  getStatusLines: (options?: TelegramBridgeStatusLineOptions) => string[];
379
380
  getStatusState: () => TelegramBridgeStatusLineState;
380
381
  }
381
- export declare function redactTelegramRuntimeMessage(message: string, botToken: string | undefined): string;
382
382
  export declare function recordStructuredTelegramRuntimeEvent(events: TelegramRuntimeEvent[], input: TelegramRuntimeEventInput, options: {
383
383
  botToken?: string;
384
384
  maxEvents: number;
@@ -394,7 +394,6 @@ export declare function recordStructuredTelegramRuntimeEvent(events: TelegramRun
394
394
  export declare function registerTelegramStatusLineProvider(provider: TelegramStatusLineProvider, options: {
395
395
  id: string;
396
396
  }): () => void;
397
- export declare function getTelegramStatusLineProviderResults(ctx: TelegramStatusLineProviderContext): TelegramStatusLineProviderResult[];
398
397
  export declare function clearTelegramStatusLineProviders(): void;
399
398
  export declare function createTelegramRuntimeEventRecorder(options: TelegramRuntimeEventRecorderOptions): TelegramRuntimeEventRecorder;
400
399
  export declare function buildTelegramRuntimeEventLines(events: TelegramRuntimeEvent[]): string[];
@@ -418,18 +417,56 @@ export declare function createTelegramRuntimeLogScope(input: {
418
417
  state: TelegramBridgeStatusLineState;
419
418
  instanceId: string;
420
419
  }): TelegramRuntimeLogScope;
421
- export declare function createTelegramStatusSnapshot(state: TelegramBridgeStatusLineState): {
420
+ export interface TelegramStatusSnapshot {
422
421
  runtime: Record<string, unknown>;
423
422
  liveRoster: Record<string, unknown>;
424
423
  diagnostics: Record<string, unknown>;
424
+ }
425
+ export interface TelegramStoredRuntimeProjection extends TelegramStatusSnapshot {
426
+ version: 1;
427
+ source: "snapshot";
428
+ writtenAtMs: number;
429
+ }
430
+ export declare function createTelegramStatusSnapshot(state: TelegramBridgeStatusLineState): TelegramStatusSnapshot;
431
+ export interface TelegramRuntimeProjectionScope {
432
+ path: string;
433
+ profile: string;
434
+ }
435
+ export interface TelegramRuntimeProjectionStorage {
436
+ read: (scope: TelegramRuntimeProjectionScope) => unknown;
437
+ /** The adapter must enforce exact owner plus the supplied physical/logical scope in one transaction. */
438
+ publish: (scope: TelegramRuntimeProjectionScope, mutate: (current: unknown) => {
439
+ value: unknown;
440
+ changed: boolean;
441
+ }, isCurrent: () => boolean) => boolean;
442
+ }
443
+ export interface TelegramRuntimeProjectionStoreOptions {
444
+ getPath: () => string;
445
+ getProfile: () => string | undefined;
446
+ /** Capture exact owner/context/session authority at submission, never renew it after the queued await. */
447
+ captureAuthority: () => (() => boolean) | undefined;
448
+ storage: TelegramRuntimeProjectionStorage;
449
+ getNowMs?: () => number;
450
+ }
451
+ /** Observational runtime storage only; Workspace, admission, transport, logs and recovery remain with their owners. */
452
+ export declare function createTelegramRuntimeProjectionStore(options: TelegramRuntimeProjectionStoreOptions): {
453
+ read: () => TelegramStoredRuntimeProjection | undefined;
454
+ persist: (snapshot: TelegramStatusSnapshot) => Promise<boolean>;
425
455
  };
426
456
  export declare function createTelegramRuntimeDiagnosticsSnapshotScheduler(deps: {
427
- persistSnapshot: () => Promise<void>;
457
+ persistSnapshot: (isCurrent: () => boolean) => Promise<void>;
428
458
  recordError: (error: unknown) => void;
459
+ captureScope?: () => (() => boolean) | undefined;
429
460
  setTimer?: (callback: () => void, ms: number) => {
430
461
  unref?: () => void;
431
- };
432
- }): () => void;
462
+ } | number;
463
+ clearTimer?: (timer: {
464
+ unref?: () => void;
465
+ } | number) => void;
466
+ }): (() => void) & {
467
+ resume(): void;
468
+ suspend(): Promise<void>;
469
+ };
433
470
  export declare function getTelegramStatusBarProcessingStatus(state: {
434
471
  hasActiveTurn: boolean;
435
472
  hasPendingDispatch: boolean;
@@ -439,6 +476,4 @@ export declare function getTelegramStatusBarProcessingStatus(state: {
439
476
  }): string | undefined;
440
477
  export declare function buildTelegramStatusBarText(theme: TelegramStatusBarTheme, state: TelegramStatusBarState): string;
441
478
  export declare function buildTelegramBridgeStatusLines(state: TelegramBridgeStatusLineState, options?: TelegramBridgeStatusLineOptions): string[];
442
- export declare function buildTelegramBridgeDiagnosticStatusLines(state: TelegramBridgeStatusLineState): string[];
443
- export declare function buildStatusHtml(ctx: TelegramStatusContext, activeModel: TelegramStatusActiveModel | undefined, bridgeStatus?: TelegramBridgeStatusLineState): string;
444
479
  export {};
@@ -1,8 +1,10 @@
1
1
  /**
2
2
  * Telegram status rendering helpers
3
3
  * Zones: telegram ui, pi agent diagnostics, tui
4
- * Owns status summaries, redacted runtime diagnostics, and compact connection-failure copy
4
+ * Owns status summaries, redacted diagnostics, projection lifecycle and compact connection-failure copy
5
+ * Excludes canonical state, transport admission and filesystem policy
5
6
  */
7
+ import { isDeepStrictEqual } from "node:util";
6
8
  /** UI copy is allowlisted; raw exception text belongs only in redacted diagnostics. */
7
9
  export function formatTelegramConnectionFailure(error) {
8
10
  const message = error instanceof Error ? error.message : typeof error === "string" ? error : "";
@@ -59,7 +61,7 @@ function truncateTelegramRuntimeEventText(text, maxLength) {
59
61
  return text;
60
62
  return `${text.slice(0, maxLength).trimEnd()}… [truncated ${text.length - maxLength} chars]`;
61
63
  }
62
- export function redactTelegramRuntimeMessage(message, botToken) {
64
+ function redactTelegramRuntimeMessage(message, botToken) {
63
65
  const redacted = botToken
64
66
  ? message.split(botToken).join("<redacted-token>")
65
67
  : message;
@@ -136,7 +138,7 @@ export function registerTelegramStatusLineProvider(provider, options) {
136
138
  registry.delete(options.id);
137
139
  };
138
140
  }
139
- export function getTelegramStatusLineProviderResults(ctx) {
141
+ function getTelegramStatusLineProviderResults(ctx) {
140
142
  const results = [];
141
143
  const registry = getOrCreateTelegramStatusLineProviderRegistry();
142
144
  for (const provider of registry.values()) {
@@ -359,7 +361,6 @@ export function createTelegramStatusSnapshot(state) {
359
361
  ...(state.localBus ? { localBus: state.localBus } : {}),
360
362
  topicTargets: state.topicTargets ?? [],
361
363
  reservations: state.threadReservations ?? [],
362
- syncObservations: state.topicSyncObservations ?? [],
363
364
  },
364
365
  diagnostics: {
365
366
  pendingDispatch: state.pendingDispatch,
@@ -372,12 +373,71 @@ export function createTelegramStatusSnapshot(state) {
372
373
  },
373
374
  };
374
375
  }
376
+ function isRuntimeProjection(value) {
377
+ if (!value || typeof value !== "object" || Array.isArray(value))
378
+ return false;
379
+ const record = value;
380
+ return record.version === 1 && record.source === "snapshot" && Number.isSafeInteger(record.writtenAtMs) && record.writtenAtMs >= 0 &&
381
+ Object.keys(record).every(key => ["version", "source", "writtenAtMs", "runtime", "liveRoster", "diagnostics"].includes(key)) &&
382
+ ["runtime", "liveRoster", "diagnostics"].every(key => record[key] !== null && typeof record[key] === "object" && !Array.isArray(record[key]));
383
+ }
384
+ /** Observational runtime storage only; Workspace, admission, transport, logs and recovery remain with their owners. */
385
+ export function createTelegramRuntimeProjectionStore(options) {
386
+ let publicationQueue = Promise.resolve();
387
+ const { getPath, getProfile: readProfile, captureAuthority } = options;
388
+ const { read, publish } = options.storage;
389
+ const getNowMs = options.getNowMs ?? Date.now;
390
+ const getProfile = () => readProfile() || TELEGRAM_STATUS_DEFAULT_PROFILE_NAME;
391
+ return {
392
+ read() {
393
+ try {
394
+ const path = getPath(), profile = getProfile(), value = read({ path, profile });
395
+ return getPath() === path && getProfile() === profile && isRuntimeProjection(value) ? value : undefined;
396
+ }
397
+ catch {
398
+ return undefined;
399
+ }
400
+ },
401
+ async persist(snapshot) {
402
+ const path = getPath(), profile = getProfile(), authority = captureAuthority();
403
+ if (!authority)
404
+ return false;
405
+ // Freeze wire content at submission, but do not duplicate the event log in persistent runtime state.
406
+ const semantic = JSON.parse(JSON.stringify({ version: 1, source: "snapshot", runtime: snapshot.runtime,
407
+ liveRoster: snapshot.liveRoster, diagnostics: snapshot.diagnostics }));
408
+ if (!isRuntimeProjection({ ...semantic, writtenAtMs: 0 }))
409
+ throw new Error("Telegram runtime projection is malformed.");
410
+ delete semantic.diagnostics.recentRuntimeEvents;
411
+ const isCurrent = () => getPath() === path && getProfile() === profile && authority() === true;
412
+ const publication = publicationQueue.then(() => {
413
+ if (!isCurrent())
414
+ return false;
415
+ return publish({ path, profile }, current => {
416
+ if (isRuntimeProjection(current)) {
417
+ const { writtenAtMs: _writtenAtMs, ...existing } = current;
418
+ if (isDeepStrictEqual(existing, semantic))
419
+ return { value: current, changed: false };
420
+ }
421
+ const writtenAtMs = getNowMs();
422
+ if (!Number.isSafeInteger(writtenAtMs) || writtenAtMs < 0)
423
+ throw new Error("Telegram runtime projection timestamp is invalid.");
424
+ return { value: { ...semantic, writtenAtMs }, changed: true };
425
+ }, isCurrent) === true;
426
+ });
427
+ publicationQueue = publication.then(() => undefined, () => undefined);
428
+ return publication;
429
+ },
430
+ };
431
+ }
375
432
  const TELEGRAM_DIAGNOSTICS_SNAPSHOT_COALESCE_MS = 100;
376
433
  export function createTelegramRuntimeDiagnosticsSnapshotScheduler(deps) {
377
434
  const setTimer = deps.setTimer ?? setTimeout;
435
+ const clearTimer = deps.clearTimer ?? ((handle) => clearTimeout(handle));
436
+ let generation = 0;
437
+ let enabled = true;
378
438
  let timer;
379
439
  let persistPromise;
380
- let pending = false;
440
+ let pending;
381
441
  const recordError = (error) => {
382
442
  try {
383
443
  deps.recordError(error);
@@ -386,32 +446,100 @@ export function createTelegramRuntimeDiagnosticsSnapshotScheduler(deps) {
386
446
  // Snapshot diagnostics cannot create an unhandled scheduler rejection.
387
447
  }
388
448
  };
389
- const request = () => {
390
- if (timer || persistPromise) {
391
- pending = true;
449
+ const cancelTimer = () => {
450
+ const previous = timer;
451
+ timer = undefined;
452
+ if (previous) {
453
+ try {
454
+ clearTimer(previous.handle);
455
+ }
456
+ catch (error) {
457
+ recordError(error);
458
+ }
459
+ }
460
+ };
461
+ const capture = () => {
462
+ if (!enabled)
463
+ return undefined;
464
+ const expectedGeneration = generation;
465
+ let scope;
466
+ try {
467
+ scope = deps.captureScope ? deps.captureScope() : () => true;
468
+ }
469
+ catch (error) {
470
+ recordError(error);
471
+ return undefined;
472
+ }
473
+ if (!scope)
474
+ return undefined;
475
+ return () => {
476
+ if (!enabled || generation !== expectedGeneration)
477
+ return false;
478
+ try {
479
+ return scope();
480
+ }
481
+ catch (error) {
482
+ recordError(error);
483
+ return false;
484
+ }
485
+ };
486
+ };
487
+ const arm = () => {
488
+ if (timer || persistPromise || !pending)
489
+ return;
490
+ const authority = pending;
491
+ if (!authority()) {
492
+ pending = undefined;
392
493
  return;
393
494
  }
394
- timer = setTimer(() => {
495
+ const token = {};
496
+ const handle = setTimer(() => {
497
+ if (timer?.token !== token)
498
+ return;
395
499
  timer = undefined;
500
+ const publicationAuthority = pending;
501
+ pending = undefined;
502
+ if (!authority() || !publicationAuthority?.())
503
+ return;
396
504
  let tracked;
397
505
  tracked = Promise.resolve()
398
- .then(deps.persistSnapshot)
506
+ .then(() => publicationAuthority() ? deps.persistSnapshot(publicationAuthority) : undefined)
399
507
  .catch(recordError)
400
508
  .finally(() => {
401
509
  if (persistPromise !== tracked)
402
510
  return;
403
511
  persistPromise = undefined;
404
- if (pending) {
405
- pending = false;
406
- request();
407
- }
512
+ // Only an explicitly captured new request can arm a successor publication.
513
+ arm();
408
514
  });
409
515
  persistPromise = tracked;
410
516
  }, TELEGRAM_DIAGNOSTICS_SNAPSHOT_COALESCE_MS);
411
- if (typeof timer !== "number")
412
- timer?.unref?.();
517
+ timer = { token, handle };
518
+ if (typeof handle !== "number")
519
+ handle.unref?.();
520
+ };
521
+ const request = () => {
522
+ const authority = capture();
523
+ if (!authority)
524
+ return;
525
+ pending = authority;
526
+ arm();
413
527
  };
414
- return request;
528
+ return Object.assign(request, {
529
+ resume() {
530
+ generation += 1;
531
+ cancelTimer();
532
+ pending = undefined;
533
+ enabled = true;
534
+ },
535
+ suspend() {
536
+ enabled = false;
537
+ generation += 1;
538
+ cancelTimer();
539
+ pending = undefined;
540
+ return persistPromise ?? Promise.resolve();
541
+ },
542
+ });
415
543
  }
416
544
  export function getTelegramStatusBarProcessingStatus(state) {
417
545
  if (state.hasPendingModelSwitch)
@@ -787,15 +915,9 @@ function buildTelegramBridgeCompactStatusLines(state) {
787
915
  : state.activeSourceMessageIds?.length
788
916
  ? "active"
789
917
  : "idle";
790
- const diagnosticsProfileName = state.activeProfileName === TELEGRAM_STATUS_DEFAULT_PROFILE_NAME
791
- ? undefined
792
- : state.activeProfileName;
793
- const profileSuffix = diagnosticsProfileName
794
- ? `.${diagnosticsProfileName.replace(/[^a-zA-Z0-9._-]+/g, "_")}`
795
- : "";
796
918
  const diagnosticsPaths = state.diagnosticPaths ?? {
797
- state: `~/.pi/agent/tmp/telegram/state${profileSuffix}.json`,
798
- logs: `~/.pi/agent/tmp/telegram/logs${profileSuffix}.jsonl`,
919
+ state: "~/.pi/agent/tmp/pi-telegram/state.json",
920
+ logs: "~/.pi/agent/tmp/pi-telegram/logs.jsonl",
799
921
  };
800
922
  return [
801
923
  "connection:",
@@ -843,7 +965,7 @@ export function buildTelegramBridgeStatusLines(state, options = {}) {
843
965
  return buildTelegramBridgeDiagnosticStatusLines(state);
844
966
  return buildTelegramBridgeCompactStatusLines(state);
845
967
  }
846
- export function buildTelegramBridgeDiagnosticStatusLines(state) {
968
+ function buildTelegramBridgeDiagnosticStatusLines(state) {
847
969
  const controlQueueCount = state.queuedItems.filter((item) => item.queueLane === "control").length;
848
970
  const priorityQueueCount = state.queuedItems.filter((item) => item.queueLane === "priority").length;
849
971
  const defaultQueueCount = state.queuedItems.filter((item) => item.queueLane === "default").length;
@@ -1002,7 +1124,7 @@ function buildTelegramStatusRoleSuffix(state) {
1002
1124
  return "";
1003
1125
  return ` @${state.busRole}`;
1004
1126
  }
1005
- export function buildStatusHtml(ctx, activeModel, bridgeStatus) {
1127
+ function buildStatusHtml(ctx, activeModel, bridgeStatus) {
1006
1128
  const stats = collectUsageStats(ctx);
1007
1129
  const usesSubscription = activeModel
1008
1130
  ? ctx.modelRegistry.isUsingOAuth(activeModel)
@@ -94,9 +94,6 @@ export interface TelegramManualThreadDisconnectDeps<TSyncState> {
94
94
  workspaceOperationKind?: string;
95
95
  getNowMs?: () => number;
96
96
  }
97
- export declare function markTelegramConfigSyncChange<TSyncState extends TelegramSyncState>(state: TSyncState, action: string, options?: {
98
- nowMs?: number;
99
- }): TSyncState;
100
97
  export declare function createTelegramPreservedLeaderQuitHandler(deps: {
101
98
  instanceId: string;
102
99
  topicTargetStore: Pick<TelegramTopicTargetStore, "load" | "list" | "listPendingCleanups" | "detachTargetOwner">;
@@ -109,7 +106,6 @@ export declare function createTelegramPreservedLeaderQuitHandler(deps: {
109
106
  export interface TelegramSessionRestartThreadCleanupDeps<TSyncState extends TelegramSyncState> extends Omit<TelegramManualThreadDisconnectDeps<TSyncState>, "stopPolling"> {
110
107
  suspendPolling: () => Promise<void>;
111
108
  }
112
- export declare function createTelegramSessionRestartThreadCleanupHandler<TSyncState extends TelegramSyncState>(deps: TelegramSessionRestartThreadCleanupDeps<TSyncState>): () => Promise<string>;
113
109
  export interface TelegramThreadDisconnectAssembly {
114
110
  disconnect: () => Promise<string>;
115
111
  cleanupForSessionRestart: () => Promise<string>;
@@ -141,7 +137,6 @@ export declare function createTelegramStaleTopicApiErrorRecoveryRuntime<TSyncSta
141
137
  export declare function settleStaleTelegramTopicExecutionFailure<TSyncState extends TelegramSyncState>(error: unknown, deps: TelegramStaleTopicApiErrorRecoveryDeps<TSyncState>): Promise<boolean>;
142
138
  export declare function recoverStaleTelegramTopicApiError<TSyncState extends TelegramSyncState>(apiBody: unknown, error: unknown, deps: TelegramStaleTopicApiErrorRecoveryDeps<TSyncState>): Promise<boolean>;
143
139
  export declare function ensureTelegramLeaderThreadBinding(deps: TelegramLeaderThreadSyncDeps): Promise<TelegramOwnTopicProvisionResult | undefined>;
144
- export declare const TELEGRAM_SYNC_SLICE_TARGET_BINDINGS = "target-bindings";
145
140
  export declare const TELEGRAM_SYNC_SLICES: readonly ["bot-identity", "bot-capabilities", "pairing", "allowed-user", "topic-capability", "topic-state", "target-bindings", "reservations", "transport-health"];
146
141
  export type TelegramSyncSlice = (typeof TELEGRAM_SYNC_SLICES)[number];
147
142
  export type TelegramSyncTrigger = "startup" | "reload" | "topic-lifecycle" | "stale-api-error" | "setup-change" | "pairing-change" | "follower-register" | "follower-prune" | "status-request" | "leader-health-tick" | "ordinary-message" | "ordinary-send";
@@ -7,9 +7,10 @@ import { getTelegramApiErrorRequestTarget, isTelegramStaleTargetHttpError } from
7
7
  import { getTelegramTargetKey } from "./target.js";
8
8
  import * as ThreadReconciler from "./thread-reconciler.js";
9
9
  import { TelegramWorkspaceSlotUnavailableError } from "./workspace-slots.js";
10
+ import { normalizeTelegramWorkspacePath } from "./workspace-identity.js";
10
11
  import { createTelegramWorkspaceAdmissionOperationId, runWithTelegramWorkspaceAdmissionsAsync, } from "./workspace-admission.js";
11
- import { createTelegramCleanupTargetProtection, commitTelegramWorkspaceProvisionBinding, getTelegramTargetFromApiBody, getTelegramThreadOwnerKey, isSameTelegramProcessInstance, isTelegramTopicTargetStaleError, normalizeTelegramWorkspacePath, provisionOwnBusTopic, } from "./threads.js";
12
- export function markTelegramConfigSyncChange(state, action, options) {
12
+ import { createTelegramCleanupTargetProtection, commitTelegramWorkspaceProvisionBinding, getTelegramTargetFromApiBody, getTelegramThreadOwnerKey, isSameTelegramProcessInstance, isTelegramTopicTargetStaleError, provisionOwnBusTopic, } from "./threads.js";
13
+ function markTelegramConfigSyncChange(state, action, options) {
13
14
  const nowMs = options?.nowMs ?? Date.now();
14
15
  let nextState = markTelegramSyncSliceFresh(state, "pairing", {
15
16
  nowMs,
@@ -61,7 +62,7 @@ export function createTelegramPreservedLeaderQuitHandler(deps) {
61
62
  };
62
63
  };
63
64
  }
64
- export function createTelegramSessionRestartThreadCleanupHandler(deps) {
65
+ function createTelegramSessionRestartThreadCleanupHandler(deps) {
65
66
  return createTelegramManualThreadDisconnectHandler({
66
67
  ...deps,
67
68
  workspaceOperationKind: "workspace.cleanup-session-restart",
@@ -655,7 +656,7 @@ export async function ensureTelegramLeaderThreadBinding(deps) {
655
656
  deps.topicTargetStore.releaseWorkspaceClaim(deps.instanceId);
656
657
  }
657
658
  }
658
- export const TELEGRAM_SYNC_SLICE_TARGET_BINDINGS = "target-bindings";
659
+ const TELEGRAM_SYNC_SLICE_TARGET_BINDINGS = "target-bindings";
659
660
  export const TELEGRAM_SYNC_SLICES = [
660
661
  "bot-identity",
661
662
  "bot-capabilities",
@@ -5,7 +5,7 @@
5
5
  * Wraps bot API calls, file uploads/downloads (including voice messages),
6
6
  * multipart sending, runtime transport binding, and Telegram temp-file lifecycle.
7
7
  */
8
- export declare const TELEGRAM_API_BASE = "https://api.telegram.org";
8
+ import type { TelegramAttachmentSource } from "./media.ts";
9
9
  export declare const TELEGRAM_FILE_MAX_BYTES: number;
10
10
  export declare function getTelegramInboundFileByteLimitFromEnv(env: NodeJS.ProcessEnv, names: string[], defaultValue?: number): number;
11
11
  export type TelegramNetworkFamilyPolicy = "auto" | "ipv4" | "ipv6" | "ipv4-fallback";
@@ -415,6 +415,8 @@ export interface TelegramBridgeApiRuntimeDeps {
415
415
  tempDir: string;
416
416
  maxFileSizeBytes: number;
417
417
  tempFileMaxAgeMs: number;
418
+ /** Bot `@username` (or numeric id) naming private-chat attachments. */
419
+ getBotScope?: () => string | undefined;
418
420
  recordRuntimeEvent: (kind: "api" | "multipart" | "download", error: unknown, details?: Record<string, unknown>) => void;
419
421
  now?: () => number;
420
422
  chatActionMinIntervalMs?: number;
@@ -423,7 +425,7 @@ export interface TelegramBridgeApiRuntimeDeps {
423
425
  export interface TelegramBridgeApiRuntime {
424
426
  call: <TResponse>(method: string, body: Record<string, unknown>, options?: TelegramApiCallOptions) => Promise<TResponse>;
425
427
  callMultipart: <TResponse>(method: string, fields: Record<string, string>, fileField: string, filePath: string, fileName: string, options?: TelegramApiCallOptions) => Promise<TResponse>;
426
- downloadFile: (fileId: string, suggestedName: string) => Promise<string>;
428
+ downloadFile: (fileId: string, suggestedName: string, source?: TelegramAttachmentSource) => Promise<string>;
427
429
  deleteWebhook: (signal?: AbortSignal) => Promise<boolean>;
428
430
  getUpdates: (body: Record<string, unknown>, signal?: AbortSignal) => Promise<TelegramUpdate[]>;
429
431
  setMyCommands: (commands: readonly {
@@ -462,6 +464,12 @@ export interface TelegramBridgeApiRuntime {
462
464
  deleteMessage: (chatId: number, messageId: number) => Promise<void>;
463
465
  prepareTempDir: () => Promise<number>;
464
466
  }
467
+ /** Names follow `<kind>-<scope>-<messageId>[-<index>][-<name>|.<ext>]`. */
468
+ /**
469
+ * `scope` is the bot `@username` (id fallback) in a private chat and the public chat `@username` (id fallback) elsewhere.
470
+ * A negative chat id loses its sign, so the name never contains two adjacent dashes.
471
+ */
472
+ export declare function createTelegramAttachmentFileName(source: TelegramAttachmentSource, generatedName: string, botScope?: string): string;
465
473
  export declare class TelegramApiCommitUnknownError extends Error {
466
474
  readonly kind: "commit-unknown";
467
475
  readonly method: string;
@@ -507,7 +515,6 @@ export declare function fetchTelegramBotIdentity(botToken: string, fetchImpl?: t
507
515
  export declare function callTelegramMultipart<TResponse>(botToken: string | undefined, method: string, fields: Record<string, string>, fileField: string, filePath: string, fileName: string, options?: TelegramApiCallOptions): Promise<TResponse>;
508
516
  export declare function downloadTelegramFile(botToken: string | undefined, fileId: string, suggestedName: string, tempDir: string, options?: TelegramFileDownloadOptions): Promise<string>;
509
517
  export declare function answerTelegramCallbackQuery(botToken: string | undefined, callbackQueryId: string, text?: string, options?: TelegramAnswerCallbackQueryOptions): Promise<void>;
510
- export declare function deleteTelegramMessage(botToken: string | undefined, chatId: number, messageId: number): Promise<void>;
511
518
  export declare function createTelegramChatActionSender<TAction extends string>(sendChatAction: (chatId: number, action: TAction, options?: {
512
519
  message_thread_id?: number;
513
520
  }) => Promise<unknown>, action: TAction): (chatId: number, options?: {
@@ -530,6 +537,7 @@ export type TelegramWorkspaceThreadDeletionTransport = (authorize: () => {
530
537
  }) => Promise<void>;
531
538
  export declare function createDefaultTelegramBridgeApiRuntime(deps: {
532
539
  getBotToken: () => string | undefined;
540
+ getBotScope?: () => string | undefined;
533
541
  recordRuntimeEvent: TelegramBridgeApiRuntimeDeps["recordRuntimeEvent"];
534
542
  captureRequestErrorHandler?: TelegramBridgeApiRuntimeDeps["captureRequestErrorHandler"];
535
543
  targetActivity?: TelegramApiTargetActivityRuntime;
@@ -7,13 +7,13 @@
7
7
  */
8
8
  import { randomUUID } from "node:crypto";
9
9
  import { createWriteStream, openAsBlob } from "node:fs";
10
- import { mkdir, readdir, stat, unlink, writeFile } from "node:fs/promises";
10
+ import { mkdir, readdir, rename, stat, unlink, writeFile } from "node:fs/promises";
11
11
  import { request as requestHttps } from "node:https";
12
12
  import { join } from "node:path";
13
- import { resolveTelegramTempDir } from "./paths.js";
13
+ import { resolveTelegramAttachmentsDir } from "./paths.js";
14
14
  import { Readable, Transform } from "node:stream";
15
15
  import { pipeline } from "node:stream/promises";
16
- export const TELEGRAM_API_BASE = "https://api.telegram.org";
16
+ const TELEGRAM_API_BASE = "https://api.telegram.org";
17
17
  export const TELEGRAM_FILE_MAX_BYTES = 50 * 1024 * 1024;
18
18
  export function getTelegramInboundFileByteLimitFromEnv(env, names, defaultValue = TELEGRAM_FILE_MAX_BYTES) {
19
19
  for (const name of names) {
@@ -27,11 +27,10 @@ export function getTelegramInboundFileByteLimitFromEnv(env, names, defaultValue
27
27
  return defaultValue;
28
28
  }
29
29
  function getTelegramApiTempDir() {
30
- return resolveTelegramTempDir();
30
+ return resolveTelegramAttachmentsDir();
31
31
  }
32
32
  const TELEGRAM_TEMP_FILE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
33
33
  const activeTelegramApiWorkspaceAdmissionOperationIds = new Set();
34
- const TELEGRAM_TEMP_SCRATCH_FILE_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}-/u;
35
34
  const TELEGRAM_INBOUND_FILE_MAX_BYTES = getTelegramInboundFileByteLimitFromEnv(process.env, ["PI_TELEGRAM_INBOUND_FILE_MAX_BYTES", "TELEGRAM_MAX_FILE_SIZE_BYTES"], TELEGRAM_FILE_MAX_BYTES);
36
35
  const TELEGRAM_NETWORK_FAMILY_ENV = "PI_TELEGRAM_NETWORK_FAMILY";
37
36
  const TELEGRAM_NETWORK_FAMILY_VALUES = new Set([
@@ -208,6 +207,23 @@ export function createTelegramApiWorkspaceAdmissionClient(client, admission, opt
208
207
  function sanitizeFileName(name) {
209
208
  return name.replace(/[^a-zA-Z0-9._-]+/g, "_");
210
209
  }
210
+ /** Names follow `<kind>-<scope>-<messageId>[-<index>][-<name>|.<ext>]`. */
211
+ /**
212
+ * `scope` is the bot `@username` (id fallback) in a private chat and the public chat `@username` (id fallback) elsewhere.
213
+ * A negative chat id loses its sign, so the name never contains two adjacent dashes.
214
+ */
215
+ export function createTelegramAttachmentFileName(source, generatedName, botScope) {
216
+ const chat = source.chat;
217
+ const clean = (value) => sanitizeFileName(value.replace(/^@/u, "")).replace(/^[-.]+/u, "");
218
+ const scope = source.scope ? clean(source.scope) : chat?.type === "private" || !chat
219
+ ? botScope ? clean(botScope) : chat ? String(Math.abs(chat.id)) : ""
220
+ : chat.username ? clean(chat.username) : String(Math.abs(chat.id));
221
+ const dot = generatedName.lastIndexOf(".");
222
+ const extension = dot > 0 ? sanitizeFileName(generatedName.slice(dot)) : "";
223
+ const tail = source.userFileName ? `-${clean(source.userFileName)}` : extension;
224
+ return [sanitizeFileName(source.kind), scope, String(source.messageId)].filter(Boolean).join("-") +
225
+ (source.index !== undefined ? `-${source.index}` : "") + tail;
226
+ }
211
227
  class TelegramApiMalformedSuccessError extends Error {
212
228
  constructor(method, detail) {
213
229
  super(`Telegram API ${method} ${detail}`);
@@ -672,9 +688,9 @@ export async function cleanupTelegramTempFiles(tempDir, maxAgeMs, now = Date.now
672
688
  return 0;
673
689
  }
674
690
  for (const entry of entries) {
675
- if (!entry.isFile() || !TELEGRAM_TEMP_SCRATCH_FILE_PATTERN.test(entry.name)) {
691
+ // The directory holds only attachment scratch files, so age alone decides.
692
+ if (!entry.isFile())
676
693
  continue;
677
- }
678
694
  const path = join(tempDir, entry.name);
679
695
  try {
680
696
  const stats = await stat(path);
@@ -760,7 +776,9 @@ export async function downloadTelegramFile(botToken, fileId, suggestedName, temp
760
776
  const file = await callTelegram(configuredBotToken, "getFile", { file_id: fileId }, { signal: options?.signal });
761
777
  assertTelegramFileSizeWithinLimit(file.file_size, options?.maxFileSizeBytes);
762
778
  await mkdir(tempDir, { recursive: true, mode: 0o700 });
763
- const targetPath = join(tempDir, `${randomUUID()}-${sanitizeFileName(suggestedName)}`);
779
+ // Names carry scope and message id, so the same message maps to the same path; publish by rename.
780
+ const targetPath = join(tempDir, sanitizeFileName(suggestedName));
781
+ const partPath = `${targetPath}.${randomUUID()}.part`;
764
782
  const response = await callTelegramTransportRequest((family) => telegramFetch(`${TELEGRAM_API_BASE}/file/bot${configuredBotToken}/${file.file_path}`, { signal: options?.signal }, family));
765
783
  if (!response.ok) {
766
784
  throw new Error(`Failed to download Telegram file: ${response.status}`);
@@ -768,10 +786,11 @@ export async function downloadTelegramFile(botToken, fileId, suggestedName, temp
768
786
  const contentLength = response.headers?.get("content-length");
769
787
  assertTelegramFileSizeWithinLimit(contentLength ? Number.parseInt(contentLength, 10) : undefined, options?.maxFileSizeBytes);
770
788
  try {
771
- await writeTelegramDownloadResponse(response, targetPath, options?.maxFileSizeBytes);
789
+ await writeTelegramDownloadResponse(response, partPath, options?.maxFileSizeBytes);
790
+ await rename(partPath, targetPath);
772
791
  }
773
792
  catch (error) {
774
- await removeTelegramPartialDownload(targetPath);
793
+ await removeTelegramPartialDownload(partPath);
775
794
  throw error;
776
795
  }
777
796
  return targetPath;
@@ -788,17 +807,6 @@ export async function answerTelegramCallbackQuery(botToken, callbackQueryId, tex
788
807
  }));
789
808
  }
790
809
  }
791
- export async function deleteTelegramMessage(botToken, chatId, messageId) {
792
- try {
793
- await callTelegram(botToken, "deleteMessage", {
794
- chat_id: chatId,
795
- message_id: messageId,
796
- });
797
- }
798
- catch {
799
- // ignore
800
- }
801
- }
802
810
  export function createTelegramChatActionSender(sendChatAction, action) {
803
811
  return (chatId, options) => sendChatAction(chatId, action, options);
804
812
  }
@@ -871,6 +879,7 @@ export function createDefaultTelegramBridgeApiRuntime(deps) {
871
879
  tempDir: getTelegramApiTempDir(),
872
880
  maxFileSizeBytes: TELEGRAM_INBOUND_FILE_MAX_BYTES,
873
881
  tempFileMaxAgeMs: TELEGRAM_TEMP_FILE_MAX_AGE_MS,
882
+ ...(deps.getBotScope ? { getBotScope: deps.getBotScope } : {}),
874
883
  recordRuntimeEvent: deps.recordRuntimeEvent,
875
884
  captureRequestErrorHandler: deps.captureRequestErrorHandler,
876
885
  });
@@ -1029,9 +1038,11 @@ export function createTelegramBridgeApiRuntime(deps) {
1029
1038
  * Downloads a file from the Telegram servers into the local temp directory.
1030
1039
  * Used for inbound voice messages, photos, documents, etc.
1031
1040
  */
1032
- downloadFile: async (fileId, suggestedName) => {
1041
+ downloadFile: async (fileId, suggestedName, source) => {
1033
1042
  try {
1034
- return await deps.client.downloadFile(fileId, suggestedName, deps.tempDir, {
1043
+ return await deps.client.downloadFile(fileId, source
1044
+ ? createTelegramAttachmentFileName(source, suggestedName, deps.getBotScope?.())
1045
+ : suggestedName, deps.tempDir, {
1035
1046
  maxFileSizeBytes: deps.maxFileSizeBytes,
1036
1047
  });
1037
1048
  }
@@ -173,6 +173,7 @@ export interface TelegramThreadCleanupWorkStore {
173
173
  }
174
174
  export declare function createTelegramThreadCleanupWorkStore(options: {
175
175
  path: string;
176
+ runtimeDir?: string;
176
177
  profileName: string;
177
178
  tokenSha256: string;
178
179
  maxWorkSets?: number;