okengine 0.22.0 → 0.23.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (312) hide show
  1. package/AGENTS.md +7 -0
  2. package/manifest.v1.schema.json +44 -0
  3. package/package.json +32 -19
  4. package/site/content/docs/ai/mcp.mdx +14 -10
  5. package/site/content/docs/ai/skills.mdx +1 -1
  6. package/site/content/docs/elements/ai/agents.mdx +61 -13
  7. package/site/content/docs/elements/ai/decide.mdx +536 -0
  8. package/site/content/docs/elements/ai/events.mdx +373 -0
  9. package/site/content/docs/elements/ai/index.mdx +9 -3
  10. package/site/content/docs/elements/ai/meta.json +1 -1
  11. package/site/content/docs/elements/ai/prompts.mdx +4 -2
  12. package/site/content/docs/elements/channel/index.mdx +4 -4
  13. package/site/content/docs/elements/channel/receipts.mdx +7 -5
  14. package/site/content/docs/elements/channel/sms.mdx +2 -1
  15. package/site/content/docs/elements/flow/index.mdx +22 -18
  16. package/site/content/docs/elements/gate/auth.mdx +4 -1
  17. package/site/content/docs/elements/signal/index.mdx +15 -14
  18. package/site/content/docs/elements/store/index.mdx +1 -1
  19. package/site/content/docs/elements/store/kv.mdx +5 -5
  20. package/site/content/docs/plugins/otp.mdx +3 -3
  21. package/site/content/docs/recipes/dragonfly.mdx +5 -5
  22. package/site/content/docs/recipes/index.mdx +6 -6
  23. package/site/content/docs/recipes/postgres.mdx +12 -8
  24. package/site/content/docs/recipes/redis.mdx +4 -4
  25. package/site/content/docs/reference/cli.mdx +2 -1
  26. package/site/content/docs/reference/configuration.mdx +9 -7
  27. package/site/content/docs/reference/fx.mdx +19 -10
  28. package/src/auth/api-key-sql.test.ts +44 -0
  29. package/src/auth/api-key-sql.ts +1 -0
  30. package/src/auth/api-keys.ts +30 -12
  31. package/src/auth/config.ts +6 -2
  32. package/src/auth/gate-auth.test.ts +20 -0
  33. package/src/auth/tenants.ts +17 -0
  34. package/src/cli/agents-md.test.ts +25 -0
  35. package/src/cli/ask-vault-gaps.test.ts +38 -1
  36. package/src/cli/ask-vault-gaps.ts +35 -2
  37. package/src/cli/decide.ts +174 -0
  38. package/src/cli/decision-lock-watch.test.ts +45 -0
  39. package/src/cli/decision-lock-watch.ts +51 -0
  40. package/src/cli/dev-app-runner.ts +34 -8
  41. package/src/cli/dev-auth-secret.test.ts +49 -0
  42. package/src/cli/dev-auth-secret.ts +49 -0
  43. package/src/cli/dev-hot.test.ts +41 -0
  44. package/src/cli/dev-hot.ts +48 -0
  45. package/src/cli/dev.test.ts +1 -1
  46. package/src/cli/dev.ts +24 -6
  47. package/src/cli/doctor-diff.test.ts +29 -1
  48. package/src/cli/doctor-diff.ts +35 -0
  49. package/src/cli/eval.ts +142 -1
  50. package/src/cli/index.ts +5 -0
  51. package/src/cli/load-config.images.test.ts +2 -2
  52. package/src/cli/load-config.ts +1 -1
  53. package/src/cli/manifest-pr-diff.ts +16 -0
  54. package/src/cli/mcp-from-console.ts +48 -0
  55. package/src/cli/registry.ts +35 -1
  56. package/src/client/agent.ts +307 -0
  57. package/src/client/transport.test.ts +20 -0
  58. package/src/client/transport.ts +2 -1
  59. package/src/client/types.ts +1 -1
  60. package/src/client-react/index.ts +7 -1
  61. package/src/client-react/use-agent-run.test.ts +271 -0
  62. package/src/client-react/use-agent-run.ts +193 -0
  63. package/src/compiler/ai-approval.test.ts +36 -0
  64. package/src/compiler/ai-repair.test.ts +22 -0
  65. package/src/compiler/decisions.extract.test.ts +164 -0
  66. package/src/compiler/effects-fetch.test.ts +77 -0
  67. package/src/compiler/effects-infer.ts +114 -17
  68. package/src/compiler/effects-join.test.ts +48 -0
  69. package/src/compiler/extract.test.ts +28 -0
  70. package/src/compiler/extract.ts +279 -13
  71. package/src/compiler/fx-index.ts +58 -4
  72. package/src/compiler/response.ts +35 -2
  73. package/src/config/index.ts +21 -3
  74. package/src/console/index.ts +1 -1
  75. package/src/console/server/ai-runs-flows.ts +269 -0
  76. package/src/console/server/ai-runs.test.ts +323 -0
  77. package/src/console/server/ai-runs.ts +562 -0
  78. package/src/console/server/ai.ts +12 -1
  79. package/src/console/server/app.ts +10 -1
  80. package/src/console/server/decisions-flows.ts +157 -0
  81. package/src/console/server/decisions.test.ts +66 -0
  82. package/src/console/server/decisions.ts +229 -0
  83. package/src/console/server/flows.ts +50 -2
  84. package/src/console/server/invoke-user-flow.ts +9 -0
  85. package/src/console/server/runs-ingest.ts +16 -0
  86. package/src/console/server/serve.ts +5 -0
  87. package/src/console/server/state.ts +25 -1
  88. package/src/console/server/store-stats.test.ts +47 -0
  89. package/src/console/server/store-stats.ts +92 -18
  90. package/src/console/ui-next/dist/assets/access-page-C_N4qvai.js +4 -0
  91. package/src/console/ui-next/dist/assets/{agent-disclosure-Bkohc_8X.js → agent-disclosure-Cq1TxniW.js} +1 -1
  92. package/src/console/ui-next/dist/assets/{cache-glyph-rL0lHxat.js → cache-glyph-BaI4uxZz.js} +1 -1
  93. package/src/console/ui-next/dist/assets/{call-pii-button-Deo4PP18.js → call-pii-button-DNyK7IIY.js} +1 -1
  94. package/src/console/ui-next/dist/assets/{collapsible-CoJ6amHf.js → collapsible-CRhBJMZR.js} +1 -1
  95. package/src/console/ui-next/dist/assets/confirm-sheet-DZfuJXP4.js +1 -0
  96. package/src/console/ui-next/dist/assets/copy-inline-button-BTjPVjRT.js +1 -0
  97. package/src/console/ui-next/dist/assets/decisions-page-D9twj8Xy.js +1 -0
  98. package/src/console/ui-next/dist/assets/detail-header-Dk5LipF6.js +1 -0
  99. package/src/console/ui-next/dist/assets/dropdown-menu-CRGrj5JU.js +1 -0
  100. package/src/console/ui-next/dist/assets/duration-tone-CsHWlVPU.js +9 -0
  101. package/src/console/ui-next/dist/assets/element-icons-C6t8Vlml.js +1 -0
  102. package/src/console/ui-next/dist/assets/explorer-chrome-Dslh2yE8.js +1 -0
  103. package/src/console/ui-next/dist/assets/explorer-empty-CIVg7Jgb.js +1 -0
  104. package/src/console/ui-next/dist/assets/flows-page-ZaTJAhR9.js +1 -0
  105. package/src/console/ui-next/dist/assets/{highlighted-json-CPYBzDh0.js → highlighted-json-DQ-EMAfz.js} +1 -1
  106. package/src/console/ui-next/dist/assets/{http-method-DTPevKmM.js → http-method-Cn_Fl79s.js} +1 -1
  107. package/src/console/ui-next/dist/assets/index-BlrN49Hs.css +2 -0
  108. package/src/console/ui-next/dist/assets/index-CmRFeWdT.js +58 -0
  109. package/src/console/ui-next/dist/assets/observability-page-Cn6R_dXz.js +8 -0
  110. package/src/console/ui-next/dist/assets/{replica-lag-BM27lXld.js → replica-lag-Bx6EZhSe.js} +7 -7
  111. package/src/console/ui-next/dist/assets/request-meta-DmdiUrfh.js +1 -0
  112. package/src/console/ui-next/dist/assets/shortcut-keys-DTE1sjTC.js +1 -0
  113. package/src/console/ui-next/dist/assets/store-page-8P6vGXnn.js +41 -0
  114. package/src/console/ui-next/dist/assets/trace-detail-sheet-BfGvpFSv.js +2 -0
  115. package/src/console/ui-next/dist/assets/tree-expand-toggle-CWdUHbc0.js +55 -0
  116. package/src/console/ui-next/dist/assets/units-page-CZOFZzay.js +1 -0
  117. package/src/console/ui-next/dist/assets/use-vault-list-BAyQG8Re.js +1 -0
  118. package/src/console/ui-next/dist/assets/vault-page-ki_ishmX.js +2 -0
  119. package/src/console/ui-next/dist/index.html +4 -3
  120. package/src/console/ui-next/seed-parked-approval.ts +126 -0
  121. package/src/console/ui-next/src/client.ts +224 -1
  122. package/src/console/ui-next/src/components/explorer/detail-header.tsx +9 -0
  123. package/src/console/ui-next/src/components/explorer/explorer-start-toggle.tsx +5 -1
  124. package/src/console/ui-next/src/components/ui/sheet-form.tsx +21 -12
  125. package/src/console/ui-next/src/features/flows/decisions/decisions-page.tsx +216 -0
  126. package/src/console/ui-next/src/features/flows/decisions/resolve.test.ts +48 -0
  127. package/src/console/ui-next/src/features/flows/decisions/resolve.ts +111 -0
  128. package/src/console/ui-next/src/features/flows/flows-page.tsx +10 -0
  129. package/src/console/ui-next/src/features/flows/graph/element-map.ts +1 -0
  130. package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +6 -0
  131. package/src/console/ui-next/src/features/flows/traces/effect-summary.test.ts +24 -0
  132. package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
  133. package/src/console/ui-next/src/features/flows/traces/http-status.ts +58 -0
  134. package/src/console/ui-next/src/features/flows/traces/request-client-card.tsx +355 -0
  135. package/src/console/ui-next/src/features/flows/traces/request-client.test.ts +114 -0
  136. package/src/console/ui-next/src/features/flows/traces/request-client.ts +423 -0
  137. package/src/console/ui-next/src/features/flows/traces/request-input-view.test.ts +52 -6
  138. package/src/console/ui-next/src/features/flows/traces/request-input-view.ts +85 -19
  139. package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +505 -80
  140. package/src/console/ui-next/src/features/flows/traces/trace-lanes.test.ts +61 -0
  141. package/src/console/ui-next/src/features/flows/traces/trace-lanes.ts +78 -0
  142. package/src/console/ui-next/src/features/flows/traces/trace-nav.test.ts +48 -0
  143. package/src/console/ui-next/src/features/flows/traces/trace-nav.ts +57 -0
  144. package/src/console/ui-next/src/features/flows/traces/trace-request-section.tsx +275 -96
  145. package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +16 -2
  146. package/src/console/ui-next/src/features/observability/detail/ai-runs.tsx +369 -0
  147. package/src/console/ui-next/src/features/observability/observability-page.tsx +31 -1
  148. package/src/console/ui-next/src/features/observability/state/observability-selection.ts +18 -3
  149. package/src/console/ui-next/src/features/store/performance/kv-performance-panel.tsx +12 -1
  150. package/src/console/ui-next/src/features/store/performance/performance-panel.tsx +14 -1
  151. package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +1 -1
  152. package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +6 -2
  153. package/src/console/ui-next/src/features/units/explorer/units-tree.tsx +73 -18
  154. package/src/console/ui-next/src/features/units/lib/unit-tree.test.ts +21 -0
  155. package/src/console/ui-next/src/features/units/lib/unit-tree.ts +82 -0
  156. package/src/console/ui-next/src/features/units/units-page.tsx +25 -7
  157. package/src/console/ui-next/src/router.tsx +17 -0
  158. package/src/console/ui-next/ui-next-seed-runs.ts +23 -0
  159. package/src/console/ui-next/vite-console-kernel-plugin.ts +43 -3
  160. package/src/console/ui-next/vite.config.ts +9 -0
  161. package/src/docker/recipes/dragonfly.ts +3 -2
  162. package/src/docker/recipes/redis.ts +1 -1
  163. package/src/drivers/ai-anthropic.ts +212 -21
  164. package/src/drivers/ai-mock.ts +34 -1
  165. package/src/drivers/ai-openai-compatible.ts +67 -26
  166. package/src/drivers/ai-preconnect.ts +18 -0
  167. package/src/drivers/ai-stream.test.ts +62 -0
  168. package/src/drivers/ai-types.ts +20 -0
  169. package/src/drivers/journal-postgres.test.ts +22 -1
  170. package/src/drivers/journal-postgres.ts +511 -13
  171. package/src/drivers/memory-ddl.test.ts +18 -0
  172. package/src/drivers/memory.ts +21 -1
  173. package/src/drivers/pg-rls.test.ts +31 -0
  174. package/src/drivers/pg-rls.ts +15 -0
  175. package/src/drivers/postgres.test.ts +9 -0
  176. package/src/drivers/postgres.ts +49 -11
  177. package/src/drivers/signal-compete.test.ts +234 -0
  178. package/src/drivers/signal-postgres.ts +39 -19
  179. package/src/drivers/signal-redis.ts +167 -11
  180. package/src/drivers/signal-types.ts +12 -0
  181. package/src/elements/ai/agent-event-slot.ts +27 -0
  182. package/src/elements/ai/approval-http.ts +207 -0
  183. package/src/elements/ai/approval.test.ts +741 -0
  184. package/src/elements/ai/approval.ts +228 -0
  185. package/src/elements/ai/decisions/bind.ts +163 -0
  186. package/src/elements/ai/decisions/certificate.test.ts +291 -0
  187. package/src/elements/ai/decisions/certificate.ts +377 -0
  188. package/src/elements/ai/decisions/certify.ts +314 -0
  189. package/src/elements/ai/decisions/decide.test.ts +953 -0
  190. package/src/elements/ai/decisions/e2e.test.ts +399 -0
  191. package/src/elements/ai/decisions/export.test.ts +120 -0
  192. package/src/elements/ai/decisions/export.ts +146 -0
  193. package/src/elements/ai/decisions/fixtures/openrouter-request.json +24 -0
  194. package/src/elements/ai/decisions/fixtures/openrouter-response.json +22 -0
  195. package/src/elements/ai/decisions/fixtures/typesafe-request.json +19 -0
  196. package/src/elements/ai/decisions/fixtures/typesafe-response.json +13 -0
  197. package/src/elements/ai/decisions/http.test.ts +154 -0
  198. package/src/elements/ai/decisions/http.ts +240 -0
  199. package/src/elements/ai/decisions/labels.test.ts +282 -0
  200. package/src/elements/ai/decisions/labels.ts +257 -0
  201. package/src/elements/ai/decisions/openrouter.live.test.ts +136 -0
  202. package/src/elements/ai/decisions/openrouter.ts +36 -0
  203. package/src/elements/ai/decisions/provider.ts +102 -0
  204. package/src/elements/ai/decisions/typesafe.ts +36 -0
  205. package/src/elements/ai/declare.ts +252 -1
  206. package/src/elements/ai/events.test.ts +379 -0
  207. package/src/elements/ai/events.ts +138 -0
  208. package/src/elements/ai/mcp-protocol.ts +5 -6
  209. package/src/elements/ai/run-events.test.ts +461 -0
  210. package/src/elements/ai/run-events.ts +522 -0
  211. package/src/elements/ai/runtime.ts +1187 -105
  212. package/src/elements/ai/stream-turn.ts +214 -0
  213. package/src/elements/ai/stream.live.test.ts +76 -0
  214. package/src/elements/ai/subagent.test.ts +136 -0
  215. package/src/elements/ai.test.ts +497 -3
  216. package/src/elements/ai.ts +16 -1
  217. package/src/elements/channel/runtime.ts +11 -0
  218. package/src/elements/channel/sql-ledger.ts +251 -0
  219. package/src/elements/clock/durable.ts +27 -1
  220. package/src/elements/signal/runtime.ts +9 -0
  221. package/src/elements/signal.test.ts +1 -0
  222. package/src/elements/store/cache.test.ts +120 -15
  223. package/src/elements/store/cache.ts +200 -39
  224. package/src/elements/store/prepare-row.test.ts +6 -2
  225. package/src/elements/store/sql-session.test.ts +53 -0
  226. package/src/elements/store/sql-session.ts +206 -10
  227. package/src/elements/store.ts +1 -0
  228. package/src/i18n/catalogs/ar.ts +5 -0
  229. package/src/i18n/catalogs/en.ts +5 -0
  230. package/src/index.ts +2 -0
  231. package/src/kernel/agent-event-store.ts +448 -0
  232. package/src/kernel/app.ts +135 -17
  233. package/src/kernel/auto-cache.test.ts +228 -0
  234. package/src/kernel/boot-bind/ai.ts +3 -0
  235. package/src/kernel/boot-bind/channel.ts +63 -2
  236. package/src/kernel/boot-bind/honor-config.test.ts +40 -10
  237. package/src/kernel/boot-bind/signal.ts +61 -25
  238. package/src/kernel/boot-bind/store.test.ts +26 -0
  239. package/src/kernel/boot-bind/store.ts +82 -6
  240. package/src/kernel/boot.ts +31 -6
  241. package/src/kernel/builtin-errors.ts +2 -0
  242. package/src/kernel/capability.ts +3 -0
  243. package/src/kernel/decision-label-store.ts +441 -0
  244. package/src/kernel/effects.test.ts +2 -1
  245. package/src/kernel/effects.ts +13 -1
  246. package/src/kernel/element-registries.ts +3 -0
  247. package/src/kernel/errors-compiler.ts +11 -0
  248. package/src/kernel/errors-text.ts +20 -0
  249. package/src/kernel/errors.ts +10 -0
  250. package/src/kernel/flow.test.ts +2 -3
  251. package/src/kernel/flow.ts +33 -3
  252. package/src/kernel/fx-call-types.test.ts +32 -0
  253. package/src/kernel/fx-decide.ts +765 -0
  254. package/src/kernel/fx-sql-handle.ts +46 -7
  255. package/src/kernel/fx.test.ts +13 -8
  256. package/src/kernel/fx.ts +339 -45
  257. package/src/kernel/http-frame.test.ts +94 -0
  258. package/src/kernel/http-frame.ts +203 -0
  259. package/src/kernel/idempotency.test.ts +2 -1
  260. package/src/kernel/journal.test.ts +114 -0
  261. package/src/kernel/journal.ts +471 -51
  262. package/src/kernel/json-result.ts +10 -0
  263. package/src/kernel/pipeline-tenant.ts +5 -1
  264. package/src/kernel/pipeline.test.ts +45 -0
  265. package/src/kernel/pipeline.ts +5 -0
  266. package/src/kernel/signal-tx.ts +28 -0
  267. package/src/kernel/sse-id.ts +32 -0
  268. package/src/kernel/store-transaction.test.ts +95 -0
  269. package/src/manifest/diff.ts +1 -0
  270. package/src/manifest/types.ts +29 -2
  271. package/src/mcp/ai-tools.test.ts +202 -0
  272. package/src/mcp/authorization.ts +66 -0
  273. package/src/mcp/docs-server.ts +19 -2
  274. package/src/mcp/mcp.test.ts +83 -0
  275. package/src/mcp/protocol.ts +8 -2
  276. package/src/mcp/server.ts +23 -2
  277. package/src/mcp/session.ts +3 -0
  278. package/src/mcp/tools.ts +168 -0
  279. package/src/mcp/versions.ts +60 -0
  280. package/src/plugins/index.ts +1 -0
  281. package/src/plugins/mena.ts +56 -0
  282. package/src/release/http-graph.test.ts +2 -0
  283. package/src/release/http-graph.ts +5 -0
  284. package/src/release/measure.ts +39 -14
  285. package/src/runs/collect.ts +4 -1
  286. package/src/runs/parquet.test.ts +27 -1
  287. package/src/runs/parquet.ts +19 -0
  288. package/src/runs/types.ts +31 -0
  289. package/src/runtime/json-code-block.test.ts +106 -47
  290. package/src/runtime/json-code-block.ts +1209 -46
  291. package/src/term.test.ts +13 -0
  292. package/src/term.ts +81 -49
  293. package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +0 -1
  294. package/src/console/ui-next/dist/assets/access-page-Ze6ckjdd.js +0 -4
  295. package/src/console/ui-next/dist/assets/copy-inline-button-CWG5_ZOh.js +0 -1
  296. package/src/console/ui-next/dist/assets/detail-header-b_ZzIsmU.js +0 -1
  297. package/src/console/ui-next/dist/assets/dropdown-menu-nwXEO1Ac.js +0 -1
  298. package/src/console/ui-next/dist/assets/duration-tone-BbQ_8z50.js +0 -9
  299. package/src/console/ui-next/dist/assets/element-icons-CwyLpVXz.js +0 -1
  300. package/src/console/ui-next/dist/assets/explorer-empty-C8sSCqYT.js +0 -1
  301. package/src/console/ui-next/dist/assets/flows-page-D2E7BtKK.js +0 -1
  302. package/src/console/ui-next/dist/assets/index-VxoEz295.css +0 -2
  303. package/src/console/ui-next/dist/assets/index-vTuwmeQz.js +0 -58
  304. package/src/console/ui-next/dist/assets/observability-page-SJTSzHFJ.js +0 -4
  305. package/src/console/ui-next/dist/assets/request-meta-BjFT7DNl.js +0 -1
  306. package/src/console/ui-next/dist/assets/shortcut-keys-CUNegEV4.js +0 -1
  307. package/src/console/ui-next/dist/assets/store-page-Qj9ThRE-.js +0 -41
  308. package/src/console/ui-next/dist/assets/trace-detail-sheet-Cmitq3da.js +0 -2
  309. package/src/console/ui-next/dist/assets/tree-expand-toggle-CbDIB-7x.js +0 -55
  310. package/src/console/ui-next/dist/assets/units-page-_PuYqFty.js +0 -1
  311. package/src/console/ui-next/dist/assets/use-vault-list-Cs45Czkt.js +0 -1
  312. package/src/console/ui-next/dist/assets/vault-page-BOMr0go9.js +0 -2
