okengine 0.22.0 → 0.23.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (312) hide show
  1. package/AGENTS.md +7 -0
  2. package/manifest.v1.schema.json +44 -0
  3. package/package.json +32 -19
  4. package/site/content/docs/ai/mcp.mdx +14 -10
  5. package/site/content/docs/ai/skills.mdx +1 -1
  6. package/site/content/docs/elements/ai/agents.mdx +61 -13
  7. package/site/content/docs/elements/ai/decide.mdx +536 -0
  8. package/site/content/docs/elements/ai/events.mdx +373 -0
  9. package/site/content/docs/elements/ai/index.mdx +9 -3
  10. package/site/content/docs/elements/ai/meta.json +1 -1
  11. package/site/content/docs/elements/ai/prompts.mdx +4 -2
  12. package/site/content/docs/elements/channel/index.mdx +4 -4
  13. package/site/content/docs/elements/channel/receipts.mdx +7 -5
  14. package/site/content/docs/elements/channel/sms.mdx +2 -1
  15. package/site/content/docs/elements/flow/index.mdx +22 -18
  16. package/site/content/docs/elements/gate/auth.mdx +4 -1
  17. package/site/content/docs/elements/signal/index.mdx +15 -14
  18. package/site/content/docs/elements/store/index.mdx +1 -1
  19. package/site/content/docs/elements/store/kv.mdx +5 -5
  20. package/site/content/docs/plugins/otp.mdx +3 -3
  21. package/site/content/docs/recipes/dragonfly.mdx +5 -5
  22. package/site/content/docs/recipes/index.mdx +6 -6
  23. package/site/content/docs/recipes/postgres.mdx +12 -8
  24. package/site/content/docs/recipes/redis.mdx +4 -4
  25. package/site/content/docs/reference/cli.mdx +2 -1
  26. package/site/content/docs/reference/configuration.mdx +9 -7
  27. package/site/content/docs/reference/fx.mdx +19 -10
  28. package/src/auth/api-key-sql.test.ts +44 -0
  29. package/src/auth/api-key-sql.ts +1 -0
  30. package/src/auth/api-keys.ts +30 -12
  31. package/src/auth/config.ts +6 -2
  32. package/src/auth/gate-auth.test.ts +20 -0
  33. package/src/auth/tenants.ts +17 -0
  34. package/src/cli/agents-md.test.ts +25 -0
  35. package/src/cli/ask-vault-gaps.test.ts +38 -1
  36. package/src/cli/ask-vault-gaps.ts +35 -2
  37. package/src/cli/decide.ts +174 -0
  38. package/src/cli/decision-lock-watch.test.ts +45 -0
  39. package/src/cli/decision-lock-watch.ts +51 -0
  40. package/src/cli/dev-app-runner.ts +34 -8
  41. package/src/cli/dev-auth-secret.test.ts +49 -0
  42. package/src/cli/dev-auth-secret.ts +49 -0
  43. package/src/cli/dev-hot.test.ts +41 -0
  44. package/src/cli/dev-hot.ts +48 -0
  45. package/src/cli/dev.test.ts +1 -1
  46. package/src/cli/dev.ts +24 -6
  47. package/src/cli/doctor-diff.test.ts +29 -1
  48. package/src/cli/doctor-diff.ts +35 -0
  49. package/src/cli/eval.ts +142 -1
  50. package/src/cli/index.ts +5 -0
  51. package/src/cli/load-config.images.test.ts +2 -2
  52. package/src/cli/load-config.ts +1 -1
  53. package/src/cli/manifest-pr-diff.ts +16 -0
  54. package/src/cli/mcp-from-console.ts +48 -0
  55. package/src/cli/registry.ts +35 -1
  56. package/src/client/agent.ts +307 -0
  57. package/src/client/transport.test.ts +20 -0
  58. package/src/client/transport.ts +2 -1
  59. package/src/client/types.ts +1 -1
  60. package/src/client-react/index.ts +7 -1
  61. package/src/client-react/use-agent-run.test.ts +271 -0
  62. package/src/client-react/use-agent-run.ts +193 -0
  63. package/src/compiler/ai-approval.test.ts +36 -0
  64. package/src/compiler/ai-repair.test.ts +22 -0
  65. package/src/compiler/decisions.extract.test.ts +164 -0
  66. package/src/compiler/effects-fetch.test.ts +77 -0
  67. package/src/compiler/effects-infer.ts +114 -17
  68. package/src/compiler/effects-join.test.ts +48 -0
  69. package/src/compiler/extract.test.ts +28 -0
  70. package/src/compiler/extract.ts +279 -13
  71. package/src/compiler/fx-index.ts +58 -4
  72. package/src/compiler/response.ts +35 -2
  73. package/src/config/index.ts +21 -3
  74. package/src/console/index.ts +1 -1
  75. package/src/console/server/ai-runs-flows.ts +269 -0
  76. package/src/console/server/ai-runs.test.ts +323 -0
  77. package/src/console/server/ai-runs.ts +562 -0
  78. package/src/console/server/ai.ts +12 -1
  79. package/src/console/server/app.ts +10 -1
  80. package/src/console/server/decisions-flows.ts +157 -0
  81. package/src/console/server/decisions.test.ts +66 -0
  82. package/src/console/server/decisions.ts +229 -0
  83. package/src/console/server/flows.ts +50 -2
  84. package/src/console/server/invoke-user-flow.ts +9 -0
  85. package/src/console/server/runs-ingest.ts +16 -0
  86. package/src/console/server/serve.ts +5 -0
  87. package/src/console/server/state.ts +25 -1
  88. package/src/console/server/store-stats.test.ts +47 -0
  89. package/src/console/server/store-stats.ts +92 -18
  90. package/src/console/ui-next/dist/assets/access-page-C_N4qvai.js +4 -0
  91. package/src/console/ui-next/dist/assets/{agent-disclosure-Bkohc_8X.js → agent-disclosure-Cq1TxniW.js} +1 -1
  92. package/src/console/ui-next/dist/assets/{cache-glyph-rL0lHxat.js → cache-glyph-BaI4uxZz.js} +1 -1
  93. package/src/console/ui-next/dist/assets/{call-pii-button-Deo4PP18.js → call-pii-button-DNyK7IIY.js} +1 -1
  94. package/src/console/ui-next/dist/assets/{collapsible-CoJ6amHf.js → collapsible-CRhBJMZR.js} +1 -1
  95. package/src/console/ui-next/dist/assets/confirm-sheet-DZfuJXP4.js +1 -0
  96. package/src/console/ui-next/dist/assets/copy-inline-button-BTjPVjRT.js +1 -0
  97. package/src/console/ui-next/dist/assets/decisions-page-D9twj8Xy.js +1 -0
  98. package/src/console/ui-next/dist/assets/detail-header-Dk5LipF6.js +1 -0
  99. package/src/console/ui-next/dist/assets/dropdown-menu-CRGrj5JU.js +1 -0
  100. package/src/console/ui-next/dist/assets/duration-tone-CsHWlVPU.js +9 -0
  101. package/src/console/ui-next/dist/assets/element-icons-C6t8Vlml.js +1 -0
  102. package/src/console/ui-next/dist/assets/explorer-chrome-Dslh2yE8.js +1 -0
  103. package/src/console/ui-next/dist/assets/explorer-empty-CIVg7Jgb.js +1 -0
  104. package/src/console/ui-next/dist/assets/flows-page-ZaTJAhR9.js +1 -0
  105. package/src/console/ui-next/dist/assets/{highlighted-json-CPYBzDh0.js → highlighted-json-DQ-EMAfz.js} +1 -1
  106. package/src/console/ui-next/dist/assets/{http-method-DTPevKmM.js → http-method-Cn_Fl79s.js} +1 -1
  107. package/src/console/ui-next/dist/assets/index-BlrN49Hs.css +2 -0
  108. package/src/console/ui-next/dist/assets/index-CmRFeWdT.js +58 -0
  109. package/src/console/ui-next/dist/assets/observability-page-Cn6R_dXz.js +8 -0
  110. package/src/console/ui-next/dist/assets/{replica-lag-BM27lXld.js → replica-lag-Bx6EZhSe.js} +7 -7
  111. package/src/console/ui-next/dist/assets/request-meta-DmdiUrfh.js +1 -0
  112. package/src/console/ui-next/dist/assets/shortcut-keys-DTE1sjTC.js +1 -0
  113. package/src/console/ui-next/dist/assets/store-page-8P6vGXnn.js +41 -0
  114. package/src/console/ui-next/dist/assets/trace-detail-sheet-BfGvpFSv.js +2 -0
  115. package/src/console/ui-next/dist/assets/tree-expand-toggle-CWdUHbc0.js +55 -0
  116. package/src/console/ui-next/dist/assets/units-page-CZOFZzay.js +1 -0
  117. package/src/console/ui-next/dist/assets/use-vault-list-BAyQG8Re.js +1 -0
  118. package/src/console/ui-next/dist/assets/vault-page-ki_ishmX.js +2 -0
  119. package/src/console/ui-next/dist/index.html +4 -3
  120. package/src/console/ui-next/seed-parked-approval.ts +126 -0
  121. package/src/console/ui-next/src/client.ts +224 -1
  122. package/src/console/ui-next/src/components/explorer/detail-header.tsx +9 -0
  123. package/src/console/ui-next/src/components/explorer/explorer-start-toggle.tsx +5 -1
  124. package/src/console/ui-next/src/components/ui/sheet-form.tsx +21 -12
  125. package/src/console/ui-next/src/features/flows/decisions/decisions-page.tsx +216 -0
  126. package/src/console/ui-next/src/features/flows/decisions/resolve.test.ts +48 -0
  127. package/src/console/ui-next/src/features/flows/decisions/resolve.ts +111 -0
  128. package/src/console/ui-next/src/features/flows/flows-page.tsx +10 -0
  129. package/src/console/ui-next/src/features/flows/graph/element-map.ts +1 -0
  130. package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +6 -0
  131. package/src/console/ui-next/src/features/flows/traces/effect-summary.test.ts +24 -0
  132. package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
  133. package/src/console/ui-next/src/features/flows/traces/http-status.ts +58 -0
  134. package/src/console/ui-next/src/features/flows/traces/request-client-card.tsx +355 -0
  135. package/src/console/ui-next/src/features/flows/traces/request-client.test.ts +114 -0
  136. package/src/console/ui-next/src/features/flows/traces/request-client.ts +423 -0
  137. package/src/console/ui-next/src/features/flows/traces/request-input-view.test.ts +52 -6
  138. package/src/console/ui-next/src/features/flows/traces/request-input-view.ts +85 -19
  139. package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +505 -80
  140. package/src/console/ui-next/src/features/flows/traces/trace-lanes.test.ts +61 -0
  141. package/src/console/ui-next/src/features/flows/traces/trace-lanes.ts +78 -0
  142. package/src/console/ui-next/src/features/flows/traces/trace-nav.test.ts +48 -0
  143. package/src/console/ui-next/src/features/flows/traces/trace-nav.ts +57 -0
  144. package/src/console/ui-next/src/features/flows/traces/trace-request-section.tsx +275 -96
  145. package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +16 -2
  146. package/src/console/ui-next/src/features/observability/detail/ai-runs.tsx +369 -0
  147. package/src/console/ui-next/src/features/observability/observability-page.tsx +31 -1
  148. package/src/console/ui-next/src/features/observability/state/observability-selection.ts +18 -3
  149. package/src/console/ui-next/src/features/store/performance/kv-performance-panel.tsx +12 -1
  150. package/src/console/ui-next/src/features/store/performance/performance-panel.tsx +14 -1
  151. package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +1 -1
  152. package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +6 -2
  153. package/src/console/ui-next/src/features/units/explorer/units-tree.tsx +73 -18
  154. package/src/console/ui-next/src/features/units/lib/unit-tree.test.ts +21 -0
  155. package/src/console/ui-next/src/features/units/lib/unit-tree.ts +82 -0
  156. package/src/console/ui-next/src/features/units/units-page.tsx +25 -7
  157. package/src/console/ui-next/src/router.tsx +17 -0
  158. package/src/console/ui-next/ui-next-seed-runs.ts +23 -0
  159. package/src/console/ui-next/vite-console-kernel-plugin.ts +43 -3
  160. package/src/console/ui-next/vite.config.ts +9 -0
  161. package/src/docker/recipes/dragonfly.ts +3 -2
  162. package/src/docker/recipes/redis.ts +1 -1
  163. package/src/drivers/ai-anthropic.ts +212 -21
  164. package/src/drivers/ai-mock.ts +34 -1
  165. package/src/drivers/ai-openai-compatible.ts +67 -26
  166. package/src/drivers/ai-preconnect.ts +18 -0
  167. package/src/drivers/ai-stream.test.ts +62 -0
  168. package/src/drivers/ai-types.ts +20 -0
  169. package/src/drivers/journal-postgres.test.ts +22 -1
  170. package/src/drivers/journal-postgres.ts +511 -13
  171. package/src/drivers/memory-ddl.test.ts +18 -0
  172. package/src/drivers/memory.ts +21 -1
  173. package/src/drivers/pg-rls.test.ts +31 -0
  174. package/src/drivers/pg-rls.ts +15 -0
  175. package/src/drivers/postgres.test.ts +9 -0
  176. package/src/drivers/postgres.ts +49 -11
  177. package/src/drivers/signal-compete.test.ts +234 -0
  178. package/src/drivers/signal-postgres.ts +39 -19
  179. package/src/drivers/signal-redis.ts +167 -11
  180. package/src/drivers/signal-types.ts +12 -0
  181. package/src/elements/ai/agent-event-slot.ts +27 -0
  182. package/src/elements/ai/approval-http.ts +207 -0
  183. package/src/elements/ai/approval.test.ts +741 -0
  184. package/src/elements/ai/approval.ts +228 -0
  185. package/src/elements/ai/decisions/bind.ts +163 -0
  186. package/src/elements/ai/decisions/certificate.test.ts +291 -0
  187. package/src/elements/ai/decisions/certificate.ts +377 -0
  188. package/src/elements/ai/decisions/certify.ts +314 -0
  189. package/src/elements/ai/decisions/decide.test.ts +953 -0
  190. package/src/elements/ai/decisions/e2e.test.ts +399 -0
  191. package/src/elements/ai/decisions/export.test.ts +120 -0
  192. package/src/elements/ai/decisions/export.ts +146 -0
  193. package/src/elements/ai/decisions/fixtures/openrouter-request.json +24 -0
  194. package/src/elements/ai/decisions/fixtures/openrouter-response.json +22 -0
  195. package/src/elements/ai/decisions/fixtures/typesafe-request.json +19 -0
  196. package/src/elements/ai/decisions/fixtures/typesafe-response.json +13 -0
  197. package/src/elements/ai/decisions/http.test.ts +154 -0
  198. package/src/elements/ai/decisions/http.ts +240 -0
  199. package/src/elements/ai/decisions/labels.test.ts +282 -0
  200. package/src/elements/ai/decisions/labels.ts +257 -0
  201. package/src/elements/ai/decisions/openrouter.live.test.ts +136 -0
  202. package/src/elements/ai/decisions/openrouter.ts +36 -0
  203. package/src/elements/ai/decisions/provider.ts +102 -0
  204. package/src/elements/ai/decisions/typesafe.ts +36 -0
  205. package/src/elements/ai/declare.ts +252 -1
  206. package/src/elements/ai/events.test.ts +379 -0
  207. package/src/elements/ai/events.ts +138 -0
  208. package/src/elements/ai/mcp-protocol.ts +5 -6
  209. package/src/elements/ai/run-events.test.ts +461 -0
  210. package/src/elements/ai/run-events.ts +522 -0
  211. package/src/elements/ai/runtime.ts +1187 -105
  212. package/src/elements/ai/stream-turn.ts +214 -0
  213. package/src/elements/ai/stream.live.test.ts +76 -0
  214. package/src/elements/ai/subagent.test.ts +136 -0
  215. package/src/elements/ai.test.ts +497 -3
  216. package/src/elements/ai.ts +16 -1
  217. package/src/elements/channel/runtime.ts +11 -0
  218. package/src/elements/channel/sql-ledger.ts +251 -0
  219. package/src/elements/clock/durable.ts +27 -1
  220. package/src/elements/signal/runtime.ts +9 -0
  221. package/src/elements/signal.test.ts +1 -0
  222. package/src/elements/store/cache.test.ts +120 -15
  223. package/src/elements/store/cache.ts +200 -39
  224. package/src/elements/store/prepare-row.test.ts +6 -2
  225. package/src/elements/store/sql-session.test.ts +53 -0
  226. package/src/elements/store/sql-session.ts +206 -10
  227. package/src/elements/store.ts +1 -0
  228. package/src/i18n/catalogs/ar.ts +5 -0
  229. package/src/i18n/catalogs/en.ts +5 -0
  230. package/src/index.ts +2 -0
  231. package/src/kernel/agent-event-store.ts +448 -0
  232. package/src/kernel/app.ts +135 -17
  233. package/src/kernel/auto-cache.test.ts +228 -0
  234. package/src/kernel/boot-bind/ai.ts +3 -0
  235. package/src/kernel/boot-bind/channel.ts +63 -2
  236. package/src/kernel/boot-bind/honor-config.test.ts +40 -10
  237. package/src/kernel/boot-bind/signal.ts +61 -25
  238. package/src/kernel/boot-bind/store.test.ts +26 -0
  239. package/src/kernel/boot-bind/store.ts +82 -6
  240. package/src/kernel/boot.ts +31 -6
  241. package/src/kernel/builtin-errors.ts +2 -0
  242. package/src/kernel/capability.ts +3 -0
  243. package/src/kernel/decision-label-store.ts +441 -0
  244. package/src/kernel/effects.test.ts +2 -1
  245. package/src/kernel/effects.ts +13 -1
  246. package/src/kernel/element-registries.ts +3 -0
  247. package/src/kernel/errors-compiler.ts +11 -0
  248. package/src/kernel/errors-text.ts +20 -0
  249. package/src/kernel/errors.ts +10 -0
  250. package/src/kernel/flow.test.ts +2 -3
  251. package/src/kernel/flow.ts +33 -3
  252. package/src/kernel/fx-call-types.test.ts +32 -0
  253. package/src/kernel/fx-decide.ts +765 -0
  254. package/src/kernel/fx-sql-handle.ts +46 -7
  255. package/src/kernel/fx.test.ts +13 -8
  256. package/src/kernel/fx.ts +339 -45
  257. package/src/kernel/http-frame.test.ts +94 -0
  258. package/src/kernel/http-frame.ts +203 -0
  259. package/src/kernel/idempotency.test.ts +2 -1
  260. package/src/kernel/journal.test.ts +114 -0
  261. package/src/kernel/journal.ts +471 -51
  262. package/src/kernel/json-result.ts +10 -0
  263. package/src/kernel/pipeline-tenant.ts +5 -1
  264. package/src/kernel/pipeline.test.ts +45 -0
  265. package/src/kernel/pipeline.ts +5 -0
  266. package/src/kernel/signal-tx.ts +28 -0
  267. package/src/kernel/sse-id.ts +32 -0
  268. package/src/kernel/store-transaction.test.ts +95 -0
  269. package/src/manifest/diff.ts +1 -0
  270. package/src/manifest/types.ts +29 -2
  271. package/src/mcp/ai-tools.test.ts +202 -0
  272. package/src/mcp/authorization.ts +66 -0
  273. package/src/mcp/docs-server.ts +19 -2
  274. package/src/mcp/mcp.test.ts +83 -0
  275. package/src/mcp/protocol.ts +8 -2
  276. package/src/mcp/server.ts +23 -2
  277. package/src/mcp/session.ts +3 -0
  278. package/src/mcp/tools.ts +168 -0
  279. package/src/mcp/versions.ts +60 -0
  280. package/src/plugins/index.ts +1 -0
  281. package/src/plugins/mena.ts +56 -0
  282. package/src/release/http-graph.test.ts +2 -0
  283. package/src/release/http-graph.ts +5 -0
  284. package/src/release/measure.ts +39 -14
  285. package/src/runs/collect.ts +4 -1
  286. package/src/runs/parquet.test.ts +27 -1
  287. package/src/runs/parquet.ts +19 -0
  288. package/src/runs/types.ts +31 -0
  289. package/src/runtime/json-code-block.test.ts +106 -47
  290. package/src/runtime/json-code-block.ts +1209 -46
  291. package/src/term.test.ts +13 -0
  292. package/src/term.ts +81 -49
  293. package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +0 -1
  294. package/src/console/ui-next/dist/assets/access-page-Ze6ckjdd.js +0 -4
  295. package/src/console/ui-next/dist/assets/copy-inline-button-CWG5_ZOh.js +0 -1
  296. package/src/console/ui-next/dist/assets/detail-header-b_ZzIsmU.js +0 -1
  297. package/src/console/ui-next/dist/assets/dropdown-menu-nwXEO1Ac.js +0 -1
  298. package/src/console/ui-next/dist/assets/duration-tone-BbQ_8z50.js +0 -9
  299. package/src/console/ui-next/dist/assets/element-icons-CwyLpVXz.js +0 -1
  300. package/src/console/ui-next/dist/assets/explorer-empty-C8sSCqYT.js +0 -1
  301. package/src/console/ui-next/dist/assets/flows-page-D2E7BtKK.js +0 -1
  302. package/src/console/ui-next/dist/assets/index-VxoEz295.css +0 -2
  303. package/src/console/ui-next/dist/assets/index-vTuwmeQz.js +0 -58
  304. package/src/console/ui-next/dist/assets/observability-page-SJTSzHFJ.js +0 -4
  305. package/src/console/ui-next/dist/assets/request-meta-BjFT7DNl.js +0 -1
  306. package/src/console/ui-next/dist/assets/shortcut-keys-CUNegEV4.js +0 -1
  307. package/src/console/ui-next/dist/assets/store-page-Qj9ThRE-.js +0 -41
  308. package/src/console/ui-next/dist/assets/trace-detail-sheet-Cmitq3da.js +0 -2
  309. package/src/console/ui-next/dist/assets/tree-expand-toggle-CbDIB-7x.js +0 -55
  310. package/src/console/ui-next/dist/assets/units-page-_PuYqFty.js +0 -1
  311. package/src/console/ui-next/dist/assets/use-vault-list-Cs45Czkt.js +0 -1
  312. package/src/console/ui-next/dist/assets/vault-page-BOMr0go9.js +0 -2
@@ -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
+ }