@llblab/pi-kit 0.29.1 → 0.30.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 (222) hide show
  1. package/BACKLOG.md +3 -1
  2. package/CHANGELOG.md +10 -0
  3. package/README.md +4 -4
  4. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +12 -0
  5. package/node_modules/@llblab/pi-state-flow/README.md +2 -2
  6. package/node_modules/@llblab/pi-state-flow/dist/lib/context.d.ts +2 -2
  7. package/node_modules/@llblab/pi-state-flow/dist/lib/context.js +17 -8
  8. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +7 -3
  9. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.d.ts +2 -1
  10. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +3 -2
  11. package/node_modules/@llblab/pi-state-flow/dist/package.json +1 -1
  12. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +3 -3
  13. package/node_modules/@llblab/pi-state-flow/docs/usage.md +3 -2
  14. package/node_modules/@llblab/pi-state-flow/lib/context.ts +17 -8
  15. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +6 -3
  16. package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +4 -2
  17. package/node_modules/@llblab/pi-state-flow/package.json +1 -1
  18. package/node_modules/@llblab/pi-telegram/AGENTS.md +13 -10
  19. package/node_modules/@llblab/pi-telegram/BACKLOG.md +1 -1
  20. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +23 -0
  21. package/node_modules/@llblab/pi-telegram/README.md +10 -11
  22. package/node_modules/@llblab/pi-telegram/api/commands.ts +3 -0
  23. package/node_modules/@llblab/pi-telegram/dist/api/commands.d.ts +1 -1
  24. package/node_modules/@llblab/pi-telegram/dist/lib/activity-verbosity.d.ts +1 -5
  25. package/node_modules/@llblab/pi-telegram/dist/lib/activity-verbosity.js +15 -42
  26. package/node_modules/@llblab/pi-telegram/dist/lib/activity.d.ts +9 -0
  27. package/node_modules/@llblab/pi-telegram/dist/lib/activity.js +39 -10
  28. package/node_modules/@llblab/pi-telegram/dist/lib/agent-messages.js +7 -3
  29. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +43 -5
  30. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +164 -20
  31. package/node_modules/@llblab/pi-telegram/dist/lib/bus-api.d.ts +12 -0
  32. package/node_modules/@llblab/pi-telegram/dist/lib/bus-api.js +181 -77
  33. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.d.ts +111 -31
  34. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +1324 -359
  35. package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.d.ts +41 -91
  36. package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +969 -637
  37. package/node_modules/@llblab/pi-telegram/dist/lib/bus-transport.js +16 -9
  38. package/node_modules/@llblab/pi-telegram/dist/lib/bus.d.ts +215 -18
  39. package/node_modules/@llblab/pi-telegram/dist/lib/bus.js +1048 -327
  40. package/node_modules/@llblab/pi-telegram/dist/lib/channel-posts.d.ts +5 -0
  41. package/node_modules/@llblab/pi-telegram/dist/lib/channel-posts.js +434 -261
  42. package/node_modules/@llblab/pi-telegram/dist/lib/command-templates.d.ts +26 -5
  43. package/node_modules/@llblab/pi-telegram/dist/lib/command-templates.js +92 -176
  44. package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +168 -82
  45. package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +1409 -291
  46. package/node_modules/@llblab/pi-telegram/dist/lib/config.d.ts +1 -8
  47. package/node_modules/@llblab/pi-telegram/dist/lib/config.js +34 -76
  48. package/node_modules/@llblab/pi-telegram/dist/lib/delivery.d.ts +2 -2
  49. package/node_modules/@llblab/pi-telegram/dist/lib/delivery.js +16 -16
  50. package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +642 -170
  51. package/node_modules/@llblab/pi-telegram/dist/lib/generative-app-worker.mjs +19 -7
  52. package/node_modules/@llblab/pi-telegram/dist/lib/generative-apps.d.ts +0 -5
  53. package/node_modules/@llblab/pi-telegram/dist/lib/generative-apps.js +61 -49
  54. package/node_modules/@llblab/pi-telegram/dist/lib/inbound.d.ts +5 -0
  55. package/node_modules/@llblab/pi-telegram/dist/lib/inbound.js +35 -59
  56. package/node_modules/@llblab/pi-telegram/dist/lib/journal.d.ts +40 -4
  57. package/node_modules/@llblab/pi-telegram/dist/lib/journal.js +2102 -760
  58. package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.d.ts +2 -6
  59. package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.js +75 -58
  60. package/node_modules/@llblab/pi-telegram/dist/lib/locks.d.ts +34 -3
  61. package/node_modules/@llblab/pi-telegram/dist/lib/locks.js +318 -119
  62. package/node_modules/@llblab/pi-telegram/dist/lib/logging.js +50 -14
  63. package/node_modules/@llblab/pi-telegram/dist/lib/media.d.ts +22 -1
  64. package/node_modules/@llblab/pi-telegram/dist/lib/media.js +46 -28
  65. package/node_modules/@llblab/pi-telegram/dist/lib/menu-model.d.ts +19 -5
  66. package/node_modules/@llblab/pi-telegram/dist/lib/menu-model.js +53 -11
  67. package/node_modules/@llblab/pi-telegram/dist/lib/menu-queue.d.ts +5 -4
  68. package/node_modules/@llblab/pi-telegram/dist/lib/menu-queue.js +45 -63
  69. package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.d.ts +5 -4
  70. package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.js +114 -78
  71. package/node_modules/@llblab/pi-telegram/dist/lib/menu-status.d.ts +1 -1
  72. package/node_modules/@llblab/pi-telegram/dist/lib/menu-status.js +8 -25
  73. package/node_modules/@llblab/pi-telegram/dist/lib/menu-thinking.d.ts +6 -1
  74. package/node_modules/@llblab/pi-telegram/dist/lib/menu-thinking.js +14 -21
  75. package/node_modules/@llblab/pi-telegram/dist/lib/menu.d.ts +8 -61
  76. package/node_modules/@llblab/pi-telegram/dist/lib/menu.js +92 -164
  77. package/node_modules/@llblab/pi-telegram/dist/lib/model.d.ts +1 -1
  78. package/node_modules/@llblab/pi-telegram/dist/lib/model.js +2 -16
  79. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.d.ts +6 -27
  80. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.js +45 -38
  81. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-buttons.js +22 -16
  82. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.d.ts +0 -2
  83. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.js +51 -64
  84. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-voice.js +6 -3
  85. package/node_modules/@llblab/pi-telegram/dist/lib/outbound.d.ts +6 -6
  86. package/node_modules/@llblab/pi-telegram/dist/lib/outbound.js +61 -51
  87. package/node_modules/@llblab/pi-telegram/dist/lib/ownership.js +6 -2
  88. package/node_modules/@llblab/pi-telegram/dist/lib/paths.d.ts +5 -0
  89. package/node_modules/@llblab/pi-telegram/dist/lib/paths.js +37 -21
  90. package/node_modules/@llblab/pi-telegram/dist/lib/pi.d.ts +2 -0
  91. package/node_modules/@llblab/pi-telegram/dist/lib/pi.js +8 -1
  92. package/node_modules/@llblab/pi-telegram/dist/lib/polling.d.ts +23 -37
  93. package/node_modules/@llblab/pi-telegram/dist/lib/polling.js +71 -52
  94. package/node_modules/@llblab/pi-telegram/dist/lib/preview.js +39 -19
  95. package/node_modules/@llblab/pi-telegram/dist/lib/process-identity.d.ts +24 -0
  96. package/node_modules/@llblab/pi-telegram/dist/lib/process-identity.js +133 -0
  97. package/node_modules/@llblab/pi-telegram/dist/lib/prompts.js +2 -7
  98. package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +16 -16
  99. package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +261 -116
  100. package/node_modules/@llblab/pi-telegram/dist/lib/recovery.d.ts +2 -83
  101. package/node_modules/@llblab/pi-telegram/dist/lib/recovery.js +20 -274
  102. package/node_modules/@llblab/pi-telegram/dist/lib/replies.d.ts +16 -24
  103. package/node_modules/@llblab/pi-telegram/dist/lib/replies.js +99 -116
  104. package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +121 -7
  105. package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +4750 -1161
  106. package/node_modules/@llblab/pi-telegram/dist/lib/runtime.d.ts +1 -11
  107. package/node_modules/@llblab/pi-telegram/dist/lib/sections.d.ts +4 -29
  108. package/node_modules/@llblab/pi-telegram/dist/lib/sections.js +7 -20
  109. package/node_modules/@llblab/pi-telegram/dist/lib/skills.js +4 -3
  110. package/node_modules/@llblab/pi-telegram/dist/lib/status.js +86 -57
  111. package/node_modules/@llblab/pi-telegram/dist/lib/sync.d.ts +6 -3
  112. package/node_modules/@llblab/pi-telegram/dist/lib/sync.js +112 -45
  113. package/node_modules/@llblab/pi-telegram/dist/lib/target.d.ts +4 -0
  114. package/node_modules/@llblab/pi-telegram/dist/lib/target.js +18 -0
  115. package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +18 -11
  116. package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +165 -49
  117. package/node_modules/@llblab/pi-telegram/dist/lib/text-groups.js +4 -39
  118. package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.d.ts +6 -27
  119. package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.js +369 -186
  120. package/node_modules/@llblab/pi-telegram/dist/lib/thread-display.d.ts +1 -2
  121. package/node_modules/@llblab/pi-telegram/dist/lib/thread-display.js +58 -38
  122. package/node_modules/@llblab/pi-telegram/dist/lib/thread-naming.d.ts +28 -8
  123. package/node_modules/@llblab/pi-telegram/dist/lib/thread-naming.js +175 -66
  124. package/node_modules/@llblab/pi-telegram/dist/lib/thread-reconciler.d.ts +42 -16
  125. package/node_modules/@llblab/pi-telegram/dist/lib/thread-reconciler.js +90 -50
  126. package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +88 -24
  127. package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +2276 -842
  128. package/node_modules/@llblab/pi-telegram/dist/lib/time-injection.js +1 -2
  129. package/node_modules/@llblab/pi-telegram/dist/lib/turns.js +1 -19
  130. package/node_modules/@llblab/pi-telegram/dist/lib/updates.d.ts +178 -82
  131. package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +2438 -645
  132. package/node_modules/@llblab/pi-telegram/dist/lib/voice.d.ts +7 -22
  133. package/node_modules/@llblab/pi-telegram/dist/lib/voice.js +7 -22
  134. package/node_modules/@llblab/pi-telegram/dist/lib/wire.js +1 -1
  135. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.d.ts +3 -1
  136. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.js +425 -211
  137. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-identity.js +8 -13
  138. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.d.ts +1 -1
  139. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.js +328 -140
  140. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-slots.js +8 -4
  141. package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
  142. package/node_modules/@llblab/pi-telegram/docs/architecture.md +43 -35
  143. package/node_modules/@llblab/pi-telegram/docs/inbound.md +1 -1
  144. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +15 -3
  145. package/node_modules/@llblab/pi-telegram/docs/outbound.md +1 -1
  146. package/node_modules/@llblab/pi-telegram/docs/public-api.md +29 -1
  147. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +25 -8
  148. package/node_modules/@llblab/pi-telegram/lib/activity-verbosity.ts +59 -82
  149. package/node_modules/@llblab/pi-telegram/lib/activity.ts +146 -102
  150. package/node_modules/@llblab/pi-telegram/lib/agent-messages.ts +12 -5
  151. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +390 -82
  152. package/node_modules/@llblab/pi-telegram/lib/bus-api.ts +262 -99
  153. package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +2440 -601
  154. package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +2209 -1290
  155. package/node_modules/@llblab/pi-telegram/lib/bus-transport.ts +46 -12
  156. package/node_modules/@llblab/pi-telegram/lib/bus.ts +1673 -444
  157. package/node_modules/@llblab/pi-telegram/lib/channel-posts.ts +848 -327
  158. package/node_modules/@llblab/pi-telegram/lib/command-templates.ts +152 -265
  159. package/node_modules/@llblab/pi-telegram/lib/commands.ts +2398 -555
  160. package/node_modules/@llblab/pi-telegram/lib/config.ts +177 -170
  161. package/node_modules/@llblab/pi-telegram/lib/delivery.ts +41 -29
  162. package/node_modules/@llblab/pi-telegram/lib/extension.ts +1209 -431
  163. package/node_modules/@llblab/pi-telegram/lib/generative-app-worker.mjs +19 -7
  164. package/node_modules/@llblab/pi-telegram/lib/generative-apps.ts +324 -211
  165. package/node_modules/@llblab/pi-telegram/lib/inbound.ts +95 -97
  166. package/node_modules/@llblab/pi-telegram/lib/journal.ts +5329 -1712
  167. package/node_modules/@llblab/pi-telegram/lib/keyboard.ts +1 -3
  168. package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +128 -78
  169. package/node_modules/@llblab/pi-telegram/lib/locks.ts +703 -212
  170. package/node_modules/@llblab/pi-telegram/lib/logging.ts +147 -58
  171. package/node_modules/@llblab/pi-telegram/lib/media.ts +82 -43
  172. package/node_modules/@llblab/pi-telegram/lib/menu-model.ts +85 -18
  173. package/node_modules/@llblab/pi-telegram/lib/menu-queue.ts +88 -129
  174. package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +260 -155
  175. package/node_modules/@llblab/pi-telegram/lib/menu-status.ts +20 -60
  176. package/node_modules/@llblab/pi-telegram/lib/menu-thinking.ts +41 -56
  177. package/node_modules/@llblab/pi-telegram/lib/menu.ts +112 -285
  178. package/node_modules/@llblab/pi-telegram/lib/model.ts +8 -22
  179. package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +156 -125
  180. package/node_modules/@llblab/pi-telegram/lib/outbound-buttons.ts +76 -49
  181. package/node_modules/@llblab/pi-telegram/lib/outbound-markup.ts +96 -94
  182. package/node_modules/@llblab/pi-telegram/lib/outbound-voice.ts +33 -27
  183. package/node_modules/@llblab/pi-telegram/lib/outbound.ts +125 -125
  184. package/node_modules/@llblab/pi-telegram/lib/ownership.ts +19 -10
  185. package/node_modules/@llblab/pi-telegram/lib/paths.ts +148 -49
  186. package/node_modules/@llblab/pi-telegram/lib/pi.ts +35 -15
  187. package/node_modules/@llblab/pi-telegram/lib/polling.ts +165 -149
  188. package/node_modules/@llblab/pi-telegram/lib/preview.ts +93 -40
  189. package/node_modules/@llblab/pi-telegram/lib/process-identity.ts +164 -0
  190. package/node_modules/@llblab/pi-telegram/lib/prompts.ts +4 -11
  191. package/node_modules/@llblab/pi-telegram/lib/queue.ts +523 -289
  192. package/node_modules/@llblab/pi-telegram/lib/recovery.ts +50 -454
  193. package/node_modules/@llblab/pi-telegram/lib/rendering.ts +1 -3
  194. package/node_modules/@llblab/pi-telegram/lib/replies.ts +248 -209
  195. package/node_modules/@llblab/pi-telegram/lib/routing.ts +8527 -2149
  196. package/node_modules/@llblab/pi-telegram/lib/runtime.ts +1 -12
  197. package/node_modules/@llblab/pi-telegram/lib/sections.ts +24 -89
  198. package/node_modules/@llblab/pi-telegram/lib/setup.ts +1 -3
  199. package/node_modules/@llblab/pi-telegram/lib/skills.ts +9 -4
  200. package/node_modules/@llblab/pi-telegram/lib/status.ts +215 -110
  201. package/node_modules/@llblab/pi-telegram/lib/sync.ts +488 -359
  202. package/node_modules/@llblab/pi-telegram/lib/target.ts +22 -3
  203. package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +322 -122
  204. package/node_modules/@llblab/pi-telegram/lib/text-groups.ts +5 -39
  205. package/node_modules/@llblab/pi-telegram/lib/thread-cleanup-manager.ts +833 -331
  206. package/node_modules/@llblab/pi-telegram/lib/thread-display.ts +213 -97
  207. package/node_modules/@llblab/pi-telegram/lib/thread-naming.ts +232 -87
  208. package/node_modules/@llblab/pi-telegram/lib/thread-reconciler.ts +155 -72
  209. package/node_modules/@llblab/pi-telegram/lib/threads.ts +5329 -1817
  210. package/node_modules/@llblab/pi-telegram/lib/time-injection.ts +1 -4
  211. package/node_modules/@llblab/pi-telegram/lib/turns.ts +15 -28
  212. package/node_modules/@llblab/pi-telegram/lib/updates.ts +5194 -1600
  213. package/node_modules/@llblab/pi-telegram/lib/voice.ts +7 -35
  214. package/node_modules/@llblab/pi-telegram/lib/wire.ts +1 -1
  215. package/node_modules/@llblab/pi-telegram/lib/workspace-admission.ts +996 -416
  216. package/node_modules/@llblab/pi-telegram/lib/workspace-identity.ts +14 -17
  217. package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +878 -376
  218. package/node_modules/@llblab/pi-telegram/lib/workspace-slots.ts +22 -13
  219. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  220. package/package.json +3 -3
  221. package/node_modules/@llblab/pi-telegram/scripts/check-downgrade.mjs +0 -472
  222. package/node_modules/@llblab/pi-telegram/scripts/measure-workspace.mjs +0 -108
@@ -6,14 +6,14 @@
6
6
  * It does not own polling, update execution, queue admission, or follower routing.
7
7
  */
8
8
  import { createHash, randomBytes, randomUUID } from "node:crypto";
