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
package/src/kernel/app.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  * pipeline, and wires `fx.call` to untriggered (and triggered) flows.
7
7
  */
8
8
 
9
+ import { bindAgentApprovalFlows } from "../elements/ai/approval-http.ts";
9
10
  import { compileRoute } from "../compiler/dynamic.ts";
10
11
  import type { CompiledRoute } from "../compiler/aot.ts";
11
12
  import { encodeExecuteResult, encodeFailure } from "../compiler/response.ts";
@@ -67,6 +68,7 @@ import {
67
68
  import { GATE_PUBLIC_NAME } from "../elements/gate/flatten.ts";
68
69
  import type { JsonCodeAuth } from "../runtime/json-code-block.ts";
69
70
  import { resolveDurationMs } from "./elapsed.ts";
71
+ import { captureHttpFrame } from "./http-frame.ts";
70
72
  import { fail, throwOke, OkeError } from "./errors.ts";
71
73
  import { FLOW_NAME_DUPLICATE, FLOW_UNNAMED } from "./errors-flow-name.ts";
72
74
  import { ONCE_SIGNAL_MULTI_FLOW } from "./errors-once-signal.ts";
@@ -217,6 +219,39 @@ function loadClientDescriptor(): typeof import("./client-descriptor.ts") {
217
219
  return lazyRequire(import.meta.dir, ["client", "descriptor"].join("-"));
218
220
  }
219
221
 
222
+ /** Decision HTTP flows. A static `import()` still lands on every HTTP bundle. */
223
+ function loadDecisionBind(): typeof import("../elements/ai/decisions/bind.ts") {
224
+ return lazyRequire(`${import.meta.dir}/../elements/ai/decisions`, ["bi", "nd"].join(""));
225
+ }
226
+
227
+ /** Decision lockfile. Loaded only when the manifest declares decisions. */
228
+ function loadDecisionCertificate(): typeof import("../elements/ai/decisions/certificate.ts") {
229
+ return lazyRequire(`${import.meta.dir}/../elements/ai/decisions`, ["certificate"].join(""));
230
+ }
231
+
232
+ /** Decision label opener. Loaded only when the manifest declares decisions. */
233
+ function loadDecisionLabels(): typeof import("../elements/ai/decisions/labels.ts") {
234
+ return lazyRequire(`${import.meta.dir}/../elements/ai/decisions`, ["lab", "els"].join(""));
235
+ }
236
+
237
+ /** Drift-flag registration. Loaded only when the manifest declares decisions. */
238
+ function loadDecisionLabelStore(): typeof import("./decision-label-store.ts") {
239
+ return lazyRequire(import.meta.dir, ["decision", "label", "store"].join("-"));
240
+ }
241
+
242
+ /** `fx.decide` gate hook. Loaded only when the manifest declares decisions. */
243
+ function loadFxDecideModule(): typeof import("./fx-decide.ts") {
244
+ return lazyRequire(import.meta.dir, ["fx", "decide"].join("-"));
245
+ }
246
+
247
+ /**
248
+ * Full boot graph (drivers, elements). A static `import()` string is still
249
+ * bundled into every HTTP app; the stem stays computed.
250
+ */
251
+ function loadBoot(): typeof import("./boot.ts") {
252
+ return lazyRequire(import.meta.dir, ["bo", "ot"].join(""));
253
+ }
254
+
220
255
  /** Options for {@link oke}. */
221
256
  export interface OkeOptions {
222
257
  /** Application name (Manifest `app`). */
@@ -333,6 +368,15 @@ export interface OkeOptions {
333
368
  };
334
369
  };
335
370
  };
371
+ /**
372
+ * Tier-1 read cache. Omitted caches pure store-read flows. `{ auto: false }`
373
+ * turns that off. A flow can still opt in with `cache: true` or a duration,
374
+ * and `cache: false` always disables. The key includes tenant, locale,
375
+ * scopes, and membership roles.
376
+ */
377
+ readonly cache?: {
378
+ readonly auto?: boolean;
379
+ };
336
380
  /** Channel runtime options. */
337
381
  readonly channel?: BootOptions["channel"];
338
382
  /** AI runtime options. */
@@ -563,6 +607,8 @@ export interface OkeApp<D extends Record<string, unknown> = {}, R extends AppRou
563
607
  readonly runId?: string;
