@llblab/pi-kit 0.27.6 → 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 (182) hide show
  1. package/BACKLOG.md +2 -5
  2. package/CHANGELOG.md +5 -0
  3. package/README.md +2 -2
  4. package/node_modules/@llblab/pi-telegram/AGENTS.md +10 -8
  5. package/node_modules/@llblab/pi-telegram/BACKLOG.md +1 -9
  6. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +13 -0
  7. package/node_modules/@llblab/pi-telegram/LICENSE +21 -0
  8. package/node_modules/@llblab/pi-telegram/README.md +15 -5
  9. package/node_modules/@llblab/pi-telegram/dist/lib/activity-verbosity.d.ts +1 -5
  10. package/node_modules/@llblab/pi-telegram/dist/lib/activity-verbosity.js +5 -7
  11. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +5 -1
  12. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +6 -1
  13. package/node_modules/@llblab/pi-telegram/dist/lib/bus-api.js +4 -2
  14. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.d.ts +54 -56
  15. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +206 -139
  16. package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.d.ts +21 -2
  17. package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +181 -25
  18. package/node_modules/@llblab/pi-telegram/dist/lib/bus-transport.d.ts +4 -1
  19. package/node_modules/@llblab/pi-telegram/dist/lib/bus-transport.js +28 -4
  20. package/node_modules/@llblab/pi-telegram/dist/lib/bus.d.ts +52 -56
  21. package/node_modules/@llblab/pi-telegram/dist/lib/bus.js +78 -246
  22. package/node_modules/@llblab/pi-telegram/dist/lib/channel-posts.d.ts +1 -3
  23. package/node_modules/@llblab/pi-telegram/dist/lib/channel-posts.js +23 -26
  24. package/node_modules/@llblab/pi-telegram/dist/lib/command-templates.d.ts +0 -5
  25. package/node_modules/@llblab/pi-telegram/dist/lib/command-templates.js +5 -5
  26. package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +0 -23
  27. package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +15 -15
  28. package/node_modules/@llblab/pi-telegram/dist/lib/config.d.ts +0 -17
  29. package/node_modules/@llblab/pi-telegram/dist/lib/config.js +12 -14
  30. package/node_modules/@llblab/pi-telegram/dist/lib/delivery.d.ts +0 -4
  31. package/node_modules/@llblab/pi-telegram/dist/lib/delivery.js +2 -5
  32. package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +255 -61
  33. package/node_modules/@llblab/pi-telegram/dist/lib/generative-apps.d.ts +0 -19
  34. package/node_modules/@llblab/pi-telegram/dist/lib/inbound.d.ts +0 -2
  35. package/node_modules/@llblab/pi-telegram/dist/lib/inbound.js +2 -2
  36. package/node_modules/@llblab/pi-telegram/dist/lib/journal.d.ts +177 -16
  37. package/node_modules/@llblab/pi-telegram/dist/lib/journal.js +1115 -249
  38. package/node_modules/@llblab/pi-telegram/dist/lib/keyboard.d.ts +0 -2
  39. package/node_modules/@llblab/pi-telegram/dist/lib/keyboard.js +2 -2
  40. package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.js +1 -1
  41. package/node_modules/@llblab/pi-telegram/dist/lib/locks.d.ts +100 -12
  42. package/node_modules/@llblab/pi-telegram/dist/lib/locks.js +380 -14
  43. package/node_modules/@llblab/pi-telegram/dist/lib/logging.d.ts +30 -12
  44. package/node_modules/@llblab/pi-telegram/dist/lib/logging.js +129 -72
  45. package/node_modules/@llblab/pi-telegram/dist/lib/media.d.ts +28 -5
  46. package/node_modules/@llblab/pi-telegram/dist/lib/media.js +26 -8
  47. package/node_modules/@llblab/pi-telegram/dist/lib/menu-model.d.ts +0 -11
  48. package/node_modules/@llblab/pi-telegram/dist/lib/menu-model.js +8 -8
  49. package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.d.ts +0 -18
  50. package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.js +18 -18
  51. package/node_modules/@llblab/pi-telegram/dist/lib/menu-status.d.ts +4 -4
  52. package/node_modules/@llblab/pi-telegram/dist/lib/menu-status.js +12 -7
  53. package/node_modules/@llblab/pi-telegram/dist/lib/menu-thinking.d.ts +1 -1
  54. package/node_modules/@llblab/pi-telegram/dist/lib/menu-thinking.js +1 -1
  55. package/node_modules/@llblab/pi-telegram/dist/lib/menu.d.ts +1 -0
  56. package/node_modules/@llblab/pi-telegram/dist/lib/menu.js +4 -3
  57. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.d.ts +0 -1
  58. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.js +1 -1
  59. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-buttons.js +1 -8
  60. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.d.ts +1 -6
  61. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.js +4 -21
  62. package/node_modules/@llblab/pi-telegram/dist/lib/outbound.d.ts +0 -8
  63. package/node_modules/@llblab/pi-telegram/dist/lib/outbound.js +5 -5
  64. package/node_modules/@llblab/pi-telegram/dist/lib/paths.d.ts +50 -7
  65. package/node_modules/@llblab/pi-telegram/dist/lib/paths.js +155 -17
  66. package/node_modules/@llblab/pi-telegram/dist/lib/pi.d.ts +1 -0
  67. package/node_modules/@llblab/pi-telegram/dist/lib/pi.js +2 -1
  68. package/node_modules/@llblab/pi-telegram/dist/lib/polling.d.ts +0 -5
  69. package/node_modules/@llblab/pi-telegram/dist/lib/polling.js +5 -5
  70. package/node_modules/@llblab/pi-telegram/dist/lib/preview.d.ts +0 -3
  71. package/node_modules/@llblab/pi-telegram/dist/lib/preview.js +3 -3
  72. package/node_modules/@llblab/pi-telegram/dist/lib/prompt-templates.d.ts +0 -1
  73. package/node_modules/@llblab/pi-telegram/dist/lib/prompt-templates.js +1 -1
  74. package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +3 -15
  75. package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +69 -8
  76. package/node_modules/@llblab/pi-telegram/dist/lib/recovery.d.ts +29 -9
  77. package/node_modules/@llblab/pi-telegram/dist/lib/recovery.js +104 -33
  78. package/node_modules/@llblab/pi-telegram/dist/lib/replies.d.ts +0 -4
  79. package/node_modules/@llblab/pi-telegram/dist/lib/replies.js +1 -1
  80. package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +73 -8
  81. package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +2206 -307
  82. package/node_modules/@llblab/pi-telegram/dist/lib/sections.d.ts +0 -2
  83. package/node_modules/@llblab/pi-telegram/dist/lib/sections.js +1 -1
  84. package/node_modules/@llblab/pi-telegram/dist/lib/status.d.ts +44 -9
  85. package/node_modules/@llblab/pi-telegram/dist/lib/status.js +149 -27
  86. package/node_modules/@llblab/pi-telegram/dist/lib/sync.d.ts +0 -5
  87. package/node_modules/@llblab/pi-telegram/dist/lib/sync.js +5 -4
  88. package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +11 -3
  89. package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +34 -23
  90. package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.d.ts +1 -0
  91. package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.js +18 -21
  92. package/node_modules/@llblab/pi-telegram/dist/lib/thread-naming.d.ts +32 -5
  93. package/node_modules/@llblab/pi-telegram/dist/lib/thread-naming.js +191 -7
  94. package/node_modules/@llblab/pi-telegram/dist/lib/thread-reconciler.d.ts +2 -0
  95. package/node_modules/@llblab/pi-telegram/dist/lib/thread-reconciler.js +1 -1
  96. package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +291 -59
  97. package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +1647 -369
  98. package/node_modules/@llblab/pi-telegram/dist/lib/turns.d.ts +0 -1
  99. package/node_modules/@llblab/pi-telegram/dist/lib/turns.js +1 -1
  100. package/node_modules/@llblab/pi-telegram/dist/lib/updates.d.ts +184 -28
  101. package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +1032 -114
  102. package/node_modules/@llblab/pi-telegram/dist/lib/wire.d.ts +12 -0
  103. package/node_modules/@llblab/pi-telegram/dist/lib/wire.js +21 -0
  104. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.d.ts +25 -17
  105. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.js +95 -20
  106. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-identity.d.ts +20 -0
  107. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-identity.js +103 -0
  108. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.d.ts +43 -8
  109. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.js +148 -52
  110. package/node_modules/@llblab/pi-telegram/dist/package.json +6 -6
  111. package/node_modules/@llblab/pi-telegram/dist/skills/generated-control-surface/SKILL.md +3 -3
  112. package/node_modules/@llblab/pi-telegram/dist/skills/telegram-bridge/references/diagnosis.md +3 -3
  113. package/node_modules/@llblab/pi-telegram/docs/README.md +1 -1
  114. package/node_modules/@llblab/pi-telegram/docs/activity.md +1 -1
  115. package/node_modules/@llblab/pi-telegram/docs/architecture.md +152 -34
  116. package/node_modules/@llblab/pi-telegram/docs/callback-namespaces.md +1 -1
  117. package/node_modules/@llblab/pi-telegram/docs/delivery.md +1 -1
  118. package/node_modules/@llblab/pi-telegram/docs/generative-apps.md +2 -2
  119. package/node_modules/@llblab/pi-telegram/docs/inbound.md +1 -1
  120. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +125 -19
  121. package/node_modules/@llblab/pi-telegram/docs/public-api.md +3 -1
  122. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +22 -8
  123. package/node_modules/@llblab/pi-telegram/docs/updates.md +1 -1
  124. package/node_modules/@llblab/pi-telegram/lib/activity-verbosity.ts +5 -9
  125. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +6 -0
  126. package/node_modules/@llblab/pi-telegram/lib/bus-api.ts +5 -2
  127. package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +243 -224
  128. package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +178 -31
  129. package/node_modules/@llblab/pi-telegram/lib/bus-transport.ts +30 -8
  130. package/node_modules/@llblab/pi-telegram/lib/bus.ts +110 -334
  131. package/node_modules/@llblab/pi-telegram/lib/channel-posts.ts +29 -29
  132. package/node_modules/@llblab/pi-telegram/lib/command-templates.ts +5 -5
  133. package/node_modules/@llblab/pi-telegram/lib/commands.ts +15 -15
  134. package/node_modules/@llblab/pi-telegram/lib/config.ts +12 -17
  135. package/node_modules/@llblab/pi-telegram/lib/delivery.ts +2 -8
  136. package/node_modules/@llblab/pi-telegram/lib/extension.ts +257 -72
  137. package/node_modules/@llblab/pi-telegram/lib/generative-apps.ts +0 -22
  138. package/node_modules/@llblab/pi-telegram/lib/inbound.ts +2 -2
  139. package/node_modules/@llblab/pi-telegram/lib/journal.ts +1182 -326
  140. package/node_modules/@llblab/pi-telegram/lib/keyboard.ts +2 -2
  141. package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +1 -1
  142. package/node_modules/@llblab/pi-telegram/lib/locks.ts +402 -23
  143. package/node_modules/@llblab/pi-telegram/lib/logging.ts +151 -101
  144. package/node_modules/@llblab/pi-telegram/lib/media.ts +51 -9
  145. package/node_modules/@llblab/pi-telegram/lib/menu-model.ts +8 -8
  146. package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +18 -18
  147. package/node_modules/@llblab/pi-telegram/lib/menu-status.ts +11 -0
  148. package/node_modules/@llblab/pi-telegram/lib/menu-thinking.ts +2 -2
  149. package/node_modules/@llblab/pi-telegram/lib/menu.ts +5 -1
  150. package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +1 -1
  151. package/node_modules/@llblab/pi-telegram/lib/outbound-buttons.ts +1 -10
  152. package/node_modules/@llblab/pi-telegram/lib/outbound-markup.ts +5 -24
  153. package/node_modules/@llblab/pi-telegram/lib/outbound.ts +5 -5
  154. package/node_modules/@llblab/pi-telegram/lib/paths.ts +177 -17
  155. package/node_modules/@llblab/pi-telegram/lib/pi.ts +5 -2
  156. package/node_modules/@llblab/pi-telegram/lib/polling.ts +5 -5
  157. package/node_modules/@llblab/pi-telegram/lib/preview.ts +3 -3
  158. package/node_modules/@llblab/pi-telegram/lib/prompt-templates.ts +1 -1
  159. package/node_modules/@llblab/pi-telegram/lib/queue.ts +67 -9
  160. package/node_modules/@llblab/pi-telegram/lib/recovery.ts +104 -44
  161. package/node_modules/@llblab/pi-telegram/lib/replies.ts +1 -1
  162. package/node_modules/@llblab/pi-telegram/lib/routing.ts +1955 -375
  163. package/node_modules/@llblab/pi-telegram/lib/sections.ts +1 -1
  164. package/node_modules/@llblab/pi-telegram/lib/status.ts +156 -36
  165. package/node_modules/@llblab/pi-telegram/lib/sync.ts +4 -4
  166. package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +48 -33
  167. package/node_modules/@llblab/pi-telegram/lib/thread-cleanup-manager.ts +24 -21
  168. package/node_modules/@llblab/pi-telegram/lib/thread-naming.ts +267 -9
  169. package/node_modules/@llblab/pi-telegram/lib/thread-reconciler.ts +3 -1
  170. package/node_modules/@llblab/pi-telegram/lib/threads.ts +1697 -488
  171. package/node_modules/@llblab/pi-telegram/lib/turns.ts +1 -1
  172. package/node_modules/@llblab/pi-telegram/lib/updates.ts +1072 -151
  173. package/node_modules/@llblab/pi-telegram/lib/wire.ts +28 -0
  174. package/node_modules/@llblab/pi-telegram/lib/workspace-admission.ts +97 -47
  175. package/node_modules/@llblab/pi-telegram/lib/workspace-identity.ts +147 -0
  176. package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +188 -89
  177. package/node_modules/@llblab/pi-telegram/package.json +6 -6
  178. package/node_modules/@llblab/pi-telegram/scripts/audit-exports.mjs +100 -0
  179. package/node_modules/@llblab/pi-telegram/scripts/check-downgrade.mjs +80 -43
  180. package/node_modules/@llblab/pi-telegram/skills/generated-control-surface/SKILL.md +3 -3
  181. package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/diagnosis.md +3 -3
  182. package/package.json +2 -2
