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.
- package/AGENTS.md +7 -0
- package/manifest.v1.schema.json +44 -0
- package/package.json +32 -19
- package/site/content/docs/ai/mcp.mdx +13 -9
- package/site/content/docs/ai/skills.mdx +1 -1
- package/site/content/docs/elements/ai/agents.mdx +61 -13
- package/site/content/docs/elements/ai/decide.mdx +536 -0
- package/site/content/docs/elements/ai/events.mdx +373 -0
- package/site/content/docs/elements/ai/index.mdx +9 -3
- package/site/content/docs/elements/ai/meta.json +1 -1
- package/site/content/docs/elements/ai/prompts.mdx +4 -2
- package/site/content/docs/elements/gate/auth.mdx +4 -1
- package/site/content/docs/elements/store/index.mdx +1 -1
- package/site/content/docs/elements/store/kv.mdx +5 -5
- package/site/content/docs/recipes/dragonfly.mdx +5 -5
- package/site/content/docs/recipes/index.mdx +6 -6
- package/site/content/docs/recipes/postgres.mdx +12 -8
- package/site/content/docs/recipes/redis.mdx +4 -4
- package/site/content/docs/reference/cli.mdx +2 -1
- package/site/content/docs/reference/configuration.mdx +9 -7
- package/site/content/docs/reference/fx.mdx +7 -6
- package/src/auth/api-key-sql.test.ts +44 -0
- package/src/auth/api-key-sql.ts +1 -0
- package/src/auth/api-keys.ts +30 -12
- package/src/auth/config.ts +6 -2
- package/src/auth/gate-auth.test.ts +20 -0
- package/src/cli/agents-md.test.ts +25 -0
- package/src/cli/ask-vault-gaps.test.ts +38 -1
- package/src/cli/ask-vault-gaps.ts +35 -2
- package/src/cli/decide.ts +174 -0
- package/src/cli/decision-lock-watch.test.ts +45 -0
- package/src/cli/decision-lock-watch.ts +51 -0
- package/src/cli/dev-app-runner.ts +34 -8
- package/src/cli/dev-auth-secret.test.ts +49 -0
- package/src/cli/dev-auth-secret.ts +49 -0
- package/src/cli/dev-hot.test.ts +41 -0
- package/src/cli/dev-hot.ts +48 -0
- package/src/cli/dev.test.ts +1 -1
- package/src/cli/dev.ts +24 -6
- package/src/cli/eval.ts +142 -1
- package/src/cli/index.ts +5 -0
- package/src/cli/load-config.images.test.ts +2 -2
- package/src/cli/load-config.ts +1 -1
- package/src/cli/mcp-from-console.ts +48 -0
- package/src/cli/registry.ts +35 -1
- package/src/client/agent.ts +307 -0
- package/src/client/transport.test.ts +20 -0
- package/src/client/transport.ts +2 -1
- package/src/client/types.ts +1 -1
- package/src/client-react/index.ts +7 -1
- package/src/client-react/use-agent-run.test.ts +271 -0
- package/src/client-react/use-agent-run.ts +193 -0
- package/src/compiler/ai-approval.test.ts +36 -0
- package/src/compiler/ai-repair.test.ts +22 -0
- package/src/compiler/decisions.extract.test.ts +164 -0
- package/src/compiler/effects-infer.ts +27 -1
- package/src/compiler/extract.ts +268 -12
- package/src/compiler/response.ts +35 -2
- package/src/config/index.ts +21 -3
- package/src/console/server/ai-runs-flows.ts +269 -0
- package/src/console/server/ai-runs.test.ts +323 -0
- package/src/console/server/ai-runs.ts +562 -0
- package/src/console/server/ai.ts +12 -1
- package/src/console/server/app.ts +10 -1
- package/src/console/server/decisions-flows.ts +157 -0
- package/src/console/server/decisions.test.ts +66 -0
- package/src/console/server/decisions.ts +229 -0
- package/src/console/server/flows.ts +50 -2
- package/src/console/server/invoke-user-flow.ts +9 -0
- package/src/console/server/runs-ingest.ts +16 -0
- package/src/console/server/serve.ts +5 -0
- package/src/console/server/state.ts +25 -1
- package/src/console/server/store-stats.test.ts +47 -0
- package/src/console/server/store-stats.ts +92 -18
- package/src/console/ui-next/dist/assets/access-page-DDKkhT9v.js +4 -0
- package/src/console/ui-next/dist/assets/{agent-disclosure-Bkohc_8X.js → agent-disclosure-62Xts8Xi.js} +1 -1
- package/src/console/ui-next/dist/assets/{cache-glyph-rL0lHxat.js → cache-glyph-DyeoKHKA.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-Deo4PP18.js → call-pii-button-DvfpLXsU.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-CoJ6amHf.js → collapsible-BY03SeCg.js} +1 -1
- package/src/console/ui-next/dist/assets/confirm-sheet-DZfuJXP4.js +1 -0
- package/src/console/ui-next/dist/assets/copy-inline-button-BTjPVjRT.js +1 -0
- package/src/console/ui-next/dist/assets/decisions-page-CgkKlA56.js +1 -0
- package/src/console/ui-next/dist/assets/detail-header-Dk5LipF6.js +1 -0
- package/src/console/ui-next/dist/assets/dropdown-menu-CRGrj5JU.js +1 -0
- package/src/console/ui-next/dist/assets/duration-tone-Y6HLjyJ5.js +9 -0
- package/src/console/ui-next/dist/assets/element-icons-C6t8Vlml.js +1 -0
- package/src/console/ui-next/dist/assets/explorer-chrome-Dslh2yE8.js +1 -0
- package/src/console/ui-next/dist/assets/explorer-empty-CIVg7Jgb.js +1 -0
- package/src/console/ui-next/dist/assets/flows-page-DLPfNy-_.js +1 -0
- package/src/console/ui-next/dist/assets/{highlighted-json-CPYBzDh0.js → highlighted-json-D7nNzpgB.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-DTPevKmM.js → http-method-DdL19zzh.js} +1 -1
- package/src/console/ui-next/dist/assets/index-BlrN49Hs.css +2 -0
- package/src/console/ui-next/dist/assets/index-C0lc_s9d.js +58 -0
- package/src/console/ui-next/dist/assets/observability-page-CRlcyLCC.js +8 -0
- package/src/console/ui-next/dist/assets/{replica-lag-BM27lXld.js → replica-lag-Bb3FWUu9.js} +7 -7
- package/src/console/ui-next/dist/assets/request-meta-DYBMWhX8.js +1 -0
- package/src/console/ui-next/dist/assets/shortcut-keys-DTE1sjTC.js +1 -0
- package/src/console/ui-next/dist/assets/store-page-CcE-SXC8.js +41 -0
- package/src/console/ui-next/dist/assets/trace-detail-sheet-CKMSEQ4s.js +2 -0
- package/src/console/ui-next/dist/assets/tree-expand-toggle-QOfk0M0H.js +55 -0
- package/src/console/ui-next/dist/assets/units-page-DKC1CDDd.js +1 -0
- package/src/console/ui-next/dist/assets/use-vault-list-BAyQG8Re.js +1 -0
- package/src/console/ui-next/dist/assets/vault-page-B54v4Yit.js +2 -0
- package/src/console/ui-next/dist/index.html +4 -3
- package/src/console/ui-next/seed-parked-approval.ts +126 -0
- package/src/console/ui-next/src/client.ts +224 -1
- package/src/console/ui-next/src/components/explorer/detail-header.tsx +9 -0
- package/src/console/ui-next/src/components/explorer/explorer-start-toggle.tsx +5 -1
- package/src/console/ui-next/src/components/ui/sheet-form.tsx +21 -12
- package/src/console/ui-next/src/features/flows/decisions/decisions-page.tsx +216 -0
- package/src/console/ui-next/src/features/flows/decisions/resolve.test.ts +48 -0
- package/src/console/ui-next/src/features/flows/decisions/resolve.ts +111 -0
- package/src/console/ui-next/src/features/flows/flows-page.tsx +10 -0
- package/src/console/ui-next/src/features/flows/graph/element-map.ts +1 -0
- package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +6 -0
- package/src/console/ui-next/src/features/flows/traces/effect-summary.test.ts +24 -0
- package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
- package/src/console/ui-next/src/features/flows/traces/http-status.ts +58 -0
- package/src/console/ui-next/src/features/flows/traces/request-client-card.tsx +355 -0
- package/src/console/ui-next/src/features/flows/traces/request-client.test.ts +114 -0
- package/src/console/ui-next/src/features/flows/traces/request-client.ts +423 -0
- package/src/console/ui-next/src/features/flows/traces/request-input-view.test.ts +52 -6
- package/src/console/ui-next/src/features/flows/traces/request-input-view.ts +85 -19
- package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +505 -80
- package/src/console/ui-next/src/features/flows/traces/trace-lanes.test.ts +61 -0
- package/src/console/ui-next/src/features/flows/traces/trace-lanes.ts +78 -0
- package/src/console/ui-next/src/features/flows/traces/trace-nav.test.ts +48 -0
- package/src/console/ui-next/src/features/flows/traces/trace-nav.ts +57 -0
- package/src/console/ui-next/src/features/flows/traces/trace-request-section.tsx +275 -96
- package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +16 -2
- package/src/console/ui-next/src/features/observability/detail/ai-runs.tsx +369 -0
- package/src/console/ui-next/src/features/observability/observability-page.tsx +31 -1
- package/src/console/ui-next/src/features/observability/state/observability-selection.ts +18 -3
- package/src/console/ui-next/src/features/store/performance/kv-performance-panel.tsx +12 -1
- package/src/console/ui-next/src/features/store/performance/performance-panel.tsx +14 -1
- package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +1 -1
- package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +6 -2
- package/src/console/ui-next/src/features/units/explorer/units-tree.tsx +73 -18
- package/src/console/ui-next/src/features/units/lib/unit-tree.test.ts +21 -0
- package/src/console/ui-next/src/features/units/lib/unit-tree.ts +82 -0
- package/src/console/ui-next/src/features/units/units-page.tsx +25 -7
- package/src/console/ui-next/src/router.tsx +17 -0
- package/src/console/ui-next/ui-next-seed-runs.ts +23 -0
- package/src/console/ui-next/vite-console-kernel-plugin.ts +43 -3
- package/src/console/ui-next/vite.config.ts +9 -0
- package/src/docker/recipes/dragonfly.ts +3 -2
- package/src/docker/recipes/redis.ts +1 -1
- package/src/drivers/ai-anthropic.ts +212 -21
- package/src/drivers/ai-mock.ts +34 -1
- package/src/drivers/ai-openai-compatible.ts +67 -26
- package/src/drivers/ai-preconnect.ts +18 -0
- package/src/drivers/ai-stream.test.ts +62 -0
- package/src/drivers/ai-types.ts +20 -0
- package/src/drivers/journal-postgres.ts +289 -2
- package/src/drivers/memory-ddl.test.ts +18 -0
- package/src/drivers/memory.ts +21 -1
- package/src/drivers/pg-rls.test.ts +31 -0
- package/src/drivers/pg-rls.ts +15 -0
- package/src/drivers/postgres.test.ts +9 -0
- package/src/drivers/postgres.ts +49 -11
- package/src/elements/ai/agent-event-slot.ts +27 -0
- package/src/elements/ai/approval-http.ts +207 -0
- package/src/elements/ai/approval.test.ts +741 -0
- package/src/elements/ai/approval.ts +228 -0
- package/src/elements/ai/decisions/bind.ts +163 -0
- package/src/elements/ai/decisions/certificate.test.ts +291 -0
- package/src/elements/ai/decisions/certificate.ts +377 -0
- package/src/elements/ai/decisions/certify.ts +314 -0
- package/src/elements/ai/decisions/decide.test.ts +953 -0
- package/src/elements/ai/decisions/e2e.test.ts +399 -0
- package/src/elements/ai/decisions/export.test.ts +120 -0
- package/src/elements/ai/decisions/export.ts +146 -0
- package/src/elements/ai/decisions/fixtures/openrouter-request.json +24 -0
- package/src/elements/ai/decisions/fixtures/openrouter-response.json +22 -0
- package/src/elements/ai/decisions/fixtures/typesafe-request.json +19 -0
- package/src/elements/ai/decisions/fixtures/typesafe-response.json +13 -0
- package/src/elements/ai/decisions/http.test.ts +154 -0
- package/src/elements/ai/decisions/http.ts +240 -0
- package/src/elements/ai/decisions/labels.test.ts +282 -0
- package/src/elements/ai/decisions/labels.ts +257 -0
- package/src/elements/ai/decisions/openrouter.live.test.ts +136 -0
- package/src/elements/ai/decisions/openrouter.ts +36 -0
- package/src/elements/ai/decisions/provider.ts +102 -0
- package/src/elements/ai/decisions/typesafe.ts +36 -0
- package/src/elements/ai/declare.ts +252 -1
- package/src/elements/ai/events.test.ts +379 -0
- package/src/elements/ai/events.ts +138 -0
- package/src/elements/ai/run-events.test.ts +461 -0
- package/src/elements/ai/run-events.ts +522 -0
- package/src/elements/ai/runtime.ts +1186 -105
- package/src/elements/ai/stream-turn.ts +214 -0
- package/src/elements/ai/stream.live.test.ts +76 -0
- package/src/elements/ai/subagent.test.ts +136 -0
- package/src/elements/ai.test.ts +497 -3
- package/src/elements/ai.ts +16 -1
- package/src/elements/clock/durable.ts +27 -1
- package/src/elements/store/sql-session.test.ts +51 -0
- package/src/elements/store/sql-session.ts +111 -1
- package/src/i18n/catalogs/ar.ts +5 -0
- package/src/i18n/catalogs/en.ts +5 -0
- package/src/index.ts +2 -0
- package/src/kernel/agent-event-store.ts +448 -0
- package/src/kernel/app.ts +103 -1
- package/src/kernel/boot-bind/ai.ts +3 -0
- package/src/kernel/boot-bind/store.test.ts +26 -0
- package/src/kernel/boot-bind/store.ts +82 -6
- package/src/kernel/boot.ts +15 -4
- package/src/kernel/builtin-errors.ts +2 -0
- package/src/kernel/capability.ts +3 -0
- package/src/kernel/decision-label-store.ts +441 -0
- package/src/kernel/effects.test.ts +2 -1
- package/src/kernel/effects.ts +13 -1
- package/src/kernel/element-registries.ts +3 -0
- package/src/kernel/errors-text.ts +4 -0
- package/src/kernel/errors.ts +2 -0
- package/src/kernel/flow.test.ts +2 -3
- package/src/kernel/fx-decide.ts +761 -0
- package/src/kernel/fx.test.ts +12 -5
- package/src/kernel/fx.ts +312 -29
- package/src/kernel/http-frame.test.ts +94 -0
- package/src/kernel/http-frame.ts +203 -0
- package/src/kernel/journal.ts +104 -0
- package/src/kernel/json-result.ts +10 -0
- package/src/kernel/pipeline.test.ts +45 -0
- package/src/kernel/sse-id.ts +32 -0
- package/src/manifest/diff.ts +1 -0
- package/src/manifest/types.ts +29 -2
- package/src/mcp/ai-tools.test.ts +202 -0
- package/src/mcp/authorization.ts +66 -0
- package/src/mcp/session.ts +3 -0
- package/src/mcp/tools.ts +168 -0
- package/src/release/http-graph.test.ts +2 -0
- package/src/release/http-graph.ts +5 -0
- package/src/release/measure.ts +39 -14
- package/src/runs/collect.ts +4 -1
- package/src/runs/parquet.test.ts +27 -1
- package/src/runs/parquet.ts +19 -0
- package/src/runs/types.ts +31 -0
- package/src/runtime/json-code-block.test.ts +106 -47
- package/src/runtime/json-code-block.ts +1209 -46
- package/src/term.test.ts +13 -0
- package/src/term.ts +81 -49
- package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +0 -1
- package/src/console/ui-next/dist/assets/access-page-Ze6ckjdd.js +0 -4
- package/src/console/ui-next/dist/assets/copy-inline-button-CWG5_ZOh.js +0 -1
- package/src/console/ui-next/dist/assets/detail-header-b_ZzIsmU.js +0 -1
- package/src/console/ui-next/dist/assets/dropdown-menu-nwXEO1Ac.js +0 -1
- package/src/console/ui-next/dist/assets/duration-tone-BbQ_8z50.js +0 -9
- package/src/console/ui-next/dist/assets/element-icons-CwyLpVXz.js +0 -1
- package/src/console/ui-next/dist/assets/explorer-empty-C8sSCqYT.js +0 -1
- package/src/console/ui-next/dist/assets/flows-page-D2E7BtKK.js +0 -1
- package/src/console/ui-next/dist/assets/index-VxoEz295.css +0 -2
- package/src/console/ui-next/dist/assets/index-vTuwmeQz.js +0 -58
- package/src/console/ui-next/dist/assets/observability-page-SJTSzHFJ.js +0 -4
- package/src/console/ui-next/dist/assets/request-meta-BjFT7DNl.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-keys-CUNegEV4.js +0 -1
- package/src/console/ui-next/dist/assets/store-page-Qj9ThRE-.js +0 -41
- package/src/console/ui-next/dist/assets/trace-detail-sheet-Cmitq3da.js +0 -2
- package/src/console/ui-next/dist/assets/tree-expand-toggle-CbDIB-7x.js +0 -55
- package/src/console/ui-next/dist/assets/units-page-_PuYqFty.js +0 -1
- package/src/console/ui-next/dist/assets/use-vault-list-Cs45Czkt.js +0 -1
- 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
|
|
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 = {
|
|
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
|
+
});
|
package/src/drivers/memory.ts
CHANGED
|
@@ -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*$/
|
|
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(
|
package/src/drivers/pg-rls.ts
CHANGED
|
@@ -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);
|
package/src/drivers/postgres.ts
CHANGED
|
@@ -43,7 +43,27 @@ type SharedPostgresEntry = {
|
|
|
43
43
|
readonly guarded: PostgresClientLike;
|
|
44
44
|
};
|
|
45
45
|
|
|
46
|
-
|
|
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
|
-
*
|
|
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
|
|
173
|
-
const
|
|
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(
|
|
212
|
+
const raw = new Bun.SQL(resolved, { max }) as unknown as PostgresClientLike;
|
|
176
213
|
const guarded = guardSharedPostgresClient(raw);
|
|
177
|
-
|
|
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 = [...
|
|
189
|
-
|
|
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
|
|
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
|
+
}
|