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
@@ -51,6 +51,7 @@ export async function bindVault(options: BootOptions, env: ConfigEnv): Promise<V
51
51
  env,
52
52
  cwd: process.cwd(),
53
53
  seed: {},
54
+ ...(options.config?.vault?.audit === undefined ? {} : { audit: options.config.vault.audit }),
54
55
  });
55
56
  const vault = createVaultRuntime({
56
57
  secrets: vaultSecrets,
@@ -282,9 +282,14 @@ export function resolveElementNeeds(options: BootOptions): ElementNeeds {
282
282
  const considerFlow = (f: AnyFlowDef): void => {
283
283
  const e = f.effects;
284
284
  if (f.durable === true) journal = true;
285
- if ((e?.reads?.length ?? 0) > 0 || (e?.writes?.length ?? 0) > 0) {
286
- store = true;
287
- }
285
+ const touchesStore = [...(e?.reads ?? []), ...(e?.writes ?? [])].some(
286
+ (ref) =>
287
+ ref.startsWith("sql:") ||
288
+ ref.startsWith("kv:") ||
289
+ ref.startsWith("files:") ||
290
+ ref.startsWith("index:"),
291
+ );
292
+ if (touchesStore) store = true;
288
293
  if ((e?.emits?.length ?? 0) > 0) signal = true;
289
294
  if ((e?.secrets?.length ?? 0) > 0) vault = true;
290
295
  if ((e?.sends?.length ?? 0) > 0) channel = true;
@@ -548,6 +553,12 @@ export async function bootApplication(input: BootOptions = {}): Promise<BootResu
548
553
  if (clockRt) void Promise.resolve(clockRt.tick()).catch(ignoreBenignSql);
549
554
  if (journal && durableResume) void Promise.resolve(durableResume()).catch(ignoreBenignSql);
550
555
  if (fleet) void fleet.maybeHeartbeat().catch(ignoreBenignSql);
556
+ const stem = ["run", "events"].join("-");
557
+ void import(`../elements/ai/${stem}.ts`)
558
+ .then((mod: typeof import("../elements/ai/run-events.ts")) =>
559
+ mod.sweepInstalledAgentEvents(),
560
+ )
561
+ .catch(ignoreBenignSql);
551
562
  }, period);
552
563
  schedulerTimer.unref?.();
553
564
  }
@@ -567,7 +578,7 @@ export async function bootApplication(input: BootOptions = {}): Promise<BootResu
567
578
  // 6. AI
568
579
  let ai = pre.ai;
569
580
  if (needs.ai && !ai) {
570
- ai = aiBind!.bindAi(options, gate, now, env, docker, vault);
581
+ ai = aiBind!.bindAi(options, gate, now, env, docker, vault, journal?.store);
571
582
  }
572
583
 
573
584
  // 7. Runs
@@ -601,6 +612,15 @@ export async function bootApplication(input: BootOptions = {}): Promise<BootResu
601
612
  docker,
602
613
  });
603
614
 
615
+ const needsIdempotencyJournal = (options.flows ?? []).some((f) => {
616
+ const mode = f.resolvedIdempotency?.mode;
617
+ return mode === "auto" || mode === "required";
618
+ });
619
+ if (!journal && needsIdempotencyJournal) {
620
+ journalBind ??= await loadBind<JournalBind>("journal");
621
+ journal = (await journalBind.bindJournal(options, env)).journal;
622
+ }
623
+
604
624
  // 9. `.adopt()` barrel freshness — opt-in only (same `rootDir` gate as
605
625
  // capability minting above; never a filesystem read the kernel performs
606
626
  // on its own).
