okengine 0.22.0 → 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 (262) 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 +13 -9
  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/gate/auth.mdx +4 -1
  13. package/site/content/docs/elements/store/index.mdx +1 -1
  14. package/site/content/docs/elements/store/kv.mdx +5 -5
  15. package/site/content/docs/recipes/dragonfly.mdx +5 -5
  16. package/site/content/docs/recipes/index.mdx +6 -6
  17. package/site/content/docs/recipes/postgres.mdx +12 -8
  18. package/site/content/docs/recipes/redis.mdx +4 -4
  19. package/site/content/docs/reference/cli.mdx +2 -1
  20. package/site/content/docs/reference/configuration.mdx +9 -7
  21. package/site/content/docs/reference/fx.mdx +7 -6
  22. package/src/auth/api-key-sql.test.ts +44 -0
  23. package/src/auth/api-key-sql.ts +1 -0
  24. package/src/auth/api-keys.ts +30 -12
  25. package/src/auth/config.ts +6 -2
  26. package/src/auth/gate-auth.test.ts +20 -0
  27. package/src/cli/agents-md.test.ts +25 -0
  28. package/src/cli/ask-vault-gaps.test.ts +38 -1
  29. package/src/cli/ask-vault-gaps.ts +35 -2
  30. package/src/cli/decide.ts +174 -0
  31. package/src/cli/decision-lock-watch.test.ts +45 -0
  32. package/src/cli/decision-lock-watch.ts +51 -0
  33. package/src/cli/dev-app-runner.ts +34 -8
  34. package/src/cli/dev-auth-secret.test.ts +49 -0
  35. package/src/cli/dev-auth-secret.ts +49 -0
  36. package/src/cli/dev-hot.test.ts +41 -0
  37. package/src/cli/dev-hot.ts +48 -0
  38. package/src/cli/dev.test.ts +1 -1
  39. package/src/cli/dev.ts +24 -6
  40. package/src/cli/eval.ts +142 -1
  41. package/src/cli/index.ts +5 -0
  42. package/src/cli/load-config.images.test.ts +2 -2
  43. package/src/cli/load-config.ts +1 -1
  44. package/src/cli/mcp-from-console.ts +48 -0
  45. package/src/cli/registry.ts +35 -1
  46. package/src/client/agent.ts +307 -0
  47. package/src/client/transport.test.ts +20 -0
  48. package/src/client/transport.ts +2 -1
  49. package/src/client/types.ts +1 -1
  50. package/src/client-react/index.ts +7 -1
  51. package/src/client-react/use-agent-run.test.ts +271 -0
  52. package/src/client-react/use-agent-run.ts +193 -0
  53. package/src/compiler/ai-approval.test.ts +36 -0
  54. package/src/compiler/ai-repair.test.ts +22 -0
  55. package/src/compiler/decisions.extract.test.ts +164 -0
  56. package/src/compiler/effects-infer.ts +27 -1
  57. package/src/compiler/extract.ts +268 -12
  58. package/src/compiler/response.ts +35 -2
  59. package/src/config/index.ts +21 -3
  60. package/src/console/server/ai-runs-flows.ts +269 -0
  61. package/src/console/server/ai-runs.test.ts +323 -0
  62. package/src/console/server/ai-runs.ts +562 -0
  63. package/src/console/server/ai.ts +12 -1
  64. package/src/console/server/app.ts +10 -1
  65. package/src/console/server/decisions-flows.ts +157 -0
  66. package/src/console/server/decisions.test.ts +66 -0
  67. package/src/console/server/decisions.ts +229 -0
  68. package/src/console/server/flows.ts +50 -2
  69. package/src/console/server/invoke-user-flow.ts +9 -0
  70. package/src/console/server/runs-ingest.ts +16 -0
  71. package/src/console/server/serve.ts +5 -0
  72. package/src/console/server/state.ts +25 -1
  73. package/src/console/server/store-stats.test.ts +47 -0
  74. package/src/console/server/store-stats.ts +92 -18
  75. package/src/console/ui-next/dist/assets/access-page-DDKkhT9v.js +4 -0
  76. package/src/console/ui-next/dist/assets/{agent-disclosure-Bkohc_8X.js → agent-disclosure-62Xts8Xi.js} +1 -1
  77. package/src/console/ui-next/dist/assets/{cache-glyph-rL0lHxat.js → cache-glyph-DyeoKHKA.js} +1 -1
  78. package/src/console/ui-next/dist/assets/{call-pii-button-Deo4PP18.js → call-pii-button-DvfpLXsU.js} +1 -1
  79. package/src/console/ui-next/dist/assets/{collapsible-CoJ6amHf.js → collapsible-BY03SeCg.js} +1 -1
  80. package/src/console/ui-next/dist/assets/confirm-sheet-DZfuJXP4.js +1 -0
  81. package/src/console/ui-next/dist/assets/copy-inline-button-BTjPVjRT.js +1 -0
  82. package/src/console/ui-next/dist/assets/decisions-page-CgkKlA56.js +1 -0
  83. package/src/console/ui-next/dist/assets/detail-header-Dk5LipF6.js +1 -0
  84. package/src/console/ui-next/dist/assets/dropdown-menu-CRGrj5JU.js +1 -0
  85. package/src/console/ui-next/dist/assets/duration-tone-Y6HLjyJ5.js +9 -0
  86. package/src/console/ui-next/dist/assets/element-icons-C6t8Vlml.js +1 -0
  87. package/src/console/ui-next/dist/assets/explorer-chrome-Dslh2yE8.js +1 -0
  88. package/src/console/ui-next/dist/assets/explorer-empty-CIVg7Jgb.js +1 -0
  89. package/src/console/ui-next/dist/assets/flows-page-DLPfNy-_.js +1 -0
  90. package/src/console/ui-next/dist/assets/{highlighted-json-CPYBzDh0.js → highlighted-json-D7nNzpgB.js} +1 -1
  91. package/src/console/ui-next/dist/assets/{http-method-DTPevKmM.js → http-method-DdL19zzh.js} +1 -1
  92. package/src/console/ui-next/dist/assets/index-BlrN49Hs.css +2 -0
  93. package/src/console/ui-next/dist/assets/index-C0lc_s9d.js +58 -0
  94. package/src/console/ui-next/dist/assets/observability-page-CRlcyLCC.js +8 -0
  95. package/src/console/ui-next/dist/assets/{replica-lag-BM27lXld.js → replica-lag-Bb3FWUu9.js} +7 -7
  96. package/src/console/ui-next/dist/assets/request-meta-DYBMWhX8.js +1 -0
  97. package/src/console/ui-next/dist/assets/shortcut-keys-DTE1sjTC.js +1 -0
  98. package/src/console/ui-next/dist/assets/store-page-CcE-SXC8.js +41 -0
  99. package/src/console/ui-next/dist/assets/trace-detail-sheet-CKMSEQ4s.js +2 -0
  100. package/src/console/ui-next/dist/assets/tree-expand-toggle-QOfk0M0H.js +55 -0
  101. package/src/console/ui-next/dist/assets/units-page-DKC1CDDd.js +1 -0
  102. package/src/console/ui-next/dist/assets/use-vault-list-BAyQG8Re.js +1 -0
  103. package/src/console/ui-next/dist/assets/vault-page-B54v4Yit.js +2 -0
  104. package/src/console/ui-next/dist/index.html +4 -3
  105. package/src/console/ui-next/seed-parked-approval.ts +126 -0
  106. package/src/console/ui-next/src/client.ts +224 -1
  107. package/src/console/ui-next/src/components/explorer/detail-header.tsx +9 -0
  108. package/src/console/ui-next/src/components/explorer/explorer-start-toggle.tsx +5 -1
  109. package/src/console/ui-next/src/components/ui/sheet-form.tsx +21 -12
  110. package/src/console/ui-next/src/features/flows/decisions/decisions-page.tsx +216 -0
  111. package/src/console/ui-next/src/features/flows/decisions/resolve.test.ts +48 -0
  112. package/src/console/ui-next/src/features/flows/decisions/resolve.ts +111 -0
  113. package/src/console/ui-next/src/features/flows/flows-page.tsx +10 -0
  114. package/src/console/ui-next/src/features/flows/graph/element-map.ts +1 -0
  115. package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +6 -0
  116. package/src/console/ui-next/src/features/flows/traces/effect-summary.test.ts +24 -0
  117. package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
  118. package/src/console/ui-next/src/features/flows/traces/http-status.ts +58 -0
  119. package/src/console/ui-next/src/features/flows/traces/request-client-card.tsx +355 -0
  120. package/src/console/ui-next/src/features/flows/traces/request-client.test.ts +114 -0
  121. package/src/console/ui-next/src/features/flows/traces/request-client.ts +423 -0
  122. package/src/console/ui-next/src/features/flows/traces/request-input-view.test.ts +52 -6
  123. package/src/console/ui-next/src/features/flows/traces/request-input-view.ts +85 -19
  124. package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +505 -80
  125. package/src/console/ui-next/src/features/flows/traces/trace-lanes.test.ts +61 -0
  126. package/src/console/ui-next/src/features/flows/traces/trace-lanes.ts +78 -0
  127. package/src/console/ui-next/src/features/flows/traces/trace-nav.test.ts +48 -0
  128. package/src/console/ui-next/src/features/flows/traces/trace-nav.ts +57 -0
  129. package/src/console/ui-next/src/features/flows/traces/trace-request-section.tsx +275 -96
  130. package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +16 -2
  131. package/src/console/ui-next/src/features/observability/detail/ai-runs.tsx +369 -0
  132. package/src/console/ui-next/src/features/observability/observability-page.tsx +31 -1
  133. package/src/console/ui-next/src/features/observability/state/observability-selection.ts +18 -3
  134. package/src/console/ui-next/src/features/store/performance/kv-performance-panel.tsx +12 -1
  135. package/src/console/ui-next/src/features/store/performance/performance-panel.tsx +14 -1
  136. package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +1 -1
  137. package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +6 -2
  138. package/src/console/ui-next/src/features/units/explorer/units-tree.tsx +73 -18
  139. package/src/console/ui-next/src/features/units/lib/unit-tree.test.ts +21 -0
  140. package/src/console/ui-next/src/features/units/lib/unit-tree.ts +82 -0
  141. package/src/console/ui-next/src/features/units/units-page.tsx +25 -7
  142. package/src/console/ui-next/src/router.tsx +17 -0
  143. package/src/console/ui-next/ui-next-seed-runs.ts +23 -0
  144. package/src/console/ui-next/vite-console-kernel-plugin.ts +43 -3
  145. package/src/console/ui-next/vite.config.ts +9 -0
  146. package/src/docker/recipes/dragonfly.ts +3 -2
  147. package/src/docker/recipes/redis.ts +1 -1
  148. package/src/drivers/ai-anthropic.ts +212 -21
  149. package/src/drivers/ai-mock.ts +34 -1
  150. package/src/drivers/ai-openai-compatible.ts +67 -26
  151. package/src/drivers/ai-preconnect.ts +18 -0
  152. package/src/drivers/ai-stream.test.ts +62 -0
  153. package/src/drivers/ai-types.ts +20 -0
  154. package/src/drivers/journal-postgres.ts +289 -2
  155. package/src/drivers/memory-ddl.test.ts +18 -0
  156. package/src/drivers/memory.ts +21 -1
  157. package/src/drivers/pg-rls.test.ts +31 -0
  158. package/src/drivers/pg-rls.ts +15 -0
  159. package/src/drivers/postgres.test.ts +9 -0
  160. package/src/drivers/postgres.ts +49 -11
  161. package/src/elements/ai/agent-event-slot.ts +27 -0
  162. package/src/elements/ai/approval-http.ts +207 -0
  163. package/src/elements/ai/approval.test.ts +741 -0
  164. package/src/elements/ai/approval.ts +228 -0
  165. package/src/elements/ai/decisions/bind.ts +163 -0
  166. package/src/elements/ai/decisions/certificate.test.ts +291 -0
  167. package/src/elements/ai/decisions/certificate.ts +377 -0
  168. package/src/elements/ai/decisions/certify.ts +314 -0
  169. package/src/elements/ai/decisions/decide.test.ts +953 -0
  170. package/src/elements/ai/decisions/e2e.test.ts +399 -0
  171. package/src/elements/ai/decisions/export.test.ts +120 -0
  172. package/src/elements/ai/decisions/export.ts +146 -0
  173. package/src/elements/ai/decisions/fixtures/openrouter-request.json +24 -0
  174. package/src/elements/ai/decisions/fixtures/openrouter-response.json +22 -0
  175. package/src/elements/ai/decisions/fixtures/typesafe-request.json +19 -0
  176. package/src/elements/ai/decisions/fixtures/typesafe-response.json +13 -0
  177. package/src/elements/ai/decisions/http.test.ts +154 -0
  178. package/src/elements/ai/decisions/http.ts +240 -0
  179. package/src/elements/ai/decisions/labels.test.ts +282 -0
  180. package/src/elements/ai/decisions/labels.ts +257 -0
  181. package/src/elements/ai/decisions/openrouter.live.test.ts +136 -0
  182. package/src/elements/ai/decisions/openrouter.ts +36 -0
  183. package/src/elements/ai/decisions/provider.ts +102 -0
  184. package/src/elements/ai/decisions/typesafe.ts +36 -0
  185. package/src/elements/ai/declare.ts +252 -1
  186. package/src/elements/ai/events.test.ts +379 -0
  187. package/src/elements/ai/events.ts +138 -0
  188. package/src/elements/ai/run-events.test.ts +461 -0
  189. package/src/elements/ai/run-events.ts +522 -0
  190. package/src/elements/ai/runtime.ts +1186 -105
  191. package/src/elements/ai/stream-turn.ts +214 -0
  192. package/src/elements/ai/stream.live.test.ts +76 -0
  193. package/src/elements/ai/subagent.test.ts +136 -0
  194. package/src/elements/ai.test.ts +497 -3
  195. package/src/elements/ai.ts +16 -1
  196. package/src/elements/clock/durable.ts +27 -1
  197. package/src/elements/store/sql-session.test.ts +51 -0
  198. package/src/elements/store/sql-session.ts +111 -1
  199. package/src/i18n/catalogs/ar.ts +5 -0
  200. package/src/i18n/catalogs/en.ts +5 -0
  201. package/src/index.ts +2 -0
  202. package/src/kernel/agent-event-store.ts +448 -0
  203. package/src/kernel/app.ts +103 -1
  204. package/src/kernel/boot-bind/ai.ts +3 -0
  205. package/src/kernel/boot-bind/store.test.ts +26 -0
  206. package/src/kernel/boot-bind/store.ts +82 -6
  207. package/src/kernel/boot.ts +15 -4
  208. package/src/kernel/builtin-errors.ts +2 -0
  209. package/src/kernel/capability.ts +3 -0
  210. package/src/kernel/decision-label-store.ts +441 -0
  211. package/src/kernel/effects.test.ts +2 -1
  212. package/src/kernel/effects.ts +13 -1
  213. package/src/kernel/element-registries.ts +3 -0
  214. package/src/kernel/errors-text.ts +4 -0
  215. package/src/kernel/errors.ts +2 -0
  216. package/src/kernel/flow.test.ts +2 -3
  217. package/src/kernel/fx-decide.ts +761 -0
  218. package/src/kernel/fx.test.ts +12 -5
  219. package/src/kernel/fx.ts +312 -29
  220. package/src/kernel/http-frame.test.ts +94 -0
  221. package/src/kernel/http-frame.ts +203 -0
  222. package/src/kernel/journal.ts +104 -0
  223. package/src/kernel/json-result.ts +10 -0
  224. package/src/kernel/pipeline.test.ts +45 -0
  225. package/src/kernel/sse-id.ts +32 -0
  226. package/src/manifest/diff.ts +1 -0
  227. package/src/manifest/types.ts +29 -2
  228. package/src/mcp/ai-tools.test.ts +202 -0
  229. package/src/mcp/authorization.ts +66 -0
  230. package/src/mcp/session.ts +3 -0
  231. package/src/mcp/tools.ts +168 -0
  232. package/src/release/http-graph.test.ts +2 -0
  233. package/src/release/http-graph.ts +5 -0
  234. package/src/release/measure.ts +39 -14
  235. package/src/runs/collect.ts +4 -1
  236. package/src/runs/parquet.test.ts +27 -1
  237. package/src/runs/parquet.ts +19 -0
  238. package/src/runs/types.ts +31 -0
  239. package/src/runtime/json-code-block.test.ts +106 -47
  240. package/src/runtime/json-code-block.ts +1209 -46
  241. package/src/term.test.ts +13 -0
  242. package/src/term.ts +81 -49
  243. package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +0 -1
  244. package/src/console/ui-next/dist/assets/access-page-Ze6ckjdd.js +0 -4
  245. package/src/console/ui-next/dist/assets/copy-inline-button-CWG5_ZOh.js +0 -1
  246. package/src/console/ui-next/dist/assets/detail-header-b_ZzIsmU.js +0 -1
  247. package/src/console/ui-next/dist/assets/dropdown-menu-nwXEO1Ac.js +0 -1
  248. package/src/console/ui-next/dist/assets/duration-tone-BbQ_8z50.js +0 -9
  249. package/src/console/ui-next/dist/assets/element-icons-CwyLpVXz.js +0 -1
  250. package/src/console/ui-next/dist/assets/explorer-empty-C8sSCqYT.js +0 -1
  251. package/src/console/ui-next/dist/assets/flows-page-D2E7BtKK.js +0 -1
  252. package/src/console/ui-next/dist/assets/index-VxoEz295.css +0 -2
  253. package/src/console/ui-next/dist/assets/index-vTuwmeQz.js +0 -58
  254. package/src/console/ui-next/dist/assets/observability-page-SJTSzHFJ.js +0 -4
  255. package/src/console/ui-next/dist/assets/request-meta-BjFT7DNl.js +0 -1
  256. package/src/console/ui-next/dist/assets/shortcut-keys-CUNegEV4.js +0 -1
  257. package/src/console/ui-next/dist/assets/store-page-Qj9ThRE-.js +0 -41
  258. package/src/console/ui-next/dist/assets/trace-detail-sheet-Cmitq3da.js +0 -2
  259. package/src/console/ui-next/dist/assets/tree-expand-toggle-CbDIB-7x.js +0 -55
  260. package/src/console/ui-next/dist/assets/units-page-_PuYqFty.js +0 -1
  261. package/src/console/ui-next/dist/assets/use-vault-list-Cs45Czkt.js +0 -1
  262. package/src/console/ui-next/dist/assets/vault-page-BOMr0go9.js +0 -2
