okengine 0.21.1 → 0.23.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 (342) hide show
  1. package/AGENTS.md +11 -4
  2. package/LICENSE +202 -0
  3. package/NOTICE +2 -0
  4. package/README.md +15 -9
  5. package/manifest.v1.schema.json +54 -0
  6. package/package.json +53 -38
  7. package/site/content/docs/ai/mcp.mdx +13 -9
  8. package/site/content/docs/ai/skills.mdx +2 -2
  9. package/site/content/docs/client/calling.mdx +27 -19
  10. package/site/content/docs/client/index.mdx +1 -1
  11. package/site/content/docs/elements/ai/agents.mdx +61 -13
  12. package/site/content/docs/elements/ai/decide.mdx +536 -0
  13. package/site/content/docs/elements/ai/events.mdx +373 -0
  14. package/site/content/docs/elements/ai/index.mdx +9 -3
  15. package/site/content/docs/elements/ai/meta.json +1 -1
  16. package/site/content/docs/elements/ai/prompts.mdx +4 -2
  17. package/site/content/docs/elements/channel/email.mdx +1 -1
  18. package/site/content/docs/elements/channel/index.mdx +1 -1
  19. package/site/content/docs/elements/flow/http.mdx +4 -4
  20. package/site/content/docs/elements/flow/index.mdx +3 -3
  21. package/site/content/docs/elements/flow/meta.json +1 -1
  22. package/site/content/docs/elements/gate/auth.mdx +4 -1
  23. package/site/content/docs/elements/gate/tenancy.mdx +1 -1
  24. package/site/content/docs/elements/store/index.mdx +1 -1
  25. package/site/content/docs/elements/store/kv.mdx +5 -5
  26. package/site/content/docs/elements/vault/rotation.mdx +15 -0
  27. package/site/content/docs/plugins/otp.mdx +31 -31
  28. package/site/content/docs/recipes/dragonfly.mdx +5 -5
  29. package/site/content/docs/recipes/index.mdx +6 -6
  30. package/site/content/docs/recipes/mailpit.mdx +1 -1
  31. package/site/content/docs/recipes/meilisearch.mdx +1 -1
  32. package/site/content/docs/recipes/pgdog.mdx +1 -1
  33. package/site/content/docs/recipes/postgres.mdx +12 -8
  34. package/site/content/docs/recipes/redis.mdx +4 -4
  35. package/site/content/docs/reference/cli.mdx +2 -1
  36. package/site/content/docs/reference/configuration.mdx +11 -9
  37. package/site/content/docs/reference/errors.mdx +12 -2
  38. package/site/content/docs/reference/fx.mdx +35 -21
  39. package/site/content/docs/reference/idempotency.mdx +192 -0
  40. package/site/content/docs/reference/index.mdx +6 -1
  41. package/site/content/docs/reference/meta.json +2 -1
  42. package/site/content/docs/understand/meta.json +1 -1
  43. package/site/content/docs/{elements/flow → understand}/routing.mdx +11 -10
  44. package/site/content/docs/understand/the-architecture.mdx +14 -12
  45. package/site/content/docs/understand/try-it.mdx +3 -3
  46. package/src/auth/api-key-sql.test.ts +44 -0
  47. package/src/auth/api-key-sql.ts +1 -0
  48. package/src/auth/api-keys.ts +30 -12
  49. package/src/auth/config.ts +6 -2
  50. package/src/auth/gate-auth.test.ts +20 -0
  51. package/src/cli/agents-md.test.ts +25 -0
  52. package/src/cli/ask-vault-gaps.test.ts +38 -1
  53. package/src/cli/ask-vault-gaps.ts +35 -2
  54. package/src/cli/competitor-mention-removal.test.ts +3 -3
  55. package/src/cli/decide.ts +174 -0
  56. package/src/cli/decision-lock-watch.test.ts +45 -0
  57. package/src/cli/decision-lock-watch.ts +51 -0
  58. package/src/cli/dev-app-runner.ts +34 -8
  59. package/src/cli/dev-auth-secret.test.ts +49 -0
  60. package/src/cli/dev-auth-secret.ts +49 -0
  61. package/src/cli/dev-hot.test.ts +41 -0
  62. package/src/cli/dev-hot.ts +48 -0
  63. package/src/cli/dev.test.ts +1 -1
  64. package/src/cli/dev.ts +24 -6
  65. package/src/cli/docker-cli.test.ts +1 -1
  66. package/src/cli/eval.ts +142 -1
  67. package/src/cli/index.ts +14 -1
  68. package/src/cli/load-config.images.test.ts +12 -12
  69. package/src/cli/load-config.ts +2 -2
  70. package/src/cli/mcp-from-console.ts +48 -0
  71. package/src/cli/registry.ts +35 -1
  72. package/src/cli/vault-cmd.test.ts +23 -0
  73. package/src/cli/vault-cmd.ts +7 -0
  74. package/src/client/agent.ts +307 -0
  75. package/src/client/create.ts +18 -1
  76. package/src/client/transport.test.ts +245 -0
  77. package/src/client/transport.ts +133 -19
  78. package/src/client/types.ts +29 -3
  79. package/src/client-react/index.ts +7 -1
  80. package/src/client-react/use-agent-run.test.ts +271 -0
  81. package/src/client-react/use-agent-run.ts +193 -0
  82. package/src/compiler/ai-approval.test.ts +36 -0
  83. package/src/compiler/ai-repair.test.ts +22 -0
  84. package/src/compiler/decisions.extract.test.ts +164 -0
  85. package/src/compiler/effects-infer.ts +753 -17
  86. package/src/compiler/extract.test.ts +37 -0
  87. package/src/compiler/extract.ts +381 -24
  88. package/src/compiler/fixtures/skyport.expected.json +24 -0
  89. package/src/compiler/fx-follow.test.ts +164 -0
  90. package/src/compiler/fx-index.ts +399 -0
  91. package/src/compiler/response.ts +35 -2
  92. package/src/config/index.ts +21 -3
  93. package/src/console/server/ai-runs-flows.ts +269 -0
  94. package/src/console/server/ai-runs.test.ts +323 -0
  95. package/src/console/server/ai-runs.ts +562 -0
  96. package/src/console/server/ai.ts +12 -1
  97. package/src/console/server/app.ts +10 -1
  98. package/src/console/server/decisions-flows.ts +157 -0
  99. package/src/console/server/decisions.test.ts +66 -0
  100. package/src/console/server/decisions.ts +229 -0
  101. package/src/console/server/flows-invoke.test.ts +2 -2
  102. package/src/console/server/flows.ts +50 -2
  103. package/src/console/server/invoke-user-flow.ts +9 -0
  104. package/src/console/server/runs-ingest.ts +16 -0
  105. package/src/console/server/serve.ts +5 -0
  106. package/src/console/server/state.ts +25 -1
  107. package/src/console/server/store-stats.test.ts +47 -0
  108. package/src/console/server/store-stats.ts +92 -18
  109. package/src/console/ui-next/dist/assets/access-page-DDKkhT9v.js +4 -0
  110. package/src/console/ui-next/dist/assets/agent-disclosure-62Xts8Xi.js +1 -0
  111. package/src/console/ui-next/dist/assets/{cache-glyph-92uM5MO7.js → cache-glyph-DyeoKHKA.js} +1 -1
  112. package/src/console/ui-next/dist/assets/{call-pii-button-DtGVPtcs.js → call-pii-button-DvfpLXsU.js} +1 -1
  113. package/src/console/ui-next/dist/assets/{collapsible-DN7l6zmC.js → collapsible-BY03SeCg.js} +1 -1
  114. package/src/console/ui-next/dist/assets/confirm-sheet-DZfuJXP4.js +1 -0
  115. package/src/console/ui-next/dist/assets/copy-inline-button-BTjPVjRT.js +1 -0
  116. package/src/console/ui-next/dist/assets/decisions-page-CgkKlA56.js +1 -0
  117. package/src/console/ui-next/dist/assets/detail-header-Dk5LipF6.js +1 -0
  118. package/src/console/ui-next/dist/assets/dropdown-menu-CRGrj5JU.js +1 -0
  119. package/src/console/ui-next/dist/assets/duration-tone-Y6HLjyJ5.js +9 -0
  120. package/src/console/ui-next/dist/assets/element-icons-C6t8Vlml.js +1 -0
  121. package/src/console/ui-next/dist/assets/explorer-chrome-Dslh2yE8.js +1 -0
  122. package/src/console/ui-next/dist/assets/explorer-empty-CIVg7Jgb.js +1 -0
  123. package/src/console/ui-next/dist/assets/flows-page-DLPfNy-_.js +1 -0
  124. package/src/console/ui-next/dist/assets/{highlighted-json-BlAEVgNW.js → highlighted-json-D7nNzpgB.js} +1 -1
  125. package/src/console/ui-next/dist/assets/{http-method-BDf7OAHv.js → http-method-DdL19zzh.js} +1 -1
  126. package/src/console/ui-next/dist/assets/index-BlrN49Hs.css +2 -0
  127. package/src/console/ui-next/dist/assets/index-C0lc_s9d.js +58 -0
  128. package/src/console/ui-next/dist/assets/observability-page-CRlcyLCC.js +8 -0
  129. package/src/console/ui-next/dist/assets/{replica-lag-B3GLNVfF.js → replica-lag-Bb3FWUu9.js} +7 -7
  130. package/src/console/ui-next/dist/assets/request-meta-DYBMWhX8.js +1 -0
  131. package/src/console/ui-next/dist/assets/shortcut-keys-DTE1sjTC.js +1 -0
  132. package/src/console/ui-next/dist/assets/store-page-CcE-SXC8.js +41 -0
  133. package/src/console/ui-next/dist/assets/trace-detail-sheet-CKMSEQ4s.js +2 -0
  134. package/src/console/ui-next/dist/assets/tree-expand-toggle-QOfk0M0H.js +55 -0
  135. package/src/console/ui-next/dist/assets/units-page-DKC1CDDd.js +1 -0
  136. package/src/console/ui-next/dist/assets/use-vault-list-BAyQG8Re.js +1 -0
  137. package/src/console/ui-next/dist/assets/vault-page-B54v4Yit.js +2 -0
  138. package/src/console/ui-next/dist/index.html +4 -3
  139. package/src/console/ui-next/seed-parked-approval.ts +126 -0
  140. package/src/console/ui-next/src/client.ts +224 -1
  141. package/src/console/ui-next/src/components/explorer/detail-header.tsx +9 -0
  142. package/src/console/ui-next/src/components/explorer/explorer-start-toggle.tsx +5 -1
  143. package/src/console/ui-next/src/components/ui/sheet-form.tsx +21 -12
  144. package/src/console/ui-next/src/features/flows/decisions/decisions-page.tsx +216 -0
  145. package/src/console/ui-next/src/features/flows/decisions/resolve.test.ts +48 -0
  146. package/src/console/ui-next/src/features/flows/decisions/resolve.ts +111 -0
  147. package/src/console/ui-next/src/features/flows/flows-page.tsx +10 -0
  148. package/src/console/ui-next/src/features/flows/graph/element-map.ts +1 -0
  149. package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +6 -0
  150. package/src/console/ui-next/src/features/flows/traces/effect-summary.test.ts +24 -0
  151. package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
  152. package/src/console/ui-next/src/features/flows/traces/http-status.ts +58 -0
  153. package/src/console/ui-next/src/features/flows/traces/request-client-card.tsx +355 -0
  154. package/src/console/ui-next/src/features/flows/traces/request-client.test.ts +114 -0
  155. package/src/console/ui-next/src/features/flows/traces/request-client.ts +423 -0
  156. package/src/console/ui-next/src/features/flows/traces/request-input-view.test.ts +52 -6
  157. package/src/console/ui-next/src/features/flows/traces/request-input-view.ts +85 -19
  158. package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +505 -80
  159. package/src/console/ui-next/src/features/flows/traces/trace-lanes.test.ts +61 -0
  160. package/src/console/ui-next/src/features/flows/traces/trace-lanes.ts +78 -0
  161. package/src/console/ui-next/src/features/flows/traces/trace-nav.test.ts +48 -0
  162. package/src/console/ui-next/src/features/flows/traces/trace-nav.ts +57 -0
  163. package/src/console/ui-next/src/features/flows/traces/trace-request-section.tsx +275 -96
  164. package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +16 -2
  165. package/src/console/ui-next/src/features/observability/detail/ai-runs.tsx +369 -0
  166. package/src/console/ui-next/src/features/observability/observability-page.tsx +31 -1
  167. package/src/console/ui-next/src/features/observability/state/observability-selection.ts +18 -3
  168. package/src/console/ui-next/src/features/store/performance/kv-performance-panel.tsx +12 -1
  169. package/src/console/ui-next/src/features/store/performance/performance-panel.tsx +14 -1
  170. package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +1 -1
  171. package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +15 -2
  172. package/src/console/ui-next/src/features/units/explorer/units-tree.tsx +73 -18
  173. package/src/console/ui-next/src/features/units/lib/unit-tree.test.ts +21 -0
  174. package/src/console/ui-next/src/features/units/lib/unit-tree.ts +82 -0
  175. package/src/console/ui-next/src/features/units/units-page.tsx +25 -7
  176. package/src/console/ui-next/src/router.tsx +17 -0
  177. package/src/console/ui-next/ui-next-seed-runs.ts +23 -0
  178. package/src/console/ui-next/vite-console-kernel-plugin.ts +43 -3
  179. package/src/console/ui-next/vite.config.ts +9 -0
  180. package/src/docker/docker.test.ts +13 -13
  181. package/src/docker/images-config.test.ts +12 -12
  182. package/src/docker/recipes/dragonfly.ts +3 -2
  183. package/src/docker/recipes/redis.ts +1 -1
  184. package/src/docker/stack-id.test.ts +1 -1
  185. package/src/drivers/ai-anthropic.ts +212 -21
  186. package/src/drivers/ai-mock.ts +34 -1
  187. package/src/drivers/ai-openai-compatible.ts +67 -26
  188. package/src/drivers/ai-preconnect.ts +18 -0
  189. package/src/drivers/ai-stream.test.ts +62 -0
  190. package/src/drivers/ai-types.ts +20 -0
  191. package/src/drivers/journal-postgres.ts +455 -3
  192. package/src/drivers/meilisearch.ts +2 -0
  193. package/src/drivers/memory-ddl.test.ts +18 -0
  194. package/src/drivers/memory.ts +21 -1
  195. package/src/drivers/pg-rls.test.ts +31 -0
  196. package/src/drivers/pg-rls.ts +15 -0
  197. package/src/drivers/postgres.test.ts +9 -0
  198. package/src/drivers/postgres.ts +49 -11
  199. package/src/drivers/vault-builtin.test.ts +10 -0
  200. package/src/drivers/vault-builtin.ts +13 -1
  201. package/src/drivers/vault-remote-bag.ts +5 -1
  202. package/src/drivers/vault-types.ts +6 -1
  203. package/src/elements/ai/agent-event-slot.ts +27 -0
  204. package/src/elements/ai/approval-http.ts +207 -0
  205. package/src/elements/ai/approval.test.ts +741 -0
  206. package/src/elements/ai/approval.ts +228 -0
  207. package/src/elements/ai/decisions/bind.ts +163 -0
  208. package/src/elements/ai/decisions/certificate.test.ts +291 -0
  209. package/src/elements/ai/decisions/certificate.ts +377 -0
  210. package/src/elements/ai/decisions/certify.ts +314 -0
  211. package/src/elements/ai/decisions/decide.test.ts +953 -0
  212. package/src/elements/ai/decisions/e2e.test.ts +399 -0
  213. package/src/elements/ai/decisions/export.test.ts +120 -0
  214. package/src/elements/ai/decisions/export.ts +146 -0
  215. package/src/elements/ai/decisions/fixtures/openrouter-request.json +24 -0
  216. package/src/elements/ai/decisions/fixtures/openrouter-response.json +22 -0
  217. package/src/elements/ai/decisions/fixtures/typesafe-request.json +19 -0
  218. package/src/elements/ai/decisions/fixtures/typesafe-response.json +13 -0
  219. package/src/elements/ai/decisions/http.test.ts +154 -0
  220. package/src/elements/ai/decisions/http.ts +240 -0
  221. package/src/elements/ai/decisions/labels.test.ts +282 -0
  222. package/src/elements/ai/decisions/labels.ts +257 -0
  223. package/src/elements/ai/decisions/openrouter.live.test.ts +136 -0
  224. package/src/elements/ai/decisions/openrouter.ts +36 -0
  225. package/src/elements/ai/decisions/provider.ts +102 -0
  226. package/src/elements/ai/decisions/typesafe.ts +36 -0
  227. package/src/elements/ai/declare.ts +252 -1
  228. package/src/elements/ai/events.test.ts +379 -0
  229. package/src/elements/ai/events.ts +138 -0
  230. package/src/elements/ai/run-events.test.ts +461 -0
  231. package/src/elements/ai/run-events.ts +522 -0
  232. package/src/elements/ai/runtime.ts +1186 -105
  233. package/src/elements/ai/stream-turn.ts +214 -0
  234. package/src/elements/ai/stream.live.test.ts +76 -0
  235. package/src/elements/ai/subagent.test.ts +136 -0
  236. package/src/elements/ai.test.ts +497 -3
  237. package/src/elements/ai.ts +16 -1
  238. package/src/elements/clock/durable.ts +27 -1
  239. package/src/elements/store/emit-drizzle.ts +2 -2
  240. package/src/elements/store/live-default.test.ts +2 -0
  241. package/src/elements/store/live-http.test.ts +1 -0
  242. package/src/elements/store/resource-list-docs.test.ts +2 -2
  243. package/src/elements/store/resource.test.ts +3 -3
  244. package/src/elements/store/schema-decl.test.ts +1 -0
  245. package/src/elements/store/sql-condition.ts +24 -7
  246. package/src/elements/store/sql-session.test.ts +51 -0
  247. package/src/elements/store/sql-session.ts +111 -1
  248. package/src/elements/store/upsert-app.test.ts +1 -1
  249. package/src/elements/vault/audit.test.ts +137 -0
  250. package/src/elements/vault/audit.ts +106 -13
  251. package/src/elements/vault/boot-chain.ts +9 -1
  252. package/src/elements/vault/builtin-adapter.ts +33 -3
  253. package/src/i18n/catalogs/ar.ts +9 -0
  254. package/src/i18n/catalogs/en.ts +9 -0
  255. package/src/index.ts +2 -0
  256. package/src/kernel/abort-scope.ts +33 -2
  257. package/src/kernel/agent-event-store.ts +448 -0
  258. package/src/kernel/app.ts +294 -19
  259. package/src/kernel/auto-cache.test.ts +6 -6
  260. package/src/kernel/boot-bind/ai.ts +3 -0
  261. package/src/kernel/boot-bind/store.test.ts +26 -0
  262. package/src/kernel/boot-bind/store.ts +82 -6
  263. package/src/kernel/boot-bind/vault.ts +1 -0
  264. package/src/kernel/boot.ts +34 -5
  265. package/src/kernel/builtin-errors.ts +10 -0
  266. package/src/kernel/capability.ts +3 -0
  267. package/src/kernel/client-descriptor.ts +1 -0
  268. package/src/kernel/concurrency.test.ts +33 -0
  269. package/src/kernel/decision-label-store.ts +441 -0
  270. package/src/kernel/effects-stamping.test.ts +2 -2
  271. package/src/kernel/effects.test.ts +2 -1
  272. package/src/kernel/effects.ts +13 -1
  273. package/src/kernel/element-registries.ts +3 -0
  274. package/src/kernel/errors-compiler.ts +20 -0
  275. package/src/kernel/errors-text.ts +4 -0
  276. package/src/kernel/errors.ts +2 -0
  277. package/src/kernel/external-effects.test.ts +36 -0
  278. package/src/kernel/flow.test.ts +2 -3
  279. package/src/kernel/flow.ts +24 -0
  280. package/src/kernel/fx-decide.ts +761 -0
  281. package/src/kernel/fx-fetch.ts +5 -1
  282. package/src/kernel/fx.test.ts +12 -5
  283. package/src/kernel/fx.ts +316 -29
  284. package/src/kernel/http-frame.test.ts +94 -0
  285. package/src/kernel/http-frame.ts +203 -0
  286. package/src/kernel/idempotency-store.ts +578 -0
  287. package/src/kernel/idempotency.test.ts +556 -0
  288. package/src/kernel/idempotency.ts +304 -0
  289. package/src/kernel/index.ts +1 -0
  290. package/src/kernel/journal.ts +160 -0
  291. package/src/kernel/json-result.ts +10 -0
  292. package/src/kernel/pipeline.test.ts +45 -0
  293. package/src/kernel/sse-id.ts +32 -0
  294. package/src/manifest/diff.ts +1 -0
  295. package/src/manifest/types.ts +37 -2
  296. package/src/mcp/ai-tools.test.ts +202 -0
  297. package/src/mcp/authorization.ts +66 -0
  298. package/src/mcp/session.ts +3 -0
  299. package/src/mcp/tools.ts +168 -0
  300. package/src/plugins/auth-delivery.mailpit.integration.test.ts +1 -1
  301. package/src/plugins/otp.test.ts +65 -1
  302. package/src/plugins/otp.ts +59 -7
  303. package/src/release/absolute-regression.test.ts +63 -0
  304. package/src/release/http-graph.test.ts +27 -0
  305. package/src/release/http-graph.ts +95 -0
  306. package/src/release/index.ts +3 -0
  307. package/src/release/limits.ts +17 -2
  308. package/src/release/measure.ts +144 -28
  309. package/src/runs/collect.ts +4 -1
  310. package/src/runs/parquet.test.ts +27 -1
  311. package/src/runs/parquet.ts +19 -0
  312. package/src/runs/types.ts +31 -0
  313. package/src/runtime/json-code-block.test.ts +106 -47
  314. package/src/runtime/json-code-block.ts +1209 -46
  315. package/src/term.test.ts +13 -0
  316. package/src/term.ts +81 -49
  317. package/src/test/create-test-app.test.ts +3 -1
  318. package/src/test/create-test-app.ts +40 -0
  319. package/src/test/live-signals.test.ts +1 -0
  320. package/src/test/provisions.integration.test.ts +1 -0
  321. package/src/test/tenant-isolation.test.ts +1 -0
  322. package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +0 -1
  323. package/src/console/ui-next/dist/assets/access-page-Bgt9bq2r.js +0 -4
  324. package/src/console/ui-next/dist/assets/agent-disclosure-B43CXZZR.js +0 -1
  325. package/src/console/ui-next/dist/assets/copy-inline-button-CAYD18cr.js +0 -1
  326. package/src/console/ui-next/dist/assets/detail-header-DVWNjWjg.js +0 -1
  327. package/src/console/ui-next/dist/assets/dropdown-menu-4h2LOVXM.js +0 -1
  328. package/src/console/ui-next/dist/assets/duration-tone-BmIR9FV8.js +0 -9
  329. package/src/console/ui-next/dist/assets/element-icons-BI8cJgdh.js +0 -1
  330. package/src/console/ui-next/dist/assets/explorer-empty-CJs5A-wm.js +0 -1
  331. package/src/console/ui-next/dist/assets/flows-page-BMHs-IzK.js +0 -1
  332. package/src/console/ui-next/dist/assets/index-BKpaes3n.js +0 -63
  333. package/src/console/ui-next/dist/assets/index-VxoEz295.css +0 -2
  334. package/src/console/ui-next/dist/assets/observability-page-WnVLI-0j.js +0 -4
  335. package/src/console/ui-next/dist/assets/request-meta-DMbnAe3f.js +0 -1
  336. package/src/console/ui-next/dist/assets/shortcut-keys-3ILd8oGn.js +0 -1
  337. package/src/console/ui-next/dist/assets/store-page-KvFDingJ.js +0 -41
  338. package/src/console/ui-next/dist/assets/trace-detail-sheet-Htk8Cm9t.js +0 -2
  339. package/src/console/ui-next/dist/assets/tree-expand-toggle-CFMPWX4f.js +0 -55
  340. package/src/console/ui-next/dist/assets/units-page-C4NdNuxP.js +0 -1
  341. package/src/console/ui-next/dist/assets/use-vault-list-CT4-gajj.js +0 -1
  342. package/src/console/ui-next/dist/assets/vault-page-CBYl0LW_.js +0 -2