@@ -3,8 +3,9 @@
3
3
  *
4
4
  * Same concurrency physics as Signal's `once` delivery and Clock's postgres
5
5
  * CronStore: claim with `FOR UPDATE SKIP LOCKED`; a crashed holder's lease is
6
- * reclaimed lazily on the next claim attempt (no sweeper, no fencing token —
7
- * completed journal steps replay instead of re-running).
6
+ * reclaimed lazily on the next claim attempt (no sweeper). Each acquire bumps
7
+ * a fencing token; writes from a previous holder are rejected. Entries are
8
+ * append-only rows, not a rewritten JSON blob.
8
9
  */
9
10
 
10
11
  import {
@@ -20,12 +21,17 @@ import {
20
21
  IDEM_RENEW_SQL,
21
22
  IDEM_SELECT_SQL,
22
23
  } from "../kernel/idempotency-store.ts";
24
+ import type { DecisionLabelStore } from "../kernel/decision-label-store.ts";
25
+ import type { AgentEventStore } from "../kernel/agent-event-store.ts";
26
+ import { throwOke } from "../kernel/errors.ts";
27
+ import { lazyRequire } from "../kernel/lazy-require.ts";
23
28
  import {
24
29
  JOURNAL_DEFAULT_LEASE_MS,
25
30
  type JournalEntry,
26
31
  type JournalLeaseStore,
27
32
  type JournalRun,
28
33
  type JournalStore,
34
+ type JournalWriteFence,
29
35
  } from "../kernel/journal.ts";
30
36
  import {
31
37
  resolvePostgresUrl,
@@ -50,6 +56,19 @@ interface JournalDbRow {
50
56
  created_at: number;
51
57
  updated_at: number;
52
58
  tenant: string | null;
59
+ lease_token: number | null;
60
+ code_version: string | null;
61
+ }
62
+
63
+ /** One append-only journal entry row. */
64
+ interface JournalEntryDbRow {
65
+ run_id: string;
66
+ seq: number;
67
+ kind: string;
68
+ name: string | null;
69
+ resource: string | null;
70
+ value: string | null;
71
+ at: number;
53
72
  }
54
73
 
55
74
  /** Minimal SQL + transaction surface for the postgres journal store. */
@@ -82,7 +101,11 @@ const CLAIM_DUE_SQL = `SELECT * FROM oke_journal_runs WHERE status='sleeping' AN
82
101
  /** Boot-time orphan discovery: running/sleeping runs with no live lease. */
83
102
  const ORPHANS_SQL = `SELECT * FROM oke_journal_runs WHERE (status='running' OR status='sleeping' OR status='compensating') AND ((locked_by IS NULL) OR (lease_expires_at IS NOT NULL AND lease_expires_at<=?))`;
84
103
 
85
- const UPDATE_LEASE_SQL = `UPDATE oke_journal_runs SET locked_by = ?, lease_expires_at = ? WHERE id = ?`;
104
+ const UPDATE_LEASE_SQL = `UPDATE oke_journal_runs SET locked_by = ?, lease_expires_at = ?, lease_token = COALESCE(lease_token, 0) + 1 WHERE id = ?`;
105
+
106
+ const INSERT_ENTRY_SQL = `INSERT INTO oke_journal_entries (run_id, seq, kind, name, resource, value, at) VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT (run_id, seq) DO UPDATE SET kind = EXCLUDED.kind, name = EXCLUDED.name, resource = EXCLUDED.resource, value = EXCLUDED.value, at = EXCLUDED.at`;
107
+
108
+ const SELECT_ENTRIES_SQL = `SELECT * FROM oke_journal_entries WHERE run_id = ? ORDER BY seq ASC`;
86
109
 
87
110
  /** Guarded release — never clears another holder's lease. */
88
111
  const RELEASE_LEASE_SQL = `UPDATE oke_journal_runs SET locked_by = NULL, lease_expires_at = NULL WHERE id = ? AND locked_by = ?`;
@@ -168,9 +191,70 @@ function rowToRun(row: Record<string, unknown>): JournalRun {
168
191
  if (r.tenant !== null && r.tenant !== undefined && String(r.tenant).length > 0) {
169
192
  run.tenant = String(r.tenant);
170
193
  }
194
+ const leaseToken = numOrNull(r.lease_token);
195
+ if (leaseToken !== null) run.leaseToken = leaseToken;
196
+ if (
197
+ r.code_version !== null &&
198
+ r.code_version !== undefined &&
199
+ String(r.code_version).length > 0
200
+ ) {
201
+ run.codeVersion = String(r.code_version);
202
+ }
171
203
  return run;
172
204
  }
173
205
 
206
+ function entryToParams(runId: string, seq: number, entry: JournalEntry): unknown[] {
207
+ if (entry.kind === "step") {
208
+ return [runId, seq, "step", entry.name, null, JSON.stringify(entry.value ?? null), entry.at];
209
+ }
210
+ if (entry.kind === "sleep") {
211
+ return [
212
+ runId,
213
+ seq,
214
+ "sleep",
215
+ entry.label,
216
+ entry.duration,
217
+ JSON.stringify(entry.wakeAt),
218
+ entry.at,
219
+ ];
220
+ }
221
+ return [
222
+ runId,
223
+ seq,
224
+ "effect",
225
+ entry.effectKind,
226
+ entry.resource,
227
+ JSON.stringify(entry.value ?? null),
228
+ entry.at,
229
+ ];
230
+ }
231
+
232
+ function entryFromRow(row: Record<string, unknown>): JournalEntry {
233
+ const kind = String(row.kind);
234
+ const at = Number(row.at);
235
+ const value =
236
+ row.value === null || row.value === undefined ? null : JSON.parse(String(row.value));
237
+ if (kind === "step") {
238
+ return { kind: "step", name: String(row.name ?? ""), value, at };
239
+ }
240
+ if (kind === "sleep") {
241
+ return {
242
+ kind: "sleep",
243
+ label: String(row.name ?? ""),
244
+ duration: String(row.resource ?? ""),
245
+ wakeAt: Number(value),
246
+ at,
247
+ };
248
+ }
249
+ return {
250
+ kind: "effect",
251
+ effectKind: String(row.name ?? ""),
252
+ resource: String(row.resource ?? ""),
253
+ value,
254
+ at,
255
+ };
256
+ }
257
+
174
258
  function runToParams(run: JournalRun): unknown[] {
175
259
  return [
176
260
  run.id,
@@ -186,6 +270,8 @@ function runToParams(run: JournalRun): unknown[] {
186
270
  run.createdAt,
187
271
  run.updatedAt,
188
272
  run.tenant ?? null,
273
+ run.leaseToken ?? null,
274
+ run.codeVersion ?? null,
189
275
  ];
190
276
  }
191
277
 
@@ -206,6 +292,18 @@ async function ensureSchema(sql: PostgresJournalSql): Promise<void> {
206
292
  tenant TEXT
207
293
  )`);
208
294
  await sql.exec(`ALTER TABLE oke_journal_runs ADD COLUMN IF NOT EXISTS tenant TEXT`);
295
+ await sql.exec(`ALTER TABLE oke_journal_runs ADD COLUMN IF NOT EXISTS lease_token BIGINT`);
296
+ await sql.exec(`ALTER TABLE oke_journal_runs ADD COLUMN IF NOT EXISTS code_version TEXT`);
297
+ await sql.exec(`CREATE TABLE IF NOT EXISTS oke_journal_entries (
298
+ run_id TEXT NOT NULL,
299
+ seq BIGINT NOT NULL,
300
+ kind TEXT NOT NULL,
301
+ name TEXT,
302
+ resource TEXT,
303
+ value TEXT,
304
+ at BIGINT NOT NULL,
305
+ PRIMARY KEY (run_id, seq)
306
+ )`);
209
307
  await sql.exec(
210
308
  `CREATE INDEX IF NOT EXISTS oke_journal_runs_wake ON oke_journal_runs (status, wake_at)`,
211
309
  );
@@ -257,9 +355,49 @@ export function createPostgresJournalFake(): PostgresJournalSql & {
257
355
  created_at: number;
258
356
  expires_at: number;
259
357
  };
260
- type State = { rows: JournalDbRow[]; idem: IdemDbRow[] };
358
+ type LabelDbRow = {
359
+ decision_id: string;
360
+ question: string;
361
+ value: string;
362
+ propensity: number;
363
+ reviewer: string;
364
+ locale: string | null;
365
+ model: string | null;
366
+ tenant: string | null;
367
+ score: number | null;
368
+ loss: number | null;
369
+ raw: string | null;
370
+ at: number;
371
+ input: string | null;
372
+ review_id?: string | null;
373
+ };
374
+ type State = {
375
+ rows: JournalDbRow[];
376
+ idem: IdemDbRow[];
377
+ labels: LabelDbRow[];
378
+ drift: { decision_id: string; suspended: number; certified_at: number }[];
379
+ candidates: { decision_id: string; body: string }[];
380
+ agentRuns: { run_id: string; header: string; sweep_claim_at: number | null }[];
381
+ agentEvents: {
382
+ run_id: string;
383
+ seq: number;
384
+ event: string;
385
+ event_type: string | null;
386
+ event_name: string | null;
387
+ }[];
388
+ entries: JournalEntryDbRow[];
389
+ };
261
390
 
262
- let committed: State = { rows: [], idem: [] };
391
+ let committed: State = {
392
+ rows: [],
393
+ idem: [],
394
+ labels: [],
395
+ drift: [],
396
+ candidates: [],
397
+ agentRuns: [],
398
+ agentEvents: [],
399
+ entries: [],
400
+ };
263
401
  let active: { state: State; locked: Set<string>; done: boolean } | null = null;
264
402
  /** Run ids held by other active transactions (SKIP LOCKED). */
265
403
  const heldByTxn = new Set<string>();
@@ -274,6 +412,12 @@ export function createPostgresJournalFake(): PostgresJournalSql & {
274
412
  return {
275
413
  rows: s.rows.map((r) => ({ ...r })),
276
414
  idem: s.idem.map((r) => ({ ...r })),
415
+ labels: s.labels.map((r) => ({ ...r })),
416
+ drift: s.drift.map((r) => ({ ...r })),
417
+ candidates: s.candidates.map((r) => ({ ...r })),
418
+ agentRuns: s.agentRuns.map((r) => ({ ...r })),
419
+ agentEvents: s.agentEvents.map((r) => ({ ...r })),
420
+ entries: s.entries.map((r) => ({ ...r })),
277
421
  };
278
422
  }
279
423
 
@@ -376,6 +520,73 @@ export function createPostgresJournalFake(): PostgresJournalSql & {
376
520
  .map((row) => ({ ...row }) as Record<string, unknown>);
377
521
  }
378
522
 
523
+ if (/FROM\s+oke_decision_labels/i.test(text)) {
524
+ let labels = state.labels;
525
+ let arg = 0;
526
+ if (/decision_id\s*=\s*\?/i.test(text)) {
527
+ const id = String(params[arg++] ?? "");
528
+ labels = labels.filter((row) => row.decision_id === id);
529
+ }
530
+ if (/tenant\s+IS\s+NOT\s+DISTINCT\s+FROM\s+\?/i.test(text)) {
531
+ const tenant = params[arg] ?? null;
532
+ labels = labels.filter((row) => row.tenant === tenant);
533
+ }
534
+ return labels.map((row) => ({ ...row }));
535
+ }
536
+
537
+ if (/FROM\s+oke_decision_drift/i.test(text)) {
538
+ return state.drift.map((row) => ({ ...row }));
539
+ }
540
+
541
+ if (/FROM\s+oke_decision_candidates/i.test(text)) {
542
+ if (/decision_id\s*=\s*\?/i.test(text)) {
543
+ const id = String(params[0] ?? "");
544
+ return state.candidates
545
+ .filter((row) => row.decision_id === id)
546
+ .map((row) => ({ ...row }));
547
+ }
548
+ return state.candidates.map((row) => ({ ...row }));
549
+ }
550
+
551
+ if (/FROM\s+oke_agent_run\b/i.test(text)) {
552
+ if (!/WHERE/i.test(text)) return state.agentRuns.map((row) => ({ ...row }));
553
+ const id = String(params[0] ?? "");
554
+ return state.agentRuns.filter((row) => row.run_id === id).map((row) => ({ ...row }));
555
+ }
556
+
557
+ if (/FROM\s+oke_agent_event\b/i.test(text)) {
558
+ const id = String(params[0] ?? "");
559
+ if (/MAX\s*\(\s*seq\s*\)/i.test(text)) {
560
+ const max = state.agentEvents
561
+ .filter((row) => row.run_id === id)
562
+ .reduce((highest, row) => Math.max(highest, row.seq), 0);
563
+ return [{ max_seq: max }];
564
+ }
565
+ if (/event_type\s*=/i.test(text)) {
566
+ const eventType = String(params[1] ?? "");
567
+ const eventName = String(params[2] ?? "");
568
+ return state.agentEvents
569
+ .filter(
570
+ (row) =>
571
+ row.run_id === id && row.event_type === eventType && row.event_name === eventName,
572
+ )
573
+ .map((row) => ({ ...row }));
574
+ }
575
+ const after = /seq\s*>\s*\?/i.test(text) ? Number(params[1] ?? 0) : 0;
576
+ return state.agentEvents
577
+ .filter((row) => row.run_id === id && row.seq > after)
578
+ .sort((a, b) => a.seq - b.seq)
579
+ .map((row) => ({ ...row }));
580
+ }
581
+
582
+ if (/FROM\s+oke_journal_entries/i.test(text)) {
583
+ const runId = String(params[0] ?? "");
584
+ return state.entries
585
+ .filter((row) => row.run_id === runId)
586
+ .sort((a, b) => a.seq - b.seq)
587
+ .map((row) => ({ ...row }));
588
+ }
589
+
379
590
  throw new Error(`postgres journal fake: unsupported query: ${sql}`);
380
591
  },