@@ -20,6 +20,9 @@ import {
20
20
  IDEM_RENEW_SQL,
21
21
  IDEM_SELECT_SQL,
22
22
  } from "../kernel/idempotency-store.ts";
23
+ import type { DecisionLabelStore } from "../kernel/decision-label-store.ts";
24
+ import type { AgentEventStore } from "../kernel/agent-event-store.ts";
25
+ import { lazyRequire } from "../kernel/lazy-require.ts";
23
26
  import {
24
27
  JOURNAL_DEFAULT_LEASE_MS,
25
28
  type JournalEntry,
@@ -257,9 +260,47 @@ export function createPostgresJournalFake(): PostgresJournalSql & {
257
260
  created_at: number;
258
261
  expires_at: number;
259
262
  };
260
- type State = { rows: JournalDbRow[]; idem: IdemDbRow[] };
263
+ type LabelDbRow = {
264
+ decision_id: string;
265
+ question: string;
266
+ value: string;
267
+ propensity: number;
268
+ reviewer: string;
269
+ locale: string | null;
270
+ model: string | null;
271
+ tenant: string | null;
272
+ score: number | null;
273
+ loss: number | null;
274
+ raw: string | null;
275
+ at: number;
276
+ input: string | null;
277
+ review_id?: string | null;
278
+ };
279
+ type State = {
280
+ rows: JournalDbRow[];
281
+ idem: IdemDbRow[];
282
+ labels: LabelDbRow[];
283
+ drift: { decision_id: string; suspended: number; certified_at: number }[];
284
+ candidates: { decision_id: string; body: string }[];
285
+ agentRuns: { run_id: string; header: string; sweep_claim_at: number | null }[];
286
+ agentEvents: {
287
+ run_id: string;
288
+ seq: number;
289
+ event: string;
290
+ event_type: string | null;
291
+ event_name: string | null;
292
+ }[];
293
+ };
261
294
 
262
- let committed: State = { rows: [], idem: [] };
295
+ let committed: State = {
296
+ rows: [],
297
+ idem: [],
298
+ labels: [],
299
+ drift: [],
300
+ candidates: [],
301
+ agentRuns: [],
302
+ agentEvents: [],
303
+ };
263
304
  let active: { state: State; locked: Set<string>; done: boolean } | null = null;
264
305
  /** Run ids held by other active transactions (SKIP LOCKED). */
265
306
  const heldByTxn = new Set<string>();
@@ -274,6 +315,11 @@ export function createPostgresJournalFake(): PostgresJournalSql & {
274
315
  return {
275
316
  rows: s.rows.map((r) => ({ ...r })),
276
317
  idem: s.idem.map((r) => ({ ...r })),
318
+ labels: s.labels.map((r) => ({ ...r })),
319
+ drift: s.drift.map((r) => ({ ...r })),
320
+ candidates: s.candidates.map((r) => ({ ...r })),
321
+ agentRuns: s.agentRuns.map((r) => ({ ...r })),
322
+ agentEvents: s.agentEvents.map((r) => ({ ...r })),
277
323
  };
278
324
  }
279
325
 
@@ -376,6 +422,65 @@ export function createPostgresJournalFake(): PostgresJournalSql & {
376
422
  .map((row) => ({ ...row }) as Record<string, unknown>);
377
423
  }
378
424
 
425
+ if (/FROM\s+oke_decision_labels/i.test(text)) {
426
+ let labels = state.labels;
427
+ let arg = 0;
428
+ if (/decision_id\s*=\s*\?/i.test(text)) {
429
+ const id = String(params[arg++] ?? "");
430
+ labels = labels.filter((row) => row.decision_id === id);
431
+ }
432
+ if (/tenant\s+IS\s+NOT\s+DISTINCT\s+FROM\s+\?/i.test(text)) {
433
+ const tenant = params[arg] ?? null;
434
+ labels = labels.filter((row) => row.tenant === tenant);
435
+ }
436
+ return labels.map((row) => ({ ...row }));
437
+ }
438
+
439
+ if (/FROM\s+oke_decision_drift/i.test(text)) {
440
+ return state.drift.map((row) => ({ ...row }));
441
+ }
442
+
443
+ if (/FROM\s+oke_decision_candidates/i.test(text)) {
444
+ if (/decision_id\s*=\s*\?/i.test(text)) {
445
+ const id = String(params[0] ?? "");
446
+ return state.candidates
447
+ .filter((row) => row.decision_id === id)
448
+ .map((row) => ({ ...row }));
449
+ }
450
+ return state.candidates.map((row) => ({ ...row }));
451
+ }
452
+
453
+ if (/FROM\s+oke_agent_run\b/i.test(text)) {
454
+ if (!/WHERE/i.test(text)) return state.agentRuns.map((row) => ({ ...row }));
455
+ const id = String(params[0] ?? "");
456
+ return state.agentRuns.filter((row) => row.run_id === id).map((row) => ({ ...row }));
457
+ }
458
+
459
+ if (/FROM\s+oke_agent_event\b/i.test(text)) {
460
+ const id = String(params[0] ?? "");
461
+ if (/MAX\s*\(\s*seq\s*\)/i.test(text)) {
462
+ const max = state.agentEvents
463
+ .filter((row) => row.run_id === id)
464
+ .reduce((highest, row) => Math.max(highest, row.seq), 0);
465
+ return [{ max_seq: max }];
466
+ }
467
+ if (/event_type\s*=/i.test(text)) {
468
+ const eventType = String(params[1] ?? "");
469
+ const eventName = String(params[2] ?? "");
470
+ return state.agentEvents
471
+ .filter(
472
+ (row) =>
473
+ row.run_id === id && row.event_type === eventType && row.event_name === eventName,
474
+ )
475
+ .map((row) => ({ ...row }));
476
+ }
477
+ const after = /seq\s*>\s*\?/i.test(text) ? Number(params[1] ?? 0) : 0;
478
+ return state.agentEvents
479
+ .filter((row) => row.run_id === id && row.seq > after)
480
+ .sort((a, b) => a.seq - b.seq)
481
+ .map((row) => ({ ...row }));
482
+ }
483
+
379
484
  throw new Error(`postgres journal fake: unsupported query: ${sql}`);
380
485
  },
