@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
@@ -284,7 +284,7 @@ function buildTelegramSectionCallbackContext(
284
284
  // --- GlobalThis Bridge ---
285
285
 
286
286
  /** @internal */
287
- export function setGlobalTelegramSectionRegistry(
287
+ function setGlobalTelegramSectionRegistry(
288
288
  registry: TelegramSectionRegistry,
289
289
  ): void {
290
290
  (globalThis as Record<string, unknown>)[SECTION_REGISTRY_KEY] = registry;
@@ -1,9 +1,12 @@
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
 
8
+ import { isDeepStrictEqual } from "node:util";
9
+
7
10
  /** UI copy is allowlisted; raw exception text belongs only in redacted diagnostics. */
8
11
  export function formatTelegramConnectionFailure(error: unknown): string {
9
12
  const message = error instanceof Error ? error.message : typeof error === "string" ? error : "";
@@ -463,7 +466,7 @@ function truncateTelegramRuntimeEventText(
463
466
  return `${text.slice(0, maxLength).trimEnd()}… [truncated ${text.length - maxLength} chars]`;
464
467
  }
465
468
 
466
- export function redactTelegramRuntimeMessage(
469
+ function redactTelegramRuntimeMessage(
467
470
  message: string,
468
471
  botToken: string | undefined,
469
472
  ): string {
@@ -579,7 +582,7 @@ export function registerTelegramStatusLineProvider(
579
582
  };
580
583
  }
581
584
 
582
- export function getTelegramStatusLineProviderResults(
585
+ function getTelegramStatusLineProviderResults(
583
586
  ctx: TelegramStatusLineProviderContext,
584
587
  ): TelegramStatusLineProviderResult[] {
585
588
  const results: TelegramStatusLineProviderResult[] = [];
@@ -842,13 +845,19 @@ export function createTelegramRuntimeLogScope(input: {
842
845
  };
843
846
  }
844
847
 
845
- export function createTelegramStatusSnapshot(
846
- state: TelegramBridgeStatusLineState,
847
- ): {
848
+ export interface TelegramStatusSnapshot {
848
849
  runtime: Record<string, unknown>;
849
850
  liveRoster: Record<string, unknown>;
850
851
  diagnostics: Record<string, unknown>;
851
- } {
852
+ }
853
+ export interface TelegramStoredRuntimeProjection extends TelegramStatusSnapshot {
854
+ version: 1;
855
+ source: "snapshot";
856
+ writtenAtMs: number;
857
+ }
858
+ export function createTelegramStatusSnapshot(
859
+ state: TelegramBridgeStatusLineState,
860
+ ): TelegramStatusSnapshot {
852
861
  return {
853
862
  runtime: {
854
863
  busRole: state.busRole,
@@ -869,7 +878,6 @@ export function createTelegramStatusSnapshot(
869
878
  ...(state.localBus ? { localBus: state.localBus } : {}),
870
879
  topicTargets: state.topicTargets ?? [],
871
880
  reservations: state.threadReservations ?? [],
872
- syncObservations: state.topicSyncObservations ?? [],
873
881
  },
874
882
  diagnostics: {
875
883
  pendingDispatch: state.pendingDispatch,
@@ -883,17 +891,94 @@ export function createTelegramStatusSnapshot(
883
891
  };
884
892
  }
885
893
 
894
+ function isRuntimeProjection(value: unknown): value is TelegramStoredRuntimeProjection {
895
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
896
+ const record = value as Record<string, unknown>;
897
+ return record.version === 1 && record.source === "snapshot" && Number.isSafeInteger(record.writtenAtMs) && (record.writtenAtMs as number) >= 0 &&
898
+ Object.keys(record).every(key => ["version", "source", "writtenAtMs", "runtime", "liveRoster", "diagnostics"].includes(key)) &&
899
+ ["runtime", "liveRoster", "diagnostics"].every(key => record[key] !== null && typeof record[key] === "object" && !Array.isArray(record[key]));
900
+ }
901
+ export interface TelegramRuntimeProjectionScope {
902
+ path: string;
903
+ profile: string;
904
+ }
905
+ export interface TelegramRuntimeProjectionStorage {
906
+ read: (scope: TelegramRuntimeProjectionScope) => unknown;
907
+ /** The adapter must enforce exact owner plus the supplied physical/logical scope in one transaction. */
908
+ publish: (
909
+ scope: TelegramRuntimeProjectionScope,
910
+ mutate: (current: unknown) => { value: unknown; changed: boolean },
911
+ isCurrent: () => boolean,
912
+ ) => boolean;
913
+ }
914
+ export interface TelegramRuntimeProjectionStoreOptions {
915
+ getPath: () => string;
916
+ getProfile: () => string | undefined;
917
+ /** Capture exact owner/context/session authority at submission, never renew it after the queued await. */
918
+ captureAuthority: () => (() => boolean) | undefined;
919
+ storage: TelegramRuntimeProjectionStorage;
920
+ getNowMs?: () => number;
921
+ }
922
+ /** Observational runtime storage only; Workspace, admission, transport, logs and recovery remain with their owners. */
923
+ export function createTelegramRuntimeProjectionStore(options: TelegramRuntimeProjectionStoreOptions): {
924
+ read: () => TelegramStoredRuntimeProjection | undefined;
925
+ persist: (snapshot: TelegramStatusSnapshot) => Promise<boolean>;
926
+ } {
927
+ let publicationQueue = Promise.resolve();
928
+ const { getPath, getProfile: readProfile, captureAuthority } = options;
929
+ const { read, publish } = options.storage;
930
+ const getNowMs = options.getNowMs ?? Date.now;
931
+ const getProfile = (): string => readProfile() || TELEGRAM_STATUS_DEFAULT_PROFILE_NAME;
932
+ return {
933
+ read() {
934
+ try {
935
+ const path = getPath(), profile = getProfile(), value = read({ path, profile });
936
+ return getPath() === path && getProfile() === profile && isRuntimeProjection(value) ? value : undefined;
937
+ } catch { return undefined; }
938
+ },
939
+ async persist(snapshot) {
940
+ const path = getPath(), profile = getProfile(), authority = captureAuthority();
941
+ if (!authority) return false;
942
+ // Freeze wire content at submission, but do not duplicate the event log in persistent runtime state.
943
+ const semantic = JSON.parse(JSON.stringify({ version: 1, source: "snapshot", runtime: snapshot.runtime,
944
+ liveRoster: snapshot.liveRoster, diagnostics: snapshot.diagnostics })) as Omit<TelegramStoredRuntimeProjection, "writtenAtMs">;
945
+ if (!isRuntimeProjection({ ...semantic, writtenAtMs: 0 })) throw new Error("Telegram runtime projection is malformed.");
946
+ delete semantic.diagnostics.recentRuntimeEvents;
947
+ const isCurrent = (): boolean => getPath() === path && getProfile() === profile && authority() === true;
948
+ const publication = publicationQueue.then(() => {
949
+ if (!isCurrent()) return false;
950
+ return publish({ path, profile }, current => {
951
+ if (isRuntimeProjection(current)) {
952
+ const { writtenAtMs: _writtenAtMs, ...existing } = current;
953
+ if (isDeepStrictEqual(existing, semantic)) return { value: current, changed: false };
954
+ }
955
+ const writtenAtMs = getNowMs();
956
+ if (!Number.isSafeInteger(writtenAtMs) || writtenAtMs < 0) throw new Error("Telegram runtime projection timestamp is invalid.");
957
+ return { value: { ...semantic, writtenAtMs }, changed: true };
958
+ }, isCurrent) === true;
959
+ });
960
+ publicationQueue = publication.then(() => undefined, () => undefined);
961
+ return publication;
962
+ },
963
+ };
964
+ }
965
+
886
966
  const TELEGRAM_DIAGNOSTICS_SNAPSHOT_COALESCE_MS = 100;
887
967
 
888
968
  export function createTelegramRuntimeDiagnosticsSnapshotScheduler(deps: {
889
- persistSnapshot: () => Promise<void>;
969
+ persistSnapshot: (isCurrent: () => boolean) => Promise<void>;
890
970
  recordError: (error: unknown) => void;
891
- setTimer?: (callback: () => void, ms: number) => { unref?: () => void };
892
- }): () => void {
971
+ captureScope?: () => (() => boolean) | undefined;
972
+ setTimer?: (callback: () => void, ms: number) => { unref?: () => void } | number;
973
+ clearTimer?: (timer: { unref?: () => void } | number) => void;
974
+ }): (() => void) & { resume(): void; suspend(): Promise<void> } {
893
975
  const setTimer = deps.setTimer ?? setTimeout;
894
- let timer: { unref?: () => void } | number | undefined;
976
+ const clearTimer = deps.clearTimer ?? ((handle) => clearTimeout(handle as ReturnType<typeof setTimeout>));
977
+ let generation = 0;
978
+ let enabled = true;
979
+ let timer: { token: object; handle: { unref?: () => void } | number } | undefined;
895
980
  let persistPromise: Promise<void> | undefined;
896
- let pending = false;
981
+ let pending: (() => boolean) | undefined;
897
982
  const recordError = (error: unknown): void => {
898
983
  try {
899
984
  deps.recordError(error);
@@ -901,30 +986,72 @@ export function createTelegramRuntimeDiagnosticsSnapshotScheduler(deps: {
901
986
  // Snapshot diagnostics cannot create an unhandled scheduler rejection.
902
987
  }
903
988
  };
904
- const request = (): void => {
905
- if (timer || persistPromise) {
906
- pending = true;
907
- return;
989
+ const cancelTimer = (): void => {
990
+ const previous = timer;
991
+ timer = undefined;
992
+ if (previous) {
993
+ try { clearTimer(previous.handle); } catch (error) { recordError(error); }
908
994
  }
909
- timer = setTimer(() => {
995
+ };
996
+ const capture = (): (() => boolean) | undefined => {
997
+ if (!enabled) return undefined;
998
+ const expectedGeneration = generation;
999
+ let scope: (() => boolean) | undefined;
1000
+ try { scope = deps.captureScope ? deps.captureScope() : () => true; }
1001
+ catch (error) { recordError(error); return undefined; }
1002
+ if (!scope) return undefined;
1003
+ return () => {
1004
+ if (!enabled || generation !== expectedGeneration) return false;
1005
+ try { return scope!(); } catch (error) { recordError(error); return false; }
1006
+ };
1007
+ };
1008
+ const arm = (): void => {
1009
+ if (timer || persistPromise || !pending) return;
1010
+ const authority = pending;
1011
+ if (!authority()) { pending = undefined; return; }
1012
+ const token = {};
1013
+ const handle = setTimer(() => {
1014
+ if (timer?.token !== token) return;
910
1015
  timer = undefined;
1016
+ const publicationAuthority = pending;
1017
+ pending = undefined;
1018
+ if (!authority() || !publicationAuthority?.()) return;
911
1019
  let tracked: Promise<void>;
912
1020
  tracked = Promise.resolve()
913
- .then(deps.persistSnapshot)
1021
+ .then(() => publicationAuthority() ? deps.persistSnapshot(publicationAuthority) : undefined)
914
1022
  .catch(recordError)
915
1023
  .finally(() => {
916
1024
  if (persistPromise !== tracked) return;
917
1025
  persistPromise = undefined;
918
- if (pending) {
919
- pending = false;
920
- request();
921
- }
1026
+ // Only an explicitly captured new request can arm a successor publication.
1027
+ arm();
922
1028
  });
923
1029
  persistPromise = tracked;
924
1030
  }, TELEGRAM_DIAGNOSTICS_SNAPSHOT_COALESCE_MS);
925
- if (typeof timer !== "number") timer?.unref?.();
1031
+ timer = { token, handle };
1032
+ if (typeof handle !== "number") handle.unref?.();
1033
+ };
1034
+ const request = (): void => {
1035
+ const authority = capture();
1036
+ if (!authority) return;
1037
+ pending = authority;
1038
+ arm();
926
1039
  };
927
- return request;
1040
+ return Object.assign(request, {
1041
+ resume() {
1042
+ generation += 1;
1043
+ cancelTimer();
1044
+ pending = undefined;
1045
+ enabled = true;
1046
+ },
1047
+ suspend() {
1048
+ enabled = false;
1049
+ generation += 1;
1050
+ cancelTimer();
1051
+ pending = undefined;
1052
+ return persistPromise ?? Promise.resolve();
1053
+ },
1054
+ });
928
1055
  }
929
1056
 
930
1057
  export function getTelegramStatusBarProcessingStatus(state: {
@@ -1382,16 +1509,9 @@ function buildTelegramBridgeCompactStatusLines(
1382
1509
  : state.activeSourceMessageIds?.length
1383
1510
  ? "active"
1384
1511
  : "idle";
1385
- const diagnosticsProfileName =
1386
- state.activeProfileName === TELEGRAM_STATUS_DEFAULT_PROFILE_NAME
1387
- ? undefined
1388
- : state.activeProfileName;
1389
- const profileSuffix = diagnosticsProfileName
1390
- ? `.${diagnosticsProfileName.replace(/[^a-zA-Z0-9._-]+/g, "_")}`
1391
- : "";
1392
1512
  const diagnosticsPaths = state.diagnosticPaths ?? {
1393
- state: `~/.pi/agent/tmp/telegram/state${profileSuffix}.json`,
1394
- logs: `~/.pi/agent/tmp/telegram/logs${profileSuffix}.jsonl`,
1513
+ state: "~/.pi/agent/tmp/pi-telegram/state.json",
1514
+ logs: "~/.pi/agent/tmp/pi-telegram/logs.jsonl",
1395
1515
  };
1396
1516
  return [
1397
1517
  "connection:",
@@ -1443,7 +1563,7 @@ export function buildTelegramBridgeStatusLines(
1443
1563
  return buildTelegramBridgeCompactStatusLines(state);
1444
1564
  }
1445
1565
 
1446
- export function buildTelegramBridgeDiagnosticStatusLines(
1566
+ function buildTelegramBridgeDiagnosticStatusLines(
1447
1567
  state: TelegramBridgeStatusLineState,
1448
1568
  ): string[] {
1449
1569
  const controlQueueCount = state.queuedItems.filter(
@@ -1620,7 +1740,7 @@ function buildTelegramStatusRoleSuffix(
1620
1740
  return ` @${state.busRole}`;
1621
1741
  }
1622
1742
 
1623
- export function buildStatusHtml(
1743
+ function buildStatusHtml(
1624
1744
  ctx: TelegramStatusContext,
1625
1745
  activeModel: TelegramStatusActiveModel | undefined,
1626
1746
  bridgeStatus?: TelegramBridgeStatusLineState,
@@ -8,6 +8,7 @@ import { getTelegramApiErrorRequestTarget, isTelegramStaleTargetHttpError } from
8
8
  import { getTelegramTargetKey, type TelegramTarget } from "./target.ts";
9
9
  import * as ThreadReconciler from "./thread-reconciler.ts";
10
10
  import { TelegramWorkspaceSlotUnavailableError } from "./workspace-slots.ts";
11
+ import { normalizeTelegramWorkspacePath } from "./workspace-identity.ts";
11
12
  import {
12
13
  createTelegramWorkspaceAdmissionOperationId,
13
14
  runWithTelegramWorkspaceAdmissionsAsync,
@@ -20,7 +21,6 @@ import {
20
21
  getTelegramThreadOwnerKey,
21
22
  isSameTelegramProcessInstance,
22
23
  isTelegramTopicTargetStaleError,
23
- normalizeTelegramWorkspacePath,
24
24
  provisionOwnBusTopic,
25
25
  type TelegramOwnTopicProvisionResult,
26
26
  type TelegramTopicTargetStore,
@@ -179,7 +179,7 @@ export interface TelegramManualThreadDisconnectDeps<TSyncState> {
179
179
  getNowMs?: () => number;
180
180
  }
181
181
 
182
- export function markTelegramConfigSyncChange<
182
+ function markTelegramConfigSyncChange<
183
183
  TSyncState extends TelegramSyncState,
184
184
  >(state: TSyncState, action: string, options?: { nowMs?: number }): TSyncState {
185
185
  const nowMs = options?.nowMs ?? Date.now();
@@ -245,7 +245,7 @@ export interface TelegramSessionRestartThreadCleanupDeps<
245
245
  suspendPolling: () => Promise<void>;
246
246
  }
247
247
 
248
- export function createTelegramSessionRestartThreadCleanupHandler<
248
+ function createTelegramSessionRestartThreadCleanupHandler<
249
249
  TSyncState extends TelegramSyncState,
250
250
  >(
251
251
  deps: TelegramSessionRestartThreadCleanupDeps<TSyncState>,
@@ -997,7 +997,7 @@ export async function ensureTelegramLeaderThreadBinding(
997
997
  }
998
998
  }
999
999
 
1000
- export const TELEGRAM_SYNC_SLICE_TARGET_BINDINGS = "target-bindings";
1000
+ const TELEGRAM_SYNC_SLICE_TARGET_BINDINGS = "target-bindings";
1001
1001
 
1002
1002
  export const TELEGRAM_SYNC_SLICES = [
1003
1003
  "bot-identity",
@@ -8,14 +8,15 @@
8
8
 
9
9
  import { randomUUID } from "node:crypto";
10
10
  import { createWriteStream, openAsBlob } from "node:fs";
11
- import { mkdir, readdir, stat, unlink, writeFile } from "node:fs/promises";
11
+ import { mkdir, readdir, rename, stat, unlink, writeFile } from "node:fs/promises";
12
12
  import { request as requestHttps } from "node:https";
13
13
  import { join } from "node:path";
14
- import { resolveTelegramTempDir } from "./paths.ts";
14
+ import type { TelegramAttachmentSource } from "./media.ts";
15
+ import { resolveTelegramAttachmentsDir } from "./paths.ts";
15
16
  import { Readable, Transform } from "node:stream";
16
17
  import { pipeline } from "node:stream/promises";
17
18
 
18
- export const TELEGRAM_API_BASE = "https://api.telegram.org";
19
+ const TELEGRAM_API_BASE = "https://api.telegram.org";
19
20
 
20
21
  export const TELEGRAM_FILE_MAX_BYTES = 50 * 1024 * 1024;
21
22
 
@@ -34,12 +35,10 @@ export function getTelegramInboundFileByteLimitFromEnv(
34
35
  }
35
36
 
36
37
  function getTelegramApiTempDir(): string {
37
- return resolveTelegramTempDir();
38
+ return resolveTelegramAttachmentsDir();
38
39
  }
39
40
  const TELEGRAM_TEMP_FILE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
40
41
  const activeTelegramApiWorkspaceAdmissionOperationIds = new Set<string>();
41
- const TELEGRAM_TEMP_SCRATCH_FILE_PATTERN =
42
- /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}-/u;
43
42
  const TELEGRAM_INBOUND_FILE_MAX_BYTES = getTelegramInboundFileByteLimitFromEnv(
44
43
  process.env,
45
44
  ["PI_TELEGRAM_INBOUND_FILE_MAX_BYTES", "TELEGRAM_MAX_FILE_SIZE_BYTES"],
@@ -750,6 +749,8 @@ export interface TelegramBridgeApiRuntimeDeps {
750
749
  tempDir: string;
751
750
  maxFileSizeBytes: number;
752
751
  tempFileMaxAgeMs: number;
752
+ /** Bot `@username` (or numeric id) naming private-chat attachments. */
753
+ getBotScope?: () => string | undefined;
753
754
  recordRuntimeEvent: (
754
755
  kind: "api" | "multipart" | "download",
755
756
  error: unknown,
@@ -774,7 +775,11 @@ export interface TelegramBridgeApiRuntime {
774
775
  fileName: string,
775
776
  options?: TelegramApiCallOptions,
776
777
  ) => Promise<TResponse>;
777
- downloadFile: (fileId: string, suggestedName: string) => Promise<string>;
778
+ downloadFile: (
779
+ fileId: string,
780
+ suggestedName: string,
781
+ source?: TelegramAttachmentSource,
782
+ ) => Promise<string>;
778
783
  deleteWebhook: (signal?: AbortSignal) => Promise<boolean>;
779
784
  getUpdates: (
780
785
  body: Record<string, unknown>,
@@ -853,6 +858,28 @@ function sanitizeFileName(name: string): string {
853
858
  return name.replace(/[^a-zA-Z0-9._-]+/g, "_");
854
859
  }
855
860
 
861
+ /** Names follow `<kind>-<scope>-<messageId>[-<index>][-<name>|.<ext>]`. */
862
+ /**
863
+ * `scope` is the bot `@username` (id fallback) in a private chat and the public chat `@username` (id fallback) elsewhere.
864
+ * A negative chat id loses its sign, so the name never contains two adjacent dashes.
865
+ */
866
+ export function createTelegramAttachmentFileName(
867
+ source: TelegramAttachmentSource,
868
+ generatedName: string,
869
+ botScope?: string,
870
+ ): string {
871
+ const chat = source.chat;
872
+ const clean = (value: string): string => sanitizeFileName(value.replace(/^@/u, "")).replace(/^[-.]+/u, "");
873
+ const scope = source.scope ? clean(source.scope) : chat?.type === "private" || !chat
874
+ ? botScope ? clean(botScope) : chat ? String(Math.abs(chat.id)) : ""
875
+ : chat.username ? clean(chat.username) : String(Math.abs(chat.id));
876
+ const dot = generatedName.lastIndexOf(".");
877
+ const extension = dot > 0 ? sanitizeFileName(generatedName.slice(dot)) : "";
878
+ const tail = source.userFileName ? `-${clean(source.userFileName)}` : extension;
879
+ return [sanitizeFileName(source.kind), scope, String(source.messageId)].filter(Boolean).join("-") +
880
+ (source.index !== undefined ? `-${source.index}` : "") + tail;
881
+ }
882
+
856
883
  class TelegramApiMalformedSuccessError extends Error {
857
884
  constructor(method: string, detail: string) {
858
885
  super(`Telegram API ${method} ${detail}`);
@@ -1490,9 +1517,8 @@ export async function cleanupTelegramTempFiles(
1490
1517
  return 0;
1491
1518
  }
1492
1519
  for (const entry of entries) {
1493
- if (!entry.isFile() || !TELEGRAM_TEMP_SCRATCH_FILE_PATTERN.test(entry.name)) {
1494
- continue;
1495
- }
1520
+ // The directory holds only attachment scratch files, so age alone decides.
1521
+ if (!entry.isFile()) continue;
1496
1522
  const path = join(tempDir, entry.name);
1497
1523
  try {
1498
1524
  const stats = await stat(path);
@@ -1643,10 +1669,9 @@ export async function downloadTelegramFile(
1643
1669
  );
1644
1670
  assertTelegramFileSizeWithinLimit(file.file_size, options?.maxFileSizeBytes);
1645
1671
  await mkdir(tempDir, { recursive: true, mode: 0o700 });
1646
- const targetPath = join(
1647
- tempDir,
1648
- `${randomUUID()}-${sanitizeFileName(suggestedName)}`,
1649
- );
1672
+ // Names carry scope and message id, so the same message maps to the same path; publish by rename.
1673
+ const targetPath = join(tempDir, sanitizeFileName(suggestedName));
1674
+ const partPath = `${targetPath}.${randomUUID()}.part`;
1650
1675
  const response = await callTelegramTransportRequest((family) =>
1651
1676
  telegramFetch(
1652
1677
  `${TELEGRAM_API_BASE}/file/bot${configuredBotToken}/${file.file_path}`,
@@ -1665,11 +1690,12 @@ export async function downloadTelegramFile(
1665
1690
  try {
1666
1691
  await writeTelegramDownloadResponse(
1667
1692
  response,
1668
- targetPath,
1693
+ partPath,
1669
1694
  options?.maxFileSizeBytes,
1670
1695
  );
1696
+ await rename(partPath, targetPath);
1671
1697
  } catch (error) {
1672
- await removeTelegramPartialDownload(targetPath);
1698
+ await removeTelegramPartialDownload(partPath);
1673
1699
  throw error;
1674
1700
  }
1675
1701
  return targetPath;
@@ -1700,21 +1726,6 @@ export async function answerTelegramCallbackQuery(
1700
1726
  }
1701
1727
  }
1702
1728
 
1703
- export async function deleteTelegramMessage(
1704
- botToken: string | undefined,
1705
- chatId: number,
1706
- messageId: number,
1707
- ): Promise<void> {
1708
- try {
1709
- await callTelegram<boolean>(botToken, "deleteMessage", {
1710
- chat_id: chatId,
1711
- message_id: messageId,
1712
- });
1713
- } catch {
1714
- // ignore
1715
- }
1716
- }
1717
-
1718
1729
  export function createTelegramChatActionSender<TAction extends string>(
1719
1730
  sendChatAction: (
1720
1731
  chatId: number,
@@ -1802,6 +1813,7 @@ export type TelegramWorkspaceThreadDeletionTransport = (
1802
1813
 
1803
1814
  export function createDefaultTelegramBridgeApiRuntime(deps: {
1804
1815
  getBotToken: () => string | undefined;
1816
+ getBotScope?: () => string | undefined;
1805
1817
  recordRuntimeEvent: TelegramBridgeApiRuntimeDeps["recordRuntimeEvent"];
1806
1818
  captureRequestErrorHandler?: TelegramBridgeApiRuntimeDeps["captureRequestErrorHandler"];
1807
1819
  targetActivity?: TelegramApiTargetActivityRuntime;
@@ -1831,6 +1843,7 @@ export function createDefaultTelegramBridgeApiRuntime(deps: {
1831
1843
  tempDir: getTelegramApiTempDir(),
1832
1844
  maxFileSizeBytes: TELEGRAM_INBOUND_FILE_MAX_BYTES,
1833
1845
  tempFileMaxAgeMs: TELEGRAM_TEMP_FILE_MAX_AGE_MS,
1846
+ ...(deps.getBotScope ? { getBotScope: deps.getBotScope } : {}),
1834
1847
  recordRuntimeEvent: deps.recordRuntimeEvent,
1835
1848
  captureRequestErrorHandler: deps.captureRequestErrorHandler,
1836
1849
  });
@@ -2040,11 +2053,13 @@ export function createTelegramBridgeApiRuntime(
2040
2053
  * Downloads a file from the Telegram servers into the local temp directory.
2041
2054
  * Used for inbound voice messages, photos, documents, etc.
2042
2055
  */
2043
- downloadFile: async (fileId, suggestedName) => {
2056
+ downloadFile: async (fileId, suggestedName, source) => {
2044
2057
  try {
2045
2058
  return await deps.client.downloadFile(
2046
2059
  fileId,
2047
- suggestedName,
2060
+ source
2061
+ ? createTelegramAttachmentFileName(source, suggestedName, deps.getBotScope?.())
2062
+ : suggestedName,
2048
2063
  deps.tempDir,
2049
2064
  {
2050
2065
  maxFileSizeBytes: deps.maxFileSizeBytes,
@@ -10,6 +10,12 @@ import { dirname } from "node:path";
10
10
  import { createHash, randomUUID } from "node:crypto";
11
11
 
12
12
  import { renameTelegramPathWithRetry, withTelegramFileTransaction } from "./locks.ts";
13
+ import { getTelegramJournalPublicationPaths } from "./paths.ts";
14
+ import {
15
+ isWireRecord as isObject,
16
+ hasOnlyWireKeys as onlyKeys,
17
+ isNonNegativeWireInteger as safeTime,
18
+ } from "./wire.ts";
13
19
  import type { TelegramWorkspaceDeletionPermit,
14
20
  TelegramWorkspaceDestructiveFence,
15
21
  TelegramWorkspaceRetirementFence } from "./workspace-admission.ts";
@@ -247,15 +253,6 @@ export interface TelegramThreadCleanupWorkStore {
247
253
  list(): TelegramThreadCleanupWorkSet[];
248
254
  }
249
255
 
250
- function isObject(value: unknown): value is Record<string, unknown> {
251
- return !!value && typeof value === "object" && !Array.isArray(value);
252
- }
253
- function onlyKeys(value: Record<string, unknown>, keys: readonly string[]): boolean {
254
- const allowed = new Set(keys); return Object.keys(value).every(key => allowed.has(key));
255
- }
256
- function safeTime(value: unknown): value is number {
257
- return Number.isSafeInteger(value) && (value as number) >= 0;
258
- }
259
256
  function validateWorkSet(value: unknown, profileName: string): TelegramThreadCleanupWorkSet {
260
257
  if (!isObject(value) || !onlyKeys(value, ["operationId", "createdAtMs", "entries"]) ||
261
258
  typeof value.operationId !== "string" || !value.operationId || !safeTime(value.createdAtMs) ||
@@ -311,11 +308,23 @@ function sameCandidates(left: readonly TelegramThreadCleanupCandidate[], right:
311
308
  return JSON.stringify(left) === JSON.stringify(right);
312
309
  }
313
310
 
311
+ function parseTelegramThreadCleanupWorkFile(value: unknown, profileName: string, tokenSha256: string): TelegramThreadCleanupWorkFile {
312
+ if (!isObject(value) || !onlyKeys(value, ["version", "profileName", "tokenSha256", "workSets"]) ||
313
+ value.version !== 1 || value.profileName !== profileName || value.tokenSha256 !== tokenSha256 || !Array.isArray(value.workSets))
314
+ throw new Error("Telegram Thread cleanup store identity or schema does not match.");
315
+ const workSets = value.workSets.map(workSet => validateWorkSet(workSet, profileName));
316
+ if (new Set(workSets.map(workSet => workSet.operationId)).size !== workSets.length)
317
+ throw new Error("Telegram Thread cleanup operation identity is ambiguous.");
318
+ return { version: 1, profileName, tokenSha256, workSets };
319
+ }
320
+
314
321
  export function createTelegramThreadCleanupWorkStore(options: {
315
- path: string; profileName: string; tokenSha256: string; maxWorkSets?: number;
322
+ path: string; runtimeDir?: string; profileName: string; tokenSha256: string; maxWorkSets?: number;
316
323
  maxBytes?: number; getNowMs?: () => number;
317
324
  onPublicationBoundary?: (boundary: "after-write-before-rename" | "after-rename") => void;
318
325
  }): TelegramThreadCleanupWorkStore {
326
+ options = { ...options };
327
+ const publicationPaths = getTelegramJournalPublicationPaths(options.path, options.runtimeDir);
319
328
  const maxWorkSets = options.maxWorkSets ?? 32;
320
329
  const maxBytes = options.maxBytes ?? 1024 * 1024;
321
330
  const now = options.getNowMs ?? Date.now;
@@ -344,21 +353,15 @@ export function createTelegramThreadCleanupWorkStore(options: {
344
353
  opened.mtimeNs !== before.mtimeNs) throw new Error("Telegram Thread cleanup store changed during inspection.");
345
354
  value = JSON.parse(readFileSync(fd, "utf8")) as unknown;
346
355
  } finally { closeSync(fd); }
347
- if (!isObject(value) || !onlyKeys(value, ["version", "profileName", "tokenSha256", "workSets"]) ||
348
- value.version !== 1 || value.profileName !== options.profileName ||
349
- value.tokenSha256 !== options.tokenSha256 || !Array.isArray(value.workSets))
350
- throw new Error("Telegram Thread cleanup store identity or schema does not match.");
351
- const workSets = value.workSets.map(workSet => validateWorkSet(workSet, options.profileName));
352
- if (new Set(workSets.map(workSet => workSet.operationId)).size !== workSets.length)
353
- throw new Error("Telegram Thread cleanup operation identity is ambiguous.");
354
- return { version: 1, profileName: options.profileName, tokenSha256: options.tokenSha256, workSets };
356
+ return parseTelegramThreadCleanupWorkFile(value, options.profileName, options.tokenSha256);
355
357
  };
356
358
  const publish = (file: TelegramThreadCleanupWorkFile): void => {
357
359
  if (file.workSets.length > maxWorkSets) throw new Error("Telegram Thread cleanup work-set capacity reached.");
358
360
  const serialized = `${JSON.stringify(file, null, 2)}\n`;
359
361
  if (Buffer.byteLength(serialized) > maxBytes) throw new Error("Telegram Thread cleanup byte capacity reached.");
360
- const temporaryPath = `${options.path}.${process.pid}.${randomUUID()}.tmp`;
362
+ const temporaryPath = `${publicationPaths.temporaryBasePath}.${process.pid}.${randomUUID()}.tmp`;
361
363
  mkdirSync(dirname(options.path), { recursive: true, mode: 0o700 });
364
+ mkdirSync(dirname(temporaryPath), { recursive: true, mode: 0o700 });
362
365
  try {
363
366
  writeFileSync(temporaryPath, serialized, { mode: 0o600 });
364
367
  chmodSync(temporaryPath, 0o600);
@@ -370,7 +373,7 @@ export function createTelegramThreadCleanupWorkStore(options: {
370
373
  } finally { try { unlinkSync(temporaryPath); } catch { /* rename consumed it */ } }
371
374
  };
372
375
  const mutate = <T>(operation: (file: TelegramThreadCleanupWorkFile) => T): T =>
373
- withTelegramFileTransaction(`${options.path}.transaction`, () => operation(read()));
376
+ withTelegramFileTransaction(publicationPaths.transactionPath, () => operation(read()));
374
377
  return {
375
378
  prepare(operationId, candidates) {
376
379
  if (!operationId || candidates.length === 0 || candidates.some(candidate => candidate.profileName !== options.profileName))
@@ -435,7 +438,7 @@ export function createTelegramThreadCleanupWorkStore(options: {
435
438
  },
436
439
  list() {
437
440
  return structuredClone(withTelegramFileTransaction(
438
- `${options.path}.transaction`,
441
+ publicationPaths.transactionPath,
439
442
  () => read().workSets,
440
443
  ));
441
444
  },