@llblab/pi-kit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (523) hide show
  1. package/AGENTS.md +35 -0
  2. package/BACKLOG.md +3 -0
  3. package/CHANGELOG.md +9 -0
  4. package/LICENSE +21 -0
  5. package/README.md +43 -0
  6. package/node_modules/@llblab/pi-actors/AGENTS.md +150 -0
  7. package/node_modules/@llblab/pi-actors/BACKLOG.md +6 -0
  8. package/node_modules/@llblab/pi-actors/CHANGELOG.md +865 -0
  9. package/node_modules/@llblab/pi-actors/README.md +231 -0
  10. package/node_modules/@llblab/pi-actors/banner.jpg +0 -0
  11. package/node_modules/@llblab/pi-actors/dist/fixtures/protocol/artifact-manifest.json +9 -0
  12. package/node_modules/@llblab/pi-actors/dist/fixtures/protocol/control-endpoint.json +6 -0
  13. package/node_modules/@llblab/pi-actors/dist/fixtures/protocol/control-record.json +9 -0
  14. package/node_modules/@llblab/pi-actors/dist/fixtures/protocol/recipe-summary.json +8 -0
  15. package/node_modules/@llblab/pi-actors/dist/fixtures/protocol/run-state.json +10 -0
  16. package/node_modules/@llblab/pi-actors/dist/fixtures/protocol/trace-event.json +9 -0
  17. package/node_modules/@llblab/pi-actors/dist/index.d.ts +7 -0
  18. package/node_modules/@llblab/pi-actors/dist/index.js +17 -0
  19. package/node_modules/@llblab/pi-actors/dist/lib/async-runs.d.ts +146 -0
  20. package/node_modules/@llblab/pi-actors/dist/lib/async-runs.js +800 -0
  21. package/node_modules/@llblab/pi-actors/dist/lib/automatic-review-runtime.d.ts +20 -0
  22. package/node_modules/@llblab/pi-actors/dist/lib/automatic-review-runtime.js +103 -0
  23. package/node_modules/@llblab/pi-actors/dist/lib/command-templates.d.ts +85 -0
  24. package/node_modules/@llblab/pi-actors/dist/lib/command-templates.js +804 -0
  25. package/node_modules/@llblab/pi-actors/dist/lib/config.d.ts +34 -0
  26. package/node_modules/@llblab/pi-actors/dist/lib/config.js +216 -0
  27. package/node_modules/@llblab/pi-actors/dist/lib/control-projection.d.ts +20 -0
  28. package/node_modules/@llblab/pi-actors/dist/lib/control-projection.js +66 -0
  29. package/node_modules/@llblab/pi-actors/dist/lib/control.d.ts +15 -0
  30. package/node_modules/@llblab/pi-actors/dist/lib/control.js +97 -0
  31. package/node_modules/@llblab/pi-actors/dist/lib/draft-consolidation-transaction.d.ts +65 -0
  32. package/node_modules/@llblab/pi-actors/dist/lib/draft-consolidation-transaction.js +610 -0
  33. package/node_modules/@llblab/pi-actors/dist/lib/draft-consolidation.d.ts +35 -0
  34. package/node_modules/@llblab/pi-actors/dist/lib/draft-consolidation.js +126 -0
  35. package/node_modules/@llblab/pi-actors/dist/lib/draft-review.d.ts +56 -0
  36. package/node_modules/@llblab/pi-actors/dist/lib/draft-review.js +254 -0
  37. package/node_modules/@llblab/pi-actors/dist/lib/draft-sleep.d.ts +65 -0
  38. package/node_modules/@llblab/pi-actors/dist/lib/draft-sleep.js +467 -0
  39. package/node_modules/@llblab/pi-actors/dist/lib/execution-output.d.ts +25 -0
  40. package/node_modules/@llblab/pi-actors/dist/lib/execution-output.js +88 -0
  41. package/node_modules/@llblab/pi-actors/dist/lib/execution-sessions.d.ts +17 -0
  42. package/node_modules/@llblab/pi-actors/dist/lib/execution-sessions.js +85 -0
  43. package/node_modules/@llblab/pi-actors/dist/lib/execution.d.ts +99 -0
  44. package/node_modules/@llblab/pi-actors/dist/lib/execution.js +725 -0
  45. package/node_modules/@llblab/pi-actors/dist/lib/extension-runtime.d.ts +21 -0
  46. package/node_modules/@llblab/pi-actors/dist/lib/extension-runtime.js +141 -0
  47. package/node_modules/@llblab/pi-actors/dist/lib/file-state.d.ts +18 -0
  48. package/node_modules/@llblab/pi-actors/dist/lib/file-state.js +310 -0
  49. package/node_modules/@llblab/pi-actors/dist/lib/identity.d.ts +9 -0
  50. package/node_modules/@llblab/pi-actors/dist/lib/identity.js +27 -0
  51. package/node_modules/@llblab/pi-actors/dist/lib/inspector-actions.d.ts +16 -0
  52. package/node_modules/@llblab/pi-actors/dist/lib/inspector-actions.js +57 -0
  53. package/node_modules/@llblab/pi-actors/dist/lib/inspector-command.d.ts +7 -0
  54. package/node_modules/@llblab/pi-actors/dist/lib/inspector-command.js +37 -0
  55. package/node_modules/@llblab/pi-actors/dist/lib/inspector-overlay.d.ts +100 -0
  56. package/node_modules/@llblab/pi-actors/dist/lib/inspector-overlay.js +824 -0
  57. package/node_modules/@llblab/pi-actors/dist/lib/inspector.d.ts +20 -0
  58. package/node_modules/@llblab/pi-actors/dist/lib/inspector.js +136 -0
  59. package/node_modules/@llblab/pi-actors/dist/lib/limits.d.ts +28 -0
  60. package/node_modules/@llblab/pi-actors/dist/lib/limits.js +28 -0
  61. package/node_modules/@llblab/pi-actors/dist/lib/model-context.d.ts +56 -0
  62. package/node_modules/@llblab/pi-actors/dist/lib/model-context.js +220 -0
  63. package/node_modules/@llblab/pi-actors/dist/lib/observability.d.ts +186 -0
  64. package/node_modules/@llblab/pi-actors/dist/lib/observability.js +951 -0
  65. package/node_modules/@llblab/pi-actors/dist/lib/paths.d.ts +27 -0
  66. package/node_modules/@llblab/pi-actors/dist/lib/paths.js +72 -0
  67. package/node_modules/@llblab/pi-actors/dist/lib/pi.d.ts +19 -0
  68. package/node_modules/@llblab/pi-actors/dist/lib/pi.js +21 -0
  69. package/node_modules/@llblab/pi-actors/dist/lib/preflight-diagnostics.d.ts +27 -0
  70. package/node_modules/@llblab/pi-actors/dist/lib/preflight-diagnostics.js +86 -0
  71. package/node_modules/@llblab/pi-actors/dist/lib/prompts.d.ts +24 -0
  72. package/node_modules/@llblab/pi-actors/dist/lib/prompts.js +49 -0
  73. package/node_modules/@llblab/pi-actors/dist/lib/recipe-control.d.ts +7 -0
  74. package/node_modules/@llblab/pi-actors/dist/lib/recipe-control.js +43 -0
  75. package/node_modules/@llblab/pi-actors/dist/lib/recipes-context.d.ts +36 -0
  76. package/node_modules/@llblab/pi-actors/dist/lib/recipes-context.js +197 -0
  77. package/node_modules/@llblab/pi-actors/dist/lib/recipes-discovery.d.ts +77 -0
  78. package/node_modules/@llblab/pi-actors/dist/lib/recipes-discovery.js +783 -0
  79. package/node_modules/@llblab/pi-actors/dist/lib/recipes-references.d.ts +113 -0
  80. package/node_modules/@llblab/pi-actors/dist/lib/recipes-references.js +1108 -0
  81. package/node_modules/@llblab/pi-actors/dist/lib/recipes-usage.d.ts +21 -0
  82. package/node_modules/@llblab/pi-actors/dist/lib/recipes-usage.js +494 -0
  83. package/node_modules/@llblab/pi-actors/dist/lib/registry.d.ts +67 -0
  84. package/node_modules/@llblab/pi-actors/dist/lib/registry.js +504 -0
  85. package/node_modules/@llblab/pi-actors/dist/lib/review-control.d.ts +14 -0
  86. package/node_modules/@llblab/pi-actors/dist/lib/review-control.js +110 -0
  87. package/node_modules/@llblab/pi-actors/dist/lib/review-diagnostics.d.ts +11 -0
  88. package/node_modules/@llblab/pi-actors/dist/lib/review-diagnostics.js +148 -0
  89. package/node_modules/@llblab/pi-actors/dist/lib/review-projection.d.ts +14 -0
  90. package/node_modules/@llblab/pi-actors/dist/lib/review-projection.js +166 -0
  91. package/node_modules/@llblab/pi-actors/dist/lib/run-evidence-policy.d.ts +95 -0
  92. package/node_modules/@llblab/pi-actors/dist/lib/run-evidence-policy.js +177 -0
  93. package/node_modules/@llblab/pi-actors/dist/lib/run-ui-runtime.d.ts +18 -0
  94. package/node_modules/@llblab/pi-actors/dist/lib/run-ui-runtime.js +125 -0
  95. package/node_modules/@llblab/pi-actors/dist/lib/runs-artifacts.d.ts +21 -0
  96. package/node_modules/@llblab/pi-actors/dist/lib/runs-artifacts.js +68 -0
  97. package/node_modules/@llblab/pi-actors/dist/lib/runs-control-delivery.d.ts +28 -0
  98. package/node_modules/@llblab/pi-actors/dist/lib/runs-control-delivery.js +150 -0
  99. package/node_modules/@llblab/pi-actors/dist/lib/runs-control.d.ts +21 -0
  100. package/node_modules/@llblab/pi-actors/dist/lib/runs-control.js +89 -0
  101. package/node_modules/@llblab/pi-actors/dist/lib/runs-controls.d.ts +39 -0
  102. package/node_modules/@llblab/pi-actors/dist/lib/runs-controls.js +283 -0
  103. package/node_modules/@llblab/pi-actors/dist/lib/runs-identity.d.ts +5 -0
  104. package/node_modules/@llblab/pi-actors/dist/lib/runs-identity.js +11 -0
  105. package/node_modules/@llblab/pi-actors/dist/lib/runs-index.d.ts +27 -0
  106. package/node_modules/@llblab/pi-actors/dist/lib/runs-index.js +142 -0
  107. package/node_modules/@llblab/pi-actors/dist/lib/runs-ownership.d.ts +7 -0
  108. package/node_modules/@llblab/pi-actors/dist/lib/runs-ownership.js +96 -0
  109. package/node_modules/@llblab/pi-actors/dist/lib/runs-parent-teardown.d.ts +51 -0
  110. package/node_modules/@llblab/pi-actors/dist/lib/runs-parent-teardown.js +172 -0
  111. package/node_modules/@llblab/pi-actors/dist/lib/runs-process.d.ts +22 -0
  112. package/node_modules/@llblab/pi-actors/dist/lib/runs-process.js +125 -0
  113. package/node_modules/@llblab/pi-actors/dist/lib/runs-retention.d.ts +18 -0
  114. package/node_modules/@llblab/pi-actors/dist/lib/runs-retention.js +108 -0
  115. package/node_modules/@llblab/pi-actors/dist/lib/runs-start.d.ts +12 -0
  116. package/node_modules/@llblab/pi-actors/dist/lib/runs-start.js +68 -0
  117. package/node_modules/@llblab/pi-actors/dist/lib/runs-status.d.ts +10 -0
  118. package/node_modules/@llblab/pi-actors/dist/lib/runs-status.js +68 -0
  119. package/node_modules/@llblab/pi-actors/dist/lib/runs-trace.d.ts +48 -0
  120. package/node_modules/@llblab/pi-actors/dist/lib/runs-trace.js +491 -0
  121. package/node_modules/@llblab/pi-actors/dist/lib/runtime-identity.d.ts +7 -0
  122. package/node_modules/@llblab/pi-actors/dist/lib/runtime-identity.js +35 -0
  123. package/node_modules/@llblab/pi-actors/dist/lib/runtime-triage.d.ts +29 -0
  124. package/node_modules/@llblab/pi-actors/dist/lib/runtime-triage.js +60 -0
  125. package/node_modules/@llblab/pi-actors/dist/lib/runtime.d.ts +69 -0
  126. package/node_modules/@llblab/pi-actors/dist/lib/runtime.js +339 -0
  127. package/node_modules/@llblab/pi-actors/dist/lib/schema.d.ts +60 -0
  128. package/node_modules/@llblab/pi-actors/dist/lib/schema.js +428 -0
  129. package/node_modules/@llblab/pi-actors/dist/lib/session-evidence.d.ts +45 -0
  130. package/node_modules/@llblab/pi-actors/dist/lib/session-evidence.js +204 -0
  131. package/node_modules/@llblab/pi-actors/dist/lib/state-readers.d.ts +21 -0
  132. package/node_modules/@llblab/pi-actors/dist/lib/state-readers.js +74 -0
  133. package/node_modules/@llblab/pi-actors/dist/lib/temp.d.ts +10 -0
  134. package/node_modules/@llblab/pi-actors/dist/lib/temp.js +90 -0
  135. package/node_modules/@llblab/pi-actors/dist/lib/tool-review-lineage-transaction.d.ts +27 -0
  136. package/node_modules/@llblab/pi-actors/dist/lib/tool-review-lineage-transaction.js +597 -0
  137. package/node_modules/@llblab/pi-actors/dist/lib/tool-review-lineage.d.ts +24 -0
  138. package/node_modules/@llblab/pi-actors/dist/lib/tool-review-lineage.js +98 -0
  139. package/node_modules/@llblab/pi-actors/dist/lib/tool-review-scheduler.d.ts +80 -0
  140. package/node_modules/@llblab/pi-actors/dist/lib/tool-review-scheduler.js +494 -0
  141. package/node_modules/@llblab/pi-actors/dist/lib/tool-review-transaction.d.ts +50 -0
  142. package/node_modules/@llblab/pi-actors/dist/lib/tool-review-transaction.js +362 -0
  143. package/node_modules/@llblab/pi-actors/dist/lib/tool-review.d.ts +56 -0
  144. package/node_modules/@llblab/pi-actors/dist/lib/tool-review.js +197 -0
  145. package/node_modules/@llblab/pi-actors/dist/lib/tools-access.d.ts +19 -0
  146. package/node_modules/@llblab/pi-actors/dist/lib/tools-access.js +44 -0
  147. package/node_modules/@llblab/pi-actors/dist/lib/tools-inspect.d.ts +15 -0
  148. package/node_modules/@llblab/pi-actors/dist/lib/tools-inspect.js +532 -0
  149. package/node_modules/@llblab/pi-actors/dist/lib/tools-local.d.ts +17 -0
  150. package/node_modules/@llblab/pi-actors/dist/lib/tools-local.js +191 -0
  151. package/node_modules/@llblab/pi-actors/dist/lib/tools-message.d.ts +9 -0
  152. package/node_modules/@llblab/pi-actors/dist/lib/tools-message.js +139 -0
  153. package/node_modules/@llblab/pi-actors/dist/lib/tools-register.d.ts +18 -0
  154. package/node_modules/@llblab/pi-actors/dist/lib/tools-register.js +41 -0
  155. package/node_modules/@llblab/pi-actors/dist/lib/tools-response.d.ts +20 -0
  156. package/node_modules/@llblab/pi-actors/dist/lib/tools-response.js +243 -0
  157. package/node_modules/@llblab/pi-actors/dist/lib/tools-spawn.d.ts +15 -0
  158. package/node_modules/@llblab/pi-actors/dist/lib/tools-spawn.js +133 -0
  159. package/node_modules/@llblab/pi-actors/dist/lib/tools.d.ts +28 -0
  160. package/node_modules/@llblab/pi-actors/dist/lib/tools.js +49 -0
  161. package/node_modules/@llblab/pi-actors/dist/lib/trace-projection.d.ts +22 -0
  162. package/node_modules/@llblab/pi-actors/dist/lib/trace-projection.js +231 -0
  163. package/node_modules/@llblab/pi-actors/dist/pi-actors/index.js +1 -0
  164. package/node_modules/@llblab/pi-actors/dist/scripts/async-runner.mjs +555 -0
  165. package/node_modules/@llblab/pi-actors/dist/scripts/build-dist.mjs +60 -0
  166. package/node_modules/@llblab/pi-actors/dist/scripts/conformance.mjs +63 -0
  167. package/node_modules/@llblab/pi-actors/dist/skills/actors/SKILL.md +126 -0
  168. package/node_modules/@llblab/pi-actors/dist/skills/actors/recipes/command-validate.json +13 -0
  169. package/node_modules/@llblab/pi-actors/dist/skills/actors/recipes/jsonl-tail.json +10 -0
  170. package/node_modules/@llblab/pi-actors/dist/skills/actors/recipes/recipe-validate.json +11 -0
  171. package/node_modules/@llblab/pi-actors/dist/skills/actors/recipes/resource-locker-snapshot.json +11 -0
  172. package/node_modules/@llblab/pi-actors/dist/skills/actors/recipes/resource-locker.json +25 -0
  173. package/node_modules/@llblab/pi-actors/dist/skills/actors/recipes/run-ops-snapshot.json +15 -0
  174. package/node_modules/@llblab/pi-actors/dist/skills/actors/recipes/run-state-files.json +13 -0
  175. package/node_modules/@llblab/pi-actors/dist/skills/actors/recipes/run-summary.json +9 -0
  176. package/node_modules/@llblab/pi-actors/dist/skills/actors/references/diagnostics.md +44 -0
  177. package/node_modules/@llblab/pi-actors/dist/skills/actors/references/persistent-tools.md +76 -0
  178. package/node_modules/@llblab/pi-actors/dist/skills/actors/references/recipes.md +51 -0
  179. package/node_modules/@llblab/pi-actors/dist/skills/actors/references/runs.md +41 -0
  180. package/node_modules/@llblab/pi-actors/dist/skills/actors/scripts/resource-locker.mjs +425 -0
  181. package/node_modules/@llblab/pi-actors/dist/skills/actors/scripts/run-utils.mjs +164 -0
  182. package/node_modules/@llblab/pi-actors/dist/skills/actors/scripts/validate-recipe.mjs +370 -0
  183. package/node_modules/@llblab/pi-actors/dist/skills/artifacts/SKILL.md +33 -0
  184. package/node_modules/@llblab/pi-actors/dist/skills/artifacts/recipes/bundle.json +77 -0
  185. package/node_modules/@llblab/pi-actors/dist/skills/artifacts/recipes/file-write.json +14 -0
  186. package/node_modules/@llblab/pi-actors/dist/skills/artifacts/recipes/manifest.json +14 -0
  187. package/node_modules/@llblab/pi-actors/dist/skills/artifacts/recipes/report.json +39 -0
  188. package/node_modules/@llblab/pi-actors/dist/skills/artifacts/recipes/write.json +53 -0
  189. package/node_modules/@llblab/pi-actors/dist/skills/artifacts/scripts/artifact-utils.mjs +63 -0
  190. package/node_modules/@llblab/pi-actors/dist/skills/music-player/SKILL.md +57 -0
  191. package/node_modules/@llblab/pi-actors/dist/skills/music-player/genapps/music-player.mjs +360 -0
  192. package/node_modules/@llblab/pi-actors/dist/skills/music-player/recipes/playback.json +29 -0
  193. package/node_modules/@llblab/pi-actors/dist/skills/music-player/scripts/playback-client.mjs +143 -0
  194. package/node_modules/@llblab/pi-actors/dist/skills/music-player/scripts/playback.mjs +1446 -0
  195. package/node_modules/@llblab/pi-actors/dist/skills/project-work/SKILL.md +37 -0
  196. package/node_modules/@llblab/pi-actors/dist/skills/project-work/recipes/changelog-head.json +11 -0
  197. package/node_modules/@llblab/pi-actors/dist/skills/project-work/recipes/changelog-section.json +11 -0
  198. package/node_modules/@llblab/pi-actors/dist/skills/project-work/recipes/docs-maintenance.json +66 -0
  199. package/node_modules/@llblab/pi-actors/dist/skills/project-work/recipes/git-log.json +11 -0
  200. package/node_modules/@llblab/pi-actors/dist/skills/project-work/recipes/git-status.json +9 -0
  201. package/node_modules/@llblab/pi-actors/dist/skills/project-work/recipes/markdown-index.json +14 -0
  202. package/node_modules/@llblab/pi-actors/dist/skills/project-work/recipes/package-summary.json +9 -0
  203. package/node_modules/@llblab/pi-actors/dist/skills/project-work/recipes/release-readiness.json +115 -0
  204. package/node_modules/@llblab/pi-actors/dist/skills/project-work/recipes/release-summary.json +74 -0
  205. package/node_modules/@llblab/pi-actors/dist/skills/project-work/recipes/repo-health.json +75 -0
  206. package/node_modules/@llblab/pi-actors/dist/skills/project-work/recipes/run-ops.json +56 -0
  207. package/node_modules/@llblab/pi-actors/dist/skills/project-work/recipes/skill-summary.json +11 -0
  208. package/node_modules/@llblab/pi-actors/dist/skills/project-work/scripts/project-utils.mjs +89 -0
  209. package/node_modules/@llblab/pi-actors/dist/skills/recipe-memory/SKILL.md +36 -0
  210. package/node_modules/@llblab/pi-actors/dist/skills/recipe-memory/recipes/draft-review.json +14 -0
  211. package/node_modules/@llblab/pi-actors/dist/skills/recipe-memory/recipes/tool-review.json +14 -0
  212. package/node_modules/@llblab/pi-actors/dist/skills/swarm/SKILL.md +84 -0
  213. package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/architect.json +82 -0
  214. package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/checkpoint-continuation.json +51 -0
  215. package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/development-tasking.json +67 -0
  216. package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/lens-review.json +63 -0
  217. package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/quorum-review.json +65 -0
  218. package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/research-synthesis.json +81 -0
  219. package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/review-readiness.json +59 -0
  220. package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-artifact.json +21 -0
  221. package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-checkpoint.json +21 -0
  222. package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-conflict-report.json +19 -0
  223. package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-contradiction-map.json +20 -0
  224. package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-critic.json +22 -0
  225. package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-evidence-map.json +20 -0
  226. package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-followup.json +21 -0
  227. package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-judge.json +22 -0
  228. package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-merge.json +24 -0
  229. package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-normalize.json +20 -0
  230. package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-plan.json +20 -0
  231. package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-preflight.json +18 -0
  232. package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-prompt.json +16 -0
  233. package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-prompts.json +39 -0
  234. package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-quorum.json +31 -0
  235. package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-review-coordinator.json +170 -0
  236. package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-review.json +26 -0
  237. package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-task-card.json +22 -0
  238. package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-tools.json +15 -0
  239. package/node_modules/@llblab/pi-actors/dist/skills/swarm/recipes/subagent-verify.json +23 -0
  240. package/node_modules/@llblab/pi-actors/dist/skills/swarm/references/development-swarm.md +189 -0
  241. package/node_modules/@llblab/pi-actors/dist/skills/swarm/references/review-swarms.md +115 -0
  242. package/node_modules/@llblab/pi-actors/docs/README.md +20 -0
  243. package/node_modules/@llblab/pi-actors/docs/actor-inspector.md +50 -0
  244. package/node_modules/@llblab/pi-actors/docs/async-runs.md +141 -0
  245. package/node_modules/@llblab/pi-actors/docs/command-templates.md +321 -0
  246. package/node_modules/@llblab/pi-actors/docs/recipe-library.md +89 -0
  247. package/node_modules/@llblab/pi-actors/docs/releasing.md +28 -0
  248. package/node_modules/@llblab/pi-actors/docs/template-recipes.md +175 -0
  249. package/node_modules/@llblab/pi-actors/docs/tool-registry.md +90 -0
  250. package/node_modules/@llblab/pi-actors/fixtures/protocol/artifact-manifest.json +9 -0
  251. package/node_modules/@llblab/pi-actors/fixtures/protocol/control-endpoint.json +6 -0
  252. package/node_modules/@llblab/pi-actors/fixtures/protocol/control-record.json +9 -0
  253. package/node_modules/@llblab/pi-actors/fixtures/protocol/recipe-summary.json +8 -0
  254. package/node_modules/@llblab/pi-actors/fixtures/protocol/run-state.json +10 -0
  255. package/node_modules/@llblab/pi-actors/fixtures/protocol/trace-event.json +9 -0
  256. package/node_modules/@llblab/pi-actors/index.ts +30 -0
  257. package/node_modules/@llblab/pi-actors/lib/async-runs.ts +1245 -0
  258. package/node_modules/@llblab/pi-actors/lib/automatic-review-runtime.ts +148 -0
  259. package/node_modules/@llblab/pi-actors/lib/command-templates.ts +1116 -0
  260. package/node_modules/@llblab/pi-actors/lib/config.ts +261 -0
  261. package/node_modules/@llblab/pi-actors/lib/control-projection.ts +105 -0
  262. package/node_modules/@llblab/pi-actors/lib/control.ts +117 -0
  263. package/node_modules/@llblab/pi-actors/lib/draft-consolidation-transaction.ts +821 -0
  264. package/node_modules/@llblab/pi-actors/lib/draft-consolidation.ts +181 -0
  265. package/node_modules/@llblab/pi-actors/lib/draft-review.ts +325 -0
  266. package/node_modules/@llblab/pi-actors/lib/draft-sleep.ts +575 -0
  267. package/node_modules/@llblab/pi-actors/lib/execution-output.ts +123 -0
  268. package/node_modules/@llblab/pi-actors/lib/execution-sessions.ts +111 -0
  269. package/node_modules/@llblab/pi-actors/lib/execution.ts +1153 -0
  270. package/node_modules/@llblab/pi-actors/lib/extension-runtime.ts +181 -0
  271. package/node_modules/@llblab/pi-actors/lib/file-state.ts +306 -0
  272. package/node_modules/@llblab/pi-actors/lib/identity.ts +29 -0
  273. package/node_modules/@llblab/pi-actors/lib/inspector-actions.ts +79 -0
  274. package/node_modules/@llblab/pi-actors/lib/inspector-command.ts +54 -0
  275. package/node_modules/@llblab/pi-actors/lib/inspector-overlay.ts +901 -0
  276. package/node_modules/@llblab/pi-actors/lib/inspector.ts +204 -0
  277. package/node_modules/@llblab/pi-actors/lib/limits.ts +29 -0
  278. package/node_modules/@llblab/pi-actors/lib/model-context.ts +359 -0
  279. package/node_modules/@llblab/pi-actors/lib/observability.ts +1337 -0
  280. package/node_modules/@llblab/pi-actors/lib/paths.ts +111 -0
  281. package/node_modules/@llblab/pi-actors/lib/pi.ts +47 -0
  282. package/node_modules/@llblab/pi-actors/lib/preflight-diagnostics.ts +132 -0
  283. package/node_modules/@llblab/pi-actors/lib/prompts.ts +69 -0
  284. package/node_modules/@llblab/pi-actors/lib/recipe-control.ts +52 -0
  285. package/node_modules/@llblab/pi-actors/lib/recipes-context.ts +268 -0
  286. package/node_modules/@llblab/pi-actors/lib/recipes-discovery.ts +1060 -0
  287. package/node_modules/@llblab/pi-actors/lib/recipes-references.ts +1598 -0
  288. package/node_modules/@llblab/pi-actors/lib/recipes-usage.ts +638 -0
  289. package/node_modules/@llblab/pi-actors/lib/registry.ts +806 -0
  290. package/node_modules/@llblab/pi-actors/lib/review-control.ts +136 -0
  291. package/node_modules/@llblab/pi-actors/lib/review-diagnostics.ts +164 -0
  292. package/node_modules/@llblab/pi-actors/lib/review-projection.ts +196 -0
  293. package/node_modules/@llblab/pi-actors/lib/run-evidence-policy.ts +242 -0
  294. package/node_modules/@llblab/pi-actors/lib/run-ui-runtime.ts +155 -0
  295. package/node_modules/@llblab/pi-actors/lib/runs-artifacts.ts +96 -0
  296. package/node_modules/@llblab/pi-actors/lib/runs-control-delivery.ts +209 -0
  297. package/node_modules/@llblab/pi-actors/lib/runs-control.ts +148 -0
  298. package/node_modules/@llblab/pi-actors/lib/runs-controls.ts +282 -0
  299. package/node_modules/@llblab/pi-actors/lib/runs-identity.ts +14 -0
  300. package/node_modules/@llblab/pi-actors/lib/runs-index.ts +192 -0
  301. package/node_modules/@llblab/pi-actors/lib/runs-ownership.ts +136 -0
  302. package/node_modules/@llblab/pi-actors/lib/runs-parent-teardown.ts +257 -0
  303. package/node_modules/@llblab/pi-actors/lib/runs-process.ts +174 -0
  304. package/node_modules/@llblab/pi-actors/lib/runs-retention.ts +140 -0
  305. package/node_modules/@llblab/pi-actors/lib/runs-start.ts +113 -0
  306. package/node_modules/@llblab/pi-actors/lib/runs-status.ts +101 -0
  307. package/node_modules/@llblab/pi-actors/lib/runs-trace.ts +611 -0
  308. package/node_modules/@llblab/pi-actors/lib/runtime-identity.ts +39 -0
  309. package/node_modules/@llblab/pi-actors/lib/runtime-triage.ts +106 -0
  310. package/node_modules/@llblab/pi-actors/lib/runtime.ts +430 -0
  311. package/node_modules/@llblab/pi-actors/lib/schema.ts +525 -0
  312. package/node_modules/@llblab/pi-actors/lib/session-evidence.ts +302 -0
  313. package/node_modules/@llblab/pi-actors/lib/state-readers.ts +93 -0
  314. package/node_modules/@llblab/pi-actors/lib/temp.ts +99 -0
  315. package/node_modules/@llblab/pi-actors/lib/tool-review-lineage-transaction.ts +881 -0
  316. package/node_modules/@llblab/pi-actors/lib/tool-review-lineage.ts +145 -0
  317. package/node_modules/@llblab/pi-actors/lib/tool-review-scheduler.ts +635 -0
  318. package/node_modules/@llblab/pi-actors/lib/tool-review-transaction.ts +563 -0
  319. package/node_modules/@llblab/pi-actors/lib/tool-review.ts +270 -0
  320. package/node_modules/@llblab/pi-actors/lib/tools-access.ts +68 -0
  321. package/node_modules/@llblab/pi-actors/lib/tools-inspect.ts +673 -0
  322. package/node_modules/@llblab/pi-actors/lib/tools-local.ts +303 -0
  323. package/node_modules/@llblab/pi-actors/lib/tools-message.ts +176 -0
  324. package/node_modules/@llblab/pi-actors/lib/tools-register.ts +63 -0
  325. package/node_modules/@llblab/pi-actors/lib/tools-response.ts +285 -0
  326. package/node_modules/@llblab/pi-actors/lib/tools-spawn.ts +212 -0
  327. package/node_modules/@llblab/pi-actors/lib/tools.ts +88 -0
  328. package/node_modules/@llblab/pi-actors/lib/trace-projection.ts +306 -0
  329. package/node_modules/@llblab/pi-actors/package.json +76 -0
  330. package/node_modules/@llblab/pi-actors/scripts/async-runner.mjs +555 -0
  331. package/node_modules/@llblab/pi-actors/scripts/build-dist.mjs +60 -0
  332. package/node_modules/@llblab/pi-actors/scripts/conformance.mjs +63 -0
  333. package/node_modules/@llblab/pi-actors/skills/actors/SKILL.md +126 -0
  334. package/node_modules/@llblab/pi-actors/skills/actors/recipes/command-validate.json +13 -0
  335. package/node_modules/@llblab/pi-actors/skills/actors/recipes/jsonl-tail.json +10 -0
  336. package/node_modules/@llblab/pi-actors/skills/actors/recipes/recipe-validate.json +11 -0
  337. package/node_modules/@llblab/pi-actors/skills/actors/recipes/resource-locker-snapshot.json +11 -0
  338. package/node_modules/@llblab/pi-actors/skills/actors/recipes/resource-locker.json +25 -0
  339. package/node_modules/@llblab/pi-actors/skills/actors/recipes/run-ops-snapshot.json +15 -0
  340. package/node_modules/@llblab/pi-actors/skills/actors/recipes/run-state-files.json +13 -0
  341. package/node_modules/@llblab/pi-actors/skills/actors/recipes/run-summary.json +9 -0
  342. package/node_modules/@llblab/pi-actors/skills/actors/references/diagnostics.md +44 -0
  343. package/node_modules/@llblab/pi-actors/skills/actors/references/persistent-tools.md +76 -0
  344. package/node_modules/@llblab/pi-actors/skills/actors/references/recipes.md +51 -0
  345. package/node_modules/@llblab/pi-actors/skills/actors/references/runs.md +41 -0
  346. package/node_modules/@llblab/pi-actors/skills/actors/scripts/resource-locker.mjs +425 -0
  347. package/node_modules/@llblab/pi-actors/skills/actors/scripts/run-utils.mjs +164 -0
  348. package/node_modules/@llblab/pi-actors/skills/actors/scripts/validate-recipe.mjs +370 -0
  349. package/node_modules/@llblab/pi-actors/skills/artifacts/SKILL.md +33 -0
  350. package/node_modules/@llblab/pi-actors/skills/artifacts/recipes/bundle.json +77 -0
  351. package/node_modules/@llblab/pi-actors/skills/artifacts/recipes/file-write.json +14 -0
  352. package/node_modules/@llblab/pi-actors/skills/artifacts/recipes/manifest.json +14 -0
  353. package/node_modules/@llblab/pi-actors/skills/artifacts/recipes/report.json +39 -0
  354. package/node_modules/@llblab/pi-actors/skills/artifacts/recipes/write.json +53 -0
  355. package/node_modules/@llblab/pi-actors/skills/artifacts/scripts/artifact-utils.mjs +63 -0
  356. package/node_modules/@llblab/pi-actors/skills/music-player/SKILL.md +57 -0
  357. package/node_modules/@llblab/pi-actors/skills/music-player/genapps/music-player.mjs +360 -0
  358. package/node_modules/@llblab/pi-actors/skills/music-player/recipes/playback.json +29 -0
  359. package/node_modules/@llblab/pi-actors/skills/music-player/scripts/playback-client.mjs +143 -0
  360. package/node_modules/@llblab/pi-actors/skills/music-player/scripts/playback.mjs +1446 -0
  361. package/node_modules/@llblab/pi-actors/skills/project-work/SKILL.md +37 -0
  362. package/node_modules/@llblab/pi-actors/skills/project-work/recipes/changelog-head.json +11 -0
  363. package/node_modules/@llblab/pi-actors/skills/project-work/recipes/changelog-section.json +11 -0
  364. package/node_modules/@llblab/pi-actors/skills/project-work/recipes/docs-maintenance.json +66 -0
  365. package/node_modules/@llblab/pi-actors/skills/project-work/recipes/git-log.json +11 -0
  366. package/node_modules/@llblab/pi-actors/skills/project-work/recipes/git-status.json +9 -0
  367. package/node_modules/@llblab/pi-actors/skills/project-work/recipes/markdown-index.json +14 -0
  368. package/node_modules/@llblab/pi-actors/skills/project-work/recipes/package-summary.json +9 -0
  369. package/node_modules/@llblab/pi-actors/skills/project-work/recipes/release-readiness.json +115 -0
  370. package/node_modules/@llblab/pi-actors/skills/project-work/recipes/release-summary.json +74 -0
  371. package/node_modules/@llblab/pi-actors/skills/project-work/recipes/repo-health.json +75 -0
  372. package/node_modules/@llblab/pi-actors/skills/project-work/recipes/run-ops.json +56 -0
  373. package/node_modules/@llblab/pi-actors/skills/project-work/recipes/skill-summary.json +11 -0
  374. package/node_modules/@llblab/pi-actors/skills/project-work/scripts/project-utils.mjs +89 -0
  375. package/node_modules/@llblab/pi-actors/skills/recipe-memory/SKILL.md +36 -0
  376. package/node_modules/@llblab/pi-actors/skills/recipe-memory/recipes/draft-review.json +14 -0
  377. package/node_modules/@llblab/pi-actors/skills/recipe-memory/recipes/tool-review.json +14 -0
  378. package/node_modules/@llblab/pi-actors/skills/swarm/SKILL.md +84 -0
  379. package/node_modules/@llblab/pi-actors/skills/swarm/recipes/architect.json +82 -0
  380. package/node_modules/@llblab/pi-actors/skills/swarm/recipes/checkpoint-continuation.json +51 -0
  381. package/node_modules/@llblab/pi-actors/skills/swarm/recipes/development-tasking.json +67 -0
  382. package/node_modules/@llblab/pi-actors/skills/swarm/recipes/lens-review.json +63 -0
  383. package/node_modules/@llblab/pi-actors/skills/swarm/recipes/quorum-review.json +65 -0
  384. package/node_modules/@llblab/pi-actors/skills/swarm/recipes/research-synthesis.json +81 -0
  385. package/node_modules/@llblab/pi-actors/skills/swarm/recipes/review-readiness.json +59 -0
  386. package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-artifact.json +21 -0
  387. package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-checkpoint.json +21 -0
  388. package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-conflict-report.json +19 -0
  389. package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-contradiction-map.json +20 -0
  390. package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-critic.json +22 -0
  391. package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-evidence-map.json +20 -0
  392. package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-followup.json +21 -0
  393. package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-judge.json +22 -0
  394. package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-merge.json +24 -0
  395. package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-normalize.json +20 -0
  396. package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-plan.json +20 -0
  397. package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-preflight.json +18 -0
  398. package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-prompt.json +16 -0
  399. package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-prompts.json +39 -0
  400. package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-quorum.json +31 -0
  401. package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-review-coordinator.json +170 -0
  402. package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-review.json +26 -0
  403. package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-task-card.json +22 -0
  404. package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-tools.json +15 -0
  405. package/node_modules/@llblab/pi-actors/skills/swarm/recipes/subagent-verify.json +23 -0
  406. package/node_modules/@llblab/pi-actors/skills/swarm/references/development-swarm.md +189 -0
  407. package/node_modules/@llblab/pi-actors/skills/swarm/references/review-swarms.md +115 -0
  408. package/node_modules/@llblab/pi-codex-usage/AGENTS.md +17 -0
  409. package/node_modules/@llblab/pi-codex-usage/BACKLOG.md +8 -0
  410. package/node_modules/@llblab/pi-codex-usage/CHANGELOG.md +80 -0
  411. package/node_modules/@llblab/pi-codex-usage/LICENSE +22 -0
  412. package/node_modules/@llblab/pi-codex-usage/README.md +113 -0
  413. package/node_modules/@llblab/pi-codex-usage/banner.jpg +0 -0
  414. package/node_modules/@llblab/pi-codex-usage/index.ts +1525 -0
  415. package/node_modules/@llblab/pi-codex-usage/package.json +59 -0
  416. package/node_modules/@llblab/pi-grow-loop/AGENTS.md +56 -0
  417. package/node_modules/@llblab/pi-grow-loop/BACKLOG.md +3 -0
  418. package/node_modules/@llblab/pi-grow-loop/CHANGELOG.md +126 -0
  419. package/node_modules/@llblab/pi-grow-loop/README.md +216 -0
  420. package/node_modules/@llblab/pi-grow-loop/banner.jpg +0 -0
  421. package/node_modules/@llblab/pi-grow-loop/index.ts +219 -0
  422. package/node_modules/@llblab/pi-grow-loop/package.json +64 -0
  423. package/node_modules/@llblab/pi-grow-loop/skills/grow-loop/SKILL.md +131 -0
  424. package/node_modules/@llblab/pi-grow-loop/skills/while-true/SKILL.md +233 -0
  425. package/node_modules/@llblab/pi-telegram/AGENTS.md +157 -0
  426. package/node_modules/@llblab/pi-telegram/BACKLOG.md +15 -0
  427. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +886 -0
  428. package/node_modules/@llblab/pi-telegram/README.md +330 -0
  429. package/node_modules/@llblab/pi-telegram/api/activity.ts +15 -0
  430. package/node_modules/@llblab/pi-telegram/api/commands.ts +11 -0
  431. package/node_modules/@llblab/pi-telegram/api/delivery.ts +21 -0
  432. package/node_modules/@llblab/pi-telegram/api/inbound.ts +14 -0
  433. package/node_modules/@llblab/pi-telegram/api/keyboard.ts +11 -0
  434. package/node_modules/@llblab/pi-telegram/api/outbound.ts +11 -0
  435. package/node_modules/@llblab/pi-telegram/api/sections.ts +17 -0
  436. package/node_modules/@llblab/pi-telegram/api/status.ts +12 -0
  437. package/node_modules/@llblab/pi-telegram/api/updates.ts +16 -0
  438. package/node_modules/@llblab/pi-telegram/api/voice.ts +24 -0
  439. package/node_modules/@llblab/pi-telegram/docs/README.md +25 -0
  440. package/node_modules/@llblab/pi-telegram/docs/activity.md +304 -0
  441. package/node_modules/@llblab/pi-telegram/docs/architecture.md +418 -0
  442. package/node_modules/@llblab/pi-telegram/docs/callback-namespaces.md +48 -0
  443. package/node_modules/@llblab/pi-telegram/docs/command-templates.md +365 -0
  444. package/node_modules/@llblab/pi-telegram/docs/compact-matrix-literal.md +176 -0
  445. package/node_modules/@llblab/pi-telegram/docs/delivery.md +225 -0
  446. package/node_modules/@llblab/pi-telegram/docs/generative-apps.md +310 -0
  447. package/node_modules/@llblab/pi-telegram/docs/inbound.md +136 -0
  448. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +467 -0
  449. package/node_modules/@llblab/pi-telegram/docs/outbound.md +193 -0
  450. package/node_modules/@llblab/pi-telegram/docs/public-api.md +599 -0
  451. package/node_modules/@llblab/pi-telegram/docs/sections.md +452 -0
  452. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +256 -0
  453. package/node_modules/@llblab/pi-telegram/docs/updates.md +171 -0
  454. package/node_modules/@llblab/pi-telegram/docs/voice.md +217 -0
  455. package/node_modules/@llblab/pi-telegram/index.ts +1336 -0
  456. package/node_modules/@llblab/pi-telegram/lib/activity-verbosity.ts +714 -0
  457. package/node_modules/@llblab/pi-telegram/lib/activity.ts +782 -0
  458. package/node_modules/@llblab/pi-telegram/lib/agent-messages.ts +121 -0
  459. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +1129 -0
  460. package/node_modules/@llblab/pi-telegram/lib/bus-api.ts +379 -0
  461. package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +1941 -0
  462. package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +2119 -0
  463. package/node_modules/@llblab/pi-telegram/lib/bus-transport.ts +244 -0
  464. package/node_modules/@llblab/pi-telegram/lib/bus.ts +2645 -0
  465. package/node_modules/@llblab/pi-telegram/lib/command-templates.ts +954 -0
  466. package/node_modules/@llblab/pi-telegram/lib/commands.ts +1854 -0
  467. package/node_modules/@llblab/pi-telegram/lib/config.ts +1143 -0
  468. package/node_modules/@llblab/pi-telegram/lib/delivery.ts +891 -0
  469. package/node_modules/@llblab/pi-telegram/lib/generative-app-worker.mjs +104 -0
  470. package/node_modules/@llblab/pi-telegram/lib/generative-apps.ts +958 -0
  471. package/node_modules/@llblab/pi-telegram/lib/inbound.ts +824 -0
  472. package/node_modules/@llblab/pi-telegram/lib/journal.ts +3140 -0
  473. package/node_modules/@llblab/pi-telegram/lib/keyboard.ts +59 -0
  474. package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +623 -0
  475. package/node_modules/@llblab/pi-telegram/lib/locks.ts +1422 -0
  476. package/node_modules/@llblab/pi-telegram/lib/logs.ts +393 -0
  477. package/node_modules/@llblab/pi-telegram/lib/media.ts +921 -0
  478. package/node_modules/@llblab/pi-telegram/lib/menu-model.ts +1182 -0
  479. package/node_modules/@llblab/pi-telegram/lib/menu-queue.ts +926 -0
  480. package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +876 -0
  481. package/node_modules/@llblab/pi-telegram/lib/menu-status.ts +274 -0
  482. package/node_modules/@llblab/pi-telegram/lib/menu-thinking.ts +185 -0
  483. package/node_modules/@llblab/pi-telegram/lib/menu.ts +867 -0
  484. package/node_modules/@llblab/pi-telegram/lib/model.ts +672 -0
  485. package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +981 -0
  486. package/node_modules/@llblab/pi-telegram/lib/outbound-buttons.ts +331 -0
  487. package/node_modules/@llblab/pi-telegram/lib/outbound-markup.ts +515 -0
  488. package/node_modules/@llblab/pi-telegram/lib/outbound-voice.ts +285 -0
  489. package/node_modules/@llblab/pi-telegram/lib/outbound.ts +1007 -0
  490. package/node_modules/@llblab/pi-telegram/lib/ownership.ts +298 -0
  491. package/node_modules/@llblab/pi-telegram/lib/paths.ts +127 -0
  492. package/node_modules/@llblab/pi-telegram/lib/pi.ts +197 -0
  493. package/node_modules/@llblab/pi-telegram/lib/polling.ts +1588 -0
  494. package/node_modules/@llblab/pi-telegram/lib/preview.ts +846 -0
  495. package/node_modules/@llblab/pi-telegram/lib/prompt-templates.ts +158 -0
  496. package/node_modules/@llblab/pi-telegram/lib/prompts.ts +218 -0
  497. package/node_modules/@llblab/pi-telegram/lib/queue.ts +3075 -0
  498. package/node_modules/@llblab/pi-telegram/lib/recovery.ts +464 -0
  499. package/node_modules/@llblab/pi-telegram/lib/rendering.ts +1213 -0
  500. package/node_modules/@llblab/pi-telegram/lib/replies.ts +892 -0
  501. package/node_modules/@llblab/pi-telegram/lib/routing.ts +2889 -0
  502. package/node_modules/@llblab/pi-telegram/lib/runtime.ts +628 -0
  503. package/node_modules/@llblab/pi-telegram/lib/sections.ts +710 -0
  504. package/node_modules/@llblab/pi-telegram/lib/setup.ts +220 -0
  505. package/node_modules/@llblab/pi-telegram/lib/skills.ts +21 -0
  506. package/node_modules/@llblab/pi-telegram/lib/status.ts +1550 -0
  507. package/node_modules/@llblab/pi-telegram/lib/sync.ts +883 -0
  508. package/node_modules/@llblab/pi-telegram/lib/target.ts +49 -0
  509. package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +1747 -0
  510. package/node_modules/@llblab/pi-telegram/lib/text-groups.ts +487 -0
  511. package/node_modules/@llblab/pi-telegram/lib/thread-reconciler.ts +1002 -0
  512. package/node_modules/@llblab/pi-telegram/lib/threads.ts +3183 -0
  513. package/node_modules/@llblab/pi-telegram/lib/time-injection.ts +78 -0
  514. package/node_modules/@llblab/pi-telegram/lib/turns.ts +753 -0
  515. package/node_modules/@llblab/pi-telegram/lib/updates.ts +4950 -0
  516. package/node_modules/@llblab/pi-telegram/lib/voice.ts +340 -0
  517. package/node_modules/@llblab/pi-telegram/package.json +83 -0
  518. package/node_modules/@llblab/pi-telegram/screenshot.png +0 -0
  519. package/node_modules/@llblab/pi-telegram/scripts/check-downgrade.mjs +435 -0
  520. package/node_modules/@llblab/pi-telegram/skills/generated-control-surface/SKILL.md +241 -0
  521. package/node_modules/@llblab/pi-telegram/skills/generative-apps/SKILL.md +114 -0
  522. package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/SKILL.md +149 -0
  523. package/package.json +67 -0