381
486
  async exec(sql, params = []) {
@@ -539,6 +644,112 @@ export function createPostgresJournalFake(): PostgresJournalSql & {
539
644
  row.run_id = String(params[0]);
540
645
  return { changes: 1 };
541
646
  }
647
+ if (/^INSERT\s+INTO\s+oke_decision_labels\b/i.test(text)) {
648
+ state.labels.push({
649
+ decision_id: String(params[0]),
650
+ question: String(params[1]),
651
+ value: String(params[2]),
652
+ propensity: Number(params[3]),
653
+ reviewer: String(params[4]),
654
+ locale: params[5] === null || params[5] === undefined ? null : String(params[5]),
655
+ model: params[6] === null || params[6] === undefined ? null : String(params[6]),
656
+ tenant: params[7] === null || params[7] === undefined ? null : String(params[7]),
657
+ score: params[8] === null || params[8] === undefined ? null : Number(params[8]),
658
+ loss: params[9] === null || params[9] === undefined ? null : Number(params[9]),
659
+ raw: params[10] === null || params[10] === undefined ? null : String(params[10]),
660
+ at: Number(params[11]),
661
+ input: params[12] === null || params[12] === undefined ? null : String(params[12]),
662
+ review_id: params[13] === null || params[13] === undefined ? null : String(params[13]),
663
+ });
664
+ return { changes: 1 };
665
+ }
666
+
667
+ if (/^INSERT\s+INTO\s+oke_decision_candidates\b/i.test(text)) {
668
+ const decisionId = String(params[0]);
669
+ const body = String(params[1]);
670
+ const idx = state.candidates.findIndex((row) => row.decision_id === decisionId);
671
+ if (idx >= 0) state.candidates[idx] = { decision_id: decisionId, body };
672
+ else state.candidates.push({ decision_id: decisionId, body });
673
+ return { changes: 1 };
674
+ }
675
+
676
+ if (/^UPDATE\s+oke_decision_drift\b/i.test(text)) return { changes: 0 };
677
+
678
+ if (/^DELETE\s+FROM\s+oke_decision_drift\b/i.test(text)) {
679
+ const id = String(params[0] ?? "");
680
+ const before = state.drift.length;
681
+ state.drift = state.drift.filter((row) => row.decision_id !== id);
682
+ return { changes: before - state.drift.length };
683
+ }
684
+
685
+ if (/^INSERT\s+INTO\s+oke_decision_drift\b/i.test(text)) {
686
+ const decisionId = String(params[0]);
687
+ const next = {
688
+ decision_id: decisionId,
689
+ suspended: Number(params[1]),
690
+ certified_at: Number(params[2] ?? 0),
691
+ };
692
+ const idx = state.drift.findIndex((row) => row.decision_id === decisionId);
693
+ if (idx >= 0) state.drift[idx] = next;
694
+ else state.drift.push(next);
695
+ return { changes: 1 };
696
+ }
697
+
698
+ if (/^UPDATE\s+oke_agent_run\s+SET\s+sweep_claim_at\b/i.test(text)) {
699
+ const until = Number(params[0]);
700
+ const runId = String(params[1] ?? "");
701
+ const now = Number(params[2]);
702
+ const row = state.agentRuns.find((candidate) => candidate.run_id === runId);
703
+ if (!row) return { changes: 0 };
704
+ if (row.sweep_claim_at !== null && row.sweep_claim_at > now) return { changes: 0 };
705
+ row.sweep_claim_at = until;
706
+ return { changes: 1 };
707
+ }
708
+
709
+ if (/^INSERT\s+INTO\s+oke_agent_run\b/i.test(text)) {
710
+ const runId = String(params[0]);
711
+ const existing = state.agentRuns.find((row) => row.run_id === runId);
712
+ const next = {
713
+ run_id: runId,
714
+ header: String(params[1]),
715
+ sweep_claim_at: existing?.sweep_claim_at ?? null,
716
+ };
717
+ const idx = state.agentRuns.findIndex((row) => row.run_id === runId);
718
+ if (idx >= 0) state.agentRuns[idx] = next;
719
+ else state.agentRuns.push(next);
720
+ return { changes: 1 };
721
+ }
722
+
723
+ if (/^INSERT\s+INTO\s+oke_agent_event\b/i.test(text)) {
724
+ const runId = String(params[0]);
725
+ const seq = Number(params[1]);
726
+ if (state.agentEvents.some((row) => row.run_id === runId && row.seq === seq)) {
727
+ throw new Error(`duplicate key value violates unique constraint "oke_agent_event_pkey"`);
728
+ }
729
+ state.agentEvents.push({
730
+ run_id: runId,
731
+ seq,
732
+ event: String(params[2]),
733
+ event_type: params[3] === undefined || params[3] === null ? null : String(params[3]),
734
+ event_name: params[4] === undefined || params[4] === null ? null : String(params[4]),
735
+ });
736
+ return { changes: 1 };
737
+ }
738
+
739
+ if (/^DELETE\s+FROM\s+oke_agent_event\b/i.test(text)) {
740
+ const id = String(params[0] ?? "");
741
+ const before = state.agentEvents.length;
742
+ state.agentEvents = state.agentEvents.filter((row) => row.run_id !== id);
743
+ return { changes: before - state.agentEvents.length };
744
+ }
745
+
746
+ if (/^DELETE\s+FROM\s+oke_agent_run\b/i.test(text)) {
747
+ const id = String(params[0] ?? "");
748
+ const before = state.agentRuns.length;
749
+ state.agentRuns = state.agentRuns.filter((row) => row.run_id !== id);
750
+ return { changes: before - state.agentRuns.length };
751
+ }
752
+
542
753
  if (text === IDEM_FORFEIT_SQL) {
543
754
  const row = state.idem.find(
544
755
  (candidate) =>
@@ -605,6 +816,61 @@ export type PostgresJournalStore = JournalStore &
605
816
  close(): Promise<void>;
606
817
  };
607
818
 
819
+ /**
820
+ * Open decision tables on first use so a journal without decisions stays empty.
821
+ *
822
+ * @param sql - Journal SQL client
823
+ */
824
+ function loadDecisionLabelModule(): typeof import("../kernel/decision-label-store.ts") {
825
+ return lazyRequire(`${import.meta.dir}/../kernel`, ["decision", "label", "store"].join("-"));
826
+ }
827
+
828
+ function loadAgentEventModule(): typeof import("../kernel/agent-event-store.ts") {
829
+ return lazyRequire(`${import.meta.dir}/../kernel`, ["agent", "event", "store"].join("-"));
830
+ }
831
+
832
+ function lazyDecisionLabels(sql: PostgresJournalSql): DecisionLabelStore {
833
+ let pending: Promise<DecisionLabelStore> | undefined;
834
+ const ready = (): Promise<DecisionLabelStore> => {
835
+ pending ??= loadDecisionLabelModule().createPostgresDecisionLabelStore(sql);
836
+ return pending;
837
+ };
838
+ return {
839
+ insert: (label, at) => ready().then((store) => store.insert(label, at)),
840
+ list: (decision, tenant) => ready().then((store) => store.list(decision, tenant)),
841
+ drift: () => ready().then((store) => store.drift()),
842
+ setDrift: (decision, record) => ready().then((store) => store.setDrift(decision, record)),
843
+ putCandidate: (decision, entry) => ready().then((store) => store.putCandidate(decision, entry)),
844
+ getCandidate: (decision) => ready().then((store) => store.getCandidate(decision)),
845
+ listCandidates: () => ready().then((store) => store.listCandidates()),
846
+ };
847
+ }
848
+
849
+ /**
850
+ * Open the agent event tables on first use.
851
+ *
852
+ * @param sql - Journal SQL client
853
+ */
854
+ function lazyAgentEvents(sql: PostgresJournalSql): AgentEventStore {
855
+ let pending: Promise<AgentEventStore> | undefined;
856
+ const ready = (): Promise<AgentEventStore> => {
857
+ pending ??= loadAgentEventModule().createPostgresAgentEventStore(sql);
858
+ return pending;
859
+ };
860
+ return {
861
+ read: (runId) => ready().then((store) => store.read(runId)),
862
+ readAfter: (runId, afterSeq) => ready().then((store) => store.readAfter(runId, afterSeq)),
863
+ maxSeq: (runId) => ready().then((store) => store.maxSeq(runId)),
864
+ readHeader: (runId) => ready().then((store) => store.readHeader(runId)),
865
+ truncated: (runId) => ready().then((store) => store.truncated(runId)),
866
+ claim: (runId) => ready().then((store) => store.claim(runId)),
867
+ listHeaders: () => ready().then((store) => store.listHeaders()),
868
+ writeHeader: (header) => ready().then((store) => store.writeHeader(header)),
869
+ append: (runId, row) => ready().then((store) => store.append(runId, row)),
870
+ remove: (runId) => ready().then((store) => store.remove(runId)),
871
+ };
872
+ }
873
+
608
874
  /**
609
875
  * Open a postgres-backed JournalStore (multi-host durable-run coordination).
610
876
  *
@@ -625,6 +891,8 @@ export async function createPostgresJournalStore(
625
891
  const store: PostgresJournalStore = {
626
892
  sql,
627
893
  idempotency: createPostgresIdempotencyStore(sql),
894
+ decisions: lazyDecisionLabels(sql),
895
+ agentEvents: lazyAgentEvents(sql),
628
896
  async get(runId) {
629
897
  const rows = await sql.query(`SELECT * FROM oke_journal_runs WHERE id = ?`, [runId]);
630
898
  if (!rows[0]) return undefined;
@@ -645,6 +913,25 @@ export async function createPostgresJournalStore(
645
913
  return true;
646
914
  });
647
915
  },
916
+ async cas(runId, instanceId, now, leaseMs, update) {
917
+ return sql.begin(async (tx) => {
918
+ const claimed = await tx.query(CLAIM_LEASE_SQL, [runId, instanceId, now]);
919
+ const row = claimed[0];
920
+ if (!row) {
921
+ const exists = await tx.query(`SELECT * FROM oke_journal_runs WHERE id = ?`, [runId]);
922
+ const held = exists[0];
923
+ if (!held) return "missing";
924
+ const current = rowToRun(held);
925
+ return { lease: true, leaseExpiresAt: current.leaseExpiresAt };
926
+ }
927
+ const current = rowToRun(row);
928
+ const next = update(current) ?? current;
929
+ next.lockedBy = instanceId;
930
+ next.leaseExpiresAt = now + leaseMs;
931
+ await tx.exec(UPSERT_SQL, runToParams(next));
932
+ return "ok";
933
+ });
934
+ },
648
935
  async releaseLease(runId, instanceId) {
649
936
  await sql.exec(RELEASE_LEASE_SQL, [runId, instanceId]);
650
937
  },
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Memory SQL accepts the DDL identity boot issues on the shared connection.
3
+ */
4
+
5
+ import { describe, expect, test } from "bun:test";
6
+ import { ensureIdentityTables } from "../auth/identity-sql.ts";
7
+ import { memorySqlDriver } from "./memory.ts";
8
+
9
+ describe("memory sql DDL", () => {
10
+ test("CREATE INDEX IF NOT EXISTS does not reject identity tables", async () => {
11
+ const conn = await memorySqlDriver.connect({ role: "primary" });
12
+ await ensureIdentityTables(conn);
13
+ await ensureIdentityTables(conn);
14
+ const rows = await conn.query("SELECT id FROM oke_credentials");
15
+ expect(rows).toEqual([]);
16
+ await conn.close();
17
+ });
18
+ });
@@ -503,7 +503,7 @@ function createMemorySqlConnection(role: "primary" | "replica"): SqlConnection {
503
503
  }
504
504
 
505
505
  const create =
506
- /^CREATE\s+TABLE\s+IF\s+NOT\s+EXISTS\s+("?[a-zA-Z_][a-zA-Z0-9_]*"?)\s*\((.+)\)\s*$/i.exec(
506
+ /^CREATE\s+TABLE\s+IF\s+NOT\s+EXISTS\s+("?[a-zA-Z_][a-zA-Z0-9_]*"?)\s*\((.+)\)\s*$/is.exec(
507
507
  text,
508
508
  );
509
509
  if (create) {
@@ -519,6 +519,26 @@ function createMemorySqlConnection(role: "primary" | "replica"): SqlConnection {
519
519
  return { changes: 0 };
520
520
  }
521
521
 
522
+ const addColumn =
523
+ /^ALTER\s+TABLE\s+("?[a-zA-Z_][a-zA-Z0-9_]*"?)\s+ADD\s+COLUMN\s+IF\s+NOT\s+EXISTS\s+("?[a-zA-Z_][a-zA-Z0-9_]*"?)\b/is.exec(
524
+ text,
525
+ );
526
+ if (addColumn) {
527
+ const name = parseIdent(addColumn[1]!);
528
+ const column = parseIdent(addColumn[2]!);
529
+ const table = tables.get(name);
530
+ if (table && !table.columns.includes(column)) table.columns.push(column);
531
+ return { changes: 0 };
532
+ }
533
+
534
+ // Scan results do not depend on indexes. Identity and vault DDL still
535
+ // issue CREATE INDEX IF NOT EXISTS on the shared memory connection.
536
+ const createIndex =
537
+ /^CREATE\s+(?:UNIQUE\s+)?INDEX\s+(?:CONCURRENTLY\s+)?(?:IF\s+NOT\s+EXISTS\s+)?(?:"?[a-zA-Z_][a-zA-Z0-9_]*"?)\s+ON\s+/is.exec(
538
+ text,
539
+ );
540
+ if (createIndex) return { changes: 0 };
541
+
522
542
  const insert =
523
543
  /^INSERT\s+INTO\s+("?[a-zA-Z_][a-zA-Z0-9_]*"?)\s*\(([^)]+)\)\s*VALUES\s*\(([^)]+)\)\s*$/i.exec(
524
544
  text,
@@ -5,6 +5,7 @@ import {
5
5
  buildRlsIdentityPreludeSql,
6
6
  emitPgPolicySource,
7
7
  formatPolicyRole,
8
+ OKE_PG_STAT_STATEMENTS_SQL,
8
9
  OKE_RLS_HELPER_STATEMENTS,
9
10
  OKE_RLS_ROLE,
10
11
  parseSqlPolicySpec,
@@ -149,6 +150,36 @@ describe("OKE_RLS_HELPER_STATEMENTS", () => {
149
150
  expect(hasScope).not.toMatch(/jsonb\s+\?/);
150
151
  });
151
152
 
153
+ test("initial oke SQL creates pg_stat_statements before helpers", async () => {
154
+ const ran: string[] = [];
155
+ const { installOkeRlsHelpers } = await import("./pg-rls.ts");
156
+ await installOkeRlsHelpers(async (sql) => {
157
+ ran.push(sql);
158
+ if (sql.startsWith("CREATE EXTENSION")) return;
159
+ if (sql.includes("pg_vault") || sql.includes("vault.")) {
160
+ throw new Error("stop before vault attach");
161
+ }
162
+ }).catch(() => undefined);
163
+ expect(ran[0]).toBe(OKE_PG_STAT_STATEMENTS_SQL);
164
+ expect(ran[1]).toBe(OKE_RLS_HELPER_STATEMENTS[0]);
165
+ });
166
+
167
+ test("a rejected pg_stat_statements create still installs helpers", async () => {
168
+ const ran: string[] = [];
169
+ const { installOkeRlsHelpers } = await import("./pg-rls.ts");
170
+ await installOkeRlsHelpers(async (sql) => {
171
+ ran.push(sql);
172
+ if (sql.startsWith("CREATE EXTENSION")) {
173
+ throw new Error('extension "pg_stat_statements" is not available');
174
+ }
175
+ if (sql.includes("CREATE OR REPLACE FUNCTION oke.gate")) {
176
+ throw new Error("stop");
177
+ }
178
+ }).catch(() => undefined);
179
+ expect(ran[0]).toBe(OKE_PG_STAT_STATEMENTS_SQL);
180
+ expect(ran.some((sql) => sql.includes("CREATE SCHEMA IF NOT EXISTS oke"))).toBe(true);
181
+ });
182
+
152
183
  test("pins search_path to public so schema oke is not the table home", () => {
153
184
  expect(OKE_RLS_HELPER_STATEMENTS).toContain("SET search_path TO public, oke");
154
185
  expect(
@@ -135,12 +135,27 @@ $oke_app_search_path$`,
135
135
  */
136
136
  export const OKE_RLS_HELPER_SQL = `${OKE_RLS_HELPER_STATEMENTS.join(";\n")};`;
137
137
 
138
+ /**
139
+ * Enable query telemetry on postgres. Preload is still required
140
+ * (`shared_preload_libraries=pg_stat_statements`). PGlite and Cockroach
141
+ * reject this; callers swallow that failure so the rest of the oke SQL runs.
142
+ */
143
+ export const OKE_PG_STAT_STATEMENTS_SQL = "CREATE EXTENSION IF NOT EXISTS pg_stat_statements";
144
+
138
145
  /**
139
146
  * Install `oke.*` helpers one statement at a time.
140
147
  *
148
+ * `pg_stat_statements` is created first when the engine allows it, so Store
149
+ * performance does not depend on a later manual `CREATE EXTENSION`.
150
+ *
141
151
  * @param exec - Statement runner
142
152
  */
143
153
  export async function installOkeRlsHelpers(exec: (sql: string) => Promise<unknown>): Promise<void> {
154
+ try {
155
+ await exec(OKE_PG_STAT_STATEMENTS_SQL);
156
+ } catch {
157
+ // Not preloaded, or this engine has no pg_stat_statements (pglite, cockroach).
158
+ }
144
159
  for (const stmt of OKE_RLS_HELPER_STATEMENTS) {
145
160
  await exec(stmt);
146
161
  }
@@ -38,6 +38,15 @@ describe("toPostgresParams", () => {
38
38
  });
39
39
 
40
40
  describe("closeSharedPostgresClients", () => {
41
+ test("a different max is a different pool on the same URL", () => {
42
+ const url = "postgres://127.0.0.1:9/oke-pool-max-shared-test";
43
+ const small = sharedPostgresClient(url, 8);
44
+ const large = sharedPostgresClient(url, 20);
45
+ expect(large).not.toBe(small);
46
+ expect(sharedPostgresClient(url, 20)).toBe(large);
47
+ expect(sharedPostgresClient(url)).toBe(small);
48
+ });
49
+
41
50
  test("drops cached pools so the next checkout is a new client", async () => {
42
51
  const url = "postgres://127.0.0.1:9/oke-close-shared-test";
43
52
  const first = sharedPostgresClient(url);
@@ -43,7 +43,27 @@ type SharedPostgresEntry = {
43
43
  readonly guarded: PostgresClientLike;
44
44
  };
45
45
 
46
- const sharedClients = new Map<string, SharedPostgresEntry>();
46
+ /**
47
+ * Process-wide pool registry.
48
+ *
49
+ * `bun --hot` re-evaluates this module on each save. A module-local `Map`
50
+ * would drop the only handle to the previous Bun.SQL pools and leave them
51
+ * connected. PgDog then queues every checkout — Console login included —
52
+ * until its server pool times out.
53
+ */
54
+ const SHARED_POSTGRES_CLIENTS = Symbol.for("oke.sharedPostgresClients");
55
+
56
+ function sharedClientMap(): Map<string, SharedPostgresEntry> {
57
+ const g = globalThis as typeof globalThis & {
58
+ [SHARED_POSTGRES_CLIENTS]?: Map<string, SharedPostgresEntry>;
59
+ };
60
+ let map = g[SHARED_POSTGRES_CLIENTS];
61
+ if (!map) {
62
+ map = new Map();
63
+ g[SHARED_POSTGRES_CLIENTS] = map;
64
+ }
65
+ return map;
66
+ }
47
67
 
48
68
  /**
49
69
  * When true, shared pools stay closed and every query fails fast — no Bun.SQL
@@ -160,21 +180,38 @@ function guardSharedPostgresClient(raw: PostgresClientLike): PostgresClientLike
160
180
  }
161
181
 
162
182
  /**
163
- * One Bun.SQL pool per URL for store / journal / clock / vault / console.
183
+ * Bun.SQL pool size. Unset, non-finite, or below 1 keeps {@link POSTGRES_POOL_MAX}.
184
+ * A configured max is not capped — callers that need a larger pool ask for it.
185
+ *
186
+ * @param poolMax - Requested `max`
187
+ */
188
+ function postgresPoolMax(poolMax: number | undefined): number {
189
+ if (poolMax === undefined || !Number.isFinite(poolMax) || poolMax < 1) return POSTGRES_POOL_MAX;
190
+ return Math.floor(poolMax);
191
+ }
192
+
193
+ /**
194
+ * One Bun.SQL pool per URL and `max` for store / journal / clock / vault / console.
164
195
  *
165
196
  * Returns a pause-aware client — holders (including fleet store wrappers)
166
197
  * fail soft after {@link pauseSharedPostgresClients} instead of reconnecting.
167
198
  *
168
199
  * @param url - Connection URL
200
+ * @param poolMax - Bun.SQL `max`. Omitted uses {@link POSTGRES_POOL_MAX}. A
201
+ * different max is a different pool — a store pin of 20 must not reuse a
202
+ * pool another subsystem already opened at the default.
169
203
  */
170
- export function sharedPostgresClient(url?: string): PostgresClientLike {
204
+ export function sharedPostgresClient(url?: string, poolMax?: number): PostgresClientLike {
171
205
  assertSharedPostgresReady();
172
- const key = resolvePostgresUrl(url);
173
- const existing = sharedClients.get(key);
206
+ const resolved = resolvePostgresUrl(url);
207
+ const max = postgresPoolMax(poolMax);
208
+ const key = `${resolved}\0${String(max)}`;
209
+ const clients = sharedClientMap();
210
+ const existing = clients.get(key);
174
211
  if (existing) return existing.guarded;
175
- const raw = new Bun.SQL(key, { max: POSTGRES_POOL_MAX }) as unknown as PostgresClientLike;
212
+ const raw = new Bun.SQL(resolved, { max }) as unknown as PostgresClientLike;
176
213
  const guarded = guardSharedPostgresClient(raw);
177
- sharedClients.set(key, { raw, guarded });
214
+ clients.set(key, { raw, guarded });
178
215
  return guarded;
179
216
  }
180
217
 
@@ -185,8 +222,8 @@ export function sharedPostgresClient(url?: string): PostgresClientLike {
185
222
  * not stop Console from opening a fresh pool on the next poll.
186
223
  */
187
224
  export async function closeSharedPostgresClients(): Promise<void> {
188
- const clients = [...sharedClients.values()];
189
- sharedClients.clear();
225
+ const clients = [...sharedClientMap().values()];
226
+ sharedClientMap().clear();
190
227
  await Promise.all(
191
228
  clients.map(async (entry) => {
192
229
  try {
@@ -283,12 +320,13 @@ export async function connectPostgres(options: SqlConnectOptions = {}): Promise<
283
320
  // must not share a pooled client — Bun raises ERR_POSTGRES_UNSAFE_TRANSACTION
284
321
  // when raw statements interleave with open transaction state on the shared
285
322
  // pool. Opt in explicitly; default behavior is unchanged.
286
- const dedicated = !injected && options.pool?.max === 1;
323
+ const requestedMax = options.pool?.max;
324
+ const dedicated = !injected && requestedMax === 1;
287
325
  const client =
288
326
  injected ??
289
327
  (dedicated
290
328
  ? dedicatedPostgresClient(resolvePostgresUrl(options.url))
291
- : sharedPostgresClient(options.url));
329
+ : sharedPostgresClient(options.url, requestedMax));
292
330
  return wrapPostgresClient(client, role, {
293
331
  shared: injected === undefined && !dedicated,
294
332
  ...(options.url !== undefined ? { url: options.url } : {}),
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Installed agent event log.
3
+ *
4
+ * The follow route and the AI runtime must share one slot. The log
5
+ * implementation stays in `run-events.ts` and loads on first use; this file
6
+ * is the only static edge so a `lazyRequire` copy cannot hide the log.
7
+ */
8
+
9
+ import type { AgentEventLog } from "./run-events.ts";
10
+
11
+ let activeLog: AgentEventLog | undefined;
12
+
13
+ /**
14
+ * Install the log the follow route reads. Boot and tests call this.
15
+ *
16
+ * @param log - Active log
17
+ */
18
+ export function setAgentEventLog(log: AgentEventLog | undefined): void {
19
+ activeLog = log;
20
+ }
21
+
22
+ /**
23
+ * Log installed by the running AI runtime.
24
+ */
25
+ export function getAgentEventLog(): AgentEventLog | undefined {
26
+ return activeLog;
27
+ }