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.
- package/AGENTS.md +7 -0
- package/manifest.v1.schema.json +44 -0
- package/package.json +32 -19
- package/site/content/docs/ai/mcp.mdx +14 -10
- 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/channel/index.mdx +4 -4
- package/site/content/docs/elements/channel/receipts.mdx +7 -5
- package/site/content/docs/elements/channel/sms.mdx +2 -1
- package/site/content/docs/elements/flow/index.mdx +22 -18
- package/site/content/docs/elements/gate/auth.mdx +4 -1
- package/site/content/docs/elements/signal/index.mdx +15 -14
- package/site/content/docs/elements/store/index.mdx +1 -1
- package/site/content/docs/elements/store/kv.mdx +5 -5
- package/site/content/docs/plugins/otp.mdx +3 -3
- 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 +19 -10
- 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/auth/tenants.ts +17 -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/doctor-diff.test.ts +29 -1
- package/src/cli/doctor-diff.ts +35 -0
- 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/manifest-pr-diff.ts +16 -0
- 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-fetch.test.ts +77 -0
- package/src/compiler/effects-infer.ts +114 -17
- package/src/compiler/effects-join.test.ts +48 -0
- package/src/compiler/extract.test.ts +28 -0
- package/src/compiler/extract.ts +279 -13
- package/src/compiler/fx-index.ts +58 -4
- package/src/compiler/response.ts +35 -2
- package/src/config/index.ts +21 -3
- package/src/console/index.ts +1 -1
- 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-C_N4qvai.js +4 -0
- package/src/console/ui-next/dist/assets/{agent-disclosure-Bkohc_8X.js → agent-disclosure-Cq1TxniW.js} +1 -1
- package/src/console/ui-next/dist/assets/{cache-glyph-rL0lHxat.js → cache-glyph-BaI4uxZz.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-Deo4PP18.js → call-pii-button-DNyK7IIY.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-CoJ6amHf.js → collapsible-CRhBJMZR.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-D9twj8Xy.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-CsHWlVPU.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-ZaTJAhR9.js +1 -0
- package/src/console/ui-next/dist/assets/{highlighted-json-CPYBzDh0.js → highlighted-json-DQ-EMAfz.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-DTPevKmM.js → http-method-Cn_Fl79s.js} +1 -1
- package/src/console/ui-next/dist/assets/index-BlrN49Hs.css +2 -0
- package/src/console/ui-next/dist/assets/index-CmRFeWdT.js +58 -0
- package/src/console/ui-next/dist/assets/observability-page-Cn6R_dXz.js +8 -0
- package/src/console/ui-next/dist/assets/{replica-lag-BM27lXld.js → replica-lag-Bx6EZhSe.js} +7 -7
- package/src/console/ui-next/dist/assets/request-meta-DmdiUrfh.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-8P6vGXnn.js +41 -0
- package/src/console/ui-next/dist/assets/trace-detail-sheet-BfGvpFSv.js +2 -0
- package/src/console/ui-next/dist/assets/tree-expand-toggle-CWdUHbc0.js +55 -0
- package/src/console/ui-next/dist/assets/units-page-CZOFZzay.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-ki_ishmX.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.test.ts +22 -1
- package/src/drivers/journal-postgres.ts +511 -13
- 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/drivers/signal-compete.test.ts +234 -0
- package/src/drivers/signal-postgres.ts +39 -19
- package/src/drivers/signal-redis.ts +167 -11
- package/src/drivers/signal-types.ts +12 -0
- 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/mcp-protocol.ts +5 -6
- 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 +1187 -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/channel/runtime.ts +11 -0
- package/src/elements/channel/sql-ledger.ts +251 -0
- package/src/elements/clock/durable.ts +27 -1
- package/src/elements/signal/runtime.ts +9 -0
- package/src/elements/signal.test.ts +1 -0
- package/src/elements/store/cache.test.ts +120 -15
- package/src/elements/store/cache.ts +200 -39
- package/src/elements/store/prepare-row.test.ts +6 -2
- package/src/elements/store/sql-session.test.ts +53 -0
- package/src/elements/store/sql-session.ts +206 -10
- package/src/elements/store.ts +1 -0
- 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 +135 -17
- package/src/kernel/auto-cache.test.ts +228 -0
- package/src/kernel/boot-bind/ai.ts +3 -0
- package/src/kernel/boot-bind/channel.ts +63 -2
- package/src/kernel/boot-bind/honor-config.test.ts +40 -10
- package/src/kernel/boot-bind/signal.ts +61 -25
- package/src/kernel/boot-bind/store.test.ts +26 -0
- package/src/kernel/boot-bind/store.ts +82 -6
- package/src/kernel/boot.ts +31 -6
- 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-compiler.ts +11 -0
- package/src/kernel/errors-text.ts +20 -0
- package/src/kernel/errors.ts +10 -0
- package/src/kernel/flow.test.ts +2 -3
- package/src/kernel/flow.ts +33 -3
- package/src/kernel/fx-call-types.test.ts +32 -0
- package/src/kernel/fx-decide.ts +765 -0
- package/src/kernel/fx-sql-handle.ts +46 -7
- package/src/kernel/fx.test.ts +13 -8
- package/src/kernel/fx.ts +339 -45
- package/src/kernel/http-frame.test.ts +94 -0
- package/src/kernel/http-frame.ts +203 -0
- package/src/kernel/idempotency.test.ts +2 -1
- package/src/kernel/journal.test.ts +114 -0
- package/src/kernel/journal.ts +471 -51
- package/src/kernel/json-result.ts +10 -0
- package/src/kernel/pipeline-tenant.ts +5 -1
- package/src/kernel/pipeline.test.ts +45 -0
- package/src/kernel/pipeline.ts +5 -0
- package/src/kernel/signal-tx.ts +28 -0
- package/src/kernel/sse-id.ts +32 -0
- package/src/kernel/store-transaction.test.ts +95 -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/docs-server.ts +19 -2
- package/src/mcp/mcp.test.ts +83 -0
- package/src/mcp/protocol.ts +8 -2
- package/src/mcp/server.ts +23 -2
- package/src/mcp/session.ts +3 -0
- package/src/mcp/tools.ts +168 -0
- package/src/mcp/versions.ts +60 -0
- package/src/plugins/index.ts +1 -0
- package/src/plugins/mena.ts +56 -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
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Same concurrency physics as Signal's `once` delivery and Clock's postgres
|
|
5
5
|
* CronStore: claim with `FOR UPDATE SKIP LOCKED`; a crashed holder's lease is
|
|
6
|
-
* reclaimed lazily on the next claim attempt (no sweeper
|
|
7
|
-
*
|
|
6
|
+
* reclaimed lazily on the next claim attempt (no sweeper). Each acquire bumps
|
|
7
|
+
* a fencing token; writes from a previous holder are rejected. Entries are
|
|
8
|
+
* append-only rows, not a rewritten JSON blob.
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
import {
|
|
@@ -20,12 +21,17 @@ import {
|
|
|
20
21
|
IDEM_RENEW_SQL,
|
|
21
22
|
IDEM_SELECT_SQL,
|
|
22
23
|
} from "../kernel/idempotency-store.ts";
|
|
24
|
+
import type { DecisionLabelStore } from "../kernel/decision-label-store.ts";
|
|
25
|
+
import type { AgentEventStore } from "../kernel/agent-event-store.ts";
|
|
26
|
+
import { throwOke } from "../kernel/errors.ts";
|
|
27
|
+
import { lazyRequire } from "../kernel/lazy-require.ts";
|
|
23
28
|
import {
|
|
24
29
|
JOURNAL_DEFAULT_LEASE_MS,
|
|
25
30
|
type JournalEntry,
|
|
26
31
|
type JournalLeaseStore,
|
|
27
32
|
type JournalRun,
|
|
28
33
|
type JournalStore,
|
|
34
|
+
type JournalWriteFence,
|
|
29
35
|
} from "../kernel/journal.ts";
|
|
30
36
|
import {
|
|
31
37
|
resolvePostgresUrl,
|
|
@@ -50,6 +56,19 @@ interface JournalDbRow {
|
|
|
50
56
|
created_at: number;
|
|
51
57
|
updated_at: number;
|
|
52
58
|
tenant: string | null;
|
|
59
|
+
lease_token: number | null;
|
|
60
|
+
code_version: string | null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** One append-only journal entry row. */
|
|
64
|
+
interface JournalEntryDbRow {
|
|
65
|
+
run_id: string;
|
|
66
|
+
seq: number;
|
|
67
|
+
kind: string;
|
|
68
|
+
name: string | null;
|
|
69
|
+
resource: string | null;
|
|
70
|
+
value: string | null;
|
|
71
|
+
at: number;
|
|
53
72
|
}
|
|
54
73
|
|
|
55
74
|
/** Minimal SQL + transaction surface for the postgres journal store. */
|
|
@@ -82,7 +101,11 @@ const CLAIM_DUE_SQL = `SELECT * FROM oke_journal_runs WHERE status='sleeping' AN
|
|
|
82
101
|
/** Boot-time orphan discovery: running/sleeping runs with no live lease. */
|
|
83
102
|
const ORPHANS_SQL = `SELECT * FROM oke_journal_runs WHERE (status='running' OR status='sleeping' OR status='compensating') AND ((locked_by IS NULL) OR (lease_expires_at IS NOT NULL AND lease_expires_at<=?))`;
|
|
84
103
|
|
|
85
|
-
const UPDATE_LEASE_SQL = `UPDATE oke_journal_runs SET locked_by = ?, lease_expires_at =
|
|
104
|
+
const UPDATE_LEASE_SQL = `UPDATE oke_journal_runs SET locked_by = ?, lease_expires_at = ?, lease_token = COALESCE(lease_token, 0) + 1 WHERE id = ?`;
|
|
105
|
+
|
|
106
|
+
const INSERT_ENTRY_SQL = `INSERT INTO oke_journal_entries (run_id, seq, kind, name, resource, value, at) VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT (run_id, seq) DO UPDATE SET kind = EXCLUDED.kind, name = EXCLUDED.name, resource = EXCLUDED.resource, value = EXCLUDED.value, at = EXCLUDED.at`;
|
|
107
|
+
|
|
108
|
+
const SELECT_ENTRIES_SQL = `SELECT * FROM oke_journal_entries WHERE run_id = ? ORDER BY seq ASC`;
|
|
86
109
|
|
|
87
110
|
/** Guarded release — never clears another holder's lease. */
|
|
88
111
|
const RELEASE_LEASE_SQL = `UPDATE oke_journal_runs SET locked_by = NULL, lease_expires_at = NULL WHERE id = ? AND locked_by = ?`;
|
|
@@ -168,9 +191,70 @@ function rowToRun(row: Record<string, unknown>): JournalRun {
|
|
|
168
191
|
if (r.tenant !== null && r.tenant !== undefined && String(r.tenant).length > 0) {
|
|
169
192
|
run.tenant = String(r.tenant);
|
|
170
193
|
}
|
|
194
|
+
const leaseToken = numOrNull(r.lease_token);
|
|
195
|
+
if (leaseToken !== null) run.leaseToken = leaseToken;
|
|
196
|
+
if (
|
|
197
|
+
r.code_version !== null &&
|
|
198
|
+
r.code_version !== undefined &&
|
|
199
|
+
String(r.code_version).length > 0
|
|
200
|
+
) {
|
|
201
|
+
run.codeVersion = String(r.code_version);
|
|
202
|
+
}
|
|
171
203
|
return run;
|
|
172
204
|
}
|
|
173
205
|
|
|
206
|
+
function entryToParams(runId: string, seq: number, entry: JournalEntry): unknown[] {
|
|
207
|
+
if (entry.kind === "step") {
|
|
208
|
+
return [runId, seq, "step", entry.name, null, JSON.stringify(entry.value ?? null), entry.at];
|
|
209
|
+
}
|
|
210
|
+
if (entry.kind === "sleep") {
|
|
211
|
+
return [
|
|
212
|
+
runId,
|
|
213
|
+
seq,
|
|
214
|
+
"sleep",
|
|
215
|
+
entry.label,
|
|
216
|
+
entry.duration,
|
|
217
|
+
JSON.stringify(entry.wakeAt),
|
|
218
|
+
entry.at,
|
|
219
|
+
];
|
|
220
|
+
}
|
|
221
|
+
return [
|
|
222
|
+
runId,
|
|
223
|
+
seq,
|
|
224
|
+
"effect",
|
|
225
|
+
entry.effectKind,
|
|
226
|
+
entry.resource,
|
|
227
|
+
JSON.stringify(entry.value ?? null),
|
|
228
|
+
entry.at,
|
|
229
|
+
];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function entryFromRow(row: Record<string, unknown>): JournalEntry {
|
|
233
|
+
const kind = String(row.kind);
|
|
234
|
+
const at = Number(row.at);
|
|
235
|
+
const value =
|
|
236
|
+
row.value === null || row.value === undefined ? null : JSON.parse(String(row.value));
|
|
237
|
+
if (kind === "step") {
|
|
238
|
+
return { kind: "step", name: String(row.name ?? ""), value, at };
|
|
239
|
+
}
|
|
240
|
+
if (kind === "sleep") {
|
|
241
|
+
return {
|
|
242
|
+
kind: "sleep",
|
|
243
|
+
label: String(row.name ?? ""),
|
|
244
|
+
duration: String(row.resource ?? ""),
|
|
245
|
+
wakeAt: Number(value),
|
|
246
|
+
at,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
kind: "effect",
|
|
251
|
+
effectKind: String(row.name ?? ""),
|
|
252
|
+
resource: String(row.resource ?? ""),
|
|
253
|
+
value,
|
|
254
|
+
at,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
174
258
|
function runToParams(run: JournalRun): unknown[] {
|
|
175
259
|
return [
|
|
176
260
|
run.id,
|
|
@@ -186,6 +270,8 @@ function runToParams(run: JournalRun): unknown[] {
|
|
|
186
270
|
run.createdAt,
|
|
187
271
|
run.updatedAt,
|
|
188
272
|
run.tenant ?? null,
|
|
273
|
+
run.leaseToken ?? null,
|
|
274
|
+
run.codeVersion ?? null,
|
|
189
275
|
];
|
|
190
276
|
}
|
|
191
277
|
|
|
@@ -206,6 +292,18 @@ async function ensureSchema(sql: PostgresJournalSql): Promise<void> {
|
|
|
206
292
|
tenant TEXT
|
|
207
293
|
)`);
|
|
208
294
|
await sql.exec(`ALTER TABLE oke_journal_runs ADD COLUMN IF NOT EXISTS tenant TEXT`);
|
|
295
|
+
await sql.exec(`ALTER TABLE oke_journal_runs ADD COLUMN IF NOT EXISTS lease_token BIGINT`);
|
|
296
|
+
await sql.exec(`ALTER TABLE oke_journal_runs ADD COLUMN IF NOT EXISTS code_version TEXT`);
|
|
297
|
+
await sql.exec(`CREATE TABLE IF NOT EXISTS oke_journal_entries (
|
|
298
|
+
run_id TEXT NOT NULL,
|
|
299
|
+
seq BIGINT NOT NULL,
|
|
300
|
+
kind TEXT NOT NULL,
|
|
301
|
+
name TEXT,
|
|
302
|
+
resource TEXT,
|
|
303
|
+
value TEXT,
|
|
304
|
+
at BIGINT NOT NULL,
|
|
305
|
+
PRIMARY KEY (run_id, seq)
|
|
306
|
+
)`);
|
|
209
307
|
await sql.exec(
|
|
210
308
|
`CREATE INDEX IF NOT EXISTS oke_journal_runs_wake ON oke_journal_runs (status, wake_at)`,
|
|
211
309
|
);
|
|
@@ -257,9 +355,49 @@ export function createPostgresJournalFake(): PostgresJournalSql & {
|
|
|
257
355
|
created_at: number;
|
|
258
356
|
expires_at: number;
|
|
259
357
|
};
|
|
260
|
-
type
|
|
358
|
+
type LabelDbRow = {
|
|
359
|
+
decision_id: string;
|
|
360
|
+
question: string;
|
|
361
|
+
value: string;
|
|
362
|
+
propensity: number;
|
|
363
|
+
reviewer: string;
|
|
364
|
+
locale: string | null;
|
|
365
|
+
model: string | null;
|
|
366
|
+
tenant: string | null;
|
|
367
|
+
score: number | null;
|
|
368
|
+
loss: number | null;
|
|
369
|
+
raw: string | null;
|
|
370
|
+
at: number;
|
|
371
|
+
input: string | null;
|
|
372
|
+
review_id?: string | null;
|
|
373
|
+
};
|
|
374
|
+
type State = {
|
|
375
|
+
rows: JournalDbRow[];
|
|
376
|
+
idem: IdemDbRow[];
|
|
377
|
+
labels: LabelDbRow[];
|
|
378
|
+
drift: { decision_id: string; suspended: number; certified_at: number }[];
|
|
379
|
+
candidates: { decision_id: string; body: string }[];
|
|
380
|
+
agentRuns: { run_id: string; header: string; sweep_claim_at: number | null }[];
|
|
381
|
+
agentEvents: {
|
|
382
|
+
run_id: string;
|
|
383
|
+
seq: number;
|
|
384
|
+
event: string;
|
|
385
|
+
event_type: string | null;
|
|
386
|
+
event_name: string | null;
|
|
387
|
+
}[];
|
|
388
|
+
entries: JournalEntryDbRow[];
|
|
389
|
+
};
|
|
261
390
|
|
|
262
|
-
let committed: State = {
|
|
391
|
+
let committed: State = {
|
|
392
|
+
rows: [],
|
|
393
|
+
idem: [],
|
|
394
|
+
labels: [],
|
|
395
|
+
drift: [],
|
|
396
|
+
candidates: [],
|
|
397
|
+
agentRuns: [],
|
|
398
|
+
agentEvents: [],
|
|
399
|
+
entries: [],
|
|
400
|
+
};
|
|
263
401
|
let active: { state: State; locked: Set<string>; done: boolean } | null = null;
|
|
264
402
|
/** Run ids held by other active transactions (SKIP LOCKED). */
|
|
265
403
|
const heldByTxn = new Set<string>();
|
|
@@ -274,6 +412,12 @@ export function createPostgresJournalFake(): PostgresJournalSql & {
|
|
|
274
412
|
return {
|
|
275
413
|
rows: s.rows.map((r) => ({ ...r })),
|
|
276
414
|
idem: s.idem.map((r) => ({ ...r })),
|
|
415
|
+
labels: s.labels.map((r) => ({ ...r })),
|
|
416
|
+
drift: s.drift.map((r) => ({ ...r })),
|
|
417
|
+
candidates: s.candidates.map((r) => ({ ...r })),
|
|
418
|
+
agentRuns: s.agentRuns.map((r) => ({ ...r })),
|
|
419
|
+
agentEvents: s.agentEvents.map((r) => ({ ...r })),
|
|
420
|
+
entries: s.entries.map((r) => ({ ...r })),
|
|
277
421
|
};
|
|
278
422
|
}
|
|
279
423
|
|
|
@@ -376,6 +520,73 @@ export function createPostgresJournalFake(): PostgresJournalSql & {
|
|
|
376
520
|
.map((row) => ({ ...row }) as Record<string, unknown>);
|
|
377
521
|
}
|
|
378
522
|
|
|
523
|
+
if (/FROM\s+oke_decision_labels/i.test(text)) {
|
|
524
|
+
let labels = state.labels;
|
|
525
|
+
let arg = 0;
|
|
526
|
+
if (/decision_id\s*=\s*\?/i.test(text)) {
|
|
527
|
+
const id = String(params[arg++] ?? "");
|
|
528
|
+
labels = labels.filter((row) => row.decision_id === id);
|
|
529
|
+
}
|
|
530
|
+
if (/tenant\s+IS\s+NOT\s+DISTINCT\s+FROM\s+\?/i.test(text)) {
|
|
531
|
+
const tenant = params[arg] ?? null;
|
|
532
|
+
labels = labels.filter((row) => row.tenant === tenant);
|
|
533
|
+
}
|
|
534
|
+
return labels.map((row) => ({ ...row }));
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (/FROM\s+oke_decision_drift/i.test(text)) {
|
|
538
|
+
return state.drift.map((row) => ({ ...row }));
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (/FROM\s+oke_decision_candidates/i.test(text)) {
|
|
542
|
+
if (/decision_id\s*=\s*\?/i.test(text)) {
|
|
543
|
+
const id = String(params[0] ?? "");
|
|
544
|
+
return state.candidates
|
|
545
|
+
.filter((row) => row.decision_id === id)
|
|
546
|
+
.map((row) => ({ ...row }));
|
|
547
|
+
}
|
|
548
|
+
return state.candidates.map((row) => ({ ...row }));
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (/FROM\s+oke_agent_run\b/i.test(text)) {
|
|
552
|
+
if (!/WHERE/i.test(text)) return state.agentRuns.map((row) => ({ ...row }));
|
|
553
|
+
const id = String(params[0] ?? "");
|
|
554
|
+
return state.agentRuns.filter((row) => row.run_id === id).map((row) => ({ ...row }));
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (/FROM\s+oke_agent_event\b/i.test(text)) {
|
|
558
|
+
const id = String(params[0] ?? "");
|
|
559
|
+
if (/MAX\s*\(\s*seq\s*\)/i.test(text)) {
|
|
560
|
+
const max = state.agentEvents
|
|
561
|
+
.filter((row) => row.run_id === id)
|
|
562
|
+
.reduce((highest, row) => Math.max(highest, row.seq), 0);
|
|
563
|
+
return [{ max_seq: max }];
|
|
564
|
+
}
|
|
565
|
+
if (/event_type\s*=/i.test(text)) {
|
|
566
|
+
const eventType = String(params[1] ?? "");
|
|
567
|
+
const eventName = String(params[2] ?? "");
|
|
568
|
+
return state.agentEvents
|
|
569
|
+
.filter(
|
|
570
|
+
(row) =>
|
|
571
|
+
row.run_id === id && row.event_type === eventType && row.event_name === eventName,
|
|
572
|
+
)
|
|
573
|
+
.map((row) => ({ ...row }));
|
|
574
|
+
}
|
|
575
|
+
const after = /seq\s*>\s*\?/i.test(text) ? Number(params[1] ?? 0) : 0;
|
|
576
|
+
return state.agentEvents
|
|
577
|
+
.filter((row) => row.run_id === id && row.seq > after)
|
|
578
|
+
.sort((a, b) => a.seq - b.seq)
|
|
579
|
+
.map((row) => ({ ...row }));
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
if (/FROM\s+oke_journal_entries/i.test(text)) {
|
|
583
|
+
const runId = String(params[0] ?? "");
|
|
584
|
+
return state.entries
|
|
585
|
+
.filter((row) => row.run_id === runId)
|
|
586
|
+
.sort((a, b) => a.seq - b.seq)
|
|
587
|
+
.map((row) => ({ ...row }));
|
|
588
|
+
}
|
|
589
|
+
|
|
379
590
|
throw new Error(`postgres journal fake: unsupported query: ${sql}`);
|
|
380
591
|
},
|
|
381
592
|
async exec(sql, params = []) {
|
|
@@ -413,6 +624,14 @@ export function createPostgresJournalFake(): PostgresJournalSql & {
|
|
|
413
624
|
created_at: Number(record.created_at ?? 0),
|
|
414
625
|
updated_at: Number(record.updated_at ?? 0),
|
|
415
626
|
tenant: (record.tenant as string | null) ?? null,
|
|
627
|
+
lease_token:
|
|
628
|
+
record.lease_token === undefined || record.lease_token === null
|
|
629
|
+
? (state.rows.find((r) => r.id === String(record.id))?.lease_token ?? null)
|
|
630
|
+
: Number(record.lease_token),
|
|
631
|
+
code_version:
|
|
632
|
+
record.code_version === undefined || record.code_version === null
|
|
633
|
+
? (state.rows.find((r) => r.id === String(record.id))?.code_version ?? null)
|
|
634
|
+
: String(record.code_version),
|
|
416
635
|
};
|
|
417
636
|
const idx = state.rows.findIndex((r) => r.id === next.id);
|
|
418
637
|
if (idx >= 0) state.rows[idx] = next;
|
|
@@ -421,15 +640,33 @@ export function createPostgresJournalFake(): PostgresJournalSql & {
|
|
|
421
640
|
}
|
|
422
641
|
|
|
423
642
|
const updLease =
|
|
424
|
-
/^UPDATE\s+oke_journal_runs\s+SET\s+locked_by\s*=\s*\?,\s*lease_expires_at\s*=\s
|
|
643
|
+
/^UPDATE\s+oke_journal_runs\s+SET\s+locked_by\s*=\s*\?,\s*lease_expires_at\s*=\s*\?/i.exec(
|
|
425
644
|
text,
|
|
426
645
|
);
|
|
427
|
-
if (updLease) {
|
|
428
|
-
const
|
|
646
|
+
if (updLease && /WHERE\s+id\s*=\s*\?/i.test(text) && !/locked_by\s*=\s*NULL/i.test(text)) {
|
|
647
|
+
const id = String(params[params.length - 1]);
|
|
648
|
+
const row = state.rows.find((r) => r.id === id);
|
|
429
649
|
if (!row) return { changes: 0 };
|
|
430
650
|
row.locked_by = params[0] === null ? null : String(params[0]);
|
|
431
651
|
row.lease_expires_at =
|
|
432
652
|
params[1] === null || params[1] === undefined ? null : Number(params[1]);
|
|
653
|
+
if (/lease_token/i.test(text)) row.lease_token = (row.lease_token ?? 0) + 1;
|
|
654
|
+
return { changes: 1 };
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
if (/^INSERT\s+INTO\s+oke_journal_entries\b/i.test(text)) {
|
|
658
|
+
const row: JournalEntryDbRow = {
|
|
659
|
+
run_id: String(params[0]),
|
|
660
|
+
seq: Number(params[1]),
|
|
661
|
+
kind: String(params[2]),
|
|
662
|
+
name: params[3] === null || params[3] === undefined ? null : String(params[3]),
|
|
663
|
+
resource: params[4] === null || params[4] === undefined ? null : String(params[4]),
|
|
664
|
+
value: params[5] === null || params[5] === undefined ? null : String(params[5]),
|
|
665
|
+
at: Number(params[6]),
|
|
666
|
+
};
|
|
667
|
+
const idx = state.entries.findIndex((e) => e.run_id === row.run_id && e.seq === row.seq);
|
|
668
|
+
if (idx >= 0) state.entries[idx] = row;
|
|
669
|
+
else state.entries.push(row);
|
|
433
670
|
return { changes: 1 };
|
|
434
671
|
}
|
|
435
672
|
|
|
@@ -539,6 +776,112 @@ export function createPostgresJournalFake(): PostgresJournalSql & {
|
|
|
539
776
|
row.run_id = String(params[0]);
|
|
540
777
|
return { changes: 1 };
|
|
541
778
|
}
|
|
779
|
+
if (/^INSERT\s+INTO\s+oke_decision_labels\b/i.test(text)) {
|
|
780
|
+
state.labels.push({
|
|
781
|
+
decision_id: String(params[0]),
|
|
782
|
+
question: String(params[1]),
|
|
783
|
+
value: String(params[2]),
|
|
784
|
+
propensity: Number(params[3]),
|
|
785
|
+
reviewer: String(params[4]),
|
|
786
|
+
locale: params[5] === null || params[5] === undefined ? null : String(params[5]),
|
|
787
|
+
model: params[6] === null || params[6] === undefined ? null : String(params[6]),
|
|
788
|
+
tenant: params[7] === null || params[7] === undefined ? null : String(params[7]),
|
|
789
|
+
score: params[8] === null || params[8] === undefined ? null : Number(params[8]),
|
|
790
|
+
loss: params[9] === null || params[9] === undefined ? null : Number(params[9]),
|
|
791
|
+
raw: params[10] === null || params[10] === undefined ? null : String(params[10]),
|
|
792
|
+
at: Number(params[11]),
|
|
793
|
+
input: params[12] === null || params[12] === undefined ? null : String(params[12]),
|
|
794
|
+
review_id: params[13] === null || params[13] === undefined ? null : String(params[13]),
|
|
795
|
+
});
|
|
796
|
+
return { changes: 1 };
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
if (/^INSERT\s+INTO\s+oke_decision_candidates\b/i.test(text)) {
|
|
800
|
+
const decisionId = String(params[0]);
|
|
801
|
+
const body = String(params[1]);
|
|
802
|
+
const idx = state.candidates.findIndex((row) => row.decision_id === decisionId);
|
|
803
|
+
if (idx >= 0) state.candidates[idx] = { decision_id: decisionId, body };
|
|
804
|
+
else state.candidates.push({ decision_id: decisionId, body });
|
|
805
|
+
return { changes: 1 };
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
if (/^UPDATE\s+oke_decision_drift\b/i.test(text)) return { changes: 0 };
|
|
809
|
+
|
|
810
|
+
if (/^DELETE\s+FROM\s+oke_decision_drift\b/i.test(text)) {
|
|
811
|
+
const id = String(params[0] ?? "");
|
|
812
|
+
const before = state.drift.length;
|
|
813
|
+
state.drift = state.drift.filter((row) => row.decision_id !== id);
|
|
814
|
+
return { changes: before - state.drift.length };
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
if (/^INSERT\s+INTO\s+oke_decision_drift\b/i.test(text)) {
|
|
818
|
+
const decisionId = String(params[0]);
|
|
819
|
+
const next = {
|
|
820
|
+
decision_id: decisionId,
|
|
821
|
+
suspended: Number(params[1]),
|
|
822
|
+
certified_at: Number(params[2] ?? 0),
|
|
823
|
+
};
|
|
824
|
+
const idx = state.drift.findIndex((row) => row.decision_id === decisionId);
|
|
825
|
+
if (idx >= 0) state.drift[idx] = next;
|
|
826
|
+
else state.drift.push(next);
|
|
827
|
+
return { changes: 1 };
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
if (/^UPDATE\s+oke_agent_run\s+SET\s+sweep_claim_at\b/i.test(text)) {
|
|
831
|
+
const until = Number(params[0]);
|
|
832
|
+
const runId = String(params[1] ?? "");
|
|
833
|
+
const now = Number(params[2]);
|
|
834
|
+
const row = state.agentRuns.find((candidate) => candidate.run_id === runId);
|
|
835
|
+
if (!row) return { changes: 0 };
|
|
836
|
+
if (row.sweep_claim_at !== null && row.sweep_claim_at > now) return { changes: 0 };
|
|
837
|
+
row.sweep_claim_at = until;
|
|
838
|
+
return { changes: 1 };
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
if (/^INSERT\s+INTO\s+oke_agent_run\b/i.test(text)) {
|
|
842
|
+
const runId = String(params[0]);
|
|
843
|
+
const existing = state.agentRuns.find((row) => row.run_id === runId);
|
|
844
|
+
const next = {
|
|
845
|
+
run_id: runId,
|
|
846
|
+
header: String(params[1]),
|
|
847
|
+
sweep_claim_at: existing?.sweep_claim_at ?? null,
|
|
848
|
+
};
|
|
849
|
+
const idx = state.agentRuns.findIndex((row) => row.run_id === runId);
|
|
850
|
+
if (idx >= 0) state.agentRuns[idx] = next;
|
|
851
|
+
else state.agentRuns.push(next);
|
|
852
|
+
return { changes: 1 };
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
if (/^INSERT\s+INTO\s+oke_agent_event\b/i.test(text)) {
|
|
856
|
+
const runId = String(params[0]);
|
|
857
|
+
const seq = Number(params[1]);
|
|
858
|
+
if (state.agentEvents.some((row) => row.run_id === runId && row.seq === seq)) {
|
|
859
|
+
throw new Error(`duplicate key value violates unique constraint "oke_agent_event_pkey"`);
|
|
860
|
+
}
|
|
861
|
+
state.agentEvents.push({
|
|
862
|
+
run_id: runId,
|
|
863
|
+
seq,
|
|
864
|
+
event: String(params[2]),
|
|
865
|
+
event_type: params[3] === undefined || params[3] === null ? null : String(params[3]),
|
|
866
|
+
event_name: params[4] === undefined || params[4] === null ? null : String(params[4]),
|
|
867
|
+
});
|
|
868
|
+
return { changes: 1 };
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
if (/^DELETE\s+FROM\s+oke_agent_event\b/i.test(text)) {
|
|
872
|
+
const id = String(params[0] ?? "");
|
|
873
|
+
const before = state.agentEvents.length;
|
|
874
|
+
state.agentEvents = state.agentEvents.filter((row) => row.run_id !== id);
|
|
875
|
+
return { changes: before - state.agentEvents.length };
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
if (/^DELETE\s+FROM\s+oke_agent_run\b/i.test(text)) {
|
|
879
|
+
const id = String(params[0] ?? "");
|
|
880
|
+
const before = state.agentRuns.length;
|
|
881
|
+
state.agentRuns = state.agentRuns.filter((row) => row.run_id !== id);
|
|
882
|
+
return { changes: before - state.agentRuns.length };
|
|
883
|
+
}
|
|
884
|
+
|
|
542
885
|
if (text === IDEM_FORFEIT_SQL) {
|
|
543
886
|
const row = state.idem.find(
|
|
544
887
|
(candidate) =>
|
|
@@ -596,15 +939,72 @@ export function createPostgresJournalFake(): PostgresJournalSql & {
|
|
|
596
939
|
return api;
|
|
597
940
|
}
|
|
598
941
|
|
|
599
|
-
const UPSERT_SQL = `INSERT INTO oke_journal_runs (id, flow, input, status, entries, wake_at, error, output, locked_by, lease_expires_at, created_at, updated_at, tenant) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT (id) DO UPDATE SET flow = EXCLUDED.flow, input = EXCLUDED.input, status = EXCLUDED.status, entries = EXCLUDED.entries, wake_at = EXCLUDED.wake_at, error = EXCLUDED.error, output = EXCLUDED.output, locked_by = EXCLUDED.locked_by, lease_expires_at = EXCLUDED.lease_expires_at, created_at = EXCLUDED.created_at, updated_at = EXCLUDED.updated_at, tenant = EXCLUDED.tenant`;
|
|
942
|
+
const UPSERT_SQL = `INSERT INTO oke_journal_runs (id, flow, input, status, entries, wake_at, error, output, locked_by, lease_expires_at, created_at, updated_at, tenant, lease_token, code_version) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT (id) DO UPDATE SET flow = EXCLUDED.flow, input = EXCLUDED.input, status = EXCLUDED.status, entries = EXCLUDED.entries, wake_at = EXCLUDED.wake_at, error = EXCLUDED.error, output = EXCLUDED.output, locked_by = EXCLUDED.locked_by, lease_expires_at = EXCLUDED.lease_expires_at, created_at = EXCLUDED.created_at, updated_at = EXCLUDED.updated_at, tenant = EXCLUDED.tenant, lease_token = EXCLUDED.lease_token, code_version = EXCLUDED.code_version`;
|
|
600
943
|
|
|
601
944
|
/** Postgres journal store with run-level lease coordination. */
|
|
602
945
|
export type PostgresJournalStore = JournalStore &
|
|
603
946
|
JournalLeaseStore & {
|
|
604
947
|
readonly sql: PostgresJournalSql;
|
|
948
|
+
/** Bytes written for entry rows plus header entry payloads. */
|
|
949
|
+
readonly writeBytes: number;
|
|
605
950
|
close(): Promise<void>;
|
|
606
951
|
};
|
|
607
952
|
|
|
953
|
+
/**
|
|
954
|
+
* Open decision tables on first use so a journal without decisions stays empty.
|
|
955
|
+
*
|
|
956
|
+
* @param sql - Journal SQL client
|
|
957
|
+
*/
|
|
958
|
+
function loadDecisionLabelModule(): typeof import("../kernel/decision-label-store.ts") {
|
|
959
|
+
return lazyRequire(`${import.meta.dir}/../kernel`, ["decision", "label", "store"].join("-"));
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
function loadAgentEventModule(): typeof import("../kernel/agent-event-store.ts") {
|
|
963
|
+
return lazyRequire(`${import.meta.dir}/../kernel`, ["agent", "event", "store"].join("-"));
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
function lazyDecisionLabels(sql: PostgresJournalSql): DecisionLabelStore {
|
|
967
|
+
let pending: Promise<DecisionLabelStore> | undefined;
|
|
968
|
+
const ready = (): Promise<DecisionLabelStore> => {
|
|
969
|
+
pending ??= loadDecisionLabelModule().createPostgresDecisionLabelStore(sql);
|
|
970
|
+
return pending;
|
|
971
|
+
};
|
|
972
|
+
return {
|
|
973
|
+
insert: (label, at) => ready().then((store) => store.insert(label, at)),
|
|
974
|
+
list: (decision, tenant) => ready().then((store) => store.list(decision, tenant)),
|
|
975
|
+
drift: () => ready().then((store) => store.drift()),
|
|
976
|
+
setDrift: (decision, record) => ready().then((store) => store.setDrift(decision, record)),
|
|
977
|
+
putCandidate: (decision, entry) => ready().then((store) => store.putCandidate(decision, entry)),
|
|
978
|
+
getCandidate: (decision) => ready().then((store) => store.getCandidate(decision)),
|
|
979
|
+
listCandidates: () => ready().then((store) => store.listCandidates()),
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
/**
|
|
984
|
+
* Open the agent event tables on first use.
|
|
985
|
+
*
|
|
986
|
+
* @param sql - Journal SQL client
|
|
987
|
+
*/
|
|
988
|
+
function lazyAgentEvents(sql: PostgresJournalSql): AgentEventStore {
|
|
989
|
+
let pending: Promise<AgentEventStore> | undefined;
|
|
990
|
+
const ready = (): Promise<AgentEventStore> => {
|
|
991
|
+
pending ??= loadAgentEventModule().createPostgresAgentEventStore(sql);
|
|
992
|
+
return pending;
|
|
993
|
+
};
|
|
994
|
+
return {
|
|
995
|
+
read: (runId) => ready().then((store) => store.read(runId)),
|
|
996
|
+
readAfter: (runId, afterSeq) => ready().then((store) => store.readAfter(runId, afterSeq)),
|
|
997
|
+
maxSeq: (runId) => ready().then((store) => store.maxSeq(runId)),
|
|
998
|
+
readHeader: (runId) => ready().then((store) => store.readHeader(runId)),
|
|
999
|
+
truncated: (runId) => ready().then((store) => store.truncated(runId)),
|
|
1000
|
+
claim: (runId) => ready().then((store) => store.claim(runId)),
|
|
1001
|
+
listHeaders: () => ready().then((store) => store.listHeaders()),
|
|
1002
|
+
writeHeader: (header) => ready().then((store) => store.writeHeader(header)),
|
|
1003
|
+
append: (runId, row) => ready().then((store) => store.append(runId, row)),
|
|
1004
|
+
remove: (runId) => ready().then((store) => store.remove(runId)),
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
|
|
608
1008
|
/**
|
|
609
1009
|
* Open a postgres-backed JournalStore (multi-host durable-run coordination).
|
|
610
1010
|
*
|
|
@@ -622,20 +1022,77 @@ export async function createPostgresJournalStore(
|
|
|
622
1022
|
|
|
623
1023
|
await ensureSchema(sql);
|
|
624
1024
|
|
|
1025
|
+
let writeBytes = 0;
|
|
1026
|
+
|
|
1027
|
+
async function hydrate(row: Record<string, unknown>): Promise<JournalRun> {
|
|
1028
|
+
const run = rowToRun(row);
|
|
1029
|
+
const entryRows = await sql.query(SELECT_ENTRIES_SQL, [run.id]);
|
|
1030
|
+
if (entryRows.length > 0) {
|
|
1031
|
+
(run as { entries: JournalEntry[] }).entries = entryRows.map((entry) => entryFromRow(entry));
|
|
1032
|
+
return run;
|
|
1033
|
+
}
|
|
1034
|
+
if (run.entries.length > 0) {
|
|
1035
|
+
for (let seq = 0; seq < run.entries.length; seq++) {
|
|
1036
|
+
const entry = run.entries[seq];
|
|
1037
|
+
if (!entry) continue;
|
|
1038
|
+
const params = entryToParams(run.id, seq, entry);
|
|
1039
|
+
writeBytes += JSON.stringify(entry).length;
|
|
1040
|
+
await sql.exec(INSERT_ENTRY_SQL, params);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
return run;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
function assertFence(run: JournalRun | undefined, fence: JournalWriteFence | undefined): void {
|
|
1047
|
+
if (!fence || !run) return;
|
|
1048
|
+
const expired = run.leaseExpiresAt !== undefined && run.leaseExpiresAt <= fence.now;
|
|
1049
|
+
if (run.lockedBy !== fence.lockedBy || run.leaseToken !== fence.leaseToken || expired) {
|
|
1050
|
+
throwOke("JOURNAL_STALE_LEASE", { runId: run.id });
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
|
|
625
1054
|
const store: PostgresJournalStore = {
|
|
626
1055
|
sql,
|
|
1056
|
+
get writeBytes() {
|
|
1057
|
+
return writeBytes;
|
|
1058
|
+
},
|
|
627
1059
|
idempotency: createPostgresIdempotencyStore(sql),
|
|
1060
|
+
decisions: lazyDecisionLabels(sql),
|
|
1061
|
+
agentEvents: lazyAgentEvents(sql),
|
|
628
1062
|
async get(runId) {
|
|
629
1063
|
const rows = await sql.query(`SELECT * FROM oke_journal_runs WHERE id = ?`, [runId]);
|
|
630
1064
|
if (!rows[0]) return undefined;
|
|
631
|
-
return
|
|
1065
|
+
return hydrate(rows[0]);
|
|
632
1066
|
},
|
|
633
|
-
async put(run) {
|
|
1067
|
+
async put(run, fence) {
|
|
1068
|
+
if (fence) {
|
|
1069
|
+
const rows = await sql.query(`SELECT * FROM oke_journal_runs WHERE id = ?`, [run.id]);
|
|
1070
|
+
assertFence(rows[0] ? rowToRun(rows[0]) : undefined, fence);
|
|
1071
|
+
}
|
|
1072
|
+
writeBytes += JSON.stringify(run.entries).length;
|
|
634
1073
|
await sql.exec(UPSERT_SQL, runToParams(run));
|
|
1074
|
+
for (let seq = 0; seq < run.entries.length; seq++) {
|
|
1075
|
+
const entry = run.entries[seq];
|
|
1076
|
+
if (!entry) continue;
|
|
1077
|
+
writeBytes += JSON.stringify(entry).length;
|
|
1078
|
+
await sql.exec(INSERT_ENTRY_SQL, entryToParams(run.id, seq, entry));
|
|
1079
|
+
}
|
|
1080
|
+
},
|
|
1081
|
+
async appendEntry(runId, seq, entry, fence) {
|
|
1082
|
+
if (fence) {
|
|
1083
|
+
const rows = await sql.query(`SELECT * FROM oke_journal_runs WHERE id = ?`, [runId]);
|
|
1084
|
+
const current = rows[0] ? rowToRun(rows[0]) : undefined;
|
|
1085
|
+
if (!current) throw new Error(`journal: run "${runId}" not found`);
|
|
1086
|
+
assertFence(current, fence);
|
|
1087
|
+
}
|
|
1088
|
+
writeBytes += JSON.stringify(entry).length;
|
|
1089
|
+
await sql.exec(INSERT_ENTRY_SQL, entryToParams(runId, seq, entry));
|
|
635
1090
|
},
|
|
636
1091
|
async list() {
|
|
637
1092
|
const rows = await sql.query(`SELECT * FROM oke_journal_runs`);
|
|
638
|
-
|
|
1093
|
+
const runs: JournalRun[] = [];
|
|
1094
|
+
for (const row of rows) runs.push(await hydrate(row));
|
|
1095
|
+
return runs;
|
|
639
1096
|
},
|
|
640
1097
|
async acquireLease(runId, instanceId, now, leaseMs) {
|
|
641
1098
|
return sql.begin(async (tx) => {
|
|
@@ -645,6 +1102,43 @@ export async function createPostgresJournalStore(
|
|
|
645
1102
|
return true;
|
|
646
1103
|
});
|
|
647
1104
|
},
|
|
1105
|
+
async cas(runId, instanceId, now, leaseMs, update) {
|
|
1106
|
+
return sql.begin(async (tx) => {
|
|
1107
|
+
const claimed = await tx.query(CLAIM_LEASE_SQL, [runId, instanceId, now]);
|
|
1108
|
+
const row = claimed[0];
|
|
1109
|
+
if (!row) {
|
|
1110
|
+
const exists = await tx.query(`SELECT * FROM oke_journal_runs WHERE id = ?`, [runId]);
|
|
1111
|
+
const held = exists[0];
|
|
1112
|
+
if (!held) return "missing";
|
|
1113
|
+
const current = rowToRun(held);
|
|
1114
|
+
return { lease: true, leaseExpiresAt: current.leaseExpiresAt };
|
|
1115
|
+
}
|
|
1116
|
+
const current = rowToRun(row);
|
|
1117
|
+
const entryRows = await tx.query(SELECT_ENTRIES_SQL, [runId]);
|
|
1118
|
+
if (entryRows.length > 0) {
|
|
1119
|
+
(current as { entries: JournalEntry[] }).entries = entryRows.map((entry) =>
|
|
1120
|
+
entryFromRow(entry),
|
|
1121
|
+
);
|
|
1122
|
+
}
|
|
1123
|
+
const next = update(current) ?? current;
|
|
1124
|
+
const renew =
|
|
1125
|
+
current.lockedBy === instanceId &&
|
|
1126
|
+
current.leaseExpiresAt !== undefined &&
|
|
1127
|
+
current.leaseExpiresAt > now &&
|
|
1128
|
+
current.leaseToken !== undefined;
|
|
1129
|
+
next.lockedBy = instanceId;
|
|
1130
|
+
next.leaseExpiresAt = now + leaseMs;
|
|
1131
|
+
next.leaseToken = renew ? current.leaseToken : (current.leaseToken ?? 0) + 1;
|
|
1132
|
+
for (let seq = 0; seq < next.entries.length; seq++) {
|
|
1133
|
+
const entry = next.entries[seq];
|
|
1134
|
+
if (!entry) continue;
|
|
1135
|
+
await tx.exec(INSERT_ENTRY_SQL, entryToParams(runId, seq, entry));
|
|
1136
|
+
}
|
|
1137
|
+
const header = { ...next, entries: [] as JournalEntry[] };
|
|
1138
|
+
await tx.exec(UPSERT_SQL, runToParams(header));
|
|
1139
|
+
return "ok";
|
|
1140
|
+
});
|
|
1141
|
+
},
|
|
648
1142
|
async releaseLease(runId, instanceId) {
|
|
649
1143
|
await sql.exec(RELEASE_LEASE_SQL, [runId, instanceId]);
|
|
650
1144
|
},
|
|
@@ -654,7 +1148,11 @@ export async function createPostgresJournalStore(
|
|
|
654
1148
|
const row = rows[0];
|
|
655
1149
|
if (!row) return undefined;
|
|
656
1150
|
await tx.exec(UPDATE_LEASE_SQL, [instanceId, now + leaseMs, String(row.id)]);
|
|
657
|
-
|
|
1151
|
+
const run = rowToRun(row);
|
|
1152
|
+
run.lockedBy = instanceId;
|
|
1153
|
+
run.leaseExpiresAt = now + leaseMs;
|
|
1154
|
+
run.leaseToken = (run.leaseToken ?? 0) + 1;
|
|
1155
|
+
return run;
|
|
658
1156
|
});
|
|
659
1157
|
},
|
|
660
1158
|
async listOrphans(now) {
|
|
@@ -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
|
+
});
|