@@ -26,7 +26,7 @@ const TELEGRAM_GET_UPDATES_CONFLICT_FAST_RETRY_LIMIT = 3;
26
26
  const TELEGRAM_GET_UPDATES_CONFLICT_FAST_RETRY_MS = 1_000;
27
27
  const TELEGRAM_GET_UPDATES_CONFLICT_SLOW_RETRY_MS = 3_000;
28
28
  const TELEGRAM_POLLING_RETRY_MS = 3_000;
29
- export const TELEGRAM_GET_UPDATES_GRACE_MS = 10_000;
29
+ const TELEGRAM_GET_UPDATES_GRACE_MS = 10_000;
30
30
 
31
31
  // Standard Telegram DM polling does not expose ordinary message-deletion events.
32
32
  // Business deletions belong to a separate namespace and default routing ignores them.
@@ -163,7 +163,7 @@ export function createTelegramPollingControllerState(): TelegramPollingControlle
163
163
  };
164
164
  }
165
165
 
166
- export function getTelegramPollingStateSnapshot(
166
+ function getTelegramPollingStateSnapshot(
167
167
  state: TelegramPollingControllerState,
168
168
  ): TelegramPollingStateSnapshot {
169
169
  return {
@@ -906,7 +906,7 @@ export function createTelegramThreadCapabilityOrchestration<TContext, TOwner>(
906
906
  };
907
907
  }
908
908
 
909
- export async function readTelegramThreadCapability(
909
+ async function readTelegramThreadCapability(
910
910
  deps: TelegramThreadCapabilityReaderDeps,
911
911
  ): Promise<boolean | undefined> {
912
912
  const bot = await deps.callApi<{ has_topics_enabled?: boolean }>("getMe", {});
@@ -915,7 +915,7 @@ export async function readTelegramThreadCapability(
915
915
  return undefined;
916
916
  }
917
917
 
918
- export async function probeTelegramStartupThreadCapability(
918
+ async function probeTelegramStartupThreadCapability(
919
919
  deps: TelegramStartupThreadCapabilityProbeDeps,
920
920
  isCurrent: () => boolean = () => true,
921
921
  ): Promise<boolean | undefined> {
@@ -1201,7 +1201,7 @@ export function createTelegramThreadAwarePollingPorts<TContext, TOwner>(
1201
1201
  };
1202
1202
  }
1203
1203
 
1204
- export function createTelegramThreadTargetObservationHandler<TContext>(
1204
+ function createTelegramThreadTargetObservationHandler<TContext>(
1205
1205
  deps: TelegramThreadCapabilityRuntimeDeps<TContext>,
1206
1206
  ): TelegramThreadTargetObservationHandler<TContext> {
1207
1207
  let transitionPending = false;
@@ -397,7 +397,7 @@ export function createTelegramPreviewController(
397
397
  };
398
398
  }
399
399
 
400
- export function createTelegramAssistantMessagePreviewHooks<TMessage>(
400
+ function createTelegramAssistantMessagePreviewHooks<TMessage>(
401
401
  deps: TelegramAssistantMessagePreviewHookDeps<TMessage>,
402
402
  ): TelegramAssistantMessagePreviewHooks<TMessage> {
403
403
  return {
@@ -425,7 +425,7 @@ export function shouldSuppressPreviewForGuestTurn(
425
425
  return !!turn?.guestQueryId;
426
426
  }
427
427
 
428
- export async function handleTelegramAssistantMessagePreviewStart<TMessage>(
428
+ async function handleTelegramAssistantMessagePreviewStart<TMessage>(
429
429
  message: TMessage,
430
430
  deps: TelegramAssistantMessagePreviewStartDeps<TMessage>,
431
431
  ): Promise<void> {
@@ -453,7 +453,7 @@ export async function handleTelegramAssistantMessagePreviewStart<TMessage>(
453
453
  deps.setState(next);
454
454
  }
455
455
 
456
- export async function handleTelegramAssistantMessagePreviewUpdate<TMessage>(
456
+ async function handleTelegramAssistantMessagePreviewUpdate<TMessage>(
457
457
  message: TMessage,
458
458
  deps: TelegramAssistantMessagePreviewUpdateDeps<TMessage>,
459
459
  ): Promise<void> {
@@ -79,7 +79,7 @@ export function substitutePromptTemplateArgs(
79
79
  return result.replace(/\$ARGUMENTS/g, allArgs).replace(/\$@/g, allArgs);
80
80
  }
81
81
 
82
- export function isTelegramPromptTemplateCommandName(name: string): boolean {
82
+ function isTelegramPromptTemplateCommandName(name: string): boolean {
83
83
  return TELEGRAM_BOT_COMMAND_NAME_PATTERN.test(name);
84
84
  }
85
85
 
@@ -7,6 +7,7 @@
7
7
  import { createHash } from "node:crypto";
8
8
 
9
9
  import { isVoiceTurn } from "./voice.ts";
10
+ import { isWireRecord as isQueuePayloadRecord } from "./wire.ts";
10
11
  import { isTelegramApiCommitUnknownError } from "./telegram-api.ts";
11
12
 
12
13
  // --- Queue Items ---
@@ -147,7 +148,7 @@ export type TelegramQueueItem<TContext = unknown> =
147
148
  PendingTelegramTurn | PendingTelegramControlItem<TContext>;
148
149
 
149
150
  export const TELEGRAM_QUEUE_HANDOFF_PAYLOAD_MAX_BYTES = 8 * 1024 * 1024;
150
- export const TELEGRAM_QUEUE_HANDOFF_MAX_RECEIPTS = 256;
151
+ const TELEGRAM_QUEUE_HANDOFF_MAX_RECEIPTS = 256;
151
152
 
152
153
  export interface TelegramQueueHandoffBase {
153
154
  chatId: number;
@@ -191,6 +192,63 @@ export interface TelegramQueueHandoff {
191
192
  payload: TelegramQueueHandoffPayload;
192
193
  }
193
194
 
195
+ function parseQueuePayloadReceipt(value: unknown, kind: TelegramQueueItemKind): TelegramQueueAdmissionReceipt | undefined {
196
+ if (!isQueuePayloadRecord(value) || value.queueKind !== kind ||
197
+ typeof value.receiptId !== "string" || !value.receiptId ||
198
+ (value.journalBindingKey !== undefined && (typeof value.journalBindingKey !== "string" || !value.journalBindingKey.trim())) ||
199
+ !Array.isArray(value.sourceUpdateIds) || value.sourceUpdateIds.length === 0 ||
200
+ value.sourceUpdateIds.some((id, index) => !Number.isSafeInteger(id) || id < 0 ||
201
+ (index > 0 && id <= (value.sourceUpdateIds as number[])[index - 1]!))) return undefined;
202
+ return { queueKind: kind, receiptId: value.receiptId, sourceUpdateIds: value.sourceUpdateIds as number[],
203
+ ...(typeof value.journalBindingKey === "string" ? { journalBindingKey: value.journalBindingKey } : {}) };
204
+ }
205
+
206
+ /** Data decoder shared by authenticated IPC and cold storage; decoding grants no admission. */
207
+ export function parseTelegramQueueHandoffPayload(value: unknown): TelegramQueueHandoffPayload | undefined {
208
+ if (!isQueuePayloadRecord(value) || (value.kind !== "prompt" && value.kind !== "control")) return undefined;
209
+ const target = isQueuePayloadRecord(value.target) && typeof value.target.chatId === "number"
210
+ ? { chatId: value.target.chatId, ...(typeof value.target.threadId === "number" ? { threadId: value.target.threadId } : {}) } : undefined;
211
+ const transportStamp = isQueuePayloadRecord(value.transportStamp) &&
212
+ typeof value.transportStamp.profile === "string" && typeof value.transportStamp.generation === "string"
213
+ ? { profile: value.transportStamp.profile, generation: value.transportStamp.generation } : undefined;
214
+ if (!Number.isSafeInteger(value.chatId) || (value.target !== undefined && !target) ||
215
+ (value.transportStamp !== undefined && !transportStamp) || !Number.isSafeInteger(value.replyToMessageId) ||
216
+ (value.guestQueryId !== undefined && typeof value.guestQueryId !== "string") ||
217
+ (value.guestInlineMessageId !== undefined && typeof value.guestInlineMessageId !== "string") ||
218
+ !Number.isSafeInteger(value.queueOrder) || (value.queueLane !== "control" && value.queueLane !== "priority" && value.queueLane !== "default") ||
219
+ !Number.isSafeInteger(value.laneOrder) || typeof value.statusSummary !== "string" || !Array.isArray(value.admissionReceipts)) return undefined;
220
+ const admissionReceipts = value.admissionReceipts.map(receipt => parseQueuePayloadReceipt(receipt, value.kind as TelegramQueueItemKind));
221
+ if (admissionReceipts.length === 0 || admissionReceipts.length > TELEGRAM_QUEUE_HANDOFF_MAX_RECEIPTS ||
222
+ admissionReceipts.some(receipt => !receipt)) return undefined;
223
+ const base = { chatId: value.chatId as number, ...(target ? { target } : {}), ...(transportStamp ? { transportStamp } : {}),
224
+ replyToMessageId: value.replyToMessageId as number,
225
+ ...(typeof value.guestQueryId === "string" ? { guestQueryId: value.guestQueryId } : {}),
226
+ ...(typeof value.guestInlineMessageId === "string" ? { guestInlineMessageId: value.guestInlineMessageId } : {}),
227
+ queueOrder: value.queueOrder as number, queueLane: value.queueLane as TelegramQueueLane,
228
+ laneOrder: value.laneOrder as number, statusSummary: value.statusSummary,
229
+ admissionReceipts: admissionReceipts as TelegramQueueAdmissionReceipt[] };
230
+ if (value.kind === "control") {
231
+ return value.queueLane === "control" && (value.controlType === "status" || value.controlType === "model")
232
+ ? { kind: "control", controlType: value.controlType, ...base } : undefined;
233
+ }
234
+ if (value.queueLane === "control" || !Array.isArray(value.sourceMessageIds) || value.sourceMessageIds.some(id => !Number.isSafeInteger(id)) ||
235
+ !Array.isArray(value.queuedAttachments) || value.queuedAttachments.some(attachment => !isQueuePayloadRecord(attachment) ||
236
+ typeof attachment.path !== "string" || typeof attachment.fileName !== "string") ||
237
+ !Array.isArray(value.content) || value.content.some(content => !isQueuePayloadRecord(content) ||
238
+ (content.type === "text" ? typeof content.text !== "string" : content.type === "image"
239
+ ? typeof content.data !== "string" || typeof content.mimeType !== "string" : true)) ||
240
+ typeof value.historyText !== "string" || (value.priorityEmoji !== undefined && typeof value.priorityEmoji !== "string") ||
241
+ (value.reactionSuppressionEmoji !== undefined && typeof value.reactionSuppressionEmoji !== "string") ||
242
+ (value.voiceReplyPreferred !== undefined && typeof value.voiceReplyPreferred !== "boolean") ||
243
+ (value.voiceReplyRequired !== undefined && typeof value.voiceReplyRequired !== "boolean")) return undefined;
244
+ return { kind: "prompt", ...base, sourceMessageIds: value.sourceMessageIds as number[],
245
+ queuedAttachments: value.queuedAttachments as QueuedAttachment[], content: value.content as TelegramPromptContent[], historyText: value.historyText,
246
+ ...(typeof value.priorityEmoji === "string" ? { priorityEmoji: value.priorityEmoji } : {}),
247
+ ...(typeof value.reactionSuppressionEmoji === "string" ? { reactionSuppressionEmoji: value.reactionSuppressionEmoji } : {}),
248
+ ...(typeof value.voiceReplyPreferred === "boolean" ? { voiceReplyPreferred: value.voiceReplyPreferred } : {}),
249
+ ...(typeof value.voiceReplyRequired === "boolean" ? { voiceReplyRequired: value.voiceReplyRequired } : {}) };
250
+ }
251
+
194
252
  export interface TelegramQueueHandoffStageReceipt {
195
253
  status: "staged";
196
254
  receiptId: string;
@@ -367,7 +425,7 @@ function getTelegramQueueLaneRank(lane: TelegramQueueLane): number {
367
425
  return getTelegramQueueLaneContract(lane).dispatchRank;
368
426
  }
369
427
 
370
- export function isPendingTelegramTurn<TContext = unknown>(
428
+ function isPendingTelegramTurn<TContext = unknown>(
371
429
  item: TelegramQueueItem<TContext>,
372
430
  ): item is PendingTelegramTurn {
373
431
  return item.kind === "prompt";
@@ -434,7 +492,7 @@ export function createTelegramTransportStampedQueueStore<TContext>(
434
492
  };
435
493
  }
436
494
 
437
- export function isTelegramQueueItemSkipped<TContext = unknown>(
495
+ function isTelegramQueueItemSkipped<TContext = unknown>(
438
496
  item: TelegramQueueItem<TContext>,
439
497
  ): boolean {
440
498
  return item.kind === "prompt" && Boolean(item.reactionSuppressionEmoji);
@@ -936,7 +994,7 @@ export function removeTelegramQueueItemsByMessageIds<TContext = unknown>(
936
994
  };
937
995
  }
938
996
 
939
- export function removeTelegramQueuedGuestPromptByOrder<TContext = unknown>(
997
+ function removeTelegramQueuedGuestPromptByOrder<TContext = unknown>(
940
998
  items: TelegramQueueItem<TContext>[],
941
999
  queueOrder: number,
942
1000
  ): {
@@ -2695,7 +2753,7 @@ function appendTelegramQueueItemRuntime<TContext>(
2695
2753
  commitReorderedTelegramQueueItemsRuntime(nextItems, deps);
2696
2754
  }
2697
2755
 
2698
- export function reorderTelegramQueueItemsRuntime<TContext>(
2756
+ function reorderTelegramQueueItemsRuntime<TContext>(
2699
2757
  deps: TelegramQueueMutationRuntimeDeps<TContext>,
2700
2758
  ): void {
2701
2759
  commitReorderedTelegramQueueItemsRuntime(deps.getQueuedItems(), deps);
@@ -2737,7 +2795,7 @@ export function removeTelegramQueueItemsByMessageIdsRuntime<TContext>(
2737
2795
  return removedCount;
2738
2796
  }
2739
2797
 
2740
- export function removeTelegramQueuedGuestPromptByOrderRuntime<TContext>(
2798
+ function removeTelegramQueuedGuestPromptByOrderRuntime<TContext>(
2741
2799
  queueOrder: number,
2742
2800
  deps: TelegramQueueMutationRuntimeDeps<TContext>,
2743
2801
  ): boolean {
@@ -2883,7 +2941,7 @@ export interface TelegramControlRuntimeDeps<
2883
2941
  chatId: number,
2884
2942
  replyToMessageId: number,
2885
2943
  text: string,
2886
- options?: { target?: TelegramQueueTarget },
2944
+ options?: { target?: TelegramQueueTarget; parseMode?: "HTML" },
2887
2945
  ) => Promise<number | undefined>;
2888
2946
  onSettled: (item: PendingTelegramControlItem<TContext>) => void;
2889
2947
  }
@@ -2936,7 +2994,7 @@ export interface TelegramDeferredQueueDispatchRuntime<TContext = unknown> {
2936
2994
  * Production debounce for deferred queue dispatch; the factory defaults to this
2937
2995
  * so the entrypoint wires ports instead of policy constants.
2938
2996
  */
2939
- export const TELEGRAM_DEFERRED_DISPATCH_DELAY_MS = 50;
2997
+ const TELEGRAM_DEFERRED_DISPATCH_DELAY_MS = 50;
2940
2998
 
2941
2999
  export function createTelegramDeferredQueueDispatchRuntime<TContext = unknown>(
2942
3000
  deps: TelegramDeferredQueueDispatchRuntimeDeps = {},
@@ -3436,7 +3494,7 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
3436
3494
  dispatchPlan.item.chatId,
3437
3495
  dispatchPlan.item.replyToMessageId,
3438
3496
  "<b>⏩ Dispatching next queued turn.</b>",
3439
- { target: dispatchPlan.item.target },
3497
+ { target: dispatchPlan.item.target, parseMode: "HTML" },
3440
3498
  ).catch((error) => {
3441
3499
  deps.recordRuntimeEvent?.("dispatch", error, {
3442
3500
  phase: "next-announcement",
@@ -4,21 +4,21 @@
4
4
  * Owns fail-safe classification of temporary ownership and routing artifacts
5
5
  */
6
6
 
7
- import { randomUUID } from "node:crypto";
8
7
  import {
9
8
  existsSync,
10
9
  lstatSync,
11
- mkdirSync,
12
10
  readFileSync,
13
11
  readdirSync,
14
- renameSync,
12
+ rmdirSync,
13
+ rmSync,
15
14
  } from "node:fs";
16
15
  import { basename, dirname, join } from "node:path";
17
16
 
17
+ import { decodeTelegramSessionDirectoryName, getTelegramProfilePathSuffix } from "./paths.ts";
18
+
18
19
  import {
19
20
  isProcessAlive as defaultIsProcessAlive,
20
21
  parseTelegramLockEntry,
21
- renameTelegramPathWithRetry,
22
22
  TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS,
23
23
  withTelegramFileTransaction,
24
24
  type TelegramFileTransactionOptions,
@@ -59,22 +59,22 @@ export type TelegramRuntimeRecoveryResult =
59
59
  | {
60
60
  kind: "blocked-live-owner";
61
61
  livePids: number[];
62
- quarantineDir?: string;
62
+ deletedPaths?: string[];
63
63
  }
64
64
  | {
65
65
  kind: "recovered";
66
66
  artifacts: TelegramRuntimeCorruptArtifact[];
67
- quarantineDir: string;
67
+ /** Damaged disposable artifacts deleted per the approved corruption policy; nothing is quarantined. */
68
+ deletedPaths: string[];
68
69
  };
69
70
 
70
71
  export interface TelegramRuntimeRecoveryOptions
71
72
  extends TelegramRuntimeRecoveryClassificationOptions {
72
73
  recoveryTransactionPath?: string;
73
- quarantineRoot?: string;
74
74
  pid?: number;
75
75
  getNowMs?: () => number;
76
- quarantineRename?: typeof renameSync;
77
- quarantineRenameRetryDelayMs?: number;
76
+ /** Fault-injection seam; production uses bounded-retry recursive removal. */
77
+ removePath?: (path: string) => void;
78
78
  transactionOptions?: TelegramFileTransactionOptions;
79
79
  }
80
80
 
@@ -145,6 +145,9 @@ function inspectOwners(
145
145
  function inspectState(path: string): ArtifactInspection {
146
146
  if (!existsSync(path)) return { source: "state", ownerPids: [] };
147
147
  try {
148
+ const stat = lstatSync(path);
149
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.size > 8 * 1024 * 1024)
150
+ return corruption("state", path, `${basename(path)} cannot be safely inspected`);
148
151
  JSON.parse(readFileSync(path, "utf8"));
149
152
  return { source: "state", ownerPids: [] };
150
153
  } catch {
@@ -293,24 +296,24 @@ export function classifyTelegramRuntimeRecovery(
293
296
  }
294
297
 
295
298
  /**
296
- * Quarantine classifier-approved disposable corruption under two guards.
299
+ * Delete classifier-approved disposable corruption under two guards.
297
300
  *
298
301
  * A dedicated recovery transaction serializes recoverers. The ownership
299
302
  * transaction then prevents a new Telegram owner from appearing between the
300
- * final classification and mutation. Every artifact is renamed within its
301
- * filesystem; durable config and diagnostics never enter the candidate set.
303
+ * final classification and mutation. Damaged ownership debris and canonical
304
+ * state are deleted (operator policy: unfinished Restores in unreadable state
305
+ * are acceptable loss). Durable config and diagnostics never enter the set.
302
306
  */
303
307
  export function recoverTelegramRuntimeState(
304
308
  options: TelegramRuntimeRecoveryOptions,
305
309
  ): TelegramRuntimeRecoveryResult {
306
- const pid = options.pid ?? process.pid;
307
310
  const transactionPath =
308
311
  options.transactionPath ?? `${options.ownersPath}.transaction`;
309
312
  const recoveryTransactionPath =
310
313
  options.recoveryTransactionPath ??
311
314
  join(dirname(options.ownersPath), "runtime-recovery.transaction");
312
- const quarantineRoot =
313
- options.quarantineRoot ?? join(dirname(options.ownersPath), "recovery");
315
+ const removePath = options.removePath ?? ((path: string) =>
316
+ rmSync(path, { recursive: true, force: true, maxRetries: 3, retryDelay: 50 }));
314
317
  const classificationOptions = {
315
318
  ownersPath: options.ownersPath,
316
319
  statePaths: options.statePaths,
@@ -332,34 +335,19 @@ export function recoverTelegramRuntimeState(
332
335
  };
333
336
  }
334
337
 
335
- let quarantineDir: string | undefined;
338
+ const deletedPaths: string[] = [];
336
339
  const recoveredArtifacts: TelegramRuntimeCorruptArtifact[] = [];
337
- const ensureQuarantineDir = (): string => {
338
- if (quarantineDir) return quarantineDir;
339
- quarantineDir = join(
340
- quarantineRoot,
341
- `${options.getNowMs?.() ?? Date.now()}-${pid}-${randomUUID()}`,
342
- );
343
- mkdirSync(quarantineDir, { recursive: true, mode: 0o700 });
344
- return quarantineDir;
345
- };
346
- const quarantineArtifact = (
340
+ const deleteArtifact = (
347
341
  artifact: TelegramRuntimeCorruptArtifact,
348
342
  ): void => {
349
343
  if (!existsSync(artifact.path)) return;
350
- const destination = join(ensureQuarantineDir(), basename(artifact.path));
351
- if (
352
- renameTelegramPathWithRetry(artifact.path, destination, {
353
- rename: options.quarantineRename,
354
- retryDelayMs: options.quarantineRenameRetryDelayMs,
355
- })
356
- ) {
357
- recoveredArtifacts.push(artifact);
358
- }
344
+ removePath(artifact.path);
345
+ deletedPaths.push(artifact.path);
346
+ recoveredArtifacts.push(artifact);
359
347
  };
360
348
 
361
349
  for (const artifact of initial.artifacts) {
362
- if (artifact.kind === "transaction") quarantineArtifact(artifact);
350
+ if (artifact.kind === "transaction") deleteArtifact(artifact);
363
351
  }
364
352
 
365
353
  return withTelegramFileTransaction(
@@ -373,19 +361,19 @@ export function recoverTelegramRuntimeState(
373
361
  return {
374
362
  kind: "blocked-live-owner",
375
363
  livePids: current.livePids,
376
- quarantineDir,
364
+ ...(deletedPaths.length > 0 ? { deletedPaths } : {}),
377
365
  };
378
366
  }
379
367
  if (current.kind === "recoverable-corruption") {
380
368
  for (const artifact of current.artifacts) {
381
- if (artifact.kind !== "transaction") quarantineArtifact(artifact);
369
+ if (artifact.kind !== "transaction") deleteArtifact(artifact);
382
370
  }
383
371
  }
384
- return recoveredArtifacts.length > 0 && quarantineDir
372
+ return recoveredArtifacts.length > 0
385
373
  ? {
386
374
  kind: "recovered",
387
375
  artifacts: recoveredArtifacts,
388
- quarantineDir,
376
+ deletedPaths,
389
377
  }
390
378
  : { kind: "not-needed" };
391
379
  },
@@ -396,6 +384,81 @@ export function recoverTelegramRuntimeState(
396
384
  );
397
385
  }
398
386
 
387
+ /**
388
+ * Remove recovery folders written by earlier releases (runtime root and session folders).
389
+ * Current releases delete damaged files instead of quarantining them; nothing reads these copies.
390
+ */
391
+ export function removeTelegramLegacyRecoveryStorage(runtimeDir: string): string[] {
392
+ const removed: string[] = [];
393
+ const remove = (path: string): void => {
394
+ try {
395
+ if (!lstatSync(path).isDirectory()) return;
396
+ rmSync(path, { recursive: true, force: true, maxRetries: 3, retryDelay: 50 });
397
+ removed.push(path);
398
+ } catch {
399
+ // Best-effort housekeeping; a later startup retries.
400
+ }
401
+ };
402
+ remove(join(runtimeDir, "recovery"));
403
+ let sessions: string[] = [];
404
+ try { sessions = readdirSync(join(runtimeDir, "sessions")); } catch { /* no sessions yet */ }
405
+ for (const session of sessions) remove(join(runtimeDir, "sessions", session, "recovery"));
406
+ return removed;
407
+ }
408
+
409
+ const TELEGRAM_SESSION_SWEEP_INTERVAL_MS = 10 * 60 * 1000;
410
+
411
+ export interface TelegramSessionFolderSweeperDeps {
412
+ getSessionsDir: () => string;
413
+ getProfileName: () => string | undefined;
414
+ /** Sessions holding a Workspace slot binding, live registrations and this process's own session. */
415
+ getKeptSessionIds: () => Iterable<string | undefined>;
416
+ getNowMs?: () => number;
417
+ intervalMs?: number;
418
+ }
419
+
420
+ /**
421
+ * Leader housekeeping (operator policy): a session without a Workspace slot loses its
422
+ * current-profile journal family; the folder disappears once no profile uses it.
423
+ */
424
+ export function createTelegramSessionFolderSweeper(deps: TelegramSessionFolderSweeperDeps): { sweep: () => string[] } {
425
+ const getNowMs = deps.getNowMs ?? Date.now;
426
+ const intervalMs = deps.intervalMs ?? TELEGRAM_SESSION_SWEEP_INTERVAL_MS;
427
+ let lastSweepAtMs: number | undefined;
428
+ return {
429
+ sweep() {
430
+ const now = getNowMs();
431
+ if (lastSweepAtMs !== undefined && now - lastSweepAtMs < intervalMs) return [];
432
+ lastSweepAtMs = now;
433
+ const sessionsDir = deps.getSessionsDir();
434
+ const kept = new Set(deps.getKeptSessionIds());
435
+ const suffix = getTelegramProfilePathSuffix(deps.getProfileName()).replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
436
+ const family = new RegExp(`^journal\\.[a-f0-9]{16}${suffix}\\.json(?:\\.segments|\\.retained)?$`, "u");
437
+ const removed: string[] = [];
438
+ let names: string[];
439
+ try { names = readdirSync(sessionsDir); } catch { return removed; }
440
+ for (const name of names) {
441
+ const sessionId = decodeTelegramSessionDirectoryName(name);
442
+ if (sessionId === undefined || kept.has(sessionId)) continue;
443
+ const folder = join(sessionsDir, name);
444
+ try {
445
+ if (!lstatSync(folder).isDirectory()) continue;
446
+ for (const entry of readdirSync(folder)) {
447
+ if (!family.test(entry)) continue;
448
+ const path = join(folder, entry);
449
+ rmSync(path, { recursive: true, force: true, maxRetries: 3, retryDelay: 50 });
450
+ removed.push(path);
451
+ }
452
+ if (readdirSync(folder).length === 0) { rmdirSync(folder); removed.push(folder); }
453
+ } catch {
454
+ // Best-effort housekeeping; the next sweep retries.
455
+ }
456
+ }
457
+ return removed;
458
+ },
459
+ };
460
+ }
461
+
399
462
  /** Build the `/telegram-connect` recovery boundary around runtime artifacts. */
400
463
  export function createTelegramPollingStartRecoveryHandler(
401
464
  deps: TelegramPollingStartRecoveryHandlerDeps,
@@ -435,10 +498,7 @@ export function createTelegramPollingStartRecoveryHandler(
435
498
  });
436
499
  }
437
500
  try {
438
- const recovery = recoverTelegramRuntimeState({
439
- ownersPath,
440
- statePaths,
441
- });
501
+ const recovery = recoverTelegramRuntimeState({ ownersPath, statePaths });
442
502
  if (recovery.kind === "blocked-live-owner") {
443
503
  return {
444
504
  kind: "blocked",
@@ -750,7 +750,7 @@ export async function sendTelegramNativeMarkdownReply<TReplyMarkup = unknown>(
750
750
  return lastMessageId;
751
751
  }
752
752
 
753
- export async function sendTelegramNativeRichMessage(
753
+ async function sendTelegramNativeRichMessage(
754
754
  chatId: number,
755
755
  richMessage: TelegramInputRichMessage,
756
756
  deps: {