@@ -6,6 +6,7 @@ import { describe, expect, test } from "bun:test";
6
6
  import { withAbortSignal } from "../kernel/abort-scope.ts";
7
7
  import { createFx, createFxContext } from "../kernel/fx.ts";
8
8
  import { ai, createAiRuntime } from "../elements/ai.ts";
9
+ import { openAnthropic } from "./ai-anthropic.ts";
9
10
  import { openOpenaiCompatible } from "./ai-openai-compatible.ts";
10
11
  import { mockAiDriver } from "./ai-mock.ts";
11
12
 
@@ -44,6 +45,29 @@ describe("openai-compatible SSE stream", () => {
44
45
  if (chunk.text) parts.push(chunk.text);
45
46
  }
46
47
  expect(parts.join("")).toBe("Hello");
48
+ const bodies: unknown[] = [];
49
+ const formatFetch = Object.assign(
50
+ async (_input: string | URL | Request, init?: RequestInit) => {
51
+ bodies.push(JSON.parse(String(init?.body)));
52
+ return new Response(sse, { status: 200, headers: { "content-type": "text/event-stream" } });
53
+ },
54
+ { preconnect: () => {} },
55
+ ) as typeof fetch;
56
+ const formatted = await openOpenaiCompatible({
57
+ apiKey: "sk-test",
58
+ baseUrl: "https://example.test/v1",
59
+ fetch: formatFetch,
60
+ });
61
+ for await (const _chunk of formatted.stream!({
62
+ messages: [{ role: "user", content: "hi" }],
63
+ responseFormat: {
64
+ type: "json_schema",
65
+ json_schema: { name: "out", schema: { type: "object" } },
66
+ },
67
+ })) {
68
+ // drain
69
+ }
70
+ expect(bodies[0]).toMatchObject({ response_format: { type: "json_object" } });
47
71
 
48
72
  const ac = new AbortController();
49
73
  const iter = client.stream!({
@@ -58,6 +82,44 @@ describe("openai-compatible SSE stream", () => {
58
82
  });
59
83
  });
60
84
 
85
+ describe("anthropic messages stream", () => {
86
+ test("text deltas, input_json_delta, and message_delta usage", async () => {
87
+ const sse = [
88
+ 'data: {"type":"message_start","message":{"usage":{"input_tokens":4,"output_tokens":1}}}',
89
+ 'data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Ok"}}',
90
+ 'data: {"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"toolu_1","name":"orders.get","input":{}}}',
91
+ 'data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"{\\"id\\":"}}',
92
+ 'data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"\\"14\\"}"}}',
93
+ 'data: {"type":"message_delta","delta":{"stop_reason":"tool_use"},"usage":{"output_tokens":6}}',
94
+ 'data: {"type":"message_stop"}',
95
+ "",
96
+ ].join("\n");
97
+ const fetchFn = Object.assign(
98
+ async () =>
99
+ new Response(sse, { status: 200, headers: { "content-type": "text/event-stream" } }),
100
+ { preconnect: () => {} },
101
+ ) as typeof fetch;
102
+ const client = await openAnthropic({
103
+ apiKey: "sk-test",
104
+ fetch: fetchFn,
105
+ });
106
+ const texts: string[] = [];
107
+ const args: string[] = [];
108
+ let usage: { inputTokens?: number; outputTokens?: number } | undefined;
109
+ for await (const chunk of client.stream!({
110
+ messages: [{ role: "user", content: "status" }],
111
+ tools: [{ name: "orders.get" }],
112
+ })) {
113
+ if (chunk.text) texts.push(chunk.text);
114
+ if (chunk.toolCall?.argumentsDelta) args.push(chunk.toolCall.argumentsDelta);
115
+ if (chunk.usage) usage = chunk.usage;
116
+ }
117
+ expect(texts.join("")).toBe("Ok");
118
+ expect(args.join("")).toBe('{"id":"14"}');
119
+ expect(usage).toEqual({ inputTokens: 4, outputTokens: 6 });
120
+ });
121
+ });
122
+
61
123
  describe("fx.stream uses ambient AbortSignal", () => {
62
124
  test("race abort cancels in-flight stream fetch", async () => {
63
125
  let sawAbort = false;
@@ -66,10 +66,30 @@ export interface AiCompleteResult {
66
66
  readonly external?: DriverExternal;
67
67
  }
68
68
 
69
+ /** One tool-call fragment inside a model stream. */
70
+ export interface AiStreamToolCallDelta {
71
+ /** Provider index for this call within the turn. */
72
+ readonly index: number;
73
+ readonly id?: string;
74
+ readonly name?: string;
75
+ /** Partial JSON arguments. Empty when the call is only announced. */
76
+ readonly argumentsDelta?: string;
77
+ }
78
+
69
79
  /** One streamed token / delta from a model. */
70
80
  export interface AiStreamChunk {
71
81
  readonly text: string;
72
82
  readonly done?: boolean;
83
+ readonly toolCall?: AiStreamToolCallDelta;
84
+ readonly usage?: {
85
+ readonly inputTokens?: number;
86
+ readonly outputTokens?: number;
87
+ readonly cost?: number;
88
+ };
89
+ /** Provider payload. Present on the terminal chunk when the driver has one. */
90
+ readonly raw?: unknown;
91
+ /** Egress identity, matching {@link AiCompleteResult.external}. */
92
+ readonly external?: DriverExternal;
73
93
  }
74
94
 
75
95
  /** Embedding request. */
@@ -7,6 +7,22 @@
7
7
  * completed journal steps replay instead of re-running).
8
8
  */
9
9
 
10
+ import {
11
+ createPostgresIdempotencyStore,
12
+ IDEM_ATTACH_SQL,
13
+ IDEM_COMPLETE_SQL,
14
+ IDEM_DELETE_EXPIRED_ONE_SQL,
15
+ IDEM_FORFEIT_SQL,
16
+ IDEM_INSERT_SQL,
17
+ IDEM_PURGE_SQL,
18
+ IDEM_RECLAIM_SQL,
19
+ IDEM_REMOVE_SQL,
20
+ IDEM_RENEW_SQL,
21
+ IDEM_SELECT_SQL,
22
+ } from "../kernel/idempotency-store.ts";
23
+ import type { DecisionLabelStore } from "../kernel/decision-label-store.ts";
24
+ import type { AgentEventStore } from "../kernel/agent-event-store.ts";
25
+ import { lazyRequire } from "../kernel/lazy-require.ts";
10
26
  import {
11
27
  JOURNAL_DEFAULT_LEASE_MS,
12
28
  type JournalEntry,
@@ -199,6 +215,26 @@ async function ensureSchema(sql: PostgresJournalSql): Promise<void> {
199
215
  await sql.exec(
200
216
  `CREATE INDEX IF NOT EXISTS oke_journal_runs_lease ON oke_journal_runs (status, lease_expires_at)`,
201
217
  );
218
+ await sql.exec(`CREATE TABLE IF NOT EXISTS oke_idempotency (
219
+ tenant TEXT NOT NULL DEFAULT '',
220
+ principal TEXT NOT NULL,
221
+ flow TEXT NOT NULL,
222
+ key TEXT NOT NULL,
223
+ fingerprint TEXT NOT NULL,
224
+ status TEXT NOT NULL,
225
+ claim_token TEXT NOT NULL,
226
+ lease_expires_at BIGINT NOT NULL,
227
+ run_id TEXT,
228
+ response_status INTEGER,
229
+ response_headers TEXT,
230
+ response_body TEXT,
231
+ created_at BIGINT NOT NULL,
232
+ expires_at BIGINT NOT NULL,
233
+ PRIMARY KEY (tenant, principal, flow, key)
234
+ )`);
235
+ await sql.exec(
236
+ `CREATE INDEX IF NOT EXISTS oke_idempotency_expires ON oke_idempotency (expires_at)`,
237
+ );
202
238
  }
203
239
 
204
240
  /**
@@ -208,9 +244,63 @@ export function createPostgresJournalFake(): PostgresJournalSql & {
208
244
  /** Force-kill mid-transaction (drops uncommitted state). */
209
245
  killActiveTransaction(): void;
210
246
  } {
211
- type State = { rows: JournalDbRow[] };
247
+ type IdemDbRow = {
248
+ tenant: string;
249
+ principal: string;
250
+ flow: string;
251
+ key: string;
252
+ fingerprint: string;
253
+ status: string;
254
+ claim_token: string;
255
+ lease_expires_at: number;
256
+ run_id: string | null;
257
+ response_status: number | null;
258
+ response_headers: string | null;
259
+ response_body: string | null;
260
+ created_at: number;
261
+ expires_at: number;
262
+ };
263
+ type LabelDbRow = {
264
+ decision_id: string;
265
+ question: string;
266
+ value: string;
267
+ propensity: number;
268
+ reviewer: string;
269
+ locale: string | null;
270
+ model: string | null;
271
+ tenant: string | null;
272
+ score: number | null;
273
+ loss: number | null;
274
+ raw: string | null;
275
+ at: number;
276
+ input: string | null;
277
+ review_id?: string | null;
278
+ };
279
+ type State = {
280
+ rows: JournalDbRow[];
281
+ idem: IdemDbRow[];
282
+ labels: LabelDbRow[];
283
+ drift: { decision_id: string; suspended: number; certified_at: number }[];
284
+ candidates: { decision_id: string; body: string }[];
285
+ agentRuns: { run_id: string; header: string; sweep_claim_at: number | null }[];
286
+ agentEvents: {
287
+ run_id: string;
288
+ seq: number;
289
+ event: string;
290
+ event_type: string | null;
291
+ event_name: string | null;
292
+ }[];
293
+ };
212
294
 
213
- let committed: State = { rows: [] };
295
+ let committed: State = {
296
+ rows: [],
297
+ idem: [],
298
+ labels: [],
299
+ drift: [],
300
+ candidates: [],
301
+ agentRuns: [],
302
+ agentEvents: [],
303
+ };
214
304
  let active: { state: State; locked: Set<string>; done: boolean } | null = null;
215
305
  /** Run ids held by other active transactions (SKIP LOCKED). */
216
306
  const heldByTxn = new Set<string>();
@@ -222,7 +312,24 @@ export function createPostgresJournalFake(): PostgresJournalSql & {
222
312
  }
223
313
 
224
314
  function cloneState(s: State): State {
225
- return { rows: s.rows.map((r) => ({ ...r })) };
315
+ return {
316
+ rows: s.rows.map((r) => ({ ...r })),
317
+ idem: s.idem.map((r) => ({ ...r })),
318
+ labels: s.labels.map((r) => ({ ...r })),
319
+ drift: s.drift.map((r) => ({ ...r })),
320
+ candidates: s.candidates.map((r) => ({ ...r })),
321
+ agentRuns: s.agentRuns.map((r) => ({ ...r })),
322
+ agentEvents: s.agentEvents.map((r) => ({ ...r })),
323
+ };
324
+ }
325
+
326
+ function idemPk(row: IdemDbRow, params: readonly unknown[], offset = 0): boolean {
327
+ return (
328
+ row.tenant === String(params[offset]) &&
329
+ row.principal === String(params[offset + 1]) &&
330
+ row.flow === String(params[offset + 2]) &&
331
+ row.key === String(params[offset + 3])
332
+ );
226
333
  }
227
334
 
228
335
  function claimable(r: JournalDbRow, holder: string, cutoff: number): boolean {
@@ -309,6 +416,71 @@ export function createPostgresJournalFake(): PostgresJournalSql & {
309
416
  return state.rows.map((r) => ({ ...r }));
310
417
  }
311
418
 
419
+ if (text === IDEM_SELECT_SQL) {
420
+ return state.idem
421
+ .filter((row) => idemPk(row, params))
422
+ .map((row) => ({ ...row }) as Record<string, unknown>);
423
+ }
424
+
425
+ if (/FROM\s+oke_decision_labels/i.test(text)) {
426
+ let labels = state.labels;
427
+ let arg = 0;
428
+ if (/decision_id\s*=\s*\?/i.test(text)) {
429
+ const id = String(params[arg++] ?? "");
430
+ labels = labels.filter((row) => row.decision_id === id);
431
+ }
432
+ if (/tenant\s+IS\s+NOT\s+DISTINCT\s+FROM\s+\?/i.test(text)) {
433
+ const tenant = params[arg] ?? null;
434
+ labels = labels.filter((row) => row.tenant === tenant);
435
+ }
436
+ return labels.map((row) => ({ ...row }));
437
+ }
438
+
439
+ if (/FROM\s+oke_decision_drift/i.test(text)) {
440
+ return state.drift.map((row) => ({ ...row }));
441
+ }
442
+
443
+ if (/FROM\s+oke_decision_candidates/i.test(text)) {
444
+ if (/decision_id\s*=\s*\?/i.test(text)) {
445
+ const id = String(params[0] ?? "");
446
+ return state.candidates
447
+ .filter((row) => row.decision_id === id)
448
+ .map((row) => ({ ...row }));
449
+ }
450
+ return state.candidates.map((row) => ({ ...row }));
451
+ }
452
+
453
+ if (/FROM\s+oke_agent_run\b/i.test(text)) {
454
+ if (!/WHERE/i.test(text)) return state.agentRuns.map((row) => ({ ...row }));
455
+ const id = String(params[0] ?? "");
456
+ return state.agentRuns.filter((row) => row.run_id === id).map((row) => ({ ...row }));
457
+ }
458
+
459
+ if (/FROM\s+oke_agent_event\b/i.test(text)) {
460
+ const id = String(params[0] ?? "");
461
+ if (/MAX\s*\(\s*seq\s*\)/i.test(text)) {
462
+ const max = state.agentEvents
463
+ .filter((row) => row.run_id === id)
464
+ .reduce((highest, row) => Math.max(highest, row.seq), 0);
465
+ return [{ max_seq: max }];
466
+ }
467
+ if (/event_type\s*=/i.test(text)) {
468
+ const eventType = String(params[1] ?? "");
469
+ const eventName = String(params[2] ?? "");
470
+ return state.agentEvents
471
+ .filter(
472
+ (row) =>
473
+ row.run_id === id && row.event_type === eventType && row.event_name === eventName,
474
+ )
475
+ .map((row) => ({ ...row }));
476
+ }
477
+ const after = /seq\s*>\s*\?/i.test(text) ? Number(params[1] ?? 0) : 0;
478
+ return state.agentEvents
479
+ .filter((row) => row.run_id === id && row.seq > after)
480
+ .sort((a, b) => a.seq - b.seq)
481
+ .map((row) => ({ ...row }));
482
+ }
483
+
312
484
  throw new Error(`postgres journal fake: unsupported query: ${sql}`);
313
485
  },
314
486
  async exec(sql, params = []) {
@@ -388,6 +560,209 @@ export function createPostgresJournalFake(): PostgresJournalSql & {
388
560
  return { changes: 0 };
389
561
  }
390
562
 
563
+ if (text === IDEM_PURGE_SQL) {
564
+ const now = Number(params[0]);
565
+ const before = state.idem.length;
566
+ state.idem = state.idem.filter((row) => row.expires_at > now);
567
+ return { changes: before - state.idem.length };
568
+ }
569
+ if (text === IDEM_DELETE_EXPIRED_ONE_SQL) {
570
+ const now = Number(params[4]);
571
+ const idx = state.idem.findIndex((row) => idemPk(row, params) && row.expires_at <= now);
572
+ if (idx < 0) return { changes: 0 };
573
+ state.idem.splice(idx, 1);
574
+ return { changes: 1 };
575
+ }
576
+ if (text === IDEM_INSERT_SQL) {
577
+ if (state.idem.some((row) => idemPk(row, params))) return { changes: 0 };
578
+ state.idem.push({
579
+ tenant: String(params[0]),
580
+ principal: String(params[1]),
581
+ flow: String(params[2]),
582
+ key: String(params[3]),
583
+ fingerprint: String(params[4]),
584
+ status: "in_progress",
585
+ claim_token: String(params[5]),
586
+ lease_expires_at: Number(params[6]),
587
+ run_id: null,
588
+ response_status: null,
589
+ response_headers: null,
590
+ response_body: null,
591
+ created_at: Number(params[7]),
592
+ expires_at: Number(params[8]),
593
+ });
594
+ return { changes: 1 };
595
+ }
596
+ if (text === IDEM_RECLAIM_SQL) {
597
+ const row = state.idem.find(
598
+ (candidate) =>
599
+ idemPk(candidate, params, 2) && candidate.claim_token === String(params[6]),
600
+ );
601
+ if (row === undefined || row.status !== "in_progress") return { changes: 0 };
602
+ row.claim_token = String(params[0]);
603
+ row.lease_expires_at = Number(params[1]);
604
+ return { changes: 1 };
605
+ }
606
+ if (text === IDEM_COMPLETE_SQL) {
607
+ const row = state.idem.find(
608
+ (candidate) =>
609
+ idemPk(candidate, params, 4) && candidate.claim_token === String(params[8]),
610
+ );
611
+ if (row === undefined) return { changes: 0 };
612
+ row.status = "completed";
613
+ row.response_status = Number(params[0]);
614
+ row.response_headers = params[1] === null ? null : String(params[1]);
615
+ row.response_body = params[2] === null ? null : String(params[2]);
616
+ if (params[3] !== null && params[3] !== undefined) row.run_id = String(params[3]);
617
+ return { changes: 1 };
618
+ }
619
+ if (text === IDEM_REMOVE_SQL) {
620
+ const idx = state.idem.findIndex(
621
+ (candidate) => idemPk(candidate, params) && candidate.claim_token === String(params[4]),
622
+ );
623
+ if (idx < 0) return { changes: 0 };
624
+ state.idem.splice(idx, 1);
625
+ return { changes: 1 };
626
+ }
627
+ if (text === IDEM_RENEW_SQL) {
628
+ const row = state.idem.find(
629
+ (candidate) =>
630
+ idemPk(candidate, params, 1) &&
631
+ candidate.claim_token === String(params[5]) &&
632
+ candidate.status === "in_progress",
633
+ );
634
+ if (row === undefined) return { changes: 0 };
635
+ row.lease_expires_at = Number(params[0]);
636
+ return { changes: 1 };
637
+ }
638
+ if (text === IDEM_ATTACH_SQL) {
639
+ const row = state.idem.find(
640
+ (candidate) =>
641
+ idemPk(candidate, params, 1) && candidate.claim_token === String(params[5]),
642
+ );
643
+ if (row === undefined) return { changes: 0 };
644
+ row.run_id = String(params[0]);
645
+ return { changes: 1 };
646
+ }
647
+ if (/^INSERT\s+INTO\s+oke_decision_labels\b/i.test(text)) {
648
+ state.labels.push({
649
+ decision_id: String(params[0]),
650
+ question: String(params[1]),
651
+ value: String(params[2]),
652
+ propensity: Number(params[3]),
653
+ reviewer: String(params[4]),
654
+ locale: params[5] === null || params[5] === undefined ? null : String(params[5]),
655
+ model: params[6] === null || params[6] === undefined ? null : String(params[6]),
656
+ tenant: params[7] === null || params[7] === undefined ? null : String(params[7]),
657
+ score: params[8] === null || params[8] === undefined ? null : Number(params[8]),
658
+ loss: params[9] === null || params[9] === undefined ? null : Number(params[9]),
659
+ raw: params[10] === null || params[10] === undefined ? null : String(params[10]),
660
+ at: Number(params[11]),
661
+ input: params[12] === null || params[12] === undefined ? null : String(params[12]),
662
+ review_id: params[13] === null || params[13] === undefined ? null : String(params[13]),
663
+ });
664
+ return { changes: 1 };
665
+ }
666
+
667
+ if (/^INSERT\s+INTO\s+oke_decision_candidates\b/i.test(text)) {
668
+ const decisionId = String(params[0]);
669
+ const body = String(params[1]);
670
+ const idx = state.candidates.findIndex((row) => row.decision_id === decisionId);
671
+ if (idx >= 0) state.candidates[idx] = { decision_id: decisionId, body };
672
+ else state.candidates.push({ decision_id: decisionId, body });
673
+ return { changes: 1 };
674
+ }
675
+
676
+ if (/^UPDATE\s+oke_decision_drift\b/i.test(text)) return { changes: 0 };
677
+
678
+ if (/^DELETE\s+FROM\s+oke_decision_drift\b/i.test(text)) {
679
+ const id = String(params[0] ?? "");
680
+ const before = state.drift.length;
681
+ state.drift = state.drift.filter((row) => row.decision_id !== id);
682
+ return { changes: before - state.drift.length };
683
+ }
684
+
685
+ if (/^INSERT\s+INTO\s+oke_decision_drift\b/i.test(text)) {
686
+ const decisionId = String(params[0]);
687
+ const next = {
688
+ decision_id: decisionId,
689
+ suspended: Number(params[1]),
690
+ certified_at: Number(params[2] ?? 0),
691
+ };
692
+ const idx = state.drift.findIndex((row) => row.decision_id === decisionId);
693
+ if (idx >= 0) state.drift[idx] = next;
694
+ else state.drift.push(next);
695
+ return { changes: 1 };
696
+ }
697
+
698
+ if (/^UPDATE\s+oke_agent_run\s+SET\s+sweep_claim_at\b/i.test(text)) {
699
+ const until = Number(params[0]);
700
+ const runId = String(params[1] ?? "");
701
+ const now = Number(params[2]);
702
+ const row = state.agentRuns.find((candidate) => candidate.run_id === runId);
703
+ if (!row) return { changes: 0 };
704
+ if (row.sweep_claim_at !== null && row.sweep_claim_at > now) return { changes: 0 };
705
+ row.sweep_claim_at = until;
706
+ return { changes: 1 };
707
+ }
708
+
709
+ if (/^INSERT\s+INTO\s+oke_agent_run\b/i.test(text)) {
710
+ const runId = String(params[0]);
711
+ const existing = state.agentRuns.find((row) => row.run_id === runId);
712
+ const next = {
713
+ run_id: runId,
714
+ header: String(params[1]),
715
+ sweep_claim_at: existing?.sweep_claim_at ?? null,
716
+ };
717
+ const idx = state.agentRuns.findIndex((row) => row.run_id === runId);
718
+ if (idx >= 0) state.agentRuns[idx] = next;
719
+ else state.agentRuns.push(next);
720
+ return { changes: 1 };
721
+ }
722
+
723
+ if (/^INSERT\s+INTO\s+oke_agent_event\b/i.test(text)) {
724
+ const runId = String(params[0]);
725
+ const seq = Number(params[1]);
726
+ if (state.agentEvents.some((row) => row.run_id === runId && row.seq === seq)) {
727
+ throw new Error(`duplicate key value violates unique constraint "oke_agent_event_pkey"`);
728
+ }
729
+ state.agentEvents.push({
730
+ run_id: runId,
731
+ seq,
732
+ event: String(params[2]),
733
+ event_type: params[3] === undefined || params[3] === null ? null : String(params[3]),
734
+ event_name: params[4] === undefined || params[4] === null ? null : String(params[4]),
735
+ });
736
+ return { changes: 1 };
737
+ }
738
+
739
+ if (/^DELETE\s+FROM\s+oke_agent_event\b/i.test(text)) {
740
+ const id = String(params[0] ?? "");
741
+ const before = state.agentEvents.length;
742
+ state.agentEvents = state.agentEvents.filter((row) => row.run_id !== id);
743
+ return { changes: before - state.agentEvents.length };
744
+ }
745
+
746
+ if (/^DELETE\s+FROM\s+oke_agent_run\b/i.test(text)) {
747
+ const id = String(params[0] ?? "");
748
+ const before = state.agentRuns.length;
749
+ state.agentRuns = state.agentRuns.filter((row) => row.run_id !== id);
750
+ return { changes: before - state.agentRuns.length };
751
+ }
752
+
753
+ if (text === IDEM_FORFEIT_SQL) {
754
+ const row = state.idem.find(
755
+ (candidate) =>
756
+ idemPk(candidate, params, 1) &&
757
+ candidate.claim_token === String(params[5]) &&
758
+ candidate.status === "in_progress",
759
+ );
760
+ if (row === undefined) return { changes: 0 };
761
+ row.claim_token = String(params[0]);
762
+ row.lease_expires_at = 0;
763
+ return { changes: 1 };
764
+ }
765
+
391
766
  throw new Error(`postgres journal fake: unsupported exec: ${sql}`);
392
767
  },
393
768
  async begin(fn) {
@@ -441,6 +816,61 @@ export type PostgresJournalStore = JournalStore &
441
816
  close(): Promise<void>;
442
817
  };
443
818
 
819
+ /**
820
+ * Open decision tables on first use so a journal without decisions stays empty.
821
+ *
822
+ * @param sql - Journal SQL client
823
+ */
824
+ function loadDecisionLabelModule(): typeof import("../kernel/decision-label-store.ts") {
825
+ return lazyRequire(`${import.meta.dir}/../kernel`, ["decision", "label", "store"].join("-"));
826
+ }
827
+
828
+ function loadAgentEventModule(): typeof import("../kernel/agent-event-store.ts") {
829
+ return lazyRequire(`${import.meta.dir}/../kernel`, ["agent", "event", "store"].join("-"));
830
+ }
831
+
832
+ function lazyDecisionLabels(sql: PostgresJournalSql): DecisionLabelStore {
833
+ let pending: Promise<DecisionLabelStore> | undefined;
834
+ const ready = (): Promise<DecisionLabelStore> => {
835
+ pending ??= loadDecisionLabelModule().createPostgresDecisionLabelStore(sql);
836
+ return pending;
837
+ };
838
+ return {
839
+ insert: (label, at) => ready().then((store) => store.insert(label, at)),
840
+ list: (decision, tenant) => ready().then((store) => store.list(decision, tenant)),
841
+ drift: () => ready().then((store) => store.drift()),
842
+ setDrift: (decision, record) => ready().then((store) => store.setDrift(decision, record)),
843
+ putCandidate: (decision, entry) => ready().then((store) => store.putCandidate(decision, entry)),
844
+ getCandidate: (decision) => ready().then((store) => store.getCandidate(decision)),
845
+ listCandidates: () => ready().then((store) => store.listCandidates()),
846
+ };
847
+ }
848
+
849
+ /**
850
+ * Open the agent event tables on first use.
851
+ *
852
+ * @param sql - Journal SQL client
853
+ */
854
+ function lazyAgentEvents(sql: PostgresJournalSql): AgentEventStore {
855
+ let pending: Promise<AgentEventStore> | undefined;
856
+ const ready = (): Promise<AgentEventStore> => {
857
+ pending ??= loadAgentEventModule().createPostgresAgentEventStore(sql);
858
+ return pending;
859
+ };
860
+ return {
861
+ read: (runId) => ready().then((store) => store.read(runId)),
862
+ readAfter: (runId, afterSeq) => ready().then((store) => store.readAfter(runId, afterSeq)),
863
+ maxSeq: (runId) => ready().then((store) => store.maxSeq(runId)),
864
+ readHeader: (runId) => ready().then((store) => store.readHeader(runId)),
865
+ truncated: (runId) => ready().then((store) => store.truncated(runId)),
866
+ claim: (runId) => ready().then((store) => store.claim(runId)),
867
+ listHeaders: () => ready().then((store) => store.listHeaders()),
868
+ writeHeader: (header) => ready().then((store) => store.writeHeader(header)),
869
+ append: (runId, row) => ready().then((store) => store.append(runId, row)),
870
+ remove: (runId) => ready().then((store) => store.remove(runId)),
871
+ };
872
+ }
873
+
444
874
  /**
445
875
  * Open a postgres-backed JournalStore (multi-host durable-run coordination).
446
876
  *
@@ -460,6 +890,9 @@ export async function createPostgresJournalStore(
460
890
 
461
891
  const store: PostgresJournalStore = {
462
892
  sql,
893
+ idempotency: createPostgresIdempotencyStore(sql),
894
+ decisions: lazyDecisionLabels(sql),
895
+ agentEvents: lazyAgentEvents(sql),
463
896
  async get(runId) {
464
897
  const rows = await sql.query(`SELECT * FROM oke_journal_runs WHERE id = ?`, [runId]);
465
898
  if (!rows[0]) return undefined;
@@ -480,6 +913,25 @@ export async function createPostgresJournalStore(
480
913
  return true;
481
914
  });
482
915
  },
916
+ async cas(runId, instanceId, now, leaseMs, update) {
917
+ return sql.begin(async (tx) => {
918
+ const claimed = await tx.query(CLAIM_LEASE_SQL, [runId, instanceId, now]);
919
+ const row = claimed[0];
920
+ if (!row) {
921
+ const exists = await tx.query(`SELECT * FROM oke_journal_runs WHERE id = ?`, [runId]);
922
+ const held = exists[0];
923
+ if (!held) return "missing";
924
+ const current = rowToRun(held);
925
+ return { lease: true, leaseExpiresAt: current.leaseExpiresAt };
926
+ }
927
+ const current = rowToRun(row);
928
+ const next = update(current) ?? current;
929
+ next.lockedBy = instanceId;
930
+ next.leaseExpiresAt = now + leaseMs;
931
+ await tx.exec(UPSERT_SQL, runToParams(next));
932
+ return "ok";
933
+ });
934
+ },
483
935
  async releaseLease(runId, instanceId) {
484
936
  await sql.exec(RELEASE_LEASE_SQL, [runId, instanceId]);
485
937
  },
@@ -22,6 +22,7 @@ import type {
22
22
  TextIndexStore,
23
23
  } from "./types.ts";
24
24
  import { hostFromUrl } from "./external.ts";
25
+ import { requestSignal } from "../kernel/abort-scope.ts";
25
26
 
26
27
  /** Error thrown when the remote Meilisearch is unreachable / unhealthy / errors. */
27
28
  export class MeilisearchUnavailableError extends Error {
@@ -134,6 +135,7 @@ export async function openMeilisearchIndex(
134
135
  res = await fetchFn(`${url}${path}`, {
135
136
  ...init,
136
137
  headers: { ...headers, ...(init?.headers ?? {}) },
138
+ signal: requestSignal(init?.signal ?? null),
137
139
  });
138
140
  } catch (err) {
139
141
  throw new MeilisearchUnavailableError(
@@ -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
+ });