@@ -0,0 +1,3140 @@
1
+ /**
2
+ * Telegram durable inbound update journal
3
+ * Zones: telegram inbound, filesystem authority, crash recovery
4
+ * Owns profile/bot-scoped raw updates, schema validation, deduplication,
5
+ * bounded atomic publication, durable queue-receipt/failure state, and compaction.
6
+ * It does not own polling, update execution, queue admission, or follower routing.
7
+ */
8
+
9
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
10
+ import {
11
+ chmodSync,
12
+ mkdirSync,
13
+ readFileSync,
14
+ readdirSync,
15
+ rmdirSync,
16
+ statSync,
17
+ unlinkSync,
18
+ writeFileSync,
19
+ } from "node:fs";
20
+ import { dirname, join } from "node:path";
21
+ import { isDeepStrictEqual } from "node:util";
22
+
23
+ import {
24
+ renameTelegramPathWithRetry,
25
+ withTelegramFileTransaction,
26
+ } from "./locks.ts";
27
+ import {
28
+ getTelegramProcessLiveness,
29
+ type TelegramProcessLiveness,
30
+ } from "./bus.ts";
31
+ import { TELEGRAM_DEFAULT_PROFILE_NAME } from "./paths.ts";
32
+
33
+ export const TELEGRAM_UPDATE_JOURNAL_VERSION = 1 as const;
34
+ export const TELEGRAM_UPDATE_JOURNAL_MAX_ENTRIES = 10_000;
35
+ export const TELEGRAM_UPDATE_JOURNAL_MAX_BYTES = 32 * 1024 * 1024;
36
+ export const TELEGRAM_UPDATE_JOURNAL_FAILURE_ID_MAX_LENGTH = 128;
37
+ export const TELEGRAM_UPDATE_JOURNAL_QUEUE_OWNER_ID_MAX_LENGTH = 256;
38
+ export const TELEGRAM_UPDATE_JOURNAL_QUEUE_HANDOFF_ID_MAX_LENGTH = 128;
39
+ export const TELEGRAM_UPDATE_JOURNAL_QUEUE_HANDOFF_TOKEN_MIN_LENGTH = 32;
40
+ export const TELEGRAM_UPDATE_JOURNAL_QUEUE_HANDOFF_TOKEN_MAX_LENGTH = 256;
41
+ export const TELEGRAM_UPDATE_JOURNAL_FAILURE_CLASS_MAX_LENGTH = 128;
42
+ export const TELEGRAM_UPDATE_JOURNAL_FAILURE_SUMMARY_MAX_LENGTH = 512;
43
+ export const TELEGRAM_UPDATE_JOURNAL_TERMINAL_REASON_MAX_LENGTH = 256;
44
+ export const TELEGRAM_UPDATE_JOURNAL_COMPACTION_SEGMENT_COUNT = 256;
45
+ export const TELEGRAM_UPDATE_JOURNAL_COMPACTION_SEGMENT_BYTES = 4 * 1024 * 1024;
46
+
47
+ export type TelegramUpdateJournalErrorCode =
48
+ | "capacity"
49
+ | "conflict"
50
+ | "identity-mismatch"
51
+ | "invalid"
52
+ | "io"
53
+ | "unsupported-version";
54
+
55
+ export class TelegramUpdateJournalError extends Error {
56
+ readonly code: TelegramUpdateJournalErrorCode;
57
+ readonly path: string;
58
+
59
+ constructor(
60
+ code: TelegramUpdateJournalErrorCode,
61
+ path: string,
62
+ message: string,
63
+ options: ErrorOptions = {},
64
+ ) {
65
+ super(message, options);
66
+ this.name = "TelegramUpdateJournalError";
67
+ this.code = code;
68
+ this.path = path;
69
+ }
70
+ }
71
+
72
+ export interface TelegramUpdateJournalBotIdentity {
73
+ botId?: number;
74
+ tokenSha256: string;
75
+ }
76
+
77
+ export interface TelegramUpdateJournalInput {
78
+ update_id: number;
79
+ }
80
+
81
+ export type TelegramJournaledUpdate = TelegramUpdateJournalInput &
82
+ Record<string, unknown>;
83
+
84
+ export type TelegramUpdateJournalEntryState =
85
+ | "pending"
86
+ | "retry-wait"
87
+ | "queued"
88
+ | "failed";
89
+ export type TelegramUpdateJournalQueueKind = "prompt" | "control";
90
+
91
+ export interface TelegramUpdateJournalQueueProcessIdentity {
92
+ processId: number;
93
+ processBirthId: string;
94
+ }
95
+
96
+ export interface TelegramUpdateJournalQueueRuntimeIdentity
97
+ extends TelegramUpdateJournalQueueProcessIdentity {
98
+ instanceId: string;
99
+ }
100
+
101
+ export interface TelegramUpdateJournalQueueOwnerIdentity
102
+ extends TelegramUpdateJournalQueueRuntimeIdentity {
103
+ sessionGeneration: number;
104
+ }
105
+
106
+ export interface TelegramUpdateJournalQueueOwner
107
+ extends TelegramUpdateJournalQueueOwnerIdentity {
108
+ acquisitionId: string;
109
+ acquiredAtMs: number;
110
+ handoffId?: string;
111
+ }
112
+
113
+ export interface TelegramUpdateJournalQueueHandoff {
114
+ handoffId: string;
115
+ offeredAtMs: number;
116
+ recipientOwner: TelegramUpdateJournalQueueOwnerIdentity;
117
+ }
118
+
119
+ export interface TelegramUpdateJournalFailure {
120
+ attemptCount: number;
121
+ failedAtMs: number;
122
+ failureClass: string;
123
+ summary: string;
124
+ }
125
+
126
+ export type TelegramUpdateJournalOperatorDispositionAction =
127
+ | "retry"
128
+ | "discard";
129
+
130
+ export interface TelegramUpdateJournalOperatorDisposition {
131
+ failureId: string;
132
+ updateId: number;
133
+ action: TelegramUpdateJournalOperatorDispositionAction;
134
+ committedAtMs: number;
135
+ attemptCount: number;
136
+ failureClass: string;
137
+ terminalAtMs: number;
138
+ terminalReason: string;
139
+ }
140
+
141
+ export interface TelegramUpdateJournalEntry {
142
+ updateId: number;
143
+ update: TelegramJournaledUpdate;
144
+ admittedAtMs: number;
145
+ state: TelegramUpdateJournalEntryState;
146
+ queueKind?: TelegramUpdateJournalQueueKind;
147
+ queueReceiptId?: string;
148
+ queueOwner?: TelegramUpdateJournalQueueOwner;
149
+ queueHandoff?: TelegramUpdateJournalQueueHandoff;
150
+ failure?: TelegramUpdateJournalFailure;
151
+ nextRetryAtMs?: number;
152
+ terminalAtMs?: number;
153
+ terminalReason?: string;
154
+ terminalFailureId?: string;
155
+ }
156
+
157
+ export interface TelegramUpdateJournalFile {
158
+ version: typeof TELEGRAM_UPDATE_JOURNAL_VERSION;
159
+ revision?: number;
160
+ profile: string;
161
+ botIdentity: TelegramUpdateJournalBotIdentity;
162
+ entries: TelegramUpdateJournalEntry[];
163
+ operatorDispositions?: TelegramUpdateJournalOperatorDisposition[];
164
+ }
165
+
166
+ export interface TelegramUpdateJournalSnapshot
167
+ extends TelegramUpdateJournalFile {
168
+ exists: boolean;
169
+ serializedBytes: number;
170
+ }
171
+
172
+ export interface TelegramUpdateJournalAppendResult {
173
+ addedUpdateIds: number[];
174
+ duplicateUpdateIds: number[];
175
+ entryCount: number;
176
+ serializedBytes: number;
177
+ }
178
+
179
+ export interface TelegramUpdateJournalRemoveResult {
180
+ removedUpdateIds: number[];
181
+ entryCount: number;
182
+ serializedBytes: number;
183
+ }
184
+
185
+ export interface TelegramUpdateJournalQueueReceipt {
186
+ queueKind: TelegramUpdateJournalQueueKind;
187
+ receiptId: string;
188
+ sourceUpdateIds: readonly number[];
189
+ owner: TelegramUpdateJournalQueueOwnerIdentity;
190
+ }
191
+
192
+ export interface TelegramUpdateJournalQueueResult {
193
+ queuedUpdateIds: number[];
194
+ duplicateUpdateIds: number[];
195
+ queueOwner?: TelegramUpdateJournalQueueOwner;
196
+ entryCount: number;
197
+ serializedBytes: number;
198
+ }
199
+
200
+ export interface TelegramUpdateJournalQueuedCompletion {
201
+ queueKind: TelegramUpdateJournalQueueKind;
202
+ receiptId: string;
203
+ sourceUpdateIds: readonly number[];
204
+ queueOwner: TelegramUpdateJournalQueueOwner;
205
+ }
206
+
207
+ export interface TelegramUpdateJournalQueueHandoffInput {
208
+ queueKind: TelegramUpdateJournalQueueKind;
209
+ receiptId: string;
210
+ sourceUpdateIds: readonly number[];
211
+ expectedOwner: TelegramUpdateJournalQueueOwner;
212
+ recipientOwner: TelegramUpdateJournalQueueOwnerIdentity;
213
+ handoffToken: string;
214
+ }
215
+
216
+ export interface TelegramUpdateJournalQueueHandoffOfferResult {
217
+ handoff: TelegramUpdateJournalQueueHandoff;
218
+ previousOwner: TelegramUpdateJournalQueueOwner;
219
+ offeredUpdateIds: number[];
220
+ duplicate: boolean;
221
+ entryCount: number;
222
+ serializedBytes: number;
223
+ }
224
+
225
+ export interface TelegramUpdateJournalQueueHandoffAcceptResult {
226
+ handoffId: string;
227
+ previousOwner?: TelegramUpdateJournalQueueOwner;
228
+ queueOwner: TelegramUpdateJournalQueueOwner;
229
+ acceptedUpdateIds: number[];
230
+ duplicate: boolean;
231
+ entryCount: number;
232
+ serializedBytes: number;
233
+ }
234
+
235
+ export interface TelegramUpdateJournalQueueHandoffCancelResult {
236
+ handoffId: string;
237
+ previousOwner: TelegramUpdateJournalQueueOwner;
238
+ cancelledUpdateIds: number[];
239
+ entryCount: number;
240
+ serializedBytes: number;
241
+ }
242
+
243
+ export interface TelegramUpdateJournalQueueDiscardInput {
244
+ queueKind: TelegramUpdateJournalQueueKind;
245
+ receiptId: string;
246
+ sourceUpdateIds: readonly number[];
247
+ expectedOwner: TelegramUpdateJournalQueueOwner;
248
+ }
249
+
250
+ export interface TelegramUpdateJournalQueueDiscardResult {
251
+ previousOwner: TelegramUpdateJournalQueueOwner;
252
+ removedUpdateIds: number[];
253
+ entryCount: number;
254
+ serializedBytes: number;
255
+ }
256
+
257
+ export interface TelegramUpdateJournalDeadQueueOwnerRecoveryInput {
258
+ queueKind: TelegramUpdateJournalQueueKind;
259
+ receiptId: string;
260
+ sourceUpdateIds: readonly number[];
261
+ deadOwner: TelegramUpdateJournalQueueOwner;
262
+ recoveryOwner: TelegramUpdateJournalQueueOwnerIdentity;
263
+ }
264
+
265
+ export type TelegramUpdateJournalDeadQueueOwnerRecoveryResult =
266
+ | {
267
+ status: "owner-alive" | "owner-unverifiable";
268
+ previousOwner: TelegramUpdateJournalQueueOwner;
269
+ recoveredUpdateIds: [];
270
+ entryCount: number;
271
+ serializedBytes: number;
272
+ }
273
+ | {
274
+ status: "recovered";
275
+ previousOwner: TelegramUpdateJournalQueueOwner;
276
+ recoveredUpdateIds: number[];
277
+ entryCount: number;
278
+ serializedBytes: number;
279
+ };
280
+
281
+ export interface TelegramUpdateJournalFailureInput {
282
+ updateId: number;
283
+ expectedAttemptCount: number;
284
+ failedAtMs: number;
285
+ failureClass: string;
286
+ summary: string;
287
+ disposition: "retry-wait" | "failed";
288
+ nextRetryAtMs?: number;
289
+ terminalReason?: string;
290
+ }
291
+
292
+ export interface TelegramUpdateJournalFailureResult {
293
+ entry: TelegramUpdateJournalEntry;
294
+ entryCount: number;
295
+ serializedBytes: number;
296
+ }
297
+
298
+ export interface TelegramUpdateJournalOperatorDispositionInput {
299
+ updateId: number;
300
+ failureId: string;
301
+ action: TelegramUpdateJournalOperatorDispositionAction;
302
+ }
303
+
304
+ export interface TelegramUpdateJournalOperatorDispositionResult {
305
+ disposition: TelegramUpdateJournalOperatorDisposition;
306
+ duplicate: boolean;
307
+ entryCount: number;
308
+ serializedBytes: number;
309
+ }
310
+
311
+ export interface TelegramUpdateJournalStore {
312
+ read(): TelegramUpdateJournalSnapshot;
313
+ appendBatch<TUpdate extends TelegramUpdateJournalInput>(
314
+ updates: readonly TUpdate[],
315
+ ): TelegramUpdateJournalAppendResult;
316
+ markQueued(
317
+ receipt: TelegramUpdateJournalQueueReceipt,
318
+ ): TelegramUpdateJournalQueueResult;
319
+ markExecutionFailure(
320
+ input: TelegramUpdateJournalFailureInput,
321
+ ): TelegramUpdateJournalFailureResult;
322
+ applyOperatorDisposition(
323
+ input: TelegramUpdateJournalOperatorDispositionInput,
324
+ ): TelegramUpdateJournalOperatorDispositionResult;
325
+ offerQueuedHandoff(
326
+ input: TelegramUpdateJournalQueueHandoffInput,
327
+ ): TelegramUpdateJournalQueueHandoffOfferResult;
328
+ acceptQueuedHandoff(
329
+ input: TelegramUpdateJournalQueueHandoffInput,
330
+ ): TelegramUpdateJournalQueueHandoffAcceptResult;
331
+ cancelQueuedHandoff(
332
+ input: TelegramUpdateJournalQueueHandoffInput,
333
+ ): TelegramUpdateJournalQueueHandoffCancelResult;
334
+ completeQueued(
335
+ receipts: readonly TelegramUpdateJournalQueuedCompletion[],
336
+ ): TelegramUpdateJournalRemoveResult;
337
+ discardQueued(
338
+ input: TelegramUpdateJournalQueueDiscardInput,
339
+ ): TelegramUpdateJournalQueueDiscardResult;
340
+ recoverDeadQueueOwner(
341
+ input: TelegramUpdateJournalDeadQueueOwnerRecoveryInput,
342
+ ): TelegramUpdateJournalDeadQueueOwnerRecoveryResult;
343
+ removeCompleted(
344
+ updateIds: readonly number[],
345
+ ): TelegramUpdateJournalRemoveResult;
346
+ }
347
+
348
+ export type TelegramUpdateJournalPublicationBoundary =
349
+ | "before-write"
350
+ | "after-write-before-rename";
351
+
352
+ export interface TelegramUpdateJournalStoreOptions {
353
+ path: string;
354
+ profileName?: string;
355
+ botIdentity: TelegramUpdateJournalBotIdentity;
356
+ maxEntries?: number;
357
+ maxBytes?: number;
358
+ getNowMs?: () => number;
359
+ queueRuntimeIdentity?: TelegramUpdateJournalQueueRuntimeIdentity;
360
+ getQueueProcessLiveness?: (
361
+ owner: TelegramUpdateJournalQueueProcessIdentity,
362
+ ) => TelegramProcessLiveness;
363
+ onPublicationBoundary?: (
364
+ boundary: TelegramUpdateJournalPublicationBoundary,
365
+ publicationPath: string,
366
+ ) => void;
367
+ }
368
+
369
+ interface ReadTelegramUpdateJournalResult {
370
+ file: TelegramUpdateJournalFile;
371
+ exists: boolean;
372
+ serializedBytes: number;
373
+ }
374
+
375
+ export interface TelegramUpdateJournalSegment {
376
+ version: typeof TELEGRAM_UPDATE_JOURNAL_VERSION;
377
+ revision: number;
378
+ previousRevision: number;
379
+ profile: string;
380
+ botIdentity: TelegramUpdateJournalBotIdentity;
381
+ upsertedEntries: TelegramUpdateJournalEntry[];
382
+ removedUpdateIds: number[];
383
+ operatorDispositions?: TelegramUpdateJournalOperatorDisposition[];
384
+ }
385
+
386
+ export interface TelegramUpdateJournalSegmentPublicationResult {
387
+ path: string;
388
+ revision: number;
389
+ serializedBytes: number;
390
+ }
391
+
392
+ function createJournalError(
393
+ code: TelegramUpdateJournalErrorCode,
394
+ path: string,
395
+ detail: string,
396
+ cause?: unknown,
397
+ ): TelegramUpdateJournalError {
398
+ return new TelegramUpdateJournalError(
399
+ code,
400
+ path,
401
+ `Telegram update journal ${detail}: ${path}`,
402
+ cause === undefined ? undefined : { cause },
403
+ );
404
+ }
405
+
406
+ function isRecord(value: unknown): value is Record<string, unknown> {
407
+ return !!value && typeof value === "object" && !Array.isArray(value);
408
+ }
409
+
410
+ function hasOnlyKeys(
411
+ value: Record<string, unknown>,
412
+ allowedKeys: readonly string[],
413
+ ): boolean {
414
+ const allowed = new Set(allowedKeys);
415
+ return Object.keys(value).every((key) => allowed.has(key));
416
+ }
417
+
418
+ function isSafeNonNegativeInteger(value: unknown): value is number {
419
+ return Number.isSafeInteger(value) && (value as number) >= 0;
420
+ }
421
+
422
+ function isSafePositiveInteger(value: unknown): value is number {
423
+ return Number.isSafeInteger(value) && (value as number) > 0;
424
+ }
425
+
426
+ function isNonEmptyString(value: unknown): value is string {
427
+ return typeof value === "string" && value.length > 0;
428
+ }
429
+
430
+ function isBoundedString(
431
+ value: unknown,
432
+ maxLength: number,
433
+ ): value is string {
434
+ return isNonEmptyString(value) && value.length <= maxLength;
435
+ }
436
+
437
+ function validateBotIdentity(
438
+ value: unknown,
439
+ path: string,
440
+ ): TelegramUpdateJournalBotIdentity {
441
+ if (
442
+ !isRecord(value) ||
443
+ !hasOnlyKeys(value, ["botId", "tokenSha256"]) ||
444
+ !isNonEmptyString(value.tokenSha256) ||
445
+ !/^[a-f0-9]{64}$/u.test(value.tokenSha256) ||
446
+ (value.botId !== undefined && !isSafePositiveInteger(value.botId))
447
+ ) {
448
+ throw createJournalError("invalid", path, "has invalid bot identity");
449
+ }
450
+ return {
451
+ ...(value.botId !== undefined ? { botId: value.botId } : {}),
452
+ tokenSha256: value.tokenSha256,
453
+ };
454
+ }
455
+
456
+ function validateJournaledUpdate(
457
+ value: unknown,
458
+ path: string,
459
+ ): TelegramJournaledUpdate {
460
+ if (!isRecord(value) || !isSafeNonNegativeInteger(value.update_id)) {
461
+ throw createJournalError(
462
+ "invalid",
463
+ path,
464
+ "contains an update without a safe integer update_id",
465
+ );
466
+ }
467
+ return value as TelegramJournaledUpdate;
468
+ }
469
+
470
+ function normalizeIncomingJournaledUpdate(
471
+ value: unknown,
472
+ path: string,
473
+ ): TelegramJournaledUpdate {
474
+ let normalized: unknown;
475
+ try {
476
+ const serialized = JSON.stringify(value);
477
+ if (serialized === undefined) {
478
+ throw new Error("Update is not JSON serializable.");
479
+ }
480
+ normalized = JSON.parse(serialized) as unknown;
481
+ } catch (error) {
482
+ throw createJournalError(
483
+ "invalid",
484
+ path,
485
+ "received a non-JSON update",
486
+ error,
487
+ );
488
+ }
489
+ return validateJournaledUpdate(normalized, path);
490
+ }
491
+
492
+ function validateJournalQueueOwnerIdentity(
493
+ value: unknown,
494
+ path: string,
495
+ ): TelegramUpdateJournalQueueOwnerIdentity {
496
+ if (
497
+ !isRecord(value) ||
498
+ !hasOnlyKeys(value, [
499
+ "instanceId",
500
+ "processId",
501
+ "processBirthId",
502
+ "sessionGeneration",
503
+ ]) ||
504
+ !isBoundedString(
505
+ value.instanceId,
506
+ TELEGRAM_UPDATE_JOURNAL_QUEUE_OWNER_ID_MAX_LENGTH,
507
+ ) ||
508
+ !isSafePositiveInteger(value.processId) ||
509
+ !isBoundedString(
510
+ value.processBirthId,
511
+ TELEGRAM_UPDATE_JOURNAL_QUEUE_OWNER_ID_MAX_LENGTH,
512
+ ) ||
513
+ !isSafePositiveInteger(value.sessionGeneration)
514
+ ) {
515
+ throw createJournalError(
516
+ "invalid",
517
+ path,
518
+ "contains invalid queue owner identity",
519
+ );
520
+ }
521
+ return {
522
+ instanceId: value.instanceId,
523
+ processId: value.processId,
524
+ processBirthId: value.processBirthId,
525
+ sessionGeneration: value.sessionGeneration,
526
+ };
527
+ }
528
+
529
+ function validateJournalQueueOwner(
530
+ value: unknown,
531
+ path: string,
532
+ ): TelegramUpdateJournalQueueOwner {
533
+ if (
534
+ !isRecord(value) ||
535
+ !hasOnlyKeys(value, [
536
+ "instanceId",
537
+ "processId",
538
+ "processBirthId",
539
+ "sessionGeneration",
540
+ "acquisitionId",
541
+ "acquiredAtMs",
542
+ "handoffId",
543
+ ]) ||
544
+ !isBoundedString(
545
+ value.acquisitionId,
546
+ TELEGRAM_UPDATE_JOURNAL_QUEUE_OWNER_ID_MAX_LENGTH,
547
+ ) ||
548
+ !isSafeNonNegativeInteger(value.acquiredAtMs) ||
549
+ (value.handoffId !== undefined &&
550
+ !isBoundedString(
551
+ value.handoffId,
552
+ TELEGRAM_UPDATE_JOURNAL_QUEUE_HANDOFF_ID_MAX_LENGTH,
553
+ ))
554
+ ) {
555
+ throw createJournalError(
556
+ "invalid",
557
+ path,
558
+ "contains invalid queue receipt acquisition",
559
+ );
560
+ }
561
+ const identity = validateJournalQueueOwnerIdentity(
562
+ {
563
+ instanceId: value.instanceId,
564
+ processId: value.processId,
565
+ processBirthId: value.processBirthId,
566
+ sessionGeneration: value.sessionGeneration,
567
+ },
568
+ path,
569
+ );
570
+ return {
571
+ ...identity,
572
+ acquisitionId: value.acquisitionId,
573
+ acquiredAtMs: value.acquiredAtMs,
574
+ ...(typeof value.handoffId === "string"
575
+ ? { handoffId: value.handoffId }
576
+ : {}),
577
+ };
578
+ }
579
+
580
+ export function parseTelegramUpdateJournalQueueOwner(
581
+ value: unknown,
582
+ ): TelegramUpdateJournalQueueOwner | undefined {
583
+ try {
584
+ return validateJournalQueueOwner(value, "Telegram queue handoff acknowledgement");
585
+ } catch {
586
+ return undefined;
587
+ }
588
+ }
589
+
590
+ export function isTelegramUpdateJournalQueueOwnerProcess(
591
+ owner: TelegramUpdateJournalQueueOwner,
592
+ identity: TelegramUpdateJournalQueueOwnerIdentity,
593
+ ): boolean {
594
+ return (
595
+ owner.instanceId === identity.instanceId &&
596
+ owner.processId === identity.processId &&
597
+ owner.processBirthId === identity.processBirthId
598
+ );
599
+ }
600
+
601
+ export function areTelegramUpdateJournalQueueOwnersEqual(
602
+ left: TelegramUpdateJournalQueueOwner,
603
+ right: TelegramUpdateJournalQueueOwner,
604
+ ): boolean {
605
+ return (
606
+ isTelegramUpdateJournalQueueOwnerProcess(left, right) &&
607
+ left.sessionGeneration === right.sessionGeneration &&
608
+ left.acquisitionId === right.acquisitionId &&
609
+ left.acquiredAtMs === right.acquiredAtMs &&
610
+ left.handoffId === right.handoffId
611
+ );
612
+ }
613
+
614
+ function cloneJournalQueueOwner(
615
+ owner: TelegramUpdateJournalQueueOwner,
616
+ ): TelegramUpdateJournalQueueOwner {
617
+ return { ...owner };
618
+ }
619
+
620
+ function createTelegramUpdateQueueHandoffId(input: {
621
+ handoffToken: string;
622
+ queueKind: TelegramUpdateJournalQueueKind;
623
+ receiptId: string;
624
+ sourceUpdateIds: readonly number[];
625
+ expectedOwner: TelegramUpdateJournalQueueOwner;
626
+ recipientOwner: TelegramUpdateJournalQueueOwnerIdentity;
627
+ }): string {
628
+ return `handoff-${createHash("sha256")
629
+ .update(
630
+ JSON.stringify({
631
+ version: 1,
632
+ token: input.handoffToken,
633
+ queueKind: input.queueKind,
634
+ receiptId: input.receiptId,
635
+ sourceUpdateIds: [...input.sourceUpdateIds].sort((a, b) => a - b),
636
+ donorAcquisitionId: input.expectedOwner.acquisitionId,
637
+ recipientOwner: input.recipientOwner,
638
+ }),
639
+ )
640
+ .digest("hex")
641
+ .slice(0, 32)}`;
642
+ }
643
+
644
+ function validateJournalQueueHandoff(
645
+ value: unknown,
646
+ path: string,
647
+ ): TelegramUpdateJournalQueueHandoff {
648
+ if (
649
+ !isRecord(value) ||
650
+ !hasOnlyKeys(value, ["handoffId", "offeredAtMs", "recipientOwner"]) ||
651
+ !isBoundedString(
652
+ value.handoffId,
653
+ TELEGRAM_UPDATE_JOURNAL_QUEUE_HANDOFF_ID_MAX_LENGTH,
654
+ ) ||
655
+ !isSafeNonNegativeInteger(value.offeredAtMs)
656
+ ) {
657
+ throw createJournalError(
658
+ "invalid",
659
+ path,
660
+ "contains invalid queue handoff metadata",
661
+ );
662
+ }
663
+ return {
664
+ handoffId: value.handoffId,
665
+ offeredAtMs: value.offeredAtMs,
666
+ recipientOwner: validateJournalQueueOwnerIdentity(
667
+ value.recipientOwner,
668
+ path,
669
+ ),
670
+ };
671
+ }
672
+
673
+ function cloneJournalQueueHandoff(
674
+ handoff: TelegramUpdateJournalQueueHandoff,
675
+ ): TelegramUpdateJournalQueueHandoff {
676
+ return {
677
+ ...handoff,
678
+ recipientOwner: { ...handoff.recipientOwner },
679
+ };
680
+ }
681
+
682
+ function validateJournalFailure(
683
+ value: unknown,
684
+ path: string,
685
+ ): TelegramUpdateJournalFailure {
686
+ if (
687
+ !isRecord(value) ||
688
+ !hasOnlyKeys(value, [
689
+ "attemptCount",
690
+ "failedAtMs",
691
+ "failureClass",
692
+ "summary",
693
+ ]) ||
694
+ !isSafePositiveInteger(value.attemptCount) ||
695
+ !isSafeNonNegativeInteger(value.failedAtMs) ||
696
+ !isBoundedString(
697
+ value.failureClass,
698
+ TELEGRAM_UPDATE_JOURNAL_FAILURE_CLASS_MAX_LENGTH,
699
+ ) ||
700
+ !isBoundedString(
701
+ value.summary,
702
+ TELEGRAM_UPDATE_JOURNAL_FAILURE_SUMMARY_MAX_LENGTH,
703
+ )
704
+ ) {
705
+ throw createJournalError(
706
+ "invalid",
707
+ path,
708
+ "contains invalid execution failure metadata",
709
+ );
710
+ }
711
+ return {
712
+ attemptCount: value.attemptCount,
713
+ failedAtMs: value.failedAtMs,
714
+ failureClass: value.failureClass,
715
+ summary: value.summary,
716
+ };
717
+ }
718
+
719
+ function createTelegramUpdateTerminalFailureId(input: {
720
+ updateId: number;
721
+ attemptCount: number;
722
+ failedAtMs: number;
723
+ failureClass: string;
724
+ terminalAtMs: number;
725
+ terminalReason: string;
726
+ }): string {
727
+ return `failure-${createHash("sha256")
728
+ .update(JSON.stringify(input))
729
+ .digest("hex")
730
+ .slice(0, 32)}`;
731
+ }
732
+
733
+ function validateJournalOperatorDisposition(
734
+ value: unknown,
735
+ path: string,
736
+ ): TelegramUpdateJournalOperatorDisposition {
737
+ if (
738
+ !isRecord(value) ||
739
+ !hasOnlyKeys(value, [
740
+ "failureId",
741
+ "updateId",
742
+ "action",
743
+ "committedAtMs",
744
+ "attemptCount",
745
+ "failureClass",
746
+ "terminalAtMs",
747
+ "terminalReason",
748
+ ]) ||
749
+ !isBoundedString(
750
+ value.failureId,
751
+ TELEGRAM_UPDATE_JOURNAL_FAILURE_ID_MAX_LENGTH,
752
+ ) ||
753
+ !isSafeNonNegativeInteger(value.updateId) ||
754
+ (value.action !== "retry" && value.action !== "discard") ||
755
+ !isSafeNonNegativeInteger(value.committedAtMs) ||
756
+ !isSafePositiveInteger(value.attemptCount) ||
757
+ !isBoundedString(
758
+ value.failureClass,
759
+ TELEGRAM_UPDATE_JOURNAL_FAILURE_CLASS_MAX_LENGTH,
760
+ ) ||
761
+ !isSafeNonNegativeInteger(value.terminalAtMs) ||
762
+ value.committedAtMs < value.terminalAtMs ||
763
+ !isBoundedString(
764
+ value.terminalReason,
765
+ TELEGRAM_UPDATE_JOURNAL_TERMINAL_REASON_MAX_LENGTH,
766
+ )
767
+ ) {
768
+ throw createJournalError(
769
+ "invalid",
770
+ path,
771
+ "contains invalid operator disposition metadata",
772
+ );
773
+ }
774
+ return {
775
+ failureId: value.failureId,
776
+ updateId: value.updateId,
777
+ action: value.action,
778
+ committedAtMs: value.committedAtMs,
779
+ attemptCount: value.attemptCount,
780
+ failureClass: value.failureClass,
781
+ terminalAtMs: value.terminalAtMs,
782
+ terminalReason: value.terminalReason,
783
+ };
784
+ }
785
+
786
+ function validateJournalEntry(
787
+ value: unknown,
788
+ path: string,
789
+ ): TelegramUpdateJournalEntry {
790
+ if (
791
+ !isRecord(value) ||
792
+ !hasOnlyKeys(value, [
793
+ "updateId",
794
+ "update",
795
+ "admittedAtMs",
796
+ "state",
797
+ "queueKind",
798
+ "queueReceiptId",
799
+ "queueOwner",
800
+ "queueHandoff",
801
+ "failure",
802
+ "nextRetryAtMs",
803
+ "terminalAtMs",
804
+ "terminalReason",
805
+ "terminalFailureId",
806
+ ]) ||
807
+ !isSafeNonNegativeInteger(value.updateId) ||
808
+ !isSafeNonNegativeInteger(value.admittedAtMs) ||
809
+ (value.state !== "pending" &&
810
+ value.state !== "retry-wait" &&
811
+ value.state !== "queued" &&
812
+ value.state !== "failed")
813
+ ) {
814
+ throw createJournalError("invalid", path, "contains an invalid entry");
815
+ }
816
+ const update = validateJournaledUpdate(value.update, path);
817
+ if (update.update_id !== value.updateId) {
818
+ throw createJournalError(
819
+ "invalid",
820
+ path,
821
+ "contains an entry/update id mismatch",
822
+ );
823
+ }
824
+ const queueKind = value.queueKind;
825
+ const queueReceiptId = value.queueReceiptId;
826
+ const hasQueueMetadata =
827
+ queueKind !== undefined ||
828
+ queueReceiptId !== undefined ||
829
+ value.queueOwner !== undefined ||
830
+ value.queueHandoff !== undefined;
831
+ const hasFailureMetadata =
832
+ value.failure !== undefined ||
833
+ value.nextRetryAtMs !== undefined ||
834
+ value.terminalAtMs !== undefined ||
835
+ value.terminalReason !== undefined ||
836
+ value.terminalFailureId !== undefined;
837
+ if (value.state === "pending" && (hasQueueMetadata || hasFailureMetadata)) {
838
+ throw createJournalError(
839
+ "invalid",
840
+ path,
841
+ "contains metadata on a pending entry",
842
+ );
843
+ }
844
+ if (
845
+ value.state === "queued" &&
846
+ ((queueKind !== "prompt" && queueKind !== "control") ||
847
+ !isNonEmptyString(queueReceiptId) ||
848
+ value.queueOwner === undefined ||
849
+ hasFailureMetadata)
850
+ ) {
851
+ throw createJournalError(
852
+ "invalid",
853
+ path,
854
+ "contains invalid queued entry metadata",
855
+ );
856
+ }
857
+ const queueOwner =
858
+ value.state === "queued" && value.queueOwner !== undefined
859
+ ? validateJournalQueueOwner(value.queueOwner, path)
860
+ : undefined;
861
+ const queueHandoff =
862
+ value.state === "queued" && value.queueHandoff !== undefined
863
+ ? validateJournalQueueHandoff(value.queueHandoff, path)
864
+ : undefined;
865
+ if (queueHandoff && !queueOwner) {
866
+ throw createJournalError(
867
+ "invalid",
868
+ path,
869
+ "contains a queue handoff without donor authority",
870
+ );
871
+ }
872
+ let failure: TelegramUpdateJournalFailure | undefined;
873
+ if (value.state === "retry-wait" || value.state === "failed") {
874
+ if (hasQueueMetadata) {
875
+ throw createJournalError(
876
+ "invalid",
877
+ path,
878
+ "contains queue metadata on a failed execution entry",
879
+ );
880
+ }
881
+ failure = validateJournalFailure(value.failure, path);
882
+ }
883
+ if (
884
+ value.state === "retry-wait" &&
885
+ (!isSafeNonNegativeInteger(value.nextRetryAtMs) ||
886
+ value.nextRetryAtMs < failure!.failedAtMs ||
887
+ value.terminalAtMs !== undefined ||
888
+ value.terminalReason !== undefined ||
889
+ value.terminalFailureId !== undefined)
890
+ ) {
891
+ throw createJournalError(
892
+ "invalid",
893
+ path,
894
+ "contains invalid retry-wait metadata",
895
+ );
896
+ }
897
+ if (
898
+ value.state === "failed" &&
899
+ (!isSafeNonNegativeInteger(value.terminalAtMs) ||
900
+ value.terminalAtMs < failure!.failedAtMs ||
901
+ !isBoundedString(
902
+ value.terminalReason,
903
+ TELEGRAM_UPDATE_JOURNAL_TERMINAL_REASON_MAX_LENGTH,
904
+ ) ||
905
+ (value.terminalFailureId !== undefined &&
906
+ !isBoundedString(
907
+ value.terminalFailureId,
908
+ TELEGRAM_UPDATE_JOURNAL_FAILURE_ID_MAX_LENGTH,
909
+ )) ||
910
+ value.nextRetryAtMs !== undefined)
911
+ ) {
912
+ throw createJournalError(
913
+ "invalid",
914
+ path,
915
+ "contains invalid terminal failure metadata",
916
+ );
917
+ }
918
+ return {
919
+ updateId: value.updateId,
920
+ update,
921
+ admittedAtMs: value.admittedAtMs,
922
+ state: value.state,
923
+ ...(queueKind === "prompt" || queueKind === "control"
924
+ ? { queueKind }
925
+ : {}),
926
+ ...(isNonEmptyString(queueReceiptId) ? { queueReceiptId } : {}),
927
+ ...(queueOwner ? { queueOwner } : {}),
928
+ ...(queueHandoff ? { queueHandoff } : {}),
929
+ ...(failure ? { failure } : {}),
930
+ ...(isSafeNonNegativeInteger(value.nextRetryAtMs)
931
+ ? { nextRetryAtMs: value.nextRetryAtMs }
932
+ : {}),
933
+ ...(isSafeNonNegativeInteger(value.terminalAtMs)
934
+ ? { terminalAtMs: value.terminalAtMs }
935
+ : {}),
936
+ ...(isNonEmptyString(value.terminalReason)
937
+ ? { terminalReason: value.terminalReason }
938
+ : {}),
939
+ ...(value.state === "failed"
940
+ ? {
941
+ terminalFailureId:
942
+ isBoundedString(
943
+ value.terminalFailureId,
944
+ TELEGRAM_UPDATE_JOURNAL_FAILURE_ID_MAX_LENGTH,
945
+ )
946
+ ? value.terminalFailureId
947
+ : createTelegramUpdateTerminalFailureId({
948
+ updateId: value.updateId,
949
+ attemptCount: failure!.attemptCount,
950
+ failedAtMs: failure!.failedAtMs,
951
+ failureClass: failure!.failureClass,
952
+ terminalAtMs: value.terminalAtMs as number,
953
+ terminalReason: value.terminalReason as string,
954
+ }),
955
+ }
956
+ : {}),
957
+ };
958
+ }
959
+
960
+ function parseJournalFile(
961
+ value: unknown,
962
+ path: string,
963
+ ): TelegramUpdateJournalFile {
964
+ if (
965
+ !isRecord(value) ||
966
+ !hasOnlyKeys(value, [
967
+ "version",
968
+ "revision",
969
+ "profile",
970
+ "botIdentity",
971
+ "entries",
972
+ "operatorDispositions",
973
+ ]) ||
974
+ !Number.isSafeInteger(value.version) ||
975
+ (value.revision !== undefined &&
976
+ (!isSafeNonNegativeInteger(value.revision) || value.revision === 0)) ||
977
+ !isNonEmptyString(value.profile) ||
978
+ !Array.isArray(value.entries) ||
979
+ (value.operatorDispositions !== undefined &&
980
+ !Array.isArray(value.operatorDispositions))
981
+ ) {
982
+ throw createJournalError("invalid", path, "has a malformed schema");
983
+ }
984
+ if (value.version !== TELEGRAM_UPDATE_JOURNAL_VERSION) {
985
+ throw createJournalError(
986
+ "unsupported-version",
987
+ path,
988
+ `uses unsupported version ${String(value.version)}`,
989
+ );
990
+ }
991
+ const entries = value.entries.map((entry) =>
992
+ validateJournalEntry(entry, path),
993
+ );
994
+ for (let index = 1; index < entries.length; index += 1) {
995
+ if (entries[index]!.updateId <= entries[index - 1]!.updateId) {
996
+ throw createJournalError(
997
+ "invalid",
998
+ path,
999
+ "contains duplicate or unordered entry ids",
1000
+ );
1001
+ }
1002
+ }
1003
+ const queuedReceipts = new Map<
1004
+ string,
1005
+ {
1006
+ queueKind: TelegramUpdateJournalQueueKind;
1007
+ queueOwner?: TelegramUpdateJournalQueueOwner;
1008
+ queueHandoff?: TelegramUpdateJournalQueueHandoff;
1009
+ }
1010
+ >();
1011
+ for (const entry of entries) {
1012
+ if (
1013
+ entry.state !== "queued" ||
1014
+ !entry.queueKind ||
1015
+ !entry.queueReceiptId
1016
+ ) {
1017
+ continue;
1018
+ }
1019
+ const existing = queuedReceipts.get(entry.queueReceiptId);
1020
+ if (
1021
+ existing &&
1022
+ (existing.queueKind !== entry.queueKind ||
1023
+ (existing.queueOwner === undefined) !==
1024
+ (entry.queueOwner === undefined) ||
1025
+ (existing.queueOwner !== undefined &&
1026
+ entry.queueOwner !== undefined &&
1027
+ !areTelegramUpdateJournalQueueOwnersEqual(
1028
+ existing.queueOwner,
1029
+ entry.queueOwner,
1030
+ )) ||
1031
+ (existing.queueHandoff === undefined) !==
1032
+ (entry.queueHandoff === undefined) ||
1033
+ (existing.queueHandoff !== undefined &&
1034
+ entry.queueHandoff !== undefined &&
1035
+ !isDeepStrictEqual(existing.queueHandoff, entry.queueHandoff)))
1036
+ ) {
1037
+ throw createJournalError(
1038
+ "invalid",
1039
+ path,
1040
+ `contains inconsistent queued receipt ${entry.queueReceiptId}`,
1041
+ );
1042
+ }
1043
+ if (!existing) {
1044
+ queuedReceipts.set(entry.queueReceiptId, {
1045
+ queueKind: entry.queueKind,
1046
+ ...(entry.queueOwner
1047
+ ? { queueOwner: cloneJournalQueueOwner(entry.queueOwner) }
1048
+ : {}),
1049
+ ...(entry.queueHandoff
1050
+ ? { queueHandoff: cloneJournalQueueHandoff(entry.queueHandoff) }
1051
+ : {}),
1052
+ });
1053
+ }
1054
+ }
1055
+ const operatorDispositions = (
1056
+ (value.operatorDispositions as unknown[] | undefined) ?? []
1057
+ ).map((disposition) =>
1058
+ validateJournalOperatorDisposition(disposition, path),
1059
+ );
1060
+ const dispositionFailureIds = new Set<string>();
1061
+ const entriesByUpdateId = new Map(
1062
+ entries.map((entry) => [entry.updateId, entry]),
1063
+ );
1064
+ for (const disposition of operatorDispositions) {
1065
+ if (dispositionFailureIds.has(disposition.failureId)) {
1066
+ throw createJournalError(
1067
+ "invalid",
1068
+ path,
1069
+ "contains duplicate operator disposition failure ids",
1070
+ );
1071
+ }
1072
+ const currentEntry = entriesByUpdateId.get(disposition.updateId);
1073
+ if (
1074
+ currentEntry?.terminalFailureId === disposition.failureId ||
1075
+ (disposition.action === "discard" && currentEntry !== undefined)
1076
+ ) {
1077
+ throw createJournalError(
1078
+ "invalid",
1079
+ path,
1080
+ "contains operator-disposed active authority",
1081
+ );
1082
+ }
1083
+ dispositionFailureIds.add(disposition.failureId);
1084
+ }
1085
+ return {
1086
+ version: TELEGRAM_UPDATE_JOURNAL_VERSION,
1087
+ ...(value.revision !== undefined
1088
+ ? { revision: value.revision as number }
1089
+ : {}),
1090
+ profile: value.profile,
1091
+ botIdentity: validateBotIdentity(value.botIdentity, path),
1092
+ entries,
1093
+ ...(operatorDispositions.length > 0 ? { operatorDispositions } : {}),
1094
+ };
1095
+ }
1096
+
1097
+ function parseJournalSegment(
1098
+ value: unknown,
1099
+ path: string,
1100
+ ): TelegramUpdateJournalSegment {
1101
+ if (
1102
+ !isRecord(value) ||
1103
+ !hasOnlyKeys(value, [
1104
+ "version",
1105
+ "revision",
1106
+ "previousRevision",
1107
+ "profile",
1108
+ "botIdentity",
1109
+ "upsertedEntries",
1110
+ "removedUpdateIds",
1111
+ "operatorDispositions",
1112
+ ]) ||
1113
+ value.version !== TELEGRAM_UPDATE_JOURNAL_VERSION ||
1114
+ !isSafePositiveInteger(value.revision) ||
1115
+ !isSafeNonNegativeInteger(value.previousRevision) ||
1116
+ !isNonEmptyString(value.profile) ||
1117
+ !Array.isArray(value.upsertedEntries) ||
1118
+ !Array.isArray(value.removedUpdateIds) ||
1119
+ (value.operatorDispositions !== undefined &&
1120
+ !Array.isArray(value.operatorDispositions))
1121
+ ) {
1122
+ throw createJournalError("invalid", path, "has a malformed segment schema");
1123
+ }
1124
+ const upsertedEntries = value.upsertedEntries.map((entry) =>
1125
+ validateJournalEntry(entry, path),
1126
+ );
1127
+ const upsertedIds = new Set<number>();
1128
+ for (const entry of upsertedEntries) {
1129
+ if (upsertedIds.has(entry.updateId)) {
1130
+ throw createJournalError("invalid", path, "has duplicate segment upserts");
1131
+ }
1132
+ upsertedIds.add(entry.updateId);
1133
+ }
1134
+ const removedUpdateIds: number[] = [];
1135
+ const removedIds = new Set<number>();
1136
+ for (const updateId of value.removedUpdateIds) {
1137
+ if (
1138
+ !isSafeNonNegativeInteger(updateId) ||
1139
+ removedIds.has(updateId) ||
1140
+ upsertedIds.has(updateId)
1141
+ ) {
1142
+ throw createJournalError("invalid", path, "has invalid segment removals");
1143
+ }
1144
+ removedIds.add(updateId);
1145
+ removedUpdateIds.push(updateId);
1146
+ }
1147
+ const operatorDispositions = (
1148
+ (value.operatorDispositions as unknown[] | undefined) ?? []
1149
+ ).map((disposition) =>
1150
+ validateJournalOperatorDisposition(disposition, path),
1151
+ );
1152
+ return {
1153
+ version: TELEGRAM_UPDATE_JOURNAL_VERSION,
1154
+ revision: value.revision,
1155
+ previousRevision: value.previousRevision,
1156
+ profile: value.profile,
1157
+ botIdentity: validateBotIdentity(value.botIdentity, path),
1158
+ upsertedEntries,
1159
+ removedUpdateIds,
1160
+ ...(value.operatorDispositions !== undefined
1161
+ ? { operatorDispositions }
1162
+ : {}),
1163
+ };
1164
+ }
1165
+
1166
+ function cloneEntry(entry: TelegramUpdateJournalEntry): TelegramUpdateJournalEntry {
1167
+ return {
1168
+ ...entry,
1169
+ update: structuredClone(entry.update),
1170
+ ...(entry.queueOwner
1171
+ ? { queueOwner: cloneJournalQueueOwner(entry.queueOwner) }
1172
+ : {}),
1173
+ ...(entry.queueHandoff
1174
+ ? { queueHandoff: cloneJournalQueueHandoff(entry.queueHandoff) }
1175
+ : {}),
1176
+ ...(entry.failure ? { failure: { ...entry.failure } } : {}),
1177
+ };
1178
+ }
1179
+
1180
+ function cloneFile(file: TelegramUpdateJournalFile): TelegramUpdateJournalFile {
1181
+ return {
1182
+ version: TELEGRAM_UPDATE_JOURNAL_VERSION,
1183
+ ...(file.revision !== undefined ? { revision: file.revision } : {}),
1184
+ profile: file.profile,
1185
+ botIdentity: { ...file.botIdentity },
1186
+ entries: file.entries.map(cloneEntry),
1187
+ ...(file.operatorDispositions?.length
1188
+ ? {
1189
+ operatorDispositions: file.operatorDispositions.map(
1190
+ (disposition) => ({ ...disposition }),
1191
+ ),
1192
+ }
1193
+ : {}),
1194
+ };
1195
+ }
1196
+
1197
+ function serializeJournalFile(file: TelegramUpdateJournalFile): string {
1198
+ return `${JSON.stringify(file, null, 2)}\n`;
1199
+ }
1200
+
1201
+ function getTelegramUpdateJournalSegmentDirectory(path: string): string {
1202
+ return `${path}.segments`;
1203
+ }
1204
+
1205
+ function getTelegramUpdateJournalSegmentPath(
1206
+ path: string,
1207
+ revision: number,
1208
+ ): string {
1209
+ return join(
1210
+ getTelegramUpdateJournalSegmentDirectory(path),
1211
+ `${String(revision).padStart(16, "0")}.json`,
1212
+ );
1213
+ }
1214
+
1215
+ function publishTelegramUpdateJournalSegmentUnlocked(
1216
+ path: string,
1217
+ segment: TelegramUpdateJournalSegment,
1218
+ onPublicationBoundary?: TelegramUpdateJournalStoreOptions["onPublicationBoundary"],
1219
+ ): TelegramUpdateJournalSegmentPublicationResult {
1220
+ const segmentDirectory = getTelegramUpdateJournalSegmentDirectory(path);
1221
+ const segmentPath = getTelegramUpdateJournalSegmentPath(
1222
+ path,
1223
+ segment.revision,
1224
+ );
1225
+ const serialized = `${JSON.stringify(segment, null, 2)}\n`;
1226
+ mkdirSync(segmentDirectory, { recursive: true, mode: 0o700 });
1227
+ const revisions = readdirSync(segmentDirectory)
1228
+ .flatMap((name) => {
1229
+ const match = name.match(/^(\d{16})\.json$/u);
1230
+ return match ? [Number(match[1])] : [];
1231
+ })
1232
+ .filter(Number.isSafeInteger);
1233
+ let snapshotRevision = 0;
1234
+ try {
1235
+ const parsed = JSON.parse(readFileSync(path, "utf8")) as unknown;
1236
+ snapshotRevision = parseJournalFile(parsed, path).revision ?? 0;
1237
+ } catch (error) {
1238
+ if ((error as { code?: unknown })?.code !== "ENOENT") throw error;
1239
+ }
1240
+ const latestRevision = Math.max(snapshotRevision, ...revisions, 0);
1241
+ if (latestRevision >= segment.revision) {
1242
+ try {
1243
+ if (readFileSync(segmentPath, "utf8") === serialized) {
1244
+ return {
1245
+ path: segmentPath,
1246
+ revision: segment.revision,
1247
+ serializedBytes: Buffer.byteLength(serialized),
1248
+ };
1249
+ }
1250
+ } catch {
1251
+ // A compacted snapshot may already contain this revision.
1252
+ }
1253
+ throw new Error("Telegram update journal segment revision conflicts.");
1254
+ }
1255
+ if (latestRevision !== segment.previousRevision) {
1256
+ throw new Error("Telegram update journal segment revision has a gap.");
1257
+ }
1258
+ writeJournalFile(segmentPath, serialized, onPublicationBoundary);
1259
+ return {
1260
+ path: segmentPath,
1261
+ revision: segment.revision,
1262
+ serializedBytes: Buffer.byteLength(serialized),
1263
+ };
1264
+ }
1265
+
1266
+ export function publishTelegramUpdateJournalSegment(
1267
+ path: string,
1268
+ segment: TelegramUpdateJournalSegment,
1269
+ ): TelegramUpdateJournalSegmentPublicationResult {
1270
+ if (
1271
+ segment.version !== TELEGRAM_UPDATE_JOURNAL_VERSION ||
1272
+ !isSafePositiveInteger(segment.revision) ||
1273
+ segment.previousRevision !== segment.revision - 1 ||
1274
+ !isNonEmptyString(segment.profile) ||
1275
+ !Array.isArray(segment.upsertedEntries) ||
1276
+ !Array.isArray(segment.removedUpdateIds)
1277
+ ) {
1278
+ throw new Error("Telegram update journal segment is invalid.");
1279
+ }
1280
+ return withTelegramFileTransaction(`${path}.transaction`, () =>
1281
+ publishTelegramUpdateJournalSegmentUnlocked(path, segment),
1282
+ );
1283
+ }
1284
+
1285
+ function normalizeCapacityLimit(
1286
+ value: number | undefined,
1287
+ fallback: number,
1288
+ label: string,
1289
+ ): number {
1290
+ const resolved = value ?? fallback;
1291
+ if (!Number.isSafeInteger(resolved) || resolved <= 0) {
1292
+ throw new Error(`Telegram update journal ${label} must be a positive integer.`);
1293
+ }
1294
+ return resolved;
1295
+ }
1296
+
1297
+ function identitiesMatch(
1298
+ left: TelegramUpdateJournalBotIdentity,
1299
+ right: TelegramUpdateJournalBotIdentity,
1300
+ ): boolean {
1301
+ if (
1302
+ left.botId !== undefined &&
1303
+ right.botId !== undefined &&
1304
+ left.botId !== right.botId
1305
+ ) {
1306
+ return false;
1307
+ }
1308
+ return (
1309
+ (left.botId !== undefined && left.botId === right.botId) ||
1310
+ left.tokenSha256 === right.tokenSha256
1311
+ );
1312
+ }
1313
+
1314
+ function mergeBotIdentity(
1315
+ stored: TelegramUpdateJournalBotIdentity,
1316
+ current: TelegramUpdateJournalBotIdentity,
1317
+ ): TelegramUpdateJournalBotIdentity {
1318
+ return {
1319
+ ...(current.botId !== undefined
1320
+ ? { botId: current.botId }
1321
+ : stored.botId !== undefined
1322
+ ? { botId: stored.botId }
1323
+ : {}),
1324
+ tokenSha256: current.tokenSha256,
1325
+ };
1326
+ }
1327
+
1328
+ function writeJournalFile(
1329
+ path: string,
1330
+ serialized: string,
1331
+ onPublicationBoundary?: TelegramUpdateJournalStoreOptions["onPublicationBoundary"],
1332
+ ): void {
1333
+ const tempPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
1334
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
1335
+ try {
1336
+ onPublicationBoundary?.("before-write", path);
1337
+ writeFileSync(tempPath, serialized, { encoding: "utf8", mode: 0o600 });
1338
+ chmodSync(tempPath, 0o600);
1339
+ onPublicationBoundary?.("after-write-before-rename", path);
1340
+ if (!renameTelegramPathWithRetry(tempPath, path)) {
1341
+ throw new Error("Temporary journal file disappeared before publication.");
1342
+ }
1343
+ chmodSync(path, 0o600);
1344
+ } finally {
1345
+ try {
1346
+ unlinkSync(tempPath);
1347
+ } catch {
1348
+ // The successful atomic rename already consumed the temporary path.
1349
+ }
1350
+ }
1351
+ }
1352
+
1353
+ export function createTelegramUpdateQueueHandoffToken(): string {
1354
+ return randomBytes(32).toString("base64url");
1355
+ }
1356
+
1357
+ export function createTelegramUpdateJournalBotIdentity(input: {
1358
+ botToken: string;
1359
+ botId?: number;
1360
+ }): TelegramUpdateJournalBotIdentity {
1361
+ if (!input.botToken) {
1362
+ throw new Error("Telegram update journal requires a configured bot token.");
1363
+ }
1364
+ if (input.botId !== undefined && !isSafePositiveInteger(input.botId)) {
1365
+ throw new Error("Telegram update journal bot id must be a safe integer.");
1366
+ }
1367
+ return {
1368
+ ...(input.botId !== undefined ? { botId: input.botId } : {}),
1369
+ tokenSha256: createHash("sha256").update(input.botToken).digest("hex"),
1370
+ };
1371
+ }
1372
+
1373
+ export function createTelegramUpdateJournalReceiptScope(input: {
1374
+ profileName?: string;
1375
+ botIdentity: TelegramUpdateJournalBotIdentity;
1376
+ }): string {
1377
+ const profile = (input.profileName ?? TELEGRAM_DEFAULT_PROFILE_NAME).trim();
1378
+ if (!profile) {
1379
+ throw new Error("Telegram update journal receipt scope requires a profile.");
1380
+ }
1381
+ if (
1382
+ input.botIdentity.botId !== undefined &&
1383
+ !isSafePositiveInteger(input.botIdentity.botId)
1384
+ ) {
1385
+ throw new Error("Telegram update journal bot id must be a safe integer.");
1386
+ }
1387
+ if (!/^[a-f0-9]{64}$/u.test(input.botIdentity.tokenSha256)) {
1388
+ throw new Error(
1389
+ "Telegram update journal receipt scope requires a SHA-256 token fingerprint.",
1390
+ );
1391
+ }
1392
+ return JSON.stringify({
1393
+ version: TELEGRAM_UPDATE_JOURNAL_VERSION,
1394
+ profile,
1395
+ bot:
1396
+ input.botIdentity.botId === undefined
1397
+ ? { tokenSha256: input.botIdentity.tokenSha256 }
1398
+ : { botId: input.botIdentity.botId },
1399
+ });
1400
+ }
1401
+
1402
+ export function createTelegramUpdateJournalBindingKey(input: {
1403
+ path: string;
1404
+ profileName?: string;
1405
+ botIdentity: TelegramUpdateJournalBotIdentity;
1406
+ }): string {
1407
+ if (!input.path) {
1408
+ throw new Error("Telegram update journal binding requires a path.");
1409
+ }
1410
+ return JSON.stringify({
1411
+ version: TELEGRAM_UPDATE_JOURNAL_VERSION,
1412
+ path: input.path,
1413
+ receiptScope: createTelegramUpdateJournalReceiptScope(input),
1414
+ });
1415
+ }
1416
+
1417
+ export function getTelegramUpdateJournalBindingPath(
1418
+ journalBindingKey: string,
1419
+ ): string | undefined {
1420
+ try {
1421
+ const value = JSON.parse(journalBindingKey) as Record<string, unknown>;
1422
+ return value.version === TELEGRAM_UPDATE_JOURNAL_VERSION &&
1423
+ typeof value.path === "string" &&
1424
+ value.path.length > 0 &&
1425
+ typeof value.receiptScope === "string"
1426
+ ? value.path
1427
+ : undefined;
1428
+ } catch {
1429
+ return undefined;
1430
+ }
1431
+ }
1432
+
1433
+ export function createTelegramUpdateJournalReceiptScopeResolver(deps: {
1434
+ getProfileName: () => string | undefined;
1435
+ getBotToken: () => string | undefined;
1436
+ getBotId: () => number | undefined;
1437
+ }): () => string | undefined {
1438
+ let identityKey: string | undefined;
1439
+ let receiptScope: string | undefined;
1440
+ return () => {
1441
+ const botToken = deps.getBotToken();
1442
+ if (!botToken) {
1443
+ identityKey = undefined;
1444
+ receiptScope = undefined;
1445
+ return undefined;
1446
+ }
1447
+ const profileName = deps.getProfileName() ?? TELEGRAM_DEFAULT_PROFILE_NAME;
1448
+ const botIdentity = createTelegramUpdateJournalBotIdentity({
1449
+ botToken,
1450
+ botId: deps.getBotId(),
1451
+ });
1452
+ const nextIdentityKey = `${profileName}\u0000${botIdentity.tokenSha256}`;
1453
+ if (nextIdentityKey === identityKey && receiptScope) return receiptScope;
1454
+ identityKey = nextIdentityKey;
1455
+ receiptScope = createTelegramUpdateJournalReceiptScope({
1456
+ profileName,
1457
+ botIdentity,
1458
+ });
1459
+ return receiptScope;
1460
+ };
1461
+ }
1462
+
1463
+ export interface TelegramUpdateJournalRuntimeBinding {
1464
+ runtimeKey: string;
1465
+ recoveryKey: string;
1466
+ journal: TelegramUpdateJournalStore;
1467
+ }
1468
+
1469
+ export interface TelegramUpdateJournalRuntimeBindingResolverDeps {
1470
+ getProfileName: () => string | undefined;
1471
+ getBotToken: () => string | undefined;
1472
+ getBotId: () => number | undefined;
1473
+ getJournalPath: (profileName?: string) => string;
1474
+ getQueueRuntimeIdentity?: () => TelegramUpdateJournalQueueRuntimeIdentity;
1475
+ }
1476
+
1477
+ export function createTelegramUpdateJournalRuntimeBindingResolver(
1478
+ deps: TelegramUpdateJournalRuntimeBindingResolverDeps,
1479
+ ): () => TelegramUpdateJournalRuntimeBinding | undefined {
1480
+ return () => {
1481
+ const botToken = deps.getBotToken();
1482
+ if (!botToken) return undefined;
1483
+ const configuredProfileName = deps.getProfileName();
1484
+ const profileName =
1485
+ configuredProfileName ?? TELEGRAM_DEFAULT_PROFILE_NAME;
1486
+ const botIdentity = createTelegramUpdateJournalBotIdentity({
1487
+ botToken,
1488
+ botId: deps.getBotId(),
1489
+ });
1490
+ const path = deps.getJournalPath(configuredProfileName);
1491
+ return {
1492
+ runtimeKey: JSON.stringify({
1493
+ path,
1494
+ profileName,
1495
+ botIdentity,
1496
+ }),
1497
+ recoveryKey: createTelegramUpdateJournalBindingKey({
1498
+ path,
1499
+ profileName,
1500
+ botIdentity,
1501
+ }),
1502
+ journal: createTelegramUpdateJournalStore({
1503
+ path,
1504
+ profileName,
1505
+ botIdentity,
1506
+ ...(deps.getQueueRuntimeIdentity
1507
+ ? { queueRuntimeIdentity: deps.getQueueRuntimeIdentity() }
1508
+ : {}),
1509
+ }),
1510
+ };
1511
+ };
1512
+ }
1513
+
1514
+ export interface TelegramUpdateJournalBindingRuntime {
1515
+ resolveLeader: () => TelegramUpdateJournalRuntimeBinding | undefined;
1516
+ resolveFollower: () => TelegramUpdateJournalRuntimeBinding | undefined;
1517
+ resolveActive: () => TelegramUpdateJournalRuntimeBinding | undefined;
1518
+ getActiveRecoveryKey: () => string | undefined;
1519
+ createRecipientResolver: (
1520
+ recipientBindingKey: string,
1521
+ ) => () => TelegramUpdateJournalRuntimeBinding | undefined;
1522
+ }
1523
+
1524
+ export function createTelegramUpdateJournalBindingRuntime(deps: {
1525
+ base: Omit<TelegramUpdateJournalRuntimeBindingResolverDeps, "getJournalPath">;
1526
+ getLeaderJournalPath: (profileName?: string) => string;
1527
+ getFollowerJournalPath: (
1528
+ bindingKey: string,
1529
+ profileName?: string,
1530
+ ) => string;
1531
+ getActiveFollowerBindingKey: () => string;
1532
+ isFollowerRegistered: () => boolean;
1533
+ }): TelegramUpdateJournalBindingRuntime {
1534
+ const resolveLeader = createTelegramUpdateJournalRuntimeBindingResolver({
1535
+ ...deps.base,
1536
+ getJournalPath: deps.getLeaderJournalPath,
1537
+ });
1538
+ const createFollowerResolver = (
1539
+ bindingKey: string,
1540
+ includeQueueRuntimeIdentity: boolean,
1541
+ ) =>
1542
+ createTelegramUpdateJournalRuntimeBindingResolver({
1543
+ getProfileName: deps.base.getProfileName,
1544
+ getBotToken: deps.base.getBotToken,
1545
+ getBotId: deps.base.getBotId,
1546
+ ...(includeQueueRuntimeIdentity && deps.base.getQueueRuntimeIdentity
1547
+ ? { getQueueRuntimeIdentity: deps.base.getQueueRuntimeIdentity }
1548
+ : {}),
1549
+ getJournalPath(profileName) {
1550
+ return deps.getFollowerJournalPath(bindingKey, profileName);
1551
+ },
1552
+ });
1553
+ const resolveFollower = () =>
1554
+ createFollowerResolver(deps.getActiveFollowerBindingKey(), true)();
1555
+ const resolveActive = () =>
1556
+ deps.isFollowerRegistered() ? resolveFollower() : resolveLeader();
1557
+ return {
1558
+ resolveLeader,
1559
+ resolveFollower,
1560
+ resolveActive,
1561
+ getActiveRecoveryKey: () => resolveActive()?.recoveryKey,
1562
+ createRecipientResolver: (bindingKey) =>
1563
+ createFollowerResolver(bindingKey, false),
1564
+ };
1565
+ }
1566
+
1567
+ export function createTelegramUpdateJournalStore(
1568
+ options: TelegramUpdateJournalStoreOptions,
1569
+ ): TelegramUpdateJournalStore {
1570
+ const path = options.path;
1571
+ const profile = options.profileName ?? TELEGRAM_DEFAULT_PROFILE_NAME;
1572
+ if (!path) throw new Error("Telegram update journal path is required.");
1573
+ if (!profile) throw new Error("Telegram update journal profile is required.");
1574
+ const expectedIdentity = validateBotIdentity(options.botIdentity, path);
1575
+ const queueRuntimeIdentity = options.queueRuntimeIdentity;
1576
+ if (
1577
+ queueRuntimeIdentity !== undefined &&
1578
+ (!isBoundedString(
1579
+ queueRuntimeIdentity.instanceId,
1580
+ TELEGRAM_UPDATE_JOURNAL_QUEUE_OWNER_ID_MAX_LENGTH,
1581
+ ) ||
1582
+ !isSafePositiveInteger(queueRuntimeIdentity.processId) ||
1583
+ !isBoundedString(
1584
+ queueRuntimeIdentity.processBirthId,
1585
+ TELEGRAM_UPDATE_JOURNAL_QUEUE_OWNER_ID_MAX_LENGTH,
1586
+ ))
1587
+ ) {
1588
+ throw new Error(
1589
+ "Telegram update journal queue process identity is invalid.",
1590
+ );
1591
+ }
1592
+ const maxEntries = normalizeCapacityLimit(
1593
+ options.maxEntries,
1594
+ TELEGRAM_UPDATE_JOURNAL_MAX_ENTRIES,
1595
+ "entry limit",
1596
+ );
1597
+ const maxBytes = normalizeCapacityLimit(
1598
+ options.maxBytes,
1599
+ TELEGRAM_UPDATE_JOURNAL_MAX_BYTES,
1600
+ "byte limit",
1601
+ );
1602
+ const getNowMs = options.getNowMs ?? Date.now;
1603
+ const onPublicationBoundary = options.onPublicationBoundary;
1604
+ const getQueueProcessLiveness =
1605
+ options.getQueueProcessLiveness ?? getTelegramProcessLiveness;
1606
+
1607
+ const validateQueueHandoffInput = (
1608
+ input: TelegramUpdateJournalQueueHandoffInput,
1609
+ operation: "offer" | "accept" | "cancel",
1610
+ ): {
1611
+ expectedOwner: TelegramUpdateJournalQueueOwner;
1612
+ recipientOwner: TelegramUpdateJournalQueueOwnerIdentity;
1613
+ requestedIds: Set<number>;
1614
+ handoffId: string;
1615
+ } => {
1616
+ if (
1617
+ (input.queueKind !== "prompt" && input.queueKind !== "control") ||
1618
+ !isNonEmptyString(input.receiptId) ||
1619
+ !Array.isArray(input.sourceUpdateIds) ||
1620
+ input.sourceUpdateIds.length === 0 ||
1621
+ !isBoundedString(
1622
+ input.handoffToken,
1623
+ TELEGRAM_UPDATE_JOURNAL_QUEUE_HANDOFF_TOKEN_MAX_LENGTH,
1624
+ ) ||
1625
+ input.handoffToken.length <
1626
+ TELEGRAM_UPDATE_JOURNAL_QUEUE_HANDOFF_TOKEN_MIN_LENGTH
1627
+ ) {
1628
+ throw createJournalError(
1629
+ "invalid",
1630
+ path,
1631
+ `received an invalid queue handoff ${operation}`,
1632
+ );
1633
+ }
1634
+ const expectedOwner = validateJournalQueueOwner(input.expectedOwner, path);
1635
+ const recipientOwner = validateJournalQueueOwnerIdentity(
1636
+ input.recipientOwner,
1637
+ path,
1638
+ );
1639
+ const runtimeIdentity = operation === "accept" ? recipientOwner : expectedOwner;
1640
+ if (
1641
+ queueRuntimeIdentity &&
1642
+ (runtimeIdentity.instanceId !== queueRuntimeIdentity.instanceId ||
1643
+ runtimeIdentity.processId !== queueRuntimeIdentity.processId ||
1644
+ runtimeIdentity.processBirthId !== queueRuntimeIdentity.processBirthId)
1645
+ ) {
1646
+ throw createJournalError(
1647
+ "conflict",
1648
+ path,
1649
+ operation === "accept"
1650
+ ? `cannot accept queue receipt ${input.receiptId} for another runtime`
1651
+ : `cannot ${operation} foreign queue receipt ${input.receiptId}`,
1652
+ );
1653
+ }
1654
+ const requestedIds = new Set<number>();
1655
+ for (const updateId of input.sourceUpdateIds) {
1656
+ if (!isSafeNonNegativeInteger(updateId) || requestedIds.has(updateId)) {
1657
+ throw createJournalError(
1658
+ "invalid",
1659
+ path,
1660
+ `received invalid queue handoff ${operation} update ids`,
1661
+ );
1662
+ }
1663
+ requestedIds.add(updateId);
1664
+ }
1665
+ return {
1666
+ expectedOwner,
1667
+ recipientOwner,
1668
+ requestedIds,
1669
+ handoffId: createTelegramUpdateQueueHandoffId({
1670
+ handoffToken: input.handoffToken,
1671
+ queueKind: input.queueKind,
1672
+ receiptId: input.receiptId,
1673
+ sourceUpdateIds: [...requestedIds],
1674
+ expectedOwner,
1675
+ recipientOwner,
1676
+ }),
1677
+ };
1678
+ };
1679
+
1680
+ const getExactQueuedReceiptEntries = (
1681
+ current: ReadTelegramUpdateJournalResult,
1682
+ input: Pick<
1683
+ TelegramUpdateJournalQueueHandoffInput,
1684
+ "queueKind" | "receiptId"
1685
+ >,
1686
+ requestedIds: ReadonlySet<number>,
1687
+ ): TelegramUpdateJournalEntry[] => {
1688
+ const receiptEntries = current.file.entries.filter(
1689
+ (entry) => entry.queueReceiptId === input.receiptId,
1690
+ );
1691
+ if (
1692
+ receiptEntries.length !== requestedIds.size ||
1693
+ receiptEntries.some(
1694
+ (entry) =>
1695
+ !requestedIds.has(entry.updateId) ||
1696
+ entry.state !== "queued" ||
1697
+ entry.queueKind !== input.queueKind,
1698
+ )
1699
+ ) {
1700
+ throw createJournalError(
1701
+ "conflict",
1702
+ path,
1703
+ `cannot hand off stale queue receipt ${input.receiptId}`,
1704
+ );
1705
+ }
1706
+ return receiptEntries;
1707
+ };
1708
+
1709
+ const assertCapacity = (
1710
+ file: TelegramUpdateJournalFile,
1711
+ serialized = serializeJournalFile(file),
1712
+ ): number => {
1713
+ if (file.entries.length > maxEntries) {
1714
+ throw createJournalError(
1715
+ "capacity",
1716
+ path,
1717
+ `exceeds its ${maxEntries}-entry limit`,
1718
+ );
1719
+ }
1720
+ if ((file.operatorDispositions?.length ?? 0) > maxEntries) {
1721
+ throw createJournalError(
1722
+ "capacity",
1723
+ path,
1724
+ `exceeds its ${maxEntries}-operator-disposition limit`,
1725
+ );
1726
+ }
1727
+ const serializedBytes = Buffer.byteLength(serialized);
1728
+ if (serializedBytes > maxBytes) {
1729
+ throw createJournalError(
1730
+ "capacity",
1731
+ path,
1732
+ `exceeds its ${maxBytes}-byte limit`,
1733
+ );
1734
+ }
1735
+ return serializedBytes;
1736
+ };
1737
+
1738
+ const emptyFile = (): TelegramUpdateJournalFile => ({
1739
+ version: TELEGRAM_UPDATE_JOURNAL_VERSION,
1740
+ profile,
1741
+ botIdentity: { ...expectedIdentity },
1742
+ entries: [],
1743
+ });
1744
+
1745
+ const readCurrent = (): ReadTelegramUpdateJournalResult => {
1746
+ let source: string;
1747
+ try {
1748
+ const size = statSync(path).size;
1749
+ if (size > maxBytes) {
1750
+ throw createJournalError(
1751
+ "capacity",
1752
+ path,
1753
+ `exceeds its ${maxBytes}-byte limit`,
1754
+ );
1755
+ }
1756
+ source = readFileSync(path, "utf8");
1757
+ } catch (error) {
1758
+ if (error instanceof TelegramUpdateJournalError) throw error;
1759
+ if ((error as { code?: unknown })?.code === "ENOENT") {
1760
+ return { file: emptyFile(), exists: false, serializedBytes: 0 };
1761
+ }
1762
+ throw createJournalError("io", path, "could not be read", error);
1763
+ }
1764
+ let parsed: unknown;
1765
+ try {
1766
+ parsed = JSON.parse(source) as unknown;
1767
+ } catch (error) {
1768
+ throw createJournalError("invalid", path, "contains invalid JSON", error);
1769
+ }
1770
+ let file = parseJournalFile(parsed, path);
1771
+ const storedProfile = file.profile;
1772
+ const storedIdentity = file.botIdentity;
1773
+ const segmentDirectory = getTelegramUpdateJournalSegmentDirectory(path);
1774
+ let segmentNames: string[] = [];
1775
+ try {
1776
+ segmentNames = readdirSync(segmentDirectory).filter((name) =>
1777
+ /^\d{16}\.json$/u.test(name),
1778
+ );
1779
+ } catch (error) {
1780
+ if ((error as { code?: unknown })?.code !== "ENOENT") {
1781
+ throw createJournalError("io", segmentDirectory, "could not be read", error);
1782
+ }
1783
+ }
1784
+ segmentNames.sort();
1785
+ let revision = file.revision ?? 0;
1786
+ let unappliedSegmentBytes = 0;
1787
+ for (const name of segmentNames) {
1788
+ const nameRevision = Number(name.slice(0, 16));
1789
+ if (nameRevision <= revision) continue;
1790
+ const segmentPath = join(segmentDirectory, name);
1791
+ let segment: TelegramUpdateJournalSegment;
1792
+ try {
1793
+ const segmentSize = statSync(segmentPath).size;
1794
+ if (segmentSize > maxBytes) {
1795
+ throw createJournalError(
1796
+ "capacity",
1797
+ segmentPath,
1798
+ `exceeds its ${maxBytes}-byte limit`,
1799
+ );
1800
+ }
1801
+ unappliedSegmentBytes += segmentSize;
1802
+ if (unappliedSegmentBytes > maxBytes) {
1803
+ throw createJournalError(
1804
+ "capacity",
1805
+ segmentDirectory,
1806
+ `exceeds its ${maxBytes}-byte unapplied-segment limit`,
1807
+ );
1808
+ }
1809
+ segment = parseJournalSegment(
1810
+ JSON.parse(readFileSync(segmentPath, "utf8")) as unknown,
1811
+ segmentPath,
1812
+ );
1813
+ } catch (error) {
1814
+ if (error instanceof TelegramUpdateJournalError) throw error;
1815
+ throw createJournalError(
1816
+ "invalid",
1817
+ segmentPath,
1818
+ "contains invalid JSON",
1819
+ error,
1820
+ );
1821
+ }
1822
+ if (segment.revision !== nameRevision) {
1823
+ throw createJournalError(
1824
+ "invalid",
1825
+ segmentPath,
1826
+ "revision does not match its file name",
1827
+ );
1828
+ }
1829
+ if (segment.previousRevision !== revision) {
1830
+ throw createJournalError(
1831
+ "invalid",
1832
+ segmentPath,
1833
+ `has a revision gap after ${revision}`,
1834
+ );
1835
+ }
1836
+ if (
1837
+ segment.profile !== storedProfile ||
1838
+ !identitiesMatch(segment.botIdentity, storedIdentity)
1839
+ ) {
1840
+ throw createJournalError(
1841
+ "identity-mismatch",
1842
+ segmentPath,
1843
+ "belongs to another journal identity",
1844
+ );
1845
+ }
1846
+ const entriesById = new Map(
1847
+ file.entries.map((entry) => [entry.updateId, entry]),
1848
+ );
1849
+ for (const updateId of segment.removedUpdateIds) {
1850
+ entriesById.delete(updateId);
1851
+ }
1852
+ for (const entry of segment.upsertedEntries) {
1853
+ entriesById.set(entry.updateId, entry);
1854
+ }
1855
+ file = parseJournalFile(
1856
+ {
1857
+ version: TELEGRAM_UPDATE_JOURNAL_VERSION,
1858
+ revision: segment.revision,
1859
+ profile: storedProfile,
1860
+ botIdentity: mergeBotIdentity(file.botIdentity, segment.botIdentity),
1861
+ entries: [...entriesById.values()].sort(
1862
+ (left, right) => left.updateId - right.updateId,
1863
+ ),
1864
+ ...(segment.operatorDispositions !== undefined
1865
+ ? { operatorDispositions: segment.operatorDispositions }
1866
+ : file.operatorDispositions?.length
1867
+ ? { operatorDispositions: file.operatorDispositions }
1868
+ : {}),
1869
+ },
1870
+ segmentPath,
1871
+ );
1872
+ revision = segment.revision;
1873
+ }
1874
+ const identityChanged =
1875
+ storedProfile !== profile ||
1876
+ !identitiesMatch(file.botIdentity, expectedIdentity);
1877
+ if (identityChanged) {
1878
+ if (file.entries.length > 0) {
1879
+ throw createJournalError(
1880
+ "identity-mismatch",
1881
+ path,
1882
+ storedProfile !== profile
1883
+ ? `belongs to profile ${storedProfile}, not ${profile}`
1884
+ : "belongs to another Telegram bot identity",
1885
+ );
1886
+ }
1887
+ file = {
1888
+ version: TELEGRAM_UPDATE_JOURNAL_VERSION,
1889
+ ...(file.revision !== undefined ? { revision: file.revision } : {}),
1890
+ profile,
1891
+ botIdentity: { ...expectedIdentity },
1892
+ entries: [],
1893
+ };
1894
+ const rebound = serializeJournalFile(file);
1895
+ const reboundBytes = assertCapacity(file, rebound);
1896
+ writeJournalFile(path, rebound, onPublicationBoundary);
1897
+ for (const name of segmentNames) {
1898
+ if (Number(name.slice(0, 16)) <= revision) {
1899
+ try {
1900
+ unlinkSync(join(segmentDirectory, name));
1901
+ } catch {
1902
+ // The rebound snapshot owns the current revision and no old
1903
+ // identity authority; redundant cleanup remains best-effort.
1904
+ }
1905
+ }
1906
+ }
1907
+ try {
1908
+ rmdirSync(segmentDirectory);
1909
+ } catch {
1910
+ // Redundant old segments are ignored at or below the snapshot revision.
1911
+ }
1912
+ return { file, exists: true, serializedBytes: reboundBytes };
1913
+ }
1914
+ const serializedBytes = assertCapacity(file);
1915
+ return { file, exists: true, serializedBytes };
1916
+ };
1917
+
1918
+ const runMutation = <T>(operation: () => T): T => {
1919
+ try {
1920
+ return withTelegramFileTransaction(`${path}.transaction`, operation);
1921
+ } catch (error) {
1922
+ if (error instanceof TelegramUpdateJournalError) throw error;
1923
+ throw createJournalError("io", path, "mutation failed", error);
1924
+ }
1925
+ };
1926
+
1927
+ const publishMutation = (
1928
+ current: ReadTelegramUpdateJournalResult,
1929
+ entries: TelegramUpdateJournalEntry[],
1930
+ contentChanged: boolean,
1931
+ operatorDispositions = current.file.operatorDispositions,
1932
+ ): { file: TelegramUpdateJournalFile; serializedBytes: number } => {
1933
+ const botIdentity = mergeBotIdentity(
1934
+ current.file.botIdentity,
1935
+ expectedIdentity,
1936
+ );
1937
+ if (
1938
+ !contentChanged &&
1939
+ isDeepStrictEqual(current.file.botIdentity, botIdentity) &&
1940
+ isDeepStrictEqual(
1941
+ current.file.operatorDispositions ?? [],
1942
+ operatorDispositions ?? [],
1943
+ )
1944
+ ) {
1945
+ return { file: current.file, serializedBytes: current.serializedBytes };
1946
+ }
1947
+ const file: TelegramUpdateJournalFile = {
1948
+ version: TELEGRAM_UPDATE_JOURNAL_VERSION,
1949
+ profile,
1950
+ botIdentity,
1951
+ entries,
1952
+ ...(operatorDispositions?.length
1953
+ ? { operatorDispositions }
1954
+ : {}),
1955
+ };
1956
+ const serialized = serializeJournalFile(file);
1957
+ const serializedBytes = assertCapacity(file, serialized);
1958
+ const changed =
1959
+ contentChanged ||
1960
+ !isDeepStrictEqual(current.file.botIdentity, file.botIdentity);
1961
+ if (!changed) {
1962
+ return { file, serializedBytes: current.serializedBytes };
1963
+ }
1964
+ if (!current.exists) {
1965
+ writeJournalFile(path, serialized, onPublicationBoundary);
1966
+ return { file, serializedBytes };
1967
+ }
1968
+ const previousEntries = new Map(
1969
+ current.file.entries.map((entry) => [entry.updateId, entry]),
1970
+ );
1971
+ const nextEntries = new Map(entries.map((entry) => [entry.updateId, entry]));
1972
+ const upsertedEntries = entries.filter(
1973
+ (entry) =>
1974
+ !previousEntries.has(entry.updateId) ||
1975
+ !isDeepStrictEqual(previousEntries.get(entry.updateId), entry),
1976
+ );
1977
+ const removedUpdateIds = current.file.entries
1978
+ .filter((entry) => !nextEntries.has(entry.updateId))
1979
+ .map((entry) => entry.updateId);
1980
+ const revision = (current.file.revision ?? 0) + 1;
1981
+ publishTelegramUpdateJournalSegmentUnlocked(path, {
1982
+ version: TELEGRAM_UPDATE_JOURNAL_VERSION,
1983
+ revision,
1984
+ previousRevision: revision - 1,
1985
+ profile,
1986
+ botIdentity: file.botIdentity,
1987
+ upsertedEntries,
1988
+ removedUpdateIds,
1989
+ ...(!isDeepStrictEqual(
1990
+ current.file.operatorDispositions ?? [],
1991
+ operatorDispositions ?? [],
1992
+ )
1993
+ ? { operatorDispositions: operatorDispositions ?? [] }
1994
+ : {}),
1995
+ }, onPublicationBoundary);
1996
+ const revisedFile: TelegramUpdateJournalFile = { ...file, revision };
1997
+ const segmentDirectory = getTelegramUpdateJournalSegmentDirectory(path);
1998
+ const segmentNames = readdirSync(segmentDirectory).filter((name) =>
1999
+ /^\d{16}\.json$/u.test(name),
2000
+ );
2001
+ let snapshotRevision = 0;
2002
+ try {
2003
+ snapshotRevision = parseJournalFile(
2004
+ JSON.parse(readFileSync(path, "utf8")) as unknown,
2005
+ path,
2006
+ ).revision ?? 0;
2007
+ } catch (error) {
2008
+ if ((error as { code?: unknown })?.code !== "ENOENT") throw error;
2009
+ }
2010
+ const unappliedSegmentNames = segmentNames.filter(
2011
+ (name) => Number(name.slice(0, 16)) > snapshotRevision,
2012
+ );
2013
+ const segmentBytes = unappliedSegmentNames.reduce(
2014
+ (total, name) => total + statSync(join(segmentDirectory, name)).size,
2015
+ 0,
2016
+ );
2017
+ if (
2018
+ unappliedSegmentNames.length >=
2019
+ TELEGRAM_UPDATE_JOURNAL_COMPACTION_SEGMENT_COUNT ||
2020
+ segmentBytes >= TELEGRAM_UPDATE_JOURNAL_COMPACTION_SEGMENT_BYTES
2021
+ ) {
2022
+ const compacted = serializeJournalFile(revisedFile);
2023
+ const compactedBytes = assertCapacity(revisedFile, compacted);
2024
+ writeJournalFile(path, compacted, onPublicationBoundary);
2025
+ for (const name of segmentNames) {
2026
+ if (Number(name.slice(0, 16)) <= revision) {
2027
+ try {
2028
+ unlinkSync(join(segmentDirectory, name));
2029
+ } catch {
2030
+ // The published snapshot already owns this revision; redundant
2031
+ // segments are safe and will be ignored on reconstruction.
2032
+ }
2033
+ }
2034
+ }
2035
+ try {
2036
+ rmdirSync(segmentDirectory);
2037
+ } catch {
2038
+ // Interrupted cleanup may leave an empty directory or old segments.
2039
+ }
2040
+ return { file: revisedFile, serializedBytes: compactedBytes };
2041
+ }
2042
+ return { file: revisedFile, serializedBytes };
2043
+ };
2044
+
2045
+ return {
2046
+ read() {
2047
+ return runMutation(() => {
2048
+ const current = readCurrent();
2049
+ return {
2050
+ ...cloneFile(current.file),
2051
+ exists: current.exists,
2052
+ serializedBytes: current.serializedBytes,
2053
+ };
2054
+ });
2055
+ },
2056
+ appendBatch(updates) {
2057
+ return runMutation(() => {
2058
+ const current = readCurrent();
2059
+ const normalizedUpdates: TelegramJournaledUpdate[] = [];
2060
+ for (const update of updates) {
2061
+ const normalized = normalizeIncomingJournaledUpdate(update, path);
2062
+ const previous = normalizedUpdates.at(-1);
2063
+ if (previous && normalized.update_id < previous.update_id) {
2064
+ throw createJournalError(
2065
+ "invalid",
2066
+ path,
2067
+ "received an unordered update batch",
2068
+ );
2069
+ }
2070
+ if (previous?.update_id === normalized.update_id) {
2071
+ if (!isDeepStrictEqual(previous, normalized)) {
2072
+ throw createJournalError(
2073
+ "conflict",
2074
+ path,
2075
+ `received conflicting update ${normalized.update_id}`,
2076
+ );
2077
+ }
2078
+ continue;
2079
+ }
2080
+ normalizedUpdates.push(normalized);
2081
+ }
2082
+
2083
+ const entriesById = new Map(
2084
+ current.file.entries.map((entry) => [entry.updateId, entry]),
2085
+ );
2086
+ const discardedUpdateIds = new Set(
2087
+ (current.file.operatorDispositions ?? [])
2088
+ .filter((disposition) => disposition.action === "discard")
2089
+ .map((disposition) => disposition.updateId),
2090
+ );
2091
+ let admittedAtMs: number | undefined;
2092
+ const addedUpdateIds: number[] = [];
2093
+ const duplicateUpdateIds: number[] = [];
2094
+ for (const update of normalizedUpdates) {
2095
+ if (discardedUpdateIds.has(update.update_id)) {
2096
+ duplicateUpdateIds.push(update.update_id);
2097
+ continue;
2098
+ }
2099
+ const existing = entriesById.get(update.update_id);
2100
+ if (existing) {
2101
+ if (!isDeepStrictEqual(existing.update, update)) {
2102
+ throw createJournalError(
2103
+ "conflict",
2104
+ path,
2105
+ `received conflicting update ${update.update_id}`,
2106
+ );
2107
+ }
2108
+ duplicateUpdateIds.push(update.update_id);
2109
+ continue;
2110
+ }
2111
+ if (admittedAtMs === undefined) {
2112
+ admittedAtMs = getNowMs();
2113
+ if (!isSafeNonNegativeInteger(admittedAtMs)) {
2114
+ throw createJournalError(
2115
+ "invalid",
2116
+ path,
2117
+ "received an invalid admission timestamp",
2118
+ );
2119
+ }
2120
+ }
2121
+ const entry: TelegramUpdateJournalEntry = {
2122
+ updateId: update.update_id,
2123
+ update,
2124
+ admittedAtMs,
2125
+ state: "pending",
2126
+ };
2127
+ entriesById.set(entry.updateId, entry);
2128
+ addedUpdateIds.push(entry.updateId);
2129
+ }
2130
+
2131
+ const contentChanged = addedUpdateIds.length > 0;
2132
+ const published = publishMutation(
2133
+ current,
2134
+ contentChanged
2135
+ ? Array.from(entriesById.values()).sort(
2136
+ (left, right) => left.updateId - right.updateId,
2137
+ )
2138
+ : current.file.entries,
2139
+ contentChanged,
2140
+ );
2141
+ return {
2142
+ addedUpdateIds,
2143
+ duplicateUpdateIds,
2144
+ entryCount: published.file.entries.length,
2145
+ serializedBytes: published.serializedBytes,
2146
+ };
2147
+ });
2148
+ },
2149
+ markQueued(receipt) {
2150
+ return runMutation(() => {
2151
+ if (
2152
+ (receipt.queueKind !== "prompt" &&
2153
+ receipt.queueKind !== "control") ||
2154
+ !isNonEmptyString(receipt.receiptId) ||
2155
+ !Array.isArray(receipt.sourceUpdateIds) ||
2156
+ receipt.sourceUpdateIds.length === 0
2157
+ ) {
2158
+ throw createJournalError(
2159
+ "invalid",
2160
+ path,
2161
+ "received an invalid queue receipt",
2162
+ );
2163
+ }
2164
+ const requestedOwner = validateJournalQueueOwnerIdentity(
2165
+ receipt.owner,
2166
+ path,
2167
+ );
2168
+ if (
2169
+ queueRuntimeIdentity &&
2170
+ (requestedOwner.instanceId !== queueRuntimeIdentity.instanceId ||
2171
+ requestedOwner.processId !== queueRuntimeIdentity.processId ||
2172
+ requestedOwner.processBirthId !==
2173
+ queueRuntimeIdentity.processBirthId)
2174
+ ) {
2175
+ throw createJournalError(
2176
+ "conflict",
2177
+ path,
2178
+ "cannot acquire a queue receipt for another runtime process generation",
2179
+ );
2180
+ }
2181
+ const requestedIds = new Set<number>();
2182
+ for (const updateId of receipt.sourceUpdateIds) {
2183
+ if (
2184
+ !isSafeNonNegativeInteger(updateId) ||
2185
+ requestedIds.has(updateId)
2186
+ ) {
2187
+ throw createJournalError(
2188
+ "invalid",
2189
+ path,
2190
+ "received invalid or duplicate queue receipt update ids",
2191
+ );
2192
+ }
2193
+ requestedIds.add(updateId);
2194
+ }
2195
+ const current = readCurrent();
2196
+ const existingReceiptEntries: TelegramUpdateJournalEntry[] = [];
2197
+ const entriesById = new Map<number, TelegramUpdateJournalEntry>();
2198
+ for (const entry of current.file.entries) {
2199
+ entriesById.set(entry.updateId, entry);
2200
+ if (entry.queueReceiptId === receipt.receiptId) {
2201
+ existingReceiptEntries.push(entry);
2202
+ }
2203
+ }
2204
+ const existingReceiptIds = new Set(
2205
+ existingReceiptEntries.map((entry) => entry.updateId),
2206
+ );
2207
+ if (
2208
+ existingReceiptIds.size > 0 &&
2209
+ (existingReceiptIds.size !== requestedIds.size ||
2210
+ [...existingReceiptIds].some(
2211
+ (updateId) => !requestedIds.has(updateId),
2212
+ ))
2213
+ ) {
2214
+ throw createJournalError(
2215
+ "conflict",
2216
+ path,
2217
+ `has a conflicting queue receipt ${receipt.receiptId}`,
2218
+ );
2219
+ }
2220
+ let queueOwner = existingReceiptEntries[0]?.queueOwner;
2221
+ if (existingReceiptEntries.length === 0) {
2222
+ const acquiredAtMs = getNowMs();
2223
+ if (!isSafeNonNegativeInteger(acquiredAtMs)) {
2224
+ throw createJournalError(
2225
+ "invalid",
2226
+ path,
2227
+ "received an invalid queue acquisition timestamp",
2228
+ );
2229
+ }
2230
+ queueOwner = {
2231
+ ...requestedOwner,
2232
+ acquisitionId: randomUUID(),
2233
+ acquiredAtMs,
2234
+ };
2235
+ }
2236
+ const queuedUpdateIds: number[] = [];
2237
+ const duplicateUpdateIds: number[] = [];
2238
+ for (const updateId of receipt.sourceUpdateIds) {
2239
+ const entry = entriesById.get(updateId);
2240
+ if (!entry) {
2241
+ throw createJournalError(
2242
+ "conflict",
2243
+ path,
2244
+ `cannot queue missing update ${updateId}`,
2245
+ );
2246
+ }
2247
+ if (entry.state === "queued") {
2248
+ if (
2249
+ entry.queueKind !== receipt.queueKind ||
2250
+ entry.queueReceiptId !== receipt.receiptId
2251
+ ) {
2252
+ throw createJournalError(
2253
+ "conflict",
2254
+ path,
2255
+ `update ${updateId} belongs to another queue receipt`,
2256
+ );
2257
+ }
2258
+ duplicateUpdateIds.push(updateId);
2259
+ continue;
2260
+ }
2261
+ if (entry.state === "failed") {
2262
+ throw createJournalError(
2263
+ "conflict",
2264
+ path,
2265
+ `cannot queue failed update ${updateId}`,
2266
+ );
2267
+ }
2268
+ entriesById.set(updateId, {
2269
+ updateId: entry.updateId,
2270
+ update: entry.update,
2271
+ admittedAtMs: entry.admittedAtMs,
2272
+ state: "queued",
2273
+ queueKind: receipt.queueKind,
2274
+ queueReceiptId: receipt.receiptId,
2275
+ queueOwner: cloneJournalQueueOwner(queueOwner!),
2276
+ });
2277
+ queuedUpdateIds.push(updateId);
2278
+ }
2279
+ const contentChanged = queuedUpdateIds.length > 0;
2280
+ const published = publishMutation(
2281
+ current,
2282
+ contentChanged
2283
+ ? current.file.entries.map(
2284
+ (entry) => entriesById.get(entry.updateId)!,
2285
+ )
2286
+ : current.file.entries,
2287
+ contentChanged,
2288
+ );
2289
+ return {
2290
+ queuedUpdateIds,
2291
+ duplicateUpdateIds,
2292
+ ...(queueOwner
2293
+ ? { queueOwner: cloneJournalQueueOwner(queueOwner) }
2294
+ : {}),
2295
+ entryCount: published.file.entries.length,
2296
+ serializedBytes: published.serializedBytes,
2297
+ };
2298
+ });
2299
+ },
2300
+ markExecutionFailure(input) {
2301
+ return runMutation(() => {
2302
+ if (
2303
+ !isSafeNonNegativeInteger(input.updateId) ||
2304
+ !isSafeNonNegativeInteger(input.expectedAttemptCount) ||
2305
+ !isSafeNonNegativeInteger(input.failedAtMs) ||
2306
+ !isBoundedString(
2307
+ input.failureClass,
2308
+ TELEGRAM_UPDATE_JOURNAL_FAILURE_CLASS_MAX_LENGTH,
2309
+ ) ||
2310
+ !isBoundedString(
2311
+ input.summary,
2312
+ TELEGRAM_UPDATE_JOURNAL_FAILURE_SUMMARY_MAX_LENGTH,
2313
+ ) ||
2314
+ (input.disposition !== "retry-wait" &&
2315
+ input.disposition !== "failed")
2316
+ ) {
2317
+ throw createJournalError(
2318
+ "invalid",
2319
+ path,
2320
+ "received invalid execution failure metadata",
2321
+ );
2322
+ }
2323
+ if (
2324
+ input.disposition === "retry-wait" &&
2325
+ (!isSafeNonNegativeInteger(input.nextRetryAtMs) ||
2326
+ input.nextRetryAtMs < input.failedAtMs ||
2327
+ input.terminalReason !== undefined)
2328
+ ) {
2329
+ throw createJournalError(
2330
+ "invalid",
2331
+ path,
2332
+ "received invalid retry-wait disposition",
2333
+ );
2334
+ }
2335
+ if (
2336
+ input.disposition === "failed" &&
2337
+ (!isBoundedString(
2338
+ input.terminalReason,
2339
+ TELEGRAM_UPDATE_JOURNAL_TERMINAL_REASON_MAX_LENGTH,
2340
+ ) ||
2341
+ input.nextRetryAtMs !== undefined)
2342
+ ) {
2343
+ throw createJournalError(
2344
+ "invalid",
2345
+ path,
2346
+ "received invalid terminal failure disposition",
2347
+ );
2348
+ }
2349
+ const current = readCurrent();
2350
+ const entry = current.file.entries.find(
2351
+ (candidate) => candidate.updateId === input.updateId,
2352
+ );
2353
+ if (!entry) {
2354
+ throw createJournalError(
2355
+ "conflict",
2356
+ path,
2357
+ `cannot fail missing update ${input.updateId}`,
2358
+ );
2359
+ }
2360
+ if (entry.state !== "pending" && entry.state !== "retry-wait") {
2361
+ throw createJournalError(
2362
+ "conflict",
2363
+ path,
2364
+ `cannot fail ${entry.state} update ${input.updateId}`,
2365
+ );
2366
+ }
2367
+ const previousAttemptCount = entry.failure?.attemptCount ?? 0;
2368
+ if (previousAttemptCount !== input.expectedAttemptCount) {
2369
+ throw createJournalError(
2370
+ "conflict",
2371
+ path,
2372
+ `update ${input.updateId} execution attempt changed`,
2373
+ );
2374
+ }
2375
+ const failure: TelegramUpdateJournalFailure = {
2376
+ attemptCount: previousAttemptCount + 1,
2377
+ failedAtMs: input.failedAtMs,
2378
+ failureClass: input.failureClass,
2379
+ summary: input.summary,
2380
+ };
2381
+ const terminalFailureId =
2382
+ input.disposition === "failed"
2383
+ ? createTelegramUpdateTerminalFailureId({
2384
+ updateId: entry.updateId,
2385
+ attemptCount: failure.attemptCount,
2386
+ failedAtMs: failure.failedAtMs,
2387
+ failureClass: failure.failureClass,
2388
+ terminalAtMs: input.failedAtMs,
2389
+ terminalReason: input.terminalReason!,
2390
+ })
2391
+ : undefined;
2392
+ const nextEntry: TelegramUpdateJournalEntry = {
2393
+ updateId: entry.updateId,
2394
+ update: entry.update,
2395
+ admittedAtMs: entry.admittedAtMs,
2396
+ state: input.disposition,
2397
+ failure,
2398
+ ...(input.disposition === "retry-wait"
2399
+ ? { nextRetryAtMs: input.nextRetryAtMs! }
2400
+ : {
2401
+ terminalAtMs: input.failedAtMs,
2402
+ terminalReason: input.terminalReason!,
2403
+ terminalFailureId: terminalFailureId!,
2404
+ }),
2405
+ };
2406
+ const published = publishMutation(
2407
+ current,
2408
+ current.file.entries.map((candidate) =>
2409
+ candidate.updateId === input.updateId ? nextEntry : candidate,
2410
+ ),
2411
+ true,
2412
+ );
2413
+ return {
2414
+ entry: cloneEntry(nextEntry),
2415
+ entryCount: published.file.entries.length,
2416
+ serializedBytes: published.serializedBytes,
2417
+ };
2418
+ });
2419
+ },
2420
+ applyOperatorDisposition(input) {
2421
+ return runMutation(() => {
2422
+ if (
2423
+ !isSafeNonNegativeInteger(input.updateId) ||
2424
+ !isBoundedString(
2425
+ input.failureId,
2426
+ TELEGRAM_UPDATE_JOURNAL_FAILURE_ID_MAX_LENGTH,
2427
+ ) ||
2428
+ (input.action !== "retry" && input.action !== "discard")
2429
+ ) {
2430
+ throw createJournalError(
2431
+ "invalid",
2432
+ path,
2433
+ "received an invalid operator disposition",
2434
+ );
2435
+ }
2436
+ const current = readCurrent();
2437
+ const existingDisposition = current.file.operatorDispositions?.find(
2438
+ (candidate) => candidate.failureId === input.failureId,
2439
+ );
2440
+ if (existingDisposition) {
2441
+ if (
2442
+ existingDisposition.updateId !== input.updateId ||
2443
+ existingDisposition.action !== input.action
2444
+ ) {
2445
+ throw createJournalError(
2446
+ "conflict",
2447
+ path,
2448
+ `terminal failure ${input.failureId} already has another operator disposition`,
2449
+ );
2450
+ }
2451
+ return {
2452
+ disposition: { ...existingDisposition },
2453
+ duplicate: true,
2454
+ entryCount: current.file.entries.length,
2455
+ serializedBytes: current.serializedBytes,
2456
+ };
2457
+ }
2458
+ const entry = current.file.entries.find(
2459
+ (candidate) => candidate.updateId === input.updateId,
2460
+ );
2461
+ if (
2462
+ entry?.state !== "failed" ||
2463
+ !entry.failure ||
2464
+ entry.terminalAtMs === undefined ||
2465
+ !entry.terminalReason ||
2466
+ entry.terminalFailureId !== input.failureId
2467
+ ) {
2468
+ throw createJournalError(
2469
+ "conflict",
2470
+ path,
2471
+ `cannot apply a stale disposition to terminal failure ${input.failureId}`,
2472
+ );
2473
+ }
2474
+ const nowMs = getNowMs();
2475
+ if (!isSafeNonNegativeInteger(nowMs)) {
2476
+ throw createJournalError(
2477
+ "invalid",
2478
+ path,
2479
+ "received an invalid operator disposition timestamp",
2480
+ );
2481
+ }
2482
+ const disposition: TelegramUpdateJournalOperatorDisposition = {
2483
+ failureId: entry.terminalFailureId,
2484
+ updateId: entry.updateId,
2485
+ action: input.action,
2486
+ committedAtMs: Math.max(nowMs, entry.terminalAtMs),
2487
+ attemptCount: entry.failure.attemptCount,
2488
+ failureClass: entry.failure.failureClass,
2489
+ terminalAtMs: entry.terminalAtMs,
2490
+ terminalReason: entry.terminalReason,
2491
+ };
2492
+ const nextEntries =
2493
+ input.action === "retry"
2494
+ ? current.file.entries.map((candidate) =>
2495
+ candidate.updateId === entry.updateId
2496
+ ? {
2497
+ updateId: entry.updateId,
2498
+ update: entry.update,
2499
+ admittedAtMs: entry.admittedAtMs,
2500
+ state: "retry-wait" as const,
2501
+ failure: entry.failure,
2502
+ nextRetryAtMs: disposition.committedAtMs,
2503
+ }
2504
+ : candidate,
2505
+ )
2506
+ : current.file.entries.filter(
2507
+ (candidate) => candidate.updateId !== entry.updateId,
2508
+ );
2509
+ const published = publishMutation(
2510
+ current,
2511
+ nextEntries,
2512
+ true,
2513
+ [...(current.file.operatorDispositions ?? []), disposition],
2514
+ );
2515
+ return {
2516
+ disposition: { ...disposition },
2517
+ duplicate: false,
2518
+ entryCount: published.file.entries.length,
2519
+ serializedBytes: published.serializedBytes,
2520
+ };
2521
+ });
2522
+ },
2523
+ offerQueuedHandoff(input) {
2524
+ return runMutation(() => {
2525
+ const { expectedOwner, recipientOwner, requestedIds, handoffId } =
2526
+ validateQueueHandoffInput(input, "offer");
2527
+ if (isTelegramUpdateJournalQueueOwnerProcess(expectedOwner, recipientOwner)) {
2528
+ throw createJournalError(
2529
+ "conflict",
2530
+ path,
2531
+ `cannot hand queue receipt ${input.receiptId} to the same runtime process`,
2532
+ );
2533
+ }
2534
+ const current = readCurrent();
2535
+ const receiptEntries = getExactQueuedReceiptEntries(
2536
+ current,
2537
+ input,
2538
+ requestedIds,
2539
+ );
2540
+ if (
2541
+ receiptEntries.some(
2542
+ (entry) =>
2543
+ !entry.queueOwner ||
2544
+ !areTelegramUpdateJournalQueueOwnersEqual(
2545
+ entry.queueOwner,
2546
+ expectedOwner,
2547
+ ),
2548
+ )
2549
+ ) {
2550
+ throw createJournalError(
2551
+ "conflict",
2552
+ path,
2553
+ `cannot offer stale queue receipt ${input.receiptId}`,
2554
+ );
2555
+ }
2556
+ const existingHandoff = receiptEntries[0]?.queueHandoff;
2557
+ if (existingHandoff) {
2558
+ if (
2559
+ existingHandoff.handoffId !== handoffId ||
2560
+ !isDeepStrictEqual(existingHandoff.recipientOwner, recipientOwner)
2561
+ ) {
2562
+ throw createJournalError(
2563
+ "conflict",
2564
+ path,
2565
+ `queue receipt ${input.receiptId} already has another handoff offer`,
2566
+ );
2567
+ }
2568
+ return {
2569
+ handoff: cloneJournalQueueHandoff(existingHandoff),
2570
+ previousOwner: cloneJournalQueueOwner(expectedOwner),
2571
+ offeredUpdateIds: [...requestedIds].sort((a, b) => a - b),
2572
+ duplicate: true,
2573
+ entryCount: current.file.entries.length,
2574
+ serializedBytes: current.serializedBytes,
2575
+ };
2576
+ }
2577
+ const offeredAtMs = getNowMs();
2578
+ if (!isSafeNonNegativeInteger(offeredAtMs)) {
2579
+ throw createJournalError(
2580
+ "invalid",
2581
+ path,
2582
+ "received an invalid queue handoff offer timestamp",
2583
+ );
2584
+ }
2585
+ const handoff: TelegramUpdateJournalQueueHandoff = {
2586
+ handoffId,
2587
+ offeredAtMs,
2588
+ recipientOwner,
2589
+ };
2590
+ const published = publishMutation(
2591
+ current,
2592
+ current.file.entries.map((entry) =>
2593
+ requestedIds.has(entry.updateId)
2594
+ ? { ...entry, queueHandoff: cloneJournalQueueHandoff(handoff) }
2595
+ : entry,
2596
+ ),
2597
+ true,
2598
+ );
2599
+ return {
2600
+ handoff: cloneJournalQueueHandoff(handoff),
2601
+ previousOwner: cloneJournalQueueOwner(expectedOwner),
2602
+ offeredUpdateIds: [...requestedIds].sort((a, b) => a - b),
2603
+ duplicate: false,
2604
+ entryCount: published.file.entries.length,
2605
+ serializedBytes: published.serializedBytes,
2606
+ };
2607
+ });
2608
+ },
2609
+ acceptQueuedHandoff(input) {
2610
+ return runMutation(() => {
2611
+ const { expectedOwner, recipientOwner, requestedIds, handoffId } =
2612
+ validateQueueHandoffInput(input, "accept");
2613
+ const current = readCurrent();
2614
+ const receiptEntries = getExactQueuedReceiptEntries(
2615
+ current,
2616
+ input,
2617
+ requestedIds,
2618
+ );
2619
+ const existingOwner = receiptEntries[0]?.queueOwner;
2620
+ if (
2621
+ existingOwner &&
2622
+ existingOwner.handoffId === handoffId &&
2623
+ isTelegramUpdateJournalQueueOwnerProcess(existingOwner, recipientOwner)
2624
+ ) {
2625
+ if (
2626
+ receiptEntries.some(
2627
+ (entry) =>
2628
+ !entry.queueOwner ||
2629
+ !areTelegramUpdateJournalQueueOwnersEqual(
2630
+ entry.queueOwner,
2631
+ existingOwner,
2632
+ ) ||
2633
+ entry.queueHandoff !== undefined,
2634
+ )
2635
+ ) {
2636
+ throw createJournalError(
2637
+ "conflict",
2638
+ path,
2639
+ `queue receipt ${input.receiptId} has inconsistent accepted handoff authority`,
2640
+ );
2641
+ }
2642
+ return {
2643
+ handoffId,
2644
+ queueOwner: cloneJournalQueueOwner(existingOwner),
2645
+ acceptedUpdateIds: [...requestedIds].sort((a, b) => a - b),
2646
+ duplicate: true,
2647
+ entryCount: current.file.entries.length,
2648
+ serializedBytes: current.serializedBytes,
2649
+ };
2650
+ }
2651
+ if (
2652
+ receiptEntries.some(
2653
+ (entry) =>
2654
+ !entry.queueOwner ||
2655
+ !areTelegramUpdateJournalQueueOwnersEqual(
2656
+ entry.queueOwner,
2657
+ expectedOwner,
2658
+ ) ||
2659
+ entry.queueHandoff?.handoffId !== handoffId ||
2660
+ !isDeepStrictEqual(
2661
+ entry.queueHandoff.recipientOwner,
2662
+ recipientOwner,
2663
+ ),
2664
+ )
2665
+ ) {
2666
+ throw createJournalError(
2667
+ "conflict",
2668
+ path,
2669
+ `cannot accept stale or unauthenticated queue handoff ${input.receiptId}`,
2670
+ );
2671
+ }
2672
+ const acquiredAtMs = getNowMs();
2673
+ if (!isSafeNonNegativeInteger(acquiredAtMs)) {
2674
+ throw createJournalError(
2675
+ "invalid",
2676
+ path,
2677
+ "received an invalid queue handoff acquisition timestamp",
2678
+ );
2679
+ }
2680
+ const queueOwner: TelegramUpdateJournalQueueOwner = {
2681
+ ...recipientOwner,
2682
+ acquisitionId: randomUUID(),
2683
+ acquiredAtMs,
2684
+ handoffId,
2685
+ };
2686
+ const published = publishMutation(
2687
+ current,
2688
+ current.file.entries.map((entry) =>
2689
+ requestedIds.has(entry.updateId)
2690
+ ? {
2691
+ updateId: entry.updateId,
2692
+ update: entry.update,
2693
+ admittedAtMs: entry.admittedAtMs,
2694
+ state: "queued" as const,
2695
+ queueKind: input.queueKind,
2696
+ queueReceiptId: input.receiptId,
2697
+ queueOwner: cloneJournalQueueOwner(queueOwner),
2698
+ }
2699
+ : entry,
2700
+ ),
2701
+ true,
2702
+ );
2703
+ return {
2704
+ handoffId,
2705
+ previousOwner: cloneJournalQueueOwner(expectedOwner),
2706
+ queueOwner: cloneJournalQueueOwner(queueOwner),
2707
+ acceptedUpdateIds: [...requestedIds].sort((a, b) => a - b),
2708
+ duplicate: false,
2709
+ entryCount: published.file.entries.length,
2710
+ serializedBytes: published.serializedBytes,
2711
+ };
2712
+ });
2713
+ },
2714
+ cancelQueuedHandoff(input) {
2715
+ return runMutation(() => {
2716
+ const { expectedOwner, recipientOwner, requestedIds, handoffId } =
2717
+ validateQueueHandoffInput(input, "cancel");
2718
+ const current = readCurrent();
2719
+ const receiptEntries = getExactQueuedReceiptEntries(
2720
+ current,
2721
+ input,
2722
+ requestedIds,
2723
+ );
2724
+ if (
2725
+ receiptEntries.some(
2726
+ (entry) =>
2727
+ !entry.queueOwner ||
2728
+ !areTelegramUpdateJournalQueueOwnersEqual(
2729
+ entry.queueOwner,
2730
+ expectedOwner,
2731
+ ),
2732
+ )
2733
+ ) {
2734
+ throw createJournalError(
2735
+ "conflict",
2736
+ path,
2737
+ `cannot cancel stale queue handoff ${input.receiptId}`,
2738
+ );
2739
+ }
2740
+ const existingHandoff = receiptEntries[0]?.queueHandoff;
2741
+ if (!existingHandoff) {
2742
+ throw createJournalError(
2743
+ "conflict",
2744
+ path,
2745
+ `cannot cancel missing queue handoff offer ${input.receiptId}`,
2746
+ );
2747
+ }
2748
+ if (
2749
+ existingHandoff.handoffId !== handoffId ||
2750
+ !isDeepStrictEqual(existingHandoff.recipientOwner, recipientOwner)
2751
+ ) {
2752
+ throw createJournalError(
2753
+ "conflict",
2754
+ path,
2755
+ `cannot cancel another queue handoff offer ${input.receiptId}`,
2756
+ );
2757
+ }
2758
+ const published = publishMutation(
2759
+ current,
2760
+ current.file.entries.map((entry) => {
2761
+ if (!requestedIds.has(entry.updateId)) return entry;
2762
+ const { queueHandoff: _queueHandoff, ...retained } = entry;
2763
+ return retained;
2764
+ }),
2765
+ true,
2766
+ );
2767
+ return {
2768
+ handoffId,
2769
+ previousOwner: cloneJournalQueueOwner(expectedOwner),
2770
+ cancelledUpdateIds: [...requestedIds].sort((a, b) => a - b),
2771
+ entryCount: published.file.entries.length,
2772
+ serializedBytes: published.serializedBytes,
2773
+ };
2774
+ });
2775
+ },
2776
+ completeQueued(receipts) {
2777
+ return runMutation(() => {
2778
+ if (!Array.isArray(receipts) || receipts.length === 0) {
2779
+ throw createJournalError(
2780
+ "invalid",
2781
+ path,
2782
+ "received no queued receipts to complete",
2783
+ );
2784
+ }
2785
+ const current = readCurrent();
2786
+ const receiptIds = new Set<string>();
2787
+ const requestedUpdateIds = new Set<number>();
2788
+ for (const receipt of receipts) {
2789
+ if (
2790
+ (receipt.queueKind !== "prompt" &&
2791
+ receipt.queueKind !== "control") ||
2792
+ !isNonEmptyString(receipt.receiptId) ||
2793
+ receiptIds.has(receipt.receiptId) ||
2794
+ !Array.isArray(receipt.sourceUpdateIds) ||
2795
+ receipt.sourceUpdateIds.length === 0
2796
+ ) {
2797
+ throw createJournalError(
2798
+ "invalid",
2799
+ path,
2800
+ "received an invalid queued completion receipt",
2801
+ );
2802
+ }
2803
+ receiptIds.add(receipt.receiptId);
2804
+ const queueOwner = validateJournalQueueOwner(
2805
+ receipt.queueOwner,
2806
+ path,
2807
+ );
2808
+ if (
2809
+ queueRuntimeIdentity &&
2810
+ (queueOwner.instanceId !== queueRuntimeIdentity.instanceId ||
2811
+ queueOwner.processId !== queueRuntimeIdentity.processId ||
2812
+ queueOwner.processBirthId !==
2813
+ queueRuntimeIdentity.processBirthId)
2814
+ ) {
2815
+ throw createJournalError(
2816
+ "conflict",
2817
+ path,
2818
+ `cannot complete foreign queue receipt ${receipt.receiptId}`,
2819
+ );
2820
+ }
2821
+ const sourceUpdateIds = new Set<number>();
2822
+ for (const updateId of receipt.sourceUpdateIds) {
2823
+ if (
2824
+ !isSafeNonNegativeInteger(updateId) ||
2825
+ sourceUpdateIds.has(updateId) ||
2826
+ requestedUpdateIds.has(updateId)
2827
+ ) {
2828
+ throw createJournalError(
2829
+ "invalid",
2830
+ path,
2831
+ "received overlapping queued completion update ids",
2832
+ );
2833
+ }
2834
+ sourceUpdateIds.add(updateId);
2835
+ requestedUpdateIds.add(updateId);
2836
+ }
2837
+ const persistedReceiptEntries = current.file.entries.filter(
2838
+ (entry) => entry.queueReceiptId === receipt.receiptId,
2839
+ );
2840
+ if (
2841
+ persistedReceiptEntries.length !== sourceUpdateIds.size ||
2842
+ persistedReceiptEntries.some(
2843
+ (entry) =>
2844
+ !sourceUpdateIds.has(entry.updateId) ||
2845
+ entry.state !== "queued" ||
2846
+ entry.queueKind !== receipt.queueKind ||
2847
+ !entry.queueOwner ||
2848
+ entry.queueHandoff !== undefined ||
2849
+ !areTelegramUpdateJournalQueueOwnersEqual(
2850
+ entry.queueOwner,
2851
+ queueOwner,
2852
+ ),
2853
+ )
2854
+ ) {
2855
+ throw createJournalError(
2856
+ "conflict",
2857
+ path,
2858
+ `cannot complete stale or foreign queue receipt ${receipt.receiptId}`,
2859
+ );
2860
+ }
2861
+ }
2862
+ const removedUpdateIds = current.file.entries
2863
+ .filter((entry) => requestedUpdateIds.has(entry.updateId))
2864
+ .map((entry) => entry.updateId);
2865
+ if (removedUpdateIds.length !== requestedUpdateIds.size) {
2866
+ throw createJournalError(
2867
+ "conflict",
2868
+ path,
2869
+ "queued completion did not resolve every source update",
2870
+ );
2871
+ }
2872
+ const published = publishMutation(
2873
+ current,
2874
+ current.file.entries.filter(
2875
+ (entry) => !requestedUpdateIds.has(entry.updateId),
2876
+ ),
2877
+ true,
2878
+ );
2879
+ return {
2880
+ removedUpdateIds,
2881
+ entryCount: published.file.entries.length,
2882
+ serializedBytes: published.serializedBytes,
2883
+ };
2884
+ });
2885
+ },
2886
+ discardQueued(input) {
2887
+ return runMutation(() => {
2888
+ if (
2889
+ (input.queueKind !== "prompt" &&
2890
+ input.queueKind !== "control") ||
2891
+ !isNonEmptyString(input.receiptId) ||
2892
+ !Array.isArray(input.sourceUpdateIds) ||
2893
+ input.sourceUpdateIds.length === 0
2894
+ ) {
2895
+ throw createJournalError(
2896
+ "invalid",
2897
+ path,
2898
+ "received an invalid queue discard",
2899
+ );
2900
+ }
2901
+ const expectedOwner = validateJournalQueueOwner(
2902
+ input.expectedOwner,
2903
+ path,
2904
+ );
2905
+ if (
2906
+ queueRuntimeIdentity &&
2907
+ (expectedOwner.instanceId !== queueRuntimeIdentity.instanceId ||
2908
+ expectedOwner.processId !== queueRuntimeIdentity.processId ||
2909
+ expectedOwner.processBirthId !==
2910
+ queueRuntimeIdentity.processBirthId)
2911
+ ) {
2912
+ throw createJournalError(
2913
+ "conflict",
2914
+ path,
2915
+ `cannot discard foreign queue receipt ${input.receiptId}`,
2916
+ );
2917
+ }
2918
+ const requestedIds = new Set<number>();
2919
+ for (const updateId of input.sourceUpdateIds) {
2920
+ if (
2921
+ !isSafeNonNegativeInteger(updateId) ||
2922
+ requestedIds.has(updateId)
2923
+ ) {
2924
+ throw createJournalError(
2925
+ "invalid",
2926
+ path,
2927
+ "received invalid queue discard update ids",
2928
+ );
2929
+ }
2930
+ requestedIds.add(updateId);
2931
+ }
2932
+ const current = readCurrent();
2933
+ const receiptEntries = current.file.entries.filter(
2934
+ (entry) => entry.queueReceiptId === input.receiptId,
2935
+ );
2936
+ if (
2937
+ receiptEntries.length !== requestedIds.size ||
2938
+ receiptEntries.some(
2939
+ (entry) =>
2940
+ !requestedIds.has(entry.updateId) ||
2941
+ entry.state !== "queued" ||
2942
+ entry.queueKind !== input.queueKind ||
2943
+ !entry.queueOwner ||
2944
+ entry.queueHandoff !== undefined ||
2945
+ !areTelegramUpdateJournalQueueOwnersEqual(
2946
+ entry.queueOwner,
2947
+ expectedOwner,
2948
+ ),
2949
+ )
2950
+ ) {
2951
+ throw createJournalError(
2952
+ "conflict",
2953
+ path,
2954
+ `cannot discard stale queue receipt ${input.receiptId}`,
2955
+ );
2956
+ }
2957
+ const removedUpdateIds = [...requestedIds].sort((a, b) => a - b);
2958
+ const published = publishMutation(
2959
+ current,
2960
+ current.file.entries.filter(
2961
+ (entry) => !requestedIds.has(entry.updateId),
2962
+ ),
2963
+ true,
2964
+ );
2965
+ return {
2966
+ previousOwner: cloneJournalQueueOwner(expectedOwner),
2967
+ removedUpdateIds,
2968
+ entryCount: published.file.entries.length,
2969
+ serializedBytes: published.serializedBytes,
2970
+ };
2971
+ });
2972
+ },
2973
+ recoverDeadQueueOwner(input) {
2974
+ return runMutation(() => {
2975
+ if (!getQueueProcessLiveness) {
2976
+ throw createJournalError(
2977
+ "conflict",
2978
+ path,
2979
+ "cannot recover queue authority without a process-liveness proof",
2980
+ );
2981
+ }
2982
+ const deadOwner = validateJournalQueueOwner(input.deadOwner, path);
2983
+ let ownerLiveness: TelegramProcessLiveness;
2984
+ try {
2985
+ ownerLiveness = getQueueProcessLiveness({
2986
+ processId: deadOwner.processId,
2987
+ processBirthId: deadOwner.processBirthId,
2988
+ });
2989
+ } catch (error) {
2990
+ throw createJournalError(
2991
+ "io",
2992
+ path,
2993
+ "could not prove queued owner liveness",
2994
+ error,
2995
+ );
2996
+ }
2997
+ const recoveryOwner = validateJournalQueueOwnerIdentity(
2998
+ input.recoveryOwner,
2999
+ path,
3000
+ );
3001
+ if (
3002
+ queueRuntimeIdentity &&
3003
+ (recoveryOwner.instanceId !== queueRuntimeIdentity.instanceId ||
3004
+ recoveryOwner.processId !== queueRuntimeIdentity.processId ||
3005
+ recoveryOwner.processBirthId !==
3006
+ queueRuntimeIdentity.processBirthId)
3007
+ ) {
3008
+ throw createJournalError(
3009
+ "conflict",
3010
+ path,
3011
+ `cannot recover queue receipt ${input.receiptId} for another runtime`,
3012
+ );
3013
+ }
3014
+ const requestedIds = new Set<number>();
3015
+ for (const updateId of input.sourceUpdateIds) {
3016
+ if (
3017
+ !isSafeNonNegativeInteger(updateId) ||
3018
+ requestedIds.has(updateId)
3019
+ ) {
3020
+ throw createJournalError(
3021
+ "invalid",
3022
+ path,
3023
+ "received invalid queue recovery update ids",
3024
+ );
3025
+ }
3026
+ requestedIds.add(updateId);
3027
+ }
3028
+ const current = readCurrent();
3029
+ const receiptEntries = current.file.entries.filter(
3030
+ (entry) => entry.queueReceiptId === input.receiptId,
3031
+ );
3032
+ if (
3033
+ (input.queueKind !== "prompt" &&
3034
+ input.queueKind !== "control") ||
3035
+ !isNonEmptyString(input.receiptId) ||
3036
+ requestedIds.size === 0 ||
3037
+ receiptEntries.length !== requestedIds.size ||
3038
+ receiptEntries.some(
3039
+ (entry) =>
3040
+ !requestedIds.has(entry.updateId) ||
3041
+ entry.state !== "queued" ||
3042
+ entry.queueKind !== input.queueKind ||
3043
+ !entry.queueOwner ||
3044
+ entry.queueHandoff !== undefined ||
3045
+ !areTelegramUpdateJournalQueueOwnersEqual(
3046
+ entry.queueOwner,
3047
+ deadOwner,
3048
+ ),
3049
+ )
3050
+ ) {
3051
+ throw createJournalError(
3052
+ "conflict",
3053
+ path,
3054
+ `cannot recover stale queue receipt ${input.receiptId}`,
3055
+ );
3056
+ }
3057
+ if (ownerLiveness !== "dead") {
3058
+ return {
3059
+ status:
3060
+ ownerLiveness === "alive"
3061
+ ? ("owner-alive" as const)
3062
+ : ("owner-unverifiable" as const),
3063
+ previousOwner: cloneJournalQueueOwner(deadOwner),
3064
+ recoveredUpdateIds: [] as [],
3065
+ entryCount: current.file.entries.length,
3066
+ serializedBytes: current.serializedBytes,
3067
+ };
3068
+ }
3069
+ const recoveredUpdateIds = [...requestedIds].sort((a, b) => a - b);
3070
+ const published = publishMutation(
3071
+ current,
3072
+ current.file.entries.map((entry) =>
3073
+ requestedIds.has(entry.updateId)
3074
+ ? {
3075
+ updateId: entry.updateId,
3076
+ update: entry.update,
3077
+ admittedAtMs: entry.admittedAtMs,
3078
+ state: "pending" as const,
3079
+ }
3080
+ : entry,
3081
+ ),
3082
+ true,
3083
+ );
3084
+ return {
3085
+ status: "recovered" as const,
3086
+ previousOwner: cloneJournalQueueOwner(deadOwner),
3087
+ recoveredUpdateIds,
3088
+ entryCount: published.file.entries.length,
3089
+ serializedBytes: published.serializedBytes,
3090
+ };
3091
+ });
3092
+ },
3093
+ removeCompleted(updateIds) {
3094
+ return runMutation(() => {
3095
+ const current = readCurrent();
3096
+ const requestedIds = new Set<number>();
3097
+ for (const updateId of updateIds) {
3098
+ if (!isSafeNonNegativeInteger(updateId)) {
3099
+ throw createJournalError(
3100
+ "invalid",
3101
+ path,
3102
+ "received an invalid removal update id",
3103
+ );
3104
+ }
3105
+ requestedIds.add(updateId);
3106
+ }
3107
+ const requestedEntries = current.file.entries.filter((entry) =>
3108
+ requestedIds.has(entry.updateId),
3109
+ );
3110
+ const protectedEntry = requestedEntries.find(
3111
+ (entry) => entry.state === "failed" || entry.state === "queued",
3112
+ );
3113
+ if (protectedEntry) {
3114
+ throw createJournalError(
3115
+ "conflict",
3116
+ path,
3117
+ protectedEntry.state === "failed"
3118
+ ? `cannot complete terminal update ${protectedEntry.updateId} without an operator disposition`
3119
+ : `cannot complete queued update ${protectedEntry.updateId} without its exact owner receipt`,
3120
+ );
3121
+ }
3122
+ const removedUpdateIds = requestedEntries.map(
3123
+ (entry) => entry.updateId,
3124
+ );
3125
+ const published = publishMutation(
3126
+ current,
3127
+ current.file.entries.filter(
3128
+ (entry) => !requestedIds.has(entry.updateId),
3129
+ ),
3130
+ removedUpdateIds.length > 0,
3131
+ );
3132
+ return {
3133
+ removedUpdateIds,
3134
+ entryCount: published.file.entries.length,
3135
+ serializedBytes: published.serializedBytes,
3136
+ };
3137
+ });
3138
+ },
3139
+ };
3140
+ }