@llblab/pi-telegram 0.52.2 → 0.53.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/AGENTS.md +9 -7
  2. package/BACKLOG.md +1 -18
  3. package/CHANGELOG.md +15 -0
  4. package/README.md +7 -11
  5. package/api/commands.ts +3 -0
  6. package/dist/api/commands.d.ts +1 -1
  7. package/dist/lib/activity-verbosity.d.ts +1 -5
  8. package/dist/lib/activity-verbosity.js +15 -42
  9. package/dist/lib/activity.d.ts +9 -0
  10. package/dist/lib/activity.js +39 -10
  11. package/dist/lib/agent-messages.js +7 -3
  12. package/dist/lib/bindings.d.ts +43 -5
  13. package/dist/lib/bindings.js +163 -20
  14. package/dist/lib/bus-api.d.ts +12 -0
  15. package/dist/lib/bus-api.js +182 -79
  16. package/dist/lib/bus-follower.d.ts +111 -31
  17. package/dist/lib/bus-follower.js +1324 -359
  18. package/dist/lib/bus-leader.d.ts +41 -91
  19. package/dist/lib/bus-leader.js +969 -637
  20. package/dist/lib/bus-transport.js +16 -9
  21. package/dist/lib/bus.d.ts +215 -18
  22. package/dist/lib/bus.js +1048 -327
  23. package/dist/lib/channel-posts.d.ts +5 -0
  24. package/dist/lib/channel-posts.js +434 -261
  25. package/dist/lib/command-templates.d.ts +26 -5
  26. package/dist/lib/command-templates.js +92 -176
  27. package/dist/lib/commands.d.ts +168 -82
  28. package/dist/lib/commands.js +1409 -291
  29. package/dist/lib/config.d.ts +1 -8
  30. package/dist/lib/config.js +34 -76
  31. package/dist/lib/delivery.d.ts +2 -2
  32. package/dist/lib/delivery.js +16 -16
  33. package/dist/lib/extension.js +639 -171
  34. package/dist/lib/generative-app-worker.mjs +19 -7
  35. package/dist/lib/generative-apps.d.ts +0 -5
  36. package/dist/lib/generative-apps.js +61 -49
  37. package/dist/lib/inbound.d.ts +5 -0
  38. package/dist/lib/inbound.js +35 -59
  39. package/dist/lib/journal.d.ts +33 -3
  40. package/dist/lib/journal.js +2063 -782
  41. package/dist/lib/lifecycle.d.ts +2 -6
  42. package/dist/lib/lifecycle.js +75 -58
  43. package/dist/lib/locks.d.ts +29 -3
  44. package/dist/lib/locks.js +289 -108
  45. package/dist/lib/logging.js +50 -14
  46. package/dist/lib/media.d.ts +22 -1
  47. package/dist/lib/media.js +46 -28
  48. package/dist/lib/menu-model.d.ts +19 -5
  49. package/dist/lib/menu-model.js +53 -11
  50. package/dist/lib/menu-queue.d.ts +5 -4
  51. package/dist/lib/menu-queue.js +45 -63
  52. package/dist/lib/menu-settings.d.ts +5 -4
  53. package/dist/lib/menu-settings.js +114 -78
  54. package/dist/lib/menu-status.d.ts +1 -1
  55. package/dist/lib/menu-status.js +8 -25
  56. package/dist/lib/menu-thinking.d.ts +6 -1
  57. package/dist/lib/menu-thinking.js +14 -21
  58. package/dist/lib/menu.d.ts +8 -61
  59. package/dist/lib/menu.js +92 -164
  60. package/dist/lib/model.d.ts +1 -1
  61. package/dist/lib/model.js +2 -16
  62. package/dist/lib/outbound-attachments.d.ts +6 -27
  63. package/dist/lib/outbound-attachments.js +45 -38
  64. package/dist/lib/outbound-buttons.js +22 -16
  65. package/dist/lib/outbound-markup.d.ts +0 -2
  66. package/dist/lib/outbound-markup.js +51 -64
  67. package/dist/lib/outbound-voice.js +6 -3
  68. package/dist/lib/outbound.d.ts +6 -6
  69. package/dist/lib/outbound.js +65 -51
  70. package/dist/lib/ownership.js +6 -2
  71. package/dist/lib/paths.d.ts +5 -0
  72. package/dist/lib/paths.js +37 -21
  73. package/dist/lib/pi.d.ts +2 -0
  74. package/dist/lib/pi.js +8 -1
  75. package/dist/lib/polling.d.ts +21 -37
  76. package/dist/lib/polling.js +59 -50
  77. package/dist/lib/preview.js +39 -19
  78. package/dist/lib/process-identity.d.ts +24 -0
  79. package/dist/lib/process-identity.js +133 -0
  80. package/dist/lib/prompts.js +2 -7
  81. package/dist/lib/queue.d.ts +14 -16
  82. package/dist/lib/queue.js +252 -127
  83. package/dist/lib/recovery.d.ts +2 -83
  84. package/dist/lib/recovery.js +20 -274
  85. package/dist/lib/replies.d.ts +16 -24
  86. package/dist/lib/replies.js +99 -116
  87. package/dist/lib/routing.d.ts +120 -7
  88. package/dist/lib/routing.js +4667 -1152
  89. package/dist/lib/runtime.d.ts +1 -11
  90. package/dist/lib/sections.d.ts +4 -29
  91. package/dist/lib/sections.js +7 -20
  92. package/dist/lib/skills.js +4 -3
  93. package/dist/lib/status.js +86 -57
  94. package/dist/lib/sync.d.ts +1 -3
  95. package/dist/lib/sync.js +67 -43
  96. package/dist/lib/target.d.ts +4 -0
  97. package/dist/lib/target.js +18 -0
  98. package/dist/lib/telegram-api.d.ts +18 -13
  99. package/dist/lib/telegram-api.js +148 -53
  100. package/dist/lib/text-groups.js +4 -39
  101. package/dist/lib/thread-cleanup-manager.d.ts +6 -27
  102. package/dist/lib/thread-cleanup-manager.js +369 -186
  103. package/dist/lib/thread-display.d.ts +1 -2
  104. package/dist/lib/thread-display.js +58 -38
  105. package/dist/lib/thread-naming.d.ts +28 -8
  106. package/dist/lib/thread-naming.js +175 -66
  107. package/dist/lib/thread-reconciler.d.ts +42 -16
  108. package/dist/lib/thread-reconciler.js +90 -50
  109. package/dist/lib/threads.d.ts +82 -20
  110. package/dist/lib/threads.js +2247 -855
  111. package/dist/lib/time-injection.js +1 -2
  112. package/dist/lib/turns.js +1 -19
  113. package/dist/lib/updates.d.ts +169 -80
  114. package/dist/lib/updates.js +2418 -641
  115. package/dist/lib/voice.d.ts +7 -22
  116. package/dist/lib/voice.js +7 -22
  117. package/dist/lib/wire.js +1 -1
  118. package/dist/lib/workspace-admission.d.ts +3 -1
  119. package/dist/lib/workspace-admission.js +425 -211
  120. package/dist/lib/workspace-identity.js +8 -13
  121. package/dist/lib/workspace-retirement.d.ts +1 -1
  122. package/dist/lib/workspace-retirement.js +328 -140
  123. package/dist/lib/workspace-slots.js +8 -4
  124. package/dist/package.json +1 -1
  125. package/docs/architecture.md +31 -29
  126. package/docs/inbound.md +1 -1
  127. package/docs/multi-instance-bus.md +15 -3
  128. package/docs/outbound.md +1 -1
  129. package/docs/public-api.md +29 -1
  130. package/docs/ui-style.md +25 -8
  131. package/lib/activity-verbosity.ts +59 -82
  132. package/lib/activity.ts +146 -102
  133. package/lib/agent-messages.ts +12 -5
  134. package/lib/bindings.ts +389 -82
  135. package/lib/bus-api.ts +263 -102
  136. package/lib/bus-follower.ts +2440 -601
  137. package/lib/bus-leader.ts +2209 -1290
  138. package/lib/bus-transport.ts +46 -12
  139. package/lib/bus.ts +1673 -444
  140. package/lib/channel-posts.ts +848 -327
  141. package/lib/command-templates.ts +152 -265
  142. package/lib/commands.ts +2398 -555
  143. package/lib/config.ts +177 -170
  144. package/lib/delivery.ts +41 -29
  145. package/lib/extension.ts +1206 -433
  146. package/lib/generative-app-worker.mjs +19 -7
  147. package/lib/generative-apps.ts +324 -211
  148. package/lib/inbound.ts +95 -97
  149. package/lib/journal.ts +5311 -1763
  150. package/lib/keyboard.ts +1 -3
  151. package/lib/lifecycle.ts +128 -78
  152. package/lib/locks.ts +677 -203
  153. package/lib/logging.ts +147 -58
  154. package/lib/media.ts +82 -43
  155. package/lib/menu-model.ts +85 -18
  156. package/lib/menu-queue.ts +88 -129
  157. package/lib/menu-settings.ts +260 -155
  158. package/lib/menu-status.ts +20 -60
  159. package/lib/menu-thinking.ts +41 -56
  160. package/lib/menu.ts +112 -285
  161. package/lib/model.ts +8 -22
  162. package/lib/outbound-attachments.ts +156 -125
  163. package/lib/outbound-buttons.ts +76 -49
  164. package/lib/outbound-markup.ts +96 -94
  165. package/lib/outbound-voice.ts +33 -27
  166. package/lib/outbound.ts +128 -125
  167. package/lib/ownership.ts +19 -10
  168. package/lib/paths.ts +148 -49
  169. package/lib/pi.ts +35 -15
  170. package/lib/polling.ts +144 -147
  171. package/lib/preview.ts +93 -40
  172. package/lib/process-identity.ts +164 -0
  173. package/lib/prompts.ts +4 -11
  174. package/lib/queue.ts +516 -301
  175. package/lib/recovery.ts +50 -454
  176. package/lib/rendering.ts +1 -3
  177. package/lib/replies.ts +248 -209
  178. package/lib/routing.ts +8509 -2186
  179. package/lib/runtime.ts +1 -12
  180. package/lib/sections.ts +24 -89
  181. package/lib/setup.ts +1 -3
  182. package/lib/skills.ts +9 -4
  183. package/lib/status.ts +215 -110
  184. package/lib/sync.ts +448 -361
  185. package/lib/target.ts +22 -3
  186. package/lib/telegram-api.ts +302 -127
  187. package/lib/text-groups.ts +5 -39
  188. package/lib/thread-cleanup-manager.ts +833 -331
  189. package/lib/thread-display.ts +213 -97
  190. package/lib/thread-naming.ts +232 -87
  191. package/lib/thread-reconciler.ts +155 -72
  192. package/lib/threads.ts +5318 -1845
  193. package/lib/time-injection.ts +1 -4
  194. package/lib/turns.ts +15 -28
  195. package/lib/updates.ts +5184 -1612
  196. package/lib/voice.ts +7 -35
  197. package/lib/wire.ts +1 -1
  198. package/lib/workspace-admission.ts +996 -416
  199. package/lib/workspace-identity.ts +14 -17
  200. package/lib/workspace-retirement.ts +878 -376
  201. package/lib/workspace-slots.ts +22 -13
  202. package/package.json +1 -1
  203. package/scripts/check-downgrade.mjs +0 -472
  204. package/scripts/measure-workspace.mjs +0 -108
@@ -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,73 +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
+ };
287
345
  }
288
346
  function inspectRoutingInputExpiry(file, journalBindingKey, updateId) {
289
- const disposition = file.operatorDispositions?.find(value => value.updateId === updateId &&
290
- "dispositionKind" in value && value.dispositionKind === "legacy-custody" && value.action === "discard" &&
347
+ const disposition = file.operatorDispositions?.find((value) => value.updateId === updateId &&
348
+ "dispositionKind" in value &&
349
+ value.dispositionKind === "legacy-custody" &&
350
+ value.action === "discard" &&
291
351
  value.failureId === `routing-expiry:${value.evidenceSha256}`);
292
- if (!disposition || !("operatorAuthorityId" in disposition) || file.entries.some(entry => entry.updateId === updateId))
352
+ if (!disposition ||
353
+ !("operatorAuthorityId" in disposition) ||
354
+ file.entries.some((entry) => entry.updateId === updateId))
293
355
  return undefined;
294
- return { journalBindingKey, updateId, operatorAuthorityId: disposition.operatorAuthorityId };
356
+ return {
357
+ journalBindingKey,
358
+ updateId,
359
+ operatorAuthorityId: disposition.operatorAuthorityId,
360
+ };
295
361
  }
296
362
  function inspectRoutingGroupExpiry(file, journalBindingKey, updateIds) {
297
- if (!Array.isArray(updateIds) || !updateIds.length || updateIds.length > TELEGRAM_UPDATE_JOURNAL_MAX_ENTRIES ||
298
- updateIds.some((id, index) => !isSafeNonNegativeInteger(id) || (index > 0 && id <= updateIds[index - 1])))
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])))
299
368
  return undefined;
300
- if (file.entries.some(entry => updateIds.includes(entry.updateId)))
369
+ if (file.entries.some((entry) => updateIds.includes(entry.updateId)))
301
370
  return undefined;
302
- const expired = updateIds.map(id => inspectRoutingInputExpiry(file, journalBindingKey, id)).filter(value => value !== undefined);
303
- if (!expired.length || expired.some(value => value.operatorAuthorityId !== expired[0].operatorAuthorityId))
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))
304
376
  return undefined;
305
377
  // Some members may have been acknowledged earlier. Whole donor absence plus one exact expiry ends this known cohort,
306
378
  // not recipient custody; fresh Thread protection still independently gates deletion.
307
- return updateIds.map(updateId => ({ journalBindingKey, updateId, operatorAuthorityId: expired[0].operatorAuthorityId }));
379
+ return updateIds.map((updateId) => ({
380
+ journalBindingKey,
381
+ updateId,
382
+ operatorAuthorityId: expired[0].operatorAuthorityId,
383
+ }));
308
384
  }