@@ -712,7 +732,12 @@ export async function mintCapabilities(
712
732
 
713
733
  let manifest = options.manifest;
714
734
  let extractError: string | undefined;
715
- const needsManifest = manifest === undefined && flows.some((f) => f.effects === undefined);
735
+ const needsManifest =
736
+ manifest === undefined &&
737
+ flows.some(
738
+ (f) =>
739
+ f.effects === undefined || (f.idempotency !== false && f.resolvedIdempotency === undefined),
740
+ );
716
741
  if (needsManifest) {
717
742
  const rootDir = options.rootDir ?? process.env["OKE_ROOT_DIR"];
718
743
  if (rootDir) {
@@ -723,6 +748,10 @@ export async function mintCapabilities(
723
748
  }
724
749
 
725
750
  for (const f of flows) {
751
+ const manifestFlow = manifest?.flows?.[f.name];
752
+ if (manifestFlow?.idempotency) f.resolvedIdempotency = manifestFlow.idempotency;
753
+ if (manifestFlow?.usesRaw === true) f.usesRaw = true;
754
+
726
755
  if (f.effects !== undefined) {
727
756
  map.set(f.name, createCapabilityToken(f.name, f.effects));
728
757
  continue;
@@ -21,6 +21,11 @@ export const BUILTIN_ERROR_STATUS = {
21
21
  AuthFailed: 400,
22
22
  InternalError: 500,
23
23
  ServiceUnavailable: 503,
24
+ IdempotencyKeyMissing: 400,
25
+ IdempotencyKeyInvalid: 400,
26
+ IdempotencyKeyReused: 422,
27
+ IdempotencyInProgress: 409,
28
+ JournalLeaseBusy: 409,
24
29
  } as const;
25
30
 
26
31
  /** Built-in error code (including `DatabaseError`). */
@@ -86,6 +91,11 @@ export type BuiltinErrorMap = {
86
91
  };
87
92
  readonly InternalError: Record<string, never>;
88
93
  readonly ServiceUnavailable: { readonly retryAfter?: number };
94
+ readonly IdempotencyKeyMissing: Record<string, never>;
95
+ readonly IdempotencyKeyInvalid: Record<string, never>;
96
+ readonly IdempotencyKeyReused: Record<string, never>;
97
+ readonly IdempotencyInProgress: { readonly retryAfter?: number };
98
+ readonly JournalLeaseBusy: { readonly retryAfter?: number };
89
99
  };
90
100
 
91
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 {
@@ -56,6 +56,7 @@ export function buildClientDescriptor(
56
56
  ...(stamp.stream === true ? { stream: true as const } : {}),
57
57
  ...(stamp.matchKey !== undefined ? { matchKey: stamp.matchKey } : {}),
58
58
  ...(stamp.gates !== undefined && stamp.gates.length > 0 ? { gates: stamp.gates } : {}),
59
+ ...(def?.resolvedIdempotency !== undefined ? { idempotency: def.resolvedIdempotency } : {}),
59
60
  };
60
61
  }
61
62
  out[unit] = unitOut;
@@ -90,6 +90,39 @@ describe("fx.race — structured concurrency", () => {
90
90
  test("empty race throws", async () => {
91
91
  await expect(fxRace([])).rejects.toThrow(/empty/);
92
92
  });
93
+
94
+ test("a gated effect does not start after the branch is aborted", async () => {
95
+ const { fx } = createFxContext({
96
+ flow: "t",
97
+ effects: { reads: ["sql:notes"] },
98
+ });
99
+ const store = fx.store("sql:notes") as { get(key: string): Promise<unknown> };
100
+ let sawAbort = false;
101
+
102
+ const winner = await fx.race([
103
+ async () => {
104
+ await new Promise<void>((resolve) => {
105
+ const signal = fx.signal;
106
+ if (signal.aborted) {
107
+ resolve();
108
+ return;
109
+ }
110
+ signal.addEventListener("abort", () => resolve(), { once: true });
111
+ });
112
+ try {
113
+ await store.get("n1");
114
+ } catch (err) {
115
+ sawAbort = isAbortError(err);
116
+ }
117
+ return "slow";
118
+ },
119
+ async () => "fast",
120
+ ]);
121
+
122
+ expect(winner).toBe("fast");
123
+ await new Promise((r) => setTimeout(r, 20));
124
+ expect(sawAbort).toBe(true);
125
+ });
93
126
  });
94
127
 
95
128
  describe("fx.using — scoped cleanup", () => {
@@ -0,0 +1,441 @@
1
+ /**
2
+ * Decision labels and the drift flag on the journal driver.
3
+ *
4
+ * A sibling of `oke_idempotency`. Rows are not journal runs. Tables are
5
+ * created only when this store is opened, which boot does when the app
6
+ * declares decisions.
7
+ *
8
+ * @module
9
+ */
10
+
11
+ import { mkdir } from "node:fs/promises";
12
+ import { dirname } from "node:path";
13
+ import type { DecisionCandidate, DecisionLabel } from "../elements/ai/decisions/certificate.ts";
14
+
15
+ /** SQL surface the postgres journal client already exposes. */
16
+ export interface DecisionLabelSql {
17
+ query(sql: string, params?: readonly unknown[]): Promise<Record<string, unknown>[]>;
18
+ exec(sql: string, params?: readonly unknown[]): Promise<{ changes: number }>;
19
+ }
20
+
21
+ /** Drift for one decision. A later certificate (`certifiedAt` newer) ignores it. */
22
+ export interface DecisionDriftFlag {
23
+ readonly suspended: boolean;
24
+ /** Epoch ms of the certificate that was current when the flag was written. */
25
+ readonly certifiedAt: number;
26
+ }
27
+
28
+ /** @deprecated Use {@link DecisionDriftFlag}. Kept for stored rows. */
29
+ export interface DecisionDriftRecord extends DecisionDriftFlag {}
30
+
31
+ /** Thrown when the postgres label store cannot open or query. */
32
+ export class DecisionLabelStoreError extends Error {
33
+ /**
34
+ * @param message - What failed
35
+ * @param options - Underlying driver error
36
+ */
37
+ constructor(message: string, options?: { cause?: unknown }) {
38
+ super(message, options);
39
+ this.name = "DecisionLabelStoreError";
40
+ }
41
+ }
42
+
43
+ /** Persistence for review labels and the app drift flag. */
44
+ export interface DecisionLabelStore {
45
+ insert(label: DecisionLabel, at: number): Promise<void>;
46
+ /**
47
+ * Labels for one decision. Omit `tenant` to read every tenant.
48
+ *
49
+ * @param decision - Decision name
50
+ * @param tenant - Tenant filter
51
+ */
52
+ list(decision?: string, tenant?: string | null): Promise<DecisionLabel[]>;
53
+ /** Drift flags keyed by decision name. */
54
+ drift(): Promise<Readonly<Record<string, DecisionDriftFlag>>>;
55
+ /**
56
+ * Persist one decision's suspension flag.
57
+ *
58
+ * @param decision - Decision name
59
+ * @param record - Flag and the certificate time it belongs to
60
+ */
61
+ setDrift(decision: string, record: DecisionDriftFlag): Promise<void>;
62
+ /**
63
+ * Store one candidate on this journal. Any instance can read it after a restart.
64
+ *
65
+ * @param decision - Decision name
66
+ * @param entry - Lock entry the clock fitted
67
+ */
68
+ putCandidate(decision: string, entry: DecisionCandidate): Promise<void>;
69
+ /**
70
+ * Candidate last written for this decision.
71
+ *
72
+ * @param decision - Decision name
73
+ */
74
+ getCandidate(decision: string): Promise<DecisionCandidate | undefined>;
75
+ /** Decision names that have a candidate. */
76
+ listCandidates(): Promise<readonly string[]>;
77
+ }
78
+
79
+ interface StoredLabel extends DecisionLabel {
80
+ readonly at: number;
81
+ }
82
+
83
+ /**
84
+ * In-memory label store.
85
+ */
86
+ export function createMemoryDecisionLabelStore(): DecisionLabelStore {
87
+ const rows: StoredLabel[] = [];
88
+ const candidates = new Map<string, DecisionCandidate>();
89
+ const driftFlags = new Map<string, DecisionDriftFlag>();
90
+ return {
91
+ async insert(label, at) {
92
+ rows.push({ ...label, at });
93
+ },
94
+ async list(decision, tenant) {
95
+ return rows
96
+ .filter((row) => decision === undefined || row.decision === decision)
97
+ .filter((row) => tenant === undefined || (row.tenant ?? null) === tenant)
98
+ .map(toLabel);
99
+ },
100
+ async drift() {
101
+ return Object.fromEntries(driftFlags);
102
+ },
103
+ async setDrift(decision, next) {
104
+ if (next.suspended) driftFlags.set(decision, next);
105
+ else driftFlags.delete(decision);
106
+ },
107
+ async putCandidate(decision, entry) {
108
+ candidates.set(decision, entry);
109
+ },
110
+ async getCandidate(decision) {
111
+ return candidates.get(decision);
112
+ },
113
+ async listCandidates() {
114
+ return [...candidates.keys()];
115
+ },
116
+ };
117
+ }
118
+
119
+ /**
120
+ * File-backed label store beside the journal file.
121
+ *
122
+ * @param path - JSON file path
123
+ */
124
+ export function createFileDecisionLabelStore(path: string): DecisionLabelStore {
125
+ let rows: StoredLabel[] | undefined;
126
+ let candidates: Record<string, DecisionCandidate> = {};
127
+ let driftFlags: Record<string, DecisionDriftFlag> = {};
128
+ const load = async (): Promise<void> => {
129
+ if (rows) return;
130
+ rows = [];
131
+ const file = Bun.file(path);
132
+ if (await file.exists()) {
133
+ const raw = (await file.json()) as {
134
+ rows?: StoredLabel[];
135
+ suspended?: boolean;
136
+ certifiedAt?: number;
137
+ drifts?: Record<string, DecisionDriftFlag>;
138
+ candidates?: Record<string, DecisionCandidate>;
139
+ };
140
+ rows = raw.rows ?? [];
141
+ candidates = raw.candidates ?? {};
142
+ driftFlags = raw.drifts ?? {};
143
+ const legacy =
144
+ driftFlags["*"] ??
145
+ (raw.suspended === true && Object.keys(driftFlags).length === 0
146
+ ? { suspended: true as const, certifiedAt: raw.certifiedAt ?? 0 }
147
+ : undefined);
148
+ if (legacy?.suspended) {
149
+ delete driftFlags["*"];
150
+ for (const name of declaredDriftDecisions) {
151
+ driftFlags[name] = { suspended: legacy.suspended, certifiedAt: legacy.certifiedAt };
152
+ }
153
+ await mkdir(dirname(path), { recursive: true });
154
+ await Bun.write(path, JSON.stringify({ rows, candidates, drifts: driftFlags }));
155
+ }
156
+ }
157
+ };
158
+ const flush = async (): Promise<void> => {
159
+ await mkdir(dirname(path), { recursive: true });
160
+ await Bun.write(
161
+ path,
162
+ JSON.stringify({
163
+ rows,
164
+ candidates,
165
+ drifts: driftFlags,
166
+ }),
167
+ );
168
+ };
169
+ return {
170
+ async insert(label, at) {
171
+ await load();
172
+ rows!.push({ ...label, at });
173
+ await flush();
174
+ },
175
+ async list(decision, tenant) {
176
+ await load();
177
+ return rows!
178
+ .filter((row) => decision === undefined || row.decision === decision)
179
+ .filter((row) => tenant === undefined || (row.tenant ?? null) === tenant)
180
+ .map(toLabel);
181
+ },
182
+ async drift() {
183
+ await load();
184
+ return driftFlags;
185
+ },
186
+ async setDrift(decision, next) {
187
+ await load();
188
+ if (next.suspended) driftFlags[decision] = next;
189
+ else delete driftFlags[decision];
190
+ await flush();
191
+ },
192
+ async putCandidate(decision, entry) {
193
+ await load();
194
+ candidates[decision] = entry;
195
+ await flush();
196
+ },
197
+ async getCandidate(decision) {
198
+ await load();
199
+ return candidates[decision];
200
+ },
201
+ async listCandidates() {
202
+ await load();
203
+ return Object.keys(candidates);
204
+ },
205
+ };
206
+ }
207
+
208
+ /** Decision names an old app-wide drift flag is copied onto. Boot sets this before open. */
209
+ let declaredDriftDecisions: readonly string[] = [];
210
+
211
+ /**
212
+ * Declare which decisions receive a legacy app-wide drift flag.
213
+ *
214
+ * @param names - Decision names from the manifest
215
+ */
216
+ export function setDeclaredDriftDecisions(names: readonly string[]): void {
217
+ declaredDriftDecisions = names;
218
+ }
219
+
220
+ /**
221
+ * Postgres label store on the journal connection. Creates the tables.
222
+ * A failed init or query throws {@link DecisionLabelStoreError}.
223
+ *
224
+ * @param sql - Journal SQL client
225
+ */
226
+ export async function createPostgresDecisionLabelStore(
227
+ sql: DecisionLabelSql,
228
+ ): Promise<DecisionLabelStore> {
229
+ try {
230
+ await sql.exec(`CREATE TABLE IF NOT EXISTS oke_decision_labels (
231
+ decision_id TEXT NOT NULL,
232
+ question TEXT NOT NULL,
233
+ value TEXT NOT NULL,
234
+ propensity DOUBLE PRECISION NOT NULL,
235
+ reviewer TEXT NOT NULL,
236
+ locale TEXT,
237
+ model TEXT,
238
+ tenant TEXT,
239
+ score DOUBLE PRECISION,
240
+ loss INTEGER,
241
+ raw TEXT,
242
+ at BIGINT NOT NULL,
243
+ input TEXT
244
+ )`);
245
+ await sql.exec(`ALTER TABLE oke_decision_labels ADD COLUMN IF NOT EXISTS input TEXT`);
246
+ await sql.exec(`ALTER TABLE oke_decision_labels ADD COLUMN IF NOT EXISTS review_id TEXT`);
247
+ await migrateDecisionDriftTable(sql, declaredDriftDecisions);
248
+ await sql.exec(`CREATE TABLE IF NOT EXISTS oke_decision_candidates (
249
+ decision_id TEXT PRIMARY KEY,
250
+ body TEXT NOT NULL
251
+ )`);
252
+ } catch (cause) {
253
+ throw new DecisionLabelStoreError("decision label store failed to open", { cause });
254
+ }
255
+ const query = async (
256
+ statement: string,
257
+ params?: readonly unknown[],
258
+ ): Promise<Record<string, unknown>[]> => {
259
+ try {
260
+ return await sql.query(statement, params);
261
+ } catch (cause) {
262
+ throw new DecisionLabelStoreError("decision label store query failed", { cause });
263
+ }
264
+ };
265
+ const exec = async (statement: string, params?: readonly unknown[]): Promise<void> => {
266
+ try {
267
+ await sql.exec(statement, params);
268
+ } catch (cause) {
269
+ throw new DecisionLabelStoreError("decision label store query failed", { cause });
270
+ }
271
+ };
272
+ return {
273
+ async insert(label, at) {
274
+ await exec(
275
+ `INSERT INTO oke_decision_labels
276
+ (decision_id, question, value, propensity, reviewer, locale, model, tenant, score, loss, raw, at, input, review_id)
277
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
278
+ [
279
+ label.decision,
280
+ label.question,
281
+ JSON.stringify(label.value),
282
+ label.propensity,
283
+ label.reviewer,
284
+ label.locale ?? null,
285
+ label.model ?? null,
286
+ label.tenant ?? null,
287
+ label.score ?? null,
288
+ label.loss ?? null,
289
+ label.raw === undefined ? null : JSON.stringify(label.raw),
290
+ at,
291
+ label.input === undefined ? null : JSON.stringify(label.input),
292
+ label.reviewId ?? null,
293
+ ],
294
+ );
295
+ },
296
+ async list(decision, tenant) {
297
+ const clauses: string[] = [];
298
+ const args: (string | null)[] = [];
299
+ if (decision !== undefined) {
300
+ clauses.push("decision_id = ?");
301
+ args.push(decision);
302
+ }
303
+ if (tenant !== undefined) {
304
+ clauses.push("tenant IS NOT DISTINCT FROM ?");
305
+ args.push(tenant);
306
+ }
307
+ const where = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
308
+ const found = await query(
309
+ `SELECT decision_id, question, value, propensity, reviewer, locale, model, tenant, score, loss, raw, at, input, review_id
310
+ FROM oke_decision_labels ${where}`,
311
+ args,
312
+ );
313
+ return found.map((row) => ({
314
+ decision: String(row.decision_id),
315
+ question: String(row.question),
316
+ value: JSON.parse(String(row.value)) as unknown,
317
+ propensity: Number(row.propensity),
318
+ reviewer: String(row.reviewer),
319
+ ...(row.locale != null ? { locale: String(row.locale) } : {}),
320
+ ...(row.model != null ? { model: String(row.model) } : {}),
321
+ ...(row.tenant != null ? { tenant: String(row.tenant) } : {}),
322
+ ...(row.score != null ? { score: Number(row.score) } : {}),
323
+ ...(row.loss != null ? { loss: Number(row.loss) } : {}),
324
+ ...(row.raw != null ? { raw: JSON.parse(String(row.raw)) as unknown } : {}),
325
+ ...(row.input != null ? { input: JSON.parse(String(row.input)) as unknown } : {}),
326
+ ...(row.review_id != null ? { reviewId: String(row.review_id) } : {}),
327
+ at: Number(row.at),
328
+ }));
329
+ },
330
+ async drift() {
331
+ const rows = await query(
332
+ `SELECT decision_id, suspended, certified_at FROM oke_decision_drift`,
333
+ );
334
+ const flags: Record<string, DecisionDriftFlag> = {};
335
+ for (const row of rows) {
336
+ const name = String(row.decision_id ?? "");
337
+ if (!name) continue;
338
+ flags[name] = {
339
+ suspended: Number(row.suspended) === 1,
340
+ certifiedAt: Number(row.certified_at ?? 0),
341
+ };
342
+ }
343
+ return flags;
344
+ },
345
+ async setDrift(decision, next) {
346
+ if (!next.suspended) {
347
+ await exec(`DELETE FROM oke_decision_drift WHERE decision_id = ?`, [decision]);
348
+ return;
349
+ }
350
+ await exec(
351
+ `INSERT INTO oke_decision_drift (decision_id, suspended, certified_at) VALUES (?, ?, ?)
352
+ ON CONFLICT (decision_id) DO UPDATE SET suspended = excluded.suspended, certified_at = excluded.certified_at`,
353
+ [decision, 1, next.certifiedAt],
354
+ );
355
+ },
356
+ async putCandidate(decision, entry) {
357
+ await exec(
358
+ `INSERT INTO oke_decision_candidates (decision_id, body) VALUES (?, ?)
359
+ ON CONFLICT (decision_id) DO UPDATE SET body = excluded.body`,
360
+ [decision, JSON.stringify(entry)],
361
+ );
362
+ },
363
+ async getCandidate(decision) {
364
+ const rows = await query(`SELECT body FROM oke_decision_candidates WHERE decision_id = ?`, [
365
+ decision,
366
+ ]);
367
+ const body = rows[0]?.body;
368
+ if (typeof body !== "string") return undefined;
369
+ const parsed = JSON.parse(body) as DecisionCandidate;
370
+ if (!parsed || typeof parsed.model !== "string" || !parsed.questions) return undefined;
371
+ return parsed;
372
+ },
373
+ async listCandidates() {
374
+ const rows = await query(`SELECT decision_id FROM oke_decision_candidates`);
375
+ return rows.map((row) => String(row.decision_id));
376
+ },
377
+ };
378
+ }
379
+
380
+ function toLabel(row: StoredLabel): DecisionLabel {
381
+ return {
382
+ decision: row.decision,
383
+ question: row.question,
384
+ value: row.value,
385
+ propensity: row.propensity,
386
+ reviewer: row.reviewer,
387
+ ...(row.locale !== undefined ? { locale: row.locale } : {}),
388
+ ...(row.model !== undefined ? { model: row.model } : {}),
389
+ ...(row.tenant !== undefined ? { tenant: row.tenant } : {}),
390
+ ...(row.score !== undefined ? { score: row.score } : {}),
391
+ ...(row.loss !== undefined ? { loss: row.loss } : {}),
392
+ ...(row.raw !== undefined ? { raw: row.raw } : {}),
393
+ ...(row.input !== undefined ? { input: row.input } : {}),
394
+ ...(row.reviewId !== undefined ? { reviewId: row.reviewId } : {}),
395
+ at: row.at,
396
+ };
397
+ }
398
+
399
+ /**
400
+ * Recreate `oke_decision_drift` when it still uses an app-wide `id` row.
401
+ * A suspended flag is copied onto every declared decision.
402
+ *
403
+ * @param sql - Journal SQL client
404
+ * @param names - Declared decision names
405
+ */
406
+ async function migrateDecisionDriftTable(
407
+ sql: DecisionLabelSql,
408
+ names: readonly string[],
409
+ ): Promise<void> {
410
+ const create = `CREATE TABLE IF NOT EXISTS oke_decision_drift (
411
+ decision_id TEXT PRIMARY KEY,
412
+ suspended INTEGER NOT NULL,
413
+ certified_at BIGINT NOT NULL
414
+ )`;
415
+ let columns: string[] = [];
416
+ try {
417
+ const rows = await sql.query(
418
+ `SELECT column_name FROM information_schema.columns WHERE table_name = 'oke_decision_drift'`,
419
+ );
420
+ columns = rows.map((row) => String(row.column_name ?? "").toLowerCase());
421
+ } catch {
422
+ columns = [];
423
+ }
424
+ if (columns.includes("id") && !columns.includes("decision_id")) {
425
+ const old = await sql.query(`SELECT * FROM oke_decision_drift`);
426
+ const flag = old[0];
427
+ await sql.exec(`DROP TABLE oke_decision_drift`);
428
+ await sql.exec(create.replace("IF NOT EXISTS ", ""));
429
+ if (flag && Number(flag.suspended) === 1) {
430
+ const certifiedAt = Number(flag.certified_at ?? 0);
431
+ for (const name of names) {
432
+ await sql.exec(
433
+ `INSERT INTO oke_decision_drift (decision_id, suspended, certified_at) VALUES (?, ?, ?)`,
434
+ [name, 1, certifiedAt],
435
+ );
436
+ }
437
+ }
438
+ return;
439
+ }
440
+ await sql.exec(create);
441
+ }
@@ -71,7 +71,7 @@ describe("boot-level: undeclared-effects flow, no manifest / rootDir", () => {
71
71
  const create = buildUnannotatedCreateFlow(db);
72
72
  const app = oke({ name: "stamp-open", gate: { policies: [gate.public] } }).adopt({ create });
73
73
  Object.assign(app.$options, { stores: [db] });
74
- await createTestApp(app); // createTestApp always boots env: "test"
74
+ await createTestApp(app, { capability: "open" });
75
75
 
76
76
  const res = await app.fetch(
77
77
  new Request("http://localhost/notes", {
@@ -157,7 +157,7 @@ describe("boot-level: table-ref resolution stays backward compatible", () => {
157
157
  create,
158
158
  });
159
159
  Object.assign(app.$options, { stores: [db] });
160
- await createTestApp(app);
160
+ await createTestApp(app, { capability: "open" });
161
161
 
162
162
  const res = await app.fetch(
163
163
  new Request("http://localhost/notes", {
@@ -21,9 +21,10 @@ describe("effects — reversibility tiers", () => {
21
21
  secret: "capability",
22
22
  call: "portal",
23
23
  fetch: "irreversible",
24
+ decide: "irreversible",
24
25
  };
25
26
 
26
- expect(EFFECT_KIND_TIERS).toHaveLength(9);
27
+ expect(EFFECT_KIND_TIERS).toHaveLength(10);
27
28
  for (const { kind, reversibility } of EFFECT_KIND_TIERS) {
28
29
  expect(reversibility).toBe(expected[kind]);
29
30
  expect(reversibilityOf(kind)).toBe(expected[kind]);