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
@@ -20,7 +20,7 @@
20
20
  * cleartext value ever reaches the log, an error message, or a `VaultError`.
21
21
  */
22
22
 
23
- import { createAuditSink, type AuditAction, type AuditSink } from "./audit.ts";
23
+ import { auditSinkForConfig, type AuditAction, type AuditSink } from "./audit.ts";
24
24
  import {
25
25
  ALGORITHM,
26
26
  buildAad,
@@ -66,6 +66,7 @@ import type {
66
66
  VaultActor,
67
67
  VaultAdapter,
68
68
  VaultAlgorithm,
69
+ VaultAuditConfig,
69
70
  VaultGetOptions,
70
71
  VaultInitResult,
71
72
  VaultListEntry,
@@ -96,8 +97,15 @@ export interface CreateBuiltinVaultOptions {
96
97
  * {@link VaultAdapter.unseal} is not required.
97
98
  */
98
99
  readonly unsealer?: Unsealer;
99
- /** Audit destination. Defaults to a `db` sink over `oke_vault_audit`. */
100
+ /** Audit destination. Wins over {@link audit} when both are set. */
100
101
  readonly auditSink?: AuditSink;
102
+ /**
103
+ * `vault.audit` from config. Omitted means the `db` hash chain.
104
+ * Ignored when {@link auditSink} is set.
105
+ */
106
+ readonly audit?: VaultAuditConfig;
107
+ /** Fetch override for the `webhook` audit sink (tests). */
108
+ readonly fetch?: typeof globalThis.fetch;
101
109
  /** DEKs re-wrapped per rotation batch. Defaults to {@link DEFAULT_KEK_REWRAP_BATCH_SIZE}. */
102
110
  readonly kekRewrapBatchSize?: number;
103
111
  /**
@@ -306,7 +314,25 @@ export function createBuiltinVaultAdapter(opts: CreateBuiltinVaultOptions): Buil
306
314
  const db = createResilientSqlExec(rawDb);
307
315
  const batchSize = Math.max(1, opts.kekRewrapBatchSize ?? DEFAULT_KEK_REWRAP_BATCH_SIZE);
308
316
  const rotateLeaseMs = Math.max(50, opts.rotateLeaseMs ?? DEFAULT_ROTATE_LEASE_MS);
309
- const audit = opts.auditSink ?? createAuditSink("db", { writer: createSqlAuditWriter(rawDb) });
317
+ const audit =
318
+ opts.auditSink ??
319
+ auditSinkForConfig(opts.audit, {
320
+ writer: createSqlAuditWriter(rawDb),
321
+ ...(opts.fetch === undefined ? {} : { fetch: opts.fetch }),
322
+ });
323
+
324
+ /**
325
+ * Hash-chain commands only exist for the `db` sink.
326
+ *
327
+ * @param op - Command name for the error
328
+ */
329
+ function requireDbAudit(op: string): void {
330
+ if (audit.kind === "db") return;
331
+ throw new VaultError(
332
+ "UNSUPPORTED",
333
+ `vault: ${op} requires audit.sink "db" (got "${audit.kind}")`,
334
+ );
335
+ }
310
336
 
311
337
  let unsealer: Unsealer | null = opts.unsealer ?? null;
312
338
  let pending: PendingRotation | null = null;
@@ -1018,6 +1044,7 @@ export function createBuiltinVaultAdapter(opts: CreateBuiltinVaultOptions): Buil
1018
1044
  },
1019
1045
 
1020
1046
  async purgeAuditBefore(before: Date): Promise<number> {
1047
+ requireDbAudit("audit purge");
1021
1048
  await ensureReady();
1022
1049
  const removed = await purgeAuditRows(db, before);
1023
1050
  await record("purge", undefined);
@@ -1043,16 +1070,19 @@ export function createBuiltinVaultAdapter(opts: CreateBuiltinVaultOptions): Buil
1043
1070
  },
1044
1071
 
1045
1072
  async verifyAudit(): Promise<AuditChainResult> {
1073
+ requireDbAudit("audit verify");
1046
1074
  await ensureReady();
1047
1075
  return verifyAuditChain(db);
1048
1076
  },
1049
1077
 
1050
1078
  async listAudit(options?: AuditPageOptions): Promise<readonly VaultAuditRecord[]> {
1079
+ requireDbAudit("audit list");
1051
1080
  await ensureReady();
1052
1081
  return readAuditPage(db, options ?? {});
1053
1082
  },
1054
1083
 
1055
1084
  async readAuditRow(id: string): Promise<VaultAuditRecord | null> {
1085
+ requireDbAudit("audit read");
1056
1086
  await ensureReady();
1057
1087
  return readAuditRow(db, id);
1058
1088
  },
@@ -40,6 +40,10 @@ export const builtinAr = {
40
40
  cause: 'التدفق "{flow}" يضمّن عبر "{resource}" دون الإعلان عنه.',
41
41
  fix: 'أضف "{resource}" إلى effects.embeds لهذا التدفق.',
42
42
  },
43
+ "1010": {
44
+ cause: 'التدفق "{flow}" يقرر "{resource}" دون الإعلان عنه.',
45
+ fix: 'أضف "{resource}" إلى effects.decides لهذا التدفق.',
46
+ },
43
47
  "1240": {
44
48
  cause: 'التدفق "{flow}" يُصدِر الإشارة "{resource}" بلا مشترك.',
45
49
  fix: "أضف on({resource}, …) أو عيّن الإشارة '{'optional: true'}'.",
@@ -86,6 +90,11 @@ export const builtinAr = {
86
90
  "DatabaseError.retryable": "قاعدة البيانات مشغولة. حاول بعد قليل.",
87
91
  ServiceUnavailable: "الخدمة غير متاحة مؤقتاً.",
88
92
  InternalError: "حدث خطأ. حاول مرة أخرى.",
93
+ IdempotencyKeyMissing: "هذا الاستدعاء يتطلب ترويسة Idempotency-Key.",
94
+ IdempotencyKeyInvalid: "يجب أن يكون Idempotency-Key من 16 إلى 255 حرفًا ASCII قابلًا للطباعة.",
95
+ IdempotencyKeyReused: "استُخدم Idempotency-Key هذا من قبل مع طلب مختلف.",
96
+ IdempotencyInProgress: "ما زال Idempotency-Key هذا قيد التنفيذ. أعد المحاولة بعد المهلة.",
97
+ JournalLeaseBusy: "هذا التشغيل مقفل لدى عامل آخر. أعد المحاولة بعد المهلة.",
89
98
  InvalidQuery: "الاستعلام غير صالح.",
90
99
  UnsupportedMediaType: "نوع المحتوى هذا غير مدعوم.",
91
100
  AuthFailed: "فشلت المصادقة.",
@@ -40,6 +40,10 @@ export const builtinEn = {
40
40
  cause: 'Flow "{flow}" embeds with "{resource}" without declaring it.',
41
41
  fix: "Add \"{resource}\" to this flow''s effects.embeds.",
42
42
  },
43
+ "1010": {
44
+ cause: 'Flow "{flow}" decides "{resource}" without declaring it.',
45
+ fix: "Add \"{resource}\" to this flow''s effects.decides.",
46
+ },
43
47
  "1240": {
44
48
  cause: 'Flow "{flow}" emits signal "{resource}" with no subscriber.',
45
49
  fix: "Add on({resource}, …) or mark the signal '{'optional: true'}'.",
@@ -86,6 +90,11 @@ export const builtinEn = {
86
90
  "DatabaseError.retryable": "The database is busy. Try again shortly.",
87
91
  ServiceUnavailable: "The service is temporarily unavailable.",
88
92
  InternalError: "Something went wrong. Try again.",
93
+ IdempotencyKeyMissing: "This call requires an Idempotency-Key header.",
94
+ IdempotencyKeyInvalid: "Idempotency-Key must be 16–255 printable ASCII characters.",
95
+ IdempotencyKeyReused: "This Idempotency-Key was already used with a different request.",
96
+ IdempotencyInProgress: "This Idempotency-Key is still running. Retry after the given delay.",
97
+ JournalLeaseBusy: "This run is locked by another worker. Retry after the given delay.",
89
98
  InvalidQuery: "The query is not valid.",
90
99
  UnsupportedMediaType: "That content type is not supported.",
91
100
  AuthFailed: "Authentication failed.",
package/src/index.ts CHANGED
@@ -175,6 +175,8 @@ export {
175
175
  type AiModelDecl,
176
176
  type AiPromptDecl,
177
177
  type AiAgentDecl,
178
+ type AiDecisionDecl,
179
+ type AiDecisionOptions,
178
180
  type AiMcpServerDecl,
179
181
  type AiMcpServerOptions,
180
182
  type AiRuntime,
@@ -2,8 +2,8 @@
2
2
  * Ambient AbortSignal scope for structured concurrency.
3
3
  *
4
4
  * Same ALS pattern as dry-run: `fx.all` / `fx.race` enter a child signal so
5
- * cooperative branches (and future driver plumbing) can observe cancellation
6
- * without every call site passing a signal argument.
5
+ * cooperative branches and outbound HTTP can observe cancellation without
6
+ * every call site passing a signal argument.
7
7
  */
8
8
 
9
9
  import { AsyncLocalStorage } from "node:async_hooks";
@@ -34,6 +34,21 @@ export async function withAbortSignal<T>(
34
34
  return storage.run(signal, fn);
35
35
  }
36
36
 
37
+ /** Never-aborted signal for work that must outlive the caller connection. */
38
+ const DETACHED: AbortSignal = new AbortController().signal;
39
+
40
+ /**
41
+ * Run `fn` without the request abort signal.
42
+ *
43
+ * A claimed idempotent `do` keeps running when the client times out. The
44
+ * lease, not the socket, is what detects a crash.
45
+ *
46
+ * @param fn - Work to detach
47
+ */
48
+ export function runDetached<T>(fn: () => T | Promise<T>): Promise<T> {
49
+ return withAbortSignal(DETACHED, fn);
50
+ }
51
+
37
52
  /**
38
53
  * When `parent` aborts, abort `child` with the same reason.
39
54
  *
@@ -88,6 +103,22 @@ export function abortError(reason?: unknown): Error {
88
103
  return err;
89
104
  }
90
105
 
106
+ /**
107
+ * Signal for one outbound request: the ambient scope, the caller's signal, or both.
108
+ *
109
+ * Read at request time, not at driver `open`, so an `fx.race` / `fx.all` abort
110
+ * reaches HTTP that is already in flight.
111
+ *
112
+ * @param explicit - Caller-supplied signal, when the API already accepts one
113
+ */
114
+ export function requestSignal(explicit?: AbortSignal | null): AbortSignal {
115
+ const ambient = currentAbortSignal();
116
+ if (explicit == null || explicit === ambient) return ambient;
117
+ if (ambient.aborted) return ambient;
118
+ if (explicit.aborted) return explicit;
119
+ return AbortSignal.any([ambient, explicit]);
120
+ }
121
+
91
122
  /**
92
123
  * Sleep that rejects promptly when `signal` aborts.
93
124
  *
@@ -0,0 +1,448 @@
1
+ /**
2
+ * Durable agent-run event log on the journal driver.
3
+ *
4
+ * Memory, file, and Postgres keep the same rows so a follower on another
5
+ * instance, or after a restart, resumes from `Last-Event-ID`. The instance
6
+ * that holds the run's journal lease is the only writer. A repeated seq is
7
+ * an error.
8
+ */
9
+
10
+ import { appendFile, mkdir, readdir, rm } from "node:fs/promises";
11
+ import { join } from "node:path";
12
+
13
+ /** Header stored beside the rows. */
14
+ export interface AgentEventHeaderRecord {
15
+ readonly runId: string;
16
+ readonly threadId: string;
17
+ /** Agent that opened the run. Absent on logs written before this field. */
18
+ readonly agent?: string;
19
+ /** Parent agent run, when this run is a nested tool. */
20
+ readonly parentRunId?: string;
21
+ readonly tenant: string | null;
22
+ readonly gates: readonly string[];
23
+ readonly userId: string | null;
24
+ readonly operatorId: string | null;
25
+ /** Epoch ms the run was opened. Sweep uses this for abandoned runs. */
26
+ readonly openedAt?: number;
27
+ readonly finishedAt?: number;
28
+ /**
29
+ * Epoch ms until which a sweep claim is held.
30
+ * Another instance may reclaim the run after this instant.
31
+ * Postgres stores the claim on `sweep_claim_at` instead.
32
+ */
33
+ readonly sweepClaimUntil?: number;
34
+ }
35
+
36
+ /** One stored SSE row. */
37
+ export interface AgentEventRowRecord {
38
+ readonly seq: number;
39
+ readonly event: unknown;
40
+ }
41
+
42
+ /** One run's durable log. */
43
+ export interface AgentEventRecord {
44
+ readonly header: AgentEventHeaderRecord;
45
+ readonly rows: readonly AgentEventRowRecord[];
46
+ }
47
+
48
+ /**
49
+ * Thrown when a writer inserts a seq that is already stored.
50
+ * The lease holder reads `MAX(seq)` before writing, so this is a bug.
51
+ */
52
+ export class AgentEventDuplicateSeqError extends Error {
53
+ /**
54
+ * @param runId - Agent run
55
+ * @param seq - Seq that was already stored
56
+ */
57
+ constructor(runId: string, seq: number) {
58
+ super(`agent events: duplicate seq ${seq} for run "${runId}"`);
59
+ this.name = "AgentEventDuplicateSeqError";
60
+ }
61
+ }
62
+
63
+ /** Persistence for {@link AgentEventRecord}. */
64
+ export interface AgentEventStore {
65
+ /** Load one run, or undefined when it was never opened. */
66
+ read(runId: string): Promise<AgentEventRecord | undefined>;
67
+ /** Rows with `seq` greater than `afterSeq`, in order. */
68
+ readAfter(runId: string, afterSeq: number): Promise<readonly AgentEventRowRecord[]>;
69
+ /** Highest stored seq, or 0 when the run has no rows. */
70
+ maxSeq(runId: string): Promise<number>;
71
+ /** Header for one run, or undefined when it was never opened. */
72
+ readHeader(runId: string): Promise<AgentEventHeaderRecord | undefined>;
73
+ /** True when this run already stored `oke.events.truncated`. */
74
+ truncated(runId: string): Promise<boolean>;
75
+ /**
76
+ * Claim the right to close this run.
77
+ * False when another instance holds an unexpired claim.
78
+ * A claim older than {@link AGENT_EVENT_SWEEP_CLAIM_MS} can be taken again.
79
+ *
80
+ * @param runId - Agent run
81
+ * @param now - Clock. Defaults to `Date.now()`.
82
+ */
83
+ claim(runId: string, now?: number): Promise<boolean>;
84
+ /** Every header, including runs this process did not open. */
85
+ listHeaders(): Promise<readonly AgentEventHeaderRecord[]>;
86
+ /** Create or replace the header. Existing rows stay. */
87
+ writeHeader(header: AgentEventHeaderRecord): Promise<void>;
88
+ /**
89
+ * Append one row. `seq` is already assigned.
90
+ * A duplicate seq throws {@link AgentEventDuplicateSeqError}.
91
+ */
92
+ append(runId: string, row: AgentEventRowRecord): Promise<void>;
93
+ /** Drop a finished run. */
94
+ remove(runId: string): Promise<void>;
95
+ }
96
+
97
+ /** How long a sweep claim blocks other instances. */
98
+ export const AGENT_EVENT_SWEEP_CLAIM_MS = 30_000;
99
+
100
+ /** In-memory event store. */
101
+ export function createMemoryAgentEventStore(): AgentEventStore {
102
+ const runs = new Map<string, { header: AgentEventHeaderRecord; rows: AgentEventRowRecord[] }>();
103
+ const claimedUntil = new Map<string, number>();
104
+ return {
105
+ async read(runId) {
106
+ const run = runs.get(runId);
107
+ if (!run) return undefined;
108
+ return { header: run.header, rows: run.rows.map((row) => ({ ...row })) };
109
+ },
110
+ async readAfter(runId, afterSeq) {
111
+ const run = runs.get(runId);
112
+ if (!run) return [];
113
+ return run.rows.filter((row) => row.seq > afterSeq).map((row) => ({ ...row }));
114
+ },
115
+ async maxSeq(runId) {
116
+ const run = runs.get(runId);
117
+ if (!run || run.rows.length === 0) return 0;
118
+ return run.rows.reduce((max, row) => Math.max(max, row.seq), 0);
119
+ },
120
+ async readHeader(runId) {
121
+ return runs.get(runId)?.header;
122
+ },
123
+ async truncated(runId) {
124
+ const run = runs.get(runId);
125
+ if (!run) return false;
126
+ return run.rows.some((row) => isTruncationEvent(row.event));
127
+ },
128
+ async claim(runId, now = Date.now()) {
129
+ if (!runs.has(runId)) return false;
130
+ const until = claimedUntil.get(runId);
131
+ if (until !== undefined && now < until) return false;
132
+ claimedUntil.set(runId, now + AGENT_EVENT_SWEEP_CLAIM_MS);
133
+ return true;
134
+ },
135
+ async listHeaders() {
136
+ return [...runs.values()].map((run) => run.header);
137
+ },
138
+ async writeHeader(header) {
139
+ const run = runs.get(header.runId);
140
+ if (run) run.header = header;
141
+ else runs.set(header.runId, { header, rows: [] });
142
+ },
143
+ async append(runId, row) {
144
+ const run = runs.get(runId);
145
+ if (!run) return;
146
+ if (run.rows.some((stored) => stored.seq === row.seq)) {
147
+ throw new AgentEventDuplicateSeqError(runId, row.seq);
148
+ }
149
+ run.rows.push(row);
150
+ },
151
+ async remove(runId) {
152
+ runs.delete(runId);
153
+ claimedUntil.delete(runId);
154
+ },
155
+ };
156
+ }
157
+
158
+ interface JsonlLine {
159
+ readonly kind: "header" | "row";
160
+ readonly header?: AgentEventHeaderRecord;
161
+ readonly seq?: number;
162
+ readonly event?: unknown;
163
+ }
164
+
165
+ /**
166
+ * Append-only event store. One JSONL file per run, guarded by a write lock.
167
+ * Header updates append a new header line. Rows are never rewritten.
168
+ *
169
+ * Single-process only. Do not use it for large logs or more than one
170
+ * process. Production logs belong on the Postgres journal driver.
171
+ *
172
+ * @param dir - Directory of `{runId}.jsonl` files
173
+ */
174
+ export function createFileAgentEventStore(dir: string): AgentEventStore {
175
+ const locks = new Map<string, Promise<void>>();
176
+
177
+ const withLock = async <T>(runId: string, fn: () => Promise<T>): Promise<T> => {
178
+ const prev = locks.get(runId) ?? Promise.resolve();
179
+ let release: () => void = () => undefined;
180
+ const gate = new Promise<void>((resolve) => {
181
+ release = resolve;
182
+ });
183
+ const tail = prev.then(() => gate);
184
+ locks.set(runId, tail);
185
+ await prev;
186
+ try {
187
+ return await fn();
188
+ } finally {
189
+ release();
190
+ }
191
+ };
192
+
193
+ const fileOf = (runId: string): string => join(dir, `${encodeURIComponent(runId)}.jsonl`);
194
+
195
+ const readLines = async (runId: string): Promise<JsonlLine[]> => {
196
+ const file = Bun.file(fileOf(runId));
197
+ if (!(await file.exists())) return [];
198
+ const text = await file.text();
199
+ const lines: JsonlLine[] = [];
200
+ for (const line of text.split("\n")) {
201
+ if (!line) continue;
202
+ lines.push(JSON.parse(line) as JsonlLine);
203
+ }
204
+ return lines;
205
+ };
206
+
207
+ const headerOf = (lines: readonly JsonlLine[]): AgentEventHeaderRecord | undefined => {
208
+ let header: AgentEventHeaderRecord | undefined;
209
+ for (const line of lines) {
210
+ if (line.kind === "header" && line.header) header = line.header;
211
+ }
212
+ return header;
213
+ };
214
+
215
+ const rowsOf = (lines: readonly JsonlLine[]): AgentEventRowRecord[] => {
216
+ const rows: AgentEventRowRecord[] = [];
217
+ for (const line of lines) {
218
+ if (line.kind === "row" && line.seq !== undefined) {
219
+ rows.push({ seq: line.seq, event: line.event });
220
+ }
221
+ }
222
+ return rows;
223
+ };
224
+
225
+ const appendLine = async (runId: string, line: JsonlLine): Promise<void> => {
226
+ await mkdir(dir, { recursive: true });
227
+ await appendFile(fileOf(runId), `${JSON.stringify(line)}\n`, "utf8");
228
+ };
229
+
230
+ return {
231
+ async read(runId) {
232
+ return withLock(runId, async () => {
233
+ const lines = await readLines(runId);
234
+ const header = headerOf(lines);
235
+ if (!header) return undefined;
236
+ return { header, rows: rowsOf(lines) };
237
+ });
238
+ },
239
+ async readAfter(runId, afterSeq) {
240
+ return withLock(runId, async () => {
241
+ const rows = rowsOf(await readLines(runId));
242
+ return rows.filter((row) => row.seq > afterSeq);
243
+ });
244
+ },
245
+ async maxSeq(runId) {
246
+ return withLock(runId, async () => {
247
+ const rows = rowsOf(await readLines(runId));
248
+ return rows.reduce((max, row) => Math.max(max, row.seq), 0);
249
+ });
250
+ },
251
+ async readHeader(runId) {
252
+ return withLock(runId, async () => headerOf(await readLines(runId)));
253
+ },
254
+ async truncated(runId) {
255
+ return withLock(runId, async () =>
256
+ rowsOf(await readLines(runId)).some((row) => isTruncationEvent(row.event)),
257
+ );
258
+ },
259
+ async claim(runId, now = Date.now()) {
260
+ return withLock(runId, async () => {
261
+ const header = headerOf(await readLines(runId));
262
+ if (!header) return false;
263
+ if (header.sweepClaimUntil !== undefined && now < header.sweepClaimUntil) return false;
264
+ await appendLine(runId, {
265
+ kind: "header",
266
+ header: { ...header, sweepClaimUntil: now + AGENT_EVENT_SWEEP_CLAIM_MS },
267
+ });
268
+ return true;
269
+ });
270
+ },
271
+ async listHeaders() {
272
+ let names: string[] = [];
273
+ try {
274
+ names = await readdir(dir);
275
+ } catch {
276
+ return [];
277
+ }
278
+ const headers: AgentEventHeaderRecord[] = [];
279
+ for (const name of names) {
280
+ if (!name.endsWith(".jsonl")) continue;
281
+ const runId = decodeURIComponent(name.slice(0, -".jsonl".length));
282
+ const header = headerOf(await readLines(runId));
283
+ if (header) headers.push(header);
284
+ }
285
+ return headers;
286
+ },
287
+ async writeHeader(header) {
288
+ await withLock(header.runId, () => appendLine(header.runId, { kind: "header", header }));
289
+ },
290
+ async append(runId, row) {
291
+ await withLock(runId, async () => {
292
+ const rows = rowsOf(await readLines(runId));
293
+ if (rows.some((stored) => stored.seq === row.seq)) {
294
+ throw new AgentEventDuplicateSeqError(runId, row.seq);
295
+ }
296
+ await appendLine(runId, { kind: "row", seq: row.seq, event: row.event });
297
+ });
298
+ },
299
+ async remove(runId) {
300
+ await withLock(runId, async () => {
301
+ await rm(fileOf(runId), { force: true });
302
+ });
303
+ },
304
+ };
305
+ }
306
+
307
+ /** SQL client the postgres event store needs. */
308
+ export interface AgentEventSql {
309
+ query(sql: string, params?: readonly unknown[]): Promise<Record<string, unknown>[]>;
310
+ exec(sql: string, params?: readonly unknown[]): Promise<{ changes: number }>;
311
+ }
312
+
313
+ /**
314
+ * Postgres event store on the journal connection.
315
+ *
316
+ * Followers read `seq > last`. A duplicate primary key is an error.
317
+ *
318
+ * @param sql - Journal SQL client
319
+ */
320
+ export async function createPostgresAgentEventStore(sql: AgentEventSql): Promise<AgentEventStore> {
321
+ await sql.exec(`CREATE TABLE IF NOT EXISTS oke_agent_run (
322
+ run_id TEXT PRIMARY KEY,
323
+ header TEXT NOT NULL,
324
+ sweep_claim_at BIGINT
325
+ )`);
326
+ await sql.exec(`CREATE TABLE IF NOT EXISTS oke_agent_event (
327
+ run_id TEXT NOT NULL,
328
+ seq INTEGER NOT NULL,
329
+ event TEXT NOT NULL,
330
+ event_type TEXT,
331
+ event_name TEXT,
332
+ PRIMARY KEY (run_id, seq)
333
+ )`);
334
+ await sql.exec(`ALTER TABLE oke_agent_run ADD COLUMN IF NOT EXISTS sweep_claim_at BIGINT`);
335
+ await sql.exec(`ALTER TABLE oke_agent_event ADD COLUMN IF NOT EXISTS event_type TEXT`);
336
+ await sql.exec(`ALTER TABLE oke_agent_event ADD COLUMN IF NOT EXISTS event_name TEXT`);
337
+
338
+ const isDuplicate = (err: unknown): boolean => {
339
+ const message = err instanceof Error ? err.message : String(err);
340
+ return /duplicate key|unique constraint|PRIMARY KEY/i.test(message);
341
+ };
342
+
343
+ const readHeader = async (runId: string): Promise<AgentEventHeaderRecord | undefined> => {
344
+ const headers = await sql.query(`SELECT header FROM oke_agent_run WHERE run_id = ?`, [runId]);
345
+ const headerRow = headers[0];
346
+ if (!headerRow) return undefined;
347
+ return JSON.parse(String(headerRow.header)) as AgentEventHeaderRecord;
348
+ };
349
+
350
+ return {
351
+ async read(runId) {
352
+ const headers = await sql.query(`SELECT header FROM oke_agent_run WHERE run_id = ?`, [runId]);
353
+ const headerRow = headers[0];
354
+ if (!headerRow) return undefined;
355
+ const header = JSON.parse(String(headerRow.header)) as AgentEventHeaderRecord;
356
+ const rows = await readAfter(sql, runId, 0);
357
+ return { header, rows };
358
+ },
359
+ async readAfter(runId, afterSeq) {
360
+ return readAfter(sql, runId, afterSeq);
361
+ },
362
+ async maxSeq(runId) {
363
+ const rows = await sql.query(
364
+ `SELECT COALESCE(MAX(seq), 0) AS max_seq FROM oke_agent_event WHERE run_id = ?`,
365
+ [runId],
366
+ );
367
+ return Number(rows[0]?.max_seq ?? 0);
368
+ },
369
+ readHeader,
370
+ async truncated(runId) {
371
+ const rows = await sql.query(
372
+ `SELECT 1 AS hit FROM oke_agent_event WHERE run_id = ? AND event_type = ? AND event_name = ? LIMIT 1`,
373
+ [runId, "CUSTOM", "oke.events.truncated"],
374
+ );
375
+ return rows.length > 0;
376
+ },
377
+ async claim(runId, now = Date.now()) {
378
+ const current = await readHeader(runId);
379
+ if (!current) return false;
380
+ const until = now + AGENT_EVENT_SWEEP_CLAIM_MS;
381
+ const updated = await sql.exec(
382
+ `UPDATE oke_agent_run SET sweep_claim_at = ? WHERE run_id = ? AND (sweep_claim_at IS NULL OR sweep_claim_at <= ?)`,
383
+ [until, runId, now],
384
+ );
385
+ return updated.changes === 1;
386
+ },
387
+ async listHeaders() {
388
+ const rows = await sql.query(`SELECT header FROM oke_agent_run`);
389
+ return rows.map((row) => JSON.parse(String(row.header)) as AgentEventHeaderRecord);
390
+ },
391
+ async writeHeader(header) {
392
+ await sql.exec(
393
+ `INSERT INTO oke_agent_run (run_id, header) VALUES (?, ?)
394
+ ON CONFLICT (run_id) DO UPDATE SET header = EXCLUDED.header`,
395
+ [header.runId, JSON.stringify(header)],
396
+ );
397
+ },
398
+ async append(runId, row) {
399
+ try {
400
+ const columns = eventColumns(row.event);
401
+ await sql.exec(
402
+ `INSERT INTO oke_agent_event (run_id, seq, event, event_type, event_name) VALUES (?, ?, ?, ?, ?)`,
403
+ [runId, row.seq, JSON.stringify(row.event), columns.type, columns.name],
404
+ );
405
+ } catch (err) {
406
+ if (isDuplicate(err)) throw new AgentEventDuplicateSeqError(runId, row.seq);
407
+ throw err;
408
+ }
409
+ },
410
+ async remove(runId) {
411
+ await sql.exec(`DELETE FROM oke_agent_event WHERE run_id = ?`, [runId]);
412
+ await sql.exec(`DELETE FROM oke_agent_run WHERE run_id = ?`, [runId]);
413
+ },
414
+ };
415
+ }
416
+
417
+ function eventColumns(event: unknown): {
418
+ readonly type: string | null;
419
+ readonly name: string | null;
420
+ } {
421
+ if (!event || typeof event !== "object") return { type: null, name: null };
422
+ const row = event as { type?: unknown; name?: unknown };
423
+ return {
424
+ type: typeof row.type === "string" ? row.type : null,
425
+ name: typeof row.name === "string" ? row.name : null,
426
+ };
427
+ }
428
+
429
+ function isTruncationEvent(event: unknown): boolean {
430
+ if (!event || typeof event !== "object") return false;
431
+ const row = event as { type?: unknown; name?: unknown };
432
+ return row.type === "CUSTOM" && row.name === "oke.events.truncated";
433
+ }
434
+
435
+ async function readAfter(
436
+ sql: AgentEventSql,
437
+ runId: string,
438
+ afterSeq: number,
439
+ ): Promise<AgentEventRowRecord[]> {
440
+ const rows = await sql.query(
441
+ `SELECT seq, event FROM oke_agent_event WHERE run_id = ? AND seq > ? ORDER BY seq`,
442
+ [runId, afterSeq],
443
+ );
444
+ return rows.map((row) => ({
445
+ seq: Number(row.seq),
446
+ event: JSON.parse(String(row.event)),
447
+ }));
448
+ }