309
385
  function createJournalError(code, path, detail, cause) {
310
386
  return new TelegramUpdateJournalError(code, path, `Telegram update journal ${detail}: ${path}`, cause === undefined ? undefined : { cause });
@@ -439,11 +515,11 @@ function createTelegramUpdateQueueHandoffId(input) {
439
515
  .slice(0, 32)}`;
440
516
  }
441
517
  function isTelegramInputHandoffId(value) {
442
- return isBoundedString(value, TELEGRAM_UPDATE_JOURNAL_QUEUE_HANDOFF_ID_MAX_LENGTH) &&
443
- /^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));
444
519
  }
445
520
  function createTelegramInputHandoffId(input) {
446
- return `input-handoff-${createHash("sha256").update(JSON.stringify({
521
+ return `input-handoff-${createHash("sha256")
522
+ .update(JSON.stringify({
447
523
  version: 1,
448
524
  token: input.handoffToken,
449
525
  source: input.journalBindingKey,
@@ -451,7 +527,9 @@ function createTelegramInputHandoffId(input) {
451
527
  donorOwner: input.donorOwner,
452
528
  recipientOwner: input.recipientOwner,
453
529
  recipientBindingKey: input.recipientBindingKey,
454
- })).digest("hex").slice(0, 32)}`;
530
+ }))
531
+ .digest("hex")
532
+ .slice(0, 32)}`;
455
533
  }
456
534
  function validateJournalHandoff(value, path, kind) {
457
535
  if (!isRecord(value) ||
@@ -501,38 +579,72 @@ function createTelegramUpdateTerminalFailureId(input) {
501
579
  }
502
580
  function validateJournalOperatorDisposition(value, path) {
503
581
  if (isRecord(value) && value.dispositionKind === "legacy-custody") {
504
- if (!hasOnlyKeys(value, ["dispositionKind", "failureId", "updateId", "action",
505
- "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
+ ]) ||
506
592
  !isBoundedString(value.failureId, TELEGRAM_UPDATE_JOURNAL_FAILURE_ID_MAX_LENGTH) ||
507
593
  !isSafeNonNegativeInteger(value.updateId) ||
508
594
  (value.action !== "requeue-v3" && value.action !== "discard") ||
509
595
  !isSafeNonNegativeInteger(value.committedAtMs) ||
510
- 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) ||
511
598
  !isBoundedString(value.operatorAuthorityId, TELEGRAM_UPDATE_JOURNAL_FAILURE_ID_MAX_LENGTH) ||
512
- !isSafeNonNegativeInteger(value.authorizedAtMs) || value.committedAtMs < value.authorizedAtMs)
599
+ !isSafeNonNegativeInteger(value.authorizedAtMs) ||
600
+ value.committedAtMs < value.authorizedAtMs)
513
601
  throw createJournalError("invalid", path, "contains invalid legacy custody disposition metadata");
514
- return { dispositionKind: "legacy-custody", failureId: value.failureId,
515
- updateId: value.updateId, action: value.action, committedAtMs: value.committedAtMs,
516
- evidenceSha256: value.evidenceSha256, operatorAuthorityId: value.operatorAuthorityId,
517
- 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
+ };
518
612
  }
519
- if (!isRecord(value) || !hasOnlyKeys(value, ["failureId", "updateId", "action",
520
- "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
+ ]) ||
521
624
  !isBoundedString(value.failureId, TELEGRAM_UPDATE_JOURNAL_FAILURE_ID_MAX_LENGTH) ||
522
625
  !isSafeNonNegativeInteger(value.updateId) ||
523
626
  (value.action !== "retry" && value.action !== "discard") ||
524
- !isSafeNonNegativeInteger(value.committedAtMs) || !isSafePositiveInteger(value.attemptCount) ||
627
+ !isSafeNonNegativeInteger(value.committedAtMs) ||
628
+ !isSafePositiveInteger(value.attemptCount) ||
525
629
  !isBoundedString(value.failureClass, TELEGRAM_UPDATE_JOURNAL_FAILURE_CLASS_MAX_LENGTH) ||
526
- !isSafeNonNegativeInteger(value.terminalAtMs) || value.committedAtMs < value.terminalAtMs ||
630
+ !isSafeNonNegativeInteger(value.terminalAtMs) ||
631
+ value.committedAtMs < value.terminalAtMs ||
527
632
  !isBoundedString(value.terminalReason, TELEGRAM_UPDATE_JOURNAL_TERMINAL_REASON_MAX_LENGTH))
528
633
  throw createJournalError("invalid", path, "contains invalid operator disposition metadata");
529
- return { failureId: value.failureId, updateId: value.updateId, action: value.action,
530
- committedAtMs: value.committedAtMs, attemptCount: value.attemptCount,
531
- failureClass: value.failureClass, terminalAtMs: value.terminalAtMs,
532
- 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
+ };
533
644
  }
534
645
  function journalRequiresExclusion(version) {
535
- return version === TELEGRAM_UPDATE_JOURNAL_EXCLUSION_VERSION || journalHasCustodyFields(version);
646
+ return (version === TELEGRAM_UPDATE_JOURNAL_EXCLUSION_VERSION ||
647
+ journalHasCustodyFields(version));
536
648
  }
537
649
  function journalHasCustodyFields(version) {
538
650
  return version === TELEGRAM_UPDATE_JOURNAL_CUSTODY_VERSION;
@@ -543,21 +655,33 @@ export function isTelegramUpdateJournalLegacyFamilyVersion(version) {
543
655
  }
544
656
  function validateJournalInputClaim(value, path, updateId) {
545
657
  if (!isRecord(value) ||
546
- !hasOnlyKeys(value, ["phase", "owner", "recipientBindingKey", "handoff", "executionUpdate"]) ||
658
+ !hasOnlyKeys(value, [
659
+ "phase",
660
+ "owner",
661
+ "recipientBindingKey",
662
+ "handoff",
663
+ "executionUpdate",
664
+ ]) ||
547
665
  (value.phase !== "ready" && value.phase !== "running") ||
548
666
  !isBoundedString(value.recipientBindingKey, TELEGRAM_UPDATE_JOURNAL_INPUT_BINDING_MAX_LENGTH) ||
549
667
  !value.recipientBindingKey.trim()) {
550
668
  throw createJournalError("invalid", path, "contains invalid input claim metadata");
551
669
  }
552
670
  const owner = validateJournalQueueOwner(value.owner, path);
553
- const handoff = value.handoff === undefined ? undefined : validateJournalHandoff(value.handoff, path, "input");
554
- if ((owner.handoffId !== undefined && !isTelegramInputHandoffId(owner.handoffId)) ||
555
- (handoff && (!isTelegramInputHandoffId(handoff.handoffId) || value.phase !== "ready" ||
556
- 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)))) {
557
680
  throw createJournalError("invalid", path, "contains conflicting input handoff metadata");
558
681
  }
559
682
  const executionUpdate = value.executionUpdate === undefined
560
- ? undefined : validateJournaledUpdate(value.executionUpdate, path);
683
+ ? undefined
684
+ : validateJournaledUpdate(value.executionUpdate, path);
561
685
  if (executionUpdate && executionUpdate.update_id !== updateId) {
562
686
  throw createJournalError("invalid", path, "contains an input claim/update id mismatch");
563
687
  }
@@ -577,20 +701,26 @@ function validateJournalInputProvenance(value, path, updateId) {
577
701
  throw createJournalError("invalid", path, "contains invalid input queue provenance");
578
702
  }
579
703
  const owner = validateJournalQueueOwner(value.owner, path);
580
- if (owner.handoffId !== undefined && !isTelegramInputHandoffId(owner.handoffId)) {
704
+ if (owner.handoffId !== undefined &&
705
+ !isTelegramInputHandoffId(owner.handoffId)) {
581
706
  throw createJournalError("invalid", path, "contains invalid input queue provenance");
582
707
  }
583
708
  const executionUpdate = value.executionUpdate === undefined
584
- ? undefined : validateJournaledUpdate(value.executionUpdate, path);
709
+ ? undefined
710
+ : validateJournaledUpdate(value.executionUpdate, path);
585
711
  if (executionUpdate && executionUpdate.update_id !== updateId) {
586
712
  throw createJournalError("invalid", path, "contains an input provenance/update id mismatch");
587
713
  }
588
- return { owner, recipientBindingKey: value.recipientBindingKey,
589
- ...(executionUpdate ? { executionUpdate } : {}) };
714
+ return {
715
+ owner,
716
+ recipientBindingKey: value.recipientBindingKey,
717
+ ...(executionUpdate ? { executionUpdate } : {}),
718
+ };
590
719
  }
591
720
  function validateJournalEntry(value, path, version) {
592
721
  if (journalRequiresExclusion(version) &&
593
- (!isRecord(value) || typeof value.preApprovalExcluded !== "boolean" ||
722
+ (!isRecord(value) ||
723
+ typeof value.preApprovalExcluded !== "boolean" ||
594
724
  (value.preApprovalExcluded && value.state === "queued"))) {
595
725
  throw createJournalError("pairing-evidence", path, "contains missing, malformed, or queued exclusion evidence");
596
726
  }
@@ -599,7 +729,9 @@ function validateJournalEntry(value, path, version) {
599
729
  "updateId",
600
730
  "update",
601
731
  "admittedAtMs",
602
- ...(isTelegramUpdateJournalLegacyFamilyVersion(version) ? ["routingInput"] : []),
732
+ ...(isTelegramUpdateJournalLegacyFamilyVersion(version)
733
+ ? ["routingInput"]
734
+ : []),
603
735
  "state",
604
736
  "queueKind",
605
737
  "queueReceiptId",
@@ -612,7 +744,8 @@ function validateJournalEntry(value, path, version) {
612
744
  "terminalFailureId",
613
745
  ...(journalRequiresExclusion(version) ? ["preApprovalExcluded"] : []),
614
746
  ...(journalHasCustodyFields(version)
615
- ? ["inputClaim", "inputProvenance"] : []),
747
+ ? ["inputClaim", "inputProvenance"]
748
+ : []),
616
749
  ]) ||
617
750
  !isSafeNonNegativeInteger(value.updateId) ||
618
751
  !isSafeNonNegativeInteger(value.admittedAtMs) ||
@@ -629,16 +762,39 @@ function validateJournalEntry(value, path, version) {
629
762
  let routingInput;
630
763
  if (value.routingInput !== undefined) {
631
764
  const routing = value.routingInput;
632
- if (!isRecord(routing) || !hasOnlyKeys(routing, ["operatorUserId", "publishedAtMs", "expiresAtMs", "phase"]) ||
633
- !isSafePositiveInteger(routing.operatorUserId) || !isSafeNonNegativeInteger(routing.publishedAtMs) ||
634
- routing.publishedAtMs < value.admittedAtMs || !isSafeNonNegativeInteger(routing.expiresAtMs) ||
635
- 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 ||
636
781
  (routing.phase !== "waiting" && routing.phase !== "selected") ||
637
782
  (routing.phase === "waiting" && value.state === "queued")) {
638
783
  throw createJournalError("invalid", path, "contains invalid routing input lifetime");
639
784
  }
640
- routingInput = { operatorUserId: routing.operatorUserId, publishedAtMs: routing.publishedAtMs,
641
- 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
+ };
642
798
  }
643
799
  const queueKind = value.queueKind;
644
800
  const queueReceiptId = value.queueReceiptId;
@@ -675,11 +831,14 @@ function validateJournalEntry(value, path, version) {
675
831
  throw createJournalError("pairing-evidence", path, "contains claimed exclusion evidence");
676
832
  }
677
833
  const inputClaim = value.inputClaim === undefined
678
- ? undefined : validateJournalInputClaim(value.inputClaim, path, value.updateId);
834
+ ? undefined
835
+ : validateJournalInputClaim(value.inputClaim, path, value.updateId);
679
836
  const inputProvenance = value.inputProvenance === undefined
680
- ? undefined : validateJournalInputProvenance(value.inputProvenance, path, value.updateId);
681
- if ((inputClaim && (value.state === "queued" ||
682
- (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"))) ||
683
842
  (inputProvenance && (value.state !== "queued" || inputClaim))) {
684
843
  throw createJournalError("invalid", path, "contains conflicting input claim state");
685
844
  }
@@ -711,15 +870,14 @@ function validateJournalEntry(value, path, version) {
711
870
  updateId: value.updateId,
712
871
  update,
713
872
  ...(journalRequiresExclusion(version)
714
- ? { preApprovalExcluded: value.preApprovalExcluded } : {}),
873
+ ? { preApprovalExcluded: value.preApprovalExcluded }
874
+ : {}),
715
875
  ...(inputClaim ? { inputClaim } : {}),
716
876
  ...(inputProvenance ? { inputProvenance } : {}),
717
877
  admittedAtMs: value.admittedAtMs,
718
878
  ...(routingInput ? { routingInput } : {}),
719
879
  state: value.state,
720
- ...(queueKind === "prompt" || queueKind === "control"
721
- ? { queueKind }
722
- : {}),
880
+ ...(queueKind === "prompt" || queueKind === "control" ? { queueKind } : {}),
723
881
  ...(isNonEmptyString(queueReceiptId) ? { queueReceiptId } : {}),
724
882
  ...(queueOwner ? { queueOwner } : {}),
725
883
  ...(queueHandoff ? { queueHandoff } : {}),
@@ -750,40 +908,56 @@ function validateJournalEntry(value, path, version) {
750
908
  };
751
909
  }
752
910
  function assertSupportedJournalVersion(value, path, version = TELEGRAM_UPDATE_JOURNAL_VERSION) {
753
- if (isRecord(value) && Number.isSafeInteger(value.version) &&
911
+ if (isRecord(value) &&
912
+ Number.isSafeInteger(value.version) &&
754
913
  value.version !== version) {
755
914
  throw createJournalError("unsupported-version", path, `uses unsupported version ${String(value.version)}`);
756
915
  }
757
916
  }
758
917
  function validateJournalSourceCompletion(value, path) {
759
- if (!isRecord(value) || !hasOnlyKeys(value, ["updateId", "sourceSha256", "completionSha256"]) ||
760
- !isSafeNonNegativeInteger(value.updateId) || typeof value.sourceSha256 !== "string" || !/^[a-f0-9]{64}$/u.test(value.sourceSha256) ||
761
- 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)) {
762
925
  throw createJournalError("invalid", path, "has invalid source completion evidence");
763
926
  }
764
- 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
+ };
765
932
  }
766
933
  function validateJournalSourceCompletions(value, path, version, allowEmpty = false) {
767
- if (!isTelegramUpdateJournalLegacyFamilyVersion(version) || !Array.isArray(value) || !allowEmpty && !value.length) {
934
+ if (!isTelegramUpdateJournalLegacyFamilyVersion(version) ||
935
+ !Array.isArray(value) ||
936
+ (!allowEmpty && !value.length)) {
768
937
  throw createJournalError("invalid", path, "has invalid source completion collection");
769
938
  }
770
- const completions = value.map(item => validateJournalSourceCompletion(item, path));
939
+ const completions = value.map((item) => validateJournalSourceCompletion(item, path));
771
940
  const scopes = new Set();
772
941
  for (let index = 0; index < completions.length; index++) {
773
942
  const completion = completions[index];
774
- 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)) {
775
945
  throw createJournalError("invalid", path, "has duplicate or unordered source completion evidence");
776
946
  }
777
947
  scopes.add(completion.completionSha256);
778
948
  }
779
949
  return completions;
780
950
  }
781
- function parseJournalFile(value, path, version = TELEGRAM_UPDATE_JOURNAL_VERSION) {
782
- assertSupportedJournalVersion(value, path, version);
951
+ function assertJournalExclusionCursor(value, path, version) {
783
952
  if (journalRequiresExclusion(version) &&
784
- (!isRecord(value) || !isSafeNonNegativeInteger(value.acceptedThroughUpdateId))) {
953
+ (!isRecord(value) ||
954
+ !isSafeNonNegativeInteger(value.acceptedThroughUpdateId))) {
785
955
  throw createJournalError("pairing-evidence", path, "is missing its exclusion-mode admission cursor");
786
956
  }
957
+ }
958
+ function parseJournalFile(value, path, version = TELEGRAM_UPDATE_JOURNAL_VERSION) {
959
+ assertSupportedJournalVersion(value, path, version);
960
+ assertJournalExclusionCursor(value, path, version);
787
961
  if (!isRecord(value) ||
788
962
  !hasOnlyKeys(value, [
789
963
  "version",
@@ -819,9 +993,7 @@ function parseJournalFile(value, path, version = TELEGRAM_UPDATE_JOURNAL_VERSION
819
993
  }
820
994
  const queuedReceipts = new Map();
821
995
  for (const entry of entries) {
822
- if (entry.state !== "queued" ||
823
- !entry.queueKind ||
824
- !entry.queueReceiptId) {
996
+ if (entry.state !== "queued" || !entry.queueKind || !entry.queueReceiptId) {
825
997
  continue;
826
998
  }
827
999
  const existing = queuedReceipts.get(entry.queueReceiptId);
@@ -852,12 +1024,18 @@ function parseJournalFile(value, path, version = TELEGRAM_UPDATE_JOURNAL_VERSION
852
1024
  }
853
1025
  }
854
1026
  const operatorDispositions = (value.operatorDispositions ?? []).map((disposition) => validateJournalOperatorDisposition(disposition, path));
855
- const sourceCompletions = value.sourceCompletions === undefined ? [] : validateJournalSourceCompletions(value.sourceCompletions, path, version);
1027
+ const sourceCompletions = value.sourceCompletions === undefined
1028
+ ? []
1029
+ : validateJournalSourceCompletions(value.sourceCompletions, path, version);
856
1030
  const botIdentity = validateBotIdentity(value.botIdentity, path);
857
- const activeIds = new Set(entries.map(entry => entry.updateId));
858
- const discardedIds = new Set(operatorDispositions.filter(disposition => disposition.action === "discard").map(disposition => disposition.updateId));
859
- if (sourceCompletions.some(completion => activeIds.has(completion.updateId) || discardedIds.has(completion.updateId) ||
860
- 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))) {
861
1039
  throw createJournalError("invalid", path, "has contradictory source completion evidence");
862
1040
  }
863
1041
  const dispositionFailureIds = new Set();
@@ -889,11 +1067,15 @@ function parseJournalFile(value, path, version = TELEGRAM_UPDATE_JOURNAL_VERSION
889
1067
  };
890
1068
  }
891
1069
  function assertJournalRoutingInputContinuity(file, entries, path) {
892
- 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]));
893
1071
  for (const entry of entries) {
894
1072
  const retained = previous.get(entry.updateId), next = entry.routingInput;
895
- if (retained && (!next || next.operatorUserId !== retained.operatorUserId || next.publishedAtMs !== retained.publishedAtMs ||
896
- 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"))) {
897
1079
  throw createJournalError("invalid", path, "regresses retained routing input lifetime");
898
1080
  }
899
1081
  }
@@ -902,14 +1084,20 @@ function assertJournalSourceCompletionContinuity(file, segment, path) {
902
1084
  assertJournalRoutingInputContinuity(file, segment.upsertedEntries, path);
903
1085
  if (segment.sourceCompletions === undefined)
904
1086
  return;
905
- const previous = new Map((file.sourceCompletions ?? []).map(completion => [completion.updateId, completion]));
906
- 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
+ ]));
907
1095
  if ([...previous].some(([id, completion]) => !isDeepStrictEqual(completion, next.get(id)))) {
908
1096
  throw createJournalError("invalid", path, "regresses retained source completion evidence");
909
1097
  }
910
- const entries = new Map(file.entries.map(entry => [entry.updateId, entry]));
911
- const removed = new Set(segment.removedUpdateIds), upserted = new Set(segment.upsertedEntries.map(entry => entry.updateId));
912
- 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));
913
1101
  // Grouped markers share one whole-receipt check rather than rescanning the group per source.
914
1102
  const queueGroups = new Map(), queueRemovals = new Map();
915
1103
  for (const entry of file.entries) {
@@ -926,8 +1114,15 @@ function assertJournalSourceCompletionContinuity(file, segment, path) {
926
1114
  if (queueRemovals.has(receiptId))
927
1115
  return queueRemovals.get(receiptId);
928
1116
  const group = queueGroups.get(receiptId);
929
- 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 &&
930
- 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));
931
1126
  queueRemovals.set(receiptId, valid);
932
1127
  return valid;
933
1128
  };
@@ -935,18 +1130,19 @@ function assertJournalSourceCompletionContinuity(file, segment, path) {
935
1130
  if (previous.has(completion.updateId))
936
1131
  continue;
937
1132
  const source = entries.get(completion.updateId);
938
- if (!removed.has(completion.updateId) || !source || (source.state === "queued" && !queueWasRemoved(source)) || source.state === "failed" ||
939
- 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) {
940
1139
  throw createJournalError("invalid", path, "source completion does not match its removal revision");
941
1140
  }
942
1141
  }
943
1142
  }
944
1143
  function parseJournalSegment(value, path, version = TELEGRAM_UPDATE_JOURNAL_VERSION) {
945
1144
  assertSupportedJournalVersion(value, path, version);
946
- if (journalRequiresExclusion(version) &&
947
- (!isRecord(value) || !isSafeNonNegativeInteger(value.acceptedThroughUpdateId))) {
948
- throw createJournalError("pairing-evidence", path, "is missing its exclusion-mode admission cursor");
949
- }
1145
+ assertJournalExclusionCursor(value, path, version);
950
1146
  if (!isRecord(value) ||
951
1147
  !hasOnlyKeys(value, [
952
1148
  "version",
@@ -1013,7 +1209,11 @@ function parseJournalSegment(value, path, version = TELEGRAM_UPDATE_JOURNAL_VERS
1013
1209
  ...(value.operatorDispositions !== undefined
1014
1210
  ? { operatorDispositions }
1015
1211
  : {}),
1016
- ...(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
+ : {}),
1017
1217
  };
1018
1218
  }
1019
1219
  /**
@@ -1046,7 +1246,10 @@ export function inspectTelegramUpdateJournalFamily(input) {
1046
1246
  /** Classifies private originals against exact discard tombstones; never replay or deletion authority. */
1047
1247
  export function inspectTelegramUpdateJournalRetention(input) {
1048
1248
  const acquired = acquireTelegramUpdateJournalFamily(input, undefined, true);
1049
- return { evidence: acquired.evidence, retainedInputs: acquired.retainedInputs ?? [] };
1249
+ return {
1250
+ evidence: acquired.evidence,
1251
+ retainedInputs: acquired.retainedInputs ?? [],
1252
+ };
1050
1253
  }
1051
1254
  /**
1052
1255
  * Read-only source evidence for cooperating writers serialized by the caller through
@@ -1073,9 +1276,18 @@ function acquireTelegramUpdateJournalFamily(input, selectedVersion, inspectReten
1073
1276
  const { path, limits } = input;
1074
1277
  const acquiredSegments = [];
1075
1278
  let snapshotRevision = 0;
1076
- const fail = (message) => { throw createJournalError("invalid", path, message); };
1077
- const capacity = () => { throw createJournalError("capacity", path, "exceeds inspection resource limits"); };
1078
- 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
+ ]) {
1079
1291
  if (!isSafePositiveInteger(limits[key]))
1080
1292
  fail("requires positive safe-integer limits");
1081
1293
  }
@@ -1084,21 +1296,34 @@ function acquireTelegramUpdateJournalFamily(input, selectedVersion, inspectReten
1084
1296
  fail("requires an exact profile");
1085
1297
  const anchor = input.directory;
1086
1298
  const contained = relative(anchor, path);
1087
- if (!isAbsolute(anchor) || resolve(anchor) !== anchor || !isAbsolute(path) ||
1088
- resolve(path) !== path || !contained || contained === ".." ||
1089
- 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))
1090
1307
  fail("escapes its canonical directory anchor");
1091
- if (!constants.O_NOFOLLOW || !constants.O_NONBLOCK)
1092
- fail("platform lacks no-follow nonblocking open evidence");
1093
1308
  let files = 0;
1094
1309
  let bytes = 0;
1095
1310
  let work = 0;
1096
1311
  const observed = new Map();
1097
1312
  const ancestors = new Set();
1098
- const sameAncestor = (a, b) => b.isDirectory() && !b.isSymbolicLink() && a.dev === b.dev && a.ino === b.ino &&
1099
- a.mode === b.mode && a.uid === b.uid && a.gid === b.gid;
1100
- const same = (a, b) => a.dev === b.dev && a.ino === b.ino && a.mode === b.mode &&
1101
- 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;
1102
1327
  const status = (target) => {
1103
1328
  try {
1104
1329
  return lstatSync(target, { bigint: true });
@@ -1113,7 +1338,9 @@ function acquireTelegramUpdateJournalFamily(input, selectedVersion, inspectReten
1113
1338
  const value = status(target);
1114
1339
  if (!value && !optional)
1115
1340
  fail("has a missing path component");
1116
- if (value && (value.isSymbolicLink() || !(directory ? value.isDirectory() : value.isFile())))
1341
+ if (value &&
1342
+ (value.isSymbolicLink() ||
1343
+ !(directory ? value.isDirectory() : value.isFile())))
1117
1344
  fail("has a linked or unexpected file type");
1118
1345
  observed.set(target, value);
1119
1346
  return value;
@@ -1121,11 +1348,18 @@ function acquireTelegramUpdateJournalFamily(input, selectedVersion, inspectReten
1121
1348
  const charge = (value) => {
1122
1349
  if (!isRecord(value))
1123
1350
  return;
1124
- 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
+ ]) {
1125
1358
  const collection = value[key];
1126
1359
  if (!Array.isArray(collection))
1127
1360
  continue;
1128
- if (collection.length > limits.maxEntries || collection.length > limits.maxWork - work)
1361
+ if (collection.length > limits.maxEntries ||
1362
+ collection.length > limits.maxWork - work)
1129
1363
  capacity();
1130
1364
  work += collection.length;
1131
1365
  }
@@ -1133,7 +1367,7 @@ function acquireTelegramUpdateJournalFamily(input, selectedVersion, inspectReten
1133
1367
  const read = (target, before) => {
1134
1368
  if (before.size > BigInt(limits.maxBytes - bytes))
1135
1369
  capacity();
1136
- const fd = openSync(target, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
1370
+ const fd = openSync(target, TELEGRAM_STRICT_READ_FLAGS);
1137
1371
  try {
1138
1372
  const opened = fstatSync(fd, { bigint: true });
1139
1373
  if (!opened.isFile() || !same(before, opened))
@@ -1173,7 +1407,9 @@ function acquireTelegramUpdateJournalFamily(input, selectedVersion, inspectReten
1173
1407
  break;
1174
1408
  if (names.length >= limits.maxFiles - usedFiles)
1175
1409
  capacity();
1176
- if (!pattern.test(entry.name) || !entry.isFile() || entry.isSymbolicLink())
1410
+ if (!pattern.test(entry.name) ||
1411
+ !entry.isFile() ||
1412
+ entry.isSymbolicLink())
1177
1413
  fail("has an unexpected journal evidence entry");
1178
1414
  names.push(entry.name);
1179
1415
  }
@@ -1189,7 +1425,9 @@ function acquireTelegramUpdateJournalFamily(input, selectedVersion, inspectReten
1189
1425
  if (realpathSync(anchor) !== anchor)
1190
1426
  fail("requires a canonical directory anchor");
1191
1427
  let parent = anchor;
1192
- 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)) {
1193
1431
  parent = join(parent, component);
1194
1432
  observe(parent, true);
1195
1433
  ancestors.add(parent);
@@ -1203,15 +1441,19 @@ function acquireTelegramUpdateJournalFamily(input, selectedVersion, inspectReten
1203
1441
  let file;
1204
1442
  let knownBotId = expected.botId;
1205
1443
  const identity = (value) => {
1206
- if (value.profile !== input.profile || value.botIdentity.tokenSha256 !== expected.tokenSha256 ||
1207
- (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)) {
1208
1449
  throw createJournalError("identity-mismatch", path, "belongs to another exact journal identity");
1209
1450
  }
1210
1451
  knownBotId ??= value.botIdentity.botId;
1211
1452
  };
1212
1453
  if (snapshot) {
1213
1454
  const raw = read(path, snapshot);
1214
- 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)) {
1215
1457
  throw createJournalError("unsupported-version", path, "has an unsupported snapshot version");
1216
1458
  }
1217
1459
  file = parseJournalFile(raw, path, raw.version);
@@ -1222,11 +1464,18 @@ function acquireTelegramUpdateJournalFamily(input, selectedVersion, inspectReten
1222
1464
  const metadata = observe(target, false);
1223
1465
  const previousWork = work;
1224
1466
  const segment = parseJournalSegment(read(target, metadata), target, file.version);
1225
- acquiredSegments.push({ name, bytes: Number(metadata.size), work: work - previousWork,
1226
- ...(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
+ });
1227
1475
  files += 1;
1228
1476
  identity(segment);
1229
- 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)
1230
1479
  fail("has an invalid intrinsic segment revision");
1231
1480
  const dispositionIds = new Set();
1232
1481
  for (const disposition of segment.operatorDispositions ?? []) {
@@ -1237,19 +1486,24 @@ function acquireTelegramUpdateJournalFamily(input, selectedVersion, inspectReten
1237
1486
  const revision = file.revision ?? 0;
1238
1487
  if (segment.revision <= revision)
1239
1488
  continue;
1240
- if (segment.revision !== revision + 1 || segment.previousRevision !== revision)
1489
+ if (segment.revision !== revision + 1 ||
1490
+ segment.previousRevision !== revision)
1241
1491
  fail("has a newer revision gap");
1242
- if (segment.acceptedThroughUpdateId !== undefined && file.acceptedThroughUpdateId !== undefined &&
1492
+ if (segment.acceptedThroughUpdateId !== undefined &&
1493
+ file.acceptedThroughUpdateId !== undefined &&
1243
1494
  segment.acceptedThroughUpdateId < file.acceptedThroughUpdateId)
1244
1495
  fail("regresses the admission cursor");
1245
1496
  assertJournalSourceCompletionContinuity(file, segment, target);
1246
- const entries = new Map(file.entries.map(entry => [entry.updateId, entry]));
1497
+ const entries = new Map(file.entries.map((entry) => [entry.updateId, entry]));
1247
1498
  for (const id of segment.removedUpdateIds)
1248
1499
  entries.delete(id);
1249
1500
  for (const entry of segment.upsertedEntries) {
1250
1501
  const previous = entries.get(entry.updateId);
1251
- if (journalRequiresExclusion(file.version) && ((previous && previous.preApprovalExcluded !== entry.preApprovalExcluded) ||
1252
- (!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)))) {
1253
1507
  throw createJournalError("pairing-evidence", target, "changes exclusion evidence or resurrects a settled source");
1254
1508
  }
1255
1509
  if (!entries.has(entry.updateId) && entries.size >= limits.maxEntries)
@@ -1259,7 +1513,9 @@ function acquireTelegramUpdateJournalFamily(input, selectedVersion, inspectReten
1259
1513
  const next = {
1260
1514
  ...file,
1261
1515
  revision: segment.revision,
1262
- ...(segment.acceptedThroughUpdateId !== undefined ? { acceptedThroughUpdateId: segment.acceptedThroughUpdateId } : {}),
1516
+ ...(segment.acceptedThroughUpdateId !== undefined
1517
+ ? { acceptedThroughUpdateId: segment.acceptedThroughUpdateId }
1518
+ : {}),
1263
1519
  entries: [...entries.values()].sort((a, b) => a.updateId - b.updateId),
1264
1520
  operatorDispositions: segment.operatorDispositions ?? file.operatorDispositions,
1265
1521
  sourceCompletions: segment.sourceCompletions ?? file.sourceCompletions,
@@ -1270,16 +1526,24 @@ function acquireTelegramUpdateJournalFamily(input, selectedVersion, inspectReten
1270
1526
  }
1271
1527
  const retainedInputs = [];
1272
1528
  const retentionDirectory = `${path}.retained`;
1273
- const retention = inspectRetention ? observe(retentionDirectory, true, true) : undefined;
1529
+ const retention = inspectRetention
1530
+ ? observe(retentionDirectory, true, true)
1531
+ : undefined;
1274
1532
  const retentionPattern = /^abandon-[a-f0-9]{64}\.json$/u;
1275
- const retentionNames = retention ? enumerate(retentionDirectory, retentionPattern) : [];
1533
+ const retentionNames = retention
1534
+ ? enumerate(retentionDirectory, retentionPattern)
1535
+ : [];
1276
1536
  if (retentionNames.length > limits.maxEntries)
1277
1537
  capacity();
1278
1538
  if (retention) {
1279
1539
  if (!file || file.version !== 1)
1280
1540
  fail("private retention requires its exact v1 journal snapshot");
1281
1541
  const current = file;
1282
- 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
+ })));
1283
1547
  const updateIds = new Set();
1284
1548
  for (const name of retentionNames) {
1285
1549
  const target = join(retentionDirectory, name);
@@ -1293,42 +1557,70 @@ function acquireTelegramUpdateJournalFamily(input, selectedVersion, inspectReten
1293
1557
  work += 2;
1294
1558
  if (!isRecord(raw))
1295
1559
  throw createJournalError("invalid", target, "private retention is not an evidence record");
1296
- if (typeof raw.journalBindingKey !== "string" || !bindingKeys.has(raw.journalBindingKey))
1560
+ if (typeof raw.journalBindingKey !== "string" ||
1561
+ !bindingKeys.has(raw.journalBindingKey))
1297
1562
  throw createJournalError("invalid", target, "has foreign private retention evidence");
1298
1563
  const entry = validateJournalEntry(raw.entry, target, 1);
1299
1564
  const disposition = validateJournalOperatorDisposition(raw.requestedDisposition, target);
1300
- const digest = createHash("sha256").update(JSON.stringify({ journalBindingKey: raw.journalBindingKey, entry })).digest("hex");
1301
- if (!("dispositionKind" in disposition) || disposition.dispositionKind !== "legacy-custody" ||
1302
- disposition.action !== "discard" || entry.state !== "pending" || disposition.updateId !== entry.updateId ||
1303
- disposition.evidenceSha256 !== digest || disposition.failureId !== `abandon-${digest}` ||
1304
- name !== `${disposition.failureId}.json` || !isDeepStrictEqual(raw, { version: 1, kind: "pending-input-retention",
1305
- 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
+ }))
1306
1583
  fail("has inconsistent private retention evidence");
1307
1584
  if (updateIds.has(entry.updateId))
1308
1585
  fail("has duplicate private retention sources");
1309
1586
  updateIds.add(entry.updateId);
1310
- const committed = current.operatorDispositions?.find(value => value.failureId === disposition.failureId || value.updateId === entry.updateId);
1311
- 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)))
1312
1592
  fail("private retention contradicts its journal disposition");
1313
- retainedInputs.push({ path: target, journalBindingKey: raw.journalBindingKey,
1314
- 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
+ });
1315
1600
  }
1316
1601
  }
1317
- if (inspectRetention && file?.operatorDispositions?.some(disposition => "dispositionKind" in disposition && disposition.dispositionKind === "legacy-custody" &&
1318
- disposition.action === "discard" && /^abandon-[a-f0-9]{64}$/u.test(disposition.failureId) &&
1319
- !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")))
1320
1609
  fail("committed abandonment lost its private original");
1321
1610
  // Re-enumeration has the same bound, rather than allocating an unbounded census.
1322
1611
  files = snapshot ? 1 : 0;
1323
1612
  if (segments && !isDeepStrictEqual(names, enumerate()))
1324
1613
  fail("changed segment namespace");
1325
1614
  files += names.length;
1326
- if (retention && !isDeepStrictEqual(retentionNames, enumerate(retentionDirectory, retentionPattern)))
1615
+ if (retention &&
1616
+ !isDeepStrictEqual(retentionNames, enumerate(retentionDirectory, retentionPattern)))
1327
1617
  fail("changed private retention namespace");
1328
1618
  files += retentionNames.length;
1329
1619
  for (const [target, before] of observed) {
1330
1620
  const after = status(target);
1331
- const compare = selectedVersion !== undefined && ancestors.has(target) ? sameAncestor : same;
1621
+ const compare = selectedVersion !== undefined && ancestors.has(target)
1622
+ ? sameAncestor
1623
+ : same;
1332
1624
  if (before ? !after || !compare(before, after) : after !== undefined)
1333
1625
  fail("changed observed namespace or file");
1334
1626
  }
@@ -1339,14 +1631,27 @@ function acquireTelegramUpdateJournalFamily(input, selectedVersion, inspectReten
1339
1631
  if (file.version !== selectedVersion) {
1340
1632
  throw createJournalError("unsupported-version", path, "does not match the selected source version");
1341
1633
  }
1342
- if (createTelegramUpdateJournalReceiptScope({ profileName: input.profile, botIdentity: expected }) !==
1343
- 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
+ })) {
1344
1642
  throw createJournalError("identity-mismatch", path, "does not retain the expected receipt scope");
1345
1643
  }
1346
1644
  }
1347
1645
  }
1348
1646
  return {
1349
- 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" },
1350
1655
  snapshotRevision,
1351
1656
  segments: acquiredSegments,
1352
1657
  ...(inspectRetention ? { retainedInputs } : {}),
@@ -1367,9 +1672,19 @@ export function inspectTelegramSessionJournalNamespace(input) {
1367
1672
  }
1368
1673
  function inspectTelegramJournalNamespace(input, includeSessions) {
1369
1674
  const { directory, profile, limits } = input;
1370
- const fail = (message) => { throw createJournalError("invalid", directory, message); };
1371
- const capacity = () => { throw createJournalError("capacity", directory, "exceeds namespace inspection resource limits"); };
1372
- 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
+ ]) {
1373
1688
  if (!isSafePositiveInteger(limits[key]))
1374
1689
  fail("requires positive safe-integer limits");
1375
1690
  }
@@ -1378,26 +1693,37 @@ function inspectTelegramJournalNamespace(input, includeSessions) {
1378
1693
  const expected = validateBotIdentity(input.botIdentity, directory);
1379
1694
  if (!isAbsolute(directory) || resolve(directory) !== directory)
1380
1695
  fail("requires a canonical directory anchor");
1381
- if (!constants.O_NOFOLLOW || !constants.O_NONBLOCK)
1382
- fail("platform lacks no-follow nonblocking open evidence");
1383
1696
  const metadata = (target) => {
1384
1697
  const value = lstatSync(target, { bigint: true });
1385
- return { dev: value.dev, ino: value.ino, mode: value.mode, nlink: value.nlink,
1386
- 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
+ };
1387
1707
  };
1388
1708
  const suffix = profile === "default" ? "" : `.${profile}`;
1389
1709
  const pollingPath = input.pollingPath ?? join(directory, `inbox${suffix}.json`);
1390
1710
  const polling = relative(directory, pollingPath);
1391
1711
  {
1392
1712
  const parts = polling.split(sep);
1393
- if (!isAbsolute(pollingPath) || resolve(pollingPath) !== pollingPath || parts.at(-1) !== `inbox${suffix}.json` ||
1394
- !(parts.length === 1 || (parts.length === 3 && parts[0] === "sessions" &&
1395
- 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)))
1396
1720
  fail("requires a canonical polling journal path");
1397
1721
  }
1398
1722
  try {
1399
1723
  const root = lstatSync(directory);
1400
- if (!root.isDirectory() || root.isSymbolicLink() || realpathSync(directory) !== directory)
1724
+ if (!root.isDirectory() ||
1725
+ root.isSymbolicLink() ||
1726
+ realpathSync(directory) !== directory)
1401
1727
  fail("requires a canonical directory anchor");
1402
1728
  const beforeRoot = metadata(directory);
1403
1729
  const census = () => {
@@ -1426,7 +1752,8 @@ function inspectTelegramJournalNamespace(input, includeSessions) {
1426
1752
  };
1427
1753
  const requireType = (target, isDirectory) => {
1428
1754
  const value = lstatSync(target);
1429
- if (value.isSymbolicLink() || !(isDirectory ? value.isDirectory() : value.isFile()) ||
1755
+ if (value.isSymbolicLink() ||
1756
+ !(isDirectory ? value.isDirectory() : value.isFile()) ||
1430
1757
  (!isDirectory && value.nlink !== 1))
1431
1758
  fail("has a linked or unexpected journal type");
1432
1759
  };
@@ -1472,7 +1799,9 @@ function inspectTelegramJournalNamespace(input, includeSessions) {
1472
1799
  if (!/inbox/iu.test(name))
1473
1800
  return;
1474
1801
  const match = /^(inbox|follower-inbox-[a-f0-9]{16})(?:\.([a-z0-9]{1,32}))?\.json(\.(?:segments|retained))?$/u.exec(name);
1475
- if (!match || match[2] === "default" || (!includeSessions && match[3] === ".retained"))
1802
+ if (!match ||
1803
+ match[2] === "default" ||
1804
+ (!includeSessions && match[3] === ".retained"))
1476
1805
  fail("has a noncanonical journal-like entry");
1477
1806
  requireType(target, !!match[3]);
1478
1807
  if (includeSessions && match[3])
@@ -1482,26 +1811,58 @@ function inspectTelegramJournalNamespace(input, includeSessions) {
1482
1811
  if (familyProfile === profile && base !== pollingPath)
1483
1812
  families.set(base, match[1] === "inbox" ? "session" : "follower");
1484
1813
  });
1485
- return { entries: [...entries].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0),
1486
- 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
+ };
1487
1819
  };
1488
1820
  const before = census();
1489
- const sourceInventory = [[pollingPath, "polling"], ...before.families];
1821
+ const sourceInventory = [
1822
+ [pollingPath, "polling"],
1823
+ ...before.families,
1824
+ ];
1490
1825
  const sources = [];
1491
1826
  const retainedInputs = [];
1492
- 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
+ };
1493
1833
  let knownBotId = expected.botId;
1494
1834
  for (const [path, role] of sourceInventory) {
1495
- const remaining = { maxFiles: limits.maxFiles - accounting.files, maxBytes: limits.maxBytes - accounting.bytes,
1496
- maxWork: limits.maxWork - accounting.work, maxEntries: limits.maxEntries };
1497
- 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)
1498
1844
  capacity();
1499
- const acquire = () => acquireTelegramUpdateJournalFamily({ directory, path, profile,
1500
- botIdentity: { ...expected, ...(knownBotId !== undefined ? { botId: knownBotId } : {}) }, limits: remaining }, undefined, includeSessions);
1501
- 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();
1502
1858
  const evidence = acquired.evidence;
1503
- retainedInputs.push(...(acquired.retainedInputs ?? []).map(original => ({ ...original, journalPath: path })));
1504
- 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`))) {
1505
1866
  fail("lost a discovered journal family");
1506
1867
  }
