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
@@ -336,18 +336,48 @@ describe("boot binders honour drivers.* config", () => {
336
336
  }
337
337
  });
338
338
 
339
- test("signal: drivers.signal postgres fails loud (never silent memory)", async () => {
340
- await expect(
341
- bootApplication({
342
- env: "test",
343
- signals: [signal.once("ping")],
344
- config: {
345
- drivers: {
346
- signal: { test: "postgres" },
339
+ test("signal: drivers.signal postgres needs DATABASE_URL (never silent memory)", async () => {
340
+ const prevDb = process.env.DATABASE_URL;
341
+ const prevStore = process.env.OKE_STORE_SQL_URL;
342
+ delete process.env.DATABASE_URL;
343
+ delete process.env.OKE_STORE_SQL_URL;
344
+ try {
345
+ await expect(
346
+ bootApplication({
347
+ env: "test",
348
+ signals: [signal.once("ping")],
349
+ config: {
350
+ drivers: {
351
+ signal: { test: "postgres" },
352
+ },
347
353
  },
354
+ }),
355
+ ).rejects.toThrow(/DATABASE_URL/);
356
+ } finally {
357
+ if (prevDb !== undefined) process.env.DATABASE_URL = prevDb;
358
+ else delete process.env.DATABASE_URL;
359
+ if (prevStore !== undefined) process.env.OKE_STORE_SQL_URL = prevStore;
360
+ else delete process.env.OKE_STORE_SQL_URL;
361
+ }
362
+ });
363
+
364
+ test("signal: drivers.signal postgres binds an injected client", async () => {
365
+ const { createPostgresSignalFake } = await import("../../drivers/signal-postgres.ts");
366
+ const result = await bootApplication({
367
+ env: "test",
368
+ signals: [signal.once("ping")],
369
+ clients: { signalSql: createPostgresSignalFake() },
370
+ config: {
371
+ drivers: {
372
+ signal: { test: "postgres" },
348
373
  },
349
- }),
350
- ).rejects.toThrow(/signal driver "postgres"/);
374
+ },
375
+ });
376
+ try {
377
+ expect(result.signal!.driverId).toBe("postgres");
378
+ } finally {
379
+ await result.close();
380
+ }
351
381
  });
352
382
 
353
383
  test("runs: drivers.runs unset in test binds memory", async () => {
@@ -4,11 +4,19 @@
4
4
 
5
5
  import { resolveDriverId, type ConfigEnv } from "../../config/index.ts";
6
6
  import { SIGNAL_DEFAULTS } from "../../config/driver-defaults.ts";
7
+ import {
8
+ sharedPostgresClient,
9
+ toPostgresParams,
10
+ withPinnedPostgres,
11
+ } from "../../drivers/postgres.ts";
12
+ import type { PostgresClientLike } from "../../drivers/postgres.ts";
7
13
  import { memorySignalDriver } from "../../drivers/signal-memory.ts";
14
+ import type { PostgresSignalSql } from "../../drivers/signal-postgres.ts";
15
+ import { postgresSignalDriver } from "../../drivers/signal-postgres.ts";
8
16
  import { createBunSignalRedisClient, redisSignalDriver } from "../../drivers/signal-redis.ts";
9
17
  import type { SignalRedisClientLike } from "../../drivers/signal-types.ts";
10
18
  import { createSignalRuntime, type SignalRuntime } from "../../elements/signal.ts";
11
- import { emitBootWarn } from "../../runtime/boot-warn.ts";
19
+ import { resolveInstanceId } from "../instance-id.ts";
12
20
  import type { BootOptions } from "../boot.ts";
13
21
 
14
22
  /**
@@ -32,29 +40,43 @@ function redisUrlFor(docker: boolean): string | undefined {
32
40
  return url;
33
41
  }
34
42
 
35
- let signalRedisWarned = false;
36
-
37
- /** One-shot: redis Signal is emit-relay + process-local outbox consume today. */
38
- function warnSignalRedisProcessLocal(): void {
39
- if (signalRedisWarned) return;
40
- signalRedisWarned = true;
41
- emitBootWarn(
42
- "oke boot: drivers.signal redis — emit relays to Redis, but consume/live/drain use a " +
43
- "process-local outbox (not multi-instance competing consumers). Prefer a shared durable " +
44
- "outbox path for multi-process tests, or a single consumer instance, until Redis Streams " +
45
- "consume ships.",
46
- );
47
- }
48
-
49
- /** Test helper — reset the one-shot redis Signal warn. */
50
- export function resetSignalRedisWarnForTests(): void {
51
- signalRedisWarned = false;
43
+ /**
44
+ * Wrap a Bun.SQL client as the signal driver's query surface.
45
+ * `listen` is omitted — Bun.SQL has no LISTEN/NOTIFY. Boot polls `drain`.
46
+ *
47
+ * @param client - Shared postgres pool
48
+ */
49
+ function asSignalSql(client: PostgresClientLike): PostgresSignalSql {
50
+ return {
51
+ async query(sql, params = []) {
52
+ const result = await client.unsafe(toPostgresParams(sql, params), [...params]);
53
+ if (Array.isArray(result)) return result as Record<string, unknown>[];
54
+ return Array.from(result as ArrayLike<Record<string, unknown>>);
55
+ },
56
+ async exec(sql, params = []) {
57
+ const result = await client.unsafe(toPostgresParams(sql, params), [...params]);
58
+ if (
59
+ result &&
60
+ typeof result === "object" &&
61
+ "changes" in result &&
62
+ typeof result.changes === "number"
63
+ ) {
64
+ return { changes: result.changes };
65
+ }
66
+ if (Array.isArray(result)) return { changes: result.length };
67
+ return { changes: 0 };
68
+ },
69
+ begin: (fn) => withPinnedPostgres(client, (tx) => fn(asSignalSql(tx))),
70
+ async close() {
71
+ /* Shared pool — boot owns the connection. */
72
+ },
73
+ };
52
74
  }
53
75
 
54
76
  /**
55
77
  * Construct a Signal runtime, register decls / binding names, start the bus.
56
78
  *
57
- * Supported ids: `memory` · `redis`. `postgres` / `nats` fail loud until a real
79
+ * Supported ids: `memory` · `redis` · `postgres`. `nats` fails loud until a
58
80
  * native client can be constructed (never silently bind memory).
59
81
  *
60
82
  * @param options - Boot options
@@ -81,19 +103,33 @@ export async function bindSignal(
81
103
  break;
82
104
  case "redis": {
83
105
  const redis = injectedRedis ?? createBunSignalRedisClient(redisUrlFor(docker));
84
- warnSignalRedisProcessLocal();
85
106
  signal = createSignalRuntime({
86
107
  driver: redisSignalDriver,
87
108
  now,
88
109
  redis,
110
+ compete: true,
111
+ consumerId: resolveInstanceId(options.instanceId),
112
+ });
113
+ break;
114
+ }
115
+ case "postgres": {
116
+ const injected = options.clients?.signalSql;
117
+ const url = process.env.DATABASE_URL ?? process.env.OKE_STORE_SQL_URL ?? undefined;
118
+ if (!injected && !url) {
119
+ throw new Error(
120
+ env === "dev"
121
+ ? 'oke boot: signal driver "postgres" needs DATABASE_URL (did `oke dev` write .env.local?)'
122
+ : 'oke boot: signal driver "postgres" needs DATABASE_URL',
123
+ );
124
+ }
125
+ signal = createSignalRuntime({
126
+ driver: postgresSignalDriver,
127
+ now,
128
+ sql: injected ?? asSignalSql(sharedPostgresClient(url)),
129
+ pollMs: injected ? undefined : 1_000,
89
130
  });
90
131
  break;
91
132
  }
92
- case "postgres":
93
- throw new Error(
94
- 'oke boot: signal driver "postgres" needs a LISTEN/NOTIFY-capable SQL client — ' +
95
- 'not available via Bun.SQL yet. Use "redis" or "memory", or inject elements.signal.',
96
- );
97
133
  case "nats":
98
134
  throw new Error(
99
135
  'oke boot: signal driver "nats" has no production client bind yet — ' +
@@ -13,6 +13,7 @@ import {
13
13
  resolveIndexDriverId,
14
14
  resolveKvDriverId,
15
15
  resolveSqlDriverId,
16
+ sqlBindingForDecl,
16
17
  } from "./store.ts";
17
18
 
18
19
  describe("bindStore driver resolution", () => {
@@ -99,6 +100,31 @@ describe("bindStore driver resolution", () => {
99
100
  });
100
101
  });
101
102
 
103
+ describe("sqlBindingForDecl", () => {
104
+ test("pool.max and replica URLs land on the binding; min is ignored", () => {
105
+ const binding = sqlBindingForDecl("db", "postgres://primary/db", {
106
+ driver: "postgres",
107
+ pool: { max: 20, min: 2 },
108
+ replicas: ["postgres://ro-1/db", "postgres://ro-2/db"],
109
+ });
110
+ expect(binding).toEqual({
111
+ name: "db",
112
+ primary: { url: "postgres://primary/db", pool: { max: 20 } },
113
+ replicas: [
114
+ { url: "postgres://ro-1/db", pool: { max: 20 } },
115
+ { url: "postgres://ro-2/db", pool: { max: 20 } },
116
+ ],
117
+ });
118
+ });
119
+
120
+ test("a string pin keeps the URL and omits pool and replicas", () => {
121
+ expect(sqlBindingForDecl("db", "memory://", "memory")).toEqual({
122
+ name: "db",
123
+ primary: { url: "memory://" },
124
+ });
125
+ });
126
+ });
127
+
102
128
  describe("bindStore durable KV routing", () => {
103
129
  const prev = {
104
130
  redis: process.env.REDIS_URL,
@@ -2,7 +2,14 @@
2
2
  * Lazy store binder — loaded only when Store is declared.
3
3
  */
4
4
 
5
- import { resolveDomainDdlMode, resolveDriverId, type ConfigEnv } from "../../config/index.ts";
5
+ import {
6
+ driverRefId,
7
+ resolveDomainDdlMode,
8
+ resolveDriverId,
9
+ resolveDriverRef,
10
+ type ConfigEnv,
11
+ type DriverRef,
12
+ } from "../../config/index.ts";
6
13
  import {
7
14
  STORE_FILES_DEFAULTS,
8
15
  STORE_KV_DEFAULTS,
@@ -18,6 +25,7 @@ import { redisDriver } from "../../drivers/redis.ts";
18
25
  import { s3Driver } from "../../drivers/s3.ts";
19
26
  import type { FilesDriver, IndexDriver, KvDriver, SqlDriver } from "../../drivers/types.ts";
20
27
  import { createStoreRuntime, type StoreRuntime } from "../../elements/store.ts";
28
+ import type { SqlRuntimeBinding } from "../../elements/store/runtime.ts";
21
29
  import type { StoreDecl } from "../../elements/store/declare.ts";
22
30
  import { emitBootWarn } from "../../runtime/boot-warn.ts";
23
31
  import type { BootOptions } from "../boot.ts"; // type-only — no cycle at runtime
@@ -129,17 +137,15 @@ export function bindStore(
129
137
  warnKvMemoryDurable(durableKvDecls.map((d) => d.name));
130
138
  }
131
139
 
132
- const sqlBindings: Record<string, { name: string; primary: { url: string } }> = {};
140
+ const sqlRef = resolveSqlDriverRef(options, env, docker);
141
+ const sqlBindings: Record<string, SqlRuntimeBinding> = {};
133
142
  const kvBindings: Record<string, { url?: string }> = {};
134
143
  const filesBindings: Record<string, { root?: string }> = {};
135
144
  const indexBindings: Record<string, { url?: string; apiKey?: string }> = {};
136
145
 
137
146
  for (const decl of options.stores ?? []) {
138
147
  if (isSqlDecl(decl)) {
139
- sqlBindings[decl.name] = {
140
- name: decl.name,
141
- primary: { url: sqlUrl },
142
- };
148
+ sqlBindings[decl.name] = sqlBindingForDecl(decl.name, sqlUrl, sqlRef);
143
149
  } else if (isKvDecl(decl)) {
144
150
  if (decl.durable === true) {
145
151
  kvBindings[decl.name] = {};
@@ -210,6 +216,76 @@ export function resolveSqlDriverId(options: BootOptions, env: ConfigEnv, docker:
210
216
  return resolveDriverId(options.config?.drivers?.store?.sql, env, STORE_SQL_DEFAULTS)!;
211
217
  }
212
218
 
219
+ /**
220
+ * Rich SQL driver ref for the active env. A docker `OKE_SQL_DRIVER` override
221
+ * that changes the protocol drops `pool` / `replicas` — those belong to the
222
+ * configured driver, not the override.
223
+ *
224
+ * @param options - Boot options
225
+ * @param env - Active environment
226
+ * @param docker - Docker mode
227
+ */
228
+ function resolveSqlDriverRef(
229
+ options: BootOptions,
230
+ env: ConfigEnv,
231
+ docker: boolean,
232
+ ): DriverRef | undefined {
233
+ const configured = resolveDriverRef(options.config?.drivers?.store?.sql, env, STORE_SQL_DEFAULTS);
234
+ const fromEnv = process.env.OKE_SQL_DRIVER?.trim();
235
+ if (docker && fromEnv && fromEnv !== driverRefId(configured)) return fromEnv;
236
+ return configured;
237
+ }
238
+
239
+ /**
240
+ * Primary URL plus documented `pool.max` and read-replica URLs.
241
+ *
242
+ * `pool.min` is ignored — Bun.SQL has no minimum pool size. Unset `max`
243
+ * leaves pool sizing to the postgres driver default of 8.
244
+ *
245
+ * @param name - Store declaration name
246
+ * @param url - Primary connection URL
247
+ * @param ref - Active driver pin (string or rich object)
248
+ */
249
+ export function sqlBindingForDecl(
250
+ name: string,
251
+ url: string,
252
+ ref: DriverRef | undefined,
253
+ ): SqlRuntimeBinding {
254
+ const max = driverPoolMax(ref);
255
+ const pool = max !== undefined ? { pool: { max } } : {};
256
+ const replicas = driverReplicaUrls(ref).map((replicaUrl) => ({
257
+ url: replicaUrl,
258
+ ...pool,
259
+ }));
260
+ return {
261
+ name,
262
+ primary: { url, ...pool },
263
+ ...(replicas.length > 0 ? { replicas } : {}),
264
+ };
265
+ }
266
+
267
+ /**
268
+ * Positive finite `pool.max` from a rich driver ref.
269
+ *
270
+ * @param ref - Driver pin
271
+ */
272
+ function driverPoolMax(ref: DriverRef | undefined): number | undefined {
273
+ if (ref === undefined || typeof ref === "string") return undefined;
274
+ const max = ref.pool?.max;
275
+ if (typeof max !== "number" || !Number.isFinite(max) || max < 1) return undefined;
276
+ return Math.floor(max);
277
+ }
278
+
279
+ /**
280
+ * Replica connection strings from a rich driver ref. Non-strings are dropped.
281
+ *
282
+ * @param ref - Driver pin
283
+ */
284
+ function driverReplicaUrls(ref: DriverRef | undefined): readonly string[] {
285
+ if (ref === undefined || typeof ref === "string" || ref.replicas === undefined) return [];
286
+ return ref.replicas.filter((item): item is string => typeof item === "string" && item.length > 0);
287
+ }
288
+
213
289
  /**
214
290
  * @param options - Boot options
215
291
  * @param env - Active env
@@ -191,6 +191,8 @@ export interface BootOptions {
191
191
  readonly clients?: {
192
192
  readonly kv?: import("../drivers/types.ts").KvClientLike;
193
193
  readonly signalRedis?: import("../drivers/signal-types.ts").SignalRedisClientLike;
194
+ /** Injected postgres signal client (tests). Production opens Bun.SQL. */
195
+ readonly signalSql?: import("../drivers/signal-postgres.ts").PostgresSignalSql;
194
196
  };
195
197
  /**
196
198
  * Process instance id for Clock / Journal / fleet registry.
@@ -282,9 +284,14 @@ export function resolveElementNeeds(options: BootOptions): ElementNeeds {
282
284
  const considerFlow = (f: AnyFlowDef): void => {
283
285
  const e = f.effects;
284
286
  if (f.durable === true) journal = true;
285
- if ((e?.reads?.length ?? 0) > 0 || (e?.writes?.length ?? 0) > 0) {
286
- store = true;
287
- }
287
+ const touchesStore = [...(e?.reads ?? []), ...(e?.writes ?? [])].some(
288
+ (ref) =>
289
+ ref.startsWith("sql:") ||
290
+ ref.startsWith("kv:") ||
291
+ ref.startsWith("files:") ||
292
+ ref.startsWith("index:"),
293
+ );
294
+ if (touchesStore) store = true;
288
295
  if ((e?.emits?.length ?? 0) > 0) signal = true;
289
296
  if ((e?.secrets?.length ?? 0) > 0) vault = true;
290
297
  if ((e?.sends?.length ?? 0) > 0) channel = true;
@@ -533,7 +540,16 @@ export async function bootApplication(input: BootOptions = {}): Promise<BootResu
533
540
  // 4d. Scheduler — one timer drives clock ticks, durable-run resume, heartbeat.
534
541
  let schedulerTimer: ReturnType<typeof setInterval> | undefined;
535
542
  const startScheduler = options.startScheduler ?? env !== "test";
536
- if (startScheduler && (clock !== undefined || journal !== undefined || instances !== undefined)) {
543
+ const signalBus = signal?.bus ?? undefined;
544
+ let refreshChannel: (() => Promise<void>) | undefined;
545
+ if (
546
+ startScheduler &&
547
+ (clock !== undefined ||
548
+ journal !== undefined ||
549
+ instances !== undefined ||
550
+ signalBus !== undefined ||
551
+ needs.channel)
552
+ ) {
537
553
  const period = options.schedulerIntervalMs ?? 1000;
538
554
  const clockRt = clock;
539
555
  const durableResume = options.onDurableResume;
@@ -545,9 +561,17 @@ export async function bootApplication(input: BootOptions = {}): Promise<BootResu
545
561
  console.error(err);
546
562
  };
547
563
  schedulerTimer = setInterval(() => {
564
+ if (signalBus) void signalBus.drain().catch(ignoreBenignSql);
565
+ if (refreshChannel) void refreshChannel().catch(ignoreBenignSql);
548
566
  if (clockRt) void Promise.resolve(clockRt.tick()).catch(ignoreBenignSql);
549
567
  if (journal && durableResume) void Promise.resolve(durableResume()).catch(ignoreBenignSql);
550
568
  if (fleet) void fleet.maybeHeartbeat().catch(ignoreBenignSql);
569
+ const stem = ["run", "events"].join("-");
570
+ void import(`../elements/ai/${stem}.ts`)
571
+ .then((mod: typeof import("../elements/ai/run-events.ts")) =>
572
+ mod.sweepInstalledAgentEvents(),
573
+ )
574
+ .catch(ignoreBenignSql);
551
575
  }, period);
552
576
  schedulerTimer.unref?.();
553
577
  }
@@ -561,13 +585,14 @@ export async function bootApplication(input: BootOptions = {}): Promise<BootResu
561
585
  // 5. Channel
562
586
  let channel = pre.channel;
563
587
  if (needs.channel && !channel) {
564
- channel = channelBind!.bindChannel(options, env, now, docker);
588
+ channel = await channelBind!.bindChannel(options, env, now, docker);
589
+ refreshChannel = channel.refresh?.bind(channel);
565
590
  }
566
591
 
567
592
  // 6. AI
568
593
  let ai = pre.ai;
569
594
  if (needs.ai && !ai) {
570
- ai = aiBind!.bindAi(options, gate, now, env, docker, vault);
595
+ ai = aiBind!.bindAi(options, gate, now, env, docker, vault, journal?.store);
571
596
  }
572
597
 
573
598
  // 7. Runs
@@ -25,6 +25,7 @@ export const BUILTIN_ERROR_STATUS = {
25
25
  IdempotencyKeyInvalid: 400,
26
26
  IdempotencyKeyReused: 422,
27
27
  IdempotencyInProgress: 409,
28
+ JournalLeaseBusy: 409,
28
29
  } as const;
29
30
 
30
31
  /** Built-in error code (including `DatabaseError`). */
@@ -94,6 +95,7 @@ export type BuiltinErrorMap = {
94
95
  readonly IdempotencyKeyInvalid: Record<string, never>;
95
96
  readonly IdempotencyKeyReused: Record<string, never>;
96
97
  readonly IdempotencyInProgress: { readonly retryAfter?: number };
98
+ readonly JournalLeaseBusy: { readonly retryAfter?: number };
97
99
  };
98
100
 
99
101
  /**
@@ -21,6 +21,7 @@ const UNDECLARED_KEY: Readonly<Record<EffectKind, keyof typeof OKE_ERRORS>> = {
21
21
  secret: "UNDECLARED_SECRET",
22
22
  call: "UNDECLARED_CALL",
23
23
  fetch: "UNDECLARED_FETCH",
24
+ decide: "UNDECLARED_DECIDE",
24
25
  };
25
26
 
26
27
  /** Maps effect kind → the corresponding `Effects` field. */
@@ -34,6 +35,7 @@ const EFFECTS_FIELD: Readonly<Record<EffectKind, keyof Effects>> = {
34
35
  secret: "secrets",
35
36
  call: "calls",
36
37
  fetch: "fetches",
38
+ decide: "decides",
37
39
  };
38
40
 
39
41
  /**
@@ -91,6 +93,7 @@ export function createCapabilityToken(flow: string, declared?: Effects): Capabil
91
93
  secret: new Set(effects.secrets ?? []),
92
94
  call: new Set(effects.calls ?? []),
93
95
  fetch: new Set(effects.fetches ?? []),
96
+ decide: new Set(effects.decides ?? []),
94
97
  };
95
98
 
96
99
  return {