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
@@ -69,7 +69,7 @@ invalidated. Delivery follows `channels` order for addresses you pass.
69
69
  </Step>
70
70
 
71
71
  <Step>
72
- ### Resend on another channel (app mode only)
72
+ ### Resend
73
73
 
74
74
  ```typescript
75
75
  const { data } = await api.auth.resendOtp({
@@ -79,8 +79,8 @@ const { data } = await api.auth.resendOtp({
79
79
  });
80
80
  ```
81
81
 
82
- Same code, same TTL. Default cooldown is 60 seconds. Provider mode has no
83
- resend surface — the provider owns the code.
82
+ App mode keeps the same code and TTL. Provider mode is SMS only and asks the
83
+ provider for a new code. Default cooldown is 60 seconds either way.
84
84
 
85
85
  </Step>
86
86
 
@@ -108,18 +108,18 @@ const { data } = await api.auth.verifyOtp({
108
108
 
109
109
  ## Modes
110
110
 
111
- | | Provider mode | App mode |
112
- | -------------------- | ----------------------------- | --------------------------------------------- |
113
- | Config | `otp({ mode: "provider" })` | `otp({ mode: "app", channels: [...] })` |
114
- | Who owns the code | Provider (Verify API) | Your app |
115
- | Delivery | `fx.sendOtp` / `fx.verifyOtp` | `fx.deliverOtp` (Channel templates) |
116
- | Channels | SMS only | `sms` · `whatsapp` · `email` (declared order) |
117
- | Resend other channel | Impossible | `POST /auth/otp/resend` |
118
- | `exposeDevOtp` | Forbidden | Optional (default off) |
111
+ | | Provider mode | App mode |
112
+ | ----------------- | ----------------------------- | --------------------------------------------- |
113
+ | Config | `otp({ mode: "provider" })` | `otp({ mode: "app", channels: [...] })` |
114
+ | Who owns the code | Provider (Verify API) | Your app |
115
+ | Delivery | `fx.sendOtp` / `fx.verifyOtp` | `fx.deliverOtp` (Channel templates) |
116
+ | Channels | SMS only | `sms` · `whatsapp` · `email` (declared order) |
117
+ | Resend | SMS only, new provider code | Same code, any declared channel |
118
+ | `exposeDevOtp` | Forbidden | Optional (default off) |
119
119
 
120
120
  <Callout type="warn" title="Provider mode limitation">
121
- Resend-via-different-channel is impossible in provider mode — the code value is never visible to
122
- OKE. Use app mode when you need SMS → email fallback for the same code.
121
+ A provider resend is a new SMS code (`resendCooldownMs`, default 60s). The code never reaches OKE,
122
+ so SMS → email for the same code needs `mode: "app"`.
123
123
  </Callout>
124
124
 
125
125
  ### Provider mode setup
@@ -154,27 +154,27 @@ Override copy with `oke({ channel.catalog })` or `.channelCatalog(…)` (later l
154
154
 
155
155
  ## Options
156
156
 
157
- | Option | Type | Default | Meaning |
158
- | ------------------ | -------------------------------- | -------------------------- | ----------------------------------- |
159
- | `mode` | `"provider" \| "app"` | required | Delivery mechanism — no auto |
160
- | `channels` | `("sms"\|"whatsapp"\|"email")[]` | required in app mode | Build-time preferred order |
161
- | `ttlMs` | `number` | 10m | Challenge lifetime |
162
- | `resendCooldownMs` | `number` | 60s | App-mode resend spacing |
163
- | `exposeDevOtp` | `boolean` | `false` | App mode only — raw OTP in response |
164
- | `from` | `string` | `OKE <no-reply@oke.local>` | Email template From |
165
- | `secret` | `string` | active\* | Auth secret (\*from `gate.auth`) |
166
- | `sessions` | `SessionStore` | active\* | Session store |
167
- | `identities` | `IdentityStore` | new | Email → user |
168
- | `phones` | `PhoneStore` | new | Phone → user |
169
- | `verifications` | `VerificationStore` | new | Challenge store |
157
+ | Option | Type | Default | Meaning |
158
+ | ------------------ | -------------------------------- | -------------------------- | ------------------------------------ |
159
+ | `mode` | `"provider" \| "app"` | required | Delivery mechanism — no auto |
160
+ | `channels` | `("sms"\|"whatsapp"\|"email")[]` | required in app mode | Build-time preferred order |
161
+ | `ttlMs` | `number` | 10m | Challenge lifetime |
162
+ | `resendCooldownMs` | `number` | 60s | Spacing between resends (both modes) |
163
+ | `exposeDevOtp` | `boolean` | `false` | App mode only — raw OTP in response |
164
+ | `from` | `string` | `OKE <no-reply@oke.local>` | Email template From |
165
+ | `secret` | `string` | active\* | Auth secret (\*from `gate.auth`) |
166
+ | `sessions` | `SessionStore` | active\* | Session store |
167
+ | `identities` | `IdentityStore` | new | Email → user |
168
+ | `phones` | `PhoneStore` | new | Phone → user |
169
+ | `verifications` | `VerificationStore` | new | Challenge store |
170
170
 
171
171
  ## Surfaces
172
172
 
173
- | Flow | Path | Gate | Mode |
174
- | ----------------- | ------------------------ | ------------------------ | ------------- |
175
- | `auth.requestOtp` | `POST /auth/otp/request` | `gate.public` + otp rate | both |
176
- | `auth.verifyOtp` | `POST /auth/otp/verify` | `gate.public` + otp rate | both |
177
- | `auth.resendOtp` | `POST /auth/otp/resend` | `gate.public` + otp rate | app mode only |
173
+ | Flow | Path | Gate | Mode |
174
+ | ----------------- | ------------------------ | ------------------------ | ------------------------- |
175
+ | `auth.requestOtp` | `POST /auth/otp/request` | `gate.public` + otp rate | both |
176
+ | `auth.verifyOtp` | `POST /auth/otp/verify` | `gate.public` + otp rate | both |
177
+ | `auth.resendOtp` | `POST /auth/otp/resend` | `gate.public` + otp rate | both (provider: SMS only) |
178
178
 
179
179
  ## Troubleshooting
180
180
 
@@ -1,13 +1,13 @@
1
1
  ---
2
2
  title: "Dragonfly"
3
- description: "Multi-threaded Redis-wire runtime — OKE_STORE_KV_PASSWORD, memlock ulimit, no maxmemory-policy flag."
3
+ description: "Default store.kv image — multi-threaded Redis-wire runtime, memlock ulimit, OKE_STORE_KV_PASSWORD."
4
4
  icon: "Zap"
5
5
  source: "docs/spec/unified-theory.md"
6
6
  ---
7
7
 
8
- Dragonfly is a multi-threaded, memory-efficient reimplementation of the Redis protocol —
9
- same `redis://` wire format, different engine. Slots behind the same `redis` driver with
10
- zero Flow changes.
8
+ Dragonfly is the default `store.kv` image in `dev` and `prod`: a multi-threaded Redis-protocol engine.
9
+ Same `redis://` wire format and the same `redis` driver, so Flows do not change.
10
+ Pin Redis or Valkey when you want those engines instead.
11
11
 
12
12
  <Callout title="The one rule">
13
13
  Driver id stays `redis`. Dragonfly is an image choice — every KV call through `fx.store` behaves
@@ -23,7 +23,7 @@ zero Flow changes.
23
23
 
24
24
  ```typescript title="oke.config.ts"
25
25
  images: {
26
- "store.kv": "docker.dragonflydb.io/dragonflydb/dragonfly",
26
+ "store.kv": "docker.dragonflydb.io/dragonflydb/dragonfly:v2.0.0",
27
27
  },
28
28
  ```
29
29
 
@@ -51,15 +51,15 @@ Driver id stays `redis` for every image below.
51
51
 
52
52
  <Cards>
53
53
  <Card
54
- title="Redis"
55
- description="Default store.kv — requirepass + maxmemory."
56
- href="/docs/recipes/redis"
54
+ title="Dragonfly"
55
+ description="Default store.kv — multi-threaded Redis-wire runtime."
56
+ href="/docs/recipes/dragonfly"
57
57
  />
58
58
  <Card title="Valkey" description="BSD-licensed Redis-wire fork." href="/docs/recipes/valkey" />
59
59
  <Card
60
- title="Dragonfly"
61
- description="Multi-threaded Redis-wire runtime."
62
- href="/docs/recipes/dragonfly"
60
+ title="Redis"
61
+ description="Opt-in Redis Open Source — requirepass + maxmemory."
62
+ href="/docs/recipes/redis"
63
63
  />
64
64
  </Cards>
65
65
 
@@ -28,7 +28,7 @@ drivers: {
28
28
  },
29
29
  },
30
30
  images: {
31
- channel: { email: "axllent/mailpit:v1.31.1" },
31
+ channel: { email: "axllent/mailpit:v1.31.2" },
32
32
  },
33
33
  ```
34
34
 
@@ -28,7 +28,7 @@ drivers: {
28
28
  },
29
29
  },
30
30
  images: {
31
- store: { index: "getmeili/meilisearch:v1.53" },
31
+ store: { index: "getmeili/meilisearch:v1.54" },
32
32
  },
33
33
  ```
34
34
 
@@ -26,7 +26,7 @@ create-oke asks **Add PgDog connection pooling…?** (or pass `--pgdog`). Manual
26
26
  ```typescript title="oke.config.ts"
27
27
  images: {
28
28
  "store.sql": "postgres:18-alpine",
29
- pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.57",
29
+ pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.59",
30
30
  },
31
31
  ```
32
32
 
@@ -106,11 +106,11 @@ Scale out with [PgDog](/docs/recipes/pgdog) so `N × Bun.SQL pool` does not exha
106
106
  Recreate `store-sql` after a recipe change — the data volume can stay.
107
107
  </Callout>
108
108
 
109
- | Step | What you do |
110
- | ------- | ----------------------------------------------------------------- |
111
- | Preload | Recipe already sets `shared_preload_libraries=pg_stat_statements` |
112
- | Create | `CREATE EXTENSION IF NOT EXISTS pg_stat_statements` |
113
- | Console | Store → SQL band → **Performance** |
109
+ | Step | What you do |
110
+ | ------- | ---------------------------------------------------------------------- |
111
+ | Preload | Recipe already sets `shared_preload_libraries=pg_stat_statements` |
112
+ | Create | `oke db push` runs `CREATE EXTENSION IF NOT EXISTS pg_stat_statements` |
113
+ | Console | Store → SQL band → **Performance** |
114
114
 
115
115
  Default `postgres:18-alpine` does **not** ship `hypopg` / `index_advisor`. Pin the
116
116
  opt-in image when you want Suggest indexes:
@@ -137,8 +137,8 @@ set. Re-run `oke dev` so the stack writes `.env.local`, or export
137
137
  <Accordion title="PgStatStatementsNotPreloaded">
138
138
 
139
139
  The Console Performance view needs the library preloaded **and** the extension
140
- created. Recreate `store-sql` so the new `command` applies, then run
141
- `CREATE EXTENSION IF NOT EXISTS pg_stat_statements`.
140
+ created. Recreate `store-sql` so the new `command` applies, then run `oke db push`
141
+ (or open Performance again — it creates the extension when preload is already on).
142
142
 
143
143
  Existing clusters that started before this recipe change keep the old
144
144
  postmaster flags until recreate.
@@ -167,5 +167,9 @@ Postgres refusing auth.
167
167
  <Cards>
168
168
  <Card title="PgDog" description="Add pooling in front of Postgres." href="/docs/recipes/pgdog" />
169
169
  <Card title="Neon" description="A managed alternative." href="/docs/providers/neon" />
170
- <Card title="Redis" description="The default store.kv image." href="/docs/recipes/redis" />
170
+ <Card
171
+ title="Dragonfly"
172
+ description="The default store.kv image."
173
+ href="/docs/recipes/dragonfly"
174
+ />
171
175
  </Cards>
@@ -1,13 +1,13 @@
1
1
  ---
2
2
  title: "Redis"
3
- description: "Default store.kv image — OKE_STORE_KV_PASSWORD, maxmemory policy, ephemeral data, REDIS_URL."
3
+ description: "Opt-in store.kv image — OKE_STORE_KV_PASSWORD, maxmemory policy, ephemeral data, REDIS_URL."
4
4
  icon: "Zap"
5
5
  source: "docs/spec/unified-theory.md"
6
6
  ---
7
7
 
8
- Redis is the default `store.kv` image in `dev` and `prod`. `oke docker` matches image
9
- references containing `redis` or `keydb` and derives a password-required server — no
10
- unauthenticated instance.
8
+ Redis is an opt-in `store.kv` image. The default pin is [Dragonfly](/docs/recipes/dragonfly).
9
+ `oke docker` matches image references containing `redis` or `keydb` and derives a
10
+ password-required server — no unauthenticated instance.
11
11
 
12
12
  <Callout title="The one rule">
13
13
  The driver id stays `redis` for every Redis-wire image — Redis, Valkey, Dragonfly, or a managed
@@ -134,7 +134,8 @@ Override app / console / mcp listen ports in `oke.config.ts` `ports` — see
134
134
  | `docker` | Derive compose · clean leftover stacks | `--prod\|-p`, `--out`, `--config`, `--manifest`; sub `clean` |
135
135
  | `images` | List / pin digests | `list` · `pin` |
136
136
  | `build` | Tree-shaken bundle | `--target\|-t bun\|node\|edge`, `--entry`, `--outdir` |
137
- | `eval` | Prompt eval sets (CI gate) | `--manifest` |
137
+ | `eval` | Prompt eval sets (CI gate) | `--manifest`, `--certify` (decision seed certificate; prompt evals stay as they are) |
138
+ | `decide` | Write a decision lockfile from the operator candidate | `promote <name>` (`--origin`, `--lock`) |
138
139
  | `ai` | Configure AI driver + models | `setup` (`--provider`, `--chat`, `--vision`, `--embed`, `--yes`) |
139
140
  | `branch` | Fork journaled state | `<name>`, `--at\|-a` |
140
141
  | `replay` | Re-invoke a past Flow from Runs | `--request-id\|-r`, `--entry`, `--dry-run`, `--live` |
@@ -136,11 +136,13 @@ sql: {
136
136
  },
137
137
  ```
138
138
 
139
- | Field | Type | Meaning |
140
- | ---------- | ----------------- | ------------------------------------------- |
141
- | `url` | connection string | Overrides the env-var resolution |
142
- | `pool` | `{ max?, min? }` | SQL pool sizing |
143
- | `replicas` | `string[]` | Read-only routing targets (read flows only) |
139
+ | Field | Type | Meaning |
140
+ | ---------- | ----------------- | --------------------------------------------------------------------------------- |
141
+ | `url` | connection string | Overrides the env-var resolution |
142
+ | `pool` | `{ max?, min? }` | `max` is the Bun.SQL pool size. Unset keeps 8. `min` is accepted and not applied |
143
+ | `replicas` | `string[]` | Read-only flows use these URLs. Each replica opens its own pool at the same `max` |
144
+
145
+ A store `max` of 20 does not share a pool another part of the process already opened at 8. Writes stay on the primary.
144
146
 
145
147
  ## images
146
148
 
@@ -150,18 +152,18 @@ Nested pins by element role — vendor choice lives here, never in driver ids:
150
152
  images: {
151
153
  store: {
152
154
  sql: "postgres:18-alpine",
153
- kv: "redis:8-alpine",
155
+ kv: "docker.dragonflydb.io/dragonflydb/dragonfly:v2.0.0",
154
156
  files: "rustfs/rustfs:1.0.0",
155
157
  },
156
- channel: { email: "axllent/mailpit:v1.31.1" },
157
- pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.57",
158
+ channel: { email: "axllent/mailpit:v1.31.2" },
159
+ pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.59",
158
160
  // proxy: "caddy:2-alpine",
159
161
  },
160
162
  ```
161
163
 
162
164
  Omitted image keys mean no container for that role. When both `store.sql` and `pgdog` are pinned, `DATABASE_URL` points at PgDog — see [Store](/docs/elements/store#multiple-environments).
163
165
 
164
- For `store.kv`, pin Redis (default), Valkey, or Dragonfly — driver id stays `redis`.
166
+ For `store.kv`, pin Dragonfly (default), Redis, or Valkey — driver id stays `redis`.
165
167
  `{ durable: true }` KV lives in SQL (`oke_kv` on `DATABASE_URL`), not a second Redis image.
166
168
 
167
169
  Compose does not manage AI inference — [OpenRouter](/docs/recipes/openrouter) or BYO
@@ -120,6 +120,10 @@ at **404**. Clients distinguish by envelope, not status. Domain codes stay **400
120
120
  | Code | Status | Helper |
121
121
  | ------------------------------------------------ | ----------- | ------------------------------------------------------ |
122
122
  | `ValidationError` | `422` | none — failed `in`, `do` never runs |
123
+ | `IdempotencyKeyMissing` | `400` | none — `idempotency: "required"` and no header |
124
+ | `IdempotencyKeyInvalid` | `400` | none — header is not 16–255 printable ASCII |
125
+ | `IdempotencyKeyReused` | `422` | none — same key, different payload |
126
+ | `IdempotencyInProgress` | `409` | none — `Retry-After` until the in-flight call finishes |
123
127
  | `Unauthorized` | `401` | `unauthorized` · also a gate denial |
124
128
  | `Forbidden` | `403` | `forbidden` · also a gate denial |
125
129
  | `NotFound` | `404` | `notFound` |
@@ -301,8 +305,14 @@ Thrown by specific subsystems — each names its own cause:
301
305
  <Accordions>
302
306
 
303
307
  <Accordion title="I hit OKE1001–1009">
304
- Explicit `effects` drifted from `do`. Add the missing ledger entry or remove the hand-written
305
- `effects` block so inference covers the Flow.
308
+ The capability token does not include what the Flow touched. Add the missing ledger entry. An
309
+ `effects` block must include every effect inference can see.
310
+ </Accordion>
311
+
312
+ <Accordion title="OKE1900 fx hidden from inference">
313
+ Cause: the Flow renames `fx`, lets a chain alias escape, or passes `fx` to a callee inference
314
+ cannot resolve. Fix: name the parameter `fx`, keep `const q = fx.store(db)` in the same function,
315
+ and include every effect the walk can see. Extra keys in an `effects` block are allowed.
306
316
  </Accordion>
307
317
 
308
318
  <Accordion title="OKE1040 pathless HTTP never stamped">
@@ -54,7 +54,9 @@ async function loadNote(id: string, fx: Fx) {
54
54
  }
55
55
  ```
56
56
 
57
- A narrower structural type will not match `store()` overloads. See [Flow](/docs/elements/flow).
57
+ Name that parameter `fx`. A narrower structural type will not match `store()` overloads. See [Flow](/docs/elements/flow).
58
+
59
+ Inference follows `fx` in four shapes: a direct `fx.*` call, a chain alias kept in the same function (`const q = fx.store(db)` then `q.select()`), a project helper whose parameter is named `fx`, and two package intrinsics — `liveQuery(fx, table, …)` records `reads: ["sql:<table>"]`, and `applySearchEmbedCdc` records the static `sqlRef` plus each column's embed model.
58
60
 
59
61
  </Step>
60
62
 
@@ -155,11 +157,13 @@ on(
155
157
  | `fx.auth.createTenant({ name, slug?, id? })` | `write` `auth:tenants` | Creator becomes a member. Session only |
156
158
  | `fx.auth.upsertTenantRole({ tenantId, roleName, scopes })` | `write` `auth:tenants` | Application scopes only — `console:*` is unknown_scope |
157
159
 
158
- `fx.call` starts the callee with an **empty** `fx.auth` (fail-closed for authorization) and
159
- propagates `fx.tenant.id`. For audit/attribution only, read `fx.principal` — it propagates the
160
- originating identity without copying into `fx.auth`. Gates never consult `fx.principal`.
161
- Controlled `return fx.fail(...)` returns a `FlowFailure` value; an unhandled `throw` rethrows
162
- (never silent `undefined`). The same rules apply to `app.call`.
160
+ `fx.call` starts the callee with an **empty** `fx.auth` (fail-closed) and propagates `fx.tenant.id`.
161
+ Read `fx.principal` for audit — it does not copy into `fx.auth`.
162
+
163
+ <Callout>
164
+ Gates never consult `fx.principal`. `return fx.fail(...)` is a `FlowFailure`; an unhandled `throw`
165
+ rethrows. The same rules apply to `app.call`.
166
+ </Callout>
163
167
 
164
168
  ## Concurrency and retry
165
169
 
@@ -190,11 +194,14 @@ const charge = await fx.step("charge", () =>
190
194
  | `when` | thrown | Predicate; skips `AbortError` and sleep park |
191
195
 
192
196
  <Callout title="Cooperative cancel">
193
- Losing branches see `fx.signal` abort. Drivers that do not yet honor the signal may still finish
194
- in the background — check `fx.signal.aborted` in long user work, and prefer `fx.all` over bare
195
- `Promise.all`.
197
+ Losing branches see `fx.signal` abort. `fx.fetch`, Meilisearch, remote Vault HTTP, and AI streams
198
+ cancel in flight. An effect that has not started throws `AbortError`.
196
199
  </Callout>
197
200
 
201
+ SQL (`Bun.sql`), Redis, and a Channel send already on the wire through sently still finish. Those
202
+ clients expose no abort hook, and the shared connection stays open. Prefer `fx.all` over bare
203
+ `Promise.all`.
204
+
198
205
  `fx.using(acquire, release, use)` scopes a process-local resource to one attempt: `release` runs
199
206
  exactly once when `use` settles **or** when the ambient signal aborts (a sibling `fx.race` winner,
200
207
  a failing `fx.all` sibling). It is not journaled — do not hold handles across durable park/resume.
@@ -221,9 +228,9 @@ contact a provider. Bodies live on `.template({ catalog })` (`{{field}}`, not IC
221
228
 
222
229
  ## Outbound HTTP
223
230
 
224
- | Signature | Records | Notes |
225
- | ---------------------- | --------------------------- | ------------------------------------------------------------------------- |
226
- | `fx.fetch(url, init?)` | `fetch` on the URL hostname | Always stamps `EffectEntry.external` with `{ host, kind: "third-party" }` |
231
+ | Signature | Records | Notes |
232
+ | ---------------------- | --------------------------- | -------------------------------------------------------------------------- |
233
+ | `fx.fetch(url, init?)` | `fetch` on the URL hostname | Stamps `external: { host, kind: "third-party" }`. Aborts with `fx.signal`. |
227
234
 
228
235
  Declare hosts in `effects.fetches` (e.g. `["api.stripe.com"]`). Prefer `fx.step`; use
229
236
  `fx.retry` only when the remote API is safe to repeat. Dry runs never hit the network.
@@ -233,12 +240,13 @@ elements.
233
240
 
234
241
  ## AI
235
242
 
236
- | Signature | Records | Returns |
237
- | ----------------------------------------------------- | ------------------------- | -------------------------------------------------------------------------------------------------------- |
238
- | `fx.ask(prompt, input?, { via?, tools?, maxSteps? })` | `ask` (+ `call` per tool) | Object validated against the prompt's `out` |
239
- | `fx.run(agent, input?)` | `ask` | Agent result |
240
- | `fx.stream(model, { prompt?, data?, via? })` | `ask` | `AsyncIterable<string>` — real driver stream; cancels via ambient `fx.signal` (HTTP disconnect included) |
241
- | `fx.search(embed, query, { topK? })` | `read` | Matches from the index/embed |
243
+ | Signature | Records | Returns |
244
+ | ----------------------------------------------------- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
245
+ | `fx.ask(prompt, input?, { via?, tools?, maxSteps? })` | `ask` (+ `call` per tool) | Object validated against the prompt's `out` |
246
+ | `fx.run(agent, input?)` | `ask` | Agent result |
247
+ | `fx.stream(model, { prompt?, data?, via? })` | `ask` | `AsyncIterable<string>` — real driver stream; cancels via ambient `fx.signal` (HTTP disconnect included) |
248
+ | `fx.search(embed, query, { topK? })` | `read` | Matches from the index/embed |
249
+ | `fx.decide(decision, input)` | `decide` | `$.<question>.how` is `auto`, `reviewed`, or `abstained`. `audited` is a boolean flag, not a `how`. Abstain makes the value `null`. |
242
250
 
243
251
  AI calls are nondeterministic: journaling is forced on and auto-cache disabled around them. `tools` are Flow refs — each model tool call goes through `fx.call` (same capability and Runs path).
244
252
 
@@ -340,9 +348,15 @@ to the key — see [Gate](/docs/elements/gate#api-keys).
340
348
  <Accordions>
341
349
 
342
350
  <Accordion title="OKE1001–1007 / 1008 / 1009 undeclared effect">
343
- An explicit `effects` block drifted from what `do` touches. Add the missing ledger entry (`reads`
344
- · `writes` · `emits` · `sends` · `asks` · `embeds` · `secrets` · `calls` · `fetches`), or drop the
345
- block so inference covers the Flow ([Errors](/docs/reference/errors)).
351
+ The Manifest token does not include what `do` touched. Add the missing ledger entry (`reads` ·
352
+ `writes` · `emits` · `sends` · `asks` · `embeds` · `secrets` · `calls` · `fetches`). An `effects`
353
+ block must include every effect inference can see ([Errors](/docs/reference/errors)).
354
+ </Accordion>
355
+
356
+ <Accordion title="OKE1900 fx hidden from inference">
357
+ Name the parameter `fx` and keep chain aliases in the same function. Passing `fx` to an unresolved
358
+ callee, or returning, reassigning, or storing `fx` or a chain alias, fails extract. An `effects`
359
+ block may add keys. It must include every effect the walk can see.
346
360
  </Accordion>
347
361
 
348
362
  <Accordion title="OKE1240 orphan emit">
@@ -0,0 +1,192 @@
1
+ ---
2
+ title: "Idempotency"
3
+ description: "Send one Idempotency-Key so a retried mutating call runs once and a later retry replays the stored response."
4
+ icon: "KeyRound"
5
+ source: "docs/spec/unified-theory.md"
6
+ ---
7
+
8
+ A checkout that times out should not charge the card twice. Send one `Idempotency-Key` for that attempt. The server runs `do` once and replays the stored response when the same key comes back.
9
+
10
+ <Callout title="The one rule">
11
+ Mint one key per logical call and send it on every retry of that call. Two submits are two calls
12
+ unless your app passes the same key. Provider webhooks do not send this header — keep a unique
13
+ constraint on the provider's event id.
14
+ </Callout>
15
+
16
+ ## Quick start
17
+
18
+ <Steps>
19
+
20
+ <Step>
21
+ ### Mark the flow
22
+
23
+ `idempotency: "required"` rejects a call that omits the header. Leave it off and the header is
24
+ optional. `"required"` on a read, GET, stream, or live flow fails extract.
25
+
26
+ ```typescript
27
+ import { on, flow, http } from "okengine";
28
+
29
+ export const charge = on(
30
+ http.post("/charges", { in: ChargeIn, out: ChargeOut }),
31
+ flow("payments.charge", {
32
+ idempotency: "required",
33
+ do: async (input, fx) => {
34
+ return fx.store(db).insert(charges).values(input);
35
+ },
36
+ }),
37
+ );
38
+ ```
39
+
40
+ </Step>
41
+
42
+ <Step>
43
+ ### Call it
44
+
45
+ The client sends `Idempotency-Key` on every non-GET call. Pass a string to reuse a key across
46
+ your own retries, or `false` to send none.
47
+
48
+ ```typescript
49
+ await api.payments.charge({ amount: 1000 });
50
+ await api.payments.charge({ amount: 1000 }, { idempotencyKey: "checkout-attempt-9f3a2c" });
51
+ ```
52
+
53
+ </Step>
54
+
55
+ <Step>
56
+ ### Read the replay
57
+
58
+ The first response is the live result. A later call with the same key, principal, and payload
59
+ returns that stored response and sets `Idempotent-Replayed: true`. The client copies that onto
60
+ `meta.idempotentReplayed`.
61
+
62
+ </Step>
63
+
64
+ </Steps>
65
+
66
+ ## When the header is honored
67
+
68
+ All of these are required. Otherwise the header is ignored.
69
+
70
+ | Check | Honored when |
71
+ | ------- | ----------------------------------------------------------------------------------------------------------------------------------- |
72
+ | Entry | HTTP, and the method is not GET. RPC counts. `fx.call` has no request, so it does not. |
73
+ | Effects | Inferred effects include a write, emit, send, call, fetch, ask, or embed — or the flow uses `fx.raw`. Secrets alone do not qualify. |
74
+ | Shape | Not `stream`, and not a live SSE feed. |
75
+ | Option | Omitted or `{ ttl }` is `auto` (header optional). `"required"` or `{ required: true }` demands the header. `false` ignores it. |
76
+
77
+ `QUERY` is not GET. It qualifies when the rest of the row matches.
78
+
79
+ | Option | Meaning |
80
+ | ------------------------------- | --------------------------------------------------- |
81
+ | omitted | `auto` when the flow qualifies. Default TTL `24h`. |
82
+ | `"required"` | Missing header is `400 IdempotencyKeyMissing`. |
83
+ | `{ ttl: "1h" }` | `auto`, with that TTL. `ms`, `s`, `m`, `h`, or `d`. |
84
+ | `{ required: true, ttl: "1h" }` | Required, with that TTL. |
85
+ | `false` | Header ignored. |
86
+
87
+ An unparseable TTL, or `required` on a flow that cannot honor the header, fails extract.
88
+
89
+ The key is 16–255 printable ASCII characters (`0x20`–`0x7E`). Anything else is
90
+ `400 IdempotencyKeyInvalid`.
91
+
92
+ The scope is tenant, principal, and flow. The principal is `user:<id>`, else `apikey:<id>`, else
93
+ `anon`. The same key from two people is two records. The fingerprint is a hash of the flow name
94
+ and the validated input. The same key with a different payload is `422 IdempotencyKeyReused`.
95
+
96
+ ## What a second request sees
97
+
98
+ The claim happens after the gate and after input validation. A `401`, `403`, or validation
99
+ failure stores nothing, so the same key can still succeed.
100
+
101
+ | Row | Response |
102
+ | ----------------------------- | -------------------------------------------------------------------------------------- |
103
+ | No row, or the TTL has passed | `do` runs. The response is stored until the TTL. |
104
+ | Same payload, completed | Stored status, body, `content-type`, and `location`, plus `Idempotent-Replayed: true`. |
105
+ | Same payload, still running | `409 IdempotencyInProgress` and `Retry-After` (seconds until the lease, at least 1). |
106
+ | Different payload | `422 IdempotencyKeyReused`. |
107
+
108
+ A stream, an SSE body, or a raw `Response` that is not a buffered JSON envelope is not stored.
109
+ The live response is returned and the row is deleted.
110
+
111
+ A throw before any non-read effect deletes the row. The same key may run again. A throw after a
112
+ write, emit, send, call, fetch, ask, embed, or `fx.raw` stores the `500 InternalError` envelope.
113
+ A new key is required. A declared `fx.fail(...)` is stored as that failure, not as a 500.
114
+
115
+ ## Crashes
116
+
117
+ The in-progress lease is 30 seconds and is renewed while `do` runs. A disconnect does not cancel
118
+ a claimed `do`. The lease is what notices a crash.
119
+
120
+ | Flow | After the lease expires |
121
+ | ----------- | ----------------------------------------------------------------------------- |
122
+ | Not durable | The next request with the same key runs `do` again. That is at-least-once. |
123
+ | Durable | The next request resumes that journal run. Completed steps are not run again. |
124
+
125
+ A durable sleep stores the `204` and parks the journal run. A retry replays the `204`. The
126
+ scheduler continues the run.
127
+
128
+ ## Where the body lives
129
+
130
+ Rows sit in `oke_idempotency` on the journal driver: memory in tests, `.oke/idempotency.json`
131
+ beside the journal file, and Postgres when that journal driver is bound.
132
+
133
+ The stored body can contain personal data and stays until the TTL. Expired rows are deleted on
134
+ the next claim and by a periodic sweep.
135
+
136
+ The Console flow contract shows an idempotency pill when the mode is `auto` or `required`.
137
+
138
+ ## Troubleshooting
139
+
140
+ <Accordions>
141
+ <Accordion title="400 IdempotencyKeyMissing">
142
+
143
+ The catalog message is `This call requires an Idempotency-Key header.` The flow is `required`
144
+ and this request did not send the header. Send a key, or drop `"required"` if the header should
145
+ stay optional.
146
+
147
+ </Accordion>
148
+ <Accordion title="400 IdempotencyKeyInvalid">
149
+
150
+ The catalog message is `Idempotency-Key must be 16–255 printable ASCII characters.` Lengthen the
151
+ key or drop characters outside printable ASCII.
152
+
153
+ </Accordion>
154
+ <Accordion title="422 IdempotencyKeyReused">
155
+
156
+ The catalog message is `This Idempotency-Key was already used with a different request.` The
157
+ payload changed. Mint a new key for the new payload.
158
+
159
+ </Accordion>
160
+ <Accordion title="409 IdempotencyInProgress">
161
+
162
+ The catalog message is `This Idempotency-Key is still running. Retry after the given delay.`
163
+ Wait for `Retry-After`, then send the same key. The client does this when `retry` is configured.
164
+
165
+ </Accordion>
166
+ </Accordions>
167
+
168
+ ## Learn more
169
+
170
+ - [Calling](/docs/client/calling) — `idempotencyKey` and the retry rule
171
+ - [Errors](/docs/reference/errors) — the four idempotency codes and their statuses
172
+ - [The Architecture](/docs/understand/the-architecture) — a retry of one call runs once
173
+
174
+ ## Next
175
+
176
+ <Cards>
177
+ <Card
178
+ title="Calling"
179
+ description="Client retry and Idempotency-Key."
180
+ href="/docs/client/calling"
181
+ />
182
+ <Card
183
+ title="Errors"
184
+ description="Statuses for the four idempotency codes."
185
+ href="/docs/reference/errors"
186
+ />
187
+ <Card
188
+ title="The Architecture"
189
+ description="One retry runs once. Two submits are two calls."
190
+ href="/docs/understand/the-architecture"
191
+ />
192
+ </Cards>