1507
1868
  if (evidence.kind === "present") {
@@ -1509,13 +1870,15 @@ function inspectTelegramJournalNamespace(input, includeSessions) {
1509
1870
  accounting.files += evidence.accounting.files;
1510
1871
  accounting.bytes += evidence.accounting.bytes;
1511
1872
  }
1512
- 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;
1513
1875
  sources.push({ role, path, evidence });
1514
1876
  }
1515
1877
  const namespaceCurrent = () => {
1516
1878
  try {
1517
- return isDeepStrictEqual(beforeRoot, metadata(directory)) && isDeepStrictEqual(before, census()) &&
1518
- isDeepStrictEqual(beforeRoot, metadata(directory));
1879
+ return (isDeepStrictEqual(beforeRoot, metadata(directory)) &&
1880
+ isDeepStrictEqual(before, census()) &&
1881
+ isDeepStrictEqual(beforeRoot, metadata(directory)));
1519
1882
  }
1520
1883
  catch {
1521
1884
  return false;
@@ -1523,7 +1886,12 @@ function inspectTelegramJournalNamespace(input, includeSessions) {
1523
1886
  };
1524
1887
  if (!namespaceCurrent())
1525
1888
  fail("changed observed root namespace or identity");
1526
- return { sources, accounting, ...(includeSessions ? { retainedInputs } : {}), ...(knownBotId !== undefined ? { knownBotId } : {}) };
1889
+ return {
1890
+ sources,
1891
+ accounting,
1892
+ ...(includeSessions ? { retainedInputs } : {}),
1893
+ ...(knownBotId !== undefined ? { knownBotId } : {}),
1894
+ };
1527
1895
  }
1528
1896
  catch (error) {
1529
1897
  if (error instanceof TelegramUpdateJournalError)
@@ -1531,35 +1899,105 @@ function inspectTelegramJournalNamespace(input, includeSessions) {
1531
1899
  throw createJournalError("io", directory, "could not establish canonical namespace evidence", error);
1532
1900
  }
1533
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
+ ];
1534
1931
  /** Complete-empty protection only; caller holds source serialization through consumption. Never deletion authority. */
1535
1932
  export function isTelegramThreadCleanupJournalNamespaceClear(input) {
1536
- if (!input.requiredJournalBindingKeys.length || input.requiredJournalBindingKeys.length > input.limits.maxDirectoryEntries)
1933
+ if (!input.requiredJournalBindingKeys.length ||
1934
+ input.requiredJournalBindingKeys.length > input.limits.maxDirectoryEntries)
1537
1935
  throw new Error("Telegram temporary cleanup requires bounded exact journal references.");
1538
1936
  const requiredPaths = new Set();
1539
1937
  for (const key of input.requiredJournalBindingKeys) {
1540
1938
  const path = getTelegramUpdateJournalBindingPath(key);
1541
- const identities = [input.botIdentity, { tokenSha256: input.botIdentity.tokenSha256 }];
1542
- if (!path || !identities.some(botIdentity => key === createTelegramUpdateJournalBindingKey({
1543
- path, profileName: input.profile, botIdentity,
1544
- })))
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
+ })))
1545
1950
  throw new Error("Telegram temporary cleanup has foreign or malformed journal references.");
1546
1951
  requiredPaths.add(path);
1547
1952
  }
1548
1953
  const namespace = inspectTelegramSessionJournalNamespace(input);
1549
- if (namespace.sources.filter(source => source.role === "polling").length !== 1 ||
1550
- namespace.retainedInputs?.some(original => original.state !== "committed") ||
1551
- [...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")))
1552
1958
  return false;
1553
- // Original Telegram targets do not certify execution ownership. No nonempty family gets a source exemption.
1554
- 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)));
1555
1990
  }