564
608
  /** Tenant identity for cron / `fx.call` (propagated, unlike auth). */
565
609
  readonly tenant?: { readonly id: string | null };
610
+ /** Explicit locale override for {@link Fx.t} / channel sends. */
611
+ readonly locale?: string;
566
612
  },
567
613
  ): Promise<ExecuteResult>;
568
614
  /**
@@ -1095,6 +1141,8 @@ export function oke(options: OkeOptions): OkeApp {
1095
1141
  // contribute auth-method Bindings before traffic (plan Phase 2).
1096
1142
  const router: Router<Binding> = smart;
1097
1143
 
1144
+ bindAgentApprovalFlows(adoptBinding);
1145
+
1098
1146
  function adoptBinding(b: Binding): void {
1099
1147
  adopted.push(b);
1100
1148
  registerFlow(b.flow);
@@ -1115,6 +1163,24 @@ export function oke(options: OkeOptions): OkeApp {
1115
1163
 
1116
1164
  /** Once-per-app: system `_oke_search_embed_*` CDC bindings from Manifest. */
1117
1165
  let searchEmbedFlowsBound = false;
1166
+ let decisionFlowsBound = false;
1167
+
1168
+ /**
1169
+ * Register the decision aggregate, drift monitor, and candidate route
1170
+ * when any decision declares autonomy.
1171
+ *
1172
+ * @param manifest - Boot manifest
1173
+ */
1174
+ async function ensureDecisionFlows(manifest: BootOptions["manifest"] | undefined): Promise<void> {
1175
+ if (decisionFlowsBound || !manifest) return;
1176
+ const { bindDecisionFlows, manifestHasDecisionAutonomy } = loadDecisionBind();
1177
+ if (!manifestHasDecisionAutonomy(manifest)) {
1178
+ decisionFlowsBound = true;
1179
+ return;
1180
+ }
1181
+ bindDecisionFlows(adoptBinding, manifest);
1182
+ decisionFlowsBound = true;
1183
+ }
1118
1184
  /**
1119
1185
  * Auto-register hybrid-search embed CDC flows when any `.embed()` column
1120
1186
  * exists. Lazy-loads search-bind so edge graphs without search stay lean.
@@ -1339,6 +1405,19 @@ export function oke(options: OkeOptions): OkeApp {
1339
1405
  ...durableResumeFx(),
1340
1406
  ...(gateConfig.auth?.tenant ? (loadAppTenant().w(5, gateConfig.auth) as object) : {}),
1341
1407
  ...(existing?.tenant ? { tenant: { id: existing.tenant } } : {}),
1408
+ callHandler: async (name, callInput) => {
1409
+ const target = flowsByName.get(name);
1410
+ if (!target) return undefined;
1411
+ const inner = await execute(
1412
+ target,
1413
+ callInput,
1414
+ { kind: "internal" } satisfies InternalTrigger,
1415
+ { parentId: runId },
1416
+ );
1417
+ if (inner.failure) return inner.failure;
1418
+ if (inner.ctx.error !== undefined) throw inner.ctx.error;
1419
+ return inner.output;
1420
+ },
1342
1421
  },
1343
1422
  });
1344
1423
  } catch (err) {
@@ -1367,7 +1446,7 @@ export function oke(options: OkeOptions): OkeApp {
1367
1446
  }
1368
1447
 
1369
1448
  async function doBoot(overrides?: Partial<BootOptions>): Promise<BootResult> {
1370
- const { bootApplication, resolveElementNeeds } = await import("./boot.ts");
1449
+ const { bootApplication, resolveElementNeeds } = loadBoot();
1371
1450
  const { assertHttpGatePosture } = await import("../elements/gate/boot.ts");
1372
1451
  const { assertPluginNeeds, buildAvailableNeedTokens } = await import("./plugin-needs.ts");
1373
1452
  const dockerFlag =
@@ -1388,6 +1467,7 @@ export function oke(options: OkeOptions): OkeApp {
1388
1467
  }
1389
1468
 
1390
1469
  const unguardedHttp = overrides?.unguardedHttp ?? gateConfig.unguardedHttp ?? "deny";
1470
+ await ensureDecisionFlows(overrides?.manifest ?? options.manifest);
1391
1471
  // Auth bindings are in `adopted` — missing posture fails with GateBootError.
1392
1472
  assertHttpGatePosture(adopted, { unguardedHttp, env: bootEnv });
1393
1473
 
@@ -1557,6 +1637,26 @@ export function oke(options: OkeOptions): OkeApp {
1557
1637
  // Built-in hybrid search — auto-register durable embed CDC flows when the
1558
1638
  // Manifest declares any `.embed()` column (writer flows stay embed-free).
1559
1639
  await ensureSearchEmbedFlows(overrides?.manifest ?? options.manifest);
1640
+ const decisionManifest = overrides?.manifest ?? options.manifest;
1641
+ const declaredDecisions = decisionManifest?.ai?.decisions;
1642
+ if (declaredDecisions && Object.keys(declaredDecisions).length > 0) {
1643
+ const { loadDecisionLockfile, setDecisionDrift } = loadDecisionCertificate();
1644
+ const { openDecisionLabelStore } = loadDecisionLabels();
1645
+ const { setDeclaredDriftDecisions } = loadDecisionLabelStore();
1646
+ const { setDecisionGateAllow } = loadFxDecideModule();
1647
+ const root = overrides?.rootDir ?? options.rootDir ?? process.env["OKE_ROOT_DIR"];
1648
+ if (!root) {
1649
+ throw new Error(
1650
+ "oke boot: decisions are declared but rootDir and OKE_ROOT_DIR are unset, so the decision lockfile cannot load.",
1651
+ );
1652
+ }
1653
+ await loadDecisionLockfile(root);
1654
+ setDeclaredDriftDecisions(Object.keys(declaredDecisions));
1655
+ if (result.journal && result.journal.store.decisions) {
1656
+ await openDecisionLabelStore(result.journal.store, setDecisionDrift);
1657
+ }
1658
+ if (result.gate) setDecisionGateAllow((names, ctx) => result.gate!.allow(names, ctx));
1659
+ }
1560
1660
  // otp() provider / app mode capability — fail loud at boot, never silent downgrade.
1561
1661
  if (result.channel) {
1562
1662
  const { assertOtpPluginCapability } = await import("../auth/otp-capability.ts");
@@ -1996,6 +2096,14 @@ export function oke(options: OkeOptions): OkeApp {
1996
2096
  : undefined;
1997
2097
  // Revealed PII must not hit a prior masked entry or land in cache.
1998
2098
  const reveal = extras?.trustedInvoke === true && extras.revealPii === true;
2099
+ const cacheAuto = options.cache?.auto !== false;
2100
+ const cacheCaller = {
2101
+ userId: fx.auth.userId,
2102
+ tenantId: fx.tenant.id,
2103
+ locale: fx.locale,
2104
+ scopes: fx.auth.scopes,
2105
+ roles: principals?.cacheRoles ?? [],
2106
+ };
1999
2107
  const cacheOk =
2000
2108
  cache !== undefined &&
2001
2109
  storeRt !== undefined &&
@@ -2003,12 +2111,13 @@ export function oke(options: OkeOptions): OkeApp {
2003
2111
  !reveal &&
2004
2112
  cache.autoCacheEligible({
2005
2113
  cache: flowDef.cache,
2114
+ auto: cacheAuto,
2006
2115
  durable: flowDef.durable,
2007
2116
  effects: cacheEffects,
2008
2117
  });
2009
2118
  const dims =
2010
2119
  cacheOk && cache && cacheEffects
2011
- ? cache.tier1DimsByResource(cacheEffects, flowDef.name, input, fx.auth.userId)
2120
+ ? cache.tier1DimsByResource(cacheEffects, flowDef.name, input, cacheCaller)
2012
2121
  : undefined;
2013
2122
  if (cacheOk && cache && dims && storeRt) {
2014
2123
  const keys = cache.tier1KeysForReads(cacheEffects, dims);
@@ -2043,39 +2152,35 @@ export function oke(options: OkeOptions): OkeApp {
2043
2152
  const output = isFlowFailure(raw) ? raw : await projectFlowOut(flowDef.out, raw);
2044
2153
  if (!isFlowFailure(output) && cache && storeRt && cacheEffects) {
2045
2154
  const ledgerFx = cache.effectsFromLedger(ledger.entries);
2155
+ const observed = cache.mergeEffects(cacheEffects, ledgerFx);
2046
2156
  const writeEffects: Effects = {
2047
- writes: [
2048
- ...new Set([...(cacheEffects.writes ?? []), ...(ledgerFx.writes ?? [])]),
2049
- ].filter(cache.isStoreResourceRef),
2157
+ writes: (observed.writes ?? []).filter(cache.isStoreResourceRef),
2050
2158
  };
2051
2159
  if ((writeEffects.writes?.length ?? 0) > 0) {
2052
2160
  storeRt.onWriteEffects(writeEffects);
2053
2161
  }
2054
- const mergedReads = [
2055
- ...new Set([...(cacheEffects.reads ?? []), ...(ledgerFx.reads ?? [])]),
2056
- ].filter(cache.isStoreResourceRef);
2057
- if (mergedReads.length > 0) {
2058
- learnedTier1Reads.set(flowDef.name, mergedReads);
2162
+ const storeReads = (observed.reads ?? []).filter(cache.isStoreResourceRef);
2163
+ if (cache.autoCachePure(observed) && storeReads.length > 0) {
2164
+ learnedTier1Reads.set(flowDef.name, storeReads);
2165
+ } else {
2166
+ learnedTier1Reads.delete(flowDef.name);
2059
2167
  }
2060
- const putEffects: Effects = {
2061
- reads: mergedReads,
2062
- ...(ledgerFx.writes ? { writes: ledgerFx.writes } : {}),
2063
- ...(ledgerFx.asks ? { asks: ledgerFx.asks } : {}),
2064
- };
2065
2168
  const storeAfter =
2066
2169
  !reveal &&
2067
2170
  cache.autoCacheEligible({
2068
2171
  cache: flowDef.cache,
2172
+ auto: cacheAuto,
2069
2173
  durable: flowDef.durable,
2070
- effects: putEffects,
2174
+ effects: observed,
2071
2175
  });
2072
2176
  if (storeAfter && output !== undefined && !loadFx().isJsonStreamResult(output)) {
2073
2177
  const ttlMs =
2074
2178
  typeof flowDef.cache === "string" ? cache.parseTtlMs(flowDef.cache) : undefined;
2179
+ const putEffects: Effects = { reads: storeReads };
2075
2180
  storeRt.putTier1(
2076
2181
  putEffects,
2077
2182
  output,
2078
- cache.tier1DimsByResource(putEffects, flowDef.name, input, fx.auth.userId),
2183
+ cache.tier1DimsByResource(putEffects, flowDef.name, input, cacheCaller),
2079
2184
  ttlMs,
2080
2185
  );
2081
2186
  if (!cacheOk) telemetry.cacheMisses += 1;
@@ -2257,6 +2362,14 @@ export function oke(options: OkeOptions): OkeApp {
2257
2362
  : durationMs;
2258
2363
  if (journalSession) {
2259
2364
  inflightRuns.delete(journalSession.runId);
2365
+ const parked = streamOut?.parked;
2366
+ if (parked) {
2367
+ ctx.state.sleeping = {
2368
+ wakeAt: parked.wakeAt,
2369
+ label: parked.label,
2370
+ runId: journalSession.runId,
2371
+ };
2372
+ }
2260
2373
  const sleeping = ctx.state.sleeping as
2261
2374
  | { readonly wakeAt: number; readonly label: string; readonly runId: string }
2262
2375
  | undefined;
@@ -2277,6 +2390,8 @@ export function oke(options: OkeOptions): OkeApp {
2277
2390
  fx,
2278
2391
  error: terminalErr,
2279
2392
  });
2393
+ } else if (parked && sleeping) {
2394
+ await journalSession.commit("sleeping", { wakeAt: sleeping.wakeAt });
2280
2395
  } else if (!sleeping) {
2281
2396
  await journalSession.commit("completed", {
2282
2397
  output: streamOut ? { streamed: true } : result.output,
@@ -2309,6 +2424,9 @@ export function oke(options: OkeOptions): OkeApp {
2309
2424
  ? { output: streamOut ? { streamed: true } : result.output }
2310
2425
  : {}),
2311
2426
  ...(parentId !== undefined ? { parentId } : {}),
2427
+ ...(trigger.kind === "http" && extras?.request
2428
+ ? { http: captureHttpFrame(extras.request, result.response) }
2429
+ : {}),
2312
2430
  },
2313
2431
  archiveCleartext,
2314
2432
  );
@@ -9,6 +9,7 @@ import { gate } from "../elements/gate.ts";
9
9
  import { field, id, now, store } from "../elements/store.ts";
10
10
  import { createTestApp, type TestApiCall, type TestApp } from "../test/create-test-app.ts";
11
11
  import { isStoreResourceRef } from "../elements/store/cache.ts";
12
+ import type { Effects } from "../manifest/types.ts";
12
13
  import { oke } from "./app.ts";
13
14
  import { resetNoEffectsWarnForTests } from "./boot.ts";
14
15
  import { flow, resetFlowSeq } from "./flow.ts";
@@ -56,6 +57,7 @@ describe("automatic tier-1 cache from effects", () => {
56
57
  const app = oke({
57
58
  name: "auto-cache",
58
59
  stores: [db],
60
+ cache: { auto: true },
59
61
  gate: { policies: [gate.public] },
60
62
  }).adopt({ list });
61
63
  const t = await createTestApp(app, { capability: "open" });
@@ -88,6 +90,7 @@ describe("automatic tier-1 cache from effects", () => {
88
90
  const app = oke({
89
91
  name: "auto-cache-execute",
90
92
  stores: [db],
93
+ cache: { auto: true },
91
94
  gate: { policies: [gate.public] },
92
95
  }).adopt({ list });
93
96
  const t = await createTestApp(app, { capability: "open" });
@@ -156,6 +159,7 @@ describe("automatic tier-1 cache from effects", () => {
156
159
  const app = oke({
157
160
  name: "auto-cache-ledger",
158
161
  stores: [db],
162
+ cache: { auto: true },
159
163
  gate: { policies: [gate.public] },
160
164
  }).adopt({ list });
161
165
  const t = await createTestApp(app, { capability: "open" });
@@ -207,6 +211,7 @@ describe("automatic tier-1 cache from effects", () => {
207
211
  const app = oke({
208
212
  name: "auto-cache-inv",
209
213
  stores: [db],
214
+ cache: { auto: true },
210
215
  gate: { policies: [gate.public] },
211
216
  }).adopt({ list, create });
212
217
  const t = await createTestApp(app, { capability: "open" });
@@ -249,6 +254,7 @@ describe("automatic tier-1 cache from effects", () => {
249
254
  const app = oke({
250
255
  name: "auto-cache-ledger-inv",
251
256
  stores: [db],
257
+ cache: { auto: true },
252
258
  gate: { policies: [gate.public] },
253
259
  }).adopt({ list, create });
254
260
  const t = await createTestApp(app, { capability: "open" });
@@ -261,4 +267,226 @@ describe("automatic tier-1 cache from effects", () => {
261
267
  expect(lists).toBe(2);
262
268
  await t.close();
263
269
  });
270
+
271
+ test("omitted cache caches a pure store read", async () => {
272
+ resetBindings();
273
+ resetFlowSeq();
274
+ const db = store.sql("app", { schema: { notes } });
275
+ let lists = 0;
276
+ const list = on(
277
+ http.get("/notes").public(),
278
+ flow("notes.list", {
279
+ effects: { reads: ["sql:notes"] },
280
+ do: () => {
281
+ lists += 1;
282
+ return [{ id: "n1", title: "Harbor" }];
283
+ },
284
+ }),
285
+ );
286
+ const app = oke({
287
+ name: "auto-cache-default-on",
288
+ stores: [db],
289
+ gate: { policies: [gate.public] },
290
+ }).adopt({ list });
291
+ const t = await createTestApp(app, { capability: "open" });
292
+ const notesApi = requireNotesApi(t);
293
+ await notesApi.list({});
294
+ await notesApi.list({});
295
+ expect(lists).toBe(1);
296
+ await t.close();
297
+ });
298
+
299
+ test("cache.auto false turns automatic caching off", async () => {
300
+ resetBindings();
301
+ resetFlowSeq();
302
+ const db = store.sql("app", { schema: { notes } });
303
+ let lists = 0;
304
+ const list = on(
305
+ http.get("/notes").public(),
306
+ flow("notes.list", {
307
+ effects: { reads: ["sql:notes"] },
308
+ do: () => {
309
+ lists += 1;
310
+ return [{ id: "n1", title: "Harbor" }];
311
+ },
312
+ }),
313
+ );
314
+ const app = oke({
315
+ name: "auto-cache-app-off",
316
+ stores: [db],
317
+ cache: { auto: false },
318
+ gate: { policies: [gate.public] },
319
+ }).adopt({ list });
320
+ const t = await createTestApp(app, { capability: "open" });
321
+ const notesApi = requireNotesApi(t);
322
+ await notesApi.list({});
323
+ await notesApi.list({});
324
+ expect(lists).toBe(2);
325
+ await t.close();
326
+ });
327
+
328
+ test("cache: true opts one flow in without the app switch", async () => {
329
+ resetBindings();
330
+ resetFlowSeq();
331
+ const db = store.sql("app", { schema: { notes } });
332
+ let lists = 0;
333
+ const list = on(
334
+ http.get("/notes", { out: z.array(NoteOut) }).public(),
335
+ flow("notes.list", {
336
+ cache: true,
337
+ effects: { reads: ["sql:notes"] },
338
+ do: () => {
339
+ lists += 1;
340
+ return [{ id: "n1", title: "Harbor" }];
341
+ },
342
+ }),
343
+ );
344
+ const app = oke({
345
+ name: "auto-cache-flow-opt-in",
346
+ stores: [db],
347
+ gate: { policies: [gate.public] },
348
+ }).adopt({ list });
349
+ const t = await createTestApp(app, { capability: "open" });
350
+ const notesApi = requireNotesApi(t);
351
+ await notesApi.list({});
352
+ await notesApi.list({});
353
+ expect(lists).toBe(1);
354
+ await t.close();
355
+ });
356
+
357
+ const sideEffects = [
358
+ { kind: "emit", effects: { reads: ["sql:notes"], emits: ["note.created"] } },
359
+ { kind: "send", effects: { reads: ["sql:notes"], sends: ["welcome"] } },
360
+ { kind: "fetch", effects: { reads: ["sql:notes"], fetches: ["api.example.com"] } },
361
+ { kind: "secret", effects: { reads: ["sql:notes"], secrets: ["stripe"] } },
362
+ { kind: "ask", effects: { reads: ["sql:notes"], asks: ["summarize"] } },
363
+ { kind: "decide", effects: { reads: ["sql:notes"], decides: ["route"] } },
364
+ { kind: "call", effects: { reads: ["sql:notes"], calls: ["notes.create"] } },
365
+ ] satisfies readonly { kind: string; effects: Effects }[];
366
+
367
+ for (const side of sideEffects) {
368
+ test(`${side.kind} effects run the handler on every call`, async () => {
369
+ resetBindings();
370
+ resetFlowSeq();
371
+ const db = store.sql("app", { schema: { notes } });
372
+ let lists = 0;
373
+ const list = on(
374
+ http.get("/notes").public(),
375
+ flow("notes.list", {
376
+ effects: side.effects,
377
+ do: () => {
378
+ lists += 1;
379
+ return [{ id: "n1", title: "Harbor" }];
380
+ },
381
+ }),
382
+ );
383
+ const app = oke({
384
+ name: `auto-cache-${side.kind}`,
385
+ stores: [db],
386
+ cache: { auto: true },
387
+ gate: { policies: [gate.public] },
388
+ }).adopt({ list });
389
+ const t = await createTestApp(app, { capability: "open" });
390
+ const notesApi = requireNotesApi(t);
391
+ await notesApi.list({});
392
+ await notesApi.list({});
393
+ expect(lists).toBe(2);
394
+ await t.close();
395
+ });
396
+ }
397
+
398
+ test("a ledgered call beside a store read is not cached", async () => {
399
+ resetBindings();
400
+ resetFlowSeq();
401
+ const db = store.sql("app", { schema: { notes } });
402
+ let lists = 0;
403
+ const list = on(
404
+ http.get("/notes", { out: z.array(NoteOut) }).public(),
405
+ flow("notes.list", {
406
+ do: async (_input, fx) => {
407
+ lists += 1;
408
+ await fx.store(db).select().from(notes);
409
+ await fx.call("notes.missing", {});
410
+ return [{ id: "n1", title: "Harbor" }];
411
+ },
412
+ }),
413
+ );
414
+ const app = oke({
415
+ name: "auto-cache-ledger-call",
416
+ stores: [db],
417
+ cache: { auto: true },
418
+ gate: { policies: [gate.public] },
419
+ }).adopt({ list });
420
+ const t = await createTestApp(app, { capability: "open" });
421
+ const notesApi = requireNotesApi(t);
422
+ await notesApi.list({});
423
+ await notesApi.list({});
424
+ expect(lists).toBe(2);
425
+ await t.close();
426
+ });
427
+
428
+ test("two tenants do not share a cache hit", async () => {
429
+ resetBindings();
430
+ resetFlowSeq();
431
+ const db = store.sql("app", { schema: { notes } });
432
+ let lists = 0;
433
+ const list = on(
434
+ http.get("/notes").public(),
435
+ flow("notes.list", {
436
+ effects: { reads: ["sql:notes"] },
437
+ do: () => {
438
+ lists += 1;
439
+ return [{ id: "n1", title: "Harbor" }];
440
+ },
441
+ }),
442
+ );
443
+ const app = oke({
444
+ name: "auto-cache-tenant",
445
+ stores: [db],
446
+ cache: { auto: true },
447
+ gate: { policies: [gate.public] },
448
+ }).adopt({ list });
449
+ const t = await createTestApp(app, { capability: "open" });
450
+ const trigger = list.triggers[0];
451
+ if (trigger === undefined) throw new Error("notes.list has no trigger");
452
+ await app.execute(list, {}, trigger, { tenant: { id: "ta" } });
453
+ await app.execute(list, {}, trigger, { tenant: { id: "ta" } });
454
+ expect(lists).toBe(1);
455
+ await app.execute(list, {}, trigger, { tenant: { id: "tb" } });
456
+ expect(lists).toBe(2);
457
+ await t.close();
458
+ });
459
+
460
+ test("two locales do not share a cache hit", async () => {
461
+ resetBindings();
462
+ resetFlowSeq();
463
+ const db = store.sql("app", { schema: { notes } });
464
+ let lists = 0;
465
+ const list = on(
466
+ http.get("/notes").public(),
467
+ flow("notes.list", {
468
+ effects: { reads: ["sql:notes"] },
469
+ do: () => {
470
+ lists += 1;
471
+ return [{ id: "n1", title: "Harbor" }];
472
+ },
473
+ }),
474
+ );
475
+ const app = oke({
476
+ name: "auto-cache-locale",
477
+ stores: [db],
478
+ cache: { auto: true },
479
+ config: { i18n: { locales: ["en", "ar"], default: "en" } },
480
+ gate: { policies: [gate.public] },
481
+ }).adopt({ list });
482
+ const t = await createTestApp(app, { capability: "open" });
483
+ const trigger = list.triggers[0];
484
+ if (trigger === undefined) throw new Error("notes.list has no trigger");
485
+ await app.execute(list, {}, trigger, { locale: "en" });
486
+ await app.execute(list, {}, trigger, { locale: "en" });
487
+ expect(lists).toBe(1);
488
+ await app.execute(list, {}, trigger, { locale: "ar" });
489
+ expect(lists).toBe(2);
490
+ await t.close();
491
+ });
264
492
  });
@@ -14,6 +14,7 @@ import type { AiDriver, AiOpenOptions } from "../../drivers/ai-types.ts";
14
14
  import { createAiRuntime, type AiRuntime } from "../../elements/ai.ts";
15
15
  import type { GateRuntime } from "../../elements/gate.ts";
16
16
  import type { VaultRuntime } from "../../elements/vault.ts";
17
+ import type { JournalStore } from "../journal.ts";
17
18
  import type { BootOptions } from "../boot.ts";
18
19
 
19
20
  /**
@@ -33,6 +34,7 @@ export function bindAi(
33
34
  env: ConfigEnv = "test",
34
35
  docker = false,
35
36
  vault?: VaultRuntime,
37
+ journalStore?: JournalStore,
36
38
  ): AiRuntime {
37
39
  const id = resolveAiDriverId(options, env, docker);
38
40
  const driver =
@@ -48,6 +50,7 @@ export function bindAi(
48
50
  },
49
51
  gates: options.ai?.gates ?? gate,
50
52
  now,
53
+ ...(journalStore ? { journalStore } : {}),
51
54
  });
52
55
  }
53
56
 
@@ -4,6 +4,8 @@
4
4
 
5
5
  import { resolveDriverId, type ConfigEnv } from "../../config/index.ts";
6
6
  import { CHANNEL_EMAIL_DEFAULTS, CHANNEL_SMS_DEFAULTS } from "../../config/driver-defaults.ts";
7
+ import { sharedPostgresClient, toPostgresParams } from "../../drivers/postgres.ts";
8
+ import type { PostgresClientLike } from "../../drivers/postgres.ts";
7
9
  import { openConsoleChannel } from "../../drivers/channel-console.ts";
8
10
  import { openMsegatChannel } from "../../drivers/channel-msegat.ts";
9
11
  import { openResendChannel } from "../../drivers/channel-resend.ts";
@@ -16,8 +18,59 @@ import { openUnifonicChannel } from "../../drivers/channel-unifonic.ts";
16
18
  import { openWaCloudChannel } from "../../drivers/channel-wa-cloud.ts";
17
19
  import type { ChannelDriver, ChannelOpenOptions } from "../../drivers/channel-types.ts";
18
20
  import { createChannelRuntime, type ChannelRuntime } from "../../elements/channel.ts";
21
+ import {
22
+ openPostgresChannelLedger,
23
+ type ChannelLedgerSql,
24
+ type PostgresChannelLedger,
25
+ } from "../../elements/channel/sql-ledger.ts";
19
26
  import type { BootOptions } from "../boot.ts";
20
27
 
28
+ /**
29
+ * Query/exec wrapper over the shared Bun.SQL pool.
30
+ *
31
+ * @param client - Shared postgres client
32
+ */
33
+ function asChannelSql(client: PostgresClientLike): ChannelLedgerSql {
34
+ return {
35
+ async query(sql, params = []) {
36
+ const result = await client.unsafe(toPostgresParams(sql, params), [...params]);
37
+ if (Array.isArray(result)) return result as Record<string, unknown>[];
38
+ return Array.from(result as ArrayLike<Record<string, unknown>>);
39
+ },
40
+ async exec(sql, params = []) {
41
+ const result = await client.unsafe(toPostgresParams(sql, params), [...params]);
42
+ if (
43
+ result &&
44
+ typeof result === "object" &&
45
+ "changes" in result &&
46
+ typeof result.changes === "number"
47
+ ) {
48
+ return { changes: result.changes };
49
+ }
50
+ if (Array.isArray(result)) return { changes: result.length };
51
+ return { changes: 0 };
52
+ },
53
+ };
54
+ }
55
+
56
+ /**
57
+ * Durable ledger when a SQL URL is set outside `test`.
58
+ * Injected stores win. `test` stays in memory.
59
+ *
60
+ * @param options - Boot options
61
+ * @param env - Active environment
62
+ */
63
+ async function maybeLedger(
64
+ options: BootOptions,
65
+ env: ConfigEnv,
66
+ ): Promise<PostgresChannelLedger | undefined> {
67
+ if (env === "test") return undefined;
68
+ if (options.channel?.suppression && options.channel.receipts) return undefined;
69
+ const url = process.env.DATABASE_URL ?? process.env.OKE_STORE_SQL_URL;
70
+ if (!url) return undefined;
71
+ return openPostgresChannelLedger(asChannelSql(sharedPostgresClient(url)));
72
+ }
73
+
21
74
  /**
22
75
  * Construct a Channel runtime (console inbox default).
23
76
  *
@@ -29,16 +82,24 @@ import type { BootOptions } from "../boot.ts";
29
82
  * @param now - Clock
30
83
  * @param docker - Prefer compose SMTP when active
31
84
  */
32
- export function bindChannel(
85
+ export async function bindChannel(
33
86
  options: BootOptions,
34
87
  env: ConfigEnv,
35
88
  now: () => number,
36
89
  docker = false,
37
- ): ChannelRuntime {
90
+ ): Promise<ChannelRuntime> {
91
+ const ledger = await maybeLedger(options, env);
38
92
  return createChannelRuntime({
39
93
  ...(options.channel ?? {}),
40
94
  drivers: options.channel?.drivers ?? defaultDrivers(options, env, docker),
41
95
  now,
96
+ ...(ledger
97
+ ? {
98
+ suppression: options.channel?.suppression ?? ledger.suppression,
99
+ receipts: options.channel?.receipts ?? ledger.receipts,
100
+ refresh: () => ledger.reload(),
101
+ }
102
+ : {}),
42
103
  });
43
104
  }
44
105