381
592
  async exec(sql, params = []) {
@@ -413,6 +624,14 @@ export function createPostgresJournalFake(): PostgresJournalSql & {
413
624
  created_at: Number(record.created_at ?? 0),
414
625
  updated_at: Number(record.updated_at ?? 0),
415
626
  tenant: (record.tenant as string | null) ?? null,
627
+ lease_token:
628
+ record.lease_token === undefined || record.lease_token === null
629
+ ? (state.rows.find((r) => r.id === String(record.id))?.lease_token ?? null)
630
+ : Number(record.lease_token),
631
+ code_version:
632
+ record.code_version === undefined || record.code_version === null
633
+ ? (state.rows.find((r) => r.id === String(record.id))?.code_version ?? null)
634
+ : String(record.code_version),
416
635
  };
417
636
  const idx = state.rows.findIndex((r) => r.id === next.id);
418
637
  if (idx >= 0) state.rows[idx] = next;
@@ -421,15 +640,33 @@ export function createPostgresJournalFake(): PostgresJournalSql & {
421
640
  }
422
641
 
423
642
  const updLease =
424
- /^UPDATE\s+oke_journal_runs\s+SET\s+locked_by\s*=\s*\?,\s*lease_expires_at\s*=\s*\?\s+WHERE\s+id\s*=\s*\?\s*$/i.exec(
643
+ /^UPDATE\s+oke_journal_runs\s+SET\s+locked_by\s*=\s*\?,\s*lease_expires_at\s*=\s*\?/i.exec(
425
644
  text,
426
645
  );
427
- if (updLease) {
428
- const row = state.rows.find((r) => r.id === params[2]);
646
+ if (updLease && /WHERE\s+id\s*=\s*\?/i.test(text) && !/locked_by\s*=\s*NULL/i.test(text)) {
647
+ const id = String(params[params.length - 1]);
648
+ const row = state.rows.find((r) => r.id === id);
429
649
  if (!row) return { changes: 0 };
430
650
  row.locked_by = params[0] === null ? null : String(params[0]);
431
651
  row.lease_expires_at =
432
652
  params[1] === null || params[1] === undefined ? null : Number(params[1]);
653
+ if (/lease_token/i.test(text)) row.lease_token = (row.lease_token ?? 0) + 1;
654
+ return { changes: 1 };
655
+ }
656
+
657
+ if (/^INSERT\s+INTO\s+oke_journal_entries\b/i.test(text)) {
658
+ const row: JournalEntryDbRow = {
659
+ run_id: String(params[0]),
660
+ seq: Number(params[1]),
661
+ kind: String(params[2]),
662
+ name: params[3] === null || params[3] === undefined ? null : String(params[3]),
663
+ resource: params[4] === null || params[4] === undefined ? null : String(params[4]),
664
+ value: params[5] === null || params[5] === undefined ? null : String(params[5]),
665
+ at: Number(params[6]),
666
+ };
667
+ const idx = state.entries.findIndex((e) => e.run_id === row.run_id && e.seq === row.seq);
668
+ if (idx >= 0) state.entries[idx] = row;
669
+ else state.entries.push(row);
433
670
  return { changes: 1 };
434
671
  }
435
672
 
@@ -539,6 +776,112 @@ export function createPostgresJournalFake(): PostgresJournalSql & {
539
776
  row.run_id = String(params[0]);
540
777
  return { changes: 1 };
541
778
  }
779
+ if (/^INSERT\s+INTO\s+oke_decision_labels\b/i.test(text)) {
780
+ state.labels.push({
781
+ decision_id: String(params[0]),
782
+ question: String(params[1]),
783
+ value: String(params[2]),
784
+ propensity: Number(params[3]),
785
+ reviewer: String(params[4]),
786
+ locale: params[5] === null || params[5] === undefined ? null : String(params[5]),
787
+ model: params[6] === null || params[6] === undefined ? null : String(params[6]),
788
+ tenant: params[7] === null || params[7] === undefined ? null : String(params[7]),
789
+ score: params[8] === null || params[8] === undefined ? null : Number(params[8]),
790
+ loss: params[9] === null || params[9] === undefined ? null : Number(params[9]),
791
+ raw: params[10] === null || params[10] === undefined ? null : String(params[10]),
792
+ at: Number(params[11]),
793
+ input: params[12] === null || params[12] === undefined ? null : String(params[12]),
794
+ review_id: params[13] === null || params[13] === undefined ? null : String(params[13]),
795
+ });
796
+ return { changes: 1 };
797
+ }
798
+
799
+ if (/^INSERT\s+INTO\s+oke_decision_candidates\b/i.test(text)) {
800
+ const decisionId = String(params[0]);
801
+ const body = String(params[1]);
802
+ const idx = state.candidates.findIndex((row) => row.decision_id === decisionId);
803
+ if (idx >= 0) state.candidates[idx] = { decision_id: decisionId, body };
804
+ else state.candidates.push({ decision_id: decisionId, body });
805
+ return { changes: 1 };
806
+ }
807
+
808
+ if (/^UPDATE\s+oke_decision_drift\b/i.test(text)) return { changes: 0 };
809
+
810
+ if (/^DELETE\s+FROM\s+oke_decision_drift\b/i.test(text)) {
811
+ const id = String(params[0] ?? "");
812
+ const before = state.drift.length;
813
+ state.drift = state.drift.filter((row) => row.decision_id !== id);
814
+ return { changes: before - state.drift.length };
815
+ }
816
+
817
+ if (/^INSERT\s+INTO\s+oke_decision_drift\b/i.test(text)) {
818
+ const decisionId = String(params[0]);
819
+ const next = {
820
+ decision_id: decisionId,
821
+ suspended: Number(params[1]),
822
+ certified_at: Number(params[2] ?? 0),
823
+ };
824
+ const idx = state.drift.findIndex((row) => row.decision_id === decisionId);
825
+ if (idx >= 0) state.drift[idx] = next;
826
+ else state.drift.push(next);
827
+ return { changes: 1 };
828
+ }
829
+
830
+ if (/^UPDATE\s+oke_agent_run\s+SET\s+sweep_claim_at\b/i.test(text)) {
831
+ const until = Number(params[0]);
832
+ const runId = String(params[1] ?? "");
833
+ const now = Number(params[2]);
834
+ const row = state.agentRuns.find((candidate) => candidate.run_id === runId);
835
+ if (!row) return { changes: 0 };
836
+ if (row.sweep_claim_at !== null && row.sweep_claim_at > now) return { changes: 0 };
837
+ row.sweep_claim_at = until;
838
+ return { changes: 1 };
839
+ }
840
+
841
+ if (/^INSERT\s+INTO\s+oke_agent_run\b/i.test(text)) {
842
+ const runId = String(params[0]);
843
+ const existing = state.agentRuns.find((row) => row.run_id === runId);
844
+ const next = {
845
+ run_id: runId,
846
+ header: String(params[1]),
847
+ sweep_claim_at: existing?.sweep_claim_at ?? null,
848
+ };
849
+ const idx = state.agentRuns.findIndex((row) => row.run_id === runId);
850
+ if (idx >= 0) state.agentRuns[idx] = next;
851
+ else state.agentRuns.push(next);
852
+ return { changes: 1 };
853
+ }
854
+
855
+ if (/^INSERT\s+INTO\s+oke_agent_event\b/i.test(text)) {
856
+ const runId = String(params[0]);
857
+ const seq = Number(params[1]);
858
+ if (state.agentEvents.some((row) => row.run_id === runId && row.seq === seq)) {
859
+ throw new Error(`duplicate key value violates unique constraint "oke_agent_event_pkey"`);
860
+ }
861
+ state.agentEvents.push({
862
+ run_id: runId,
863
+ seq,
864
+ event: String(params[2]),
865
+ event_type: params[3] === undefined || params[3] === null ? null : String(params[3]),
866
+ event_name: params[4] === undefined || params[4] === null ? null : String(params[4]),
867
+ });
868
+ return { changes: 1 };
869
+ }
870
+
871
+ if (/^DELETE\s+FROM\s+oke_agent_event\b/i.test(text)) {
872
+ const id = String(params[0] ?? "");
873
+ const before = state.agentEvents.length;
874
+ state.agentEvents = state.agentEvents.filter((row) => row.run_id !== id);
875
+ return { changes: before - state.agentEvents.length };
876
+ }
877
+
878
+ if (/^DELETE\s+FROM\s+oke_agent_run\b/i.test(text)) {
879
+ const id = String(params[0] ?? "");
880
+ const before = state.agentRuns.length;
881
+ state.agentRuns = state.agentRuns.filter((row) => row.run_id !== id);
882
+ return { changes: before - state.agentRuns.length };
883
+ }
884
+
542
885
  if (text === IDEM_FORFEIT_SQL) {
543
886
  const row = state.idem.find(
544
887
  (candidate) =>
@@ -596,15 +939,72 @@ export function createPostgresJournalFake(): PostgresJournalSql & {
596
939
  return api;
597
940
  }
598
941
 
599
- const UPSERT_SQL = `INSERT INTO oke_journal_runs (id, flow, input, status, entries, wake_at, error, output, locked_by, lease_expires_at, created_at, updated_at, tenant) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT (id) DO UPDATE SET flow = EXCLUDED.flow, input = EXCLUDED.input, status = EXCLUDED.status, entries = EXCLUDED.entries, wake_at = EXCLUDED.wake_at, error = EXCLUDED.error, output = EXCLUDED.output, locked_by = EXCLUDED.locked_by, lease_expires_at = EXCLUDED.lease_expires_at, created_at = EXCLUDED.created_at, updated_at = EXCLUDED.updated_at, tenant = EXCLUDED.tenant`;
942
+ const UPSERT_SQL = `INSERT INTO oke_journal_runs (id, flow, input, status, entries, wake_at, error, output, locked_by, lease_expires_at, created_at, updated_at, tenant, lease_token, code_version) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT (id) DO UPDATE SET flow = EXCLUDED.flow, input = EXCLUDED.input, status = EXCLUDED.status, entries = EXCLUDED.entries, wake_at = EXCLUDED.wake_at, error = EXCLUDED.error, output = EXCLUDED.output, locked_by = EXCLUDED.locked_by, lease_expires_at = EXCLUDED.lease_expires_at, created_at = EXCLUDED.created_at, updated_at = EXCLUDED.updated_at, tenant = EXCLUDED.tenant, lease_token = EXCLUDED.lease_token, code_version = EXCLUDED.code_version`;
600
943
 
601
944
  /** Postgres journal store with run-level lease coordination. */
602
945
  export type PostgresJournalStore = JournalStore &
603
946
  JournalLeaseStore & {
604
947
  readonly sql: PostgresJournalSql;
948
+ /** Bytes written for entry rows plus header entry payloads. */
949
+ readonly writeBytes: number;
605
950
  close(): Promise<void>;
606
951
  };
607
952
 
953
+ /**
954
+ * Open decision tables on first use so a journal without decisions stays empty.
955
+ *
956
+ * @param sql - Journal SQL client
957
+ */
958
+ function loadDecisionLabelModule(): typeof import("../kernel/decision-label-store.ts") {
959
+ return lazyRequire(`${import.meta.dir}/../kernel`, ["decision", "label", "store"].join("-"));
960
+ }
961
+
962
+ function loadAgentEventModule(): typeof import("../kernel/agent-event-store.ts") {
963
+ return lazyRequire(`${import.meta.dir}/../kernel`, ["agent", "event", "store"].join("-"));
964
+ }
965
+
966
+ function lazyDecisionLabels(sql: PostgresJournalSql): DecisionLabelStore {
967
+ let pending: Promise<DecisionLabelStore> | undefined;
968
+ const ready = (): Promise<DecisionLabelStore> => {
969
+ pending ??= loadDecisionLabelModule().createPostgresDecisionLabelStore(sql);
970
+ return pending;
971
+ };
972
+ return {
973
+ insert: (label, at) => ready().then((store) => store.insert(label, at)),
974
+ list: (decision, tenant) => ready().then((store) => store.list(decision, tenant)),
975
+ drift: () => ready().then((store) => store.drift()),
976
+ setDrift: (decision, record) => ready().then((store) => store.setDrift(decision, record)),
977
+ putCandidate: (decision, entry) => ready().then((store) => store.putCandidate(decision, entry)),
978
+ getCandidate: (decision) => ready().then((store) => store.getCandidate(decision)),
979
+ listCandidates: () => ready().then((store) => store.listCandidates()),
980
+ };
981
+ }
982
+
983
+ /**
984
+ * Open the agent event tables on first use.
985
+ *
986
+ * @param sql - Journal SQL client
987
+ */
988
+ function lazyAgentEvents(sql: PostgresJournalSql): AgentEventStore {
989
+ let pending: Promise<AgentEventStore> | undefined;
990
+ const ready = (): Promise<AgentEventStore> => {
991
+ pending ??= loadAgentEventModule().createPostgresAgentEventStore(sql);
992
+ return pending;
993
+ };
994
+ return {
995
+ read: (runId) => ready().then((store) => store.read(runId)),
996
+ readAfter: (runId, afterSeq) => ready().then((store) => store.readAfter(runId, afterSeq)),
997
+ maxSeq: (runId) => ready().then((store) => store.maxSeq(runId)),
998
+ readHeader: (runId) => ready().then((store) => store.readHeader(runId)),
999
+ truncated: (runId) => ready().then((store) => store.truncated(runId)),
1000
+ claim: (runId) => ready().then((store) => store.claim(runId)),
1001
+ listHeaders: () => ready().then((store) => store.listHeaders()),
1002
+ writeHeader: (header) => ready().then((store) => store.writeHeader(header)),
1003
+ append: (runId, row) => ready().then((store) => store.append(runId, row)),
1004
+ remove: (runId) => ready().then((store) => store.remove(runId)),
1005
+ };
1006
+ }
1007
+
608
1008
  /**
609
1009
  * Open a postgres-backed JournalStore (multi-host durable-run coordination).
610
1010
  *
@@ -622,20 +1022,77 @@ export async function createPostgresJournalStore(
622
1022
 
623
1023
  await ensureSchema(sql);
624
1024
 
1025
+ let writeBytes = 0;
1026
+
1027
+ async function hydrate(row: Record<string, unknown>): Promise<JournalRun> {
1028
+ const run = rowToRun(row);
1029
+ const entryRows = await sql.query(SELECT_ENTRIES_SQL, [run.id]);
1030
+ if (entryRows.length > 0) {
1031
+ (run as { entries: JournalEntry[] }).entries = entryRows.map((entry) => entryFromRow(entry));
1032
+ return run;
1033
+ }
1034
+ if (run.entries.length > 0) {
1035
+ for (let seq = 0; seq < run.entries.length; seq++) {
1036
+ const entry = run.entries[seq];
1037
+ if (!entry) continue;
1038
+ const params = entryToParams(run.id, seq, entry);
1039
+ writeBytes += JSON.stringify(entry).length;
1040
+ await sql.exec(INSERT_ENTRY_SQL, params);
1041
+ }
1042
+ }
1043
+ return run;
1044
+ }
1045
+
1046
+ function assertFence(run: JournalRun | undefined, fence: JournalWriteFence | undefined): void {
1047
+ if (!fence || !run) return;
1048
+ const expired = run.leaseExpiresAt !== undefined && run.leaseExpiresAt <= fence.now;
1049
+ if (run.lockedBy !== fence.lockedBy || run.leaseToken !== fence.leaseToken || expired) {
1050
+ throwOke("JOURNAL_STALE_LEASE", { runId: run.id });
1051
+ }
1052
+ }
1053
+
625
1054
  const store: PostgresJournalStore = {
626
1055
  sql,
1056
+ get writeBytes() {
1057
+ return writeBytes;
1058
+ },
627
1059
  idempotency: createPostgresIdempotencyStore(sql),
1060
+ decisions: lazyDecisionLabels(sql),
1061
+ agentEvents: lazyAgentEvents(sql),
628
1062
  async get(runId) {
629
1063
  const rows = await sql.query(`SELECT * FROM oke_journal_runs WHERE id = ?`, [runId]);
630
1064
  if (!rows[0]) return undefined;
631
- return rowToRun(rows[0]);
1065
+ return hydrate(rows[0]);
632
1066
  },
633
- async put(run) {
1067
+ async put(run, fence) {
1068
+ if (fence) {
1069
+ const rows = await sql.query(`SELECT * FROM oke_journal_runs WHERE id = ?`, [run.id]);
1070
+ assertFence(rows[0] ? rowToRun(rows[0]) : undefined, fence);
1071
+ }
1072
+ writeBytes += JSON.stringify(run.entries).length;
634
1073
  await sql.exec(UPSERT_SQL, runToParams(run));
1074
+ for (let seq = 0; seq < run.entries.length; seq++) {
1075
+ const entry = run.entries[seq];
1076
+ if (!entry) continue;
1077
+ writeBytes += JSON.stringify(entry).length;
1078
+ await sql.exec(INSERT_ENTRY_SQL, entryToParams(run.id, seq, entry));
1079
+ }
1080
+ },
1081
+ async appendEntry(runId, seq, entry, fence) {
1082
+ if (fence) {
1083
+ const rows = await sql.query(`SELECT * FROM oke_journal_runs WHERE id = ?`, [runId]);
1084
+ const current = rows[0] ? rowToRun(rows[0]) : undefined;
1085
+ if (!current) throw new Error(`journal: run "${runId}" not found`);
1086
+ assertFence(current, fence);
1087
+ }
1088
+ writeBytes += JSON.stringify(entry).length;
1089
+ await sql.exec(INSERT_ENTRY_SQL, entryToParams(runId, seq, entry));
635
1090
  },
636
1091
  async list() {
637
1092
  const rows = await sql.query(`SELECT * FROM oke_journal_runs`);
638
- return rows.map((r) => rowToRun(r));
1093
+ const runs: JournalRun[] = [];
1094
+ for (const row of rows) runs.push(await hydrate(row));
1095
+ return runs;
639
1096
  },
640
1097
  async acquireLease(runId, instanceId, now, leaseMs) {
641
1098
  return sql.begin(async (tx) => {
@@ -645,6 +1102,43 @@ export async function createPostgresJournalStore(
645
1102
  return true;
646
1103
  });
647
1104
  },
1105
+ async cas(runId, instanceId, now, leaseMs, update) {
1106
+ return sql.begin(async (tx) => {
1107
+ const claimed = await tx.query(CLAIM_LEASE_SQL, [runId, instanceId, now]);
1108
+ const row = claimed[0];
1109
+ if (!row) {
1110
+ const exists = await tx.query(`SELECT * FROM oke_journal_runs WHERE id = ?`, [runId]);
1111
+ const held = exists[0];
1112
+ if (!held) return "missing";
1113
+ const current = rowToRun(held);
1114
+ return { lease: true, leaseExpiresAt: current.leaseExpiresAt };
1115
+ }
1116
+ const current = rowToRun(row);
1117
+ const entryRows = await tx.query(SELECT_ENTRIES_SQL, [runId]);
1118
+ if (entryRows.length > 0) {
1119
+ (current as { entries: JournalEntry[] }).entries = entryRows.map((entry) =>
1120
+ entryFromRow(entry),
1121
+ );
1122
+ }
1123
+ const next = update(current) ?? current;
1124
+ const renew =
1125
+ current.lockedBy === instanceId &&
1126
+ current.leaseExpiresAt !== undefined &&
1127
+ current.leaseExpiresAt > now &&
1128
+ current.leaseToken !== undefined;
1129
+ next.lockedBy = instanceId;
1130
+ next.leaseExpiresAt = now + leaseMs;
1131
+ next.leaseToken = renew ? current.leaseToken : (current.leaseToken ?? 0) + 1;
1132
+ for (let seq = 0; seq < next.entries.length; seq++) {
1133
+ const entry = next.entries[seq];
1134
+ if (!entry) continue;
1135
+ await tx.exec(INSERT_ENTRY_SQL, entryToParams(runId, seq, entry));
1136
+ }
1137
+ const header = { ...next, entries: [] as JournalEntry[] };
1138
+ await tx.exec(UPSERT_SQL, runToParams(header));
1139
+ return "ok";
1140
+ });
1141
+ },
648
1142
  async releaseLease(runId, instanceId) {
649
1143
  await sql.exec(RELEASE_LEASE_SQL, [runId, instanceId]);
650
1144
  },
@@ -654,7 +1148,11 @@ export async function createPostgresJournalStore(
654
1148
  const row = rows[0];
655
1149
  if (!row) return undefined;
656
1150
  await tx.exec(UPDATE_LEASE_SQL, [instanceId, now + leaseMs, String(row.id)]);
657
- return rowToRun(row);
1151
+ const run = rowToRun(row);
1152
+ run.lockedBy = instanceId;
1153
+ run.leaseExpiresAt = now + leaseMs;
1154
+ run.leaseToken = (run.leaseToken ?? 0) + 1;
1155
+ return run;
658
1156
  });
659
1157
  },
660
1158
  async listOrphans(now) {
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Memory SQL accepts the DDL identity boot issues on the shared connection.
3
+ */
4
+
5
+ import { describe, expect, test } from "bun:test";
6
+ import { ensureIdentityTables } from "../auth/identity-sql.ts";
7
+ import { memorySqlDriver } from "./memory.ts";
8
+
9
+ describe("memory sql DDL", () => {
10
+ test("CREATE INDEX IF NOT EXISTS does not reject identity tables", async () => {
11
+ const conn = await memorySqlDriver.connect({ role: "primary" });
12
+ await ensureIdentityTables(conn);
13
+ await ensureIdentityTables(conn);
14
+ const rows = await conn.query("SELECT id FROM oke_credentials");
15
+ expect(rows).toEqual([]);
16
+ await conn.close();
17
+ });
18
+ });