1556
1991
  function cloneEntry(entry) {
1557
1992
  return {
1558
1993
  ...entry,
1559
1994
  update: structuredClone(entry.update),
1560
- ...(entry.inputClaim ? { inputClaim: structuredClone(entry.inputClaim) } : {}),
1995
+ ...(entry.inputClaim
1996
+ ? { inputClaim: structuredClone(entry.inputClaim) }
1997
+ : {}),
1561
1998
  ...(entry.inputProvenance
1562
- ? { inputProvenance: structuredClone(entry.inputProvenance) } : {}),
1999
+ ? { inputProvenance: structuredClone(entry.inputProvenance) }
2000
+ : {}),
1563
2001
  ...(entry.queueOwner
1564
2002
  ? { queueOwner: cloneJournalQueueOwner(entry.queueOwner) }
1565
2003
  : {}),
@@ -1580,7 +2018,13 @@ function cloneFile(file) {
1580
2018
  profile: file.profile,
1581
2019
  botIdentity: { ...file.botIdentity },
1582
2020
  entries: file.entries.map(cloneEntry),
1583
- ...(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
+ : {}),
1584
2028
  ...(file.operatorDispositions?.length
1585
2029
  ? {
1586
2030
  operatorDispositions: file.operatorDispositions.map((disposition) => ({ ...disposition })),
@@ -1611,7 +2055,8 @@ function publishTelegramUpdateJournalSegmentUnlocked(path, segment, onPublicatio
1611
2055
  let snapshotRevision = 0;
1612
2056
  try {
1613
2057
  const parsed = JSON.parse(readFileSync(path, "utf8"));
1614
- snapshotRevision = parseJournalFile(parsed, path, segment.version).revision ?? 0;
2058
+ snapshotRevision =
2059
+ parseJournalFile(parsed, path, segment.version).revision ?? 0;
1615
2060
  }
1616
2061
  catch (error) {
1617
2062
  if (error?.code !== "ENOENT")
@@ -1682,7 +2127,9 @@ function mergeBotIdentity(stored, current) {
1682
2127
  }
1683
2128
  function createPendingRetentionReference(input) {
1684
2129
  const { path, journalBindingKey, entry, maxBytes } = input;
1685
- 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");
1686
2133
  const failureId = `abandon-${evidenceSha256}`;
1687
2134
  const retainedDirectory = `${path}.retained`;
1688
2135
  const retainedPath = join(retainedDirectory, `${failureId}.json`);
@@ -1708,22 +2155,37 @@ function createPendingRetentionReference(input) {
1708
2155
  return undefined;
1709
2156
  throw error;
1710
2157
  }
1711
- if (!stat.isFile() || stat.isSymbolicLink() || stat.size > maxBytes + 4096) {
2158
+ if (!stat.isFile() ||
2159
+ stat.isSymbolicLink() ||
2160
+ stat.size > maxBytes + 4096) {
1712
2161
  throw createJournalError("invalid", retainedPath, "retained input is not a bounded regular file");
1713
2162
  }
1714
2163
  const retained = JSON.parse(readFileSync(retainedPath, "utf8"));
1715
2164
  if (!isRecord(retained))
1716
2165
  throw createJournalError("invalid", retainedPath, "retained input is not an evidence record");
1717
2166
  const disposition = validateJournalOperatorDisposition(retained.requestedDisposition, retainedPath);
1718
- if (!("dispositionKind" in disposition) || disposition.dispositionKind !== "legacy-custody" ||
1719
- disposition.failureId !== failureId || disposition.updateId !== entry.updateId ||
1720
- disposition.action !== "discard" || disposition.evidenceSha256 !== evidenceSha256 ||
1721
- !isDeepStrictEqual(retained, { version: 1, kind: "pending-input-retention", journalBindingKey,
1722
- 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
+ })) {
1723
2180
  throw createJournalError("conflict", retainedPath, "retained input does not match abandonment evidence");
1724
2181
  }
1725
- return { version: 1, kind: "pending-input-retention", journalBindingKey, entry,
1726
- requestedDisposition: disposition };
2182
+ return {
2183
+ version: 1,
2184
+ kind: "pending-input-retention",
2185
+ journalBindingKey,
2186
+ entry,
2187
+ requestedDisposition: disposition,
2188
+ };
1727
2189
  };
1728
2190
  return { evidenceSha256, failureId, retainedPath, read };
1729
2191
  }
@@ -1847,26 +2309,47 @@ export function createTelegramUpdateJournalRuntimeBindingResolver(deps) {
1847
2309
  });
1848
2310
  const path = deps.getJournalPath(configuredProfileName);
1849
2311
  const workspaceAdmission = deps.getWorkspaceAdmission?.();
1850
- const sourceAccess = { directory: dirname(path), limits: {
1851
- maxFiles: 1024, maxBytes: TELEGRAM_UPDATE_JOURNAL_MAX_BYTES * 2,
1852
- maxEntries: TELEGRAM_UPDATE_JOURNAL_MAX_ENTRIES, maxWork: TELEGRAM_UPDATE_JOURNAL_MAX_ENTRIES * 1024,
1853
- } };
1854
- const options = { path, profileName, botIdentity,
1855
- ...(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
+ : {}),
1856
2328
  ...(workspaceAdmission ? { workspaceAdmission } : {}),
1857
- ...(deps.withWriterAdmission ? { withWriterAdmission: deps.withWriterAdmission } : {}),
1858
- ...(deps.withSourceSerialization ? { withSourceSerialization: deps.withSourceSerialization } : {}),
1859
- ...(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
+ };
1860
2337
  const journal = createTelegramUpdateJournalStore(options);
1861
- // Scoped completion and receipt observation use strict private handles where the platform provides no-follow
1862
- // evidence; elsewhere (Windows) the same capabilities read through the ordinary journal so queue receipts still work.
1863
- 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;
1864
2340
  const completionJournal = deps.withSourceSerialization
1865
- ? createTelegramUpdateJournalStore({ ...options, ...(strict ? { sourceAccess } : {}) }) : undefined;
2341
+ ? createTelegramUpdateJournalStore({
2342
+ ...options,
2343
+ ...(strict ? { sourceAccess } : {}),
2344
+ })
2345
+ : undefined;
1866
2346
  // Queue receipt readiness gates every ordinary prompt; without strict handles it uses the ordinary journal read.
1867
2347
  const observeQueuedReceipt = (expected) => strict
1868
2348
  ? completionJournal.inspectQueuedReceipt(expected)
1869
- : inspectJournalQueuedReceipt(journal.read(), { ...expected, queueOwner: validateQueuedReceiptObservation(expected, path) }, path);
2349
+ : inspectJournalQueuedReceipt(journal.read(), {
2350
+ ...expected,
2351
+ queueOwner: validateQueuedReceiptObservation(expected, path),
2352
+ }, path);
1870
2353
  return {
1871
2354
  runtimeKey: JSON.stringify({
1872
2355
  path,
@@ -1881,25 +2364,48 @@ export function createTelegramUpdateJournalRuntimeBindingResolver(deps) {
1881
2364
  readForProtection() {
1882
2365
  // Protection must never turn corruption/recovery into empty-work evidence.
1883
2366
  const evidence = inspectTelegramUpdateJournalFamily({
1884
- ...sourceAccess, path, profile: profileName, botIdentity,
2367
+ ...sourceAccess,
2368
+ path,
2369
+ profile: profileName,
2370
+ botIdentity,
1885
2371
  });
1886
- 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
+ };
1887
2376
  },
1888
- journal: completionJournal ? { ...journal,
1889
- removeCompletedExact(updateIds, expectedSources, completions) {
1890
- return completions !== undefined ? completionJournal.removeCompletedExact(updateIds, expectedSources, completions)
1891
- : journal.removeCompletedExact(updateIds, expectedSources);
1892
- },
1893
- completeQueuedExact(receipts, completions) { return completionJournal.completeQueuedExact(receipts, completions); },
1894
- inspectSourceCompletion(expected) { return completionJournal.inspectSourceCompletion(expected); },
1895
- inspectQueuedReceipt: observeQueuedReceipt,
1896
- isQueueReceiptCurrent(receipt, owner) {
1897
- if (receipt.journalBindingKey !== createTelegramUpdateJournalBindingKey({ path, profileName, botIdentity }))
1898
- return false;
1899
- return observeQueuedReceipt({ queueKind: receipt.queueKind, receiptId: receipt.receiptId,
1900
- sourceUpdateIds: [...receipt.sourceUpdateIds], queueOwner: { ...owner } }) !== undefined;
1901
- },
1902
- } : 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,
1903
2409
  };
1904
2410
  };
1905
2411
  }
@@ -1920,12 +2426,13 @@ export function createTelegramUpdateJournalReferenceRegistry(input = {}) {
1920
2426
  released = true;
1921
2427
  };
1922
2428
  },
1923
- list: () => [...active.values()].map(reference => ({ ...reference })),
2429
+ list: () => [...active.values()].map((reference) => ({ ...reference })),
1924
2430
  withReference(reference, operation) {
1925
2431
  const release = this.acquire(reference);
1926
2432
  try {
1927
2433
  const result = operation();
1928
- if (result && typeof result.finally === "function")
2434
+ if (result &&
2435
+ typeof result.finally === "function")
1929
2436
  return result.finally(release);
1930
2437
  release();
1931
2438
  return result;
@@ -1941,15 +2448,23 @@ export function withTelegramResolvedUpdateJournalReference(input) {
1941
2448
  const binding = input.resolveBinding();
1942
2449
  if (!binding)
1943
2450
  return undefined;
1944
- return input.registry.withReference({ referenceClass: input.referenceClass,
1945
- recoveryKey: binding.recoveryKey }, () => input.operation(binding));
2451
+ return input.registry.withReference({ referenceClass: input.referenceClass, recoveryKey: binding.recoveryKey }, () => input.operation(binding));
1946
2452
  }
1947
2453
  function validateQueuedReceiptObservation(expected, path) {
1948
- 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") ||
1949
2462
  !isNonEmptyString(expected.receiptId) ||
1950
- !Array.isArray(expected.sourceUpdateIds) || expected.sourceUpdateIds.length === 0 ||
2463
+ !Array.isArray(expected.sourceUpdateIds) ||
2464
+ expected.sourceUpdateIds.length === 0 ||
1951
2465
  expected.sourceUpdateIds.length > TELEGRAM_UPDATE_JOURNAL_MAX_ENTRIES ||
1952
- 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]))) {
1953
2468
  throw createJournalError("invalid", path, "received invalid queue receipt observation authority");
1954
2469
  }
1955
2470
  return validateJournalQueueOwner(expected.queueOwner, path);
@@ -1958,19 +2473,31 @@ function inspectJournalQueuedReceipt(file, expected, path) {
1958
2473
  if (!isTelegramUpdateJournalLegacyFamilyVersion(file.version))
1959
2474
  throw createJournalError("invalid", path, "queue receipt observation requires an exact legacy v1 source handle");
1960
2475
  const { queueOwner } = expected;
1961
- const entries = file.entries.filter(entry => entry.queueReceiptId === expected.receiptId);
1962
- if (entries.length !== expected.sourceUpdateIds.length || entries.some((entry, index) => entry.updateId !== expected.sourceUpdateIds[index] || entry.state !== "queued" || entry.queueKind !== expected.queueKind ||
1963
- 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)))
1964
2483
  return undefined;
1965
- return { receipt: { queueKind: expected.queueKind, receiptId: expected.receiptId,
1966
- 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
+ },
1967
2491
  sources: entries.map(createTelegramUpdateJournalEntryDigest),
1968
- queueOwnerSha256: createHash("sha256").update(JSON.stringify(queueOwner)).digest("hex") };
2492
+ queueOwnerSha256: createHash("sha256")
2493
+ .update(JSON.stringify(queueOwner))
2494
+ .digest("hex"),
2495
+ };
1969
2496
  }
1970
2497
  function inspectJournalSourceCompletion(file, completion, path) {
1971
2498
  if (!isTelegramUpdateJournalLegacyFamilyVersion(file.version))
1972
2499
  throw createJournalError("invalid", path, "source completion requires a legacy v1 source");
1973
- const retained = file.sourceCompletions?.find(value => value.updateId === completion.updateId);
2500
+ const retained = file.sourceCompletions?.find((value) => value.updateId === completion.updateId);
1974
2501
  if (!retained)
1975
2502
  return undefined;
1976
2503
  if (!isDeepStrictEqual(retained, completion))
@@ -1980,35 +2507,48 @@ function inspectJournalSourceCompletion(file, completion, path) {
1980
2507
  /** Operator-distinct authority: routing never treats it as an owner cancellation. */
1981
2508
  export const TELEGRAM_SESSION_ADOPTION_AUTHORITY_PREFIX = "session-successor:";
1982
2509
  function isAdoptablePendingEntry(entry) {
1983
- return entry.state === "pending" && !entry.inputClaim && !entry.inputProvenance && !entry.routingInput &&
1984
- !entry.queueOwner && !entry.queueHandoff && !entry.queueReceiptId && !entry.queueKind && !entry.failure &&
1985
- 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);
1986
2520
  }
1987
2521
  export function createTelegramUpdateJournalBindingRuntime(deps) {
1988
2522
  const resolveLeader = createTelegramUpdateJournalRuntimeBindingResolver({
1989
2523
  ...deps.base,
1990
2524
  getJournalPath: deps.getLeaderJournalPath,
1991
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
+ });
1992
2543
  const createFollowerResolver = (bindingKey, includeQueueRuntimeIdentity, sessionId, legacy = false) => {
1993
2544
  // Session-aware composition must not fall back to a process journal before preparation supplies its ID.
1994
2545
  if (!legacy && deps.getActiveFollowerSessionId && !sessionId)
1995
2546
  return () => undefined;
1996
2547
  return createTelegramUpdateJournalRuntimeBindingResolver({
1997
- getProfileName: deps.base.getProfileName,
1998
- getBotToken: deps.base.getBotToken,
1999
- getBotId: deps.base.getBotId,
2548
+ ...getSharedResolverPorts(),
2000
2549
  ...(includeQueueRuntimeIdentity && deps.base.getQueueRuntimeIdentity
2001
2550
  ? { getQueueRuntimeIdentity: deps.base.getQueueRuntimeIdentity }
2002
2551
  : {}),
2003
- ...(deps.base.withWriterAdmission
2004
- ? { withWriterAdmission: deps.base.withWriterAdmission } : {}),
2005
- // Follower journals need the same strict exact-receipt ports as the leader; without them queue publication cannot complete.
2006
- ...(deps.base.withSourceSerialization
2007
- ? { withSourceSerialization: deps.base.withSourceSerialization } : {}),
2008
- ...(deps.base.getWorkspaceAdmission
2009
- ? { getWorkspaceAdmission: deps.base.getWorkspaceAdmission }
2010
- : {}),
2011
- ...(deps.base.onRecovery ? { onRecovery: deps.base.onRecovery } : {}),
2012
2552
  getJournalPath(profileName) {
2013
2553
  return deps.getFollowerJournalPath(bindingKey, profileName, sessionId);
2014
2554
  },
@@ -2022,8 +2562,15 @@ export function createTelegramUpdateJournalBindingRuntime(deps) {
2022
2562
  const profile = deps.base.getProfileName() ?? TELEGRAM_DEFAULT_PROFILE_NAME;
2023
2563
  if (!path || !token)
2024
2564
  return undefined;
2025
- const botIdentity = createTelegramUpdateJournalBotIdentity({ botToken: token, botId });
2026
- 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
+ }));
2027
2574
  if (!keys.includes(journalBindingKey))
2028
2575
  return undefined;
2029
2576
  const getRoot = () => deps.getRuntimeDir?.() ?? dirname(deps.getLeaderJournalPath(profile));
@@ -2032,23 +2579,43 @@ export function createTelegramUpdateJournalBindingRuntime(deps) {
2032
2579
  const suffix = escapeTelegramJournalPathPattern(getTelegramProfilePathSuffix(profile));
2033
2580
  const flat = new RegExp(`^(?:inbox|follower-inbox-[a-f0-9]{16})${suffix}\\.json$`, "u");
2034
2581
  const session = new RegExp(`^(?:journal\\.[a-f0-9]{16}|inbox)${suffix}\\.json$`, "u");
2035
- if (!isAbsolute(root) || resolve(root) !== root || !isAbsolute(path) || resolve(path) !== path ||
2036
- !(parts.length === 1 && flat.test(parts[0]) || parts.length === 3 && parts[0] === "sessions" &&
2037
- 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]))))
2038
2591
  return undefined;
2039
- const current = () => deps.base.getBotToken() === token && deps.base.getBotId() === botId &&
2040
- (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;
2041
2597
  const inspect = () => {
2042
2598
  if (!current())
2043
2599
  return undefined;
2044
- const observed = inspectTelegramUpdateJournalRetention({ directory: root, path, profile, botIdentity,
2045
- limits: { maxFiles: 1024, maxBytes: TELEGRAM_UPDATE_JOURNAL_MAX_BYTES * 2,
2046
- 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
+ });
2047
2612
  if (observed.evidence.kind !== "present" || !current())
2048
2613
  return undefined;
2049
2614
  return operation(observed.evidence.file, observed.retainedInputs, path);
2050
2615
  };
2051
- const result = deps.base.withSourceSerialization ? deps.base.withSourceSerialization(inspect) : inspect();
2616
+ const result = deps.base.withSourceSerialization
2617
+ ? deps.base.withSourceSerialization(inspect)
2618
+ : inspect();
2052
2619
  return current() ? result : undefined;
2053
2620
  };
2054
2621
  let preparedFollower;
@@ -2063,9 +2630,15 @@ export function createTelegramUpdateJournalBindingRuntime(deps) {
2063
2630
  if (!sessionId)
2064
2631
  return undefined;
2065
2632
  const previous = preparedFollower;
2066
- const result = previous && previous.recipientBindingKey === recipientBindingKey && previous.sessionId !== sessionId
2067
- ? runtime.adoptPredecessorPending({ recipientBindingKey, predecessorSessionId: previous.sessionId,
2068
- 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
+ })
2069
2642
  : undefined;
2070
2643
  // Only a completed adoption advances the predecessor; failures retry idempotently on the next preparation.
2071
2644
  preparedFollower = { recipientBindingKey, sessionId };
@@ -2075,18 +2648,26 @@ export function createTelegramUpdateJournalBindingRuntime(deps) {
2075
2648
  if (!isSafeNonNegativeInteger(updateId))
2076
2649
  throw new Error("Telegram abandonment proof requires an exact update ID.");
2077
2650
  return inspectHistoricalSource(journalBindingKey, (file, originals) => {
2078
- const original = originals.find(value => value.updateId === updateId && value.state === "committed" &&
2651
+ const original = originals.find((value) => value.updateId === updateId &&
2652
+ value.state === "committed" &&
2079
2653
  value.journalBindingKey === journalBindingKey);
2080
2654
  if (!original)
2081
2655
  return undefined;
2082
- const disposition = file.operatorDispositions?.find(value => value.failureId === original.failureId);
2083
- 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")
2084
2660
  return undefined;
2085
- return { journalBindingKey, updateId, retainedPath: original.path, operatorAuthorityId: disposition.operatorAuthorityId };
2661
+ return {
2662
+ journalBindingKey,
2663
+ updateId,
2664
+ retainedPath: original.path,
2665
+ operatorAuthorityId: disposition.operatorAuthorityId,
2666
+ };
2086
2667
  });
2087
2668
  },
2088
2669
  inspectSourceGroupExpiry(journalBindingKey, updateIds) {
2089
- return inspectHistoricalSource(journalBindingKey, file => inspectRoutingGroupExpiry(file, journalBindingKey, updateIds));
2670
+ return inspectHistoricalSource(journalBindingKey, (file) => inspectRoutingGroupExpiry(file, journalBindingKey, updateIds));
2090
2671
  },
2091
2672
  inspectSourceCompletion(journalBindingKey, expected) {
2092
2673
  const completion = validateJournalSourceCompletion(expected, getTelegramUpdateJournalBindingPath(journalBindingKey) ?? "journal");
@@ -2097,9 +2678,12 @@ export function createTelegramUpdateJournalBindingRuntime(deps) {
2097
2678
  return inspectHistoricalSource(journalBindingKey, (file, _originals, path) => inspectJournalQueuedReceipt(file, { ...expected, queueOwner }, path));
2098
2679
  },
2099
2680
  adoptPredecessorPending(input) {
2100
- const { recipientBindingKey, predecessorSessionId, successorSessionId, isCurrent } = input;
2101
- if (!recipientBindingKey || !predecessorSessionId || !successorSessionId ||
2102
- 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") {
2103
2687
  throw new Error("Telegram session adoption requires distinct exact session identities.");
2104
2688
  }
2105
2689
  const assertCurrent = () => {
@@ -2109,18 +2693,29 @@ export function createTelegramUpdateJournalBindingRuntime(deps) {
2109
2693
  assertCurrent();
2110
2694
  const predecessor = createFollowerResolver(recipientBindingKey, false, predecessorSessionId)();
2111
2695
  const successor = createFollowerResolver(recipientBindingKey, false, successorSessionId)();
2112
- if (!predecessor?.readForProtection || !successor || predecessor.recoveryKey === successor.recoveryKey) {
2696
+ if (!predecessor?.readForProtection ||
2697
+ !successor ||
2698
+ predecessor.recoveryKey === successor.recoveryKey) {
2113
2699
  throw new Error("Telegram session adoption requires exact predecessor and successor journals.");
2114
2700
  }
2115
- const result = { adoptedUpdateIds: [], retainedUpdateIds: [] };
2701
+ const result = {
2702
+ adoptedUpdateIds: [],
2703
+ retainedUpdateIds: [],
2704
+ };
2116
2705
  // Non-repairing selection; the abandonment CAS rechecks each exact entry under the source lock.
2117
- const candidates = predecessor.readForProtection().entries.filter(isAdoptablePendingEntry)
2706
+ const candidates = predecessor
2707
+ .readForProtection()
2708
+ .entries.filter(isAdoptablePendingEntry)
2118
2709
  .sort((left, right) => left.updateId - right.updateId);
2119
2710
  for (const entry of candidates) {
2120
2711
  assertCurrent();
2121
2712
  // Commit away first: a crash afterwards preserves the private original instead of duplicating execution.
2122
- const committed = predecessor.journal.abandonPending({ journalBindingKey: predecessor.recoveryKey, entry,
2123
- 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
+ });
2124
2719
  if (committed.duplicate) {
2125
2720
  result.retainedUpdateIds.push(entry.updateId);
2126
2721
  continue;
@@ -2134,17 +2729,7 @@ export function createTelegramUpdateJournalBindingRuntime(deps) {
2134
2729
  createRecipientResolver: (bindingKey, sessionId) => createFollowerResolver(bindingKey, false, sessionId),
2135
2730
  createLegacyRecipientResolver: (bindingKey) => createFollowerResolver(bindingKey, false, undefined, true),
2136
2731
  createPathResolver: (path) => createTelegramUpdateJournalRuntimeBindingResolver({
2137
- getProfileName: deps.base.getProfileName,
2138
- getBotToken: deps.base.getBotToken,
2139
- getBotId: deps.base.getBotId,
2140
- ...(deps.base.withWriterAdmission
2141
- ? { withWriterAdmission: deps.base.withWriterAdmission } : {}),
2142
- ...(deps.base.withSourceSerialization
2143
- ? { withSourceSerialization: deps.base.withSourceSerialization } : {}),
2144
- ...(deps.base.getWorkspaceAdmission
2145
- ? { getWorkspaceAdmission: deps.base.getWorkspaceAdmission }
2146
- : {}),
2147
- ...(deps.base.onRecovery ? { onRecovery: deps.base.onRecovery } : {}),
2732
+ ...getSharedResolverPorts(),
2148
2733
  getJournalPath: () => path,
2149
2734
  }),
2150
2735
  };
@@ -2156,15 +2741,26 @@ export function createTelegramUpdateJournalStore(options) {
2156
2741
  /** Opt-in v3 only; does not migrate old files or expose legacy unowned mutation ports. */
2157
2742
  export function createTelegramInputJournalStore(options) {
2158
2743
  const captured = { ...options };
2159
- if (!captured.sourceAccess || !captured.queueRuntimeIdentity ||
2744
+ if (!captured.sourceAccess ||
2745
+ !captured.queueRuntimeIdentity ||
2160
2746
  typeof captured.withSourceSerialization !== "function" ||
2161
2747
  typeof captured.withPairingAdmission !== "function" ||
2162
2748
  typeof captured.getInputContext !== "function" ||
2163
- ("withPairedAdmission" in captured && captured.withPairedAdmission !== undefined)) {
2749
+ ("withPairedAdmission" in captured &&
2750
+ captured.withPairedAdmission !== undefined)) {
2164
2751
  throw new Error("Telegram input custody requires strict serialized polling admission and runtime identity.");
2165
2752
  }
2166
2753
  return createJournalStoreCore({ ...captured, queueRuntimeIdentity: { ...captured.queueRuntimeIdentity } }, captured.getInputContext).input;
2167
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
+ }
2168
2764
  function createJournalStoreCore(options, getInputContext) {
2169
2765
  const path = options.path;
2170
2766
  const profile = options.profileName ?? TELEGRAM_DEFAULT_PROFILE_NAME;
@@ -2198,8 +2794,11 @@ function createJournalStoreCore(options, getInputContext) {
2198
2794
  if (withPairingAdmission && withPairedAdmission) {
2199
2795
  throw new Error("Telegram journal admission modes are mutually exclusive.");
2200
2796
  }
2201
- const version = getInputContext ? TELEGRAM_UPDATE_JOURNAL_CUSTODY_VERSION
2202
- : 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;
2203
2802
  const notifyRecovery = (event) => {
2204
2803
  try {
2205
2804
  options.onRecovery?.(event);
@@ -2385,7 +2984,8 @@ function createJournalStoreCore(options, getInputContext) {
2385
2984
  }
2386
2985
  if (segment.profile !== storedProfile ||
2387
2986
  !identitiesMatch(segment.botIdentity, storedIdentity) ||
2388
- (withPairingAdmission && segment.botIdentity.tokenSha256 !== storedIdentity.tokenSha256)) {
2987
+ (withPairingAdmission &&
2988
+ segment.botIdentity.tokenSha256 !== storedIdentity.tokenSha256)) {
2389
2989
  throw createJournalError("identity-mismatch", segmentPath, "belongs to another journal identity");
2390
2990
  }
2391
2991
  assertJournalSourceCompletionContinuity(file, segment, segmentPath);
@@ -2401,8 +3001,10 @@ function createJournalStoreCore(options, getInputContext) {
2401
3001
  for (const entry of segment.upsertedEntries) {
2402
3002
  const previous = entriesById.get(entry.updateId);
2403
3003
  if (withPairingAdmission &&
2404
- ((previous && previous.preApprovalExcluded !== entry.preApprovalExcluded) ||
2405
- (!previous && entry.updateId <= (file.acceptedThroughUpdateId ?? -1)))) {
3004
+ ((previous &&
3005
+ previous.preApprovalExcluded !== entry.preApprovalExcluded) ||
3006
+ (!previous &&
3007
+ entry.updateId <= (file.acceptedThroughUpdateId ?? -1)))) {
2406
3008
  throw createJournalError("pairing-evidence", segmentPath, "changes exclusion evidence or resurrects a settled source");
2407
3009
  }
2408
3010
  entriesById.set(entry.updateId, entry);
@@ -2425,16 +3027,23 @@ function createJournalStoreCore(options, getInputContext) {
2425
3027
  : file.operatorDispositions?.length
2426
3028
  ? { operatorDispositions: file.operatorDispositions }
2427
3029
  : {}),
2428
- ...(segment.sourceCompletions !== undefined ? { sourceCompletions: segment.sourceCompletions }
2429
- : file.sourceCompletions?.length ? { sourceCompletions: file.sourceCompletions } : {}),
3030
+ ...(segment.sourceCompletions !== undefined
3031
+ ? { sourceCompletions: segment.sourceCompletions }
3032
+ : file.sourceCompletions?.length
3033
+ ? { sourceCompletions: file.sourceCompletions }
3034
+ : {}),
2430
3035
  }, segmentPath, version);
2431
3036
  revision = segment.revision;
2432
3037
  }
2433
3038
  const identityChanged = storedProfile !== profile ||
2434
3039
  !identitiesMatch(file.botIdentity, expectedIdentity) ||
2435
- ((withPairingAdmission || !!file.sourceCompletions?.length) && file.botIdentity.tokenSha256 !== expectedIdentity.tokenSha256);
3040
+ ((withPairingAdmission || !!file.sourceCompletions?.length) &&
3041
+ file.botIdentity.tokenSha256 !== expectedIdentity.tokenSha256);
2436
3042
  if (identityChanged) {
2437
- if (file.entries.length > 0 || file.sourceCompletions?.length || withPairingAdmission || !allowReconciliation) {
3043
+ if (file.entries.length > 0 ||
3044
+ file.sourceCompletions?.length ||
3045
+ withPairingAdmission ||
3046
+ !allowReconciliation) {
2438
3047
  throw createJournalError("identity-mismatch", path, storedProfile !== profile
2439
3048
  ? `belongs to profile ${storedProfile}, not ${profile}`
2440
3049
  : "belongs to another Telegram bot identity");
@@ -2616,15 +3225,26 @@ function createJournalStoreCore(options, getInputContext) {
2616
3225
  try {
2617
3226
  // Acquire before the transaction helper can create parents or staging names.
2618
3227
  // The config continuation excludes participating writers through consumption.
2619
- const source = sourceAccess ? acquireTelegramUpdateJournalFamily({
2620
- ...sourceAccess, path, profile, botIdentity: expectedIdentity,
2621
- }, version) : undefined;
3228
+ const source = sourceAccess
3229
+ ? acquireTelegramUpdateJournalFamily({
3230
+ ...sourceAccess,
3231
+ path,
3232
+ profile,
3233
+ botIdentity: expectedIdentity,
3234
+ }, version)
3235
+ : undefined;
2622
3236
  const readSource = () => {
2623
3237
  if (!source)
2624
3238
  return readCurrent();
2625
- const file = source.evidence.kind === "present" ? source.evidence.file : emptyFile();
2626
- return { file, exists: source.evidence.kind === "present",
2627
- 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
+ };
2628
3248
  };
2629
3249
  return withTelegramFileTransaction(`${path}.transaction`, () => operation(readSource));
2630
3250
  }
@@ -2644,7 +3264,9 @@ function createJournalStoreCore(options, getInputContext) {
2644
3264
  if (!sourceAccess)
2645
3265
  return;
2646
3266
  const limits = sourceAccess.limits;
2647
- if (files > limits.maxFiles || bytes > limits.maxBytes || work > limits.maxWork ||
3267
+ if (files > limits.maxFiles ||
3268
+ bytes > limits.maxBytes ||
3269
+ work > limits.maxWork ||
2648
3270
  collections.some((length) => length > limits.maxEntries)) {
2649
3271
  throw createJournalError("capacity", path, "publication would exceed source inspection resource limits");
2650
3272
  }
@@ -2652,20 +3274,31 @@ function createJournalStoreCore(options, getInputContext) {
2652
3274
  const inputHeadroomText = "\0".repeat(TELEGRAM_UPDATE_JOURNAL_QUEUE_OWNER_ID_MAX_LENGTH);
2653
3275
  const inputBindingHeadroomText = "\0".repeat(TELEGRAM_UPDATE_JOURNAL_INPUT_BINDING_MAX_LENGTH);
2654
3276
  const inputHandoffHeadroomText = "\0".repeat(TELEGRAM_UPDATE_JOURNAL_QUEUE_HANDOFF_ID_MAX_LENGTH);
2655
- 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);
2656
3280
  const advanceInputHeadroomState = (state, entries, projectionSlack) => {
2657
3281
  const revision = (state.file.revision ?? 0) + 1;
2658
3282
  if (!isSafePositiveInteger(revision))
2659
3283
  throw createJournalError("capacity", path, "exhausted input custody revisions");
2660
3284
  return { file: { ...state.file, revision, entries }, projectionSlack };
2661
3285
  };
2662
- 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]);
2663
3291
  const createInputHeadroomClaim = (entry, phase) => ({
2664
3292
  phase,
2665
- owner: { instanceId: inputHeadroomText, processId: Number.MAX_SAFE_INTEGER,
2666
- processBirthId: inputHeadroomText, sessionGeneration: Number.MAX_SAFE_INTEGER,
2667
- acquisitionId: inputHeadroomText, acquiredAtMs: Number.MAX_SAFE_INTEGER,
2668
- 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
+ },
2669
3302
  recipientBindingKey: inputBindingHeadroomText,
2670
3303
  executionUpdate: structuredClone(entry.update),
2671
3304
  });
@@ -2680,29 +3313,64 @@ function createJournalStoreCore(options, getInputContext) {
2680
3313
  const createInputHeadroomTransitionPlan = (file, entry) => {
2681
3314
  const initial = { file, projectionSlack: 0 };
2682
3315
  if (entry.inputClaim?.phase === "running")
2683
- return [advanceInputHeadroomState(initial, replaceInputHeadroomEntry(file, entry.updateId), TELEGRAM_UPDATE_JOURNAL_INPUT_TRANSITION_HEADROOM_BYTES)];
2684
- if (entry.state === "pending" && entry.inputClaim?.phase === "ready" && entry.inputClaim.handoff) {
2685
- const acceptedClaim = { ...entry.inputClaim,
2686
- owner: { ...entry.inputClaim.handoff.recipientOwner, acquisitionId: inputHeadroomText,
2687
- 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
+ };
2688
3331
  delete acceptedClaim.handoff;
2689
3332
  const acceptedEntry = { ...entry, inputClaim: acceptedClaim };
2690
3333
  const acceptedState = advanceInputHeadroomState(initial, replaceInputHeadroomEntry(file, entry.updateId, acceptedEntry), TELEGRAM_UPDATE_JOURNAL_INPUT_TRANSITION_HEADROOM_BYTES);
2691
- return [acceptedState, ...createInputHeadroomTransitionPlan(acceptedState.file, acceptedEntry)];
3334
+ return [
3335
+ acceptedState,
3336
+ ...createInputHeadroomTransitionPlan(acceptedState.file, acceptedEntry),
3337
+ ];
2692
3338
  }
2693
3339
  if (entry.state === "pending" && entry.inputClaim?.phase === "ready") {
2694
- const runningEntry = { ...entry, inputClaim: { ...entry.inputClaim, phase: "running" } };
3340
+ const runningEntry = {
3341
+ ...entry,
3342
+ inputClaim: { ...entry.inputClaim, phase: "running" },
3343
+ };
2695
3344
  const runningState = advanceInputHeadroomState(initial, replaceInputHeadroomEntry(file, entry.updateId, runningEntry), TELEGRAM_UPDATE_JOURNAL_INPUT_TRANSITION_HEADROOM_BYTES);
2696
- 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
+ ];
2697
3349
  }
2698
- if (entry.state === "pending" && entry.preApprovalExcluded === false && !entry.inputClaim) {
2699
- 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
+ };
2700
3357
  const readyState = advanceInputHeadroomState(initial, replaceInputHeadroomEntry(file, entry.updateId, readyEntry), TELEGRAM_UPDATE_JOURNAL_INPUT_PROJECTION_HEADROOM_BYTES);
2701
- const runningEntry = { ...readyEntry, inputClaim: { ...readyEntry.inputClaim, phase: "running" } };
3358
+ const runningEntry = {
3359
+ ...readyEntry,
3360
+ inputClaim: { ...readyEntry.inputClaim, phase: "running" },
3361
+ };
2702
3362
  const runningState = advanceInputHeadroomState(readyState, replaceInputHeadroomEntry(readyState.file, entry.updateId, runningEntry), TELEGRAM_UPDATE_JOURNAL_INPUT_PROJECTION_HEADROOM_BYTES);
2703
- 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
+ ];
2704
3368
  }
2705
- 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
+ : [];
2706
3374
  };
2707
3375
  const createInputHeadroomCancelPlan = (file, entry) => {
2708
3376
  if (!entry.inputClaim?.handoff)
@@ -2712,40 +3380,54 @@ function createJournalStoreCore(options, getInputContext) {
2712
3380
  const cancelledEntry = { ...entry, inputClaim: claim };
2713
3381
  const initial = { file, projectionSlack: 0 };
2714
3382
  const cancelledState = advanceInputHeadroomState(initial, replaceInputHeadroomEntry(file, entry.updateId, cancelledEntry), TELEGRAM_UPDATE_JOURNAL_INPUT_TRANSITION_HEADROOM_BYTES);
2715
- return [cancelledState, ...createInputHeadroomTransitionPlan(cancelledState.file, cancelledEntry)];
3383
+ return [
3384
+ cancelledState,
3385
+ ...createInputHeadroomTransitionPlan(cancelledState.file, cancelledEntry),
3386
+ ];
2716
3387
  };
2717
3388
  const createInputHeadroomReleasePlan = (file, entry, allowOffered = false) => {
2718
3389
  if (entry.inputClaim?.handoff && !allowOffered)
2719
3390
  return [];
2720
3391
  const normal = createInputHeadroomTransitionPlan(file, entry);
2721
- 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];
2722
3395
  if (!readyState)
2723
3396
  return [];
2724
- const readyEntry = readyState.file.entries.find(candidate => candidate.updateId === entry.updateId);
3397
+ const readyEntry = readyState.file.entries.find((candidate) => candidate.updateId === entry.updateId);
2725
3398
  if (readyEntry?.inputClaim?.phase !== "ready")
2726
3399
  return [];
2727
3400
  const releasedEntry = cloneEntry(readyEntry);
2728
3401
  delete releasedEntry.inputClaim;
2729
3402
  const releasedState = advanceInputHeadroomState(readyState, replaceInputHeadroomEntry(readyState.file, entry.updateId, releasedEntry), TELEGRAM_UPDATE_JOURNAL_INPUT_TRANSITION_HEADROOM_BYTES);
2730
3403
  const reacquisition = createInputHeadroomTransitionPlan(releasedState.file, releasedEntry);
2731
- return [...(readyState.file === file ? [] : [readyState]), releasedState, ...reacquisition];
3404
+ return [
3405
+ ...(readyState.file === file ? [] : [readyState]),
3406
+ releasedState,
3407
+ ...reacquisition,
3408
+ ];
2732
3409
  };
2733
3410
  const createInputHeadroomQueueCompletionPlan = (file, entries) => {
2734
- 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))
2735
3413
  return [];
2736
- const requestedIds = new Set(entries.map(entry => entry.updateId));
3414
+ const requestedIds = new Set(entries.map((entry) => entry.updateId));
2737
3415
  const initial = { file, projectionSlack: 0 };
2738
- 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
+ ];
2739
3419
  };
2740
3420
  const getInputHeadroomTransitionPlans = (file, updateId) => {
2741
3421
  if (updateId !== "all") {
2742
3422
  const requestedIds = new Set(typeof updateId === "number" ? [updateId] : [...updateId]);
2743
- const requestedEntries = file.entries.filter(entry => requestedIds.has(entry.updateId));
3423
+ const requestedEntries = file.entries.filter((entry) => requestedIds.has(entry.updateId));
2744
3424
  const queuedReceiptId = requestedEntries[0]?.queueReceiptId;
2745
- if (requestedEntries.length === requestedIds.size && queuedReceiptId &&
2746
- requestedEntries.every(entry => entry.state === "queued" &&
3425
+ if (requestedEntries.length === requestedIds.size &&
3426
+ queuedReceiptId &&
3427
+ requestedEntries.every((entry) => entry.state === "queued" &&
2747
3428
  entry.queueReceiptId === queuedReceiptId) &&
2748
- file.entries.filter(entry => entry.queueReceiptId === queuedReceiptId).length === requestedIds.size) {
3429
+ file.entries.filter((entry) => entry.queueReceiptId === queuedReceiptId)
3430
+ .length === requestedIds.size) {
2749
3431
  const completion = createInputHeadroomQueueCompletionPlan(file, requestedEntries);
2750
3432
  return completion.length > 0 ? [completion] : [];
2751
3433
  }
@@ -2755,19 +3437,47 @@ function createJournalStoreCore(options, getInputContext) {
2755
3437
  const plan = entry ? createInputHeadroomTransitionPlan(file, entry) : [];
2756
3438
  const cancel = entry ? createInputHeadroomCancelPlan(file, entry) : [];
2757
3439
  const recover = entry?.inputClaim?.handoff
2758
- ? createInputHeadroomReleasePlan(file, entry, true) : [];
2759
- 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);
2760
3443
  }
2761
- const running = smallestInputHeadroomEntry(file.entries.filter(entry => entry.inputClaim?.phase === "running"));
2762
- const offered = largestInputHeadroomEntry(file.entries.filter(entry => entry.state === "pending" && entry.inputClaim?.phase === "ready" && entry.inputClaim.handoff), entry => ({ ...entry, inputClaim: { ...entry.inputClaim, owner: {
2763
- ...entry.inputClaim.handoff.recipientOwner, acquisitionId: inputHeadroomText,
2764
- acquiredAtMs: Number.MAX_SAFE_INTEGER, handoffId: inputHandoffHeadroomText
2765
- }, handoff: undefined } }));
2766
- const ready = largestInputHeadroomEntry(file.entries.filter(entry => entry.state === "pending" && entry.inputClaim?.phase === "ready" && !entry.inputClaim.handoff), entry => ({ ...entry, inputClaim: { ...entry.inputClaim, phase: "running" } }));
2767
- const unclaimed = largestInputHeadroomEntry(file.entries.filter(entry => entry.state === "pending" && entry.preApprovalExcluded === false && !entry.inputClaim), entry => ({ ...entry, inputClaim: createInputHeadroomClaim(entry, "running") }));
2768
- const excluded = smallestInputHeadroomEntry(file.entries.filter(entry => entry.preApprovalExcluded === true));
2769
- const representatives = [running, offered, ready, unclaimed, excluded].filter((entry) => entry !== undefined);
2770
- 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));
2771
3481
  if (offered) {
2772
3482
  plans.push(createInputHeadroomCancelPlan(file, offered));
2773
3483
  plans.push(createInputHeadroomReleasePlan(file, offered, true));
@@ -2778,7 +3488,9 @@ function createJournalStoreCore(options, getInputContext) {
2778
3488
  }
2779
3489
  const queuedReceipts = new Map();
2780
3490
  for (const entry of file.entries) {
2781
- if (entry.state !== "queued" || !entry.queueReceiptId || entry.queueHandoff)
3491
+ if (entry.state !== "queued" ||
3492
+ !entry.queueReceiptId ||
3493
+ entry.queueHandoff)
2782
3494
  continue;
2783
3495
  const grouped = queuedReceipts.get(entry.queueReceiptId) ?? [];
2784
3496
  grouped.push(entry);
@@ -2792,14 +3504,23 @@ function createJournalStoreCore(options, getInputContext) {
2792
3504
  return plans;
2793
3505
  };
2794
3506
  const createInputHeadroomSegment = (previous, next) => {
2795
- const previousEntries = new Map(previous.entries.map(entry => [entry.updateId, entry]));
2796
- const nextEntries = new Map(next.entries.map(entry => [entry.updateId, entry]));
2797
- return { version,
2798
- revision: next.revision, previousRevision: next.revision - 1,
2799
- profile: next.profile, botIdentity: next.botIdentity,
2800
- upsertedEntries: next.entries.filter(entry => !previousEntries.has(entry.updateId) || !isDeepStrictEqual(previousEntries.get(entry.updateId), entry)),
2801
- removedUpdateIds: previous.entries.filter(entry => !nextEntries.has(entry.updateId)).map(entry => entry.updateId),
2802
- ...(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
+ };
2803
3524
  };
2804
3525
  const assertInputHeadroomCapacity = (file, extraBytes) => {
2805
3526
  const bytes = assertCapacity(file);
@@ -2821,46 +3542,74 @@ function createJournalStoreCore(options, getInputContext) {
2821
3542
  ? { ...current.source.evidence.accounting }
2822
3543
  : { files: 1, bytes: publishedBytes, work: getFileWork(publishedFile) };
2823
3544
  if (publishedSegment) {
2824
- const segmentWork = publishedSegment.upsertedEntries.length + publishedSegment.removedUpdateIds.length +
2825
- (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);
2826
3549
  const segmentBytes = Buffer.byteLength(`${JSON.stringify(publishedSegment, null, 2)}\n`);
2827
3550
  baseRetainedCount += 1;
2828
3551
  baseRetainedBytes += segmentBytes;
2829
3552
  baseRetainedWork += segmentWork;
2830
- const segmentFirst = { files: baseAccounting.files + 1, bytes: baseAccounting.bytes + segmentBytes,
2831
- work: baseAccounting.work + segmentWork + getFileWork(publishedFile) };
2832
- const compactResidue = { files: 1 + baseRetainedCount,
2833
- bytes: publishedBytes + baseRetainedBytes, work: getFileWork(publishedFile) + baseRetainedWork };
2834
- 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),
2835
3565
  bytes: Math.max(segmentFirst.bytes, compactResidue.bytes),
2836
- work: Math.max(segmentFirst.work, compactResidue.work) };
3566
+ work: Math.max(segmentFirst.work, compactResidue.work),
3567
+ };
2837
3568
  }
2838
3569
  for (const transitions of plans) {
2839
3570
  let retainedCount = baseRetainedCount;
2840
3571
  let retainedBytes = baseRetainedBytes;
2841
3572
  let retainedWork = baseRetainedWork;
2842
3573
  let accounting = { ...baseAccounting };
2843
- let state = { file: publishedFile, projectionSlack: 0 };
3574
+ let state = {
3575
+ file: publishedFile,
3576
+ projectionSlack: 0,
3577
+ };
2844
3578
  for (const next of transitions) {
2845
3579
  const segment = createInputHeadroomSegment(state.file, next.file);
2846
- const segmentCollections = [segment.upsertedEntries.length, segment.removedUpdateIds.length,
2847
- segment.operatorDispositions?.length ?? 0, next.file.entries.length,
2848
- next.file.operatorDispositions?.length ?? 0];
2849
- const segmentWork = segmentCollections[0] + segmentCollections[1] + segmentCollections[2];
2850
- 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;
2851
3592
  const nextBytes = assertInputHeadroomCapacity(next.file, next.projectionSlack);
2852
- const segmentFirst = { files: accounting.files + 1, bytes: accounting.bytes + segmentBytes,
2853
- 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
+ };
2854
3598
  assertSourceResources(segmentFirst.files, segmentFirst.bytes, segmentFirst.work, segmentCollections);
2855
3599
  retainedCount += 1;
2856
3600
  retainedBytes += segmentBytes;
2857
3601
  retainedWork += segmentWork;
2858
- const compactResidue = { files: 1 + retainedCount, bytes: nextBytes + retainedBytes,
2859
- work: getFileWork(next.file) + retainedWork };
3602
+ const compactResidue = {
3603
+ files: 1 + retainedCount,
3604
+ bytes: nextBytes + retainedBytes,
3605
+ work: getFileWork(next.file) + retainedWork,
3606
+ };
2860
3607
  assertSourceResources(compactResidue.files, compactResidue.bytes, compactResidue.work, segmentCollections);
2861
- accounting = { files: Math.max(segmentFirst.files, compactResidue.files),
3608
+ accounting = {
3609
+ files: Math.max(segmentFirst.files, compactResidue.files),
2862
3610
  bytes: Math.max(segmentFirst.bytes, compactResidue.bytes),
2863
- work: Math.max(segmentFirst.work, compactResidue.work) };
3611
+ work: Math.max(segmentFirst.work, compactResidue.work),
3612
+ };
2864
3613
  state = next;
2865
3614
  }
2866
3615
  }
@@ -2874,9 +3623,15 @@ function createJournalStoreCore(options, getInputContext) {
2874
3623
  const serializedBytes = assertCapacity(revisedFile, serialized);
2875
3624
  const segmentText = `${JSON.stringify(segment, null, 2)}\n`;
2876
3625
  const segmentBytes = Buffer.byteLength(segmentText);
2877
- const collections = [segment.upsertedEntries.length, segment.removedUpdateIds.length,
2878
- segment.operatorDispositions?.length ?? 0, file.entries.length, file.operatorDispositions?.length ?? 0,
2879
- 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
+ ];
2880
3635
  const segmentWork = collections[0] + collections[1] + collections[2] + collections[5];
2881
3636
  const fileWork = collections[3] + collections[4] + collections[6];
2882
3637
  const accounting = source.evidence.accounting;
@@ -2884,10 +3639,14 @@ function createJournalStoreCore(options, getInputContext) {
2884
3639
  assertSourceResources(accounting.files + 1, accounting.bytes + segmentBytes, accounting.work + segmentWork + fileWork, collections);
2885
3640
  const unapplied = source.segments.filter((item) => Number(item.name.slice(0, 16)) > source.snapshotRevision);
2886
3641
  const compact = forceCompact ||
2887
- unapplied.length + 1 >= TELEGRAM_UPDATE_JOURNAL_COMPACTION_SEGMENT_COUNT ||
2888
- 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;
2889
3646
  if (compact) {
2890
- 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);
2891
3650
  }
2892
3651
  const segmentPath = getTelegramUpdateJournalSegmentPath(path, segment.revision);
2893
3652
  // A crash may retain staging, so keep it outside the strictly enumerated segment directory.
@@ -2896,9 +3655,13 @@ function createJournalStoreCore(options, getInputContext) {
2896
3655
  writeJournalFile(path, serialized, publicationBoundary);
2897
3656
  // Snapshot scope must not change. Preserve a validated redundant ID witness instead.
2898
3657
  const witness = file.botIdentity.botId === undefined
2899
- ? source.segments.find((item) => item.botId !== undefined)?.name : undefined;
3658
+ ? source.segments.find((item) => item.botId !== undefined)?.name
3659
+ : undefined;
2900
3660
  const segmentDirectory = getTelegramUpdateJournalSegmentDirectory(path);
2901
- const names = [...source.segments.map((item) => item.name), basename(segmentPath)];
3661
+ const names = [
3662
+ ...source.segments.map((item) => item.name),
3663
+ basename(segmentPath),
3664
+ ];
2902
3665
  for (const name of names) {
2903
3666
  if (name === witness)
2904
3667
  continue;
@@ -2912,12 +3675,18 @@ function createJournalStoreCore(options, getInputContext) {
2912
3675
  try {
2913
3676
  rmdirSync(segmentDirectory);
2914
3677
  }
2915
- catch { /* Retained witness or cleanup residue. */ }
3678
+ catch {
3679
+ /* Retained witness or cleanup residue. */
3680
+ }
2916
3681
  }
2917
3682
  return { file: revisedFile, serializedBytes };
2918
3683
  };
2919
- const publishMutation = (current, entries, contentChanged, operatorDispositions = current.file.operatorDispositions, acceptedThroughUpdateId = current.file.acceptedThroughUpdateId, publicationBoundary = onPublicationBoundary, inputHeadroom = journalHasCustodyFields(version) ? "all" : false, sourceCompletions = current.file.sourceCompletions) => {
2920
- 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);
2921
3690
  if (!contentChanged &&
2922
3691
  isDeepStrictEqual(current.file.botIdentity, botIdentity) &&
2923
3692
  isDeepStrictEqual(current.file.operatorDispositions ?? [], operatorDispositions ?? []) &&
@@ -2933,9 +3702,7 @@ function createJournalStoreCore(options, getInputContext) {
2933
3702
  ...(acceptedThroughUpdateId !== undefined
2934
3703
  ? { acceptedThroughUpdateId }
2935
3704
  : {}),
2936
- ...(operatorDispositions?.length
2937
- ? { operatorDispositions }
2938
- : {}),
3705
+ ...(operatorDispositions?.length ? { operatorDispositions } : {}),
2939
3706
  ...(sourceCompletions?.length ? { sourceCompletions } : {}),
2940
3707
  };
2941
3708
  assertJournalRoutingInputContinuity(current.file, entries, path);
@@ -2952,7 +3719,11 @@ function createJournalStoreCore(options, getInputContext) {
2952
3719
  }
2953
3720
  if (!current.exists) {
2954
3721
  if (current.source) {
2955
- 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
+ ];
2956
3727
  assertSourceResources(1, serializedBytes, collections[0] + collections[1] + collections[2], collections);
2957
3728
  }
2958
3729
  if (inputHeadroom !== false)
@@ -2961,10 +3732,11 @@ function createJournalStoreCore(options, getInputContext) {
2961
3732
  return { file, serializedBytes };
2962
3733
  }
2963
3734
  const previousEntries = new Map(current.file.entries.map((entry) => [entry.updateId, entry]));
2964
- if (withPairingAdmission && entries.some((entry) => {
2965
- const previous = previousEntries.get(entry.updateId);
2966
- return previous && previous.preApprovalExcluded !== entry.preApprovalExcluded;
2967
- })) {
3735
+ if (withPairingAdmission &&
3736
+ entries.some((entry) => {
3737
+ const previous = previousEntries.get(entry.updateId);
3738
+ return (previous && previous.preApprovalExcluded !== entry.preApprovalExcluded);
3739
+ })) {
2968
3740
  throw createJournalError("pairing-evidence", path, "changes immutable exclusion evidence");
2969
3741
  }
2970
3742
  const nextEntries = new Map(entries.map((entry) => [entry.updateId, entry]));
@@ -2988,7 +3760,9 @@ function createJournalStoreCore(options, getInputContext) {
2988
3760
  ...(!isDeepStrictEqual(current.file.operatorDispositions ?? [], operatorDispositions ?? [])
2989
3761
  ? { operatorDispositions: operatorDispositions ?? [] }
2990
3762
  : {}),
2991
- ...(!isDeepStrictEqual(current.file.sourceCompletions ?? [], sourceCompletions ?? []) ? { sourceCompletions: sourceCompletions ?? [] } : {}),
3763
+ ...(!isDeepStrictEqual(current.file.sourceCompletions ?? [], sourceCompletions ?? [])
3764
+ ? { sourceCompletions: sourceCompletions ?? [] }
3765
+ : {}),
2992
3766
  };
2993
3767
  if (current.source) {
2994
3768
  if (!isSafePositiveInteger(revision))
@@ -3004,7 +3778,8 @@ function createJournalStoreCore(options, getInputContext) {
3004
3778
  const segmentNames = readdirSync(segmentDirectory).filter((name) => /^\d{16}\.json$/u.test(name));
3005
3779
  let snapshotRevision = 0;
3006
3780
  try {
3007
- snapshotRevision = parseJournalFile(JSON.parse(readFileSync(path, "utf8")), path, version).revision ?? 0;
3781
+ snapshotRevision =
3782
+ parseJournalFile(JSON.parse(readFileSync(path, "utf8")), path, version).revision ?? 0;
3008
3783
  }
3009
3784
  catch (error) {
3010
3785
  if (error?.code !== "ENOENT")
@@ -3039,11 +3814,18 @@ function createJournalStoreCore(options, getInputContext) {
3039
3814
  }
3040
3815
  return { file: revisedFile, serializedBytes };
3041
3816
  };
3042
- 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();
3043
3823
  const current = exact && !sourceAccess ? readCurrentStrict(false) : readCurrent();
3044
3824
  if (completionInput !== undefined && !sourceAccess)
3045
3825
  throw createJournalError("invalid", path, "source completion requires an exact source handle");
3046
- const completions = completionInput === undefined ? undefined : validateJournalSourceCompletions(completionInput, path, version);
3826
+ const completions = completionInput === undefined
3827
+ ? undefined
3828
+ : validateJournalSourceCompletions(completionInput, path, version);
3047
3829
  if (exact && expectedSources === undefined)
3048
3830
  throw createJournalError("invalid", path, "received no exact completion sources");
3049
3831
  const requestedIds = new Set();
@@ -3057,133 +3839,221 @@ function createJournalStoreCore(options, getInputContext) {
3057
3839
  throw createJournalError("invalid", path, "received no exact completion sources");
3058
3840
  const expectedIds = new Set();
3059
3841
  for (const expected of expectedSources) {
3060
- if (!isRecord(expected) || Object.keys(expected).some(key => !["updateId", "sourceSha256"].includes(key)) ||
3061
- !isSafeNonNegativeInteger(expected.updateId) || !requestedIds.has(expected.updateId) || expectedIds.has(expected.updateId) ||
3062
- 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)) {
3063
3849
  throw createJournalError("invalid", path, "received an invalid exact completion source");
3064
3850
  }
3065
3851
  expectedIds.add(expected.updateId);
3066
- const entry = current.file.entries.find(value => value.updateId === expected.updateId);
3067
- 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) {
3068
3856
  throw createJournalError("conflict", path, "exact completion source changed or disappeared");
3069
3857
  }
3070
3858
  }
3071
3859
  }
3072
3860
  if (completions) {
3073
- const guards = new Map((expectedSources ?? []).map(source => [source.updateId, source.sourceSha256]));
3074
- 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)) {
3075
3866
  throw createJournalError("invalid", path, "source completion has no matching exact guard");
3076
3867
  }
3077
3868
  }
3078
- const requestedEntries = current.file.entries.filter(entry => requestedIds.has(entry.updateId));
3079
- 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");
3080
3871
  if (protectedEntry)
3081
3872
  throw createJournalError("conflict", path, protectedEntry.state === "failed"
3082
3873
  ? `cannot complete terminal update ${protectedEntry.updateId} without an operator disposition`
3083
3874
  : `cannot complete queued update ${protectedEntry.updateId} without its exact owner receipt`);
3084
- const removedUpdateIds = requestedEntries.map(entry => entry.updateId);
3875
+ const removedUpdateIds = requestedEntries.map((entry) => entry.updateId);
3085
3876
  const sourceCompletions = completions
3086
- ? [...(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;
3087
3879
  if (sourceCompletions?.length)
3088
3880
  validateJournalSourceCompletions(sourceCompletions, path, version);
3089
- 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);
3090
- return { removedUpdateIds, entryCount: published.file.entries.length, serializedBytes: published.serializedBytes,
3091
- ...(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
+ };
3092
3901
  });
3093
3902
  const completeQueued = (receipts, completionInput, exact = false) => runMutation((readCurrent) => {
3094
3903
  if (exact && !sourceAccess)
3095
3904
  throw createJournalError("invalid", path, "source completion requires an exact source handle");
3096
- const completions = exact ? validateJournalSourceCompletions(completionInput, path, version) : undefined;
3905
+ const completions = exact
3906
+ ? validateJournalSourceCompletions(completionInput, path, version)
3907
+ : undefined;
3097
3908
  if (!Array.isArray(receipts) || receipts.length === 0)
3098
3909
  throw createJournalError("invalid", path, "received no queued receipts to complete");
3099
3910
  const current = readCurrent();
3100
3911
  const receiptIds = new Set(), requestedUpdateIds = new Set();
3101
3912
  for (const receipt of receipts) {
3102
- if ((receipt.queueKind !== "prompt" && receipt.queueKind !== "control") || !isNonEmptyString(receipt.receiptId) ||
3103
- 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) {
3104
3918
  throw createJournalError("invalid", path, "received an invalid queued completion receipt");
3105
3919
  }
3106
3920
  receiptIds.add(receipt.receiptId);
3107
3921
  const queueOwner = validateJournalQueueOwner(receipt.queueOwner, path);
3108
- if (queueRuntimeIdentity && (queueOwner.instanceId !== queueRuntimeIdentity.instanceId || queueOwner.processId !== queueRuntimeIdentity.processId ||
3109
- queueOwner.processBirthId !== queueRuntimeIdentity.processBirthId)) {
3922
+ if (queueRuntimeIdentity &&
3923
+ (queueOwner.instanceId !== queueRuntimeIdentity.instanceId ||
3924
+ queueOwner.processId !== queueRuntimeIdentity.processId ||
3925
+ queueOwner.processBirthId !== queueRuntimeIdentity.processBirthId)) {
3110
3926
  throw createJournalError("conflict", path, `cannot complete foreign queue receipt ${receipt.receiptId}`);
3111
3927
  }
3112
3928
  const sourceUpdateIds = new Set();
3113
3929
  for (const updateId of receipt.sourceUpdateIds) {
3114
- if (!isSafeNonNegativeInteger(updateId) || sourceUpdateIds.has(updateId) || requestedUpdateIds.has(updateId)) {
3930
+ if (!isSafeNonNegativeInteger(updateId) ||
3931
+ sourceUpdateIds.has(updateId) ||
3932
+ requestedUpdateIds.has(updateId)) {
3115
3933
  throw createJournalError("invalid", path, "received overlapping queued completion update ids");
3116
3934
  }
3117
3935
  sourceUpdateIds.add(updateId);
3118
3936
  requestedUpdateIds.add(updateId);
3119
3937
  }
3120
- const persistedReceiptEntries = current.file.entries.filter(entry => entry.queueReceiptId === receipt.receiptId);
3121
- if (persistedReceiptEntries.length !== sourceUpdateIds.size || persistedReceiptEntries.some(entry => !sourceUpdateIds.has(entry.updateId) || entry.state !== "queued" || entry.queueKind !== receipt.queueKind || !entry.queueOwner ||
3122
- 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))) {
3123
3946
  throw createJournalError("conflict", path, `cannot complete stale or foreign queue receipt ${receipt.receiptId}`);
3124
3947
  }
3125
3948
  }
3126
- const removedEntries = current.file.entries.filter(entry => requestedUpdateIds.has(entry.updateId));
3127
- 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);
3128
3951
  if (removedUpdateIds.length !== requestedUpdateIds.size)
3129
3952
  throw createJournalError("conflict", path, "queued completion did not resolve every source update");
3130
3953
  if (completions) {
3131
- const sources = new Map(removedEntries.map(entry => [entry.updateId, createTelegramUpdateJournalEntryDigest(entry).sourceSha256]));
3132
- 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)) {
3133
3959
  throw createJournalError("conflict", path, "queued completion source changed or belongs to another receipt");
3134
3960
  }
3135
3961
  }
3136
- 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)
3137
3964
  : current.file.sourceCompletions;
3138
3965
  if (sourceCompletions?.length)
3139
3966
  validateJournalSourceCompletions(sourceCompletions, path, version);
3140
- 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);
3141
- return { removedUpdateIds, entryCount: published.file.entries.length, serializedBytes: published.serializedBytes,
3142
- ...(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
+ };
3143
3980
  });
3144
- const mutateRoutingInputs = (input, select, publishedAtMs) => runMutation(readCurrent => {
3981
+ const mutateRoutingInputs = (input, select, publishedAtMs, chooser) => runMutation((readCurrent) => {
3145
3982
  if (!isTelegramUpdateJournalLegacyFamilyVersion(version))
3146
3983
  throw createJournalError("unsupported-version", path, "routing input lifetime requires a private v1 source");
3147
- const binding = createTelegramUpdateJournalBindingKey({ path, profileName: profile, botIdentity: expectedIdentity });
3148
- if (input.journalBindingKey !== binding || !isSafePositiveInteger(input.operatorUserId) ||
3149
- 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) {
3150
3995
  throw createJournalError("invalid", path, "invalid routing input lifetime authority");
3151
3996
  }
3152
- const assertCurrent = () => { if (!input.isCurrent())
3153
- 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
+ };
3154
4001
  assertCurrent();
3155
- const expected = input.entries.map(entry => validateJournalEntry(entry, path, version));
3156
- 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)
3157
4005
  throw createJournalError("invalid", path, "duplicate routing input sources");
3158
4006
  const current = sourceAccess ? readCurrent() : readCurrentStrict(false);
3159
4007
  const nowMs = select ? getNowMs() : publishedAtMs;
3160
- 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()))
3161
4011
  throw createJournalError("invalid", path, "invalid routing input lifetime clock");
3162
4012
  let issued = false;
3163
4013
  const changed = new Map();
3164
- const result = expected.map(original => {
3165
- const entry = current.file.entries.find(value => value.updateId === original.updateId);
3166
- 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) {
3167
4022
  throw createJournalError("conflict", path, "routing input source is not unclaimed pending work");
3168
4023
  }
3169
- const withoutLifetime = (value) => { const copy = cloneEntry(value); delete copy.routingInput; return copy; };
3170
- if (!isDeepStrictEqual(entry, original) && (!isDeepStrictEqual(withoutLifetime(entry), withoutLifetime(original)) ||
3171
- (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)))) {
3172
4033
  throw createJournalError("conflict", path, "routing input source changed");
3173
4034
  }
3174
4035
  const lifetime = entry.routingInput;
3175
4036
  if (lifetime && lifetime.operatorUserId !== input.operatorUserId)
3176
4037
  throw createJournalError("conflict", path, "routing input belongs to another operator");
3177
- if (createPendingRetentionReference({ path, journalBindingKey: binding, entry, maxBytes }).read()) {
4038
+ if (createPendingRetentionReference({
4039
+ path,
4040
+ journalBindingKey: binding,
4041
+ entry,
4042
+ maxBytes,
4043
+ }).read()) {
3178
4044
  throw createJournalError("conflict", path, "routing input has protected retention");
3179
4045
  }
3180
4046
  if (select) {
3181
- if (!lifetime || (lifetime.phase === "waiting" && nowMs >= lifetime.expiresAtMs))
4047
+ if (!lifetime ||
4048
+ (lifetime.phase === "waiting" && nowMs >= lifetime.expiresAtMs))
3182
4049
  throw createJournalError("conflict", path, "routing input choice expired or was not armed");
3183
4050
  if (lifetime.phase === "selected")
3184
4051
  return cloneEntry(entry);
3185
4052
  issued = true;
3186
- const selected = { ...cloneEntry(entry), routingInput: { ...lifetime, phase: "selected" } };
4053
+ const selected = {
4054
+ ...cloneEntry(entry),
4055
+ routingInput: { ...lifetime, phase: "selected" },
4056
+ };
3187
4057
  changed.set(entry.updateId, selected);
3188
4058
  return cloneEntry(selected);
3189
4059
  }
@@ -3195,16 +4065,29 @@ function createJournalStoreCore(options, getInputContext) {
3195
4065
  if (nowMs < entry.admittedAtMs)
3196
4066
  throw createJournalError("conflict", path, "routing input publication predates admission");
3197
4067
  issued = true;
3198
- const armed = { ...cloneEntry(entry), routingInput: { operatorUserId: input.operatorUserId,
3199
- 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
+ };
3200
4078
  changed.set(entry.updateId, armed);
3201
4079
  return cloneEntry(armed);
3202
4080
  });
3203
4081
  assertCurrent();
3204
- if (select && issued && result.some(entry => !changed.has(entry.updateId)))
4082
+ if (select &&
4083
+ issued &&
4084
+ result.some((entry) => !changed.has(entry.updateId)))
3205
4085
  throw createJournalError("conflict", path, "routing input selection has mixed prior phases");
3206
4086
  if (changed.size)
3207
- 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
+ });
3208
4091
  assertCurrent();
3209
4092
  return { issued, entries: result };
3210
4093
  });
@@ -3236,7 +4119,9 @@ function createJournalStoreCore(options, getInputContext) {
3236
4119
  normalizedUpdates.push(normalized);
3237
4120
  }
3238
4121
  const appendWithEvidence = (preApprovalExcluded) => runJournalTransaction((readCurrent) => {
3239
- if (withPairingAdmission && (typeof preApprovalExcluded !== "boolean" || requestedAcceptedThroughUpdateId === undefined)) {
4122
+ if (withPairingAdmission &&
4123
+ (typeof preApprovalExcluded !== "boolean" ||
4124
+ requestedAcceptedThroughUpdateId === undefined)) {
3240
4125
  throw createJournalError("pairing-evidence", path, "requires exclusion evidence and an admission cursor");
3241
4126
  }
3242
4127
  if (requestedAcceptedThroughUpdateId !== undefined &&
@@ -3250,7 +4135,7 @@ function createJournalStoreCore(options, getInputContext) {
3250
4135
  ...(current.file.operatorDispositions ?? [])
3251
4136
  .filter((disposition) => disposition.action === "discard")
3252
4137
  .map((disposition) => disposition.updateId),
3253
- ...(current.file.sourceCompletions ?? []).map(completion => completion.updateId),
4138
+ ...(current.file.sourceCompletions ?? []).map((completion) => completion.updateId),
3254
4139
  ]);
3255
4140
  let admittedAtMs;
3256
4141
  const addedUpdateIds = [];
@@ -3268,7 +4153,9 @@ function createJournalStoreCore(options, getInputContext) {
3268
4153
  duplicateUpdateIds.push(update.update_id);
3269
4154
  continue;
3270
4155
  }
3271
- if (withPairingAdmission && previousAcceptedThroughUpdateId !== undefined && update.update_id <= previousAcceptedThroughUpdateId) {
4156
+ if (withPairingAdmission &&
4157
+ previousAcceptedThroughUpdateId !== undefined &&
4158
+ update.update_id <= previousAcceptedThroughUpdateId) {
3272
4159
  duplicateUpdateIds.push(update.update_id);
3273
4160
  continue;
3274
4161
  }
@@ -3281,7 +4168,9 @@ function createJournalStoreCore(options, getInputContext) {
3281
4168
  const entry = {
3282
4169
  updateId: update.update_id,
3283
4170
  update,
3284
- ...(withPairingAdmission ? { preApprovalExcluded: preApprovalExcluded } : {}),
4171
+ ...(withPairingAdmission
4172
+ ? { preApprovalExcluded: preApprovalExcluded }
4173
+ : {}),
3285
4174
  admittedAtMs,
3286
4175
  state: "pending",
3287
4176
  };
@@ -3307,7 +4196,9 @@ function createJournalStoreCore(options, getInputContext) {
3307
4196
  return {
3308
4197
  nonExcludedUpdateIds: normalizedUpdates.flatMap((update) => {
3309
4198
  const entry = entriesById.get(update.update_id);
3310
- return entry && entry.preApprovalExcluded !== true ? [entry.updateId] : [];
4199
+ return entry && entry.preApprovalExcluded !== true
4200
+ ? [entry.updateId]
4201
+ : [];
3311
4202
  }),
3312
4203
  addedUpdateIds,
3313
4204
  duplicateUpdateIds,
@@ -3345,12 +4236,12 @@ function createJournalStoreCore(options, getInputContext) {
3345
4236
  });
3346
4237
  };
3347
4238
  return options.withWriterAdmission
3348
- ? options.withWriterAdmission(admittedAppend) : admittedAppend();
4239
+ ? options.withWriterAdmission(admittedAppend)
4240
+ : admittedAppend();
3349
4241
  },
3350
4242
  markQueued(receipt) {
3351
4243
  return runMutation((readCurrent) => {
3352
- if ((receipt.queueKind !== "prompt" &&
3353
- receipt.queueKind !== "control") ||
4244
+ if ((receipt.queueKind !== "prompt" && receipt.queueKind !== "control") ||
3354
4245
  !isNonEmptyString(receipt.receiptId) ||
3355
4246
  !Array.isArray(receipt.sourceUpdateIds) ||
3356
4247
  receipt.sourceUpdateIds.length === 0) {
@@ -3424,8 +4315,12 @@ function createJournalStoreCore(options, getInputContext) {
3424
4315
  updateId: entry.updateId,
3425
4316
  update: entry.update,
3426
4317
  admittedAtMs: entry.admittedAtMs,
3427
- ...(entry.preApprovalExcluded !== undefined ? { preApprovalExcluded: entry.preApprovalExcluded } : {}),
3428
- ...(entry.routingInput ? { routingInput: { ...entry.routingInput } } : {}),
4318
+ ...(entry.preApprovalExcluded !== undefined
4319
+ ? { preApprovalExcluded: entry.preApprovalExcluded }
4320
+ : {}),
4321
+ ...(entry.routingInput
4322
+ ? { routingInput: { ...entry.routingInput } }
4323
+ : {}),
3429
4324
  state: "queued",
3430
4325
  queueKind: receipt.queueKind,
3431
4326
  queueReceiptId: receipt.receiptId,
@@ -3455,8 +4350,7 @@ function createJournalStoreCore(options, getInputContext) {
3455
4350
  !isSafeNonNegativeInteger(input.failedAtMs) ||
3456
4351
  !isBoundedString(input.failureClass, TELEGRAM_UPDATE_JOURNAL_FAILURE_CLASS_MAX_LENGTH) ||
3457
4352
  !isBoundedString(input.summary, TELEGRAM_UPDATE_JOURNAL_FAILURE_SUMMARY_MAX_LENGTH) ||
3458
- (input.disposition !== "retry-wait" &&
3459
- input.disposition !== "failed")) {
4353
+ (input.disposition !== "retry-wait" && input.disposition !== "failed")) {
3460
4354
  throw createJournalError("invalid", path, "received invalid execution failure metadata");
3461
4355
  }
3462
4356
  if (input.disposition === "retry-wait" &&
@@ -3502,8 +4396,12 @@ function createJournalStoreCore(options, getInputContext) {
3502
4396
  updateId: entry.updateId,
3503
4397
  update: entry.update,
3504
4398
  admittedAtMs: entry.admittedAtMs,
3505
- ...(entry.routingInput ? { routingInput: { ...entry.routingInput } } : {}),
3506
- ...(entry.preApprovalExcluded !== undefined ? { preApprovalExcluded: entry.preApprovalExcluded } : {}),
4399
+ ...(entry.routingInput
4400
+ ? { routingInput: { ...entry.routingInput } }
4401
+ : {}),
4402
+ ...(entry.preApprovalExcluded !== undefined
4403
+ ? { preApprovalExcluded: entry.preApprovalExcluded }
4404
+ : {}),
3507
4405
  state: input.disposition,
3508
4406
  failure,
3509
4407
  ...(input.disposition === "retry-wait"
@@ -3573,14 +4471,19 @@ function createJournalStoreCore(options, getInputContext) {
3573
4471
  updateId: entry.updateId,
3574
4472
  update: entry.update,
3575
4473
  admittedAtMs: entry.admittedAtMs,
3576
- ...(entry.preApprovalExcluded !== undefined ? { preApprovalExcluded: entry.preApprovalExcluded } : {}),
4474
+ ...(entry.preApprovalExcluded !== undefined
4475
+ ? { preApprovalExcluded: entry.preApprovalExcluded }
4476
+ : {}),
3577
4477
  state: "retry-wait",
3578
4478
  failure: entry.failure,
3579
4479
  nextRetryAtMs: disposition.committedAtMs,
3580
4480
  }
3581
4481
  : candidate)
3582
4482
  : current.file.entries.filter((candidate) => candidate.updateId !== entry.updateId);
3583
- const published = publishMutation(current, nextEntries, true, [...(current.file.operatorDispositions ?? []), disposition]);
4483
+ const published = publishMutation(current, nextEntries, true, [
4484
+ ...(current.file.operatorDispositions ?? []),
4485
+ disposition,
4486
+ ]);
3584
4487
  return {
3585
4488
  disposition: { ...disposition },
3586
4489
  duplicate: false,
@@ -3594,30 +4497,44 @@ function createJournalStoreCore(options, getInputContext) {
3594
4497
  const current = readCurrent();
3595
4498
  if (!journalHasCustodyFields(current.file.version))
3596
4499
  throw createJournalError("conflict", path, "legacy custody disposition requires a custody journal");
3597
- const existing = current.file.operatorDispositions?.find(candidate => candidate.failureId === authority.dispositionId);
4500
+ const existing = current.file.operatorDispositions?.find((candidate) => candidate.failureId === authority.dispositionId);
3598
4501
  if (existing) {
3599
- if (!("dispositionKind" in existing) || existing.dispositionKind !== "legacy-custody")
4502
+ if (!("dispositionKind" in existing) ||
4503
+ existing.dispositionKind !== "legacy-custody")
3600
4504
  throw createJournalError("conflict", path, `legacy custody disposition ${authority.dispositionId} already has another authority`);
3601
- const normalizedDuplicate = normalizeTelegramUpdateJournalLegacyCustodyDispositionAuthority(authority, { updateId: existing.updateId, state: "retry-wait", attemptCount: 1,
3602
- failedAtMs: 0, failureClass: "retained-audit", summary: "retained-audit",
3603
- evidenceSha256: existing.evidenceSha256 });
3604
- if (!normalizedDuplicate || existing.action !== normalizedDuplicate.action ||
3605
- 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 ||
3606
4518
  existing.authorizedAtMs !== normalizedDuplicate.authorizedAtMs)
3607
4519
  throw createJournalError("conflict", path, `legacy custody disposition ${authority.dispositionId} already has another authority`);
3608
4520
  let authorized = false;
3609
4521
  try {
3610
- authorized = options.authorizeLegacyCustodyDisposition?.(normalizedDuplicate) === true;
4522
+ authorized =
4523
+ options.authorizeLegacyCustodyDisposition?.(normalizedDuplicate) === true;
3611
4524
  }
3612
4525
  catch {
3613
4526
  authorized = false;
3614
4527
  }
3615
4528
  if (!authorized)
3616
4529
  throw createJournalError("conflict", path, "legacy custody disposition is unauthorized");
3617
- return { disposition: { ...existing }, duplicate: true,
3618
- 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
+ };
3619
4536
  }
3620
- const entry = current.file.entries.find(candidate => candidate.updateId === authority.updateId);
4537
+ const entry = current.file.entries.find((candidate) => candidate.updateId === authority.updateId);
3621
4538
  const evidence = entry && createTelegramUpdateJournalLegacyCustodyEvidence(entry);
3622
4539
  if (!evidence)
3623
4540
  throw createJournalError("conflict", path, `cannot dispose non-quarantined update ${authority.updateId}`);
@@ -3626,7 +4543,8 @@ function createJournalStoreCore(options, getInputContext) {
3626
4543
  throw createJournalError("conflict", path, `legacy custody evidence changed for update ${authority.updateId}`);
3627
4544
  let authorized = false;
3628
4545
  try {
3629
- authorized = options.authorizeLegacyCustodyDisposition?.(normalized) === true;
4546
+ authorized =
4547
+ options.authorizeLegacyCustodyDisposition?.(normalized) === true;
3630
4548
  }
3631
4549
  catch {
3632
4550
  authorized = false;
@@ -3637,24 +4555,38 @@ function createJournalStoreCore(options, getInputContext) {
3637
4555
  if (!isSafeNonNegativeInteger(nowMs))
3638
4556
  throw createJournalError("invalid", path, "received an invalid legacy custody disposition timestamp");
3639
4557
  const disposition = {
3640
- dispositionKind: "legacy-custody", failureId: normalized.dispositionId,
3641
- updateId: normalized.updateId, action: normalized.action,
4558
+ dispositionKind: "legacy-custody",
4559
+ failureId: normalized.dispositionId,
4560
+ updateId: normalized.updateId,
4561
+ action: normalized.action,
3642
4562
  committedAtMs: Math.max(nowMs, normalized.authorizedAtMs),
3643
4563
  evidenceSha256: normalized.evidenceSha256,
3644
4564
  operatorAuthorityId: normalized.operatorAuthorityId,
3645
- authorizedAtMs: normalized.authorizedAtMs
4565
+ authorizedAtMs: normalized.authorizedAtMs,
3646
4566
  };
3647
4567
  const nextEntries = normalized.action === "discard"
3648
- ? current.file.entries.filter(candidate => candidate.updateId !== entry.updateId)
3649
- : current.file.entries.map(candidate => candidate.updateId === entry.updateId
3650
- ? { 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,
3651
4573
  admittedAtMs: entry.admittedAtMs,
3652
- ...(entry.preApprovalExcluded === undefined ? {} :
3653
- { preApprovalExcluded: entry.preApprovalExcluded }), state: "pending" }
4574
+ ...(entry.preApprovalExcluded === undefined
4575
+ ? {}
4576
+ : { preApprovalExcluded: entry.preApprovalExcluded }),
4577
+ state: "pending",
4578
+ }
3654
4579
  : candidate);
3655
- const published = publishMutation(current, nextEntries, true, [...(current.file.operatorDispositions ?? []), disposition]);
3656
- return { disposition: { ...disposition }, duplicate: false,
3657
- 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
+ };
3658
4590
  });
3659
4591
  },
3660
4592
  offerQueuedHandoff(input) {
@@ -3750,13 +4682,18 @@ function createJournalStoreCore(options, getInputContext) {
3750
4682
  updateId: entry.updateId,
3751
4683
  update: entry.update,
3752
4684
  admittedAtMs: entry.admittedAtMs,
3753
- ...(entry.preApprovalExcluded !== undefined ? { preApprovalExcluded: entry.preApprovalExcluded } : {}),
4685
+ ...(entry.preApprovalExcluded !== undefined
4686
+ ? { preApprovalExcluded: entry.preApprovalExcluded }
4687
+ : {}),
3754
4688
  state: "queued",
3755
4689
  queueKind: input.queueKind,
3756
4690
  queueReceiptId: input.receiptId,
3757
4691
  queueOwner: cloneJournalQueueOwner(queueOwner),
3758
4692
  ...(entry.inputProvenance
3759
- ? { inputProvenance: structuredClone(entry.inputProvenance) } : {}),
4693
+ ? {
4694
+ inputProvenance: structuredClone(entry.inputProvenance),
4695
+ }
4696
+ : {}),
3760
4697
  }
3761
4698
  : entry), true);
3762
4699
  return {
@@ -3802,12 +4739,15 @@ function createJournalStoreCore(options, getInputContext) {
3802
4739
  };
3803
4740
  });
3804
4741
  },
3805
- completeQueued(receipts) { return completeQueued(receipts); },
3806
- 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
+ },
3807
4748
  discardQueued(input) {
3808
4749
  return runMutation((readCurrent) => {
3809
- if ((input.queueKind !== "prompt" &&
3810
- input.queueKind !== "control") ||
4750
+ if ((input.queueKind !== "prompt" && input.queueKind !== "control") ||
3811
4751
  !isNonEmptyString(input.receiptId) ||
3812
4752
  !Array.isArray(input.sourceUpdateIds) ||
3813
4753
  input.sourceUpdateIds.length === 0) {
@@ -3832,12 +4772,7 @@ function createJournalStoreCore(options, getInputContext) {
3832
4772
  const current = readCurrent();
3833
4773
  const receiptEntries = current.file.entries.filter((entry) => entry.queueReceiptId === input.receiptId);
3834
4774
  if (receiptEntries.length !== requestedIds.size ||
3835
- receiptEntries.some((entry) => !requestedIds.has(entry.updateId) ||
3836
- entry.state !== "queued" ||
3837
- entry.queueKind !== input.queueKind ||
3838
- !entry.queueOwner ||
3839
- entry.queueHandoff !== undefined ||
3840
- !areTelegramUpdateJournalQueueOwnersEqual(entry.queueOwner, expectedOwner))) {
4775
+ receiptEntries.some((entry) => !isExactQueuedReceiptEntry(entry, requestedIds, input.queueKind, expectedOwner))) {
3841
4776
  throw createJournalError("conflict", path, `cannot discard stale queue receipt ${input.receiptId}`);
3842
4777
  }
3843
4778
  const removedUpdateIds = [...requestedIds].sort((a, b) => a - b);
@@ -3884,17 +4819,11 @@ function createJournalStoreCore(options, getInputContext) {
3884
4819
  }
3885
4820
  const current = readCurrent();
3886
4821
  const receiptEntries = current.file.entries.filter((entry) => entry.queueReceiptId === input.receiptId);
3887
- if ((input.queueKind !== "prompt" &&
3888
- input.queueKind !== "control") ||
4822
+ if ((input.queueKind !== "prompt" && input.queueKind !== "control") ||
3889
4823
  !isNonEmptyString(input.receiptId) ||
3890
4824
  requestedIds.size === 0 ||
3891
4825
  receiptEntries.length !== requestedIds.size ||
3892
- receiptEntries.some((entry) => !requestedIds.has(entry.updateId) ||
3893
- entry.state !== "queued" ||
3894
- entry.queueKind !== input.queueKind ||
3895
- !entry.queueOwner ||
3896
- entry.queueHandoff !== undefined ||
3897
- !areTelegramUpdateJournalQueueOwnersEqual(entry.queueOwner, deadOwner))) {
4826
+ receiptEntries.some((entry) => !isExactQueuedReceiptEntry(entry, requestedIds, input.queueKind, deadOwner))) {
3898
4827
  throw createJournalError("conflict", path, `cannot recover stale queue receipt ${input.receiptId}`);
3899
4828
  }
3900
4829
  if (ownerLiveness !== "dead") {
@@ -3928,14 +4857,21 @@ function createJournalStoreCore(options, getInputContext) {
3928
4857
  throw createJournalError("invalid", path, "invalid abandonment update id");
3929
4858
  // Inspection never repairs or quarantines source evidence.
3930
4859
  const current = sourceAccess ? readCurrent() : readCurrentStrict(false);
3931
- const matches = (current.file.operatorDispositions ?? []).filter(item => item.updateId === updateId);
4860
+ const matches = (current.file.operatorDispositions ?? []).filter((item) => item.updateId === updateId);
3932
4861
  const disposition = matches[0];
3933
- if (matches.length !== 1 || !disposition || !("dispositionKind" in disposition) ||
3934
- 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" ||
3935
4867
  disposition.failureId !== `abandon-${disposition.evidenceSha256}` ||
3936
- current.file.entries.some(entry => entry.updateId === updateId))
4868
+ current.file.entries.some((entry) => entry.updateId === updateId))
3937
4869
  return undefined;
3938
- const journalBindingKey = createTelegramUpdateJournalBindingKey({ path, profileName: profile, botIdentity: expectedIdentity });
4870
+ const journalBindingKey = createTelegramUpdateJournalBindingKey({
4871
+ path,
4872
+ profileName: profile,
4873
+ botIdentity: expectedIdentity,
4874
+ });
3939
4875
  const retainedPath = join(`${path}.retained`, `${disposition.failureId}.json`);
3940
4876
  let stat;
3941
4877
  try {
@@ -3946,20 +4882,33 @@ function createJournalStoreCore(options, getInputContext) {
3946
4882
  throw createJournalError("conflict", retainedPath, "committed abandonment lost its retained input");
3947
4883
  throw error;
3948
4884
  }
3949
- if (!stat.isFile() || stat.isSymbolicLink() || stat.size > maxBytes + 4096) {
4885
+ if (!stat.isFile() ||
4886
+ stat.isSymbolicLink() ||
4887
+ stat.size > maxBytes + 4096) {
3950
4888
  throw createJournalError("invalid", retainedPath, "retained input is not a bounded regular file");
3951
4889
  }
3952
4890
  const parsed = JSON.parse(readFileSync(retainedPath, "utf8"));
3953
4891
  if (!isRecord(parsed))
3954
4892
  throw createJournalError("invalid", retainedPath, "retained input is not an evidence record");
3955
4893
  const entry = validateJournalEntry(parsed.entry, retainedPath, version);
3956
- const reference = createPendingRetentionReference({ path, journalBindingKey, entry, maxBytes });
4894
+ const reference = createPendingRetentionReference({
4895
+ path,
4896
+ journalBindingKey,
4897
+ entry,
4898
+ maxBytes,
4899
+ });
3957
4900
  const retained = reference.read();
3958
- if (entry.updateId !== updateId || reference.retainedPath !== retainedPath ||
4901
+ if (entry.updateId !== updateId ||
4902
+ reference.retainedPath !== retainedPath ||
3959
4903
  !isDeepStrictEqual(retained?.requestedDisposition, disposition)) {
3960
4904
  throw createJournalError("conflict", retainedPath, "retained input does not match committed abandonment");
3961
4905
  }
3962
- return { journalBindingKey, updateId, retainedPath, operatorAuthorityId: disposition.operatorAuthorityId };
4906
+ return {
4907
+ journalBindingKey,
4908
+ updateId,
4909
+ retainedPath,
4910
+ operatorAuthorityId: disposition.operatorAuthorityId,
4911
+ };
3963
4912
  });
3964
4913
  },
3965
4914
  inspectPendingRetention(entry) {
@@ -3970,11 +4919,25 @@ function createJournalStoreCore(options, getInputContext) {
3970
4919
  const expected = validateJournalEntry(entry, path, version);
3971
4920
  if (expected.state !== "pending")
3972
4921
  throw createJournalError("conflict", path, "retention inspection requires a pending source");
3973
- const journalBindingKey = createTelegramUpdateJournalBindingKey({ path, profileName: profile, botIdentity: expectedIdentity });
3974
- 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
+ });
3975
4933
  const retained = reference.read();
3976
- return retained ? { journalBindingKey, retainedPath: reference.retainedPath,
3977
- requestedDisposition: retained.requestedDisposition } : undefined;
4934
+ return retained
4935
+ ? {
4936
+ journalBindingKey,
4937
+ retainedPath: reference.retainedPath,
4938
+ requestedDisposition: retained.requestedDisposition,
4939
+ }
4940
+ : undefined;
3978
4941
  });
3979
4942
  },
3980
4943
  abandonPending(input) {
@@ -3982,12 +4945,17 @@ function createJournalStoreCore(options, getInputContext) {
3982
4945
  if (!isTelegramUpdateJournalLegacyFamilyVersion(version)) {
3983
4946
  throw createJournalError("unsupported-version", path, "pending abandonment requires the legacy v1 source");
3984
4947
  }
3985
- const journalBindingKey = createTelegramUpdateJournalBindingKey({ path, profileName: profile, botIdentity: expectedIdentity });
4948
+ const journalBindingKey = createTelegramUpdateJournalBindingKey({
4949
+ path,
4950
+ profileName: profile,
4951
+ botIdentity: expectedIdentity,
4952
+ });
3986
4953
  if (input.journalBindingKey !== journalBindingKey) {
3987
4954
  throw createJournalError("identity-mismatch", path, "pending abandonment belongs to another source");
3988
4955
  }
3989
4956
  if (!isBoundedString(input.operatorAuthorityId, TELEGRAM_UPDATE_JOURNAL_FAILURE_ID_MAX_LENGTH) ||
3990
- !input.operatorAuthorityId.trim() || typeof input.isCurrent !== "function") {
4957
+ !input.operatorAuthorityId.trim() ||
4958
+ typeof input.isCurrent !== "function") {
3991
4959
  throw createJournalError("invalid", path, "pending abandonment requires explicit operator authority");
3992
4960
  }
3993
4961
  const assertCurrent = () => {
@@ -3996,30 +4964,52 @@ function createJournalStoreCore(options, getInputContext) {
3996
4964
  };
3997
4965
  assertCurrent();
3998
4966
  const expected = validateJournalEntry(input.entry, path, version);
3999
- if (expected.state !== "pending" || expected.inputClaim || expected.inputProvenance ||
4000
- 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) {
4001
4974
  throw createJournalError("conflict", path, "only an exact unclaimed pending source may be abandoned");
4002
4975
  }
4003
4976
  // Never repair/quarantine source evidence while deciding an operator cancellation.
4004
- const readStrict = sourceAccess ? readCurrent : () => readCurrentStrict(false);
4977
+ const readStrict = sourceAccess
4978
+ ? readCurrent
4979
+ : () => readCurrentStrict(false);
4005
4980
  const current = readStrict();
4006
- const { evidenceSha256, failureId, retainedPath, read: readRetention } = createPendingRetentionReference({ path, journalBindingKey, entry: expected, maxBytes });
4007
- const existing = current.file.operatorDispositions?.find(item => item.failureId === failureId || item.updateId === expected.updateId);
4008
- 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);
4009
4989
  const assertRetention = (retained, disposition) => {
4010
- if (!retained || !isDeepStrictEqual(retained.requestedDisposition, disposition)) {
4990
+ if (!retained ||
4991
+ !isDeepStrictEqual(retained.requestedDisposition, disposition)) {
4011
4992
  throw createJournalError("conflict", retainedPath, "retained input does not match abandonment evidence");
4012
4993
  }
4013
4994
  };
4014
4995
  if (existing) {
4015
- if (currentEntry || !("dispositionKind" in existing) || existing.dispositionKind !== "legacy-custody" ||
4016
- 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) {
4017
5002
  throw createJournalError("conflict", path, "pending source already has another disposition");
4018
5003
  }
4019
5004
  assertRetention(readRetention(), existing);
4020
5005
  assertCurrent();
4021
- return { disposition: existing, retainedPath, duplicate: true,
4022
- 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
+ };
4023
5013
  }
4024
5014
  if (!currentEntry || !isDeepStrictEqual(currentEntry, expected)) {
4025
5015
  throw createJournalError("conflict", path, "pending source changed before abandonment");
@@ -4035,11 +5025,23 @@ function createJournalStoreCore(options, getInputContext) {
4035
5025
  else {
4036
5026
  // Reuse the existing legacy discard tombstone so older v1 readers also refuse replay.
4037
5027
  // This is an operator disposition, not a fabricated execution failure or completion.
4038
- disposition = { dispositionKind: "legacy-custody", failureId, updateId: expected.updateId,
4039
- action: "discard", committedAtMs: nowMs, evidenceSha256,
4040
- operatorAuthorityId: input.operatorAuthorityId, authorizedAtMs: nowMs };
4041
- const copy = { version: 1, kind: "pending-input-retention",
4042
- 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
+ };
4043
5045
  writeJournalFile(retainedPath, `${JSON.stringify(copy)}\n`, (boundary, target) => {
4044
5046
  onPublicationBoundary?.(boundary, target);
4045
5047
  assertCurrent();
@@ -4047,12 +5049,20 @@ function createJournalStoreCore(options, getInputContext) {
4047
5049
  }
4048
5050
  assertCurrent();
4049
5051
  assertRetention(readRetention(), disposition);
4050
- const committed = (snapshot) => !snapshot.file.entries.some(entry => entry.updateId === expected.updateId) &&
4051
- 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));
4052
5054
  try {
4053
- 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(); });
4054
- return { disposition, retainedPath, duplicate: false,
4055
- 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
+ };
4056
5066
  }
4057
5067
  catch (error) {
4058
5068
  // A segment may be durable even if later snapshot compaction failed.
@@ -4067,16 +5077,26 @@ function createJournalStoreCore(options, getInputContext) {
4067
5077
  if (!committed(observed))
4068
5078
  throw error;
4069
5079
  assertRetention(readRetention(), disposition);
4070
- return { disposition, retainedPath, duplicate: false,
4071
- 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
+ };
4072
5087
  }
4073
5088
  });
4074
5089
  },
4075
- removeCompleted(updateIds) { return removeCompleted(updateIds); },
4076
- 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
+ },
4077
5096
  inspectQueuedReceipt(expected) {
4078
- return runMutationCore(readCurrent => {
4079
- if (!sourceAccess || !isTelegramUpdateJournalLegacyFamilyVersion(version)) {
5097
+ return runMutationCore((readCurrent) => {
5098
+ if (!sourceAccess ||
5099
+ !isTelegramUpdateJournalLegacyFamilyVersion(version)) {
4080
5100
  throw createJournalError("invalid", path, "queue receipt observation requires an exact legacy v1 source handle");
4081
5101
  }
4082
5102
  const queueOwner = validateQueuedReceiptObservation(expected, path);
@@ -4085,7 +5105,7 @@ function createJournalStoreCore(options, getInputContext) {
4085
5105
  });
4086
5106
  },
4087
5107
  inspectSourceCompletion(expected) {
4088
- return runMutation(readCurrent => {
5108
+ return runMutation((readCurrent) => {
4089
5109
  if (!sourceAccess)
4090
5110
  throw createJournalError("invalid", path, "source completion requires an exact source handle");
4091
5111
  const completion = validateJournalSourceCompletion(expected, path);
@@ -4096,62 +5116,126 @@ function createJournalStoreCore(options, getInputContext) {
4096
5116
  };
4097
5117
  if (isTelegramUpdateJournalLegacyFamilyVersion(version))
4098
5118
  journal.routingInputs = {
4099
- arm(input) { return mutateRoutingInputs(input, false, input.publishedAtMs).entries; },
4100
- 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
+ },
4101
5128
  inspectExpiry(updateId) {
4102
- return runMutation(readCurrent => {
5129
+ return runMutation((readCurrent) => {
4103
5130
  if (!isSafeNonNegativeInteger(updateId))
4104
5131
  throw createJournalError("invalid", path, "invalid routing expiry update ID");
4105
- return inspectRoutingInputExpiry(readCurrent().file, createTelegramUpdateJournalBindingKey({ path, profileName: profile, botIdentity: expectedIdentity }), updateId);
5132
+ return inspectRoutingInputExpiry(readCurrent().file, createTelegramUpdateJournalBindingKey({
5133
+ path,
5134
+ profileName: profile,
5135
+ botIdentity: expectedIdentity,
5136
+ }), updateId);
4106
5137
  });
4107
5138
  },
4108
5139
  inspectGroupExpiry(updateIds) {
4109
- return runMutation(readCurrent => inspectRoutingGroupExpiry(readCurrent().file, createTelegramUpdateJournalBindingKey({ path, profileName: profile, botIdentity: expectedIdentity }), updateIds));
5140
+ return runMutation((readCurrent) => inspectRoutingGroupExpiry(readCurrent().file, createTelegramUpdateJournalBindingKey({
5141
+ path,
5142
+ profileName: profile,
5143
+ botIdentity: expectedIdentity,
5144
+ }), updateIds));
4110
5145
  },
4111
5146
  expire(input) {
4112
- return runMutation(readCurrent => {
5147
+ return runMutation((readCurrent) => {
4113
5148
  const original = validateJournalEntry(input.entry, path, version), lifetime = original.routingInput;
4114
- const binding = createTelegramUpdateJournalBindingKey({ path, profileName: profile, botIdentity: expectedIdentity });
4115
- if (!lifetime || original.state !== "pending" || original.inputClaim || original.inputProvenance || original.queueOwner ||
4116
- original.queueReceiptId || original.queueHandoff || original.failure || input.journalBindingKey !== binding ||
4117
- lifetime.operatorUserId !== input.operatorUserId || !isSafePositiveInteger(input.operatorUserId) || typeof input.isCurrent !== "function") {
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") {
4118
5166
  throw createJournalError("conflict", path, "routing expiry requires an exact unclaimed chooser source");
4119
5167
  }
4120
5168
  const assertCurrent = () => {
4121
5169
  const now = getNowMs();
4122
- if (!isSafeNonNegativeInteger(now) || now < lifetime.expiresAtMs || !input.isCurrent())
5170
+ if (!isSafeNonNegativeInteger(now) ||
5171
+ now < lifetime.expiresAtMs ||
5172
+ !input.isCurrent())
4123
5173
  throw createJournalError("conflict", path, "routing source has not expired under current authority");
4124
5174
  };
4125
5175
  assertCurrent();
4126
- const current = sourceAccess ? readCurrent() : readCurrentStrict(false);
4127
- const evidenceSha256 = createHash("sha256").update(JSON.stringify(original)).digest("hex");
5176
+ const current = sourceAccess
5177
+ ? readCurrent()
5178
+ : readCurrentStrict(false);
5179
+ const evidenceSha256 = createHash("sha256")
5180
+ .update(JSON.stringify(original))
5181
+ .digest("hex");
4128
5182
  const failureId = `routing-expiry:${evidenceSha256}`;
4129
- const existing = current.file.operatorDispositions?.find(value => value.updateId === original.updateId);
4130
- const entry = current.file.entries.find(value => value.updateId === original.updateId);
5183
+ const existing = current.file.operatorDispositions?.find((value) => value.updateId === original.updateId);
5184
+ const entry = current.file.entries.find((value) => value.updateId === original.updateId);
4131
5185
  if (existing) {
4132
- if (entry || !("dispositionKind" in existing) || existing.dispositionKind !== "legacy-custody" || existing.action !== "discard" ||
4133
- existing.failureId !== failureId || existing.evidenceSha256 !== evidenceSha256 || existing.operatorAuthorityId !== `telegram-owner:${input.operatorUserId}`)
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}`)
4134
5194
  throw createJournalError("conflict", path, "routing source has contradictory expiry evidence");
4135
5195
  assertCurrent();
4136
- return { disposition: existing, duplicate: true, entryCount: current.file.entries.length, serializedBytes: current.serializedBytes };
5196
+ return {
5197
+ disposition: existing,
5198
+ duplicate: true,
5199
+ entryCount: current.file.entries.length,
5200
+ serializedBytes: current.serializedBytes,
5201
+ };
4137
5202
  }
4138
5203
  if (!entry || !isDeepStrictEqual(entry, original))
4139
5204
  throw createJournalError("conflict", path, "routing source changed before expiry");
4140
5205
  // A discard tombstone prevents old readers and delayed reports from replaying; no prompt-body archive is written.
4141
5206
  const now = getNowMs();
4142
- const disposition = { dispositionKind: "legacy-custody", failureId,
4143
- updateId: original.updateId, action: "discard", committedAtMs: now, authorizedAtMs: now, evidenceSha256,
4144
- operatorAuthorityId: `telegram-owner:${input.operatorUserId}` };
4145
- const published = publishMutation(current, current.file.entries.filter(value => value.updateId !== original.updateId), true, [...(current.file.operatorDispositions ?? []), disposition], current.file.acceptedThroughUpdateId, (boundary, target) => { onPublicationBoundary?.(boundary, target); assertCurrent(); });
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
+ });
4146
5221
  assertCurrent();
4147
- return { disposition, duplicate: false, entryCount: published.file.entries.length, serializedBytes: published.serializedBytes };
5222
+ return {
5223
+ disposition,
5224
+ duplicate: false,
5225
+ entryCount: published.file.entries.length,
5226
+ serializedBytes: published.serializedBytes,
5227
+ };
4148
5228
  });
4149
5229
  },
4150
5230
  };
4151
5231
  if (!getInputContext)
4152
5232
  return { journal };
4153
5233
  // Strict source acquisition already requires this exact stored receipt scope.
4154
- const bindingKey = createTelegramUpdateJournalBindingKey({ path, profileName: profile, botIdentity: expectedIdentity });
5234
+ const bindingKey = createTelegramUpdateJournalBindingKey({
5235
+ path,
5236
+ profileName: profile,
5237
+ botIdentity: expectedIdentity,
5238
+ });
4155
5239
  const createInputSourceReference = (updateId) => ({
4156
5240
  journalBindingKey: bindingKey,
4157
5241
  tokenSha256: expectedIdentity.tokenSha256,
@@ -4184,10 +5268,15 @@ function createJournalStoreCore(options, getInputContext) {
4184
5268
  assertInputProcess(owner);
4185
5269
  return { owner, recipientBindingKey };
4186
5270
  };
4187
- const runInputAdmission = (kind, operation) => workspaceAdmission ? runWithTelegramWorkspaceAdmissions({
4188
- ledger: workspaceAdmission, operationId: `input:${randomUUID()}`, operationKind: `journal.input.${kind}`,
4189
- scopes: [{ kind: "profile" }], operation,
4190
- }) : 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();
4191
5280
  const runInputMutation = (kind, operation) => runInputAdmission(kind, () => runMutation(operation));
4192
5281
  const publishInputMutation = (current, entries, updateId, context) => publishMutation(current, entries, true, current.file.operatorDispositions, current.file.acceptedThroughUpdateId, (boundary, publicationPath) => {
4193
5282
  onPublicationBoundary?.(boundary, publicationPath);
@@ -4197,33 +5286,49 @@ function createJournalStoreCore(options, getInputContext) {
4197
5286
  }, updateId);
4198
5287
  const publishInputSettlement = (current, entries, contentChanged) => publishMutation(current, entries, contentChanged, current.file.operatorDispositions, current.file.acceptedThroughUpdateId, onPublicationBoundary, false);
4199
5288
  const normalizeInputSourceReference = (value, operation) => {
4200
- if (!isRecord(value) || !hasOnlyKeys(value, ["journalBindingKey", "tokenSha256", "updateId"])) {
5289
+ if (!isRecord(value) ||
5290
+ !hasOnlyKeys(value, ["journalBindingKey", "tokenSha256", "updateId"])) {
4201
5291
  throw createJournalError("invalid", path, `received invalid input handoff ${operation} source`);
4202
5292
  }
4203
5293
  const journalBindingKey = value.journalBindingKey;
4204
5294
  const updateId = value.updateId;
4205
- if (journalBindingKey !== bindingKey || value.tokenSha256 !== expectedIdentity.tokenSha256 ||
5295
+ if (journalBindingKey !== bindingKey ||
5296
+ value.tokenSha256 !== expectedIdentity.tokenSha256 ||
4206
5297
  !isSafeNonNegativeInteger(updateId)) {
4207
5298
  throw createJournalError("conflict", path, "input receipt does not match its source identity");
4208
5299
  }
4209
- return { journalBindingKey, tokenSha256: expectedIdentity.tokenSha256, updateId };
5300
+ return {
5301
+ journalBindingKey,
5302
+ tokenSha256: expectedIdentity.tokenSha256,
5303
+ updateId,
5304
+ };
4210
5305
  };
4211
5306
  const normalizeInputReceipt = (value, requireCurrentProcess = true) => {
4212
5307
  if (!isRecord(value) ||
4213
- !hasOnlyKeys(value, ["journalBindingKey", "tokenSha256", "updateId", "owner"])) {
5308
+ !hasOnlyKeys(value, [
5309
+ "journalBindingKey",
5310
+ "tokenSha256",
5311
+ "updateId",
5312
+ "owner",
5313
+ ])) {
4214
5314
  throw createJournalError("invalid", path, "received an invalid input receipt");
4215
5315
  }
4216
- const source = normalizeInputSourceReference({ journalBindingKey: value.journalBindingKey,
4217
- 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");
4218
5321
  const owner = validateJournalQueueOwner(value.owner, path);
4219
5322
  if (requireCurrentProcess)
4220
5323
  assertInputProcess(owner);
4221
5324
  return { ...source, owner };
4222
5325
  };
4223
5326
  const normalizeInputHandoffOffer = (value) => {
4224
- if (!isRecord(value) || !hasOnlyKeys(value, ["receipt", "recipientOwner", "handoffToken"]) ||
5327
+ if (!isRecord(value) ||
5328
+ !hasOnlyKeys(value, ["receipt", "recipientOwner", "handoffToken"]) ||
4225
5329
  !isBoundedString(value.handoffToken, TELEGRAM_UPDATE_JOURNAL_QUEUE_HANDOFF_TOKEN_MAX_LENGTH) ||
4226
- value.handoffToken.length < TELEGRAM_UPDATE_JOURNAL_QUEUE_HANDOFF_TOKEN_MIN_LENGTH) {
5330
+ value.handoffToken.length <
5331
+ TELEGRAM_UPDATE_JOURNAL_QUEUE_HANDOFF_TOKEN_MIN_LENGTH) {
4227
5332
  throw createJournalError("invalid", path, "received invalid input handoff offer");
4228
5333
  }
4229
5334
  const receipt = normalizeInputReceipt(value.receipt);
@@ -4240,52 +5345,81 @@ function createJournalStoreCore(options, getInputContext) {
4240
5345
  return value;
4241
5346
  };
4242
5347
  const normalizeInputHandoffAccept = (value) => {
4243
- if (!isRecord(value) || !hasOnlyKeys(value, ["source", "recipientOwner", "handoffId"])) {
5348
+ if (!isRecord(value) ||
5349
+ !hasOnlyKeys(value, ["source", "recipientOwner", "handoffId"])) {
4244
5350
  throw createJournalError("invalid", path, "received invalid input handoff accept");
4245
5351
  }
4246
5352
  const source = normalizeInputSourceReference(value.source, "accept");
4247
5353
  const recipientOwner = validateJournalQueueOwnerIdentity(value.recipientOwner, path);
4248
5354
  assertInputProcess(recipientOwner);
4249
- return { source, recipientOwner, handoffId: normalizeInputHandoffId(value.handoffId, "accept") };
5355
+ return {
5356
+ source,
5357
+ recipientOwner,
5358
+ handoffId: normalizeInputHandoffId(value.handoffId, "accept"),
5359
+ };
4250
5360
  };
4251
5361
  const normalizeInputHandoffCancel = (value) => {
4252
- if (!isRecord(value) || !hasOnlyKeys(value, ["receipt", "recipientOwner", "handoffId"])) {
5362
+ if (!isRecord(value) ||
5363
+ !hasOnlyKeys(value, ["receipt", "recipientOwner", "handoffId"])) {
4253
5364
  throw createJournalError("invalid", path, "received invalid input handoff cancel");
4254
5365
  }
4255
- return { receipt: normalizeInputReceipt(value.receipt),
5366
+ return {
5367
+ receipt: normalizeInputReceipt(value.receipt),
4256
5368
  recipientOwner: validateJournalQueueOwnerIdentity(value.recipientOwner, path),
4257
- handoffId: normalizeInputHandoffId(value.handoffId, "cancel") };
5369
+ handoffId: normalizeInputHandoffId(value.handoffId, "cancel"),
5370
+ };
4258
5371
  };
4259
5372
  const normalizeInputQueue = (value) => {
4260
- if (!isRecord(value) || !hasOnlyKeys(value, ["queueKind", "receiptId", "receipts"]) ||
5373
+ if (!isRecord(value) ||
5374
+ !hasOnlyKeys(value, ["queueKind", "receiptId", "receipts"]) ||
4261
5375
  (value.queueKind !== "prompt" && value.queueKind !== "control") ||
4262
5376
  !isBoundedString(value.receiptId, TELEGRAM_UPDATE_JOURNAL_QUEUE_OWNER_ID_MAX_LENGTH) ||
4263
- !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) {
4264
5380
  throw createJournalError("invalid", path, "received invalid input queue transition");
4265
5381
  }
4266
- 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);
4267
5385
  if (receipts.some((receipt, index) => index > 0 && receipt.updateId === receipts[index - 1].updateId)) {
4268
5386
  throw createJournalError("invalid", path, "received duplicate input queue transition receipts");
4269
5387
  }
4270
5388
  return { queueKind: value.queueKind, receiptId: value.receiptId, receipts };
4271
5389
  };
4272
5390
  const ownedInput = (current, receipt) => {
4273
- const entry = current.file.entries.find(candidate => candidate.updateId === receipt.updateId);
4274
- 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" ||
4275
5394
  !areTelegramUpdateJournalQueueOwnersEqual(entry.inputClaim.owner, receipt.owner)) {
4276
5395
  throw createJournalError("conflict", path, "input receipt lost its exact acquisition or phase");
4277
5396
  }
4278
5397
  return entry;
4279
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
+ };
4280
5411
  const getReadyInputRelease = (current, receipt, allowOffered = false) => {
4281
- const entry = current.file.entries.find(candidate => candidate.updateId === receipt.updateId);
4282
- 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) {
4283
5416
  throw createJournalError("conflict", path, "input is unavailable for ready-claim release");
4284
5417
  }
4285
5418
  if (!entry.inputClaim)
4286
5419
  return { entry, unclaimed: true };
4287
5420
  if (!areTelegramUpdateJournalQueueOwnersEqual(entry.inputClaim.owner, receipt.owner) ||
4288
- entry.inputClaim.phase !== "ready" || (!allowOffered && entry.inputClaim.handoff !== undefined)) {
5421
+ entry.inputClaim.phase !== "ready" ||
5422
+ (!allowOffered && entry.inputClaim.handoff !== undefined)) {
4289
5423
  throw createJournalError("conflict", path, "cannot release stale, running, or offered input authority");
4290
5424
  }
4291
5425
  return { entry, unclaimed: false };
@@ -4293,7 +5427,7 @@ function createJournalStoreCore(options, getInputContext) {
4293
5427
  const publishReadyInputRelease = (current, entry) => {
4294
5428
  const released = cloneEntry(entry);
4295
5429
  delete released.inputClaim;
4296
- 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);
4297
5431
  };
4298
5432
  const input = {
4299
5433
  read: journal.read,
@@ -4301,12 +5435,16 @@ function createJournalStoreCore(options, getInputContext) {
4301
5435
  listLegacyCustodyCandidates() {
4302
5436
  const sourceAccess = options.sourceAccess;
4303
5437
  const evidence = readTelegramUpdateJournalSource({
4304
- directory: sourceAccess.directory, path, profile,
4305
- botIdentity: expectedIdentity, limits: sourceAccess.limits,
5438
+ directory: sourceAccess.directory,
5439
+ path,
5440
+ profile,
5441
+ botIdentity: expectedIdentity,
5442
+ limits: sourceAccess.limits,
4306
5443
  version,
4307
5444
  });
4308
5445
  return evidence.kind === "present"
4309
- ? listTelegramUpdateJournalLegacyCustodyCandidates(evidence.file) : [];
5446
+ ? listTelegramUpdateJournalLegacyCustodyCandidates(evidence.file)
5447
+ : [];
4310
5448
  },
4311
5449
  applyLegacyCustodyDisposition(value) {
4312
5450
  return runInputAdmission("legacy-custody-disposition", () => journal.applyLegacyCustodyDisposition(value));
@@ -4337,117 +5475,163 @@ function createJournalStoreCore(options, getInputContext) {
4337
5475
  }
4338
5476
  requestedIds.add(updateId);
4339
5477
  }
4340
- return runInputMutation("remove-excluded", read => {
5478
+ return runInputMutation("remove-excluded", (read) => {
4341
5479
  const current = read();
4342
- if (!current.exists || [...requestedIds].some(id => id > (current.file.acceptedThroughUpdateId ?? -1))) {
5480
+ if (!current.exists ||
5481
+ [...requestedIds].some((id) => id > (current.file.acceptedThroughUpdateId ?? -1))) {
4343
5482
  throw createJournalError("conflict", path, "exclusion removal lacks retained source admission evidence");
4344
5483
  }
4345
- const selected = current.file.entries.filter(entry => requestedIds.has(entry.updateId));
4346
- 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)) {
4347
5486
  throw createJournalError("conflict", path, "cannot remove input without immutable exclusion evidence");
4348
5487
  }
4349
5488
  // Strict v3 decoding already excludes raw claims and queued authority on vetoed entries.
4350
- const removedUpdateIds = selected.map(entry => entry.updateId);
4351
- const published = publishInputSettlement(current, current.file.entries.filter(entry => !requestedIds.has(entry.updateId)), removedUpdateIds.length > 0);
4352
- 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
+ };
4353
5496
  });
4354
5497
  },
4355
5498
  releaseInput(value) {
4356
5499
  const receipt = normalizeInputReceipt(value);
4357
- return runInputMutation("release", read => {
5500
+ return runInputMutation("release", (read) => {
4358
5501
  const current = read();
4359
5502
  const release = getReadyInputRelease(current, receipt);
4360
5503
  if (release.unclaimed)
4361
- return { released: false,
4362
- entryCount: current.file.entries.length, serializedBytes: current.serializedBytes };
5504
+ return {
5505
+ released: false,
5506
+ entryCount: current.file.entries.length,
5507
+ serializedBytes: current.serializedBytes,
5508
+ };
4363
5509
  const published = publishReadyInputRelease(current, release.entry);
4364
- return { released: true, entryCount: published.file.entries.length,
4365
- serializedBytes: published.serializedBytes };
5510
+ return {
5511
+ released: true,
5512
+ entryCount: published.file.entries.length,
5513
+ serializedBytes: published.serializedBytes,
5514
+ };
4366
5515
  });
4367
5516
  },
4368
5517
  recoverReadyInput(value) {
4369
- if (!isRecord(value) || !hasOnlyKeys(value, ["receipt", "recoveryOwner"])) {
5518
+ if (!isRecord(value) ||
5519
+ !hasOnlyKeys(value, ["receipt", "recoveryOwner"])) {
4370
5520
  throw createJournalError("invalid", path, "received invalid ready-input recovery authority");
4371
5521
  }
4372
5522
  const receipt = normalizeInputReceipt(value.receipt, false);
4373
5523
  const recoveryOwner = validateJournalQueueOwnerIdentity(value.recoveryOwner, path);
4374
5524
  assertInputProcess(recoveryOwner);
4375
- return runInputMutation("recover-ready", read => {
5525
+ return runInputMutation("recover-ready", (read) => {
4376
5526
  const current = read();
4377
5527
  const release = getReadyInputRelease(current, receipt, true);
4378
5528
  if (release.unclaimed)
4379
- return { status: "unclaimed",
4380
- entryCount: current.file.entries.length, serializedBytes: current.serializedBytes };
5529
+ return {
5530
+ status: "unclaimed",
5531
+ entryCount: current.file.entries.length,
5532
+ serializedBytes: current.serializedBytes,
5533
+ };
4381
5534
  let liveness;
4382
5535
  try {
4383
- liveness = getQueueProcessLiveness({ processId: receipt.owner.processId,
4384
- processBirthId: receipt.owner.processBirthId });
5536
+ liveness = getQueueProcessLiveness({
5537
+ processId: receipt.owner.processId,
5538
+ processBirthId: receipt.owner.processBirthId,
5539
+ });
4385
5540
  }
4386
5541
  catch (error) {
4387
5542
  throw createJournalError("io", path, "could not prove ready input owner liveness", error);
4388
5543
  }
4389
5544
  if (liveness !== "dead")
4390
- return { status: liveness === "alive" ? "owner-alive" : "owner-unverifiable",
4391
- 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
+ };
4392
5552
  const published = publishReadyInputRelease(current, release.entry);
4393
- return { status: "recovered", entryCount: published.file.entries.length,
4394
- serializedBytes: published.serializedBytes };
5553
+ return {
5554
+ status: "recovered",
5555
+ entryCount: published.file.entries.length,
5556
+ serializedBytes: published.serializedBytes,
5557
+ };
4395
5558
  });
4396
5559
  },
4397
5560
  offerInputHandoff(value) {
4398
5561
  const { receipt, recipientOwner, handoffToken } = normalizeInputHandoffOffer(value);
4399
- return runInputMutation("offer", read => {
4400
- const context = currentInputContext();
4401
- const current = read();
4402
- const entry = ownedInput(current, receipt);
4403
- const claim = entry.inputClaim;
4404
- if (claim.phase !== "ready" || !inputOwnerMatchesIdentity(receipt.owner, context.owner) ||
4405
- claim.recipientBindingKey !== context.recipientBindingKey) {
4406
- throw createJournalError("conflict", path, "input handoff donor authority is stale or running");
4407
- }
5562
+ return runInputMutation("offer", (read) => {
5563
+ const { context, current, claim } = readReadyDonorInput(read(), receipt);
4408
5564
  if (claim.handoff) {
4409
5565
  if (!isDeepStrictEqual(claim.handoff.recipientOwner, recipientOwner)) {
4410
5566
  throw createJournalError("conflict", path, "input already has another handoff offer");
4411
5567
  }
4412
- return { source: createInputSourceReference(receipt.updateId),
5568
+ return {
5569
+ source: createInputSourceReference(receipt.updateId),
4413
5570
  handoff: cloneJournalQueueHandoff(claim.handoff),
4414
- previousOwner: cloneJournalQueueOwner(receipt.owner), duplicate: true,
4415
- 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
+ };
4416
5576
  }
4417
- const handoffId = createTelegramInputHandoffId({ handoffToken, journalBindingKey: bindingKey,
4418
- updateId: receipt.updateId, donorOwner: receipt.owner, recipientOwner,
4419
- 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
+ });
4420
5585
  const offeredAtMs = getNowMs();
4421
5586
  if (!isSafeNonNegativeInteger(offeredAtMs)) {
4422
5587
  throw createJournalError("invalid", path, "received an invalid input handoff offer timestamp");
4423
5588
  }
4424
- const handoff = { handoffId, offeredAtMs,
4425
- recipientOwner: { ...recipientOwner } };
5589
+ const handoff = {
5590
+ handoffId,
5591
+ offeredAtMs,
5592
+ recipientOwner: { ...recipientOwner },
5593
+ };
4426
5594
  const offeredClaim = { ...claim, handoff };
4427
- const published = publishInputMutation(current, current.file.entries.map(candidate => candidate.updateId === receipt.updateId ? { ...candidate, inputClaim: offeredClaim } : candidate), receipt.updateId, context);
4428
- 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),
4429
5600
  handoff: cloneJournalQueueHandoff(handoff),
4430
- previousOwner: cloneJournalQueueOwner(receipt.owner), duplicate: false,
4431
- 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
+ };
4432
5606
  });
4433
5607
  },
4434
5608
  acceptInputHandoff(value) {
4435
5609
  const { source, recipientOwner, handoffId } = normalizeInputHandoffAccept(value);
4436
- return runInputMutation("accept", read => {
5610
+ return runInputMutation("accept", (read) => {
4437
5611
  const context = currentInputContext();
4438
5612
  const current = read();
4439
- const entry = current.file.entries.find(candidate => candidate.updateId === source.updateId);
5613
+ const entry = current.file.entries.find((candidate) => candidate.updateId === source.updateId);
4440
5614
  const claim = entry?.inputClaim;
4441
- if (!entry || entry.state !== "pending" || entry.preApprovalExcluded !== false || !claim ||
5615
+ if (!entry ||
5616
+ entry.state !== "pending" ||
5617
+ entry.preApprovalExcluded !== false ||
5618
+ !claim ||
4442
5619
  !isDeepStrictEqual(context.owner, recipientOwner) ||
4443
5620
  context.recipientBindingKey !== claim.recipientBindingKey) {
4444
5621
  throw createJournalError("conflict", path, "input handoff recipient authority is unavailable or changed");
4445
5622
  }
4446
- if (claim.owner.handoffId === handoffId && inputOwnerMatchesIdentity(claim.owner, recipientOwner)) {
4447
- return { handoffId, receipt: createInputReceipt(source.updateId, claim.owner), duplicate: true,
4448
- 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
+ };
4449
5632
  }
4450
- if (claim.phase !== "ready" || claim.handoff?.handoffId !== handoffId ||
5633
+ if (claim.phase !== "ready" ||
5634
+ claim.handoff?.handoffId !== handoffId ||
4451
5635
  !isDeepStrictEqual(claim.handoff.recipientOwner, recipientOwner)) {
4452
5636
  throw createJournalError("conflict", path, "cannot accept stale or unauthenticated input handoff");
4453
5637
  }
@@ -4456,75 +5640,109 @@ function createJournalStoreCore(options, getInputContext) {
4456
5640
  throw createJournalError("invalid", path, "received an invalid input handoff acquisition timestamp");
4457
5641
  }
4458
5642
  const previousOwner = cloneJournalQueueOwner(claim.owner);
4459
- const owner = { ...recipientOwner,
4460
- acquisitionId: randomUUID(), acquiredAtMs, handoffId };
5643
+ const owner = {
5644
+ ...recipientOwner,
5645
+ acquisitionId: randomUUID(),
5646
+ acquiredAtMs,
5647
+ handoffId,
5648
+ };
4461
5649
  const acceptedClaim = structuredClone(claim);
4462
5650
  acceptedClaim.owner = owner;
4463
5651
  delete acceptedClaim.handoff;
4464
- 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);
4465
5655
  const published = publishInputMutation(current, publishedEntries, source.updateId, context);
4466
- return { handoffId, previousOwner, receipt: createInputReceipt(source.updateId, owner), duplicate: false,
4467
- 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
+ };
4468
5664
  });
4469
5665
  },
4470
5666
  cancelInputHandoff(value) {
4471
5667
  const { receipt, recipientOwner, handoffId } = normalizeInputHandoffCancel(value);
4472
- return runInputMutation("cancel", read => {
4473
- const context = currentInputContext();
4474
- const current = read();
4475
- const entry = ownedInput(current, receipt);
4476
- const claim = entry.inputClaim;
4477
- if (claim.phase !== "ready" || !inputOwnerMatchesIdentity(receipt.owner, context.owner) ||
4478
- claim.recipientBindingKey !== context.recipientBindingKey) {
4479
- throw createJournalError("conflict", path, "input handoff donor authority is stale or running");
4480
- }
5668
+ return runInputMutation("cancel", (read) => {
5669
+ const { context, current, claim } = readReadyDonorInput(read(), receipt);
4481
5670
  if (!claim.handoff)
4482
- return { handoffId, previousOwner: cloneJournalQueueOwner(receipt.owner),
4483
- 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
+ };
4484
5678
  if (claim.handoff.handoffId !== handoffId ||
4485
5679
  !isDeepStrictEqual(claim.handoff.recipientOwner, recipientOwner)) {
4486
5680
  throw createJournalError("conflict", path, "cannot cancel another input handoff offer");
4487
5681
  }
4488
5682
  const cancelledClaim = structuredClone(claim);
4489
5683
  delete cancelledClaim.handoff;
4490
- 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);
4491
5687
  const published = publishInputMutation(current, publishedEntries, receipt.updateId, context);
4492
- return { handoffId, previousOwner: cloneJournalQueueOwner(receipt.owner), cancelled: true,
4493
- 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
+ };
4494
5695
  });
4495
5696
  },
4496
5697
  queueInputs(value) {
4497
5698
  const { queueKind, receiptId, receipts } = normalizeInputQueue(value);
4498
- return runInputMutation("queue", read => {
5699
+ return runInputMutation("queue", (read) => {
4499
5700
  const context = currentInputContext();
4500
- if (receipts.some(receipt => !inputOwnerMatchesIdentity(receipt.owner, context.owner))) {
5701
+ if (receipts.some((receipt) => !inputOwnerMatchesIdentity(receipt.owner, context.owner))) {
4501
5702
  throw createJournalError("conflict", path, "input queue transition belongs to another session");
4502
5703
  }
4503
5704
  const current = read();
4504
- const receiptsById = new Map(receipts.map(receipt => [receipt.updateId, receipt]));
4505
- const requestedIds = receipts.map(receipt => receipt.updateId);
4506
- 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);
4507
5708
  if (existingReceiptEntries.length > 0) {
4508
5709
  const queueOwner = existingReceiptEntries[0]?.queueOwner;
4509
- if (existingReceiptEntries.length !== receipts.length || !queueOwner ||
5710
+ if (existingReceiptEntries.length !== receipts.length ||
5711
+ !queueOwner ||
4510
5712
  !inputOwnerMatchesIdentity(queueOwner, context.owner) ||
4511
- existingReceiptEntries.some(entry => {
5713
+ existingReceiptEntries.some((entry) => {
4512
5714
  const receipt = receiptsById.get(entry.updateId);
4513
- return !receipt || entry.state !== "queued" || entry.queueKind !== queueKind ||
4514
- entry.queueHandoff !== undefined || !entry.inputProvenance ||
5715
+ return (!receipt ||
5716
+ entry.state !== "queued" ||
5717
+ entry.queueKind !== queueKind ||
5718
+ entry.queueHandoff !== undefined ||
5719
+ !entry.inputProvenance ||
4515
5720
  !areTelegramUpdateJournalQueueOwnersEqual(entry.inputProvenance.owner, receipt.owner) ||
4516
- entry.inputProvenance.recipientBindingKey !== context.recipientBindingKey;
5721
+ entry.inputProvenance.recipientBindingKey !==
5722
+ context.recipientBindingKey);
4517
5723
  })) {
4518
5724
  throw createJournalError("conflict", path, "input queue transition conflicts with queued authority");
4519
5725
  }
4520
- return { queued: false, queueReceipt: { queueKind, receiptId,
4521
- sourceUpdateIds: requestedIds, queueOwner: cloneJournalQueueOwner(queueOwner) },
4522
- 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
+ };
4523
5737
  }
4524
5738
  for (const receipt of receipts) {
4525
- const entry = current.file.entries.find(candidate => candidate.updateId === receipt.updateId);
4526
- if (!entry || entry.state !== "pending" || entry.preApprovalExcluded !== false ||
4527
- !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 ||
4528
5746
  !areTelegramUpdateJournalQueueOwnersEqual(entry.inputClaim.owner, receipt.owner) ||
4529
5747
  entry.inputClaim.recipientBindingKey !== context.recipientBindingKey) {
4530
5748
  throw createJournalError("conflict", path, "input queue transition lost exact running authority");
@@ -4534,76 +5752,122 @@ function createJournalStoreCore(options, getInputContext) {
4534
5752
  if (!isSafeNonNegativeInteger(acquiredAtMs)) {
4535
5753
  throw createJournalError("invalid", path, "received an invalid input queue acquisition timestamp");
4536
5754
  }
4537
- const queueOwner = { ...context.owner,
4538
- acquisitionId: randomUUID(), acquiredAtMs };
4539
- 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) => {
4540
5761
  const receipt = receiptsById.get(entry.updateId);
4541
5762
  if (!receipt)
4542
5763
  return entry;
4543
5764
  const claim = entry.inputClaim;
4544
- return { updateId: entry.updateId, update: entry.update,
4545
- admittedAtMs: entry.admittedAtMs, preApprovalExcluded: false, state: "queued",
4546
- queueKind, queueReceiptId: receiptId, queueOwner: cloneJournalQueueOwner(queueOwner),
4547
- 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),
4548
5776
  recipientBindingKey: claim.recipientBindingKey,
4549
5777
  ...(claim.executionUpdate
4550
- ? { executionUpdate: structuredClone(claim.executionUpdate) } : {}) } };
5778
+ ? { executionUpdate: structuredClone(claim.executionUpdate) }
5779
+ : {}),
5780
+ },
5781
+ };
4551
5782
  });
4552
5783
  const published = publishInputMutation(current, queuedEntries, requestedIds, context);
4553
- return { queued: true, queueReceipt: { queueKind, receiptId,
4554
- sourceUpdateIds: requestedIds, queueOwner: cloneJournalQueueOwner(queueOwner) },
4555
- 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
+ };
4556
5795
  });
4557
5796
  },
4558
5797
  acquireInput(value) {
4559
5798
  const updateId = value.updateId;
4560
5799
  const recipientBindingKey = value.recipientBindingKey;
4561
- const executionUpdate = value.executionUpdate === undefined ? undefined : normalizeIncomingJournaledUpdate(value.executionUpdate, path);
5800
+ const executionUpdate = value.executionUpdate === undefined
5801
+ ? undefined
5802
+ : normalizeIncomingJournaledUpdate(value.executionUpdate, path);
4562
5803
  if (!isSafeNonNegativeInteger(updateId) ||
4563
5804
  !isBoundedString(recipientBindingKey, TELEGRAM_UPDATE_JOURNAL_INPUT_BINDING_MAX_LENGTH) ||
4564
- !recipientBindingKey.trim() || (executionUpdate && executionUpdate.update_id !== updateId)) {
5805
+ !recipientBindingKey.trim() ||
5806
+ (executionUpdate && executionUpdate.update_id !== updateId)) {
4565
5807
  throw createJournalError("invalid", path, "received invalid input acquisition identity");
4566
5808
  }
4567
- return runInputMutation("acquire", read => {
5809
+ return runInputMutation("acquire", (read) => {
4568
5810
  const context = currentInputContext();
4569
5811
  const identity = context.owner;
4570
5812
  if (recipientBindingKey !== context.recipientBindingKey) {
4571
5813
  throw createJournalError("conflict", path, "input acquisition targets another execution binding");
4572
5814
  }
4573
5815
  const current = read();
4574
- const entry = current.file.entries.find(candidate => candidate.updateId === updateId);
4575
- 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) {
4576
5820
  throw createJournalError("conflict", path, "input is not eligible for acquisition");
4577
5821
  }
4578
5822
  const projected = executionUpdate ?? entry.update;
4579
5823
  const existing = entry.inputClaim;
4580
5824
  if (existing) {
4581
5825
  if (!isTelegramUpdateJournalQueueOwnerProcess(existing.owner, identity) ||
4582
- existing.owner.sessionGeneration !== identity.sessionGeneration || existing.recipientBindingKey !== recipientBindingKey ||
5826
+ existing.owner.sessionGeneration !== identity.sessionGeneration ||
5827
+ existing.recipientBindingKey !== recipientBindingKey ||
4583
5828
  !isDeepStrictEqual(existing.executionUpdate ?? entry.update, projected)) {
4584
5829
  throw createJournalError("conflict", path, "input already has another owner or execution projection");
4585
5830
  }
4586
- return { acquired: false, receipt: createInputReceipt(updateId, existing.owner) };
5831
+ return {
5832
+ acquired: false,
5833
+ receipt: createInputReceipt(updateId, existing.owner),
5834
+ };
4587
5835
  }
4588
- const claim = { phase: "ready", recipientBindingKey,
4589
- owner: { ...identity, acquisitionId: randomUUID(), acquiredAtMs: getNowMs() },
4590
- ...(!isDeepStrictEqual(projected, entry.update) ? { executionUpdate: projected } : {}) };
4591
- const claimedEntries = current.file.entries.map(candidate => candidate.updateId === updateId ? { ...candidate, inputClaim: claim } : candidate);
4592
- 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
+ };
4593
5855
  const reservedEntries = replaceInputHeadroomEntry(current.file, updateId, reservedEntry);
4594
5856
  const actualBytes = Buffer.byteLength(serializeJournalFile({ ...current.file, entries: claimedEntries }));
4595
- const reservedBytes = Buffer.byteLength(serializeJournalFile({ ...current.file, entries: reservedEntries })) +
4596
- TELEGRAM_UPDATE_JOURNAL_INPUT_PROJECTION_HEADROOM_BYTES;
5857
+ const reservedBytes = Buffer.byteLength(serializeJournalFile({ ...current.file, entries: reservedEntries })) + TELEGRAM_UPDATE_JOURNAL_INPUT_PROJECTION_HEADROOM_BYTES;
4597
5858
  if (actualBytes > reservedBytes) {
4598
5859
  throw createJournalError("capacity", path, "execution projection exceeds reserved input headroom");
4599
5860
  }
4600
5861
  publishInputMutation(current, claimedEntries, updateId, context);
4601
- return { acquired: true, receipt: createInputReceipt(updateId, claim.owner) };
5862
+ return {
5863
+ acquired: true,
5864
+ receipt: createInputReceipt(updateId, claim.owner),
5865
+ };
4602
5866
  });
4603
5867
  },
4604
5868
  startInput(value) {
4605
5869
  const receipt = normalizeInputReceipt(value);
4606
- return runInputMutation("start", read => {
5870
+ return runInputMutation("start", (read) => {
4607
5871
  const context = currentInputContext();
4608
5872
  const current = read();
4609
5873
  const entry = ownedInput(current, receipt);
@@ -4616,24 +5880,41 @@ function createJournalStoreCore(options, getInputContext) {
4616
5880
  return { started: false };
4617
5881
  if (claim.handoff)
4618
5882
  throw createJournalError("conflict", path, "input handoff freezes donor execution");
4619
- publishInputMutation(current, current.file.entries.map(candidate => candidate.updateId === receipt.updateId
4620
- ? { ...candidate, inputClaim: { ...claim, phase: "running" } } : candidate), receipt.updateId, context);
4621
- 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
+ };
4622
5893
  });
4623
5894
  },
4624
5895
  completeInput(value) {
4625
5896
  const receipt = normalizeInputReceipt(value);
4626
- return runInputMutation("complete", read => {
5897
+ return runInputMutation("complete", (read) => {
4627
5898
  const current = read();
4628
- const exists = current.file.entries.some(entry => entry.updateId === receipt.updateId);
4629
- if (!exists && current.exists && receipt.updateId <= (current.file.acceptedThroughUpdateId ?? -1)) {
4630
- 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
+ };
4631
5908
  }
4632
5909
  const entry = ownedInput(current, receipt);
4633
5910
  if (entry.inputClaim.phase !== "running")
4634
5911
  throw createJournalError("conflict", path, "input has not started");
4635
- const published = publishInputSettlement(current, current.file.entries.filter(candidate => candidate.updateId !== receipt.updateId), true);
4636
- 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
+ };
4637
5918
  });
4638
5919
  },
4639
5920
  };