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