9
- import { chmodSync, constants, closeSync, fstatSync, lstatSync, openSync, opendirSync, readSync, realpathSync, mkdirSync, readFileSync, readdirSync, rmdirSync, rmSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
10
- import { basename, dirname, join, resolve, relative, isAbsolute, sep } from "node:path";
9
+ import { chmodSync, closeSync, fstatSync, lstatSync, mkdirSync, opendirSync, openSync, readdirSync, readFileSync, readSync, realpathSync, rmdirSync, rmSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
10
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep, } from "node:path";
11
11
  import { isDeepStrictEqual } from "node:util";
12
- import { renameTelegramPathWithRetry, withTelegramFileTransaction, } from "./locks.js";
13
- import { getTelegramProcessLiveness, } from "./bus.js";
12
+ import { TELEGRAM_STRICT_READ_FLAGS, renameTelegramPathWithRetry, withTelegramFileTransaction, } from "./locks.js";
14
13
  import { decodeTelegramSessionDirectoryName, getTelegramProfilePathSuffix, TELEGRAM_DEFAULT_PROFILE_NAME, } from "./paths.js";
15
- import { runWithTelegramWorkspaceAdmissions, } from "./workspace-admission.js";
16
- import { isWireRecord as isRecord, hasOnlyWireKeys as hasOnlyKeys, isNonNegativeWireInteger as isSafeNonNegativeInteger, isNonEmptyWireString as isNonEmptyString, } from "./wire.js";
14
+ import { getTelegramProcessLiveness, } from "./process-identity.js";
15
+ import { hasOnlyWireKeys as hasOnlyKeys, isNonEmptyWireString as isNonEmptyString, isWireRecord as isRecord, isNonNegativeWireInteger as isSafeNonNegativeInteger, } from "./wire.js";
16
+ import { getTelegramWorkspaceAdmissionScopeKey, runWithTelegramWorkspaceAdmissions, } from "./workspace-admission.js";
17
17
  export const TELEGRAM_UPDATE_JOURNAL_VERSION = 1;
18
18
  export const TELEGRAM_UPDATE_JOURNAL_EXCLUSION_VERSION = 2;
19
19
  export const TELEGRAM_UPDATE_JOURNAL_CUSTODY_VERSION = 3;
@@ -105,13 +105,17 @@ export function discoverTelegramSessionJournalPaths(input) {
105
105
  return { paths: [], complete: false };
106
106
  }
107
107
  catch (error) {
108
- return { paths: [], complete: error.code === "ENOENT" };
108
+ return {
109
+ paths: [],
110
+ complete: error.code === "ENOENT",
111
+ };
109
112
  }
110
113
  try {
111
114
  for (const session of scan(root)) {
112
115
  if (!complete)
113
116
  break;
114
- if (!session.isDirectory() || decodeTelegramSessionDirectoryName(session.name) === undefined) {
117
+ if (!session.isDirectory() ||
118
+ decodeTelegramSessionDirectoryName(session.name) === undefined) {
115
119
  complete = false;
116
120
  break;
117
121
  }
@@ -125,7 +129,8 @@ export function discoverTelegramSessionJournalPaths(input) {
125
129
  if (!complete)
126
130
  break;
127
131
  // The leader polling journal and legacy recovery folders are not recipient custody.
128
- if (entry.name === "recovery" || /^inbox(?:\.[a-zA-Z0-9._-]+)?\.json(?:\.segments|\.retained)?$/u.test(entry.name))
132
+ if (entry.name === "recovery" ||
133
+ /^inbox(?:\.[a-zA-Z0-9._-]+)?\.json(?:\.segments|\.retained)?$/u.test(entry.name))
129
134
  continue;
130
135
  const match = /^journal\.([a-f0-9]{16})(\.[a-zA-Z0-9._-]+)?\.json(\.segments|\.retained)?$/u.exec(entry.name);
131
136
  if (!match) {
@@ -133,7 +138,8 @@ export function discoverTelegramSessionJournalPaths(input) {
133
138
  break;
134
139
  }
135
140
  const stat = lstatSync(join(directory, entry.name));
136
- if (stat.isSymbolicLink() || (match[3] ? !stat.isDirectory() : !stat.isFile())) {
141
+ if (stat.isSymbolicLink() ||
142
+ (match[3] ? !stat.isDirectory() : !stat.isFile())) {
137
143
  complete = false;
138
144
  break;
139
145
  }
@@ -152,8 +158,10 @@ export function discoverTelegramSessionJournalPaths(input) {
152
158
  export function discoverTelegramRecipientJournalPaths(input) {
153
159
  const legacy = discoverTelegramFollowerJournalPaths(input);
154
160
  const sessions = discoverTelegramSessionJournalPaths(input);
155
- return { paths: Array.from(new Set([...legacy.paths, ...sessions.paths])).sort(),
156
- complete: legacy.complete && sessions.complete };
161
+ return {
162
+ paths: Array.from(new Set([...legacy.paths, ...sessions.paths])).sort(),
163
+ complete: legacy.complete && sessions.complete,
164
+ };
157
165
  }
158
166
  const TELEGRAM_UPDATE_JOURNAL_TERMINAL_REASON_MAX_LENGTH = 256;
159
167
  export const TELEGRAM_UPDATE_JOURNAL_COMPACTION_SEGMENT_COUNT = 256;
@@ -212,18 +220,11 @@ function getUpdateAdmissionScope(update) {
212
220
  if (payloadKey === "callback_query") {
213
221
  const query = update.callback_query;
214
222
  return isRecord(query)
215
- ? getUpdateCarrierScope(query.message) ?? { kind: "profile" }
223
+ ? (getUpdateCarrierScope(query.message) ?? { kind: "profile" })
216
224
  : { kind: "profile" };
217
225
  }
218
226
  return { kind: "profile" };
219
227
  }
220
- function getWorkspaceAdmissionScopeKey(scope) {
221
- if (scope.kind === "profile")
222
- return "profile";
223
- if (scope.kind === "chat")
224
- return `chat:${scope.chatId}`;
225
- return `target:${scope.target.chatId}:${scope.target.threadId}`;
226
- }
227
228
  export function getTelegramUpdateJournalAdmissionScopes(updates) {
228
229
  if (updates.length === 0)
229
230
  return [{ kind: "profile" }];
@@ -238,52 +239,148 @@ export function getTelegramUpdateJournalAdmissionScopes(updates) {
238
239
  for (const scope of scopes) {
239
240
  if (scope.kind === "target" && chatIds.has(scope.target.chatId))
240
241
  continue;
241
- unique.set(getWorkspaceAdmissionScopeKey(scope), scope);
242
+ unique.set(getTelegramWorkspaceAdmissionScopeKey(scope), scope);
242
243
  }
243
244
  return Array.from(unique.entries())
244
245
  .sort(([left], [right]) => left.localeCompare(right))
245
246
  .map(([, scope]) => scope);
246
247
  }
247
248
  function listTelegramUpdateJournalLegacyCustodyCandidates(snapshot) {
248
- return snapshot.entries.flatMap(entry => {
249
+ return snapshot.entries
250
+ .flatMap((entry) => {
249
251
  const evidence = createTelegramUpdateJournalLegacyCustodyEvidence(entry);
250
- return evidence ? [{ updateId: evidence.updateId, state: evidence.state,
251
- attemptCount: evidence.attemptCount, failureClass: evidence.failureClass,
252
- evidenceSha256: evidence.evidenceSha256 }] : [];
253
- }).sort((left, right) => left.updateId - right.updateId);
252
+ return evidence
253
+ ? [
254
+ {
255
+ updateId: evidence.updateId,
256
+ state: evidence.state,
257
+ attemptCount: evidence.attemptCount,
258
+ failureClass: evidence.failureClass,
259
+ evidenceSha256: evidence.evidenceSha256,
260
+ },
261
+ ]
262
+ : [];
263
+ })
264
+ .sort((left, right) => left.updateId - right.updateId);
254
265
  }
255
266
  export function createTelegramUpdateJournalLegacyCustodyEvidence(entry) {
256
- if ((entry.state !== "retry-wait" && entry.state !== "failed") || !entry.failure ||
257
- entry.inputClaim || entry.inputProvenance)
267
+ if ((entry.state !== "retry-wait" && entry.state !== "failed") ||
268
+ !entry.failure ||
269
+ entry.inputClaim ||
270
+ entry.inputProvenance)
258
271
  return undefined;
259
- const base = { updateId: entry.updateId, state: entry.state,
260
- attemptCount: entry.failure.attemptCount, failedAtMs: entry.failure.failedAtMs,
261
- failureClass: entry.failure.failureClass, summary: entry.failure.summary,
262
- ...(entry.nextRetryAtMs === undefined ? {} : { nextRetryAtMs: entry.nextRetryAtMs }),
263
- ...(entry.terminalAtMs === undefined ? {} : { terminalAtMs: entry.terminalAtMs }),
264
- ...(entry.terminalReason === undefined ? {} : { terminalReason: entry.terminalReason }),
265
- ...(entry.terminalFailureId === undefined ? {} : { terminalFailureId: entry.terminalFailureId }) };
266
- return { ...base, evidenceSha256: createHash("sha256").update(JSON.stringify(base)).digest("hex") };
272
+ const base = {
273
+ updateId: entry.updateId,
274
+ state: entry.state,
275
+ attemptCount: entry.failure.attemptCount,
276
+ failedAtMs: entry.failure.failedAtMs,
277
+ failureClass: entry.failure.failureClass,
278
+ summary: entry.failure.summary,
279
+ ...(entry.nextRetryAtMs === undefined
280
+ ? {}
281
+ : { nextRetryAtMs: entry.nextRetryAtMs }),
282
+ ...(entry.terminalAtMs === undefined
283
+ ? {}
284
+ : { terminalAtMs: entry.terminalAtMs }),
285
+ ...(entry.terminalReason === undefined
286
+ ? {}
287
+ : { terminalReason: entry.terminalReason }),
288
+ ...(entry.terminalFailureId === undefined
289
+ ? {}
290
+ : { terminalFailureId: entry.terminalFailureId }),
291
+ };
292
+ return {
293
+ ...base,
294
+ evidenceSha256: createHash("sha256")
295
+ .update(JSON.stringify(base))
296
+ .digest("hex"),
297
+ };
267
298
  }
268
299
  export function normalizeTelegramUpdateJournalLegacyCustodyDispositionAuthority(value, expected) {
269
300
  if (!isRecord(value))
270
301
  return undefined;
271
- const keys = ["version", "dispositionId", "updateId", "evidenceSha256", "action",
272
- "operatorAuthorityId", "authorizedAtMs"];
273
- if (Object.keys(value).some(key => !keys.includes(key)) || value.version !== 1 ||
302
+ const keys = [
303
+ "version",
304
+ "dispositionId",
305
+ "updateId",
306
+ "evidenceSha256",
307
+ "action",
308
+ "operatorAuthorityId",
309
+ "authorizedAtMs",
310
+ ];
311
+ if (Object.keys(value).some((key) => !keys.includes(key)) ||
312
+ value.version !== 1 ||
274
313
  !isBoundedString(value.dispositionId, TELEGRAM_UPDATE_JOURNAL_FAILURE_ID_MAX_LENGTH) ||
275
- value.updateId !== expected.updateId || value.evidenceSha256 !== expected.evidenceSha256 ||
314
+ value.updateId !== expected.updateId ||
315
+ value.evidenceSha256 !== expected.evidenceSha256 ||
276
316
  (value.action !== "requeue-v3" && value.action !== "discard") ||
277
317
  !isBoundedString(value.operatorAuthorityId, TELEGRAM_UPDATE_JOURNAL_FAILURE_ID_MAX_LENGTH) ||
278
318
  !isSafeNonNegativeInteger(value.authorizedAtMs))
279
319
  return undefined;
280
- return { version: 1, dispositionId: value.dispositionId, updateId: expected.updateId,
281
- evidenceSha256: expected.evidenceSha256, action: value.action,
282
- operatorAuthorityId: value.operatorAuthorityId, authorizedAtMs: value.authorizedAtMs };
320
+ return {
321
+ version: 1,
322
+ dispositionId: value.dispositionId,
323
+ updateId: expected.updateId,
324
+ evidenceSha256: expected.evidenceSha256,
325
+ action: value.action,
326
+ operatorAuthorityId: value.operatorAuthorityId,
327
+ authorizedAtMs: value.authorizedAtMs,
328
+ };
283
329
  }
284
330
  export const TELEGRAM_ROUTING_INPUT_TTL_MS = 60 * 60 * 1000;
331
+ function isTelegramUpdateJournalRoutingChooser(value) {
332
+ return (isRecord(value) &&
333
+ hasOnlyKeys(value, ["chatId", "threadId", "messageId"]) &&
334
+ Number.isSafeInteger(value.chatId) &&
335
+ (value.threadId === undefined || isSafePositiveInteger(value.threadId)) &&
336
+ isSafePositiveInteger(value.messageId));
337
+ }
285
338
  export function createTelegramUpdateJournalEntryDigest(entry) {
286
- return { updateId: entry.updateId, sourceSha256: createHash("sha256").update(JSON.stringify(entry)).digest("hex") };
339
+ return {
340
+ updateId: entry.updateId,
341
+ sourceSha256: createHash("sha256")
342
+ .update(JSON.stringify(entry))
343
+ .digest("hex"),
344
+ };
345
+ }
346
+ function inspectRoutingInputExpiry(file, journalBindingKey, updateId) {
347
+ const disposition = file.operatorDispositions?.find((value) => value.updateId === updateId &&
348
+ "dispositionKind" in value &&
349
+ value.dispositionKind === "legacy-custody" &&
350
+ value.action === "discard" &&
351
+ value.failureId === `routing-expiry:${value.evidenceSha256}`);
352
+ if (!disposition ||
353
+ !("operatorAuthorityId" in disposition) ||
354
+ file.entries.some((entry) => entry.updateId === updateId))
355
+ return undefined;
356
+ return {
357
+ journalBindingKey,
358
+ updateId,
359
+ operatorAuthorityId: disposition.operatorAuthorityId,
360
+ };
361
+ }
362
+ function inspectRoutingGroupExpiry(file, journalBindingKey, updateIds) {
363
+ if (!Array.isArray(updateIds) ||
364
+ !updateIds.length ||
365
+ updateIds.length > TELEGRAM_UPDATE_JOURNAL_MAX_ENTRIES ||
366
+ updateIds.some((id, index) => !isSafeNonNegativeInteger(id) ||
367
+ (index > 0 && id <= updateIds[index - 1])))
368
+ return undefined;
369
+ if (file.entries.some((entry) => updateIds.includes(entry.updateId)))
370
+ return undefined;
371
+ const expired = updateIds
372
+ .map((id) => inspectRoutingInputExpiry(file, journalBindingKey, id))
373
+ .filter((value) => value !== undefined);
374
+ if (!expired.length ||
375
+ expired.some((value) => value.operatorAuthorityId !== expired[0].operatorAuthorityId))
376
+ return undefined;
377
+ // Some members may have been acknowledged earlier. Whole donor absence plus one exact expiry ends this known cohort,
378
+ // not recipient custody; fresh Thread protection still independently gates deletion.
379
+ return updateIds.map((updateId) => ({
380
+ journalBindingKey,
381
+ updateId,
382
+ operatorAuthorityId: expired[0].operatorAuthorityId,
383
+ }));
287
384
  }
288
385
  function createJournalError(code, path, detail, cause) {
289
386
  return new TelegramUpdateJournalError(code, path, `Telegram update journal ${detail}: ${path}`, cause === undefined ? undefined : { cause });
@@ -418,11 +515,11 @@ function createTelegramUpdateQueueHandoffId(input) {
418
515
  .slice(0, 32)}`;
419
516
  }
420
517
  function isTelegramInputHandoffId(value) {
421
- return isBoundedString(value, TELEGRAM_UPDATE_JOURNAL_QUEUE_HANDOFF_ID_MAX_LENGTH) &&
422
- /^input-handoff-[a-f0-9]{32}$/u.test(value);
518
+ return (isBoundedString(value, TELEGRAM_UPDATE_JOURNAL_QUEUE_HANDOFF_ID_MAX_LENGTH) && /^input-handoff-[a-f0-9]{32}$/u.test(value));
423
519
  }
424
520
  function createTelegramInputHandoffId(input) {
425
- return `input-handoff-${createHash("sha256").update(JSON.stringify({
521
+ return `input-handoff-${createHash("sha256")
522
+ .update(JSON.stringify({
426
523
  version: 1,
427
524
  token: input.handoffToken,
428
525
  source: input.journalBindingKey,
@@ -430,7 +527,9 @@ function createTelegramInputHandoffId(input) {
430
527
  donorOwner: input.donorOwner,
431
528
  recipientOwner: input.recipientOwner,
432
529
  recipientBindingKey: input.recipientBindingKey,
433
- })).digest("hex").slice(0, 32)}`;
530
+ }))
531
+ .digest("hex")
532
+ .slice(0, 32)}`;
434
533
  }
435
534
  function validateJournalHandoff(value, path, kind) {
436
535
  if (!isRecord(value) ||
@@ -480,38 +579,72 @@ function createTelegramUpdateTerminalFailureId(input) {
480
579
  }
481
580
  function validateJournalOperatorDisposition(value, path) {
482
581
  if (isRecord(value) && value.dispositionKind === "legacy-custody") {
483
- if (!hasOnlyKeys(value, ["dispositionKind", "failureId", "updateId", "action",
484
- "committedAtMs", "evidenceSha256", "operatorAuthorityId", "authorizedAtMs"]) ||
582
+ if (!hasOnlyKeys(value, [
583
+ "dispositionKind",
584
+ "failureId",
585
+ "updateId",
586
+ "action",
587
+ "committedAtMs",
588
+ "evidenceSha256",
589
+ "operatorAuthorityId",
590
+ "authorizedAtMs",
591
+ ]) ||
485
592
  !isBoundedString(value.failureId, TELEGRAM_UPDATE_JOURNAL_FAILURE_ID_MAX_LENGTH) ||
486
593
  !isSafeNonNegativeInteger(value.updateId) ||
487
594
  (value.action !== "requeue-v3" && value.action !== "discard") ||
488
595
  !isSafeNonNegativeInteger(value.committedAtMs) ||
489
- typeof value.evidenceSha256 !== "string" || !/^[a-f0-9]{64}$/u.test(value.evidenceSha256) ||
596
+ typeof value.evidenceSha256 !== "string" ||
597
+ !/^[a-f0-9]{64}$/u.test(value.evidenceSha256) ||
490
598
  !isBoundedString(value.operatorAuthorityId, TELEGRAM_UPDATE_JOURNAL_FAILURE_ID_MAX_LENGTH) ||
491
- !isSafeNonNegativeInteger(value.authorizedAtMs) || value.committedAtMs < value.authorizedAtMs)
599
+ !isSafeNonNegativeInteger(value.authorizedAtMs) ||
600
+ value.committedAtMs < value.authorizedAtMs)
492
601
  throw createJournalError("invalid", path, "contains invalid legacy custody disposition metadata");
493
- return { dispositionKind: "legacy-custody", failureId: value.failureId,
494
- updateId: value.updateId, action: value.action, committedAtMs: value.committedAtMs,
495
- evidenceSha256: value.evidenceSha256, operatorAuthorityId: value.operatorAuthorityId,
496
- authorizedAtMs: value.authorizedAtMs };
602
+ return {
603
+ dispositionKind: "legacy-custody",
604
+ failureId: value.failureId,
605
+ updateId: value.updateId,
606
+ action: value.action,
607
+ committedAtMs: value.committedAtMs,
608
+ evidenceSha256: value.evidenceSha256,
609
+ operatorAuthorityId: value.operatorAuthorityId,
610
+ authorizedAtMs: value.authorizedAtMs,
611
+ };
497
612
  }
498
- if (!isRecord(value) || !hasOnlyKeys(value, ["failureId", "updateId", "action",
499
- "committedAtMs", "attemptCount", "failureClass", "terminalAtMs", "terminalReason"]) ||
613
+ if (!isRecord(value) ||
614
+ !hasOnlyKeys(value, [
615
+ "failureId",
616
+ "updateId",
617
+ "action",
618
+ "committedAtMs",
619
+ "attemptCount",
620
+ "failureClass",
621
+ "terminalAtMs",
622
+ "terminalReason",
623
+ ]) ||
500
624
  !isBoundedString(value.failureId, TELEGRAM_UPDATE_JOURNAL_FAILURE_ID_MAX_LENGTH) ||
501
625
  !isSafeNonNegativeInteger(value.updateId) ||
502
626
  (value.action !== "retry" && value.action !== "discard") ||
503
- !isSafeNonNegativeInteger(value.committedAtMs) || !isSafePositiveInteger(value.attemptCount) ||
627
+ !isSafeNonNegativeInteger(value.committedAtMs) ||
628
+ !isSafePositiveInteger(value.attemptCount) ||
504
629
  !isBoundedString(value.failureClass, TELEGRAM_UPDATE_JOURNAL_FAILURE_CLASS_MAX_LENGTH) ||
505
- !isSafeNonNegativeInteger(value.terminalAtMs) || value.committedAtMs < value.terminalAtMs ||
630
+ !isSafeNonNegativeInteger(value.terminalAtMs) ||
631
+ value.committedAtMs < value.terminalAtMs ||
506
632
  !isBoundedString(value.terminalReason, TELEGRAM_UPDATE_JOURNAL_TERMINAL_REASON_MAX_LENGTH))
507
633
  throw createJournalError("invalid", path, "contains invalid operator disposition metadata");
508
- return { failureId: value.failureId, updateId: value.updateId, action: value.action,
509
- committedAtMs: value.committedAtMs, attemptCount: value.attemptCount,
510
- failureClass: value.failureClass, terminalAtMs: value.terminalAtMs,
511
- terminalReason: value.terminalReason };
634
+ return {
635
+ failureId: value.failureId,
636
+ updateId: value.updateId,
637
+ action: value.action,
638
+ committedAtMs: value.committedAtMs,
639
+ attemptCount: value.attemptCount,
640
+ failureClass: value.failureClass,
641
+ terminalAtMs: value.terminalAtMs,
642
+ terminalReason: value.terminalReason,
643
+ };
512
644
  }
513
645
  function journalRequiresExclusion(version) {
514
- return version === TELEGRAM_UPDATE_JOURNAL_EXCLUSION_VERSION || journalHasCustodyFields(version);
646
+ return (version === TELEGRAM_UPDATE_JOURNAL_EXCLUSION_VERSION ||
647
+ journalHasCustodyFields(version));
515
648
  }
516
649
  function journalHasCustodyFields(version) {
517
650
  return version === TELEGRAM_UPDATE_JOURNAL_CUSTODY_VERSION;
@@ -522,21 +655,33 @@ export function isTelegramUpdateJournalLegacyFamilyVersion(version) {
522
655
  }
523
656
  function validateJournalInputClaim(value, path, updateId) {
524
657
  if (!isRecord(value) ||
525
- !hasOnlyKeys(value, ["phase", "owner", "recipientBindingKey", "handoff", "executionUpdate"]) ||
658
+ !hasOnlyKeys(value, [
659
+ "phase",
660
+ "owner",
661
+ "recipientBindingKey",
662
+ "handoff",
663
+ "executionUpdate",
664
+ ]) ||
526
665
  (value.phase !== "ready" && value.phase !== "running") ||
527
666
  !isBoundedString(value.recipientBindingKey, TELEGRAM_UPDATE_JOURNAL_INPUT_BINDING_MAX_LENGTH) ||
528
667
  !value.recipientBindingKey.trim()) {
529
668
  throw createJournalError("invalid", path, "contains invalid input claim metadata");
530
669
  }
531
670
  const owner = validateJournalQueueOwner(value.owner, path);
532
- const handoff = value.handoff === undefined ? undefined : validateJournalHandoff(value.handoff, path, "input");
533
- if ((owner.handoffId !== undefined && !isTelegramInputHandoffId(owner.handoffId)) ||
534
- (handoff && (!isTelegramInputHandoffId(handoff.handoffId) || value.phase !== "ready" ||
535
- isTelegramUpdateJournalQueueOwnerProcess(owner, handoff.recipientOwner)))) {
671
+ const handoff = value.handoff === undefined
672
+ ? undefined
673
+ : validateJournalHandoff(value.handoff, path, "input");
674
+ if ((owner.handoffId !== undefined &&
675
+ !isTelegramInputHandoffId(owner.handoffId)) ||
676
+ (handoff &&
677
+ (!isTelegramInputHandoffId(handoff.handoffId) ||
678
+ value.phase !== "ready" ||
679
+ isTelegramUpdateJournalQueueOwnerProcess(owner, handoff.recipientOwner)))) {
536
680
  throw createJournalError("invalid", path, "contains conflicting input handoff metadata");
537
681
  }
538
682
  const executionUpdate = value.executionUpdate === undefined
539
- ? undefined : validateJournaledUpdate(value.executionUpdate, path);
683
+ ? undefined
684
+ : validateJournaledUpdate(value.executionUpdate, path);
540
685
  if (executionUpdate && executionUpdate.update_id !== updateId) {
541
686
  throw createJournalError("invalid", path, "contains an input claim/update id mismatch");
542
687
  }
@@ -556,20 +701,26 @@ function validateJournalInputProvenance(value, path, updateId) {
556
701
  throw createJournalError("invalid", path, "contains invalid input queue provenance");
557
702
  }
558
703
  const owner = validateJournalQueueOwner(value.owner, path);
559
- if (owner.handoffId !== undefined && !isTelegramInputHandoffId(owner.handoffId)) {
704
+ if (owner.handoffId !== undefined &&
705
+ !isTelegramInputHandoffId(owner.handoffId)) {
560
706
  throw createJournalError("invalid", path, "contains invalid input queue provenance");
561
707
  }
562
708
  const executionUpdate = value.executionUpdate === undefined
563
- ? undefined : validateJournaledUpdate(value.executionUpdate, path);
709
+ ? undefined
710
+ : validateJournaledUpdate(value.executionUpdate, path);
564
711
  if (executionUpdate && executionUpdate.update_id !== updateId) {
565
712
  throw createJournalError("invalid", path, "contains an input provenance/update id mismatch");
566
713
  }
567
- return { owner, recipientBindingKey: value.recipientBindingKey,
568
- ...(executionUpdate ? { executionUpdate } : {}) };
714
+ return {
715
+ owner,
716
+ recipientBindingKey: value.recipientBindingKey,
717
+ ...(executionUpdate ? { executionUpdate } : {}),
718
+ };
569
719
  }
570
720
  function validateJournalEntry(value, path, version) {
571
721
  if (journalRequiresExclusion(version) &&
572
- (!isRecord(value) || typeof value.preApprovalExcluded !== "boolean" ||
722
+ (!isRecord(value) ||
723
+ typeof value.preApprovalExcluded !== "boolean" ||
573
724
  (value.preApprovalExcluded && value.state === "queued"))) {
574
725
  throw createJournalError("pairing-evidence", path, "contains missing, malformed, or queued exclusion evidence");
575
726
  }
@@ -578,7 +729,9 @@ function validateJournalEntry(value, path, version) {
578
729
  "updateId",
579
730
  "update",
580
731
  "admittedAtMs",
581
- ...(isTelegramUpdateJournalLegacyFamilyVersion(version) ? ["routingInput"] : []),
732
+ ...(isTelegramUpdateJournalLegacyFamilyVersion(version)
733
+ ? ["routingInput"]
734
+ : []),
582
735
  "state",
583
736
  "queueKind",
584
737
  "queueReceiptId",
@@ -591,7 +744,8 @@ function validateJournalEntry(value, path, version) {
591
744
  "terminalFailureId",
592
745
  ...(journalRequiresExclusion(version) ? ["preApprovalExcluded"] : []),
593
746
  ...(journalHasCustodyFields(version)
594
- ? ["inputClaim", "inputProvenance"] : []),
747
+ ? ["inputClaim", "inputProvenance"]
748
+ : []),
595
749
  ]) ||
596
750
  !isSafeNonNegativeInteger(value.updateId) ||
597
751
  !isSafeNonNegativeInteger(value.admittedAtMs) ||
@@ -608,16 +762,39 @@ function validateJournalEntry(value, path, version) {
608
762
  let routingInput;
609
763
  if (value.routingInput !== undefined) {
610
764
  const routing = value.routingInput;
611
- if (!isRecord(routing) || !hasOnlyKeys(routing, ["operatorUserId", "publishedAtMs", "expiresAtMs", "phase"]) ||
612
- !isSafePositiveInteger(routing.operatorUserId) || !isSafeNonNegativeInteger(routing.publishedAtMs) ||
613
- routing.publishedAtMs < value.admittedAtMs || !isSafeNonNegativeInteger(routing.expiresAtMs) ||
614
- routing.expiresAtMs !== routing.publishedAtMs + TELEGRAM_ROUTING_INPUT_TTL_MS ||
765
+ if (!isRecord(routing) ||
766
+ !hasOnlyKeys(routing, [
767
+ "operatorUserId",
768
+ "publishedAtMs",
769
+ "expiresAtMs",
770
+ "phase",
771
+ "chooser",
772
+ ]) ||
773
+ (routing.chooser !== undefined &&
774
+ !isTelegramUpdateJournalRoutingChooser(routing.chooser)) ||
775
+ !isSafePositiveInteger(routing.operatorUserId) ||
776
+ !isSafeNonNegativeInteger(routing.publishedAtMs) ||
777
+ routing.publishedAtMs < value.admittedAtMs ||
778
+ !isSafeNonNegativeInteger(routing.expiresAtMs) ||
779
+ routing.expiresAtMs !==
780
+ routing.publishedAtMs + TELEGRAM_ROUTING_INPUT_TTL_MS ||
615
781
  (routing.phase !== "waiting" && routing.phase !== "selected") ||
616
782
  (routing.phase === "waiting" && value.state === "queued")) {
617
783
  throw createJournalError("invalid", path, "contains invalid routing input lifetime");
618
784
  }
619
- routingInput = { operatorUserId: routing.operatorUserId, publishedAtMs: routing.publishedAtMs,
620
- expiresAtMs: routing.expiresAtMs, phase: routing.phase };
785
+ routingInput = {
786
+ operatorUserId: routing.operatorUserId,
787
+ publishedAtMs: routing.publishedAtMs,
788
+ expiresAtMs: routing.expiresAtMs,
789
+ phase: routing.phase,
790
+ ...(routing.chooser !== undefined
791
+ ? {
792
+ chooser: {
793
+ ...routing.chooser,
794
+ },
795
+ }
796
+ : {}),
797
+ };
621
798
  }
622
799
  const queueKind = value.queueKind;
623
800
  const queueReceiptId = value.queueReceiptId;
@@ -654,11 +831,14 @@ function validateJournalEntry(value, path, version) {
654
831
  throw createJournalError("pairing-evidence", path, "contains claimed exclusion evidence");
655
832
  }
656
833
  const inputClaim = value.inputClaim === undefined
657
- ? undefined : validateJournalInputClaim(value.inputClaim, path, value.updateId);
834
+ ? undefined
835
+ : validateJournalInputClaim(value.inputClaim, path, value.updateId);
658
836
  const inputProvenance = value.inputProvenance === undefined
659
- ? undefined : validateJournalInputProvenance(value.inputProvenance, path, value.updateId);
660
- if ((inputClaim && (value.state === "queued" ||
661
- (inputClaim.phase === "running" && value.state !== "pending"))) ||
837
+ ? undefined
838
+ : validateJournalInputProvenance(value.inputProvenance, path, value.updateId);
839
+ if ((inputClaim &&
840
+ (value.state === "queued" ||
841
+ (inputClaim.phase === "running" && value.state !== "pending"))) ||
662
842
  (inputProvenance && (value.state !== "queued" || inputClaim))) {
663
843
  throw createJournalError("invalid", path, "contains conflicting input claim state");
664
844
  }
@@ -690,15 +870,14 @@ function validateJournalEntry(value, path, version) {
690
870
  updateId: value.updateId,
691
871
  update,
692
872
  ...(journalRequiresExclusion(version)
693
- ? { preApprovalExcluded: value.preApprovalExcluded } : {}),
873
+ ? { preApprovalExcluded: value.preApprovalExcluded }
874
+ : {}),
694
875
  ...(inputClaim ? { inputClaim } : {}),
695
876
  ...(inputProvenance ? { inputProvenance } : {}),
696
877
  admittedAtMs: value.admittedAtMs,
697
878
  ...(routingInput ? { routingInput } : {}),
698
879
  state: value.state,
699
- ...(queueKind === "prompt" || queueKind === "control"
700
- ? { queueKind }
701
- : {}),
880
+ ...(queueKind === "prompt" || queueKind === "control" ? { queueKind } : {}),
702
881
  ...(isNonEmptyString(queueReceiptId) ? { queueReceiptId } : {}),
703
882
  ...(queueOwner ? { queueOwner } : {}),
704
883
  ...(queueHandoff ? { queueHandoff } : {}),
@@ -729,40 +908,56 @@ function validateJournalEntry(value, path, version) {
729
908
  };
730
909
  }
731
910
  function assertSupportedJournalVersion(value, path, version = TELEGRAM_UPDATE_JOURNAL_VERSION) {
732
- if (isRecord(value) && Number.isSafeInteger(value.version) &&
911
+ if (isRecord(value) &&
912
+ Number.isSafeInteger(value.version) &&
733
913
  value.version !== version) {
734
914
  throw createJournalError("unsupported-version", path, `uses unsupported version ${String(value.version)}`);
735
915
  }
736
916
  }
737
917
  function validateJournalSourceCompletion(value, path) {
738
- if (!isRecord(value) || !hasOnlyKeys(value, ["updateId", "sourceSha256", "completionSha256"]) ||
739
- !isSafeNonNegativeInteger(value.updateId) || typeof value.sourceSha256 !== "string" || !/^[a-f0-9]{64}$/u.test(value.sourceSha256) ||
740
- typeof value.completionSha256 !== "string" || !/^[a-f0-9]{64}$/u.test(value.completionSha256)) {
918
+ if (!isRecord(value) ||
919
+ !hasOnlyKeys(value, ["updateId", "sourceSha256", "completionSha256"]) ||
920
+ !isSafeNonNegativeInteger(value.updateId) ||
921
+ typeof value.sourceSha256 !== "string" ||
922
+ !/^[a-f0-9]{64}$/u.test(value.sourceSha256) ||
923
+ typeof value.completionSha256 !== "string" ||
924
+ !/^[a-f0-9]{64}$/u.test(value.completionSha256)) {
741
925
  throw createJournalError("invalid", path, "has invalid source completion evidence");
742
926
  }
743
- return { updateId: value.updateId, sourceSha256: value.sourceSha256, completionSha256: value.completionSha256 };
927
+ return {
928
+ updateId: value.updateId,
929
+ sourceSha256: value.sourceSha256,
930
+ completionSha256: value.completionSha256,
931
+ };
744
932
  }
745
933
  function validateJournalSourceCompletions(value, path, version, allowEmpty = false) {
746
- if (!isTelegramUpdateJournalLegacyFamilyVersion(version) || !Array.isArray(value) || !allowEmpty && !value.length) {
934
+ if (!isTelegramUpdateJournalLegacyFamilyVersion(version) ||
935
+ !Array.isArray(value) ||
936
+ (!allowEmpty && !value.length)) {
747
937
  throw createJournalError("invalid", path, "has invalid source completion collection");
748
938
  }
749
- const completions = value.map(item => validateJournalSourceCompletion(item, path));
939
+ const completions = value.map((item) => validateJournalSourceCompletion(item, path));
750
940
  const scopes = new Set();
751
941
  for (let index = 0; index < completions.length; index++) {
752
942
  const completion = completions[index];
753
- if ((index > 0 && completions[index - 1].updateId >= completion.updateId) || scopes.has(completion.completionSha256)) {
943
+ if ((index > 0 && completions[index - 1].updateId >= completion.updateId) ||
944
+ scopes.has(completion.completionSha256)) {
754
945
  throw createJournalError("invalid", path, "has duplicate or unordered source completion evidence");
755
946
  }
756
947
  scopes.add(completion.completionSha256);
757
948
  }
758
949
  return completions;
759
950
  }
760
- function parseJournalFile(value, path, version = TELEGRAM_UPDATE_JOURNAL_VERSION) {
761
- assertSupportedJournalVersion(value, path, version);
951
+ function assertJournalExclusionCursor(value, path, version) {
762
952
  if (journalRequiresExclusion(version) &&
763
- (!isRecord(value) || !isSafeNonNegativeInteger(value.acceptedThroughUpdateId))) {
953
+ (!isRecord(value) ||
954
+ !isSafeNonNegativeInteger(value.acceptedThroughUpdateId))) {
764
955
  throw createJournalError("pairing-evidence", path, "is missing its exclusion-mode admission cursor");
765
956
  }
957
+ }
958
+ function parseJournalFile(value, path, version = TELEGRAM_UPDATE_JOURNAL_VERSION) {
959
+ assertSupportedJournalVersion(value, path, version);
960
+ assertJournalExclusionCursor(value, path, version);
766
961
  if (!isRecord(value) ||
767
962
  !hasOnlyKeys(value, [
768
963
  "version",
@@ -798,9 +993,7 @@ function parseJournalFile(value, path, version = TELEGRAM_UPDATE_JOURNAL_VERSION
798
993
  }
799
994
  const queuedReceipts = new Map();
800
995
  for (const entry of entries) {
801
- if (entry.state !== "queued" ||
802
- !entry.queueKind ||
803
- !entry.queueReceiptId) {
996
+ if (entry.state !== "queued" || !entry.queueKind || !entry.queueReceiptId) {
804
997
  continue;
805
998
  }
806
999
  const existing = queuedReceipts.get(entry.queueReceiptId);
@@ -831,12 +1024,18 @@ function parseJournalFile(value, path, version = TELEGRAM_UPDATE_JOURNAL_VERSION
831
1024
  }
832
1025
  }
833
1026
  const operatorDispositions = (value.operatorDispositions ?? []).map((disposition) => validateJournalOperatorDisposition(disposition, path));
834
- const sourceCompletions = value.sourceCompletions === undefined ? [] : validateJournalSourceCompletions(value.sourceCompletions, path, version);
1027
+ const sourceCompletions = value.sourceCompletions === undefined
1028
+ ? []
1029
+ : validateJournalSourceCompletions(value.sourceCompletions, path, version);
835
1030
  const botIdentity = validateBotIdentity(value.botIdentity, path);
836
- const activeIds = new Set(entries.map(entry => entry.updateId));
837
- const discardedIds = new Set(operatorDispositions.filter(disposition => disposition.action === "discard").map(disposition => disposition.updateId));
838
- if (sourceCompletions.some(completion => activeIds.has(completion.updateId) || discardedIds.has(completion.updateId) ||
839
- value.acceptedThroughUpdateId !== undefined && completion.updateId > value.acceptedThroughUpdateId)) {
1031
+ const activeIds = new Set(entries.map((entry) => entry.updateId));
1032
+ const discardedIds = new Set(operatorDispositions
1033
+ .filter((disposition) => disposition.action === "discard")
1034
+ .map((disposition) => disposition.updateId));
1035
+ if (sourceCompletions.some((completion) => activeIds.has(completion.updateId) ||
1036
+ discardedIds.has(completion.updateId) ||
1037
+ (value.acceptedThroughUpdateId !== undefined &&
1038
+ completion.updateId > value.acceptedThroughUpdateId))) {
840
1039
  throw createJournalError("invalid", path, "has contradictory source completion evidence");
841
1040
  }
842
1041
  const dispositionFailureIds = new Set();
@@ -868,11 +1067,15 @@ function parseJournalFile(value, path, version = TELEGRAM_UPDATE_JOURNAL_VERSION
868
1067
  };
869
1068
  }
870
1069
  function assertJournalRoutingInputContinuity(file, entries, path) {
871
- const previous = new Map(file.entries.map(entry => [entry.updateId, entry.routingInput]));
1070
+ const previous = new Map(file.entries.map((entry) => [entry.updateId, entry.routingInput]));
872
1071
  for (const entry of entries) {
873
1072
  const retained = previous.get(entry.updateId), next = entry.routingInput;
874
- if (retained && (!next || next.operatorUserId !== retained.operatorUserId || next.publishedAtMs !== retained.publishedAtMs ||
875
- next.expiresAtMs !== retained.expiresAtMs || (retained.phase === "selected" && next.phase !== "selected"))) {
1073
+ if (retained &&
1074
+ (!next ||
1075
+ next.operatorUserId !== retained.operatorUserId ||
1076
+ next.publishedAtMs !== retained.publishedAtMs ||
1077
+ next.expiresAtMs !== retained.expiresAtMs ||
1078
+ (retained.phase === "selected" && next.phase !== "selected"))) {
876
1079
  throw createJournalError("invalid", path, "regresses retained routing input lifetime");
877
1080
  }
878
1081
  }
@@ -881,14 +1084,20 @@ function assertJournalSourceCompletionContinuity(file, segment, path) {
881
1084
  assertJournalRoutingInputContinuity(file, segment.upsertedEntries, path);
882
1085
  if (segment.sourceCompletions === undefined)
883
1086
  return;
884
- const previous = new Map((file.sourceCompletions ?? []).map(completion => [completion.updateId, completion]));
885
- const next = new Map(segment.sourceCompletions.map(completion => [completion.updateId, completion]));
1087
+ const previous = new Map((file.sourceCompletions ?? []).map((completion) => [
1088
+ completion.updateId,
1089
+ completion,
1090
+ ]));
1091
+ const next = new Map(segment.sourceCompletions.map((completion) => [
1092
+ completion.updateId,
1093
+ completion,
1094
+ ]));
886
1095
  if ([...previous].some(([id, completion]) => !isDeepStrictEqual(completion, next.get(id)))) {
887
1096
  throw createJournalError("invalid", path, "regresses retained source completion evidence");
888
1097
  }
889
- const entries = new Map(file.entries.map(entry => [entry.updateId, entry]));
890
- const removed = new Set(segment.removedUpdateIds), upserted = new Set(segment.upsertedEntries.map(entry => entry.updateId));
891
- const upsertedReceipts = new Set(segment.upsertedEntries.map(entry => entry.queueReceiptId));
1098
+ const entries = new Map(file.entries.map((entry) => [entry.updateId, entry]));
1099
+ const removed = new Set(segment.removedUpdateIds), upserted = new Set(segment.upsertedEntries.map((entry) => entry.updateId));
1100
+ const upsertedReceipts = new Set(segment.upsertedEntries.map((entry) => entry.queueReceiptId));
892
1101
  // Grouped markers share one whole-receipt check rather than rescanning the group per source.
893
1102
  const queueGroups = new Map(), queueRemovals = new Map();
894
1103
  for (const entry of file.entries) {
@@ -905,8 +1114,15 @@ function assertJournalSourceCompletionContinuity(file, segment, path) {
905
1114
  if (queueRemovals.has(receiptId))
906
1115
  return queueRemovals.get(receiptId);
907
1116
  const group = queueGroups.get(receiptId);
908
- const valid = !!group?.length && !upsertedReceipts.has(receiptId) && group.every(entry => removed.has(entry.updateId) && !upserted.has(entry.updateId) && entry.state === "queued" && entry.queueKind === source.queueKind &&
909
- entry.queueHandoff === undefined && !!entry.queueOwner && areTelegramUpdateJournalQueueOwnersEqual(entry.queueOwner, source.queueOwner));
1117
+ const valid = !!group?.length &&
1118
+ !upsertedReceipts.has(receiptId) &&
1119
+ group.every((entry) => removed.has(entry.updateId) &&
1120
+ !upserted.has(entry.updateId) &&
1121
+ entry.state === "queued" &&
1122
+ entry.queueKind === source.queueKind &&
1123
+ entry.queueHandoff === undefined &&
1124
+ !!entry.queueOwner &&
1125
+ areTelegramUpdateJournalQueueOwnersEqual(entry.queueOwner, source.queueOwner));
910
1126
  queueRemovals.set(receiptId, valid);
911
1127
  return valid;
912
1128
  };
@@ -914,18 +1130,19 @@ function assertJournalSourceCompletionContinuity(file, segment, path) {
914
1130
  if (previous.has(completion.updateId))
915
1131
  continue;
916
1132
  const source = entries.get(completion.updateId);
917
- if (!removed.has(completion.updateId) || !source || (source.state === "queued" && !queueWasRemoved(source)) || source.state === "failed" ||
918
- createTelegramUpdateJournalEntryDigest(source).sourceSha256 !== completion.sourceSha256) {
1133
+ if (!removed.has(completion.updateId) ||
1134
+ !source ||
1135
+ (source.state === "queued" && !queueWasRemoved(source)) ||
1136
+ source.state === "failed" ||
1137
+ createTelegramUpdateJournalEntryDigest(source).sourceSha256 !==
1138
+ completion.sourceSha256) {
919
1139
  throw createJournalError("invalid", path, "source completion does not match its removal revision");
920
1140
  }
921
1141
  }
922
1142
  }
923
1143
  function parseJournalSegment(value, path, version = TELEGRAM_UPDATE_JOURNAL_VERSION) {
924
1144
  assertSupportedJournalVersion(value, path, version);
925
- if (journalRequiresExclusion(version) &&
926
- (!isRecord(value) || !isSafeNonNegativeInteger(value.acceptedThroughUpdateId))) {
927
- throw createJournalError("pairing-evidence", path, "is missing its exclusion-mode admission cursor");
928
- }
1145
+ assertJournalExclusionCursor(value, path, version);
929
1146
  if (!isRecord(value) ||
930
1147
  !hasOnlyKeys(value, [
931
1148
  "version",
@@ -992,7 +1209,11 @@ function parseJournalSegment(value, path, version = TELEGRAM_UPDATE_JOURNAL_VERS
992
1209
  ...(value.operatorDispositions !== undefined
993
1210
  ? { operatorDispositions }
994
1211
  : {}),
995
- ...(value.sourceCompletions !== undefined ? { sourceCompletions: validateJournalSourceCompletions(value.sourceCompletions, path, version, true) } : {}),
1212
+ ...(value.sourceCompletions !== undefined
1213
+ ? {
1214
+ sourceCompletions: validateJournalSourceCompletions(value.sourceCompletions, path, version, true),
1215
+ }
1216
+ : {}),
996
1217
  };
997
1218
  }
998
1219
  /**
@@ -1025,7 +1246,10 @@ export function inspectTelegramUpdateJournalFamily(input) {
1025
1246
  /** Classifies private originals against exact discard tombstones; never replay or deletion authority. */
1026
1247
  export function inspectTelegramUpdateJournalRetention(input) {
1027
1248
  const acquired = acquireTelegramUpdateJournalFamily(input, undefined, true);
1028
- return { evidence: acquired.evidence, retainedInputs: acquired.retainedInputs ?? [] };
1249
+ return {
1250
+ evidence: acquired.evidence,
1251
+ retainedInputs: acquired.retainedInputs ?? [],
1252
+ };
1029
1253
  }
1030
1254
  /**
1031
1255
  * Read-only source evidence for cooperating writers serialized by the caller through
@@ -1052,9 +1276,18 @@ function acquireTelegramUpdateJournalFamily(input, selectedVersion, inspectReten
1052
1276
  const { path, limits } = input;
1053
1277
  const acquiredSegments = [];
1054
1278
  let snapshotRevision = 0;
1055
- const fail = (message) => { throw createJournalError("invalid", path, message); };
1056
- const capacity = () => { throw createJournalError("capacity", path, "exceeds inspection resource limits"); };
1057
- for (const key of ["maxFiles", "maxBytes", "maxEntries", "maxWork"]) {
1279
+ const fail = (message) => {
1280
+ throw createJournalError("invalid", path, message);
1281
+ };
1282
+ const capacity = () => {
1283
+ throw createJournalError("capacity", path, "exceeds inspection resource limits");
1284
+ };
1285
+ for (const key of [
1286
+ "maxFiles",
1287
+ "maxBytes",
1288
+ "maxEntries",
1289
+ "maxWork",
1290
+ ]) {
1058
1291
  if (!isSafePositiveInteger(limits[key]))
1059
1292
  fail("requires positive safe-integer limits");
1060
1293
  }
@@ -1063,21 +1296,34 @@ function acquireTelegramUpdateJournalFamily(input, selectedVersion, inspectReten
1063
1296
  fail("requires an exact profile");
1064
1297
  const anchor = input.directory;
1065
1298
  const contained = relative(anchor, path);
1066
- if (!isAbsolute(anchor) || resolve(anchor) !== anchor || !isAbsolute(path) ||
1067
- resolve(path) !== path || !contained || contained === ".." ||
1068
- contained.startsWith(`..${sep}`) || isAbsolute(contained))
1299
+ if (!isAbsolute(anchor) ||
1300
+ resolve(anchor) !== anchor ||
1301
+ !isAbsolute(path) ||
1302
+ resolve(path) !== path ||
1303
+ !contained ||
1304
+ contained === ".." ||
1305
+ contained.startsWith(`..${sep}`) ||
1306
+ isAbsolute(contained))
1069
1307
  fail("escapes its canonical directory anchor");
1070
- if (!constants.O_NOFOLLOW || !constants.O_NONBLOCK)
1071
- fail("platform lacks no-follow nonblocking open evidence");
1072
1308
  let files = 0;
1073
1309
  let bytes = 0;
1074
1310
  let work = 0;
1075
1311
  const observed = new Map();
1076
1312
  const ancestors = new Set();
1077
- const sameAncestor = (a, b) => b.isDirectory() && !b.isSymbolicLink() && a.dev === b.dev && a.ino === b.ino &&
1078
- a.mode === b.mode && a.uid === b.uid && a.gid === b.gid;
1079
- const same = (a, b) => a.dev === b.dev && a.ino === b.ino && a.mode === b.mode &&
1080
- a.nlink === b.nlink && a.size === b.size && a.mtimeNs === b.mtimeNs && a.ctimeNs === b.ctimeNs;
1313
+ const sameAncestor = (a, b) => b.isDirectory() &&
1314
+ !b.isSymbolicLink() &&
1315
+ a.dev === b.dev &&
1316
+ a.ino === b.ino &&
1317
+ a.mode === b.mode &&
1318
+ a.uid === b.uid &&
1319
+ a.gid === b.gid;
1320
+ const same = (a, b) => a.dev === b.dev &&
1321
+ a.ino === b.ino &&
1322
+ a.mode === b.mode &&
1323
+ a.nlink === b.nlink &&
1324
+ a.size === b.size &&
1325
+ a.mtimeNs === b.mtimeNs &&
1326
+ a.ctimeNs === b.ctimeNs;
1081
1327
  const status = (target) => {
1082
1328
  try {
1083
1329
  return lstatSync(target, { bigint: true });
@@ -1092,7 +1338,9 @@ function acquireTelegramUpdateJournalFamily(input, selectedVersion, inspectReten
1092
1338
  const value = status(target);
1093
1339
  if (!value && !optional)
1094
1340
  fail("has a missing path component");
1095
- if (value && (value.isSymbolicLink() || !(directory ? value.isDirectory() : value.isFile())))
1341
+ if (value &&
1342
+ (value.isSymbolicLink() ||
1343
+ !(directory ? value.isDirectory() : value.isFile())))
1096
1344
  fail("has a linked or unexpected file type");
1097
1345
  observed.set(target, value);
1098
1346
  return value;
@@ -1100,11 +1348,18 @@ function acquireTelegramUpdateJournalFamily(input, selectedVersion, inspectReten
1100
1348
  const charge = (value) => {
1101
1349
  if (!isRecord(value))
1102
1350
  return;
1103
- for (const key of ["entries", "upsertedEntries", "removedUpdateIds", "operatorDispositions", "sourceCompletions"]) {
1351
+ for (const key of [
1352
+ "entries",
1353
+ "upsertedEntries",
1354
+ "removedUpdateIds",
1355
+ "operatorDispositions",
1356
+ "sourceCompletions",
1357
+ ]) {
1104
1358
  const collection = value[key];
1105
1359
  if (!Array.isArray(collection))
1106
1360
  continue;
1107
- if (collection.length > limits.maxEntries || collection.length > limits.maxWork - work)
1361
+ if (collection.length > limits.maxEntries ||
1362
+ collection.length > limits.maxWork - work)
1108
1363
  capacity();
1109
1364
  work += collection.length;
1110
1365
  }
@@ -1112,7 +1367,7 @@ function acquireTelegramUpdateJournalFamily(input, selectedVersion, inspectReten
1112
1367
  const read = (target, before) => {
1113
1368
  if (before.size > BigInt(limits.maxBytes - bytes))
1114
1369
  capacity();
1115
- const fd = openSync(target, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
1370
+ const fd = openSync(target, TELEGRAM_STRICT_READ_FLAGS);
1116
1371
  try {
1117
1372
  const opened = fstatSync(fd, { bigint: true });
1118
1373
  if (!opened.isFile() || !same(before, opened))
@@ -1152,7 +1407,9 @@ function acquireTelegramUpdateJournalFamily(input, selectedVersion, inspectReten
1152
1407
  break;
1153
1408
  if (names.length >= limits.maxFiles - usedFiles)
1154
1409
  capacity();
1155
- if (!pattern.test(entry.name) || !entry.isFile() || entry.isSymbolicLink())
1410
+ if (!pattern.test(entry.name) ||
1411
+ !entry.isFile() ||
1412
+ entry.isSymbolicLink())
1156
1413
  fail("has an unexpected journal evidence entry");
1157
1414
  names.push(entry.name);
1158
1415
  }
@@ -1168,7 +1425,9 @@ function acquireTelegramUpdateJournalFamily(input, selectedVersion, inspectReten
1168
1425
  if (realpathSync(anchor) !== anchor)
1169
1426
  fail("requires a canonical directory anchor");
1170
1427
  let parent = anchor;
1171
- for (const component of relative(anchor, dirname(path)).split(sep).filter(Boolean)) {
1428
+ for (const component of relative(anchor, dirname(path))
1429
+ .split(sep)
1430
+ .filter(Boolean)) {
1172
1431
  parent = join(parent, component);
1173
1432
  observe(parent, true);
1174
1433
  ancestors.add(parent);
@@ -1182,15 +1441,19 @@ function acquireTelegramUpdateJournalFamily(input, selectedVersion, inspectReten
1182
1441
  let file;
1183
1442
  let knownBotId = expected.botId;
1184
1443
  const identity = (value) => {
1185
- if (value.profile !== input.profile || value.botIdentity.tokenSha256 !== expected.tokenSha256 ||
1186
- (knownBotId !== undefined && value.botIdentity.botId !== undefined && knownBotId !== value.botIdentity.botId)) {
1444
+ if (value.profile !== input.profile ||
1445
+ value.botIdentity.tokenSha256 !== expected.tokenSha256 ||
1446
+ (knownBotId !== undefined &&
1447
+ value.botIdentity.botId !== undefined &&
1448
+ knownBotId !== value.botIdentity.botId)) {
1187
1449
  throw createJournalError("identity-mismatch", path, "belongs to another exact journal identity");
1188
1450
  }
1189
1451
  knownBotId ??= value.botIdentity.botId;
1190
1452
  };
1191
1453
  if (snapshot) {
1192
1454
  const raw = read(path, snapshot);
1193
- if (!isRecord(raw) || (raw.version !== 1 && raw.version !== 2 && raw.version !== 3)) {
1455
+ if (!isRecord(raw) ||
1456
+ (raw.version !== 1 && raw.version !== 2 && raw.version !== 3)) {
1194
1457
  throw createJournalError("unsupported-version", path, "has an unsupported snapshot version");
1195
1458
  }
1196
1459
  file = parseJournalFile(raw, path, raw.version);
@@ -1201,11 +1464,18 @@ function acquireTelegramUpdateJournalFamily(input, selectedVersion, inspectReten
1201
1464
  const metadata = observe(target, false);
1202
1465
  const previousWork = work;
1203
1466
  const segment = parseJournalSegment(read(target, metadata), target, file.version);
1204
- acquiredSegments.push({ name, bytes: Number(metadata.size), work: work - previousWork,
1205
- ...(segment.botIdentity.botId !== undefined ? { botId: segment.botIdentity.botId } : {}) });
1467
+ acquiredSegments.push({
1468
+ name,
1469
+ bytes: Number(metadata.size),
1470
+ work: work - previousWork,
1471
+ ...(segment.botIdentity.botId !== undefined
1472
+ ? { botId: segment.botIdentity.botId }
1473
+ : {}),
1474
+ });
1206
1475
  files += 1;
1207
1476
  identity(segment);
1208
- if (segment.revision !== Number(name.slice(0, 16)) || segment.previousRevision !== segment.revision - 1)
1477
+ if (segment.revision !== Number(name.slice(0, 16)) ||
1478
+ segment.previousRevision !== segment.revision - 1)
1209
1479
  fail("has an invalid intrinsic segment revision");
1210
1480
  const dispositionIds = new Set();
1211
1481
  for (const disposition of segment.operatorDispositions ?? []) {
@@ -1216,19 +1486,24 @@ function acquireTelegramUpdateJournalFamily(input, selectedVersion, inspectReten
1216
1486
  const revision = file.revision ?? 0;
1217
1487
  if (segment.revision <= revision)
1218
1488
  continue;
1219
- if (segment.revision !== revision + 1 || segment.previousRevision !== revision)
1489
+ if (segment.revision !== revision + 1 ||
1490
+ segment.previousRevision !== revision)
1220
1491
  fail("has a newer revision gap");
1221
- if (segment.acceptedThroughUpdateId !== undefined && file.acceptedThroughUpdateId !== undefined &&
1492
+ if (segment.acceptedThroughUpdateId !== undefined &&
1493
+ file.acceptedThroughUpdateId !== undefined &&
1222
1494
  segment.acceptedThroughUpdateId < file.acceptedThroughUpdateId)
1223
1495
  fail("regresses the admission cursor");
1224
1496
  assertJournalSourceCompletionContinuity(file, segment, target);
1225
- const entries = new Map(file.entries.map(entry => [entry.updateId, entry]));
1497
+ const entries = new Map(file.entries.map((entry) => [entry.updateId, entry]));
1226
1498
  for (const id of segment.removedUpdateIds)
1227
1499
  entries.delete(id);
1228
1500
  for (const entry of segment.upsertedEntries) {
1229
1501
  const previous = entries.get(entry.updateId);
1230
- if (journalRequiresExclusion(file.version) && ((previous && previous.preApprovalExcluded !== entry.preApprovalExcluded) ||
1231
- (!previous && entry.updateId <= (file.acceptedThroughUpdateId ?? -1)))) {
1502
+ if (journalRequiresExclusion(file.version) &&
1503
+ ((previous &&
1504
+ previous.preApprovalExcluded !== entry.preApprovalExcluded) ||
1505
+ (!previous &&
1506
+ entry.updateId <= (file.acceptedThroughUpdateId ?? -1)))) {
1232
1507
  throw createJournalError("pairing-evidence", target, "changes exclusion evidence or resurrects a settled source");
1233
1508
  }
1234
1509
  if (!entries.has(entry.updateId) && entries.size >= limits.maxEntries)
@@ -1238,7 +1513,9 @@ function acquireTelegramUpdateJournalFamily(input, selectedVersion, inspectReten
1238
1513
  const next = {
1239
1514
  ...file,
1240
1515
  revision: segment.revision,
1241
- ...(segment.acceptedThroughUpdateId !== undefined ? { acceptedThroughUpdateId: segment.acceptedThroughUpdateId } : {}),
1516
+ ...(segment.acceptedThroughUpdateId !== undefined
1517
+ ? { acceptedThroughUpdateId: segment.acceptedThroughUpdateId }
1518
+ : {}),
1242
1519
  entries: [...entries.values()].sort((a, b) => a.updateId - b.updateId),
1243
1520
  operatorDispositions: segment.operatorDispositions ?? file.operatorDispositions,
1244
1521
  sourceCompletions: segment.sourceCompletions ?? file.sourceCompletions,
@@ -1249,16 +1526,24 @@ function acquireTelegramUpdateJournalFamily(input, selectedVersion, inspectReten
1249
1526
  }
1250
1527
  const retainedInputs = [];
1251
1528
  const retentionDirectory = `${path}.retained`;
1252
- const retention = inspectRetention ? observe(retentionDirectory, true, true) : undefined;
1529
+ const retention = inspectRetention
1530
+ ? observe(retentionDirectory, true, true)
1531
+ : undefined;
1253
1532
  const retentionPattern = /^abandon-[a-f0-9]{64}\.json$/u;
1254
- const retentionNames = retention ? enumerate(retentionDirectory, retentionPattern) : [];
1533
+ const retentionNames = retention
1534
+ ? enumerate(retentionDirectory, retentionPattern)
1535
+ : [];
1255
1536
  if (retentionNames.length > limits.maxEntries)
1256
1537
  capacity();
1257
1538
  if (retention) {
1258
1539
  if (!file || file.version !== 1)
1259
1540
  fail("private retention requires its exact v1 journal snapshot");
1260
1541
  const current = file;
1261
- const bindingKeys = new Set([expected, current.botIdentity].map(botIdentity => createTelegramUpdateJournalBindingKey({ path, profileName: input.profile, botIdentity })));
1542
+ const bindingKeys = new Set([expected, current.botIdentity].map((botIdentity) => createTelegramUpdateJournalBindingKey({
1543
+ path,
1544
+ profileName: input.profile,
1545
+ botIdentity,
1546
+ })));
1262
1547
  const updateIds = new Set();
1263
1548
  for (const name of retentionNames) {
1264
1549
  const target = join(retentionDirectory, name);
@@ -1272,42 +1557,70 @@ function acquireTelegramUpdateJournalFamily(input, selectedVersion, inspectReten
1272
1557
  work += 2;
1273
1558
  if (!isRecord(raw))
1274
1559
  throw createJournalError("invalid", target, "private retention is not an evidence record");
1275
- if (typeof raw.journalBindingKey !== "string" || !bindingKeys.has(raw.journalBindingKey))
1560
+ if (typeof raw.journalBindingKey !== "string" ||
1561
+ !bindingKeys.has(raw.journalBindingKey))
1276
1562
  throw createJournalError("invalid", target, "has foreign private retention evidence");
1277
1563
  const entry = validateJournalEntry(raw.entry, target, 1);
1278
1564
  const disposition = validateJournalOperatorDisposition(raw.requestedDisposition, target);
1279
- const digest = createHash("sha256").update(JSON.stringify({ journalBindingKey: raw.journalBindingKey, entry })).digest("hex");
1280
- if (!("dispositionKind" in disposition) || disposition.dispositionKind !== "legacy-custody" ||
1281
- disposition.action !== "discard" || entry.state !== "pending" || disposition.updateId !== entry.updateId ||
1282
- disposition.evidenceSha256 !== digest || disposition.failureId !== `abandon-${digest}` ||
1283
- name !== `${disposition.failureId}.json` || !isDeepStrictEqual(raw, { version: 1, kind: "pending-input-retention",
1284
- journalBindingKey: raw.journalBindingKey, entry, requestedDisposition: disposition }))
1565
+ const digest = createHash("sha256")
1566
+ .update(JSON.stringify({ journalBindingKey: raw.journalBindingKey, entry }))
1567
+ .digest("hex");
1568
+ if (!("dispositionKind" in disposition) ||
1569
+ disposition.dispositionKind !== "legacy-custody" ||
1570
+ disposition.action !== "discard" ||
1571
+ entry.state !== "pending" ||
1572
+ disposition.updateId !== entry.updateId ||
1573
+ disposition.evidenceSha256 !== digest ||
1574
+ disposition.failureId !== `abandon-${digest}` ||
1575
+ name !== `${disposition.failureId}.json` ||
1576
+ !isDeepStrictEqual(raw, {
1577
+ version: 1,
1578
+ kind: "pending-input-retention",
1579
+ journalBindingKey: raw.journalBindingKey,
1580
+ entry,
1581
+ requestedDisposition: disposition,
1582
+ }))
1285
1583
  fail("has inconsistent private retention evidence");
1286
1584
  if (updateIds.has(entry.updateId))
1287
1585
  fail("has duplicate private retention sources");
1288
1586
  updateIds.add(entry.updateId);
1289
- const committed = current.operatorDispositions?.find(value => value.failureId === disposition.failureId || value.updateId === entry.updateId);
1290
- if (committed && (!isDeepStrictEqual(committed, disposition) || current.entries.some(value => value.updateId === entry.updateId)))
1587
+ const committed = current.operatorDispositions?.find((value) => value.failureId === disposition.failureId ||
1588
+ value.updateId === entry.updateId);
1589
+ if (committed &&
1590
+ (!isDeepStrictEqual(committed, disposition) ||
1591
+ current.entries.some((value) => value.updateId === entry.updateId)))
1291
1592
  fail("private retention contradicts its journal disposition");
1292
- retainedInputs.push({ path: target, journalBindingKey: raw.journalBindingKey,
1293
- failureId: disposition.failureId, updateId: entry.updateId, state: committed ? "committed" : "uncommitted" });
1593
+ retainedInputs.push({
1594
+ path: target,
1595
+ journalBindingKey: raw.journalBindingKey,
1596
+ failureId: disposition.failureId,
1597
+ updateId: entry.updateId,
1598
+ state: committed ? "committed" : "uncommitted",
1599
+ });
1294
1600
  }
1295
1601
  }
1296
- if (inspectRetention && file?.operatorDispositions?.some(disposition => "dispositionKind" in disposition && disposition.dispositionKind === "legacy-custody" &&
1297
- disposition.action === "discard" && /^abandon-[a-f0-9]{64}$/u.test(disposition.failureId) &&
1298
- !retainedInputs.some(original => original.updateId === disposition.updateId && original.state === "committed")))
1602
+ if (inspectRetention &&
1603
+ file?.operatorDispositions?.some((disposition) => "dispositionKind" in disposition &&
1604
+ disposition.dispositionKind === "legacy-custody" &&
1605
+ disposition.action === "discard" &&
1606
+ /^abandon-[a-f0-9]{64}$/u.test(disposition.failureId) &&
1607
+ !retainedInputs.some((original) => original.updateId === disposition.updateId &&
1608
+ original.state === "committed")))
1299
1609
  fail("committed abandonment lost its private original");
1300
1610
  // Re-enumeration has the same bound, rather than allocating an unbounded census.
1301
1611
  files = snapshot ? 1 : 0;
1302
1612
  if (segments && !isDeepStrictEqual(names, enumerate()))
1303
1613
  fail("changed segment namespace");
1304
1614
  files += names.length;
1305
- if (retention && !isDeepStrictEqual(retentionNames, enumerate(retentionDirectory, retentionPattern)))
1615
+ if (retention &&
1616
+ !isDeepStrictEqual(retentionNames, enumerate(retentionDirectory, retentionPattern)))
1306
1617
  fail("changed private retention namespace");
1307
1618
  files += retentionNames.length;
1308
1619
  for (const [target, before] of observed) {
1309
1620
  const after = status(target);
1310
- const compare = selectedVersion !== undefined && ancestors.has(target) ? sameAncestor : same;
1621
+ const compare = selectedVersion !== undefined && ancestors.has(target)
1622
+ ? sameAncestor
1623
+ : same;
1311
1624
  if (before ? !after || !compare(before, after) : after !== undefined)
1312
1625
  fail("changed observed namespace or file");
1313
1626
  }
@@ -1318,14 +1631,27 @@ function acquireTelegramUpdateJournalFamily(input, selectedVersion, inspectReten
1318
1631
  if (file.version !== selectedVersion) {
1319
1632
  throw createJournalError("unsupported-version", path, "does not match the selected source version");
1320
1633
  }
1321
- if (createTelegramUpdateJournalReceiptScope({ profileName: input.profile, botIdentity: expected }) !==
1322
- createTelegramUpdateJournalReceiptScope({ profileName: file.profile, botIdentity: file.botIdentity })) {
1634
+ if (createTelegramUpdateJournalReceiptScope({
1635
+ profileName: input.profile,
1636
+ botIdentity: expected,
1637
+ }) !==
1638
+ createTelegramUpdateJournalReceiptScope({
1639
+ profileName: file.profile,
1640
+ botIdentity: file.botIdentity,
1641
+ })) {
1323
1642
  throw createJournalError("identity-mismatch", path, "does not retain the expected receipt scope");
1324
1643
  }
1325
1644
  }
1326
1645
  }
1327
1646
  return {
1328
- evidence: file ? { kind: "present", file, ...(knownBotId !== undefined ? { knownBotId } : {}), accounting: { files, bytes, work } } : { kind: "absent" },
1647
+ evidence: file
1648
+ ? {
1649
+ kind: "present",
1650
+ file,
1651
+ ...(knownBotId !== undefined ? { knownBotId } : {}),
1652
+ accounting: { files, bytes, work },
1653
+ }
1654
+ : { kind: "absent" },
1329
1655
  snapshotRevision,
1330
1656
  segments: acquiredSegments,
1331
1657
  ...(inspectRetention ? { retainedInputs } : {}),
@@ -1346,9 +1672,19 @@ export function inspectTelegramSessionJournalNamespace(input) {
1346
1672
  }
1347
1673
  function inspectTelegramJournalNamespace(input, includeSessions) {
1348
1674
  const { directory, profile, limits } = input;
1349
- const fail = (message) => { throw createJournalError("invalid", directory, message); };
1350
- const capacity = () => { throw createJournalError("capacity", directory, "exceeds namespace inspection resource limits"); };
1351
- for (const key of ["maxDirectoryEntries", "maxFiles", "maxBytes", "maxEntries", "maxWork"]) {
1675
+ const fail = (message) => {
1676
+ throw createJournalError("invalid", directory, message);
1677
+ };
1678
+ const capacity = () => {
1679
+ throw createJournalError("capacity", directory, "exceeds namespace inspection resource limits");
1680
+ };
1681
+ for (const key of [
1682
+ "maxDirectoryEntries",
1683
+ "maxFiles",
1684
+ "maxBytes",
1685
+ "maxEntries",
1686
+ "maxWork",
1687
+ ]) {
1352
1688
  if (!isSafePositiveInteger(limits[key]))
1353
1689
  fail("requires positive safe-integer limits");
1354
1690
  }
@@ -1357,26 +1693,37 @@ function inspectTelegramJournalNamespace(input, includeSessions) {
1357
1693
  const expected = validateBotIdentity(input.botIdentity, directory);
1358
1694
  if (!isAbsolute(directory) || resolve(directory) !== directory)
1359
1695
  fail("requires a canonical directory anchor");
1360
- if (!constants.O_NOFOLLOW || !constants.O_NONBLOCK)
1361
- fail("platform lacks no-follow nonblocking open evidence");
1362
1696
  const metadata = (target) => {
1363
1697
  const value = lstatSync(target, { bigint: true });
1364
- return { dev: value.dev, ino: value.ino, mode: value.mode, nlink: value.nlink,
1365
- size: value.size, mtimeNs: value.mtimeNs, ctimeNs: value.ctimeNs };
1698
+ return {
1699
+ dev: value.dev,
1700
+ ino: value.ino,
1701
+ mode: value.mode,
1702
+ nlink: value.nlink,
1703
+ size: value.size,
1704
+ mtimeNs: value.mtimeNs,
1705
+ ctimeNs: value.ctimeNs,
1706
+ };
1366
1707
  };
1367
1708
  const suffix = profile === "default" ? "" : `.${profile}`;
1368
1709
  const pollingPath = input.pollingPath ?? join(directory, `inbox${suffix}.json`);
1369
1710
  const polling = relative(directory, pollingPath);
1370
1711
  {
1371
1712
  const parts = polling.split(sep);
1372
- if (!isAbsolute(pollingPath) || resolve(pollingPath) !== pollingPath || parts.at(-1) !== `inbox${suffix}.json` ||
1373
- !(parts.length === 1 || (parts.length === 3 && parts[0] === "sessions" &&
1374
- decodeTelegramSessionDirectoryName(parts[1]) !== undefined)))
1713
+ if (!isAbsolute(pollingPath) ||
1714
+ resolve(pollingPath) !== pollingPath ||
1715
+ parts.at(-1) !== `inbox${suffix}.json` ||
1716
+ !(parts.length === 1 ||
1717
+ (parts.length === 3 &&
1718
+ parts[0] === "sessions" &&
1719
+ decodeTelegramSessionDirectoryName(parts[1]) !== undefined)))
1375
1720
  fail("requires a canonical polling journal path");
1376
1721
  }
1377
1722
  try {
1378
1723
  const root = lstatSync(directory);
1379
- if (!root.isDirectory() || root.isSymbolicLink() || realpathSync(directory) !== directory)
1724
+ if (!root.isDirectory() ||
1725
+ root.isSymbolicLink() ||
1726
+ realpathSync(directory) !== directory)
1380
1727
  fail("requires a canonical directory anchor");
1381
1728
  const beforeRoot = metadata(directory);
1382
1729
  const census = () => {
@@ -1405,7 +1752,8 @@ function inspectTelegramJournalNamespace(input, includeSessions) {
1405
1752
  };
1406
1753
  const requireType = (target, isDirectory) => {
1407
1754
  const value = lstatSync(target);
1408
- if (value.isSymbolicLink() || !(isDirectory ? value.isDirectory() : value.isFile()) ||
1755
+ if (value.isSymbolicLink() ||
1756
+ !(isDirectory ? value.isDirectory() : value.isFile()) ||
1409
1757
  (!isDirectory && value.nlink !== 1))
1410
1758
  fail("has a linked or unexpected journal type");
1411
1759
  };
@@ -1451,7 +1799,9 @@ function inspectTelegramJournalNamespace(input, includeSessions) {
1451
1799
  if (!/inbox/iu.test(name))
1452
1800
  return;
1453
1801
  const match = /^(inbox|follower-inbox-[a-f0-9]{16})(?:\.([a-z0-9]{1,32}))?\.json(\.(?:segments|retained))?$/u.exec(name);
1454
- if (!match || match[2] === "default" || (!includeSessions && match[3] === ".retained"))
1802
+ if (!match ||
1803
+ match[2] === "default" ||
1804
+ (!includeSessions && match[3] === ".retained"))
1455
1805
  fail("has a noncanonical journal-like entry");
1456
1806
  requireType(target, !!match[3]);
1457
1807
  if (includeSessions && match[3])
@@ -1461,26 +1811,58 @@ function inspectTelegramJournalNamespace(input, includeSessions) {
1461
1811
  if (familyProfile === profile && base !== pollingPath)
1462
1812
  families.set(base, match[1] === "inbox" ? "session" : "follower");
1463
1813
  });
1464
- return { entries: [...entries].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0),
1465
- families: [...families].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0), profiles: [...profiles].sort() };
1814
+ return {
1815
+ entries: [...entries].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)),
1816
+ families: [...families].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0),
1817
+ profiles: [...profiles].sort(),
1818
+ };
1466
1819
  };
1467
1820
  const before = census();
1468
- const sourceInventory = [[pollingPath, "polling"], ...before.families];
1821
+ const sourceInventory = [
1822
+ [pollingPath, "polling"],
1823
+ ...before.families,
1824
+ ];
1469
1825
  const sources = [];
1470
1826
  const retainedInputs = [];
1471
- const accounting = { directoryEntries: before.entries.length, files: 0, bytes: 0, work: 0 };
1827
+ const accounting = {
1828
+ directoryEntries: before.entries.length,
1829
+ files: 0,
1830
+ bytes: 0,
1831
+ work: 0,
1832
+ };
1472
1833
  let knownBotId = expected.botId;
1473
1834
  for (const [path, role] of sourceInventory) {
1474
- const remaining = { maxFiles: limits.maxFiles - accounting.files, maxBytes: limits.maxBytes - accounting.bytes,
1475
- maxWork: limits.maxWork - accounting.work, maxEntries: limits.maxEntries };
1476
- if (remaining.maxFiles <= 0 || remaining.maxBytes <= 0 || remaining.maxWork <= 0)
1835
+ const remaining = {
1836
+ maxFiles: limits.maxFiles - accounting.files,
1837
+ maxBytes: limits.maxBytes - accounting.bytes,
1838
+ maxWork: limits.maxWork - accounting.work,
1839
+ maxEntries: limits.maxEntries,
1840
+ };
1841
+ if (remaining.maxFiles <= 0 ||
1842
+ remaining.maxBytes <= 0 ||
1843
+ remaining.maxWork <= 0)
1477
1844
  capacity();
1478
- const acquire = () => acquireTelegramUpdateJournalFamily({ directory, path, profile,
1479
- botIdentity: { ...expected, ...(knownBotId !== undefined ? { botId: knownBotId } : {}) }, limits: remaining }, undefined, includeSessions);
1480
- const acquired = input.withSourceReference ? input.withSourceReference(path, acquire) : acquire();
1845
+ const acquire = () => acquireTelegramUpdateJournalFamily({
1846
+ directory,
1847
+ path,
1848
+ profile,
1849
+ botIdentity: {
1850
+ ...expected,
1851
+ ...(knownBotId !== undefined ? { botId: knownBotId } : {}),
1852
+ },
1853
+ limits: remaining,
1854
+ }, undefined, includeSessions);
1855
+ const acquired = input.withSourceReference
1856
+ ? input.withSourceReference(path, acquire)
1857
+ : acquire();
1481
1858
  const evidence = acquired.evidence;
1482
- retainedInputs.push(...(acquired.retainedInputs ?? []).map(original => ({ ...original, journalPath: path })));
1483
- if (evidence.kind === "absent" && (role !== "polling" || before.entries.some(([entry]) => entry === polling || entry === `${polling}.segments`))) {
1859
+ retainedInputs.push(...(acquired.retainedInputs ?? []).map((original) => ({
1860
+ ...original,
1861
+ journalPath: path,
1862
+ })));
1863
+ if (evidence.kind === "absent" &&
1864
+ (role !== "polling" ||
1865
+ before.entries.some(([entry]) => entry === polling || entry === `${polling}.segments`))) {
1484
1866
  fail("lost a discovered journal family");
1485
1867
  }
1486
1868
  if (evidence.kind === "present") {
@@ -1488,13 +1870,15 @@ function inspectTelegramJournalNamespace(input, includeSessions) {
1488
1870
  accounting.files += evidence.accounting.files;
1489
1871
  accounting.bytes += evidence.accounting.bytes;
1490
1872
  }
1491
- accounting.work += evidence.kind === "present" ? Math.max(1, evidence.accounting.work) : 1;
1873
+ accounting.work +=
1874
+ evidence.kind === "present" ? Math.max(1, evidence.accounting.work) : 1;
1492
1875
  sources.push({ role, path, evidence });
1493
1876
  }
1494
1877
  const namespaceCurrent = () => {
1495
1878
  try {
1496
- return isDeepStrictEqual(beforeRoot, metadata(directory)) && isDeepStrictEqual(before, census()) &&
1497
- isDeepStrictEqual(beforeRoot, metadata(directory));
1879
+ return (isDeepStrictEqual(beforeRoot, metadata(directory)) &&
1880
+ isDeepStrictEqual(before, census()) &&
1881
+ isDeepStrictEqual(beforeRoot, metadata(directory)));
1498
1882
  }
1499
1883
  catch {
1500
1884
  return false;
@@ -1502,7 +1886,12 @@ function inspectTelegramJournalNamespace(input, includeSessions) {
1502
1886
  };
1503
1887
  if (!namespaceCurrent())
1504
1888
  fail("changed observed root namespace or identity");
1505
- return { sources, accounting, ...(includeSessions ? { retainedInputs } : {}), ...(knownBotId !== undefined ? { knownBotId } : {}) };
1889
+ return {
1890
+ sources,
1891
+ accounting,
1892
+ ...(includeSessions ? { retainedInputs } : {}),
1893
+ ...(knownBotId !== undefined ? { knownBotId } : {}),
1894
+ };
1506
1895
  }
1507
1896
  catch (error) {
1508
1897
  if (error instanceof TelegramUpdateJournalError)
@@ -1510,35 +1899,105 @@ function inspectTelegramJournalNamespace(input, includeSessions) {
1510
1899
  throw createJournalError("io", directory, "could not establish canonical namespace evidence", error);
1511
1900
  }
1512
1901
  }
1902
+ /** Whether an update addresses this private-chat Thread anywhere in its payload (message, callback, reaction, …). */
1903
+ export function doesTelegramJournalUpdateNameThread(update, target) {
1904
+ const matches = (value, depth) => {
1905
+ if (depth > 8 || !value || typeof value !== "object")
1906
+ return false;
1907
+ const record = value;
1908
+ if (record.message_thread_id === target.threadId) {
1909
+ const chat = record.chat;
1910
+ if (!chat || typeof chat !== "object" || chat.id === target.chatId)
1911
+ return true;
1912
+ }
1913
+ return Object.values(record).some((child) => matches(child, depth + 1));
1914
+ };
1915
+ return matches(update, 0);
1916
+ }
1917
+ /** A button tap alone carries no input, so it never keeps a tab whose inputs are resolved (the in-flight Cancel itself). */
1918
+ export function isTelegramJournalLoneCallbackUpdate(update) {
1919
+ if (!update || typeof update !== "object")
1920
+ return false;
1921
+ const kinds = Object.keys(update).filter((key) => key !== "update_id");
1922
+ return kinds.length === 1 && kinds[0] === "callback_query";
1923
+ }
1924
+ /** Plain inbound kinds whose address is readable; anything else stays protective. */
1925
+ const TELEGRAM_CLEANUP_PLAIN_UPDATE_KINDS = [
1926
+ "message",
1927
+ "edited_message",
1928
+ "callback_query",
1929
+ "message_reaction",
1930
+ ];
1513
1931
  /** Complete-empty protection only; caller holds source serialization through consumption. Never deletion authority. */
1514
1932
  export function isTelegramThreadCleanupJournalNamespaceClear(input) {
1515
- if (!input.requiredJournalBindingKeys.length || input.requiredJournalBindingKeys.length > input.limits.maxDirectoryEntries)
1933
+ if (!input.requiredJournalBindingKeys.length ||
1934
+ input.requiredJournalBindingKeys.length > input.limits.maxDirectoryEntries)
1516
1935
  throw new Error("Telegram temporary cleanup requires bounded exact journal references.");
1517
1936
  const requiredPaths = new Set();
1518
1937
  for (const key of input.requiredJournalBindingKeys) {
1519
1938
  const path = getTelegramUpdateJournalBindingPath(key);
1520
- const identities = [input.botIdentity, { tokenSha256: input.botIdentity.tokenSha256 }];
1521
- if (!path || !identities.some(botIdentity => key === createTelegramUpdateJournalBindingKey({
1522
- path, profileName: input.profile, botIdentity,
1523
- })))
1939
+ const identities = [
1940
+ input.botIdentity,
1941
+ { tokenSha256: input.botIdentity.tokenSha256 },
1942
+ ];
1943
+ if (!path ||
1944
+ !identities.some((botIdentity) => key ===
1945
+ createTelegramUpdateJournalBindingKey({
1946
+ path,
1947
+ profileName: input.profile,
1948
+ botIdentity,
1949
+ })))
1524
1950
  throw new Error("Telegram temporary cleanup has foreign or malformed journal references.");
1525
1951
  requiredPaths.add(path);
1526
1952
  }
1527
1953
  const namespace = inspectTelegramSessionJournalNamespace(input);
1528
- if (namespace.sources.filter(source => source.role === "polling").length !== 1 ||
1529
- namespace.retainedInputs?.some(original => original.state !== "committed") ||
1530
- [...requiredPaths].some(path => !namespace.sources.some(source => source.path === path && source.evidence.kind === "present")))
1954
+ if (namespace.sources.filter((source) => source.role === "polling").length !==
1955
+ 1 ||
1956
+ namespace.retainedInputs?.some((original) => original.state !== "committed") ||
1957
+ [...requiredPaths].some((path) => !namespace.sources.some((source) => source.path === path && source.evidence.kind === "present")))
1531
1958
  return false;
1532
- // Original Telegram targets do not certify execution ownership. No nonempty family gets a source exemption.
1533
- return namespace.sources.every(source => source.evidence.kind === "absent" || source.evidence.file.entries.length === 0);
1959
+ const cleanup = input.cleanup;
1960
+ const ownPaths = new Map();
1961
+ for (const own of cleanup?.ownInputs ?? []) {
1962
+ const path = getTelegramUpdateJournalBindingPath(own.journalBindingKey);
1963
+ if (!path)
1964
+ return false;
1965
+ const ids = ownPaths.get(path) ?? new Set();
1966
+ for (const id of own.updateIds)
1967
+ ids.add(id);
1968
+ ownPaths.set(path, ids);
1969
+ }
1970
+ // Custody-bearing, failed or unreadable work protects every tab: its reply target is not its message address.
1971
+ const isUnrelatedPlainInput = (entry, path) => {
1972
+ const kinds = Object.keys(entry.update).filter((key) => key !== "update_id");
1973
+ return (!!cleanup &&
1974
+ entry.state === "pending" &&
1975
+ !entry.queueOwner &&
1976
+ !entry.queueReceiptId &&
1977
+ !entry.queueKind &&
1978
+ !entry.queueHandoff &&
1979
+ !entry.inputClaim &&
1980
+ !("inputProvenance" in entry) &&
1981
+ !entry.failure &&
1982
+ kinds.length === 1 &&
1983
+ TELEGRAM_CLEANUP_PLAIN_UPDATE_KINDS.includes(kinds[0]) &&
1984
+ (isTelegramJournalLoneCallbackUpdate(entry.update) ||
1985
+ !doesTelegramJournalUpdateNameThread(entry.update, cleanup.target)) &&
1986
+ !ownPaths.get(path)?.has(entry.updateId));
1987
+ };
1988
+ return namespace.sources.every((source) => source.evidence.kind === "absent" ||
1989
+ source.evidence.file.entries.every((entry) => isUnrelatedPlainInput(entry, source.path)));
1534
1990
  }
1535
1991
  function cloneEntry(entry) {
1536
1992
  return {
1537
1993
  ...entry,
1538
1994
  update: structuredClone(entry.update),
1539
- ...(entry.inputClaim ? { inputClaim: structuredClone(entry.inputClaim) } : {}),
1995
+ ...(entry.inputClaim
1996
+ ? { inputClaim: structuredClone(entry.inputClaim) }
1997
+ : {}),
1540
1998
  ...(entry.inputProvenance
1541
- ? { inputProvenance: structuredClone(entry.inputProvenance) } : {}),
1999
+ ? { inputProvenance: structuredClone(entry.inputProvenance) }
2000
+ : {}),
1542
2001
  ...(entry.queueOwner
1543
2002
  ? { queueOwner: cloneJournalQueueOwner(entry.queueOwner) }
1544
2003
  : {}),
@@ -1559,7 +2018,13 @@ function cloneFile(file) {
1559
2018
  profile: file.profile,
1560
2019
  botIdentity: { ...file.botIdentity },
1561
2020
  entries: file.entries.map(cloneEntry),
1562
- ...(file.sourceCompletions?.length ? { sourceCompletions: file.sourceCompletions.map(completion => ({ ...completion })) } : {}),
2021
+ ...(file.sourceCompletions?.length
2022
+ ? {
2023
+ sourceCompletions: file.sourceCompletions.map((completion) => ({
2024
+ ...completion,
2025
+ })),
2026
+ }
2027
+ : {}),
1563
2028
  ...(file.operatorDispositions?.length
1564
2029
  ? {
1565
2030
  operatorDispositions: file.operatorDispositions.map((disposition) => ({ ...disposition })),
@@ -1590,7 +2055,8 @@ function publishTelegramUpdateJournalSegmentUnlocked(path, segment, onPublicatio
1590
2055
  let snapshotRevision = 0;
1591
2056
  try {
1592
2057
  const parsed = JSON.parse(readFileSync(path, "utf8"));
1593
- snapshotRevision = parseJournalFile(parsed, path, segment.version).revision ?? 0;
2058
+ snapshotRevision =
2059
+ parseJournalFile(parsed, path, segment.version).revision ?? 0;
1594
2060
  }
1595
2061
  catch (error) {
1596
2062
  if (error?.code !== "ENOENT")
@@ -1661,7 +2127,9 @@ function mergeBotIdentity(stored, current) {
1661
2127
  }
1662
2128
  function createPendingRetentionReference(input) {
1663
2129
  const { path, journalBindingKey, entry, maxBytes } = input;
1664
- const evidenceSha256 = createHash("sha256").update(JSON.stringify({ journalBindingKey, entry })).digest("hex");
2130
+ const evidenceSha256 = createHash("sha256")
2131
+ .update(JSON.stringify({ journalBindingKey, entry }))
2132
+ .digest("hex");
1665
2133
  const failureId = `abandon-${evidenceSha256}`;
1666
2134
  const retainedDirectory = `${path}.retained`;
1667
2135
  const retainedPath = join(retainedDirectory, `${failureId}.json`);
@@ -1687,22 +2155,37 @@ function createPendingRetentionReference(input) {
1687
2155
  return undefined;
1688
2156
  throw error;
1689
2157
  }
1690
- if (!stat.isFile() || stat.isSymbolicLink() || stat.size > maxBytes + 4096) {
2158
+ if (!stat.isFile() ||
2159
+ stat.isSymbolicLink() ||
2160
+ stat.size > maxBytes + 4096) {
1691
2161
  throw createJournalError("invalid", retainedPath, "retained input is not a bounded regular file");
1692
2162
  }
1693
2163
  const retained = JSON.parse(readFileSync(retainedPath, "utf8"));
1694
2164
  if (!isRecord(retained))
1695
2165
  throw createJournalError("invalid", retainedPath, "retained input is not an evidence record");
1696
2166
  const disposition = validateJournalOperatorDisposition(retained.requestedDisposition, retainedPath);
1697
- if (!("dispositionKind" in disposition) || disposition.dispositionKind !== "legacy-custody" ||
1698
- disposition.failureId !== failureId || disposition.updateId !== entry.updateId ||
1699
- disposition.action !== "discard" || disposition.evidenceSha256 !== evidenceSha256 ||
1700
- !isDeepStrictEqual(retained, { version: 1, kind: "pending-input-retention", journalBindingKey,
1701
- entry, requestedDisposition: disposition })) {
2167
+ if (!("dispositionKind" in disposition) ||
2168
+ disposition.dispositionKind !== "legacy-custody" ||
2169
+ disposition.failureId !== failureId ||
2170
+ disposition.updateId !== entry.updateId ||
2171
+ disposition.action !== "discard" ||
2172
+ disposition.evidenceSha256 !== evidenceSha256 ||
2173
+ !isDeepStrictEqual(retained, {
2174
+ version: 1,
2175
+ kind: "pending-input-retention",
2176
+ journalBindingKey,
2177
+ entry,
2178
+ requestedDisposition: disposition,
2179
+ })) {
1702
2180
  throw createJournalError("conflict", retainedPath, "retained input does not match abandonment evidence");
1703
2181
  }
1704
- return { version: 1, kind: "pending-input-retention", journalBindingKey, entry,
1705
- requestedDisposition: disposition };
2182
+ return {
2183
+ version: 1,
2184
+ kind: "pending-input-retention",
2185
+ journalBindingKey,
2186
+ entry,
2187
+ requestedDisposition: disposition,
2188
+ };
1706
2189
  };
1707
2190
  return { evidenceSha256, failureId, retainedPath, read };
1708
2191
  }
@@ -1826,26 +2309,47 @@ export function createTelegramUpdateJournalRuntimeBindingResolver(deps) {
1826
2309
  });
1827
2310
  const path = deps.getJournalPath(configuredProfileName);
1828
2311
  const workspaceAdmission = deps.getWorkspaceAdmission?.();
1829
- const sourceAccess = { directory: dirname(path), limits: {
1830
- maxFiles: 1024, maxBytes: TELEGRAM_UPDATE_JOURNAL_MAX_BYTES * 2,
1831
- maxEntries: TELEGRAM_UPDATE_JOURNAL_MAX_ENTRIES, maxWork: TELEGRAM_UPDATE_JOURNAL_MAX_ENTRIES * 1024,
1832
- } };
1833
- const options = { path, profileName, botIdentity,
1834
- ...(deps.getQueueRuntimeIdentity ? { queueRuntimeIdentity: deps.getQueueRuntimeIdentity() } : {}),
2312
+ const sourceAccess = {
2313
+ directory: dirname(path),
2314
+ limits: {
2315
+ maxFiles: 1024,
2316
+ maxBytes: TELEGRAM_UPDATE_JOURNAL_MAX_BYTES * 2,
2317
+ maxEntries: TELEGRAM_UPDATE_JOURNAL_MAX_ENTRIES,
2318
+ maxWork: TELEGRAM_UPDATE_JOURNAL_MAX_ENTRIES * 1024,
2319
+ },
2320
+ };
2321
+ const options = {
2322
+ path,
2323
+ profileName,
2324
+ botIdentity,
2325
+ ...(deps.getQueueRuntimeIdentity
2326
+ ? { queueRuntimeIdentity: deps.getQueueRuntimeIdentity() }
2327
+ : {}),
1835
2328
  ...(workspaceAdmission ? { workspaceAdmission } : {}),
1836
- ...(deps.withWriterAdmission ? { withWriterAdmission: deps.withWriterAdmission } : {}),
1837
- ...(deps.withSourceSerialization ? { withSourceSerialization: deps.withSourceSerialization } : {}),
1838
- ...(deps.onRecovery ? { onRecovery: deps.onRecovery } : {}) };
2329
+ ...(deps.withWriterAdmission
2330
+ ? { withWriterAdmission: deps.withWriterAdmission }
2331
+ : {}),
2332
+ ...(deps.withSourceSerialization
2333
+ ? { withSourceSerialization: deps.withSourceSerialization }
2334
+ : {}),
2335
+ ...(deps.onRecovery ? { onRecovery: deps.onRecovery } : {}),
2336
+ };
1839
2337
  const journal = createTelegramUpdateJournalStore(options);
1840
- // Scoped completion and receipt observation use strict private handles where the platform provides no-follow
1841
- // evidence; elsewhere (Windows) the same capabilities read through the ordinary journal so queue receipts still work.
1842
- const strict = deps.strictSourceAccess ?? Boolean(constants.O_NOFOLLOW && constants.O_NONBLOCK);
2338
+ // Scoped completion and receipt observation always use strict private handles; see TELEGRAM_STRICT_READ_FLAGS.
2339
+ const strict = deps.strictSourceAccess ?? true;
1843
2340
  const completionJournal = deps.withSourceSerialization
1844
- ? createTelegramUpdateJournalStore({ ...options, ...(strict ? { sourceAccess } : {}) }) : undefined;
2341
+ ? createTelegramUpdateJournalStore({
2342
+ ...options,
2343
+ ...(strict ? { sourceAccess } : {}),
2344
+ })
2345
+ : undefined;
1845
2346
  // Queue receipt readiness gates every ordinary prompt; without strict handles it uses the ordinary journal read.
1846
2347
  const observeQueuedReceipt = (expected) => strict
1847
2348
  ? completionJournal.inspectQueuedReceipt(expected)
1848
- : inspectJournalQueuedReceipt(journal.read(), { ...expected, queueOwner: validateQueuedReceiptObservation(expected, path) }, path);
2349
+ : inspectJournalQueuedReceipt(journal.read(), {
2350
+ ...expected,
2351
+ queueOwner: validateQueuedReceiptObservation(expected, path),
2352
+ }, path);
1849
2353
  return {
1850
2354
  runtimeKey: JSON.stringify({
1851
2355
  path,
@@ -1860,25 +2364,48 @@ export function createTelegramUpdateJournalRuntimeBindingResolver(deps) {
1860
2364
  readForProtection() {
1861
2365
  // Protection must never turn corruption/recovery into empty-work evidence.
1862
2366
  const evidence = inspectTelegramUpdateJournalFamily({
1863
- ...sourceAccess, path, profile: profileName, botIdentity,
2367
+ ...sourceAccess,
2368
+ path,
2369
+ profile: profileName,
2370
+ botIdentity,
1864
2371
  });
1865
- return { entries: evidence.kind === "present" ? evidence.file.entries : [], exists: evidence.kind === "present" };
2372
+ return {
2373
+ entries: evidence.kind === "present" ? evidence.file.entries : [],
2374
+ exists: evidence.kind === "present",
2375
+ };
1866
2376
  },
1867
- journal: completionJournal ? { ...journal,
1868
- removeCompletedExact(updateIds, expectedSources, completions) {
1869
- return completions !== undefined ? completionJournal.removeCompletedExact(updateIds, expectedSources, completions)
1870
- : journal.removeCompletedExact(updateIds, expectedSources);
1871
- },
1872
- completeQueuedExact(receipts, completions) { return completionJournal.completeQueuedExact(receipts, completions); },
1873
- inspectSourceCompletion(expected) { return completionJournal.inspectSourceCompletion(expected); },
1874
- inspectQueuedReceipt: observeQueuedReceipt,
1875
- isQueueReceiptCurrent(receipt, owner) {
1876
- if (receipt.journalBindingKey !== createTelegramUpdateJournalBindingKey({ path, profileName, botIdentity }))
1877
- return false;
1878
- return observeQueuedReceipt({ queueKind: receipt.queueKind, receiptId: receipt.receiptId,
1879
- sourceUpdateIds: [...receipt.sourceUpdateIds], queueOwner: { ...owner } }) !== undefined;
1880
- },
1881
- } : journal,
2377
+ journal: completionJournal
2378
+ ? {
2379
+ ...journal,
2380
+ removeCompletedExact(updateIds, expectedSources, completions, isCurrent) {
2381
+ return completions !== undefined
2382
+ ? completionJournal.removeCompletedExact(updateIds, expectedSources, completions, isCurrent)
2383
+ : journal.removeCompletedExact(updateIds, expectedSources, undefined, isCurrent);
2384
+ },
2385
+ completeQueuedExact(receipts, completions) {
2386
+ return completionJournal.completeQueuedExact(receipts, completions);
2387
+ },
2388
+ inspectSourceCompletion(expected) {
2389
+ return completionJournal.inspectSourceCompletion(expected);
2390
+ },
2391
+ inspectQueuedReceipt: observeQueuedReceipt,
2392
+ isQueueReceiptCurrent(receipt, owner) {
2393
+ if (receipt.journalBindingKey !==
2394
+ createTelegramUpdateJournalBindingKey({
2395
+ path,
2396
+ profileName,
2397
+ botIdentity,
2398
+ }))
2399
+ return false;
2400
+ return (observeQueuedReceipt({
2401
+ queueKind: receipt.queueKind,
2402
+ receiptId: receipt.receiptId,
2403
+ sourceUpdateIds: [...receipt.sourceUpdateIds],
2404
+ queueOwner: { ...owner },
2405
+ }) !== undefined);
2406
+ },
2407
+ }
2408
+ : journal,
1882
2409
  };
1883
2410
  };
1884
2411
  }
@@ -1899,12 +2426,13 @@ export function createTelegramUpdateJournalReferenceRegistry(input = {}) {
1899
2426
  released = true;
1900
2427
  };
1901
2428
  },
1902
- list: () => [...active.values()].map(reference => ({ ...reference })),
2429
+ list: () => [...active.values()].map((reference) => ({ ...reference })),
1903
2430
  withReference(reference, operation) {
1904
2431
  const release = this.acquire(reference);
1905
2432
  try {
1906
2433
  const result = operation();
1907
- if (result && typeof result.finally === "function")
2434
+ if (result &&
2435
+ typeof result.finally === "function")
1908
2436
  return result.finally(release);
1909
2437
  release();
1910
2438
  return result;
@@ -1920,15 +2448,23 @@ export function withTelegramResolvedUpdateJournalReference(input) {
1920
2448
  const binding = input.resolveBinding();
1921
2449
  if (!binding)
1922
2450
  return undefined;
1923
- return input.registry.withReference({ referenceClass: input.referenceClass,
1924
- recoveryKey: binding.recoveryKey }, () => input.operation(binding));
2451
+ return input.registry.withReference({ referenceClass: input.referenceClass, recoveryKey: binding.recoveryKey }, () => input.operation(binding));
1925
2452
  }
1926
2453
  function validateQueuedReceiptObservation(expected, path) {
1927
- if (!isRecord(expected) || !hasOnlyKeys(expected, ["queueKind", "receiptId", "sourceUpdateIds", "queueOwner"]) || (expected.queueKind !== "prompt" && expected.queueKind !== "control") ||
2454
+ if (!isRecord(expected) ||
2455
+ !hasOnlyKeys(expected, [
2456
+ "queueKind",
2457
+ "receiptId",
2458
+ "sourceUpdateIds",
2459
+ "queueOwner",
2460
+ ]) ||
2461
+ (expected.queueKind !== "prompt" && expected.queueKind !== "control") ||
1928
2462
  !isNonEmptyString(expected.receiptId) ||
1929
- !Array.isArray(expected.sourceUpdateIds) || expected.sourceUpdateIds.length === 0 ||
2463
+ !Array.isArray(expected.sourceUpdateIds) ||
2464
+ expected.sourceUpdateIds.length === 0 ||
1930
2465
  expected.sourceUpdateIds.length > TELEGRAM_UPDATE_JOURNAL_MAX_ENTRIES ||
1931
- expected.sourceUpdateIds.some((id, index) => !isSafeNonNegativeInteger(id) || (index > 0 && id <= expected.sourceUpdateIds[index - 1]))) {
2466
+ expected.sourceUpdateIds.some((id, index) => !isSafeNonNegativeInteger(id) ||
2467
+ (index > 0 && id <= expected.sourceUpdateIds[index - 1]))) {
1932
2468
  throw createJournalError("invalid", path, "received invalid queue receipt observation authority");
1933
2469
  }
1934
2470
  return validateJournalQueueOwner(expected.queueOwner, path);
@@ -1937,19 +2473,31 @@ function inspectJournalQueuedReceipt(file, expected, path) {
1937
2473
  if (!isTelegramUpdateJournalLegacyFamilyVersion(file.version))
1938
2474
  throw createJournalError("invalid", path, "queue receipt observation requires an exact legacy v1 source handle");
1939
2475
  const { queueOwner } = expected;
1940
- const entries = file.entries.filter(entry => entry.queueReceiptId === expected.receiptId);
1941
- if (entries.length !== expected.sourceUpdateIds.length || entries.some((entry, index) => entry.updateId !== expected.sourceUpdateIds[index] || entry.state !== "queued" || entry.queueKind !== expected.queueKind ||
1942
- entry.queueHandoff !== undefined || !isDeepStrictEqual(entry.queueOwner, queueOwner)))
2476
+ const entries = file.entries.filter((entry) => entry.queueReceiptId === expected.receiptId);
2477
+ if (entries.length !== expected.sourceUpdateIds.length ||
2478
+ entries.some((entry, index) => entry.updateId !== expected.sourceUpdateIds[index] ||
2479
+ entry.state !== "queued" ||
2480
+ entry.queueKind !== expected.queueKind ||
2481
+ entry.queueHandoff !== undefined ||
2482
+ !isDeepStrictEqual(entry.queueOwner, queueOwner)))
1943
2483
  return undefined;
1944
- return { receipt: { queueKind: expected.queueKind, receiptId: expected.receiptId,
1945
- sourceUpdateIds: [...expected.sourceUpdateIds], queueOwner: { ...queueOwner } },
2484
+ return {
2485
+ receipt: {
2486
+ queueKind: expected.queueKind,
2487
+ receiptId: expected.receiptId,
2488
+ sourceUpdateIds: [...expected.sourceUpdateIds],
2489
+ queueOwner: { ...queueOwner },
2490
+ },
1946
2491
  sources: entries.map(createTelegramUpdateJournalEntryDigest),
1947
- queueOwnerSha256: createHash("sha256").update(JSON.stringify(queueOwner)).digest("hex") };
2492
+ queueOwnerSha256: createHash("sha256")
2493
+ .update(JSON.stringify(queueOwner))
2494
+ .digest("hex"),
2495
+ };
1948
2496
  }
1949
2497
  function inspectJournalSourceCompletion(file, completion, path) {
1950
2498
  if (!isTelegramUpdateJournalLegacyFamilyVersion(file.version))
1951
2499
  throw createJournalError("invalid", path, "source completion requires a legacy v1 source");
1952
- const retained = file.sourceCompletions?.find(value => value.updateId === completion.updateId);
2500
+ const retained = file.sourceCompletions?.find((value) => value.updateId === completion.updateId);
1953
2501
  if (!retained)
1954
2502
  return undefined;
1955
2503
  if (!isDeepStrictEqual(retained, completion))
@@ -1959,35 +2507,48 @@ function inspectJournalSourceCompletion(file, completion, path) {
1959
2507
  /** Operator-distinct authority: routing never treats it as an owner cancellation. */
1960
2508
  export const TELEGRAM_SESSION_ADOPTION_AUTHORITY_PREFIX = "session-successor:";
1961
2509
  function isAdoptablePendingEntry(entry) {
1962
- return entry.state === "pending" && !entry.inputClaim && !entry.inputProvenance && !entry.routingInput &&
1963
- !entry.queueOwner && !entry.queueHandoff && !entry.queueReceiptId && !entry.queueKind && !entry.failure &&
1964
- entry.preApprovalExcluded !== true;
2510
+ return (entry.state === "pending" &&
2511
+ !entry.inputClaim &&
2512
+ !entry.inputProvenance &&
2513
+ !entry.routingInput &&
2514
+ !entry.queueOwner &&
2515
+ !entry.queueHandoff &&
2516
+ !entry.queueReceiptId &&
2517
+ !entry.queueKind &&
2518
+ !entry.failure &&
2519
+ entry.preApprovalExcluded !== true);
1965
2520
  }
1966
2521
  export function createTelegramUpdateJournalBindingRuntime(deps) {
1967
2522
  const resolveLeader = createTelegramUpdateJournalRuntimeBindingResolver({
1968
2523
  ...deps.base,
1969
2524
  getJournalPath: deps.getLeaderJournalPath,
1970
2525
  });
2526
+ // Follower and path journals share identity and admission ports, never the leader's queue identity or strict source mode.
2527
+ const getSharedResolverPorts = () => ({
2528
+ getProfileName: deps.base.getProfileName,
2529
+ getBotToken: deps.base.getBotToken,
2530
+ getBotId: deps.base.getBotId,
2531
+ ...(deps.base.withWriterAdmission
2532
+ ? { withWriterAdmission: deps.base.withWriterAdmission }
2533
+ : {}),
2534
+ // Follower journals need the same strict exact-receipt ports as the leader; without them queue publication cannot complete.
2535
+ ...(deps.base.withSourceSerialization
2536
+ ? { withSourceSerialization: deps.base.withSourceSerialization }
2537
+ : {}),
2538
+ ...(deps.base.getWorkspaceAdmission
2539
+ ? { getWorkspaceAdmission: deps.base.getWorkspaceAdmission }
2540
+ : {}),
2541
+ ...(deps.base.onRecovery ? { onRecovery: deps.base.onRecovery } : {}),
2542
+ });
1971
2543
  const createFollowerResolver = (bindingKey, includeQueueRuntimeIdentity, sessionId, legacy = false) => {
1972
2544
  // Session-aware composition must not fall back to a process journal before preparation supplies its ID.
1973
2545
  if (!legacy && deps.getActiveFollowerSessionId && !sessionId)
1974
2546
  return () => undefined;
1975
2547
  return createTelegramUpdateJournalRuntimeBindingResolver({
1976
- getProfileName: deps.base.getProfileName,
1977
- getBotToken: deps.base.getBotToken,
1978
- getBotId: deps.base.getBotId,
2548
+ ...getSharedResolverPorts(),
1979
2549
  ...(includeQueueRuntimeIdentity && deps.base.getQueueRuntimeIdentity
1980
2550
  ? { getQueueRuntimeIdentity: deps.base.getQueueRuntimeIdentity }
1981
2551
  : {}),
1982
- ...(deps.base.withWriterAdmission
1983
- ? { withWriterAdmission: deps.base.withWriterAdmission } : {}),
1984
- // Follower journals need the same strict exact-receipt ports as the leader; without them queue publication cannot complete.
1985
- ...(deps.base.withSourceSerialization
1986
- ? { withSourceSerialization: deps.base.withSourceSerialization } : {}),
1987
- ...(deps.base.getWorkspaceAdmission
1988
- ? { getWorkspaceAdmission: deps.base.getWorkspaceAdmission }
1989
- : {}),
1990
- ...(deps.base.onRecovery ? { onRecovery: deps.base.onRecovery } : {}),
1991
2552
  getJournalPath(profileName) {
1992
2553
  return deps.getFollowerJournalPath(bindingKey, profileName, sessionId);
1993
2554
  },
@@ -2001,8 +2562,15 @@ export function createTelegramUpdateJournalBindingRuntime(deps) {
2001
2562
  const profile = deps.base.getProfileName() ?? TELEGRAM_DEFAULT_PROFILE_NAME;
2002
2563
  if (!path || !token)
2003
2564
  return undefined;
2004
- const botIdentity = createTelegramUpdateJournalBotIdentity({ botToken: token, botId });
2005
- const keys = [botIdentity, { tokenSha256: botIdentity.tokenSha256 }].map(identity => createTelegramUpdateJournalBindingKey({ path, profileName: profile, botIdentity: identity }));
2565
+ const botIdentity = createTelegramUpdateJournalBotIdentity({
2566
+ botToken: token,
2567
+ botId,
2568
+ });
2569
+ const keys = [botIdentity, { tokenSha256: botIdentity.tokenSha256 }].map((identity) => createTelegramUpdateJournalBindingKey({
2570
+ path,
2571
+ profileName: profile,
2572
+ botIdentity: identity,
2573
+ }));
2006
2574
  if (!keys.includes(journalBindingKey))
2007
2575
  return undefined;
2008
2576
  const getRoot = () => deps.getRuntimeDir?.() ?? dirname(deps.getLeaderJournalPath(profile));
@@ -2011,23 +2579,43 @@ export function createTelegramUpdateJournalBindingRuntime(deps) {
2011
2579
  const suffix = escapeTelegramJournalPathPattern(getTelegramProfilePathSuffix(profile));
2012
2580
  const flat = new RegExp(`^(?:inbox|follower-inbox-[a-f0-9]{16})${suffix}\\.json$`, "u");
2013
2581
  const session = new RegExp(`^(?:journal\\.[a-f0-9]{16}|inbox)${suffix}\\.json$`, "u");
2014
- if (!isAbsolute(root) || resolve(root) !== root || !isAbsolute(path) || resolve(path) !== path ||
2015
- !(parts.length === 1 && flat.test(parts[0]) || parts.length === 3 && parts[0] === "sessions" &&
2016
- decodeTelegramSessionDirectoryName(parts[1]) !== undefined && session.test(parts[2])))
2582
+ if (!isAbsolute(root) ||
2583
+ resolve(root) !== root ||
2584
+ !isAbsolute(path) ||
2585
+ resolve(path) !== path ||
2586
+ !((parts.length === 1 && flat.test(parts[0])) ||
2587
+ (parts.length === 3 &&
2588
+ parts[0] === "sessions" &&
2589
+ decodeTelegramSessionDirectoryName(parts[1]) !== undefined &&
2590
+ session.test(parts[2]))))
2017
2591
  return undefined;
2018
- const current = () => deps.base.getBotToken() === token && deps.base.getBotId() === botId &&
2019
- (deps.base.getProfileName() ?? TELEGRAM_DEFAULT_PROFILE_NAME) === profile && getRoot() === root;
2592
+ const current = () => deps.base.getBotToken() === token &&
2593
+ deps.base.getBotId() === botId &&
2594
+ (deps.base.getProfileName() ?? TELEGRAM_DEFAULT_PROFILE_NAME) ===
2595
+ profile &&
2596
+ getRoot() === root;
2020
2597
  const inspect = () => {
2021
2598
  if (!current())
2022
2599
  return undefined;
2023
- const observed = inspectTelegramUpdateJournalRetention({ directory: root, path, profile, botIdentity,
2024
- limits: { maxFiles: 1024, maxBytes: TELEGRAM_UPDATE_JOURNAL_MAX_BYTES * 2,
2025
- maxEntries: TELEGRAM_UPDATE_JOURNAL_MAX_ENTRIES, maxWork: TELEGRAM_UPDATE_JOURNAL_MAX_ENTRIES * 1024 } });
2600
+ const observed = inspectTelegramUpdateJournalRetention({
2601
+ directory: root,
2602
+ path,
2603
+ profile,
2604
+ botIdentity,
2605
+ limits: {
2606
+ maxFiles: 1024,
2607
+ maxBytes: TELEGRAM_UPDATE_JOURNAL_MAX_BYTES * 2,
2608
+ maxEntries: TELEGRAM_UPDATE_JOURNAL_MAX_ENTRIES,
2609
+ maxWork: TELEGRAM_UPDATE_JOURNAL_MAX_ENTRIES * 1024,
2610
+ },
2611
+ });
2026
2612
  if (observed.evidence.kind !== "present" || !current())
2027
2613
  return undefined;
2028
2614
  return operation(observed.evidence.file, observed.retainedInputs, path);
2029
2615
  };
2030
- const result = deps.base.withSourceSerialization ? deps.base.withSourceSerialization(inspect) : inspect();
2616
+ const result = deps.base.withSourceSerialization
2617
+ ? deps.base.withSourceSerialization(inspect)
2618
+ : inspect();
2031
2619
  return current() ? result : undefined;
2032
2620
  };
2033
2621
  let preparedFollower;
@@ -2042,9 +2630,15 @@ export function createTelegramUpdateJournalBindingRuntime(deps) {
2042
2630
  if (!sessionId)
2043
2631
  return undefined;
2044
2632
  const previous = preparedFollower;
2045
- const result = previous && previous.recipientBindingKey === recipientBindingKey && previous.sessionId !== sessionId
2046
- ? runtime.adoptPredecessorPending({ recipientBindingKey, predecessorSessionId: previous.sessionId,
2047
- successorSessionId: sessionId, isCurrent })
2633
+ const result = previous &&
2634
+ previous.recipientBindingKey === recipientBindingKey &&
2635
+ previous.sessionId !== sessionId
2636
+ ? runtime.adoptPredecessorPending({
2637
+ recipientBindingKey,
2638
+ predecessorSessionId: previous.sessionId,
2639
+ successorSessionId: sessionId,
2640
+ isCurrent,
2641
+ })
2048
2642
  : undefined;
2049
2643
  // Only a completed adoption advances the predecessor; failures retry idempotently on the next preparation.
2050
2644
  preparedFollower = { recipientBindingKey, sessionId };
@@ -2054,16 +2648,27 @@ export function createTelegramUpdateJournalBindingRuntime(deps) {
2054
2648
  if (!isSafeNonNegativeInteger(updateId))
2055
2649
  throw new Error("Telegram abandonment proof requires an exact update ID.");
2056
2650
  return inspectHistoricalSource(journalBindingKey, (file, originals) => {
2057
- const original = originals.find(value => value.updateId === updateId && value.state === "committed" &&
2651
+ const original = originals.find((value) => value.updateId === updateId &&
2652
+ value.state === "committed" &&
2058
2653
  value.journalBindingKey === journalBindingKey);
2059
2654
  if (!original)
2060
2655
  return undefined;
2061
- const disposition = file.operatorDispositions?.find(value => value.failureId === original.failureId);
2062
- if (!disposition || !("dispositionKind" in disposition) || disposition.dispositionKind !== "legacy-custody")
2656
+ const disposition = file.operatorDispositions?.find((value) => value.failureId === original.failureId);
2657
+ if (!disposition ||
2658
+ !("dispositionKind" in disposition) ||
2659
+ disposition.dispositionKind !== "legacy-custody")
2063
2660
  return undefined;
2064
- return { journalBindingKey, updateId, retainedPath: original.path, operatorAuthorityId: disposition.operatorAuthorityId };
2661
+ return {
2662
+ journalBindingKey,
2663
+ updateId,
2664
+ retainedPath: original.path,
2665
+ operatorAuthorityId: disposition.operatorAuthorityId,
2666
+ };
2065
2667
  });
2066
2668
  },
2669
+ inspectSourceGroupExpiry(journalBindingKey, updateIds) {
2670
+ return inspectHistoricalSource(journalBindingKey, (file) => inspectRoutingGroupExpiry(file, journalBindingKey, updateIds));
2671
+ },
2067
2672
  inspectSourceCompletion(journalBindingKey, expected) {
2068
2673
  const completion = validateJournalSourceCompletion(expected, getTelegramUpdateJournalBindingPath(journalBindingKey) ?? "journal");
2069
2674
  return inspectHistoricalSource(journalBindingKey, (file, _originals, path) => inspectJournalSourceCompletion(file, completion, path));
@@ -2073,9 +2678,12 @@ export function createTelegramUpdateJournalBindingRuntime(deps) {
2073
2678
  return inspectHistoricalSource(journalBindingKey, (file, _originals, path) => inspectJournalQueuedReceipt(file, { ...expected, queueOwner }, path));
2074
2679
  },
2075
2680
  adoptPredecessorPending(input) {
2076
- const { recipientBindingKey, predecessorSessionId, successorSessionId, isCurrent } = input;
2077
- if (!recipientBindingKey || !predecessorSessionId || !successorSessionId ||
2078
- predecessorSessionId === successorSessionId || typeof isCurrent !== "function") {
2681
+ const { recipientBindingKey, predecessorSessionId, successorSessionId, isCurrent, } = input;
2682
+ if (!recipientBindingKey ||
2683
+ !predecessorSessionId ||
2684
+ !successorSessionId ||
2685
+ predecessorSessionId === successorSessionId ||
2686
+ typeof isCurrent !== "function") {
2079
2687
  throw new Error("Telegram session adoption requires distinct exact session identities.");
2080
2688
  }
2081
2689
  const assertCurrent = () => {
@@ -2085,18 +2693,29 @@ export function createTelegramUpdateJournalBindingRuntime(deps) {
2085
2693
  assertCurrent();
2086
2694
  const predecessor = createFollowerResolver(recipientBindingKey, false, predecessorSessionId)();
2087
2695
  const successor = createFollowerResolver(recipientBindingKey, false, successorSessionId)();
2088
- if (!predecessor?.readForProtection || !successor || predecessor.recoveryKey === successor.recoveryKey) {
2696
+ if (!predecessor?.readForProtection ||
2697
+ !successor ||
2698
+ predecessor.recoveryKey === successor.recoveryKey) {
2089
2699
  throw new Error("Telegram session adoption requires exact predecessor and successor journals.");
2090
2700
  }
2091
- const result = { adoptedUpdateIds: [], retainedUpdateIds: [] };
2701
+ const result = {
2702
+ adoptedUpdateIds: [],
2703
+ retainedUpdateIds: [],
2704
+ };
2092
2705
  // Non-repairing selection; the abandonment CAS rechecks each exact entry under the source lock.
2093
- const candidates = predecessor.readForProtection().entries.filter(isAdoptablePendingEntry)
2706
+ const candidates = predecessor
2707
+ .readForProtection()
2708
+ .entries.filter(isAdoptablePendingEntry)
2094
2709
  .sort((left, right) => left.updateId - right.updateId);
2095
2710
  for (const entry of candidates) {
2096
2711
  assertCurrent();
2097
2712
  // Commit away first: a crash afterwards preserves the private original instead of duplicating execution.
2098
- const committed = predecessor.journal.abandonPending({ journalBindingKey: predecessor.recoveryKey, entry,
2099
- operatorAuthorityId: `${TELEGRAM_SESSION_ADOPTION_AUTHORITY_PREFIX}${successorSessionId}`, isCurrent });
2713
+ const committed = predecessor.journal.abandonPending({
2714
+ journalBindingKey: predecessor.recoveryKey,
2715
+ entry,
2716
+ operatorAuthorityId: `${TELEGRAM_SESSION_ADOPTION_AUTHORITY_PREFIX}${successorSessionId}`,
2717
+ isCurrent,
2718
+ });
2100
2719
  if (committed.duplicate) {
2101
2720
  result.retainedUpdateIds.push(entry.updateId);
2102
2721
  continue;
@@ -2110,17 +2729,7 @@ export function createTelegramUpdateJournalBindingRuntime(deps) {
2110
2729
  createRecipientResolver: (bindingKey, sessionId) => createFollowerResolver(bindingKey, false, sessionId),
2111
2730
  createLegacyRecipientResolver: (bindingKey) => createFollowerResolver(bindingKey, false, undefined, true),
2112
2731
  createPathResolver: (path) => createTelegramUpdateJournalRuntimeBindingResolver({
2113
- getProfileName: deps.base.getProfileName,
2114
- getBotToken: deps.base.getBotToken,
2115
- getBotId: deps.base.getBotId,
2116
- ...(deps.base.withWriterAdmission
2117
- ? { withWriterAdmission: deps.base.withWriterAdmission } : {}),
2118
- ...(deps.base.withSourceSerialization
2119
- ? { withSourceSerialization: deps.base.withSourceSerialization } : {}),
2120
- ...(deps.base.getWorkspaceAdmission
2121
- ? { getWorkspaceAdmission: deps.base.getWorkspaceAdmission }
2122
- : {}),
2123
- ...(deps.base.onRecovery ? { onRecovery: deps.base.onRecovery } : {}),
2732
+ ...getSharedResolverPorts(),
2124
2733
  getJournalPath: () => path,
2125
2734
  }),
2126
2735
  };
@@ -2132,15 +2741,26 @@ export function createTelegramUpdateJournalStore(options) {
2132
2741
  /** Opt-in v3 only; does not migrate old files or expose legacy unowned mutation ports. */
2133
2742
  export function createTelegramInputJournalStore(options) {
2134
2743
  const captured = { ...options };
2135
- if (!captured.sourceAccess || !captured.queueRuntimeIdentity ||
2744
+ if (!captured.sourceAccess ||
2745
+ !captured.queueRuntimeIdentity ||
2136
2746
  typeof captured.withSourceSerialization !== "function" ||
2137
2747
  typeof captured.withPairingAdmission !== "function" ||
2138
2748
  typeof captured.getInputContext !== "function" ||
2139
- ("withPairedAdmission" in captured && captured.withPairedAdmission !== undefined)) {
2749
+ ("withPairedAdmission" in captured &&
2750
+ captured.withPairedAdmission !== undefined)) {
2140
2751
  throw new Error("Telegram input custody requires strict serialized polling admission and runtime identity.");
2141
2752
  }
2142
2753
  return createJournalStoreCore({ ...captured, queueRuntimeIdentity: { ...captured.queueRuntimeIdentity } }, captured.getInputContext).input;
2143
2754
  }
2755
+ /** Exact unhanded queued receipt member: requested id, queue kind and owner identity. */
2756
+ function isExactQueuedReceiptEntry(entry, requestedIds, queueKind, owner) {
2757
+ return (requestedIds.has(entry.updateId) &&
2758
+ entry.state === "queued" &&
2759
+ entry.queueKind === queueKind &&
2760
+ !!entry.queueOwner &&
2761
+ entry.queueHandoff === undefined &&
2762
+ areTelegramUpdateJournalQueueOwnersEqual(entry.queueOwner, owner));
2763
+ }
2144
2764
  function createJournalStoreCore(options, getInputContext) {
2145
2765
  const path = options.path;
2146
2766
  const profile = options.profileName ?? TELEGRAM_DEFAULT_PROFILE_NAME;
@@ -2174,8 +2794,11 @@ function createJournalStoreCore(options, getInputContext) {
2174
2794
  if (withPairingAdmission && withPairedAdmission) {
2175
2795
  throw new Error("Telegram journal admission modes are mutually exclusive.");
2176
2796
  }
2177
- const version = getInputContext ? TELEGRAM_UPDATE_JOURNAL_CUSTODY_VERSION
2178
- : withPairingAdmission ? TELEGRAM_UPDATE_JOURNAL_EXCLUSION_VERSION : TELEGRAM_UPDATE_JOURNAL_VERSION;
2797
+ const version = getInputContext
2798
+ ? TELEGRAM_UPDATE_JOURNAL_CUSTODY_VERSION
2799
+ : withPairingAdmission
2800
+ ? TELEGRAM_UPDATE_JOURNAL_EXCLUSION_VERSION
2801
+ : TELEGRAM_UPDATE_JOURNAL_VERSION;
2179
2802
  const notifyRecovery = (event) => {
2180
2803
  try {
2181
2804
  options.onRecovery?.(event);
@@ -2361,7 +2984,8 @@ function createJournalStoreCore(options, getInputContext) {
2361
2984
  }
2362
2985
  if (segment.profile !== storedProfile ||
2363
2986
  !identitiesMatch(segment.botIdentity, storedIdentity) ||
2364
- (withPairingAdmission && segment.botIdentity.tokenSha256 !== storedIdentity.tokenSha256)) {
2987
+ (withPairingAdmission &&
2988
+ segment.botIdentity.tokenSha256 !== storedIdentity.tokenSha256)) {
2365
2989
  throw createJournalError("identity-mismatch", segmentPath, "belongs to another journal identity");
2366
2990
  }
2367
2991
  assertJournalSourceCompletionContinuity(file, segment, segmentPath);
@@ -2377,8 +3001,10 @@ function createJournalStoreCore(options, getInputContext) {
2377
3001
  for (const entry of segment.upsertedEntries) {
2378
3002
  const previous = entriesById.get(entry.updateId);
2379
3003
  if (withPairingAdmission &&
2380
- ((previous && previous.preApprovalExcluded !== entry.preApprovalExcluded) ||
2381
- (!previous && entry.updateId <= (file.acceptedThroughUpdateId ?? -1)))) {
3004
+ ((previous &&
3005
+ previous.preApprovalExcluded !== entry.preApprovalExcluded) ||
3006
+ (!previous &&
3007
+ entry.updateId <= (file.acceptedThroughUpdateId ?? -1)))) {
2382
3008
  throw createJournalError("pairing-evidence", segmentPath, "changes exclusion evidence or resurrects a settled source");
2383
3009
  }
2384
3010
  entriesById.set(entry.updateId, entry);
@@ -2401,16 +3027,23 @@ function createJournalStoreCore(options, getInputContext) {
2401
3027
  : file.operatorDispositions?.length
2402
3028
  ? { operatorDispositions: file.operatorDispositions }
2403
3029
  : {}),
2404
- ...(segment.sourceCompletions !== undefined ? { sourceCompletions: segment.sourceCompletions }
2405
- : file.sourceCompletions?.length ? { sourceCompletions: file.sourceCompletions } : {}),
3030
+ ...(segment.sourceCompletions !== undefined
3031
+ ? { sourceCompletions: segment.sourceCompletions }
3032
+ : file.sourceCompletions?.length
3033
+ ? { sourceCompletions: file.sourceCompletions }
3034
+ : {}),
2406
3035
  }, segmentPath, version);
2407
3036
  revision = segment.revision;
2408
3037
  }
2409
3038
  const identityChanged = storedProfile !== profile ||
2410
3039
  !identitiesMatch(file.botIdentity, expectedIdentity) ||
2411
- ((withPairingAdmission || !!file.sourceCompletions?.length) && file.botIdentity.tokenSha256 !== expectedIdentity.tokenSha256);
3040
+ ((withPairingAdmission || !!file.sourceCompletions?.length) &&
3041
+ file.botIdentity.tokenSha256 !== expectedIdentity.tokenSha256);
2412
3042
  if (identityChanged) {
2413
- if (file.entries.length > 0 || file.sourceCompletions?.length || withPairingAdmission || !allowReconciliation) {
3043
+ if (file.entries.length > 0 ||
3044
+ file.sourceCompletions?.length ||
3045
+ withPairingAdmission ||
3046
+ !allowReconciliation) {
2414
3047
  throw createJournalError("identity-mismatch", path, storedProfile !== profile
2415
3048
  ? `belongs to profile ${storedProfile}, not ${profile}`
2416
3049
  : "belongs to another Telegram bot identity");
@@ -2592,15 +3225,26 @@ function createJournalStoreCore(options, getInputContext) {
2592
3225
  try {
2593
3226
  // Acquire before the transaction helper can create parents or staging names.
2594
3227
  // The config continuation excludes participating writers through consumption.
2595
- const source = sourceAccess ? acquireTelegramUpdateJournalFamily({
2596
- ...sourceAccess, path, profile, botIdentity: expectedIdentity,
2597
- }, version) : undefined;
3228
+ const source = sourceAccess
3229
+ ? acquireTelegramUpdateJournalFamily({
3230
+ ...sourceAccess,
3231
+ path,
3232
+ profile,
3233
+ botIdentity: expectedIdentity,
3234
+ }, version)
3235
+ : undefined;
2598
3236
  const readSource = () => {
2599
3237
  if (!source)
2600
3238
  return readCurrent();
2601
- const file = source.evidence.kind === "present" ? source.evidence.file : emptyFile();
2602
- return { file, exists: source.evidence.kind === "present",
2603
- serializedBytes: assertCapacity(file), source };
3239
+ const file = source.evidence.kind === "present"
3240
+ ? source.evidence.file
3241
+ : emptyFile();
3242
+ return {
3243
+ file,
3244
+ exists: source.evidence.kind === "present",
3245
+ serializedBytes: assertCapacity(file),
3246
+ source,
3247
+ };
2604
3248
  };
2605
3249
  return withTelegramFileTransaction(`${path}.transaction`, () => operation(readSource));
2606
3250
  }
@@ -2620,7 +3264,9 @@ function createJournalStoreCore(options, getInputContext) {
2620
3264
  if (!sourceAccess)
2621
3265
  return;
2622
3266
  const limits = sourceAccess.limits;
2623
- if (files > limits.maxFiles || bytes > limits.maxBytes || work > limits.maxWork ||
3267
+ if (files > limits.maxFiles ||
3268
+ bytes > limits.maxBytes ||
3269
+ work > limits.maxWork ||
2624
3270
  collections.some((length) => length > limits.maxEntries)) {
2625
3271
  throw createJournalError("capacity", path, "publication would exceed source inspection resource limits");
2626
3272
  }
@@ -2628,20 +3274,31 @@ function createJournalStoreCore(options, getInputContext) {
2628
3274
  const inputHeadroomText = "\0".repeat(TELEGRAM_UPDATE_JOURNAL_QUEUE_OWNER_ID_MAX_LENGTH);
2629
3275
  const inputBindingHeadroomText = "\0".repeat(TELEGRAM_UPDATE_JOURNAL_INPUT_BINDING_MAX_LENGTH);
2630
3276
  const inputHandoffHeadroomText = "\0".repeat(TELEGRAM_UPDATE_JOURNAL_QUEUE_HANDOFF_ID_MAX_LENGTH);
2631
- const getFileWork = (file) => file.entries.length + (file.operatorDispositions?.length ?? 0) + (file.sourceCompletions?.length ?? 0);
3277
+ const getFileWork = (file) => file.entries.length +
3278
+ (file.operatorDispositions?.length ?? 0) +
3279
+ (file.sourceCompletions?.length ?? 0);
2632
3280
  const advanceInputHeadroomState = (state, entries, projectionSlack) => {
2633
3281
  const revision = (state.file.revision ?? 0) + 1;
2634
3282
  if (!isSafePositiveInteger(revision))
2635
3283
  throw createJournalError("capacity", path, "exhausted input custody revisions");
2636
3284
  return { file: { ...state.file, revision, entries }, projectionSlack };
2637
3285
  };
2638
- const replaceInputHeadroomEntry = (file, updateId, replacement) => file.entries.flatMap(entry => entry.updateId === updateId ? (replacement ? [replacement] : []) : [entry]);
3286
+ const replaceInputHeadroomEntry = (file, updateId, replacement) => file.entries.flatMap((entry) => entry.updateId === updateId
3287
+ ? replacement
3288
+ ? [replacement]
3289
+ : []
3290
+ : [entry]);
2639
3291
  const createInputHeadroomClaim = (entry, phase) => ({
2640
3292
  phase,
2641
- owner: { instanceId: inputHeadroomText, processId: Number.MAX_SAFE_INTEGER,
2642
- processBirthId: inputHeadroomText, sessionGeneration: Number.MAX_SAFE_INTEGER,
2643
- acquisitionId: inputHeadroomText, acquiredAtMs: Number.MAX_SAFE_INTEGER,
2644
- handoffId: inputHandoffHeadroomText },
3293
+ owner: {
3294
+ instanceId: inputHeadroomText,
3295
+ processId: Number.MAX_SAFE_INTEGER,
3296
+ processBirthId: inputHeadroomText,
3297
+ sessionGeneration: Number.MAX_SAFE_INTEGER,
3298
+ acquisitionId: inputHeadroomText,
3299
+ acquiredAtMs: Number.MAX_SAFE_INTEGER,
3300
+ handoffId: inputHandoffHeadroomText,
3301
+ },
2645
3302
  recipientBindingKey: inputBindingHeadroomText,
2646
3303
  executionUpdate: structuredClone(entry.update),
2647
3304
  });
@@ -2656,29 +3313,64 @@ function createJournalStoreCore(options, getInputContext) {
2656
3313
  const createInputHeadroomTransitionPlan = (file, entry) => {
2657
3314
  const initial = { file, projectionSlack: 0 };
2658
3315
  if (entry.inputClaim?.phase === "running")
2659
- return [advanceInputHeadroomState(initial, replaceInputHeadroomEntry(file, entry.updateId), TELEGRAM_UPDATE_JOURNAL_INPUT_TRANSITION_HEADROOM_BYTES)];
2660
- if (entry.state === "pending" && entry.inputClaim?.phase === "ready" && entry.inputClaim.handoff) {
2661
- const acceptedClaim = { ...entry.inputClaim,
2662
- owner: { ...entry.inputClaim.handoff.recipientOwner, acquisitionId: inputHeadroomText,
2663
- acquiredAtMs: Number.MAX_SAFE_INTEGER, handoffId: inputHandoffHeadroomText } };
3316
+ return [
3317
+ advanceInputHeadroomState(initial, replaceInputHeadroomEntry(file, entry.updateId), TELEGRAM_UPDATE_JOURNAL_INPUT_TRANSITION_HEADROOM_BYTES),
3318
+ ];
3319
+ if (entry.state === "pending" &&
3320
+ entry.inputClaim?.phase === "ready" &&
3321
+ entry.inputClaim.handoff) {
3322
+ const acceptedClaim = {
3323
+ ...entry.inputClaim,
3324
+ owner: {
3325
+ ...entry.inputClaim.handoff.recipientOwner,
3326
+ acquisitionId: inputHeadroomText,
3327
+ acquiredAtMs: Number.MAX_SAFE_INTEGER,
3328
+ handoffId: inputHandoffHeadroomText,
3329
+ },
3330
+ };
2664
3331
  delete acceptedClaim.handoff;
2665
3332
  const acceptedEntry = { ...entry, inputClaim: acceptedClaim };
2666
3333
  const acceptedState = advanceInputHeadroomState(initial, replaceInputHeadroomEntry(file, entry.updateId, acceptedEntry), TELEGRAM_UPDATE_JOURNAL_INPUT_TRANSITION_HEADROOM_BYTES);
2667
- return [acceptedState, ...createInputHeadroomTransitionPlan(acceptedState.file, acceptedEntry)];
3334
+ return [
3335
+ acceptedState,
3336
+ ...createInputHeadroomTransitionPlan(acceptedState.file, acceptedEntry),
3337
+ ];
2668
3338
  }
2669
3339
  if (entry.state === "pending" && entry.inputClaim?.phase === "ready") {
2670
- const runningEntry = { ...entry, inputClaim: { ...entry.inputClaim, phase: "running" } };
3340
+ const runningEntry = {
3341
+ ...entry,
3342
+ inputClaim: { ...entry.inputClaim, phase: "running" },
3343
+ };
2671
3344
  const runningState = advanceInputHeadroomState(initial, replaceInputHeadroomEntry(file, entry.updateId, runningEntry), TELEGRAM_UPDATE_JOURNAL_INPUT_TRANSITION_HEADROOM_BYTES);
2672
- return [runningState, advanceInputHeadroomState(runningState, replaceInputHeadroomEntry(runningState.file, entry.updateId), TELEGRAM_UPDATE_JOURNAL_INPUT_TRANSITION_HEADROOM_BYTES)];
3345
+ return [
3346
+ runningState,
3347
+ advanceInputHeadroomState(runningState, replaceInputHeadroomEntry(runningState.file, entry.updateId), TELEGRAM_UPDATE_JOURNAL_INPUT_TRANSITION_HEADROOM_BYTES),
3348
+ ];
2673
3349
  }
2674
- if (entry.state === "pending" && entry.preApprovalExcluded === false && !entry.inputClaim) {
2675
- const readyEntry = { ...entry, inputClaim: createInputHeadroomClaim(entry, "ready") };
3350
+ if (entry.state === "pending" &&
3351
+ entry.preApprovalExcluded === false &&
3352
+ !entry.inputClaim) {
3353
+ const readyEntry = {
3354
+ ...entry,
3355
+ inputClaim: createInputHeadroomClaim(entry, "ready"),
3356
+ };
2676
3357
  const readyState = advanceInputHeadroomState(initial, replaceInputHeadroomEntry(file, entry.updateId, readyEntry), TELEGRAM_UPDATE_JOURNAL_INPUT_PROJECTION_HEADROOM_BYTES);
2677
- const runningEntry = { ...readyEntry, inputClaim: { ...readyEntry.inputClaim, phase: "running" } };
3358
+ const runningEntry = {
3359
+ ...readyEntry,
3360
+ inputClaim: { ...readyEntry.inputClaim, phase: "running" },
3361
+ };
2678
3362
  const runningState = advanceInputHeadroomState(readyState, replaceInputHeadroomEntry(readyState.file, entry.updateId, runningEntry), TELEGRAM_UPDATE_JOURNAL_INPUT_PROJECTION_HEADROOM_BYTES);
2679
- return [readyState, runningState, advanceInputHeadroomState(runningState, replaceInputHeadroomEntry(runningState.file, entry.updateId), TELEGRAM_UPDATE_JOURNAL_INPUT_TRANSITION_HEADROOM_BYTES)];
3363
+ return [
3364
+ readyState,
3365
+ runningState,
3366
+ advanceInputHeadroomState(runningState, replaceInputHeadroomEntry(runningState.file, entry.updateId), TELEGRAM_UPDATE_JOURNAL_INPUT_TRANSITION_HEADROOM_BYTES),
3367
+ ];
2680
3368
  }
2681
- return entry.preApprovalExcluded === true ? [advanceInputHeadroomState(initial, replaceInputHeadroomEntry(file, entry.updateId), TELEGRAM_UPDATE_JOURNAL_INPUT_TRANSITION_HEADROOM_BYTES)] : [];
3369
+ return entry.preApprovalExcluded === true
3370
+ ? [
3371
+ advanceInputHeadroomState(initial, replaceInputHeadroomEntry(file, entry.updateId), TELEGRAM_UPDATE_JOURNAL_INPUT_TRANSITION_HEADROOM_BYTES),
3372
+ ]
3373
+ : [];
2682
3374
  };
2683
3375
  const createInputHeadroomCancelPlan = (file, entry) => {
2684
3376
  if (!entry.inputClaim?.handoff)
@@ -2688,40 +3380,54 @@ function createJournalStoreCore(options, getInputContext) {
2688
3380
  const cancelledEntry = { ...entry, inputClaim: claim };
2689
3381
  const initial = { file, projectionSlack: 0 };
2690
3382
  const cancelledState = advanceInputHeadroomState(initial, replaceInputHeadroomEntry(file, entry.updateId, cancelledEntry), TELEGRAM_UPDATE_JOURNAL_INPUT_TRANSITION_HEADROOM_BYTES);
2691
- return [cancelledState, ...createInputHeadroomTransitionPlan(cancelledState.file, cancelledEntry)];
3383
+ return [
3384
+ cancelledState,
3385
+ ...createInputHeadroomTransitionPlan(cancelledState.file, cancelledEntry),
3386
+ ];
2692
3387
  };
2693
3388
  const createInputHeadroomReleasePlan = (file, entry, allowOffered = false) => {
2694
3389
  if (entry.inputClaim?.handoff && !allowOffered)
2695
3390
  return [];
2696
3391
  const normal = createInputHeadroomTransitionPlan(file, entry);
2697
- const readyState = entry.inputClaim?.phase === "ready" ? { file, projectionSlack: 0 } : normal[0];
3392
+ const readyState = entry.inputClaim?.phase === "ready"
3393
+ ? { file, projectionSlack: 0 }
3394
+ : normal[0];
2698
3395
  if (!readyState)
2699
3396
  return [];
2700
- const readyEntry = readyState.file.entries.find(candidate => candidate.updateId === entry.updateId);
3397
+ const readyEntry = readyState.file.entries.find((candidate) => candidate.updateId === entry.updateId);
2701
3398
  if (readyEntry?.inputClaim?.phase !== "ready")
2702
3399
  return [];
2703
3400
  const releasedEntry = cloneEntry(readyEntry);
2704
3401
  delete releasedEntry.inputClaim;
2705
3402
  const releasedState = advanceInputHeadroomState(readyState, replaceInputHeadroomEntry(readyState.file, entry.updateId, releasedEntry), TELEGRAM_UPDATE_JOURNAL_INPUT_TRANSITION_HEADROOM_BYTES);
2706
3403
  const reacquisition = createInputHeadroomTransitionPlan(releasedState.file, releasedEntry);
2707
- return [...(readyState.file === file ? [] : [readyState]), releasedState, ...reacquisition];
3404
+ return [
3405
+ ...(readyState.file === file ? [] : [readyState]),
3406
+ releasedState,
3407
+ ...reacquisition,
3408
+ ];
2708
3409
  };
2709
3410
  const createInputHeadroomQueueCompletionPlan = (file, entries) => {
2710
- if (entries.length === 0 || entries.some(entry => entry.state !== "queued" || entry.queueHandoff))
3411
+ if (entries.length === 0 ||
3412
+ entries.some((entry) => entry.state !== "queued" || entry.queueHandoff))
2711
3413
  return [];
2712
- const requestedIds = new Set(entries.map(entry => entry.updateId));
3414
+ const requestedIds = new Set(entries.map((entry) => entry.updateId));
2713
3415
  const initial = { file, projectionSlack: 0 };
2714
- return [advanceInputHeadroomState(initial, file.entries.filter(entry => !requestedIds.has(entry.updateId)), TELEGRAM_UPDATE_JOURNAL_INPUT_TRANSITION_HEADROOM_BYTES)];
3416
+ return [
3417
+ advanceInputHeadroomState(initial, file.entries.filter((entry) => !requestedIds.has(entry.updateId)), TELEGRAM_UPDATE_JOURNAL_INPUT_TRANSITION_HEADROOM_BYTES),
3418
+ ];
2715
3419
  };
2716
3420
  const getInputHeadroomTransitionPlans = (file, updateId) => {
2717
3421
  if (updateId !== "all") {
2718
3422
  const requestedIds = new Set(typeof updateId === "number" ? [updateId] : [...updateId]);
2719
- const requestedEntries = file.entries.filter(entry => requestedIds.has(entry.updateId));
3423
+ const requestedEntries = file.entries.filter((entry) => requestedIds.has(entry.updateId));
2720
3424
  const queuedReceiptId = requestedEntries[0]?.queueReceiptId;
2721
- if (requestedEntries.length === requestedIds.size && queuedReceiptId &&
2722
- requestedEntries.every(entry => entry.state === "queued" &&
3425
+ if (requestedEntries.length === requestedIds.size &&
3426
+ queuedReceiptId &&
3427
+ requestedEntries.every((entry) => entry.state === "queued" &&
2723
3428
  entry.queueReceiptId === queuedReceiptId) &&
2724
- file.entries.filter(entry => entry.queueReceiptId === queuedReceiptId).length === requestedIds.size) {
3429
+ file.entries.filter((entry) => entry.queueReceiptId === queuedReceiptId)
3430
+ .length === requestedIds.size) {
2725
3431
  const completion = createInputHeadroomQueueCompletionPlan(file, requestedEntries);
2726
3432
  return completion.length > 0 ? [completion] : [];
2727
3433
  }
@@ -2731,19 +3437,47 @@ function createJournalStoreCore(options, getInputContext) {
2731
3437
  const plan = entry ? createInputHeadroomTransitionPlan(file, entry) : [];
2732
3438
  const cancel = entry ? createInputHeadroomCancelPlan(file, entry) : [];
2733
3439
  const recover = entry?.inputClaim?.handoff
2734
- ? createInputHeadroomReleasePlan(file, entry, true) : [];
2735
- return [plan, cancel, recover].filter(candidate => candidate.length > 0);
3440
+ ? createInputHeadroomReleasePlan(file, entry, true)
3441
+ : [];
3442
+ return [plan, cancel, recover].filter((candidate) => candidate.length > 0);
2736
3443
  }
2737
- const running = smallestInputHeadroomEntry(file.entries.filter(entry => entry.inputClaim?.phase === "running"));
2738
- const offered = largestInputHeadroomEntry(file.entries.filter(entry => entry.state === "pending" && entry.inputClaim?.phase === "ready" && entry.inputClaim.handoff), entry => ({ ...entry, inputClaim: { ...entry.inputClaim, owner: {
2739
- ...entry.inputClaim.handoff.recipientOwner, acquisitionId: inputHeadroomText,
2740
- acquiredAtMs: Number.MAX_SAFE_INTEGER, handoffId: inputHandoffHeadroomText
2741
- }, handoff: undefined } }));
2742
- const ready = largestInputHeadroomEntry(file.entries.filter(entry => entry.state === "pending" && entry.inputClaim?.phase === "ready" && !entry.inputClaim.handoff), entry => ({ ...entry, inputClaim: { ...entry.inputClaim, phase: "running" } }));
2743
- const unclaimed = largestInputHeadroomEntry(file.entries.filter(entry => entry.state === "pending" && entry.preApprovalExcluded === false && !entry.inputClaim), entry => ({ ...entry, inputClaim: createInputHeadroomClaim(entry, "running") }));
2744
- const excluded = smallestInputHeadroomEntry(file.entries.filter(entry => entry.preApprovalExcluded === true));
2745
- const representatives = [running, offered, ready, unclaimed, excluded].filter((entry) => entry !== undefined);
2746
- const plans = representatives.map(entry => createInputHeadroomTransitionPlan(file, entry));
3444
+ const running = smallestInputHeadroomEntry(file.entries.filter((entry) => entry.inputClaim?.phase === "running"));
3445
+ const offered = largestInputHeadroomEntry(file.entries.filter((entry) => entry.state === "pending" &&
3446
+ entry.inputClaim?.phase === "ready" &&
3447
+ entry.inputClaim.handoff), (entry) => ({
3448
+ ...entry,
3449
+ inputClaim: {
3450
+ ...entry.inputClaim,
3451
+ owner: {
3452
+ ...entry.inputClaim.handoff.recipientOwner,
3453
+ acquisitionId: inputHeadroomText,
3454
+ acquiredAtMs: Number.MAX_SAFE_INTEGER,
3455
+ handoffId: inputHandoffHeadroomText,
3456
+ },
3457
+ handoff: undefined,
3458
+ },
3459
+ }));
3460
+ const ready = largestInputHeadroomEntry(file.entries.filter((entry) => entry.state === "pending" &&
3461
+ entry.inputClaim?.phase === "ready" &&
3462
+ !entry.inputClaim.handoff), (entry) => ({
3463
+ ...entry,
3464
+ inputClaim: { ...entry.inputClaim, phase: "running" },
3465
+ }));
3466
+ const unclaimed = largestInputHeadroomEntry(file.entries.filter((entry) => entry.state === "pending" &&
3467
+ entry.preApprovalExcluded === false &&
3468
+ !entry.inputClaim), (entry) => ({
3469
+ ...entry,
3470
+ inputClaim: createInputHeadroomClaim(entry, "running"),
3471
+ }));
3472
+ const excluded = smallestInputHeadroomEntry(file.entries.filter((entry) => entry.preApprovalExcluded === true));
3473
+ const representatives = [
3474
+ running,
3475
+ offered,
3476
+ ready,
3477
+ unclaimed,
3478
+ excluded,
3479
+ ].filter((entry) => entry !== undefined);
3480
+ const plans = representatives.map((entry) => createInputHeadroomTransitionPlan(file, entry));
2747
3481
  if (offered) {
2748
3482
  plans.push(createInputHeadroomCancelPlan(file, offered));
2749
3483
  plans.push(createInputHeadroomReleasePlan(file, offered, true));
@@ -2754,7 +3488,9 @@ function createJournalStoreCore(options, getInputContext) {
2754
3488
  }
2755
3489
  const queuedReceipts = new Map();
2756
3490
  for (const entry of file.entries) {
2757
- if (entry.state !== "queued" || !entry.queueReceiptId || entry.queueHandoff)
3491
+ if (entry.state !== "queued" ||
3492
+ !entry.queueReceiptId ||
3493
+ entry.queueHandoff)
2758
3494
  continue;
2759
3495
  const grouped = queuedReceipts.get(entry.queueReceiptId) ?? [];
2760
3496
  grouped.push(entry);
@@ -2768,14 +3504,23 @@ function createJournalStoreCore(options, getInputContext) {
2768
3504
  return plans;
2769
3505
  };
2770
3506
  const createInputHeadroomSegment = (previous, next) => {
2771
- const previousEntries = new Map(previous.entries.map(entry => [entry.updateId, entry]));
2772
- const nextEntries = new Map(next.entries.map(entry => [entry.updateId, entry]));
2773
- return { version,
2774
- revision: next.revision, previousRevision: next.revision - 1,
2775
- profile: next.profile, botIdentity: next.botIdentity,
2776
- upsertedEntries: next.entries.filter(entry => !previousEntries.has(entry.updateId) || !isDeepStrictEqual(previousEntries.get(entry.updateId), entry)),
2777
- removedUpdateIds: previous.entries.filter(entry => !nextEntries.has(entry.updateId)).map(entry => entry.updateId),
2778
- ...(next.acceptedThroughUpdateId !== undefined ? { acceptedThroughUpdateId: next.acceptedThroughUpdateId } : {}) };
3507
+ const previousEntries = new Map(previous.entries.map((entry) => [entry.updateId, entry]));
3508
+ const nextEntries = new Map(next.entries.map((entry) => [entry.updateId, entry]));
3509
+ return {
3510
+ version,
3511
+ revision: next.revision,
3512
+ previousRevision: next.revision - 1,
3513
+ profile: next.profile,
3514
+ botIdentity: next.botIdentity,
3515
+ upsertedEntries: next.entries.filter((entry) => !previousEntries.has(entry.updateId) ||
3516
+ !isDeepStrictEqual(previousEntries.get(entry.updateId), entry)),
3517
+ removedUpdateIds: previous.entries
3518
+ .filter((entry) => !nextEntries.has(entry.updateId))
3519
+ .map((entry) => entry.updateId),
3520
+ ...(next.acceptedThroughUpdateId !== undefined
3521
+ ? { acceptedThroughUpdateId: next.acceptedThroughUpdateId }
3522
+ : {}),
3523
+ };
2779
3524
  };
2780
3525
  const assertInputHeadroomCapacity = (file, extraBytes) => {
2781
3526
  const bytes = assertCapacity(file);
@@ -2797,46 +3542,74 @@ function createJournalStoreCore(options, getInputContext) {
2797
3542
  ? { ...current.source.evidence.accounting }
2798
3543
  : { files: 1, bytes: publishedBytes, work: getFileWork(publishedFile) };
2799
3544
  if (publishedSegment) {
2800
- const segmentWork = publishedSegment.upsertedEntries.length + publishedSegment.removedUpdateIds.length +
2801
- (publishedSegment.operatorDispositions?.length ?? 0) + (publishedSegment.sourceCompletions?.length ?? 0);
3545
+ const segmentWork = publishedSegment.upsertedEntries.length +
3546
+ publishedSegment.removedUpdateIds.length +
3547
+ (publishedSegment.operatorDispositions?.length ?? 0) +
3548
+ (publishedSegment.sourceCompletions?.length ?? 0);
2802
3549
  const segmentBytes = Buffer.byteLength(`${JSON.stringify(publishedSegment, null, 2)}\n`);
2803
3550
  baseRetainedCount += 1;
2804
3551
  baseRetainedBytes += segmentBytes;
2805
3552
  baseRetainedWork += segmentWork;
2806
- const segmentFirst = { files: baseAccounting.files + 1, bytes: baseAccounting.bytes + segmentBytes,
2807
- work: baseAccounting.work + segmentWork + getFileWork(publishedFile) };
2808
- const compactResidue = { files: 1 + baseRetainedCount,
2809
- bytes: publishedBytes + baseRetainedBytes, work: getFileWork(publishedFile) + baseRetainedWork };
2810
- baseAccounting = { files: Math.max(segmentFirst.files, compactResidue.files),
3553
+ const segmentFirst = {
3554
+ files: baseAccounting.files + 1,
3555
+ bytes: baseAccounting.bytes + segmentBytes,
3556
+ work: baseAccounting.work + segmentWork + getFileWork(publishedFile),
3557
+ };
3558
+ const compactResidue = {
3559
+ files: 1 + baseRetainedCount,
3560
+ bytes: publishedBytes + baseRetainedBytes,
3561
+ work: getFileWork(publishedFile) + baseRetainedWork,
3562
+ };
3563
+ baseAccounting = {
3564
+ files: Math.max(segmentFirst.files, compactResidue.files),
2811
3565
  bytes: Math.max(segmentFirst.bytes, compactResidue.bytes),
2812
- work: Math.max(segmentFirst.work, compactResidue.work) };
3566
+ work: Math.max(segmentFirst.work, compactResidue.work),
3567
+ };
2813
3568
  }
2814
3569
  for (const transitions of plans) {
2815
3570
  let retainedCount = baseRetainedCount;
2816
3571
  let retainedBytes = baseRetainedBytes;
2817
3572
  let retainedWork = baseRetainedWork;
2818
3573
  let accounting = { ...baseAccounting };
2819
- let state = { file: publishedFile, projectionSlack: 0 };
3574
+ let state = {
3575
+ file: publishedFile,
3576
+ projectionSlack: 0,
3577
+ };
2820
3578
  for (const next of transitions) {
2821
3579
  const segment = createInputHeadroomSegment(state.file, next.file);
2822
- const segmentCollections = [segment.upsertedEntries.length, segment.removedUpdateIds.length,
2823
- segment.operatorDispositions?.length ?? 0, next.file.entries.length,
2824
- next.file.operatorDispositions?.length ?? 0];
2825
- const segmentWork = segmentCollections[0] + segmentCollections[1] + segmentCollections[2];
2826
- const segmentBytes = Buffer.byteLength(`${JSON.stringify(segment, null, 2)}\n`) + next.projectionSlack;
3580
+ const segmentCollections = [
3581
+ segment.upsertedEntries.length,
3582
+ segment.removedUpdateIds.length,
3583
+ segment.operatorDispositions?.length ?? 0,
3584
+ next.file.entries.length,
3585
+ next.file.operatorDispositions?.length ?? 0,
3586
+ ];
3587
+ const segmentWork = segmentCollections[0] +
3588
+ segmentCollections[1] +
3589
+ segmentCollections[2];
3590
+ const segmentBytes = Buffer.byteLength(`${JSON.stringify(segment, null, 2)}\n`) +
3591
+ next.projectionSlack;
2827
3592
  const nextBytes = assertInputHeadroomCapacity(next.file, next.projectionSlack);
2828
- const segmentFirst = { files: accounting.files + 1, bytes: accounting.bytes + segmentBytes,
2829
- work: accounting.work + segmentWork + getFileWork(next.file) };
3593
+ const segmentFirst = {
3594
+ files: accounting.files + 1,
3595
+ bytes: accounting.bytes + segmentBytes,
3596
+ work: accounting.work + segmentWork + getFileWork(next.file),
3597
+ };
2830
3598
  assertSourceResources(segmentFirst.files, segmentFirst.bytes, segmentFirst.work, segmentCollections);
2831
3599
  retainedCount += 1;
2832
3600
  retainedBytes += segmentBytes;
2833
3601
  retainedWork += segmentWork;
2834
- const compactResidue = { files: 1 + retainedCount, bytes: nextBytes + retainedBytes,
2835
- work: getFileWork(next.file) + retainedWork };
3602
+ const compactResidue = {
3603
+ files: 1 + retainedCount,
3604
+ bytes: nextBytes + retainedBytes,
3605
+ work: getFileWork(next.file) + retainedWork,
3606
+ };
2836
3607
  assertSourceResources(compactResidue.files, compactResidue.bytes, compactResidue.work, segmentCollections);
2837
- accounting = { files: Math.max(segmentFirst.files, compactResidue.files),
3608
+ accounting = {
3609
+ files: Math.max(segmentFirst.files, compactResidue.files),
2838
3610
  bytes: Math.max(segmentFirst.bytes, compactResidue.bytes),
2839
- work: Math.max(segmentFirst.work, compactResidue.work) };
3611
+ work: Math.max(segmentFirst.work, compactResidue.work),
3612
+ };
2840
3613
  state = next;
2841
3614
  }
2842
3615
  }
@@ -2850,9 +3623,15 @@ function createJournalStoreCore(options, getInputContext) {
2850
3623
  const serializedBytes = assertCapacity(revisedFile, serialized);
2851
3624
  const segmentText = `${JSON.stringify(segment, null, 2)}\n`;
2852
3625
  const segmentBytes = Buffer.byteLength(segmentText);
2853
- const collections = [segment.upsertedEntries.length, segment.removedUpdateIds.length,
2854
- segment.operatorDispositions?.length ?? 0, file.entries.length, file.operatorDispositions?.length ?? 0,
2855
- segment.sourceCompletions?.length ?? 0, file.sourceCompletions?.length ?? 0];
3626
+ const collections = [
3627
+ segment.upsertedEntries.length,
3628
+ segment.removedUpdateIds.length,
3629
+ segment.operatorDispositions?.length ?? 0,
3630
+ file.entries.length,
3631
+ file.operatorDispositions?.length ?? 0,
3632
+ segment.sourceCompletions?.length ?? 0,
3633
+ file.sourceCompletions?.length ?? 0,
3634
+ ];
2856
3635
  const segmentWork = collections[0] + collections[1] + collections[2] + collections[5];
2857
3636
  const fileWork = collections[3] + collections[4] + collections[6];
2858
3637
  const accounting = source.evidence.accounting;
@@ -2860,10 +3639,14 @@ function createJournalStoreCore(options, getInputContext) {
2860
3639
  assertSourceResources(accounting.files + 1, accounting.bytes + segmentBytes, accounting.work + segmentWork + fileWork, collections);
2861
3640
  const unapplied = source.segments.filter((item) => Number(item.name.slice(0, 16)) > source.snapshotRevision);
2862
3641
  const compact = forceCompact ||
2863
- unapplied.length + 1 >= TELEGRAM_UPDATE_JOURNAL_COMPACTION_SEGMENT_COUNT ||
2864
- unapplied.reduce((sum, item) => sum + item.bytes, segmentBytes) >= TELEGRAM_UPDATE_JOURNAL_COMPACTION_SEGMENT_BYTES;
3642
+ unapplied.length + 1 >=
3643
+ TELEGRAM_UPDATE_JOURNAL_COMPACTION_SEGMENT_COUNT ||
3644
+ unapplied.reduce((sum, item) => sum + item.bytes, segmentBytes) >=
3645
+ TELEGRAM_UPDATE_JOURNAL_COMPACTION_SEGMENT_BYTES;
2865
3646
  if (compact) {
2866
- assertSourceResources(accounting.files + 1, serializedBytes + source.segments.reduce((sum, item) => sum + item.bytes, segmentBytes), fileWork + source.segments.reduce((sum, item) => sum + item.work, segmentWork), collections);
3647
+ assertSourceResources(accounting.files + 1, serializedBytes +
3648
+ source.segments.reduce((sum, item) => sum + item.bytes, segmentBytes), fileWork +
3649
+ source.segments.reduce((sum, item) => sum + item.work, segmentWork), collections);
2867
3650
  }
2868
3651
  const segmentPath = getTelegramUpdateJournalSegmentPath(path, segment.revision);
2869
3652
  // A crash may retain staging, so keep it outside the strictly enumerated segment directory.
@@ -2872,9 +3655,13 @@ function createJournalStoreCore(options, getInputContext) {
2872
3655
  writeJournalFile(path, serialized, publicationBoundary);
2873
3656
  // Snapshot scope must not change. Preserve a validated redundant ID witness instead.
2874
3657
  const witness = file.botIdentity.botId === undefined
2875
- ? source.segments.find((item) => item.botId !== undefined)?.name : undefined;
3658
+ ? source.segments.find((item) => item.botId !== undefined)?.name
3659
+ : undefined;
2876
3660
  const segmentDirectory = getTelegramUpdateJournalSegmentDirectory(path);
2877
- const names = [...source.segments.map((item) => item.name), basename(segmentPath)];
3661
+ const names = [
3662
+ ...source.segments.map((item) => item.name),
3663
+ basename(segmentPath),
3664
+ ];
2878
3665
  for (const name of names) {
2879
3666
  if (name === witness)
2880
3667
  continue;
@@ -2888,12 +3675,18 @@ function createJournalStoreCore(options, getInputContext) {
2888
3675
  try {
2889
3676
  rmdirSync(segmentDirectory);
2890
3677
  }
2891
- catch { /* Retained witness or cleanup residue. */ }
3678
+ catch {
3679
+ /* Retained witness or cleanup residue. */
3680
+ }
2892
3681
  }
2893
3682
  return { file: revisedFile, serializedBytes };
2894
3683
  };
2895
- const publishMutation = (current, entries, contentChanged, operatorDispositions = current.file.operatorDispositions, acceptedThroughUpdateId = current.file.acceptedThroughUpdateId, publicationBoundary = onPublicationBoundary, inputHeadroom = journalHasCustodyFields(version) ? "all" : false, sourceCompletions = current.file.sourceCompletions) => {
2896
- const botIdentity = current.source ? current.file.botIdentity : mergeBotIdentity(current.file.botIdentity, expectedIdentity);
3684
+ const publishMutation = (current, entries, contentChanged, operatorDispositions = current.file.operatorDispositions, acceptedThroughUpdateId = current.file.acceptedThroughUpdateId, publicationBoundary = onPublicationBoundary, inputHeadroom = journalHasCustodyFields(version)
3685
+ ? "all"
3686
+ : false, sourceCompletions = current.file.sourceCompletions) => {
3687
+ const botIdentity = current.source
3688
+ ? current.file.botIdentity
3689
+ : mergeBotIdentity(current.file.botIdentity, expectedIdentity);
2897
3690
  if (!contentChanged &&
2898
3691
  isDeepStrictEqual(current.file.botIdentity, botIdentity) &&
2899
3692
  isDeepStrictEqual(current.file.operatorDispositions ?? [], operatorDispositions ?? []) &&
@@ -2909,9 +3702,7 @@ function createJournalStoreCore(options, getInputContext) {
2909
3702
  ...(acceptedThroughUpdateId !== undefined
2910
3703
  ? { acceptedThroughUpdateId }
2911
3704
  : {}),
2912
- ...(operatorDispositions?.length
2913
- ? { operatorDispositions }
2914
- : {}),
3705
+ ...(operatorDispositions?.length ? { operatorDispositions } : {}),
2915
3706
  ...(sourceCompletions?.length ? { sourceCompletions } : {}),
2916
3707
  };
2917
3708
  assertJournalRoutingInputContinuity(current.file, entries, path);
@@ -2928,7 +3719,11 @@ function createJournalStoreCore(options, getInputContext) {
2928
3719
  }
2929
3720
  if (!current.exists) {
2930
3721
  if (current.source) {
2931
- const collections = [file.entries.length, file.operatorDispositions?.length ?? 0, file.sourceCompletions?.length ?? 0];
3722
+ const collections = [
3723
+ file.entries.length,
3724
+ file.operatorDispositions?.length ?? 0,
3725
+ file.sourceCompletions?.length ?? 0,
3726
+ ];
2932
3727
  assertSourceResources(1, serializedBytes, collections[0] + collections[1] + collections[2], collections);
2933
3728
  }
2934
3729
  if (inputHeadroom !== false)
@@ -2937,10 +3732,11 @@ function createJournalStoreCore(options, getInputContext) {
2937
3732
  return { file, serializedBytes };
2938
3733
  }
2939
3734
  const previousEntries = new Map(current.file.entries.map((entry) => [entry.updateId, entry]));
2940
- if (withPairingAdmission && entries.some((entry) => {
2941
- const previous = previousEntries.get(entry.updateId);
2942
- return previous && previous.preApprovalExcluded !== entry.preApprovalExcluded;
2943
- })) {
3735
+ if (withPairingAdmission &&
3736
+ entries.some((entry) => {
3737
+ const previous = previousEntries.get(entry.updateId);
3738
+ return (previous && previous.preApprovalExcluded !== entry.preApprovalExcluded);
3739
+ })) {
2944
3740
  throw createJournalError("pairing-evidence", path, "changes immutable exclusion evidence");
2945
3741
  }
2946
3742
  const nextEntries = new Map(entries.map((entry) => [entry.updateId, entry]));
@@ -2964,7 +3760,9 @@ function createJournalStoreCore(options, getInputContext) {
2964
3760
  ...(!isDeepStrictEqual(current.file.operatorDispositions ?? [], operatorDispositions ?? [])
2965
3761
  ? { operatorDispositions: operatorDispositions ?? [] }
2966
3762
  : {}),
2967
- ...(!isDeepStrictEqual(current.file.sourceCompletions ?? [], sourceCompletions ?? []) ? { sourceCompletions: sourceCompletions ?? [] } : {}),
3763
+ ...(!isDeepStrictEqual(current.file.sourceCompletions ?? [], sourceCompletions ?? [])
3764
+ ? { sourceCompletions: sourceCompletions ?? [] }
3765
+ : {}),
2968
3766
  };
2969
3767
  if (current.source) {
2970
3768
  if (!isSafePositiveInteger(revision))
@@ -2980,7 +3778,8 @@ function createJournalStoreCore(options, getInputContext) {
2980
3778
  const segmentNames = readdirSync(segmentDirectory).filter((name) => /^\d{16}\.json$/u.test(name));
2981
3779
  let snapshotRevision = 0;
2982
3780
  try {
2983
- snapshotRevision = parseJournalFile(JSON.parse(readFileSync(path, "utf8")), path, version).revision ?? 0;
3781
+ snapshotRevision =
3782
+ parseJournalFile(JSON.parse(readFileSync(path, "utf8")), path, version).revision ?? 0;
2984
3783
  }
2985
3784
  catch (error) {
2986
3785
  if (error?.code !== "ENOENT")
@@ -3015,11 +3814,18 @@ function createJournalStoreCore(options, getInputContext) {
3015
3814
  }
3016
3815
  return { file: revisedFile, serializedBytes };
3017
3816
  };
3018
- const removeCompleted = (updateIds, expectedSources, exact = false, completionInput) => runMutation((readCurrent) => {
3817
+ const removeCompleted = (updateIds, expectedSources, exact = false, completionInput, isCurrent) => runMutation((readCurrent) => {
3818
+ const assertCurrent = () => {
3819
+ if (isCurrent && !isCurrent())
3820
+ throw createJournalError("conflict", path, "exact removal authority ended");
3821
+ };
3822
+ assertCurrent();
3019
3823
  const current = exact && !sourceAccess ? readCurrentStrict(false) : readCurrent();
3020
3824
  if (completionInput !== undefined && !sourceAccess)
3021
3825
  throw createJournalError("invalid", path, "source completion requires an exact source handle");
3022
- const completions = completionInput === undefined ? undefined : validateJournalSourceCompletions(completionInput, path, version);
3826
+ const completions = completionInput === undefined
3827
+ ? undefined
3828
+ : validateJournalSourceCompletions(completionInput, path, version);
3023
3829
  if (exact && expectedSources === undefined)
3024
3830
  throw createJournalError("invalid", path, "received no exact completion sources");
3025
3831
  const requestedIds = new Set();
@@ -3033,133 +3839,221 @@ function createJournalStoreCore(options, getInputContext) {
3033
3839
  throw createJournalError("invalid", path, "received no exact completion sources");
3034
3840
  const expectedIds = new Set();
3035
3841
  for (const expected of expectedSources) {
3036
- if (!isRecord(expected) || Object.keys(expected).some(key => !["updateId", "sourceSha256"].includes(key)) ||
3037
- !isSafeNonNegativeInteger(expected.updateId) || !requestedIds.has(expected.updateId) || expectedIds.has(expected.updateId) ||
3038
- typeof expected.sourceSha256 !== "string" || !/^[a-f0-9]{64}$/u.test(expected.sourceSha256)) {
3842
+ if (!isRecord(expected) ||
3843
+ Object.keys(expected).some((key) => !["updateId", "sourceSha256"].includes(key)) ||
3844
+ !isSafeNonNegativeInteger(expected.updateId) ||
3845
+ !requestedIds.has(expected.updateId) ||
3846
+ expectedIds.has(expected.updateId) ||
3847
+ typeof expected.sourceSha256 !== "string" ||
3848
+ !/^[a-f0-9]{64}$/u.test(expected.sourceSha256)) {
3039
3849
  throw createJournalError("invalid", path, "received an invalid exact completion source");
3040
3850
  }
3041
3851
  expectedIds.add(expected.updateId);
3042
- const entry = current.file.entries.find(value => value.updateId === expected.updateId);
3043
- if (!entry || createTelegramUpdateJournalEntryDigest(entry).sourceSha256 !== expected.sourceSha256) {
3852
+ const entry = current.file.entries.find((value) => value.updateId === expected.updateId);
3853
+ if (!entry ||
3854
+ createTelegramUpdateJournalEntryDigest(entry).sourceSha256 !==
3855
+ expected.sourceSha256) {
3044
3856
  throw createJournalError("conflict", path, "exact completion source changed or disappeared");
3045
3857
  }
3046
3858
  }
3047
3859
  }
3048
3860
  if (completions) {
3049
- const guards = new Map((expectedSources ?? []).map(source => [source.updateId, source.sourceSha256]));
3050
- if (completions.some(completion => guards.get(completion.updateId) !== completion.sourceSha256)) {
3861
+ const guards = new Map((expectedSources ?? []).map((source) => [
3862
+ source.updateId,
3863
+ source.sourceSha256,
3864
+ ]));
3865
+ if (completions.some((completion) => guards.get(completion.updateId) !== completion.sourceSha256)) {
3051
3866
  throw createJournalError("invalid", path, "source completion has no matching exact guard");
3052
3867
  }
3053
3868
  }
3054
- const requestedEntries = current.file.entries.filter(entry => requestedIds.has(entry.updateId));
3055
- const protectedEntry = requestedEntries.find(entry => entry.state === "failed" || entry.state === "queued");
3869
+ const requestedEntries = current.file.entries.filter((entry) => requestedIds.has(entry.updateId));
3870
+ const protectedEntry = requestedEntries.find((entry) => entry.state === "failed" || entry.state === "queued");
3056
3871
  if (protectedEntry)
3057
3872
  throw createJournalError("conflict", path, protectedEntry.state === "failed"
3058
3873
  ? `cannot complete terminal update ${protectedEntry.updateId} without an operator disposition`
3059
3874
  : `cannot complete queued update ${protectedEntry.updateId} without its exact owner receipt`);
3060
- const removedUpdateIds = requestedEntries.map(entry => entry.updateId);
3875
+ const removedUpdateIds = requestedEntries.map((entry) => entry.updateId);
3061
3876
  const sourceCompletions = completions
3062
- ? [...(current.file.sourceCompletions ?? []), ...completions].sort((a, b) => a.updateId - b.updateId) : current.file.sourceCompletions;
3877
+ ? [...(current.file.sourceCompletions ?? []), ...completions].sort((a, b) => a.updateId - b.updateId)
3878
+ : current.file.sourceCompletions;
3063
3879
  if (sourceCompletions?.length)
3064
3880
  validateJournalSourceCompletions(sourceCompletions, path, version);
3065
- const published = publishMutation(current, current.file.entries.filter(entry => !requestedIds.has(entry.updateId)), removedUpdateIds.length > 0, current.file.operatorDispositions, current.file.acceptedThroughUpdateId, onPublicationBoundary, false, sourceCompletions);
3066
- return { removedUpdateIds, entryCount: published.file.entries.length, serializedBytes: published.serializedBytes,
3067
- ...(completions ? { sourceCompletions: completions.map(completion => ({ ...completion })) } : {}) };
3881
+ assertCurrent();
3882
+ const published = publishMutation(current, current.file.entries.filter((entry) => !requestedIds.has(entry.updateId)), removedUpdateIds.length > 0, current.file.operatorDispositions, current.file.acceptedThroughUpdateId, isCurrent
3883
+ ? (boundary, target) => {
3884
+ onPublicationBoundary?.(boundary, target);
3885
+ assertCurrent();
3886
+ }
3887
+ : onPublicationBoundary, false, sourceCompletions);
3888
+ assertCurrent();
3889
+ return {
3890
+ removedUpdateIds,
3891
+ entryCount: published.file.entries.length,
3892
+ serializedBytes: published.serializedBytes,
3893
+ ...(completions
3894
+ ? {
3895
+ sourceCompletions: completions.map((completion) => ({
3896
+ ...completion,
3897
+ })),
3898
+ }
3899
+ : {}),
3900
+ };
3068
3901
  });
3069
3902
  const completeQueued = (receipts, completionInput, exact = false) => runMutation((readCurrent) => {
3070
3903
  if (exact && !sourceAccess)
3071
3904
  throw createJournalError("invalid", path, "source completion requires an exact source handle");
3072
- const completions = exact ? validateJournalSourceCompletions(completionInput, path, version) : undefined;
3905
+ const completions = exact
3906
+ ? validateJournalSourceCompletions(completionInput, path, version)
3907
+ : undefined;
3073
3908
  if (!Array.isArray(receipts) || receipts.length === 0)
3074
3909
  throw createJournalError("invalid", path, "received no queued receipts to complete");
3075
3910
  const current = readCurrent();
3076
3911
  const receiptIds = new Set(), requestedUpdateIds = new Set();
3077
3912
  for (const receipt of receipts) {
3078
- if ((receipt.queueKind !== "prompt" && receipt.queueKind !== "control") || !isNonEmptyString(receipt.receiptId) ||
3079
- receiptIds.has(receipt.receiptId) || !Array.isArray(receipt.sourceUpdateIds) || receipt.sourceUpdateIds.length === 0) {
3913
+ if ((receipt.queueKind !== "prompt" && receipt.queueKind !== "control") ||
3914
+ !isNonEmptyString(receipt.receiptId) ||
3915
+ receiptIds.has(receipt.receiptId) ||
3916
+ !Array.isArray(receipt.sourceUpdateIds) ||
3917
+ receipt.sourceUpdateIds.length === 0) {
3080
3918
  throw createJournalError("invalid", path, "received an invalid queued completion receipt");
3081
3919
  }
3082
3920
  receiptIds.add(receipt.receiptId);
3083
3921
  const queueOwner = validateJournalQueueOwner(receipt.queueOwner, path);
3084
- if (queueRuntimeIdentity && (queueOwner.instanceId !== queueRuntimeIdentity.instanceId || queueOwner.processId !== queueRuntimeIdentity.processId ||
3085
- queueOwner.processBirthId !== queueRuntimeIdentity.processBirthId)) {
3922
+ if (queueRuntimeIdentity &&
3923
+ (queueOwner.instanceId !== queueRuntimeIdentity.instanceId ||
3924
+ queueOwner.processId !== queueRuntimeIdentity.processId ||
3925
+ queueOwner.processBirthId !== queueRuntimeIdentity.processBirthId)) {
3086
3926
  throw createJournalError("conflict", path, `cannot complete foreign queue receipt ${receipt.receiptId}`);
3087
3927
  }
3088
3928
  const sourceUpdateIds = new Set();
3089
3929
  for (const updateId of receipt.sourceUpdateIds) {
3090
- if (!isSafeNonNegativeInteger(updateId) || sourceUpdateIds.has(updateId) || requestedUpdateIds.has(updateId)) {
3930
+ if (!isSafeNonNegativeInteger(updateId) ||
3931
+ sourceUpdateIds.has(updateId) ||
3932
+ requestedUpdateIds.has(updateId)) {
3091
3933
  throw createJournalError("invalid", path, "received overlapping queued completion update ids");
3092
3934
  }
3093
3935
  sourceUpdateIds.add(updateId);
3094
3936
  requestedUpdateIds.add(updateId);
3095
3937
  }
3096
- const persistedReceiptEntries = current.file.entries.filter(entry => entry.queueReceiptId === receipt.receiptId);
3097
- if (persistedReceiptEntries.length !== sourceUpdateIds.size || persistedReceiptEntries.some(entry => !sourceUpdateIds.has(entry.updateId) || entry.state !== "queued" || entry.queueKind !== receipt.queueKind || !entry.queueOwner ||
3098
- entry.queueHandoff !== undefined || !areTelegramUpdateJournalQueueOwnersEqual(entry.queueOwner, queueOwner))) {
3938
+ const persistedReceiptEntries = current.file.entries.filter((entry) => entry.queueReceiptId === receipt.receiptId);
3939
+ if (persistedReceiptEntries.length !== sourceUpdateIds.size ||
3940
+ persistedReceiptEntries.some((entry) => !sourceUpdateIds.has(entry.updateId) ||
3941
+ entry.state !== "queued" ||
3942
+ entry.queueKind !== receipt.queueKind ||
3943
+ !entry.queueOwner ||
3944
+ entry.queueHandoff !== undefined ||
3945
+ !areTelegramUpdateJournalQueueOwnersEqual(entry.queueOwner, queueOwner))) {
3099
3946
  throw createJournalError("conflict", path, `cannot complete stale or foreign queue receipt ${receipt.receiptId}`);
3100
3947
  }
3101
3948
  }
3102
- const removedEntries = current.file.entries.filter(entry => requestedUpdateIds.has(entry.updateId));
3103
- const removedUpdateIds = removedEntries.map(entry => entry.updateId);
3949
+ const removedEntries = current.file.entries.filter((entry) => requestedUpdateIds.has(entry.updateId));
3950
+ const removedUpdateIds = removedEntries.map((entry) => entry.updateId);
3104
3951
  if (removedUpdateIds.length !== requestedUpdateIds.size)
3105
3952
  throw createJournalError("conflict", path, "queued completion did not resolve every source update");
3106
3953
  if (completions) {
3107
- const sources = new Map(removedEntries.map(entry => [entry.updateId, createTelegramUpdateJournalEntryDigest(entry).sourceSha256]));
3108
- if (completions.some(completion => sources.get(completion.updateId) !== completion.sourceSha256)) {
3954
+ const sources = new Map(removedEntries.map((entry) => [
3955
+ entry.updateId,
3956
+ createTelegramUpdateJournalEntryDigest(entry).sourceSha256,
3957
+ ]));
3958
+ if (completions.some((completion) => sources.get(completion.updateId) !== completion.sourceSha256)) {
3109
3959
  throw createJournalError("conflict", path, "queued completion source changed or belongs to another receipt");
3110
3960
  }
3111
3961
  }
3112
- const sourceCompletions = completions ? [...(current.file.sourceCompletions ?? []), ...completions].sort((a, b) => a.updateId - b.updateId)
3962
+ const sourceCompletions = completions
3963
+ ? [...(current.file.sourceCompletions ?? []), ...completions].sort((a, b) => a.updateId - b.updateId)
3113
3964
  : current.file.sourceCompletions;
3114
3965
  if (sourceCompletions?.length)
3115
3966
  validateJournalSourceCompletions(sourceCompletions, path, version);
3116
- const published = publishMutation(current, current.file.entries.filter(entry => !requestedUpdateIds.has(entry.updateId)), true, current.file.operatorDispositions, current.file.acceptedThroughUpdateId, onPublicationBoundary, exact ? false : undefined, sourceCompletions);
3117
- return { removedUpdateIds, entryCount: published.file.entries.length, serializedBytes: published.serializedBytes,
3118
- ...(completions ? { sourceCompletions: completions.map(completion => ({ ...completion })) } : {}) };
3967
+ const published = publishMutation(current, current.file.entries.filter((entry) => !requestedUpdateIds.has(entry.updateId)), true, current.file.operatorDispositions, current.file.acceptedThroughUpdateId, onPublicationBoundary, exact ? false : undefined, sourceCompletions);
3968
+ return {
3969
+ removedUpdateIds,
3970
+ entryCount: published.file.entries.length,
3971
+ serializedBytes: published.serializedBytes,
3972
+ ...(completions
3973
+ ? {
3974
+ sourceCompletions: completions.map((completion) => ({
3975
+ ...completion,
3976
+ })),
3977
+ }
3978
+ : {}),
3979
+ };
3119
3980
  });
3120
- const mutateRoutingInputs = (input, select, publishedAtMs) => runMutation(readCurrent => {
3981
+ const mutateRoutingInputs = (input, select, publishedAtMs, chooser) => runMutation((readCurrent) => {
3121
3982
  if (!isTelegramUpdateJournalLegacyFamilyVersion(version))
3122
3983
  throw createJournalError("unsupported-version", path, "routing input lifetime requires a private v1 source");
3123
- const binding = createTelegramUpdateJournalBindingKey({ path, profileName: profile, botIdentity: expectedIdentity });
3124
- if (input.journalBindingKey !== binding || !isSafePositiveInteger(input.operatorUserId) ||
3125
- typeof input.isCurrent !== "function" || !Array.isArray(input.entries) || !input.entries.length || input.entries.length > maxEntries) {
3984
+ const binding = createTelegramUpdateJournalBindingKey({
3985
+ path,
3986
+ profileName: profile,
3987
+ botIdentity: expectedIdentity,
3988
+ });
3989
+ if (input.journalBindingKey !== binding ||
3990
+ !isSafePositiveInteger(input.operatorUserId) ||
3991
+ typeof input.isCurrent !== "function" ||
3992
+ !Array.isArray(input.entries) ||
3993
+ !input.entries.length ||
3994
+ input.entries.length > maxEntries) {
3126
3995
  throw createJournalError("invalid", path, "invalid routing input lifetime authority");
3127
3996
  }
3128
- const assertCurrent = () => { if (!input.isCurrent())
3129
- throw createJournalError("conflict", path, "routing input lifetime authority ended"); };
3997
+ const assertCurrent = () => {
3998
+ if (!input.isCurrent())
3999
+ throw createJournalError("conflict", path, "routing input lifetime authority ended");
4000
+ };
3130
4001
  assertCurrent();
3131
- const expected = input.entries.map(entry => validateJournalEntry(entry, path, version));
3132
- if (new Set(expected.map(entry => entry.updateId)).size !== expected.length)
4002
+ const expected = input.entries.map((entry) => validateJournalEntry(entry, path, version));
4003
+ if (new Set(expected.map((entry) => entry.updateId)).size !==
4004
+ expected.length)
3133
4005
  throw createJournalError("invalid", path, "duplicate routing input sources");
3134
4006
  const current = sourceAccess ? readCurrent() : readCurrentStrict(false);
3135
4007
  const nowMs = select ? getNowMs() : publishedAtMs;
3136
- if (!isSafeNonNegativeInteger(nowMs) || !Number.isSafeInteger(nowMs + TELEGRAM_ROUTING_INPUT_TTL_MS) || (!select && nowMs > getNowMs()))
4008
+ if (!isSafeNonNegativeInteger(nowMs) ||
4009
+ !Number.isSafeInteger(nowMs + TELEGRAM_ROUTING_INPUT_TTL_MS) ||
4010
+ (!select && nowMs > getNowMs()))
3137
4011
  throw createJournalError("invalid", path, "invalid routing input lifetime clock");
3138
4012
  let issued = false;
3139
4013
  const changed = new Map();
3140
- const result = expected.map(original => {
3141
- const entry = current.file.entries.find(value => value.updateId === original.updateId);
3142
- if (!entry || entry.state !== "pending" || original.state !== "pending" || entry.inputClaim || entry.inputProvenance || entry.queueOwner) {
4014
+ const result = expected.map((original) => {
4015
+ const entry = current.file.entries.find((value) => value.updateId === original.updateId);
4016
+ if (!entry ||
4017
+ entry.state !== "pending" ||
4018
+ original.state !== "pending" ||
4019
+ entry.inputClaim ||
4020
+ entry.inputProvenance ||
4021
+ entry.queueOwner) {
3143
4022
  throw createJournalError("conflict", path, "routing input source is not unclaimed pending work");
3144
4023
  }
3145
- const withoutLifetime = (value) => { const copy = cloneEntry(value); delete copy.routingInput; return copy; };
3146
- if (!isDeepStrictEqual(entry, original) && (!isDeepStrictEqual(withoutLifetime(entry), withoutLifetime(original)) ||
3147
- (original.routingInput && !isDeepStrictEqual(original.routingInput, entry.routingInput)))) {
4024
+ const withoutLifetime = (value) => {
4025
+ const copy = cloneEntry(value);
4026
+ delete copy.routingInput;
4027
+ return copy;
4028
+ };
4029
+ if (!isDeepStrictEqual(entry, original) &&
4030
+ (!isDeepStrictEqual(withoutLifetime(entry), withoutLifetime(original)) ||
4031
+ (original.routingInput &&
4032
+ !isDeepStrictEqual(original.routingInput, entry.routingInput)))) {
3148
4033
  throw createJournalError("conflict", path, "routing input source changed");
3149
4034
  }
3150
4035
  const lifetime = entry.routingInput;
3151
4036
  if (lifetime && lifetime.operatorUserId !== input.operatorUserId)
3152
4037
  throw createJournalError("conflict", path, "routing input belongs to another operator");
3153
- if (createPendingRetentionReference({ path, journalBindingKey: binding, entry, maxBytes }).read()) {
4038
+ if (createPendingRetentionReference({
4039
+ path,
4040
+ journalBindingKey: binding,
4041
+ entry,
4042
+ maxBytes,
4043
+ }).read()) {
3154
4044
  throw createJournalError("conflict", path, "routing input has protected retention");
3155
4045
  }
3156
4046
  if (select) {
3157
- if (!lifetime || (lifetime.phase === "waiting" && nowMs >= lifetime.expiresAtMs))
4047
+ if (!lifetime ||
4048
+ (lifetime.phase === "waiting" && nowMs >= lifetime.expiresAtMs))
3158
4049
  throw createJournalError("conflict", path, "routing input choice expired or was not armed");
3159
4050
  if (lifetime.phase === "selected")
3160
4051
  return cloneEntry(entry);
3161
4052
  issued = true;
3162
- const selected = { ...cloneEntry(entry), routingInput: { ...lifetime, phase: "selected" } };
4053
+ const selected = {
4054
+ ...cloneEntry(entry),
4055
+ routingInput: { ...lifetime, phase: "selected" },
4056
+ };
3163
4057
  changed.set(entry.updateId, selected);
3164
4058
  return cloneEntry(selected);
3165
4059
  }
@@ -3171,16 +4065,29 @@ function createJournalStoreCore(options, getInputContext) {
3171
4065
  if (nowMs < entry.admittedAtMs)
3172
4066
  throw createJournalError("conflict", path, "routing input publication predates admission");
3173
4067
  issued = true;
3174
- const armed = { ...cloneEntry(entry), routingInput: { operatorUserId: input.operatorUserId,
3175
- publishedAtMs: nowMs, expiresAtMs: nowMs + TELEGRAM_ROUTING_INPUT_TTL_MS, phase: "waiting" } };
4068
+ const armed = {
4069
+ ...cloneEntry(entry),
4070
+ routingInput: {
4071
+ operatorUserId: input.operatorUserId,
4072
+ publishedAtMs: nowMs,
4073
+ expiresAtMs: nowMs + TELEGRAM_ROUTING_INPUT_TTL_MS,
4074
+ phase: "waiting",
4075
+ ...(chooser ? { chooser: { ...chooser } } : {}),
4076
+ },
4077
+ };
3176
4078
  changed.set(entry.updateId, armed);
3177
4079
  return cloneEntry(armed);
3178
4080
  });
3179
4081
  assertCurrent();
3180
- if (select && issued && result.some(entry => !changed.has(entry.updateId)))
4082
+ if (select &&
4083
+ issued &&
4084
+ result.some((entry) => !changed.has(entry.updateId)))
3181
4085
  throw createJournalError("conflict", path, "routing input selection has mixed prior phases");
3182
4086
  if (changed.size)
3183
- publishMutation(current, current.file.entries.map(entry => changed.get(entry.updateId) ?? entry), true, current.file.operatorDispositions, current.file.acceptedThroughUpdateId, (boundary, target) => { onPublicationBoundary?.(boundary, target); assertCurrent(); });
4087
+ publishMutation(current, current.file.entries.map((entry) => changed.get(entry.updateId) ?? entry), true, current.file.operatorDispositions, current.file.acceptedThroughUpdateId, (boundary, target) => {
4088
+ onPublicationBoundary?.(boundary, target);
4089
+ assertCurrent();
4090
+ });
3184
4091
  assertCurrent();
3185
4092
  return { issued, entries: result };
3186
4093
  });
@@ -3212,7 +4119,9 @@ function createJournalStoreCore(options, getInputContext) {
3212
4119
  normalizedUpdates.push(normalized);
3213
4120
  }
3214
4121
  const appendWithEvidence = (preApprovalExcluded) => runJournalTransaction((readCurrent) => {
3215
- if (withPairingAdmission && (typeof preApprovalExcluded !== "boolean" || requestedAcceptedThroughUpdateId === undefined)) {
4122
+ if (withPairingAdmission &&
4123
+ (typeof preApprovalExcluded !== "boolean" ||
4124
+ requestedAcceptedThroughUpdateId === undefined)) {
3216
4125
  throw createJournalError("pairing-evidence", path, "requires exclusion evidence and an admission cursor");
3217
4126
  }
3218
4127
  if (requestedAcceptedThroughUpdateId !== undefined &&
@@ -3226,7 +4135,7 @@ function createJournalStoreCore(options, getInputContext) {
3226
4135
  ...(current.file.operatorDispositions ?? [])
3227
4136
  .filter((disposition) => disposition.action === "discard")
3228
4137
  .map((disposition) => disposition.updateId),
3229
- ...(current.file.sourceCompletions ?? []).map(completion => completion.updateId),
4138
+ ...(current.file.sourceCompletions ?? []).map((completion) => completion.updateId),
3230
4139
  ]);
3231
4140
  let admittedAtMs;
3232
4141
  const addedUpdateIds = [];
@@ -3244,7 +4153,9 @@ function createJournalStoreCore(options, getInputContext) {
3244
4153
  duplicateUpdateIds.push(update.update_id);
3245
4154
  continue;
3246
4155
  }
3247
- if (withPairingAdmission && previousAcceptedThroughUpdateId !== undefined && update.update_id <= previousAcceptedThroughUpdateId) {
4156
+ if (withPairingAdmission &&
4157
+ previousAcceptedThroughUpdateId !== undefined &&
4158
+ update.update_id <= previousAcceptedThroughUpdateId) {
3248
4159
  duplicateUpdateIds.push(update.update_id);
3249
4160
  continue;
3250
4161
  }
@@ -3257,7 +4168,9 @@ function createJournalStoreCore(options, getInputContext) {
3257
4168
  const entry = {
3258
4169
  updateId: update.update_id,
3259
4170
  update,
3260
- ...(withPairingAdmission ? { preApprovalExcluded: preApprovalExcluded } : {}),
4171
+ ...(withPairingAdmission
4172
+ ? { preApprovalExcluded: preApprovalExcluded }
4173
+ : {}),
3261
4174
  admittedAtMs,
3262
4175
  state: "pending",
3263
4176
  };
@@ -3283,7 +4196,9 @@ function createJournalStoreCore(options, getInputContext) {
3283
4196
  return {
3284
4197
  nonExcludedUpdateIds: normalizedUpdates.flatMap((update) => {
3285
4198
  const entry = entriesById.get(update.update_id);
3286
- return entry && entry.preApprovalExcluded !== true ? [entry.updateId] : [];
4199
+ return entry && entry.preApprovalExcluded !== true
4200
+ ? [entry.updateId]
4201
+ : [];
3287
4202
  }),
3288
4203
  addedUpdateIds,
3289
4204
  duplicateUpdateIds,
@@ -3321,12 +4236,12 @@ function createJournalStoreCore(options, getInputContext) {
3321
4236
  });
3322
4237
  };
3323
4238
  return options.withWriterAdmission
3324
- ? options.withWriterAdmission(admittedAppend) : admittedAppend();
4239
+ ? options.withWriterAdmission(admittedAppend)
4240
+ : admittedAppend();
3325
4241
  },
3326
4242
  markQueued(receipt) {
3327
4243
  return runMutation((readCurrent) => {
3328
- if ((receipt.queueKind !== "prompt" &&
3329
- receipt.queueKind !== "control") ||
4244
+ if ((receipt.queueKind !== "prompt" && receipt.queueKind !== "control") ||
3330
4245
  !isNonEmptyString(receipt.receiptId) ||
3331
4246
  !Array.isArray(receipt.sourceUpdateIds) ||
3332
4247
  receipt.sourceUpdateIds.length === 0) {
@@ -3400,7 +4315,12 @@ function createJournalStoreCore(options, getInputContext) {
3400
4315
  updateId: entry.updateId,
3401
4316
  update: entry.update,
3402
4317
  admittedAtMs: entry.admittedAtMs,
3403
- ...(entry.preApprovalExcluded !== undefined ? { preApprovalExcluded: entry.preApprovalExcluded } : {}),
4318
+ ...(entry.preApprovalExcluded !== undefined
4319
+ ? { preApprovalExcluded: entry.preApprovalExcluded }
4320
+ : {}),
4321
+ ...(entry.routingInput
4322
+ ? { routingInput: { ...entry.routingInput } }
4323
+ : {}),
3404
4324
  state: "queued",
3405
4325
  queueKind: receipt.queueKind,
3406
4326
  queueReceiptId: receipt.receiptId,
@@ -3430,8 +4350,7 @@ function createJournalStoreCore(options, getInputContext) {
3430
4350
  !isSafeNonNegativeInteger(input.failedAtMs) ||
3431
4351
  !isBoundedString(input.failureClass, TELEGRAM_UPDATE_JOURNAL_FAILURE_CLASS_MAX_LENGTH) ||
3432
4352
  !isBoundedString(input.summary, TELEGRAM_UPDATE_JOURNAL_FAILURE_SUMMARY_MAX_LENGTH) ||
3433
- (input.disposition !== "retry-wait" &&
3434
- input.disposition !== "failed")) {
4353
+ (input.disposition !== "retry-wait" && input.disposition !== "failed")) {
3435
4354
  throw createJournalError("invalid", path, "received invalid execution failure metadata");
3436
4355
  }
3437
4356
  if (input.disposition === "retry-wait" &&
@@ -3477,8 +4396,12 @@ function createJournalStoreCore(options, getInputContext) {
3477
4396
  updateId: entry.updateId,
3478
4397
  update: entry.update,
3479
4398
  admittedAtMs: entry.admittedAtMs,
3480
- ...(entry.routingInput ? { routingInput: { ...entry.routingInput } } : {}),
3481
- ...(entry.preApprovalExcluded !== undefined ? { preApprovalExcluded: entry.preApprovalExcluded } : {}),
4399
+ ...(entry.routingInput
4400
+ ? { routingInput: { ...entry.routingInput } }
4401
+ : {}),
4402
+ ...(entry.preApprovalExcluded !== undefined
4403
+ ? { preApprovalExcluded: entry.preApprovalExcluded }
4404
+ : {}),
3482
4405
  state: input.disposition,
3483
4406
  failure,
3484
4407
  ...(input.disposition === "retry-wait"
@@ -3548,14 +4471,19 @@ function createJournalStoreCore(options, getInputContext) {
3548
4471
  updateId: entry.updateId,
3549
4472
  update: entry.update,
3550
4473
  admittedAtMs: entry.admittedAtMs,
3551
- ...(entry.preApprovalExcluded !== undefined ? { preApprovalExcluded: entry.preApprovalExcluded } : {}),
4474
+ ...(entry.preApprovalExcluded !== undefined
4475
+ ? { preApprovalExcluded: entry.preApprovalExcluded }
4476
+ : {}),
3552
4477
  state: "retry-wait",
3553
4478
  failure: entry.failure,
3554
4479
  nextRetryAtMs: disposition.committedAtMs,
3555
4480
  }
3556
4481
  : candidate)
3557
4482
  : current.file.entries.filter((candidate) => candidate.updateId !== entry.updateId);
3558
- const published = publishMutation(current, nextEntries, true, [...(current.file.operatorDispositions ?? []), disposition]);
4483
+ const published = publishMutation(current, nextEntries, true, [
4484
+ ...(current.file.operatorDispositions ?? []),
4485
+ disposition,
4486
+ ]);
3559
4487
  return {
3560
4488
  disposition: { ...disposition },
3561
4489
  duplicate: false,
@@ -3569,30 +4497,44 @@ function createJournalStoreCore(options, getInputContext) {
3569
4497
  const current = readCurrent();
3570
4498
  if (!journalHasCustodyFields(current.file.version))
3571
4499
  throw createJournalError("conflict", path, "legacy custody disposition requires a custody journal");
3572
- const existing = current.file.operatorDispositions?.find(candidate => candidate.failureId === authority.dispositionId);
4500
+ const existing = current.file.operatorDispositions?.find((candidate) => candidate.failureId === authority.dispositionId);
3573
4501
  if (existing) {
3574
- if (!("dispositionKind" in existing) || existing.dispositionKind !== "legacy-custody")
4502
+ if (!("dispositionKind" in existing) ||
4503
+ existing.dispositionKind !== "legacy-custody")
3575
4504
  throw createJournalError("conflict", path, `legacy custody disposition ${authority.dispositionId} already has another authority`);
3576
- const normalizedDuplicate = normalizeTelegramUpdateJournalLegacyCustodyDispositionAuthority(authority, { updateId: existing.updateId, state: "retry-wait", attemptCount: 1,
3577
- failedAtMs: 0, failureClass: "retained-audit", summary: "retained-audit",
3578
- evidenceSha256: existing.evidenceSha256 });
3579
- if (!normalizedDuplicate || existing.action !== normalizedDuplicate.action ||
3580
- existing.operatorAuthorityId !== normalizedDuplicate.operatorAuthorityId ||
4505
+ const normalizedDuplicate = normalizeTelegramUpdateJournalLegacyCustodyDispositionAuthority(authority, {
4506
+ updateId: existing.updateId,
4507
+ state: "retry-wait",
4508
+ attemptCount: 1,
4509
+ failedAtMs: 0,
4510
+ failureClass: "retained-audit",
4511
+ summary: "retained-audit",
4512
+ evidenceSha256: existing.evidenceSha256,
4513
+ });
4514
+ if (!normalizedDuplicate ||
4515
+ existing.action !== normalizedDuplicate.action ||
4516
+ existing.operatorAuthorityId !==
4517
+ normalizedDuplicate.operatorAuthorityId ||
3581
4518
  existing.authorizedAtMs !== normalizedDuplicate.authorizedAtMs)
3582
4519
  throw createJournalError("conflict", path, `legacy custody disposition ${authority.dispositionId} already has another authority`);
3583
4520
  let authorized = false;
3584
4521
  try {
3585
- authorized = options.authorizeLegacyCustodyDisposition?.(normalizedDuplicate) === true;
4522
+ authorized =
4523
+ options.authorizeLegacyCustodyDisposition?.(normalizedDuplicate) === true;
3586
4524
  }
3587
4525
  catch {
3588
4526
  authorized = false;
3589
4527
  }
3590
4528
  if (!authorized)
3591
4529
  throw createJournalError("conflict", path, "legacy custody disposition is unauthorized");
3592
- return { disposition: { ...existing }, duplicate: true,
3593
- entryCount: current.file.entries.length, serializedBytes: current.serializedBytes };
4530
+ return {
4531
+ disposition: { ...existing },
4532
+ duplicate: true,
4533
+ entryCount: current.file.entries.length,
4534
+ serializedBytes: current.serializedBytes,
4535
+ };
3594
4536
  }
3595
- const entry = current.file.entries.find(candidate => candidate.updateId === authority.updateId);
4537
+ const entry = current.file.entries.find((candidate) => candidate.updateId === authority.updateId);
3596
4538
  const evidence = entry && createTelegramUpdateJournalLegacyCustodyEvidence(entry);
3597
4539
  if (!evidence)
3598
4540
  throw createJournalError("conflict", path, `cannot dispose non-quarantined update ${authority.updateId}`);
@@ -3601,7 +4543,8 @@ function createJournalStoreCore(options, getInputContext) {
3601
4543
  throw createJournalError("conflict", path, `legacy custody evidence changed for update ${authority.updateId}`);
3602
4544
  let authorized = false;
3603
4545
  try {
3604
- authorized = options.authorizeLegacyCustodyDisposition?.(normalized) === true;
4546
+ authorized =
4547
+ options.authorizeLegacyCustodyDisposition?.(normalized) === true;
3605
4548
  }
3606
4549
  catch {
3607
4550
  authorized = false;
@@ -3612,24 +4555,38 @@ function createJournalStoreCore(options, getInputContext) {
3612
4555
  if (!isSafeNonNegativeInteger(nowMs))
3613
4556
  throw createJournalError("invalid", path, "received an invalid legacy custody disposition timestamp");
3614
4557
  const disposition = {
3615
- dispositionKind: "legacy-custody", failureId: normalized.dispositionId,
3616
- updateId: normalized.updateId, action: normalized.action,
4558
+ dispositionKind: "legacy-custody",
4559
+ failureId: normalized.dispositionId,
4560
+ updateId: normalized.updateId,
4561
+ action: normalized.action,
3617
4562
  committedAtMs: Math.max(nowMs, normalized.authorizedAtMs),
3618
4563
  evidenceSha256: normalized.evidenceSha256,
3619
4564
  operatorAuthorityId: normalized.operatorAuthorityId,
3620
- authorizedAtMs: normalized.authorizedAtMs
4565
+ authorizedAtMs: normalized.authorizedAtMs,
3621
4566
  };
3622
4567
  const nextEntries = normalized.action === "discard"
3623
- ? current.file.entries.filter(candidate => candidate.updateId !== entry.updateId)
3624
- : current.file.entries.map(candidate => candidate.updateId === entry.updateId
3625
- ? { updateId: entry.updateId, update: entry.update,
4568
+ ? current.file.entries.filter((candidate) => candidate.updateId !== entry.updateId)
4569
+ : current.file.entries.map((candidate) => candidate.updateId === entry.updateId
4570
+ ? {
4571
+ updateId: entry.updateId,
4572
+ update: entry.update,
3626
4573
  admittedAtMs: entry.admittedAtMs,
3627
- ...(entry.preApprovalExcluded === undefined ? {} :
3628
- { preApprovalExcluded: entry.preApprovalExcluded }), state: "pending" }
4574
+ ...(entry.preApprovalExcluded === undefined
4575
+ ? {}
4576
+ : { preApprovalExcluded: entry.preApprovalExcluded }),
4577
+ state: "pending",
4578
+ }
3629
4579
  : candidate);
3630
- const published = publishMutation(current, nextEntries, true, [...(current.file.operatorDispositions ?? []), disposition]);
3631
- return { disposition: { ...disposition }, duplicate: false,
3632
- entryCount: published.file.entries.length, serializedBytes: published.serializedBytes };
4580
+ const published = publishMutation(current, nextEntries, true, [
4581
+ ...(current.file.operatorDispositions ?? []),
4582
+ disposition,
4583
+ ]);
4584
+ return {
4585
+ disposition: { ...disposition },
4586
+ duplicate: false,
4587
+ entryCount: published.file.entries.length,
4588
+ serializedBytes: published.serializedBytes,
4589
+ };
3633
4590
  });
3634
4591
  },
3635
4592
  offerQueuedHandoff(input) {
@@ -3725,13 +4682,18 @@ function createJournalStoreCore(options, getInputContext) {
3725
4682
  updateId: entry.updateId,
3726
4683
  update: entry.update,
3727
4684
  admittedAtMs: entry.admittedAtMs,
3728
- ...(entry.preApprovalExcluded !== undefined ? { preApprovalExcluded: entry.preApprovalExcluded } : {}),
4685
+ ...(entry.preApprovalExcluded !== undefined
4686
+ ? { preApprovalExcluded: entry.preApprovalExcluded }
4687
+ : {}),
3729
4688
  state: "queued",
3730
4689
  queueKind: input.queueKind,
3731
4690
  queueReceiptId: input.receiptId,
3732
4691
  queueOwner: cloneJournalQueueOwner(queueOwner),
3733
4692
  ...(entry.inputProvenance
3734
- ? { inputProvenance: structuredClone(entry.inputProvenance) } : {}),
4693
+ ? {
4694
+ inputProvenance: structuredClone(entry.inputProvenance),
4695
+ }
4696
+ : {}),
3735
4697
  }
3736
4698
  : entry), true);
3737
4699
  return {
@@ -3777,12 +4739,15 @@ function createJournalStoreCore(options, getInputContext) {
3777
4739
  };
3778
4740
  });
3779
4741
  },
3780
- completeQueued(receipts) { return completeQueued(receipts); },
3781
- completeQueuedExact(receipts, completions) { return completeQueued(receipts, completions, true); },
4742
+ completeQueued(receipts) {
4743
+ return completeQueued(receipts);
4744
+ },
4745
+ completeQueuedExact(receipts, completions) {
4746
+ return completeQueued(receipts, completions, true);
4747
+ },
3782
4748
  discardQueued(input) {
3783
4749
  return runMutation((readCurrent) => {
3784
- if ((input.queueKind !== "prompt" &&
3785
- input.queueKind !== "control") ||
4750
+ if ((input.queueKind !== "prompt" && input.queueKind !== "control") ||
3786
4751
  !isNonEmptyString(input.receiptId) ||
3787
4752
  !Array.isArray(input.sourceUpdateIds) ||
3788
4753
  input.sourceUpdateIds.length === 0) {
@@ -3807,12 +4772,7 @@ function createJournalStoreCore(options, getInputContext) {
3807
4772
  const current = readCurrent();
3808
4773
  const receiptEntries = current.file.entries.filter((entry) => entry.queueReceiptId === input.receiptId);
3809
4774
  if (receiptEntries.length !== requestedIds.size ||
3810
- receiptEntries.some((entry) => !requestedIds.has(entry.updateId) ||
3811
- entry.state !== "queued" ||
3812
- entry.queueKind !== input.queueKind ||
3813
- !entry.queueOwner ||
3814
- entry.queueHandoff !== undefined ||
3815
- !areTelegramUpdateJournalQueueOwnersEqual(entry.queueOwner, expectedOwner))) {
4775
+ receiptEntries.some((entry) => !isExactQueuedReceiptEntry(entry, requestedIds, input.queueKind, expectedOwner))) {
3816
4776
  throw createJournalError("conflict", path, `cannot discard stale queue receipt ${input.receiptId}`);
3817
4777
  }
3818
4778
  const removedUpdateIds = [...requestedIds].sort((a, b) => a - b);
@@ -3859,17 +4819,11 @@ function createJournalStoreCore(options, getInputContext) {
3859
4819
  }
3860
4820
  const current = readCurrent();
3861
4821
  const receiptEntries = current.file.entries.filter((entry) => entry.queueReceiptId === input.receiptId);
3862
- if ((input.queueKind !== "prompt" &&
3863
- input.queueKind !== "control") ||
4822
+ if ((input.queueKind !== "prompt" && input.queueKind !== "control") ||
3864
4823
  !isNonEmptyString(input.receiptId) ||
3865
4824
  requestedIds.size === 0 ||
3866
4825
  receiptEntries.length !== requestedIds.size ||
3867
- receiptEntries.some((entry) => !requestedIds.has(entry.updateId) ||
3868
- entry.state !== "queued" ||
3869
- entry.queueKind !== input.queueKind ||
3870
- !entry.queueOwner ||
3871
- entry.queueHandoff !== undefined ||
3872
- !areTelegramUpdateJournalQueueOwnersEqual(entry.queueOwner, deadOwner))) {
4826
+ receiptEntries.some((entry) => !isExactQueuedReceiptEntry(entry, requestedIds, input.queueKind, deadOwner))) {
3873
4827
  throw createJournalError("conflict", path, `cannot recover stale queue receipt ${input.receiptId}`);
3874
4828
  }
3875
4829
  if (ownerLiveness !== "dead") {
@@ -3903,14 +4857,21 @@ function createJournalStoreCore(options, getInputContext) {
3903
4857
  throw createJournalError("invalid", path, "invalid abandonment update id");
3904
4858
  // Inspection never repairs or quarantines source evidence.
3905
4859
  const current = sourceAccess ? readCurrent() : readCurrentStrict(false);
3906
- const matches = (current.file.operatorDispositions ?? []).filter(item => item.updateId === updateId);
4860
+ const matches = (current.file.operatorDispositions ?? []).filter((item) => item.updateId === updateId);
3907
4861
  const disposition = matches[0];
3908
- if (matches.length !== 1 || !disposition || !("dispositionKind" in disposition) ||
3909
- disposition.dispositionKind !== "legacy-custody" || disposition.action !== "discard" ||
4862
+ if (matches.length !== 1 ||
4863
+ !disposition ||
4864
+ !("dispositionKind" in disposition) ||
4865
+ disposition.dispositionKind !== "legacy-custody" ||
4866
+ disposition.action !== "discard" ||
3910
4867
  disposition.failureId !== `abandon-${disposition.evidenceSha256}` ||
3911
- current.file.entries.some(entry => entry.updateId === updateId))
4868
+ current.file.entries.some((entry) => entry.updateId === updateId))
3912
4869
  return undefined;
3913
- const journalBindingKey = createTelegramUpdateJournalBindingKey({ path, profileName: profile, botIdentity: expectedIdentity });
4870
+ const journalBindingKey = createTelegramUpdateJournalBindingKey({
4871
+ path,
4872
+ profileName: profile,
4873
+ botIdentity: expectedIdentity,
4874
+ });
3914
4875
  const retainedPath = join(`${path}.retained`, `${disposition.failureId}.json`);
3915
4876
  let stat;
3916
4877
  try {
@@ -3921,20 +4882,33 @@ function createJournalStoreCore(options, getInputContext) {
3921
4882
  throw createJournalError("conflict", retainedPath, "committed abandonment lost its retained input");
3922
4883
  throw error;
3923
4884
  }
3924
- if (!stat.isFile() || stat.isSymbolicLink() || stat.size > maxBytes + 4096) {
4885
+ if (!stat.isFile() ||
4886
+ stat.isSymbolicLink() ||
4887
+ stat.size > maxBytes + 4096) {
3925
4888
  throw createJournalError("invalid", retainedPath, "retained input is not a bounded regular file");
3926
4889
  }
3927
4890
  const parsed = JSON.parse(readFileSync(retainedPath, "utf8"));
3928
4891
  if (!isRecord(parsed))
3929
4892
  throw createJournalError("invalid", retainedPath, "retained input is not an evidence record");
3930
4893
  const entry = validateJournalEntry(parsed.entry, retainedPath, version);
3931
- const reference = createPendingRetentionReference({ path, journalBindingKey, entry, maxBytes });
4894
+ const reference = createPendingRetentionReference({
4895
+ path,
4896
+ journalBindingKey,
4897
+ entry,
4898
+ maxBytes,
4899
+ });
3932
4900
  const retained = reference.read();
3933
- if (entry.updateId !== updateId || reference.retainedPath !== retainedPath ||
4901
+ if (entry.updateId !== updateId ||
4902
+ reference.retainedPath !== retainedPath ||
3934
4903
  !isDeepStrictEqual(retained?.requestedDisposition, disposition)) {
3935
4904
  throw createJournalError("conflict", retainedPath, "retained input does not match committed abandonment");
3936
4905
  }
3937
- return { journalBindingKey, updateId, retainedPath, operatorAuthorityId: disposition.operatorAuthorityId };
4906
+ return {
4907
+ journalBindingKey,
4908
+ updateId,
4909
+ retainedPath,
4910
+ operatorAuthorityId: disposition.operatorAuthorityId,
4911
+ };
3938
4912
  });
3939
4913
  },
3940
4914
  inspectPendingRetention(entry) {
@@ -3945,11 +4919,25 @@ function createJournalStoreCore(options, getInputContext) {
3945
4919
  const expected = validateJournalEntry(entry, path, version);
3946
4920
  if (expected.state !== "pending")
3947
4921
  throw createJournalError("conflict", path, "retention inspection requires a pending source");
3948
- const journalBindingKey = createTelegramUpdateJournalBindingKey({ path, profileName: profile, botIdentity: expectedIdentity });
3949
- const reference = createPendingRetentionReference({ path, journalBindingKey, entry: expected, maxBytes });
4922
+ const journalBindingKey = createTelegramUpdateJournalBindingKey({
4923
+ path,
4924
+ profileName: profile,
4925
+ botIdentity: expectedIdentity,
4926
+ });
4927
+ const reference = createPendingRetentionReference({
4928
+ path,
4929
+ journalBindingKey,
4930
+ entry: expected,
4931
+ maxBytes,
4932
+ });
3950
4933
  const retained = reference.read();
3951
- return retained ? { journalBindingKey, retainedPath: reference.retainedPath,
3952
- requestedDisposition: retained.requestedDisposition } : undefined;
4934
+ return retained
4935
+ ? {
4936
+ journalBindingKey,
4937
+ retainedPath: reference.retainedPath,
4938
+ requestedDisposition: retained.requestedDisposition,
4939
+ }
4940
+ : undefined;
3953
4941
  });
3954
4942
  },
3955
4943
  abandonPending(input) {
@@ -3957,12 +4945,17 @@ function createJournalStoreCore(options, getInputContext) {
3957
4945
  if (!isTelegramUpdateJournalLegacyFamilyVersion(version)) {
3958
4946
  throw createJournalError("unsupported-version", path, "pending abandonment requires the legacy v1 source");
3959
4947
  }
3960
- const journalBindingKey = createTelegramUpdateJournalBindingKey({ path, profileName: profile, botIdentity: expectedIdentity });
4948
+ const journalBindingKey = createTelegramUpdateJournalBindingKey({
4949
+ path,
4950
+ profileName: profile,
4951
+ botIdentity: expectedIdentity,
4952
+ });
3961
4953
  if (input.journalBindingKey !== journalBindingKey) {
3962
4954
  throw createJournalError("identity-mismatch", path, "pending abandonment belongs to another source");
3963
4955
  }
3964
4956
  if (!isBoundedString(input.operatorAuthorityId, TELEGRAM_UPDATE_JOURNAL_FAILURE_ID_MAX_LENGTH) ||
3965
- !input.operatorAuthorityId.trim() || typeof input.isCurrent !== "function") {
4957
+ !input.operatorAuthorityId.trim() ||
4958
+ typeof input.isCurrent !== "function") {
3966
4959
  throw createJournalError("invalid", path, "pending abandonment requires explicit operator authority");
3967
4960
  }
3968
4961
  const assertCurrent = () => {
@@ -3971,30 +4964,52 @@ function createJournalStoreCore(options, getInputContext) {
3971
4964
  };
3972
4965
  assertCurrent();
3973
4966
  const expected = validateJournalEntry(input.entry, path, version);
3974
- if (expected.state !== "pending" || expected.inputClaim || expected.inputProvenance ||
3975
- expected.queueOwner || expected.queueHandoff || expected.queueReceiptId || expected.failure) {
4967
+ if (expected.state !== "pending" ||
4968
+ expected.inputClaim ||
4969
+ expected.inputProvenance ||
4970
+ expected.queueOwner ||
4971
+ expected.queueHandoff ||
4972
+ expected.queueReceiptId ||
4973
+ expected.failure) {
3976
4974
  throw createJournalError("conflict", path, "only an exact unclaimed pending source may be abandoned");
3977
4975
  }
3978
4976
  // Never repair/quarantine source evidence while deciding an operator cancellation.
3979
- const readStrict = sourceAccess ? readCurrent : () => readCurrentStrict(false);
4977
+ const readStrict = sourceAccess
4978
+ ? readCurrent
4979
+ : () => readCurrentStrict(false);
3980
4980
  const current = readStrict();
3981
- const { evidenceSha256, failureId, retainedPath, read: readRetention } = createPendingRetentionReference({ path, journalBindingKey, entry: expected, maxBytes });
3982
- const existing = current.file.operatorDispositions?.find(item => item.failureId === failureId || item.updateId === expected.updateId);
3983
- const currentEntry = current.file.entries.find(entry => entry.updateId === expected.updateId);
4981
+ const { evidenceSha256, failureId, retainedPath, read: readRetention, } = createPendingRetentionReference({
4982
+ path,
4983
+ journalBindingKey,
4984
+ entry: expected,
4985
+ maxBytes,
4986
+ });
4987
+ const existing = current.file.operatorDispositions?.find((item) => item.failureId === failureId || item.updateId === expected.updateId);
4988
+ const currentEntry = current.file.entries.find((entry) => entry.updateId === expected.updateId);
3984
4989
  const assertRetention = (retained, disposition) => {
3985
- if (!retained || !isDeepStrictEqual(retained.requestedDisposition, disposition)) {
4990
+ if (!retained ||
4991
+ !isDeepStrictEqual(retained.requestedDisposition, disposition)) {
3986
4992
  throw createJournalError("conflict", retainedPath, "retained input does not match abandonment evidence");
3987
4993
  }
3988
4994
  };
3989
4995
  if (existing) {
3990
- if (currentEntry || !("dispositionKind" in existing) || existing.dispositionKind !== "legacy-custody" ||
3991
- existing.failureId !== failureId || existing.action !== "discard" || existing.evidenceSha256 !== evidenceSha256) {
4996
+ if (currentEntry ||
4997
+ !("dispositionKind" in existing) ||
4998
+ existing.dispositionKind !== "legacy-custody" ||
4999
+ existing.failureId !== failureId ||
5000
+ existing.action !== "discard" ||
5001
+ existing.evidenceSha256 !== evidenceSha256) {
3992
5002
  throw createJournalError("conflict", path, "pending source already has another disposition");
3993
5003
  }
3994
5004
  assertRetention(readRetention(), existing);
3995
5005
  assertCurrent();
3996
- return { disposition: existing, retainedPath, duplicate: true,
3997
- entryCount: current.file.entries.length, serializedBytes: current.serializedBytes };
5006
+ return {
5007
+ disposition: existing,
5008
+ retainedPath,
5009
+ duplicate: true,
5010
+ entryCount: current.file.entries.length,
5011
+ serializedBytes: current.serializedBytes,
5012
+ };
3998
5013
  }
3999
5014
  if (!currentEntry || !isDeepStrictEqual(currentEntry, expected)) {
4000
5015
  throw createJournalError("conflict", path, "pending source changed before abandonment");
@@ -4010,11 +5025,23 @@ function createJournalStoreCore(options, getInputContext) {
4010
5025
  else {
4011
5026
  // Reuse the existing legacy discard tombstone so older v1 readers also refuse replay.
4012
5027
  // This is an operator disposition, not a fabricated execution failure or completion.
4013
- disposition = { dispositionKind: "legacy-custody", failureId, updateId: expected.updateId,
4014
- action: "discard", committedAtMs: nowMs, evidenceSha256,
4015
- operatorAuthorityId: input.operatorAuthorityId, authorizedAtMs: nowMs };
4016
- const copy = { version: 1, kind: "pending-input-retention",
4017
- journalBindingKey, entry: expected, requestedDisposition: disposition };
5028
+ disposition = {
5029
+ dispositionKind: "legacy-custody",
5030
+ failureId,
5031
+ updateId: expected.updateId,
5032
+ action: "discard",
5033
+ committedAtMs: nowMs,
5034
+ evidenceSha256,
5035
+ operatorAuthorityId: input.operatorAuthorityId,
5036
+ authorizedAtMs: nowMs,
5037
+ };
5038
+ const copy = {
5039
+ version: 1,
5040
+ kind: "pending-input-retention",
5041
+ journalBindingKey,
5042
+ entry: expected,
5043
+ requestedDisposition: disposition,
5044
+ };
4018
5045
  writeJournalFile(retainedPath, `${JSON.stringify(copy)}\n`, (boundary, target) => {
4019
5046
  onPublicationBoundary?.(boundary, target);
4020
5047
  assertCurrent();
@@ -4022,12 +5049,20 @@ function createJournalStoreCore(options, getInputContext) {
4022
5049
  }
4023
5050
  assertCurrent();
4024
5051
  assertRetention(readRetention(), disposition);
4025
- const committed = (snapshot) => !snapshot.file.entries.some(entry => entry.updateId === expected.updateId) &&
4026
- snapshot.file.operatorDispositions?.some(item => isDeepStrictEqual(item, disposition));
5052
+ const committed = (snapshot) => !snapshot.file.entries.some((entry) => entry.updateId === expected.updateId) &&
5053
+ snapshot.file.operatorDispositions?.some((item) => isDeepStrictEqual(item, disposition));
4027
5054
  try {
4028
- const published = publishMutation(current, current.file.entries.filter(entry => entry.updateId !== expected.updateId), true, [...(current.file.operatorDispositions ?? []), disposition], current.file.acceptedThroughUpdateId, (boundary, target) => { onPublicationBoundary?.(boundary, target); assertCurrent(); });
4029
- return { disposition, retainedPath, duplicate: false,
4030
- entryCount: published.file.entries.length, serializedBytes: published.serializedBytes };
5055
+ const published = publishMutation(current, current.file.entries.filter((entry) => entry.updateId !== expected.updateId), true, [...(current.file.operatorDispositions ?? []), disposition], current.file.acceptedThroughUpdateId, (boundary, target) => {
5056
+ onPublicationBoundary?.(boundary, target);
5057
+ assertCurrent();
5058
+ });
5059
+ return {
5060
+ disposition,
5061
+ retainedPath,
5062
+ duplicate: false,
5063
+ entryCount: published.file.entries.length,
5064
+ serializedBytes: published.serializedBytes,
5065
+ };
4031
5066
  }
4032
5067
  catch (error) {
4033
5068
  // A segment may be durable even if later snapshot compaction failed.
@@ -4042,16 +5077,26 @@ function createJournalStoreCore(options, getInputContext) {
4042
5077
  if (!committed(observed))
4043
5078
  throw error;
4044
5079
  assertRetention(readRetention(), disposition);
4045
- return { disposition, retainedPath, duplicate: false,
4046
- entryCount: observed.file.entries.length, serializedBytes: observed.serializedBytes };
5080
+ return {
5081
+ disposition,
5082
+ retainedPath,
5083
+ duplicate: false,
5084
+ entryCount: observed.file.entries.length,
5085
+ serializedBytes: observed.serializedBytes,
5086
+ };
4047
5087
  }
4048
5088
  });
4049
5089
  },
4050
- removeCompleted(updateIds) { return removeCompleted(updateIds); },
4051
- removeCompletedExact(updateIds, expectedSources, completions) { return removeCompleted(updateIds, expectedSources, true, completions); },
5090
+ removeCompleted(updateIds) {
5091
+ return removeCompleted(updateIds);
5092
+ },
5093
+ removeCompletedExact(updateIds, expectedSources, completions, isCurrent) {
5094
+ return removeCompleted(updateIds, expectedSources, true, completions, isCurrent);
5095
+ },
4052
5096
  inspectQueuedReceipt(expected) {
4053
- return runMutationCore(readCurrent => {
4054
- if (!sourceAccess || !isTelegramUpdateJournalLegacyFamilyVersion(version)) {
5097
+ return runMutationCore((readCurrent) => {
5098
+ if (!sourceAccess ||
5099
+ !isTelegramUpdateJournalLegacyFamilyVersion(version)) {
4055
5100
  throw createJournalError("invalid", path, "queue receipt observation requires an exact legacy v1 source handle");
4056
5101
  }
4057
5102
  const queueOwner = validateQueuedReceiptObservation(expected, path);
@@ -4060,7 +5105,7 @@ function createJournalStoreCore(options, getInputContext) {
4060
5105
  });
4061
5106
  },
4062
5107
  inspectSourceCompletion(expected) {
4063
- return runMutation(readCurrent => {
5108
+ return runMutation((readCurrent) => {
4064
5109
  if (!sourceAccess)
4065
5110
  throw createJournalError("invalid", path, "source completion requires an exact source handle");
4066
5111
  const completion = validateJournalSourceCompletion(expected, path);
@@ -4069,28 +5114,128 @@ function createJournalStoreCore(options, getInputContext) {
4069
5114
  });
4070
5115
  },
4071
5116
  };
4072
- const abandonRoutingPending = journal.abandonPending.bind(journal);
4073
5117
  if (isTelegramUpdateJournalLegacyFamilyVersion(version))
4074
5118
  journal.routingInputs = {
4075
- arm(input) { return mutateRoutingInputs(input, false, input.publishedAtMs).entries; },
4076
- select(input) { return mutateRoutingInputs(input, true); },
5119
+ arm(input) {
5120
+ if (input.chooser !== undefined &&
5121
+ !isTelegramUpdateJournalRoutingChooser(input.chooser))
5122
+ throw createJournalError("invalid", path, "invalid routing input chooser location");
5123
+ return mutateRoutingInputs(input, false, input.publishedAtMs, input.chooser).entries;
5124
+ },
5125
+ select(input) {
5126
+ return mutateRoutingInputs(input, true);
5127
+ },
5128
+ inspectExpiry(updateId) {
5129
+ return runMutation((readCurrent) => {
5130
+ if (!isSafeNonNegativeInteger(updateId))
5131
+ throw createJournalError("invalid", path, "invalid routing expiry update ID");
5132
+ return inspectRoutingInputExpiry(readCurrent().file, createTelegramUpdateJournalBindingKey({
5133
+ path,
5134
+ profileName: profile,
5135
+ botIdentity: expectedIdentity,
5136
+ }), updateId);
5137
+ });
5138
+ },
5139
+ inspectGroupExpiry(updateIds) {
5140
+ return runMutation((readCurrent) => inspectRoutingGroupExpiry(readCurrent().file, createTelegramUpdateJournalBindingKey({
5141
+ path,
5142
+ profileName: profile,
5143
+ botIdentity: expectedIdentity,
5144
+ }), updateIds));
5145
+ },
4077
5146
  expire(input) {
4078
- const original = validateJournalEntry(input.entry, path, version), lifetime = original.routingInput;
4079
- if (!lifetime || lifetime.phase !== "waiting" || lifetime.operatorUserId !== input.operatorUserId ||
4080
- !isSafePositiveInteger(input.operatorUserId) || typeof input.isCurrent !== "function") {
4081
- throw createJournalError("conflict", path, "routing input expiry lacks exact waiting authority");
4082
- }
4083
- const current = () => { const now = getNowMs(); return isSafeNonNegativeInteger(now) && now >= lifetime.expiresAtMs && input.isCurrent(); };
4084
- if (!current())
4085
- throw createJournalError("conflict", path, "routing input has not expired under current authority");
4086
- return abandonRoutingPending({ journalBindingKey: input.journalBindingKey, entry: original,
4087
- operatorAuthorityId: `telegram-owner:${input.operatorUserId}`, isCurrent: current });
5147
+ return runMutation((readCurrent) => {
5148
+ const original = validateJournalEntry(input.entry, path, version), lifetime = original.routingInput;
5149
+ const binding = createTelegramUpdateJournalBindingKey({
5150
+ path,
5151
+ profileName: profile,
5152
+ botIdentity: expectedIdentity,
5153
+ });
5154
+ if (!lifetime ||
5155
+ original.state !== "pending" ||
5156
+ original.inputClaim ||
5157
+ original.inputProvenance ||
5158
+ original.queueOwner ||
5159
+ original.queueReceiptId ||
5160
+ original.queueHandoff ||
5161
+ original.failure ||
5162
+ input.journalBindingKey !== binding ||
5163
+ lifetime.operatorUserId !== input.operatorUserId ||
5164
+ !isSafePositiveInteger(input.operatorUserId) ||
5165
+ typeof input.isCurrent !== "function") {
5166
+ throw createJournalError("conflict", path, "routing expiry requires an exact unclaimed chooser source");
5167
+ }
5168
+ const assertCurrent = () => {
5169
+ const now = getNowMs();
5170
+ if (!isSafeNonNegativeInteger(now) ||
5171
+ now < lifetime.expiresAtMs ||
5172
+ !input.isCurrent())
5173
+ throw createJournalError("conflict", path, "routing source has not expired under current authority");
5174
+ };
5175
+ assertCurrent();
5176
+ const current = sourceAccess
5177
+ ? readCurrent()
5178
+ : readCurrentStrict(false);
5179
+ const evidenceSha256 = createHash("sha256")
5180
+ .update(JSON.stringify(original))
5181
+ .digest("hex");
5182
+ const failureId = `routing-expiry:${evidenceSha256}`;
5183
+ const existing = current.file.operatorDispositions?.find((value) => value.updateId === original.updateId);
5184
+ const entry = current.file.entries.find((value) => value.updateId === original.updateId);
5185
+ if (existing) {
5186
+ if (entry ||
5187
+ !("dispositionKind" in existing) ||
5188
+ existing.dispositionKind !== "legacy-custody" ||
5189
+ existing.action !== "discard" ||
5190
+ existing.failureId !== failureId ||
5191
+ existing.evidenceSha256 !== evidenceSha256 ||
5192
+ existing.operatorAuthorityId !==
5193
+ `telegram-owner:${input.operatorUserId}`)
5194
+ throw createJournalError("conflict", path, "routing source has contradictory expiry evidence");
5195
+ assertCurrent();
5196
+ return {
5197
+ disposition: existing,
5198
+ duplicate: true,
5199
+ entryCount: current.file.entries.length,
5200
+ serializedBytes: current.serializedBytes,
5201
+ };
5202
+ }
5203
+ if (!entry || !isDeepStrictEqual(entry, original))
5204
+ throw createJournalError("conflict", path, "routing source changed before expiry");
5205
+ // A discard tombstone prevents old readers and delayed reports from replaying; no prompt-body archive is written.
5206
+ const now = getNowMs();
5207
+ const disposition = {
5208
+ dispositionKind: "legacy-custody",
5209
+ failureId,
5210
+ updateId: original.updateId,
5211
+ action: "discard",
5212
+ committedAtMs: now,
5213
+ authorizedAtMs: now,
5214
+ evidenceSha256,
5215
+ operatorAuthorityId: `telegram-owner:${input.operatorUserId}`,
5216
+ };
5217
+ const published = publishMutation(current, current.file.entries.filter((value) => value.updateId !== original.updateId), true, [...(current.file.operatorDispositions ?? []), disposition], current.file.acceptedThroughUpdateId, (boundary, target) => {
5218
+ onPublicationBoundary?.(boundary, target);
5219
+ assertCurrent();
5220
+ });
5221
+ assertCurrent();
5222
+ return {
5223
+ disposition,
5224
+ duplicate: false,
5225
+ entryCount: published.file.entries.length,
5226
+ serializedBytes: published.serializedBytes,
5227
+ };
5228
+ });
4088
5229
  },
4089
5230
  };
4090
5231
  if (!getInputContext)
4091
5232
  return { journal };
4092
5233
  // Strict source acquisition already requires this exact stored receipt scope.
4093
- const bindingKey = createTelegramUpdateJournalBindingKey({ path, profileName: profile, botIdentity: expectedIdentity });
5234
+ const bindingKey = createTelegramUpdateJournalBindingKey({
5235
+ path,
5236
+ profileName: profile,
5237
+ botIdentity: expectedIdentity,
5238
+ });
4094
5239
  const createInputSourceReference = (updateId) => ({
4095
5240
  journalBindingKey: bindingKey,
4096
5241
  tokenSha256: expectedIdentity.tokenSha256,
@@ -4123,10 +5268,15 @@ function createJournalStoreCore(options, getInputContext) {
4123
5268
  assertInputProcess(owner);
4124
5269
  return { owner, recipientBindingKey };
4125
5270
  };
4126
- const runInputAdmission = (kind, operation) => workspaceAdmission ? runWithTelegramWorkspaceAdmissions({
4127
- ledger: workspaceAdmission, operationId: `input:${randomUUID()}`, operationKind: `journal.input.${kind}`,
4128
- scopes: [{ kind: "profile" }], operation,
4129
- }) : operation();
5271
+ const runInputAdmission = (kind, operation) => workspaceAdmission
5272
+ ? runWithTelegramWorkspaceAdmissions({
5273
+ ledger: workspaceAdmission,
5274
+ operationId: `input:${randomUUID()}`,
5275
+ operationKind: `journal.input.${kind}`,
5276
+ scopes: [{ kind: "profile" }],
5277
+ operation,
5278
+ })
5279
+ : operation();
4130
5280
  const runInputMutation = (kind, operation) => runInputAdmission(kind, () => runMutation(operation));
4131
5281
  const publishInputMutation = (current, entries, updateId, context) => publishMutation(current, entries, true, current.file.operatorDispositions, current.file.acceptedThroughUpdateId, (boundary, publicationPath) => {
4132
5282
  onPublicationBoundary?.(boundary, publicationPath);
@@ -4136,33 +5286,49 @@ function createJournalStoreCore(options, getInputContext) {
4136
5286
  }, updateId);
4137
5287
  const publishInputSettlement = (current, entries, contentChanged) => publishMutation(current, entries, contentChanged, current.file.operatorDispositions, current.file.acceptedThroughUpdateId, onPublicationBoundary, false);
4138
5288
  const normalizeInputSourceReference = (value, operation) => {
4139
- if (!isRecord(value) || !hasOnlyKeys(value, ["journalBindingKey", "tokenSha256", "updateId"])) {
5289
+ if (!isRecord(value) ||
5290
+ !hasOnlyKeys(value, ["journalBindingKey", "tokenSha256", "updateId"])) {
4140
5291
  throw createJournalError("invalid", path, `received invalid input handoff ${operation} source`);
4141
5292
  }
4142
5293
  const journalBindingKey = value.journalBindingKey;
4143
5294
  const updateId = value.updateId;
4144
- if (journalBindingKey !== bindingKey || value.tokenSha256 !== expectedIdentity.tokenSha256 ||
5295
+ if (journalBindingKey !== bindingKey ||
5296
+ value.tokenSha256 !== expectedIdentity.tokenSha256 ||
4145
5297
  !isSafeNonNegativeInteger(updateId)) {
4146
5298
  throw createJournalError("conflict", path, "input receipt does not match its source identity");
4147
5299
  }
4148
- return { journalBindingKey, tokenSha256: expectedIdentity.tokenSha256, updateId };
5300
+ return {
5301
+ journalBindingKey,
5302
+ tokenSha256: expectedIdentity.tokenSha256,
5303
+ updateId,
5304
+ };
4149
5305
  };
4150
5306
  const normalizeInputReceipt = (value, requireCurrentProcess = true) => {
4151
5307
  if (!isRecord(value) ||
4152
- !hasOnlyKeys(value, ["journalBindingKey", "tokenSha256", "updateId", "owner"])) {
5308
+ !hasOnlyKeys(value, [
5309
+ "journalBindingKey",
5310
+ "tokenSha256",
5311
+ "updateId",
5312
+ "owner",
5313
+ ])) {
4153
5314
  throw createJournalError("invalid", path, "received an invalid input receipt");
4154
5315
  }
4155
- const source = normalizeInputSourceReference({ journalBindingKey: value.journalBindingKey,
4156
- tokenSha256: value.tokenSha256, updateId: value.updateId }, "receipt");
5316
+ const source = normalizeInputSourceReference({
5317
+ journalBindingKey: value.journalBindingKey,
5318
+ tokenSha256: value.tokenSha256,
5319
+ updateId: value.updateId,
5320
+ }, "receipt");
4157
5321
  const owner = validateJournalQueueOwner(value.owner, path);
4158
5322
  if (requireCurrentProcess)
4159
5323
  assertInputProcess(owner);
4160
5324
  return { ...source, owner };
4161
5325
  };
4162
5326
  const normalizeInputHandoffOffer = (value) => {
4163
- if (!isRecord(value) || !hasOnlyKeys(value, ["receipt", "recipientOwner", "handoffToken"]) ||
5327
+ if (!isRecord(value) ||
5328
+ !hasOnlyKeys(value, ["receipt", "recipientOwner", "handoffToken"]) ||
4164
5329
  !isBoundedString(value.handoffToken, TELEGRAM_UPDATE_JOURNAL_QUEUE_HANDOFF_TOKEN_MAX_LENGTH) ||
4165
- value.handoffToken.length < TELEGRAM_UPDATE_JOURNAL_QUEUE_HANDOFF_TOKEN_MIN_LENGTH) {
5330
+ value.handoffToken.length <
5331
+ TELEGRAM_UPDATE_JOURNAL_QUEUE_HANDOFF_TOKEN_MIN_LENGTH) {
4166
5332
  throw createJournalError("invalid", path, "received invalid input handoff offer");
4167
5333
  }
4168
5334
  const receipt = normalizeInputReceipt(value.receipt);
@@ -4179,52 +5345,81 @@ function createJournalStoreCore(options, getInputContext) {
4179
5345
  return value;
4180
5346
  };
4181
5347
  const normalizeInputHandoffAccept = (value) => {
4182
- if (!isRecord(value) || !hasOnlyKeys(value, ["source", "recipientOwner", "handoffId"])) {
5348
+ if (!isRecord(value) ||
5349
+ !hasOnlyKeys(value, ["source", "recipientOwner", "handoffId"])) {
4183
5350
  throw createJournalError("invalid", path, "received invalid input handoff accept");
4184
5351
  }
4185
5352
  const source = normalizeInputSourceReference(value.source, "accept");
4186
5353
  const recipientOwner = validateJournalQueueOwnerIdentity(value.recipientOwner, path);
4187
5354
  assertInputProcess(recipientOwner);
4188
- return { source, recipientOwner, handoffId: normalizeInputHandoffId(value.handoffId, "accept") };
5355
+ return {
5356
+ source,
5357
+ recipientOwner,
5358
+ handoffId: normalizeInputHandoffId(value.handoffId, "accept"),
5359
+ };
4189
5360
  };
4190
5361
  const normalizeInputHandoffCancel = (value) => {
4191
- if (!isRecord(value) || !hasOnlyKeys(value, ["receipt", "recipientOwner", "handoffId"])) {
5362
+ if (!isRecord(value) ||
5363
+ !hasOnlyKeys(value, ["receipt", "recipientOwner", "handoffId"])) {
4192
5364
  throw createJournalError("invalid", path, "received invalid input handoff cancel");
4193
5365
  }
4194
- return { receipt: normalizeInputReceipt(value.receipt),
5366
+ return {
5367
+ receipt: normalizeInputReceipt(value.receipt),
4195
5368
  recipientOwner: validateJournalQueueOwnerIdentity(value.recipientOwner, path),
4196
- handoffId: normalizeInputHandoffId(value.handoffId, "cancel") };
5369
+ handoffId: normalizeInputHandoffId(value.handoffId, "cancel"),
5370
+ };
4197
5371
  };
4198
5372
  const normalizeInputQueue = (value) => {
4199
- if (!isRecord(value) || !hasOnlyKeys(value, ["queueKind", "receiptId", "receipts"]) ||
5373
+ if (!isRecord(value) ||
5374
+ !hasOnlyKeys(value, ["queueKind", "receiptId", "receipts"]) ||
4200
5375
  (value.queueKind !== "prompt" && value.queueKind !== "control") ||
4201
5376
  !isBoundedString(value.receiptId, TELEGRAM_UPDATE_JOURNAL_QUEUE_OWNER_ID_MAX_LENGTH) ||
4202
- !value.receiptId.trim() || !Array.isArray(value.receipts) || value.receipts.length === 0) {
5377
+ !value.receiptId.trim() ||
5378
+ !Array.isArray(value.receipts) ||
5379
+ value.receipts.length === 0) {
4203
5380
  throw createJournalError("invalid", path, "received invalid input queue transition");
4204
5381
  }
4205
- const receipts = value.receipts.map(receipt => normalizeInputReceipt(receipt)).sort((left, right) => left.updateId - right.updateId);
5382
+ const receipts = value.receipts
5383
+ .map((receipt) => normalizeInputReceipt(receipt))
5384
+ .sort((left, right) => left.updateId - right.updateId);
4206
5385
  if (receipts.some((receipt, index) => index > 0 && receipt.updateId === receipts[index - 1].updateId)) {
4207
5386
  throw createJournalError("invalid", path, "received duplicate input queue transition receipts");
4208
5387
  }
4209
5388
  return { queueKind: value.queueKind, receiptId: value.receiptId, receipts };
4210
5389
  };
4211
5390
  const ownedInput = (current, receipt) => {
4212
- const entry = current.file.entries.find(candidate => candidate.updateId === receipt.updateId);
4213
- if (!entry?.inputClaim || entry.state !== "pending" ||
5391
+ const entry = current.file.entries.find((candidate) => candidate.updateId === receipt.updateId);
5392
+ if (!entry?.inputClaim ||
5393
+ entry.state !== "pending" ||
4214
5394
  !areTelegramUpdateJournalQueueOwnersEqual(entry.inputClaim.owner, receipt.owner)) {
4215
5395
  throw createJournalError("conflict", path, "input receipt lost its exact acquisition or phase");
4216
5396
  }
4217
5397
  return entry;
4218
5398
  };
5399
+ // Handoff offer/cancel require an idle ready donor claim owned by this exact context and binding.
5400
+ const readReadyDonorInput = (current, receipt) => {
5401
+ const context = currentInputContext();
5402
+ const entry = ownedInput(current, receipt);
5403
+ const claim = entry.inputClaim;
5404
+ if (claim.phase !== "ready" ||
5405
+ !inputOwnerMatchesIdentity(receipt.owner, context.owner) ||
5406
+ claim.recipientBindingKey !== context.recipientBindingKey) {
5407
+ throw createJournalError("conflict", path, "input handoff donor authority is stale or running");
5408
+ }
5409
+ return { context, current, entry, claim };
5410
+ };
4219
5411
  const getReadyInputRelease = (current, receipt, allowOffered = false) => {
4220
- const entry = current.file.entries.find(candidate => candidate.updateId === receipt.updateId);
4221
- if (!entry || entry.state !== "pending" || entry.preApprovalExcluded !== false) {
5412
+ const entry = current.file.entries.find((candidate) => candidate.updateId === receipt.updateId);
5413
+ if (!entry ||
5414
+ entry.state !== "pending" ||
5415
+ entry.preApprovalExcluded !== false) {
4222
5416
  throw createJournalError("conflict", path, "input is unavailable for ready-claim release");
4223
5417
  }
4224
5418
  if (!entry.inputClaim)
4225
5419
  return { entry, unclaimed: true };
4226
5420
  if (!areTelegramUpdateJournalQueueOwnersEqual(entry.inputClaim.owner, receipt.owner) ||
4227
- entry.inputClaim.phase !== "ready" || (!allowOffered && entry.inputClaim.handoff !== undefined)) {
5421
+ entry.inputClaim.phase !== "ready" ||
5422
+ (!allowOffered && entry.inputClaim.handoff !== undefined)) {
4228
5423
  throw createJournalError("conflict", path, "cannot release stale, running, or offered input authority");
4229
5424
  }
4230
5425
  return { entry, unclaimed: false };
@@ -4232,7 +5427,7 @@ function createJournalStoreCore(options, getInputContext) {
4232
5427
  const publishReadyInputRelease = (current, entry) => {
4233
5428
  const released = cloneEntry(entry);
4234
5429
  delete released.inputClaim;
4235
- return publishMutation(current, current.file.entries.map(candidate => candidate.updateId === entry.updateId ? released : candidate), true, current.file.operatorDispositions, current.file.acceptedThroughUpdateId, onPublicationBoundary, entry.updateId);
5430
+ return publishMutation(current, current.file.entries.map((candidate) => candidate.updateId === entry.updateId ? released : candidate), true, current.file.operatorDispositions, current.file.acceptedThroughUpdateId, onPublicationBoundary, entry.updateId);
4236
5431
  };
4237
5432
  const input = {
4238
5433
  read: journal.read,
@@ -4240,12 +5435,16 @@ function createJournalStoreCore(options, getInputContext) {
4240
5435
  listLegacyCustodyCandidates() {
4241
5436
  const sourceAccess = options.sourceAccess;
4242
5437
  const evidence = readTelegramUpdateJournalSource({
4243
- directory: sourceAccess.directory, path, profile,
4244
- botIdentity: expectedIdentity, limits: sourceAccess.limits,
5438
+ directory: sourceAccess.directory,
5439
+ path,
5440
+ profile,
5441
+ botIdentity: expectedIdentity,
5442
+ limits: sourceAccess.limits,
4245
5443
  version,
4246
5444
  });
4247
5445
  return evidence.kind === "present"
4248
- ? listTelegramUpdateJournalLegacyCustodyCandidates(evidence.file) : [];
5446
+ ? listTelegramUpdateJournalLegacyCustodyCandidates(evidence.file)
5447
+ : [];
4249
5448
  },
4250
5449
  applyLegacyCustodyDisposition(value) {
4251
5450
  return runInputAdmission("legacy-custody-disposition", () => journal.applyLegacyCustodyDisposition(value));
@@ -4276,117 +5475,163 @@ function createJournalStoreCore(options, getInputContext) {
4276
5475
  }
4277
5476
  requestedIds.add(updateId);
4278
5477
  }
4279
- return runInputMutation("remove-excluded", read => {
5478
+ return runInputMutation("remove-excluded", (read) => {
4280
5479
  const current = read();
4281
- if (!current.exists || [...requestedIds].some(id => id > (current.file.acceptedThroughUpdateId ?? -1))) {
5480
+ if (!current.exists ||
5481
+ [...requestedIds].some((id) => id > (current.file.acceptedThroughUpdateId ?? -1))) {
4282
5482
  throw createJournalError("conflict", path, "exclusion removal lacks retained source admission evidence");
4283
5483
  }
4284
- const selected = current.file.entries.filter(entry => requestedIds.has(entry.updateId));
4285
- if (selected.some(entry => entry.preApprovalExcluded !== true)) {
5484
+ const selected = current.file.entries.filter((entry) => requestedIds.has(entry.updateId));
5485
+ if (selected.some((entry) => entry.preApprovalExcluded !== true)) {
4286
5486
  throw createJournalError("conflict", path, "cannot remove input without immutable exclusion evidence");
4287
5487
  }
4288
5488
  // Strict v3 decoding already excludes raw claims and queued authority on vetoed entries.
4289
- const removedUpdateIds = selected.map(entry => entry.updateId);
4290
- const published = publishInputSettlement(current, current.file.entries.filter(entry => !requestedIds.has(entry.updateId)), removedUpdateIds.length > 0);
4291
- return { removedUpdateIds, entryCount: published.file.entries.length, serializedBytes: published.serializedBytes };
5489
+ const removedUpdateIds = selected.map((entry) => entry.updateId);
5490
+ const published = publishInputSettlement(current, current.file.entries.filter((entry) => !requestedIds.has(entry.updateId)), removedUpdateIds.length > 0);
5491
+ return {
5492
+ removedUpdateIds,
5493
+ entryCount: published.file.entries.length,
5494
+ serializedBytes: published.serializedBytes,
5495
+ };
4292
5496
  });
4293
5497
  },
4294
5498
  releaseInput(value) {
4295
5499
  const receipt = normalizeInputReceipt(value);
4296
- return runInputMutation("release", read => {
5500
+ return runInputMutation("release", (read) => {
4297
5501
  const current = read();
4298
5502
  const release = getReadyInputRelease(current, receipt);
4299
5503
  if (release.unclaimed)
4300
- return { released: false,
4301
- entryCount: current.file.entries.length, serializedBytes: current.serializedBytes };
5504
+ return {
5505
+ released: false,
5506
+ entryCount: current.file.entries.length,
5507
+ serializedBytes: current.serializedBytes,
5508
+ };
4302
5509
  const published = publishReadyInputRelease(current, release.entry);
4303
- return { released: true, entryCount: published.file.entries.length,
4304
- serializedBytes: published.serializedBytes };
5510
+ return {
5511
+ released: true,
5512
+ entryCount: published.file.entries.length,
5513
+ serializedBytes: published.serializedBytes,
5514
+ };
4305
5515
  });
4306
5516
  },
4307
5517
  recoverReadyInput(value) {
4308
- if (!isRecord(value) || !hasOnlyKeys(value, ["receipt", "recoveryOwner"])) {
5518
+ if (!isRecord(value) ||
5519
+ !hasOnlyKeys(value, ["receipt", "recoveryOwner"])) {
4309
5520
  throw createJournalError("invalid", path, "received invalid ready-input recovery authority");
4310
5521
  }
4311
5522
  const receipt = normalizeInputReceipt(value.receipt, false);
4312
5523
  const recoveryOwner = validateJournalQueueOwnerIdentity(value.recoveryOwner, path);
4313
5524
  assertInputProcess(recoveryOwner);
4314
- return runInputMutation("recover-ready", read => {
5525
+ return runInputMutation("recover-ready", (read) => {
4315
5526
  const current = read();
4316
5527
  const release = getReadyInputRelease(current, receipt, true);
4317
5528
  if (release.unclaimed)
4318
- return { status: "unclaimed",
4319
- entryCount: current.file.entries.length, serializedBytes: current.serializedBytes };
5529
+ return {
5530
+ status: "unclaimed",
5531
+ entryCount: current.file.entries.length,
5532
+ serializedBytes: current.serializedBytes,
5533
+ };
4320
5534
  let liveness;
4321
5535
  try {
4322
- liveness = getQueueProcessLiveness({ processId: receipt.owner.processId,
4323
- processBirthId: receipt.owner.processBirthId });
5536
+ liveness = getQueueProcessLiveness({
5537
+ processId: receipt.owner.processId,
5538
+ processBirthId: receipt.owner.processBirthId,
5539
+ });
4324
5540
  }
4325
5541
  catch (error) {
4326
5542
  throw createJournalError("io", path, "could not prove ready input owner liveness", error);
4327
5543
  }
4328
5544
  if (liveness !== "dead")
4329
- return { status: liveness === "alive" ? "owner-alive" : "owner-unverifiable",
4330
- entryCount: current.file.entries.length, serializedBytes: current.serializedBytes };
5545
+ return {
5546
+ status: liveness === "alive"
5547
+ ? "owner-alive"
5548
+ : "owner-unverifiable",
5549
+ entryCount: current.file.entries.length,
5550
+ serializedBytes: current.serializedBytes,
5551
+ };
4331
5552
  const published = publishReadyInputRelease(current, release.entry);
4332
- return { status: "recovered", entryCount: published.file.entries.length,
4333
- serializedBytes: published.serializedBytes };
5553
+ return {
5554
+ status: "recovered",
5555
+ entryCount: published.file.entries.length,
5556
+ serializedBytes: published.serializedBytes,
5557
+ };
4334
5558
  });
4335
5559
  },
4336
5560
  offerInputHandoff(value) {
4337
5561
  const { receipt, recipientOwner, handoffToken } = normalizeInputHandoffOffer(value);
4338
- return runInputMutation("offer", read => {
4339
- const context = currentInputContext();
4340
- const current = read();
4341
- const entry = ownedInput(current, receipt);
4342
- const claim = entry.inputClaim;
4343
- if (claim.phase !== "ready" || !inputOwnerMatchesIdentity(receipt.owner, context.owner) ||
4344
- claim.recipientBindingKey !== context.recipientBindingKey) {
4345
- throw createJournalError("conflict", path, "input handoff donor authority is stale or running");
4346
- }
5562
+ return runInputMutation("offer", (read) => {
5563
+ const { context, current, claim } = readReadyDonorInput(read(), receipt);
4347
5564
  if (claim.handoff) {
4348
5565
  if (!isDeepStrictEqual(claim.handoff.recipientOwner, recipientOwner)) {
4349
5566
  throw createJournalError("conflict", path, "input already has another handoff offer");
4350
5567
  }
4351
- return { source: createInputSourceReference(receipt.updateId),
5568
+ return {
5569
+ source: createInputSourceReference(receipt.updateId),
4352
5570
  handoff: cloneJournalQueueHandoff(claim.handoff),
4353
- previousOwner: cloneJournalQueueOwner(receipt.owner), duplicate: true,
4354
- entryCount: current.file.entries.length, serializedBytes: current.serializedBytes };
5571
+ previousOwner: cloneJournalQueueOwner(receipt.owner),
5572
+ duplicate: true,
5573
+ entryCount: current.file.entries.length,
5574
+ serializedBytes: current.serializedBytes,
5575
+ };
4355
5576
  }
4356
- const handoffId = createTelegramInputHandoffId({ handoffToken, journalBindingKey: bindingKey,
4357
- updateId: receipt.updateId, donorOwner: receipt.owner, recipientOwner,
4358
- recipientBindingKey: claim.recipientBindingKey });
5577
+ const handoffId = createTelegramInputHandoffId({
5578
+ handoffToken,
5579
+ journalBindingKey: bindingKey,
5580
+ updateId: receipt.updateId,
5581
+ donorOwner: receipt.owner,
5582
+ recipientOwner,
5583
+ recipientBindingKey: claim.recipientBindingKey,
5584
+ });
4359
5585
  const offeredAtMs = getNowMs();
4360
5586
  if (!isSafeNonNegativeInteger(offeredAtMs)) {
4361
5587
  throw createJournalError("invalid", path, "received an invalid input handoff offer timestamp");
4362
5588
  }
4363
- const handoff = { handoffId, offeredAtMs,
4364
- recipientOwner: { ...recipientOwner } };
5589
+ const handoff = {
5590
+ handoffId,
5591
+ offeredAtMs,
5592
+ recipientOwner: { ...recipientOwner },
5593
+ };
4365
5594
  const offeredClaim = { ...claim, handoff };
4366
- const published = publishInputMutation(current, current.file.entries.map(candidate => candidate.updateId === receipt.updateId ? { ...candidate, inputClaim: offeredClaim } : candidate), receipt.updateId, context);
4367
- return { source: createInputSourceReference(receipt.updateId),
5595
+ const published = publishInputMutation(current, current.file.entries.map((candidate) => candidate.updateId === receipt.updateId
5596
+ ? { ...candidate, inputClaim: offeredClaim }
5597
+ : candidate), receipt.updateId, context);
5598
+ return {
5599
+ source: createInputSourceReference(receipt.updateId),
4368
5600
  handoff: cloneJournalQueueHandoff(handoff),
4369
- previousOwner: cloneJournalQueueOwner(receipt.owner), duplicate: false,
4370
- entryCount: published.file.entries.length, serializedBytes: published.serializedBytes };
5601
+ previousOwner: cloneJournalQueueOwner(receipt.owner),
5602
+ duplicate: false,
5603
+ entryCount: published.file.entries.length,
5604
+ serializedBytes: published.serializedBytes,
5605
+ };
4371
5606
  });
4372
5607
  },
4373
5608
  acceptInputHandoff(value) {
4374
5609
  const { source, recipientOwner, handoffId } = normalizeInputHandoffAccept(value);
4375
- return runInputMutation("accept", read => {
5610
+ return runInputMutation("accept", (read) => {
4376
5611
  const context = currentInputContext();
4377
5612
  const current = read();
4378
- const entry = current.file.entries.find(candidate => candidate.updateId === source.updateId);
5613
+ const entry = current.file.entries.find((candidate) => candidate.updateId === source.updateId);
4379
5614
  const claim = entry?.inputClaim;
4380
- if (!entry || entry.state !== "pending" || entry.preApprovalExcluded !== false || !claim ||
5615
+ if (!entry ||
5616
+ entry.state !== "pending" ||
5617
+ entry.preApprovalExcluded !== false ||
5618
+ !claim ||
4381
5619
  !isDeepStrictEqual(context.owner, recipientOwner) ||
4382
5620
  context.recipientBindingKey !== claim.recipientBindingKey) {
4383
5621
  throw createJournalError("conflict", path, "input handoff recipient authority is unavailable or changed");
4384
5622
  }
4385
- if (claim.owner.handoffId === handoffId && inputOwnerMatchesIdentity(claim.owner, recipientOwner)) {
4386
- return { handoffId, receipt: createInputReceipt(source.updateId, claim.owner), duplicate: true,
4387
- entryCount: current.file.entries.length, serializedBytes: current.serializedBytes };
5623
+ if (claim.owner.handoffId === handoffId &&
5624
+ inputOwnerMatchesIdentity(claim.owner, recipientOwner)) {
5625
+ return {
5626
+ handoffId,
5627
+ receipt: createInputReceipt(source.updateId, claim.owner),
5628
+ duplicate: true,
5629
+ entryCount: current.file.entries.length,
5630
+ serializedBytes: current.serializedBytes,
5631
+ };
4388
5632
  }
4389
- if (claim.phase !== "ready" || claim.handoff?.handoffId !== handoffId ||
5633
+ if (claim.phase !== "ready" ||
5634
+ claim.handoff?.handoffId !== handoffId ||
4390
5635
  !isDeepStrictEqual(claim.handoff.recipientOwner, recipientOwner)) {
4391
5636
  throw createJournalError("conflict", path, "cannot accept stale or unauthenticated input handoff");
4392
5637
  }
@@ -4395,75 +5640,109 @@ function createJournalStoreCore(options, getInputContext) {
4395
5640
  throw createJournalError("invalid", path, "received an invalid input handoff acquisition timestamp");
4396
5641
  }
4397
5642
  const previousOwner = cloneJournalQueueOwner(claim.owner);
4398
- const owner = { ...recipientOwner,
4399
- acquisitionId: randomUUID(), acquiredAtMs, handoffId };
5643
+ const owner = {
5644
+ ...recipientOwner,
5645
+ acquisitionId: randomUUID(),
5646
+ acquiredAtMs,
5647
+ handoffId,
5648
+ };
4400
5649
  const acceptedClaim = structuredClone(claim);
4401
5650
  acceptedClaim.owner = owner;
4402
5651
  delete acceptedClaim.handoff;
4403
- const publishedEntries = current.file.entries.map(candidate => candidate.updateId === source.updateId ? { ...candidate, inputClaim: acceptedClaim } : candidate);
5652
+ const publishedEntries = current.file.entries.map((candidate) => candidate.updateId === source.updateId
5653
+ ? { ...candidate, inputClaim: acceptedClaim }
5654
+ : candidate);
4404
5655
  const published = publishInputMutation(current, publishedEntries, source.updateId, context);
4405
- return { handoffId, previousOwner, receipt: createInputReceipt(source.updateId, owner), duplicate: false,
4406
- entryCount: published.file.entries.length, serializedBytes: published.serializedBytes };
5656
+ return {
5657
+ handoffId,
5658
+ previousOwner,
5659
+ receipt: createInputReceipt(source.updateId, owner),
5660
+ duplicate: false,
5661
+ entryCount: published.file.entries.length,
5662
+ serializedBytes: published.serializedBytes,
5663
+ };
4407
5664
  });
4408
5665
  },
4409
5666
  cancelInputHandoff(value) {
4410
5667
  const { receipt, recipientOwner, handoffId } = normalizeInputHandoffCancel(value);
4411
- return runInputMutation("cancel", read => {
4412
- const context = currentInputContext();
4413
- const current = read();
4414
- const entry = ownedInput(current, receipt);
4415
- const claim = entry.inputClaim;
4416
- if (claim.phase !== "ready" || !inputOwnerMatchesIdentity(receipt.owner, context.owner) ||
4417
- claim.recipientBindingKey !== context.recipientBindingKey) {
4418
- throw createJournalError("conflict", path, "input handoff donor authority is stale or running");
4419
- }
5668
+ return runInputMutation("cancel", (read) => {
5669
+ const { context, current, claim } = readReadyDonorInput(read(), receipt);
4420
5670
  if (!claim.handoff)
4421
- return { handoffId, previousOwner: cloneJournalQueueOwner(receipt.owner),
4422
- cancelled: false, entryCount: current.file.entries.length, serializedBytes: current.serializedBytes };
5671
+ return {
5672
+ handoffId,
5673
+ previousOwner: cloneJournalQueueOwner(receipt.owner),
5674
+ cancelled: false,
5675
+ entryCount: current.file.entries.length,
5676
+ serializedBytes: current.serializedBytes,
5677
+ };
4423
5678
  if (claim.handoff.handoffId !== handoffId ||
4424
5679
  !isDeepStrictEqual(claim.handoff.recipientOwner, recipientOwner)) {
4425
5680
  throw createJournalError("conflict", path, "cannot cancel another input handoff offer");
4426
5681
  }
4427
5682
  const cancelledClaim = structuredClone(claim);
4428
5683
  delete cancelledClaim.handoff;
4429
- const publishedEntries = current.file.entries.map(candidate => candidate.updateId === receipt.updateId ? { ...candidate, inputClaim: cancelledClaim } : candidate);
5684
+ const publishedEntries = current.file.entries.map((candidate) => candidate.updateId === receipt.updateId
5685
+ ? { ...candidate, inputClaim: cancelledClaim }
5686
+ : candidate);
4430
5687
  const published = publishInputMutation(current, publishedEntries, receipt.updateId, context);
4431
- return { handoffId, previousOwner: cloneJournalQueueOwner(receipt.owner), cancelled: true,
4432
- entryCount: published.file.entries.length, serializedBytes: published.serializedBytes };
5688
+ return {
5689
+ handoffId,
5690
+ previousOwner: cloneJournalQueueOwner(receipt.owner),
5691
+ cancelled: true,
5692
+ entryCount: published.file.entries.length,
5693
+ serializedBytes: published.serializedBytes,
5694
+ };
4433
5695
  });
4434
5696
  },
4435
5697
  queueInputs(value) {
4436
5698
  const { queueKind, receiptId, receipts } = normalizeInputQueue(value);
4437
- return runInputMutation("queue", read => {
5699
+ return runInputMutation("queue", (read) => {
4438
5700
  const context = currentInputContext();
4439
- if (receipts.some(receipt => !inputOwnerMatchesIdentity(receipt.owner, context.owner))) {
5701
+ if (receipts.some((receipt) => !inputOwnerMatchesIdentity(receipt.owner, context.owner))) {
4440
5702
  throw createJournalError("conflict", path, "input queue transition belongs to another session");
4441
5703
  }
4442
5704
  const current = read();
4443
- const receiptsById = new Map(receipts.map(receipt => [receipt.updateId, receipt]));
4444
- const requestedIds = receipts.map(receipt => receipt.updateId);
4445
- const existingReceiptEntries = current.file.entries.filter(entry => entry.queueReceiptId === receiptId);
5705
+ const receiptsById = new Map(receipts.map((receipt) => [receipt.updateId, receipt]));
5706
+ const requestedIds = receipts.map((receipt) => receipt.updateId);
5707
+ const existingReceiptEntries = current.file.entries.filter((entry) => entry.queueReceiptId === receiptId);
4446
5708
  if (existingReceiptEntries.length > 0) {
4447
5709
  const queueOwner = existingReceiptEntries[0]?.queueOwner;
4448
- if (existingReceiptEntries.length !== receipts.length || !queueOwner ||
5710
+ if (existingReceiptEntries.length !== receipts.length ||
5711
+ !queueOwner ||
4449
5712
  !inputOwnerMatchesIdentity(queueOwner, context.owner) ||
4450
- existingReceiptEntries.some(entry => {
5713
+ existingReceiptEntries.some((entry) => {
4451
5714
  const receipt = receiptsById.get(entry.updateId);
4452
- return !receipt || entry.state !== "queued" || entry.queueKind !== queueKind ||
4453
- entry.queueHandoff !== undefined || !entry.inputProvenance ||
5715
+ return (!receipt ||
5716
+ entry.state !== "queued" ||
5717
+ entry.queueKind !== queueKind ||
5718
+ entry.queueHandoff !== undefined ||
5719
+ !entry.inputProvenance ||
4454
5720
  !areTelegramUpdateJournalQueueOwnersEqual(entry.inputProvenance.owner, receipt.owner) ||
4455
- entry.inputProvenance.recipientBindingKey !== context.recipientBindingKey;
5721
+ entry.inputProvenance.recipientBindingKey !==
5722
+ context.recipientBindingKey);
4456
5723
  })) {
4457
5724
  throw createJournalError("conflict", path, "input queue transition conflicts with queued authority");
4458
5725
  }
4459
- return { queued: false, queueReceipt: { queueKind, receiptId,
4460
- sourceUpdateIds: requestedIds, queueOwner: cloneJournalQueueOwner(queueOwner) },
4461
- entryCount: current.file.entries.length, serializedBytes: current.serializedBytes };
5726
+ return {
5727
+ queued: false,
5728
+ queueReceipt: {
5729
+ queueKind,
5730
+ receiptId,
5731
+ sourceUpdateIds: requestedIds,
5732
+ queueOwner: cloneJournalQueueOwner(queueOwner),
5733
+ },
5734
+ entryCount: current.file.entries.length,
5735
+ serializedBytes: current.serializedBytes,
5736
+ };
4462
5737
  }
4463
5738
  for (const receipt of receipts) {
4464
- const entry = current.file.entries.find(candidate => candidate.updateId === receipt.updateId);
4465
- if (!entry || entry.state !== "pending" || entry.preApprovalExcluded !== false ||
4466
- !entry.inputClaim || entry.inputClaim.phase !== "running" || entry.inputClaim.handoff ||
5739
+ const entry = current.file.entries.find((candidate) => candidate.updateId === receipt.updateId);
5740
+ if (!entry ||
5741
+ entry.state !== "pending" ||
5742
+ entry.preApprovalExcluded !== false ||
5743
+ !entry.inputClaim ||
5744
+ entry.inputClaim.phase !== "running" ||
5745
+ entry.inputClaim.handoff ||
4467
5746
  !areTelegramUpdateJournalQueueOwnersEqual(entry.inputClaim.owner, receipt.owner) ||
4468
5747
  entry.inputClaim.recipientBindingKey !== context.recipientBindingKey) {
4469
5748
  throw createJournalError("conflict", path, "input queue transition lost exact running authority");
@@ -4473,76 +5752,122 @@ function createJournalStoreCore(options, getInputContext) {
4473
5752
  if (!isSafeNonNegativeInteger(acquiredAtMs)) {
4474
5753
  throw createJournalError("invalid", path, "received an invalid input queue acquisition timestamp");
4475
5754
  }
4476
- const queueOwner = { ...context.owner,
4477
- acquisitionId: randomUUID(), acquiredAtMs };
4478
- const queuedEntries = current.file.entries.map(entry => {
5755
+ const queueOwner = {
5756
+ ...context.owner,
5757
+ acquisitionId: randomUUID(),
5758
+ acquiredAtMs,
5759
+ };
5760
+ const queuedEntries = current.file.entries.map((entry) => {
4479
5761
  const receipt = receiptsById.get(entry.updateId);
4480
5762
  if (!receipt)
4481
5763
  return entry;
4482
5764
  const claim = entry.inputClaim;
4483
- return { updateId: entry.updateId, update: entry.update,
4484
- admittedAtMs: entry.admittedAtMs, preApprovalExcluded: false, state: "queued",
4485
- queueKind, queueReceiptId: receiptId, queueOwner: cloneJournalQueueOwner(queueOwner),
4486
- inputProvenance: { owner: cloneJournalQueueOwner(claim.owner),
5765
+ return {
5766
+ updateId: entry.updateId,
5767
+ update: entry.update,
5768
+ admittedAtMs: entry.admittedAtMs,
5769
+ preApprovalExcluded: false,
5770
+ state: "queued",
5771
+ queueKind,
5772
+ queueReceiptId: receiptId,
5773
+ queueOwner: cloneJournalQueueOwner(queueOwner),
5774
+ inputProvenance: {
5775
+ owner: cloneJournalQueueOwner(claim.owner),
4487
5776
  recipientBindingKey: claim.recipientBindingKey,
4488
5777
  ...(claim.executionUpdate
4489
- ? { executionUpdate: structuredClone(claim.executionUpdate) } : {}) } };
5778
+ ? { executionUpdate: structuredClone(claim.executionUpdate) }
5779
+ : {}),
5780
+ },
5781
+ };
4490
5782
  });
4491
5783
  const published = publishInputMutation(current, queuedEntries, requestedIds, context);
4492
- return { queued: true, queueReceipt: { queueKind, receiptId,
4493
- sourceUpdateIds: requestedIds, queueOwner: cloneJournalQueueOwner(queueOwner) },
4494
- entryCount: published.file.entries.length, serializedBytes: published.serializedBytes };
5784
+ return {
5785
+ queued: true,
5786
+ queueReceipt: {
5787
+ queueKind,
5788
+ receiptId,
5789
+ sourceUpdateIds: requestedIds,
5790
+ queueOwner: cloneJournalQueueOwner(queueOwner),
5791
+ },
5792
+ entryCount: published.file.entries.length,
5793
+ serializedBytes: published.serializedBytes,
5794
+ };
4495
5795
  });
4496
5796
  },
4497
5797
  acquireInput(value) {
4498
5798
  const updateId = value.updateId;
4499
5799
  const recipientBindingKey = value.recipientBindingKey;
4500
- const executionUpdate = value.executionUpdate === undefined ? undefined : normalizeIncomingJournaledUpdate(value.executionUpdate, path);
5800
+ const executionUpdate = value.executionUpdate === undefined
5801
+ ? undefined
5802
+ : normalizeIncomingJournaledUpdate(value.executionUpdate, path);
4501
5803
  if (!isSafeNonNegativeInteger(updateId) ||
4502
5804
  !isBoundedString(recipientBindingKey, TELEGRAM_UPDATE_JOURNAL_INPUT_BINDING_MAX_LENGTH) ||
4503
- !recipientBindingKey.trim() || (executionUpdate && executionUpdate.update_id !== updateId)) {
5805
+ !recipientBindingKey.trim() ||
5806
+ (executionUpdate && executionUpdate.update_id !== updateId)) {
4504
5807
  throw createJournalError("invalid", path, "received invalid input acquisition identity");
4505
5808
  }
4506
- return runInputMutation("acquire", read => {
5809
+ return runInputMutation("acquire", (read) => {
4507
5810
  const context = currentInputContext();
4508
5811
  const identity = context.owner;
4509
5812
  if (recipientBindingKey !== context.recipientBindingKey) {
4510
5813
  throw createJournalError("conflict", path, "input acquisition targets another execution binding");
4511
5814
  }
4512
5815
  const current = read();
4513
- const entry = current.file.entries.find(candidate => candidate.updateId === updateId);
4514
- if (!entry || entry.state !== "pending" || entry.preApprovalExcluded !== false) {
5816
+ const entry = current.file.entries.find((candidate) => candidate.updateId === updateId);
5817
+ if (!entry ||
5818
+ entry.state !== "pending" ||
5819
+ entry.preApprovalExcluded !== false) {
4515
5820
  throw createJournalError("conflict", path, "input is not eligible for acquisition");
4516
5821
  }
4517
5822
  const projected = executionUpdate ?? entry.update;
4518
5823
  const existing = entry.inputClaim;
4519
5824
  if (existing) {
4520
5825
  if (!isTelegramUpdateJournalQueueOwnerProcess(existing.owner, identity) ||
4521
- existing.owner.sessionGeneration !== identity.sessionGeneration || existing.recipientBindingKey !== recipientBindingKey ||
5826
+ existing.owner.sessionGeneration !== identity.sessionGeneration ||
5827
+ existing.recipientBindingKey !== recipientBindingKey ||
4522
5828
  !isDeepStrictEqual(existing.executionUpdate ?? entry.update, projected)) {
4523
5829
  throw createJournalError("conflict", path, "input already has another owner or execution projection");
4524
5830
  }
4525
- return { acquired: false, receipt: createInputReceipt(updateId, existing.owner) };
5831
+ return {
5832
+ acquired: false,
5833
+ receipt: createInputReceipt(updateId, existing.owner),
5834
+ };
4526
5835
  }
4527
- const claim = { phase: "ready", recipientBindingKey,
4528
- owner: { ...identity, acquisitionId: randomUUID(), acquiredAtMs: getNowMs() },
4529
- ...(!isDeepStrictEqual(projected, entry.update) ? { executionUpdate: projected } : {}) };
4530
- const claimedEntries = current.file.entries.map(candidate => candidate.updateId === updateId ? { ...candidate, inputClaim: claim } : candidate);
4531
- const reservedEntry = { ...entry, inputClaim: createInputHeadroomClaim(entry, "ready") };
5836
+ const claim = {
5837
+ phase: "ready",
5838
+ recipientBindingKey,
5839
+ owner: {
5840
+ ...identity,
5841
+ acquisitionId: randomUUID(),
5842
+ acquiredAtMs: getNowMs(),
5843
+ },
5844
+ ...(!isDeepStrictEqual(projected, entry.update)
5845
+ ? { executionUpdate: projected }
5846
+ : {}),
5847
+ };
5848
+ const claimedEntries = current.file.entries.map((candidate) => candidate.updateId === updateId
5849
+ ? { ...candidate, inputClaim: claim }
5850
+ : candidate);
5851
+ const reservedEntry = {
5852
+ ...entry,
5853
+ inputClaim: createInputHeadroomClaim(entry, "ready"),
5854
+ };
4532
5855
  const reservedEntries = replaceInputHeadroomEntry(current.file, updateId, reservedEntry);
4533
5856
  const actualBytes = Buffer.byteLength(serializeJournalFile({ ...current.file, entries: claimedEntries }));
4534
- const reservedBytes = Buffer.byteLength(serializeJournalFile({ ...current.file, entries: reservedEntries })) +
4535
- TELEGRAM_UPDATE_JOURNAL_INPUT_PROJECTION_HEADROOM_BYTES;
5857
+ const reservedBytes = Buffer.byteLength(serializeJournalFile({ ...current.file, entries: reservedEntries })) + TELEGRAM_UPDATE_JOURNAL_INPUT_PROJECTION_HEADROOM_BYTES;
4536
5858
  if (actualBytes > reservedBytes) {
4537
5859
  throw createJournalError("capacity", path, "execution projection exceeds reserved input headroom");
4538
5860
  }
4539
5861
  publishInputMutation(current, claimedEntries, updateId, context);
4540
- return { acquired: true, receipt: createInputReceipt(updateId, claim.owner) };
5862
+ return {
5863
+ acquired: true,
5864
+ receipt: createInputReceipt(updateId, claim.owner),
5865
+ };
4541
5866
  });
4542
5867
  },
4543
5868
  startInput(value) {
4544
5869
  const receipt = normalizeInputReceipt(value);
4545
- return runInputMutation("start", read => {
5870
+ return runInputMutation("start", (read) => {
4546
5871
  const context = currentInputContext();
4547
5872
  const current = read();
4548
5873
  const entry = ownedInput(current, receipt);
@@ -4555,24 +5880,41 @@ function createJournalStoreCore(options, getInputContext) {
4555
5880
  return { started: false };
4556
5881
  if (claim.handoff)
4557
5882
  throw createJournalError("conflict", path, "input handoff freezes donor execution");
4558
- publishInputMutation(current, current.file.entries.map(candidate => candidate.updateId === receipt.updateId
4559
- ? { ...candidate, inputClaim: { ...claim, phase: "running" } } : candidate), receipt.updateId, context);
4560
- return { started: true, update: structuredClone(claim.executionUpdate ?? entry.update) };
5883
+ publishInputMutation(current, current.file.entries.map((candidate) => candidate.updateId === receipt.updateId
5884
+ ? {
5885
+ ...candidate,
5886
+ inputClaim: { ...claim, phase: "running" },
5887
+ }
5888
+ : candidate), receipt.updateId, context);
5889
+ return {
5890
+ started: true,
5891
+ update: structuredClone(claim.executionUpdate ?? entry.update),
5892
+ };
4561
5893
  });
4562
5894
  },
4563
5895
  completeInput(value) {
4564
5896
  const receipt = normalizeInputReceipt(value);
4565
- return runInputMutation("complete", read => {
5897
+ return runInputMutation("complete", (read) => {
4566
5898
  const current = read();
4567
- const exists = current.file.entries.some(entry => entry.updateId === receipt.updateId);
4568
- if (!exists && current.exists && receipt.updateId <= (current.file.acceptedThroughUpdateId ?? -1)) {
4569
- return { removedUpdateIds: [], entryCount: current.file.entries.length, serializedBytes: current.serializedBytes };
5899
+ const exists = current.file.entries.some((entry) => entry.updateId === receipt.updateId);
5900
+ if (!exists &&
5901
+ current.exists &&
5902
+ receipt.updateId <= (current.file.acceptedThroughUpdateId ?? -1)) {
5903
+ return {
5904
+ removedUpdateIds: [],
5905
+ entryCount: current.file.entries.length,
5906
+ serializedBytes: current.serializedBytes,
5907
+ };
4570
5908
  }
4571
5909
  const entry = ownedInput(current, receipt);
4572
5910
  if (entry.inputClaim.phase !== "running")
4573
5911
  throw createJournalError("conflict", path, "input has not started");
4574
- const published = publishInputSettlement(current, current.file.entries.filter(candidate => candidate.updateId !== receipt.updateId), true);
4575
- return { removedUpdateIds: [receipt.updateId], entryCount: published.file.entries.length, serializedBytes: published.serializedBytes };
5912
+ const published = publishInputSettlement(current, current.file.entries.filter((candidate) => candidate.updateId !== receipt.updateId), true);
5913
+ return {
5914
+ removedUpdateIds: [receipt.updateId],
5915
+ entryCount: published.file.entries.length,
5916
+ serializedBytes: published.serializedBytes,
5917
+ };
4576
5918
  });
4577
5919
  },
4578
5920
  };