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
package/src/kernel/journal.ts
CHANGED
|
@@ -7,9 +7,12 @@
|
|
|
7
7
|
* (four-applications · Provisions).
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
10
11
|
import { mkdir } from "node:fs/promises";
|
|
11
12
|
import { dirname } from "node:path";
|
|
12
13
|
import { okid } from "../okid.ts";
|
|
14
|
+
import { throwOke } from "./errors.ts";
|
|
15
|
+
import type { DecisionLabelStore } from "./decision-label-store.ts";
|
|
13
16
|
import type { IdempotencyStore } from "./idempotency-store.ts";
|
|
14
17
|
import { JournalSuspend } from "./journal-suspend.ts";
|
|
15
18
|
import { lazyRequire } from "./lazy-require.ts";
|
|
@@ -105,17 +108,31 @@ export interface JournalRun {
|
|
|
105
108
|
lockedBy?: string;
|
|
106
109
|
/** Lease expiry epoch-ms; a crashed holder's run is reclaimable after this. */
|
|
107
110
|
leaseExpiresAt?: number;
|
|
111
|
+
/**
|
|
112
|
+
* Fencing token. Bumped on each acquire that is not a live same-holder renew.
|
|
113
|
+
* Writes from a previous holder are rejected.
|
|
114
|
+
*/
|
|
115
|
+
leaseToken?: number;
|
|
116
|
+
/** Code version stamped at start. Resume fails when the process version differs. */
|
|
117
|
+
codeVersion?: string;
|
|
108
118
|
readonly createdAt: number;
|
|
109
119
|
updatedAt: number;
|
|
110
120
|
/** Isolation context for resume (`fx.tenant`). */
|
|
111
121
|
tenant?: string | null;
|
|
112
122
|
}
|
|
113
123
|
|
|
124
|
+
/** Fence presented with every leased journal write. */
|
|
125
|
+
export interface JournalWriteFence {
|
|
126
|
+
readonly lockedBy: string;
|
|
127
|
+
readonly leaseToken: number;
|
|
128
|
+
/** Epoch-ms used to reject an expired holder. */
|
|
129
|
+
readonly now: number;
|
|
130
|
+
}
|
|
131
|
+
|
|
114
132
|
/**
|
|
115
133
|
* Run-level lease coordination — same SKIP LOCKED + lazy-reclaim physics as
|
|
116
|
-
* Signal's message claims and Clock's tick claims. No sweeper
|
|
117
|
-
* token
|
|
118
|
-
* steps from re-running.
|
|
134
|
+
* Signal's message claims and Clock's tick claims. No sweeper. A fencing
|
|
135
|
+
* token rejects writes from a holder whose lease was reclaimed.
|
|
119
136
|
*/
|
|
120
137
|
export interface JournalLeaseStore {
|
|
121
138
|
/**
|
|
@@ -128,6 +145,24 @@ export interface JournalLeaseStore {
|
|
|
128
145
|
* @param leaseMs - Lease duration
|
|
129
146
|
*/
|
|
130
147
|
acquireLease(runId: string, instanceId: string, now: number, leaseMs: number): Promise<boolean>;
|
|
148
|
+
/**
|
|
149
|
+
* Take the lease and write `update(run)` in that same hold.
|
|
150
|
+
* `undefined` from `update` keeps the previous entries and still holds the lease.
|
|
151
|
+
* `"lease"` means another holder won. `"missing"` means no run.
|
|
152
|
+
*
|
|
153
|
+
* @param runId - Run id
|
|
154
|
+
* @param instanceId - Claimant instance
|
|
155
|
+
* @param now - Epoch-ms
|
|
156
|
+
* @param leaseMs - Lease duration
|
|
157
|
+
* @param update - Next snapshot, or undefined to leave entries
|
|
158
|
+
*/
|
|
159
|
+
cas(
|
|
160
|
+
runId: string,
|
|
161
|
+
instanceId: string,
|
|
162
|
+
now: number,
|
|
163
|
+
leaseMs: number,
|
|
164
|
+
update: (run: JournalRun) => JournalRun | undefined,
|
|
165
|
+
): Promise<"ok" | "missing" | { readonly lease: true; readonly leaseExpiresAt?: number }>;
|
|
131
166
|
/**
|
|
132
167
|
* Release a lease held by `instanceId` (no-op for other holders).
|
|
133
168
|
*
|
|
@@ -161,6 +196,20 @@ function loadIdempotencyStore(): typeof import("./idempotency-store.ts") {
|
|
|
161
196
|
return lazyRequire(import.meta.dir, ["idempotency", "store"].join("-"));
|
|
162
197
|
}
|
|
163
198
|
|
|
199
|
+
/**
|
|
200
|
+
* Load the decision-label table without a static import.
|
|
201
|
+
*/
|
|
202
|
+
function loadDecisionLabelStore(): typeof import("./decision-label-store.ts") {
|
|
203
|
+
return lazyRequire(import.meta.dir, ["decision", "label", "store"].join("-"));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Load the agent-event table without a static import.
|
|
208
|
+
*/
|
|
209
|
+
function loadAgentEventStore(): typeof import("./agent-event-store.ts") {
|
|
210
|
+
return lazyRequire(import.meta.dir, ["agent", "event", "store"].join("-"));
|
|
211
|
+
}
|
|
212
|
+
|
|
164
213
|
/** Persistence backend for journal runs. */
|
|
165
214
|
export interface JournalStore extends Partial<JournalLeaseStore> {
|
|
166
215
|
/**
|
|
@@ -168,6 +217,16 @@ export interface JournalStore extends Partial<JournalLeaseStore> {
|
|
|
168
217
|
* only implements run `get` / `put`.
|
|
169
218
|
*/
|
|
170
219
|
readonly idempotency?: IdempotencyStore;
|
|
220
|
+
/**
|
|
221
|
+
* Decision labels and the drift flag on this same driver. Opened only when
|
|
222
|
+
* the app declares decisions.
|
|
223
|
+
*/
|
|
224
|
+
readonly decisions?: DecisionLabelStore;
|
|
225
|
+
/**
|
|
226
|
+
* Agent-run event log on this same driver. Followers resume from these rows
|
|
227
|
+
* after a restart and from another instance.
|
|
228
|
+
*/
|
|
229
|
+
readonly agentEvents?: import("./agent-event-store.ts").AgentEventStore;
|
|
171
230
|
/**
|
|
172
231
|
* Load a run by id.
|
|
173
232
|
*
|
|
@@ -175,11 +234,31 @@ export interface JournalStore extends Partial<JournalLeaseStore> {
|
|
|
175
234
|
*/
|
|
176
235
|
get(runId: string): Promise<JournalRun | undefined>;
|
|
177
236
|
/**
|
|
178
|
-
* Persist a run (create or replace).
|
|
237
|
+
* Persist a run header (create or replace).
|
|
238
|
+
*
|
|
239
|
+
* When {@link JournalStore.appendEntry} is present, `entries` on this
|
|
240
|
+
* snapshot may be empty — the store keeps previously appended rows.
|
|
179
241
|
*
|
|
180
242
|
* @param run - Run snapshot
|
|
243
|
+
* @param fence - Required for a leased update. Omitted on the first insert
|
|
244
|
+
* and on uncoordinated stores.
|
|
181
245
|
*/
|
|
182
|
-
put(run: JournalRun): Promise<void>;
|
|
246
|
+
put(run: JournalRun, fence?: JournalWriteFence): Promise<void>;
|
|
247
|
+
/**
|
|
248
|
+
* Append one journal entry. Built-in stores implement this so a step does
|
|
249
|
+
* not rewrite earlier entries.
|
|
250
|
+
*
|
|
251
|
+
* @param runId - Run id
|
|
252
|
+
* @param seq - Zero-based position
|
|
253
|
+
* @param entry - JSON-safe entry
|
|
254
|
+
* @param fence - Lease fence when the run is coordinated
|
|
255
|
+
*/
|
|
256
|
+
appendEntry?(
|
|
257
|
+
runId: string,
|
|
258
|
+
seq: number,
|
|
259
|
+
entry: JournalEntry,
|
|
260
|
+
fence?: JournalWriteFence,
|
|
261
|
+
): Promise<void>;
|
|
183
262
|
/** List all runs (test / console helper). */
|
|
184
263
|
list(): Promise<readonly JournalRun[]>;
|
|
185
264
|
}
|
|
@@ -234,16 +313,43 @@ function leaseMethods(
|
|
|
234
313
|
load: () => Promise<Map<string, JournalRun>>,
|
|
235
314
|
flush?: (map: Map<string, JournalRun>) => Promise<void>,
|
|
236
315
|
): JournalLeaseStore {
|
|
316
|
+
let gate: Promise<void> = Promise.resolve();
|
|
237
317
|
return {
|
|
238
318
|
async acquireLease(runId, instanceId, now, leaseMs) {
|
|
239
319
|
const map = await load();
|
|
240
320
|
const run = map.get(runId);
|
|
241
321
|
if (!run || !claimable(run, instanceId, now)) return false;
|
|
242
|
-
run
|
|
243
|
-
run.leaseExpiresAt = now + leaseMs;
|
|
322
|
+
holdLease(run, instanceId, now, leaseMs);
|
|
244
323
|
await flush?.(map);
|
|
245
324
|
return true;
|
|
246
325
|
},
|
|
326
|
+
async cas(runId, instanceId, now, leaseMs, update) {
|
|
327
|
+
let release!: () => void;
|
|
328
|
+
const slot = new Promise<void>((resolve) => {
|
|
329
|
+
release = resolve;
|
|
330
|
+
});
|
|
331
|
+
const prev = gate;
|
|
332
|
+
gate = slot;
|
|
333
|
+
await prev;
|
|
334
|
+
try {
|
|
335
|
+
const map = await load();
|
|
336
|
+
const run = map.get(runId);
|
|
337
|
+
if (!run) return "missing";
|
|
338
|
+
if (!claimable(run, instanceId, now)) {
|
|
339
|
+
return { lease: true, leaseExpiresAt: run.leaseExpiresAt };
|
|
340
|
+
}
|
|
341
|
+
const next = update(cloneRun(run)) ?? run;
|
|
342
|
+
holdLease(next, instanceId, now, leaseMs);
|
|
343
|
+
if (run.entries.length > 0 && next.entries.length === 0) {
|
|
344
|
+
(next as { entries: JournalEntry[] }).entries = run.entries;
|
|
345
|
+
}
|
|
346
|
+
map.set(runId, next);
|
|
347
|
+
await flush?.(map);
|
|
348
|
+
return "ok";
|
|
349
|
+
} finally {
|
|
350
|
+
release();
|
|
351
|
+
}
|
|
352
|
+
},
|
|
247
353
|
async releaseLease(runId, instanceId) {
|
|
248
354
|
const map = await load();
|
|
249
355
|
const run = map.get(runId);
|
|
@@ -264,8 +370,7 @@ function leaseMethods(
|
|
|
264
370
|
)
|
|
265
371
|
.sort((a, b) => (a.wakeAt ?? 0) - (b.wakeAt ?? 0))[0];
|
|
266
372
|
if (!due) return undefined;
|
|
267
|
-
due
|
|
268
|
-
due.leaseExpiresAt = now + leaseMs;
|
|
373
|
+
holdLease(due, instanceId, now, leaseMs);
|
|
269
374
|
await flush?.(map);
|
|
270
375
|
return cloneRun(due);
|
|
271
376
|
},
|
|
@@ -294,8 +399,16 @@ export function createMemoryJournalStore(seed?: readonly JournalRun[]): JournalS
|
|
|
294
399
|
const r = runs.get(runId);
|
|
295
400
|
return r ? cloneRun(r) : undefined;
|
|
296
401
|
},
|
|
297
|
-
async put(run) {
|
|
298
|
-
runs.
|
|
402
|
+
async put(run, fence) {
|
|
403
|
+
const existing = runs.get(run.id);
|
|
404
|
+
assertJournalFence(existing, fence);
|
|
405
|
+
runs.set(run.id, mergeJournalPut(existing, run));
|
|
406
|
+
},
|
|
407
|
+
async appendEntry(runId, seq, entry, fence) {
|
|
408
|
+
const existing = runs.get(runId);
|
|
409
|
+
if (!existing) throw new Error(`journal: run "${runId}" not found`);
|
|
410
|
+
assertJournalFence(existing, fence);
|
|
411
|
+
appendStoredEntry(existing, seq, entry);
|
|
299
412
|
},
|
|
300
413
|
async list() {
|
|
301
414
|
return [...runs.values()].map(cloneRun);
|
|
@@ -306,6 +419,16 @@ export function createMemoryJournalStore(seed?: readonly JournalRun[]): JournalS
|
|
|
306
419
|
Object.defineProperty(this, "idempotency", { value: created });
|
|
307
420
|
return created;
|
|
308
421
|
},
|
|
422
|
+
get decisions(): DecisionLabelStore {
|
|
423
|
+
const created = loadDecisionLabelStore().createMemoryDecisionLabelStore();
|
|
424
|
+
Object.defineProperty(this, "decisions", { value: created });
|
|
425
|
+
return created;
|
|
426
|
+
},
|
|
427
|
+
get agentEvents(): import("./agent-event-store.ts").AgentEventStore {
|
|
428
|
+
const created = loadAgentEventStore().createMemoryAgentEventStore();
|
|
429
|
+
Object.defineProperty(this, "agentEvents", { value: created });
|
|
430
|
+
return created;
|
|
431
|
+
},
|
|
309
432
|
};
|
|
310
433
|
}
|
|
311
434
|
|
|
@@ -341,9 +464,19 @@ export function createFileJournalStore(path: string): JournalStore {
|
|
|
341
464
|
const r = map.get(runId);
|
|
342
465
|
return r ? cloneRun(r) : undefined;
|
|
343
466
|
},
|
|
344
|
-
async put(run) {
|
|
467
|
+
async put(run, fence) {
|
|
345
468
|
const map = await load();
|
|
346
|
-
map.
|
|
469
|
+
const existing = map.get(run.id);
|
|
470
|
+
assertJournalFence(existing, fence);
|
|
471
|
+
map.set(run.id, mergeJournalPut(existing, run));
|
|
472
|
+
await flush(map);
|
|
473
|
+
},
|
|
474
|
+
async appendEntry(runId, seq, entry, fence) {
|
|
475
|
+
const map = await load();
|
|
476
|
+
const existing = map.get(runId);
|
|
477
|
+
if (!existing) throw new Error(`journal: run "${runId}" not found`);
|
|
478
|
+
assertJournalFence(existing, fence);
|
|
479
|
+
appendStoredEntry(existing, seq, entry);
|
|
347
480
|
await flush(map);
|
|
348
481
|
},
|
|
349
482
|
async list() {
|
|
@@ -359,6 +492,20 @@ export function createFileJournalStore(path: string): JournalStore {
|
|
|
359
492
|
Object.defineProperty(this, "idempotency", { value: created });
|
|
360
493
|
return created;
|
|
361
494
|
},
|
|
495
|
+
get decisions(): DecisionLabelStore {
|
|
496
|
+
const created = loadDecisionLabelStore().createFileDecisionLabelStore(
|
|
497
|
+
`${dirname(path)}/decision-labels.json`,
|
|
498
|
+
);
|
|
499
|
+
Object.defineProperty(this, "decisions", { value: created });
|
|
500
|
+
return created;
|
|
501
|
+
},
|
|
502
|
+
get agentEvents(): import("./agent-event-store.ts").AgentEventStore {
|
|
503
|
+
const created = loadAgentEventStore().createFileAgentEventStore(
|
|
504
|
+
`${dirname(path)}/agent-events`,
|
|
505
|
+
);
|
|
506
|
+
Object.defineProperty(this, "agentEvents", { value: created });
|
|
507
|
+
return created;
|
|
508
|
+
},
|
|
362
509
|
};
|
|
363
510
|
}
|
|
364
511
|
|
|
@@ -385,6 +532,11 @@ export interface CreateJournalOptions {
|
|
|
385
532
|
* sleeping/finished run never holds a 30s lock.
|
|
386
533
|
*/
|
|
387
534
|
readonly lease?: JournalLeaseOptions;
|
|
535
|
+
/**
|
|
536
|
+
* Code version stamped on new runs. Resume of a different version fails.
|
|
537
|
+
* Defaults to the package version.
|
|
538
|
+
*/
|
|
539
|
+
readonly codeVersion?: string;
|
|
388
540
|
}
|
|
389
541
|
|
|
390
542
|
/**
|
|
@@ -432,7 +584,17 @@ export interface JournalSession {
|
|
|
432
584
|
* instead of treating the cursor as past them. Clears the in-memory undo stack
|
|
433
585
|
* so frames re-bind on the next walk.
|
|
434
586
|
*/
|
|
587
|
+
/**
|
|
588
|
+
* Increments on {@link JournalSession.rewind}. Agent run ids key off this
|
|
589
|
+
* so `flow.retry` replays the same id.
|
|
590
|
+
*/
|
|
591
|
+
readonly epoch: number;
|
|
435
592
|
rewind(): void;
|
|
593
|
+
/**
|
|
594
|
+
* Entries already replayed or appended. The row under the cursor is excluded
|
|
595
|
+
* so a replayed call does not count itself.
|
|
596
|
+
*/
|
|
597
|
+
recordedBeforeCursor(): readonly JournalEntry[];
|
|
436
598
|
/** Registered per-step undos in persist/replay order (LIFO compensate). */
|
|
437
599
|
undoStack(): readonly JournalUndoFrame[];
|
|
438
600
|
/**
|
|
@@ -471,6 +633,7 @@ export function createJournalSlot(): {
|
|
|
471
633
|
const session = slot.session;
|
|
472
634
|
if (!session) {
|
|
473
635
|
if (prop === "runId") return "";
|
|
636
|
+
if (prop === "epoch") return 0;
|
|
474
637
|
if (prop === "stampTenant") return async () => undefined;
|
|
475
638
|
if (prop === "rewind") return () => undefined;
|
|
476
639
|
return undefined;
|
|
@@ -511,24 +674,71 @@ export function createJournal(options: CreateJournalOptions): Journal {
|
|
|
511
674
|
const now = options.now ?? (() => Date.now());
|
|
512
675
|
const newId = options.id ?? (() => okid());
|
|
513
676
|
const lease = options.lease;
|
|
677
|
+
const codeVersion = options.codeVersion ?? packageVersion();
|
|
514
678
|
const coordinated = lease !== undefined && hasJournalLease(options.store);
|
|
515
679
|
|
|
516
680
|
function openSession(run: JournalRun, leased: boolean): JournalSession {
|
|
517
681
|
/** Next entry index to consume on replay. */
|
|
518
682
|
let cursor = 0;
|
|
683
|
+
let epoch = 0;
|
|
519
684
|
let leaseHeld = leased;
|
|
520
685
|
let registrationPass = false;
|
|
521
686
|
let undoExecution = false;
|
|
687
|
+
let stepDepth = 0;
|
|
688
|
+
/** Outer `effect` calls re-enter when the next row was written by a nested call. */
|
|
689
|
+
let effectDepth = 0;
|
|
522
690
|
const undos: JournalUndoFrame[] = [];
|
|
523
691
|
|
|
524
|
-
|
|
692
|
+
function fence(): JournalWriteFence | undefined {
|
|
693
|
+
if (!leaseHeld || !lease || run.leaseToken === undefined || run.lockedBy === undefined) {
|
|
694
|
+
return undefined;
|
|
695
|
+
}
|
|
696
|
+
return { lockedBy: run.lockedBy, leaseToken: run.leaseToken, now: now() };
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
async function persist(entry?: JournalEntry): Promise<void> {
|
|
525
700
|
run.updatedAt = now();
|
|
526
701
|
// Natural heartbeat — a live holder renews on every journal write.
|
|
527
702
|
if (leaseHeld && lease) {
|
|
528
703
|
run.lockedBy = lease.instanceId;
|
|
529
704
|
run.leaseExpiresAt = now() + (lease.leaseMs ?? JOURNAL_DEFAULT_LEASE_MS);
|
|
530
705
|
}
|
|
531
|
-
|
|
706
|
+
const writeFence = fence();
|
|
707
|
+
if (entry && options.store.appendEntry) {
|
|
708
|
+
try {
|
|
709
|
+
await options.store.appendEntry(run.id, run.entries.length - 1, entry, writeFence);
|
|
710
|
+
} catch (err) {
|
|
711
|
+
// The session list was edited in place (a test drops replayed rows).
|
|
712
|
+
// The stored rows no longer match, so replace them from the session.
|
|
713
|
+
if (!(err instanceof Error) || !err.message.includes("does not append")) throw err;
|
|
714
|
+
await options.store.put(cloneRun(run), writeFence);
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
const header = cloneRun(run);
|
|
719
|
+
if (options.store.appendEntry) (header as { entries: JournalEntry[] }).entries = [];
|
|
720
|
+
await options.store.put(header, writeFence);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* The next entry must match this call. Scanning forward would hide a
|
|
725
|
+
* reordered step.
|
|
726
|
+
*/
|
|
727
|
+
function takeReplay(
|
|
728
|
+
match: (entry: JournalEntry) => boolean,
|
|
729
|
+
called: string,
|
|
730
|
+
): JournalEntry | undefined {
|
|
731
|
+
if (cursor >= run.entries.length) return undefined;
|
|
732
|
+
const entry = run.entries[cursor];
|
|
733
|
+
if (entry === undefined || !match(entry)) {
|
|
734
|
+
throwOke("JOURNAL_REPLAY_DIVERGENCE", {
|
|
735
|
+
runId: run.id,
|
|
736
|
+
expected: entry === undefined ? "end" : entryLabel(entry),
|
|
737
|
+
actual: called,
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
cursor += 1;
|
|
741
|
+
return entry;
|
|
532
742
|
}
|
|
533
743
|
|
|
534
744
|
/** Parking / terminal states must not hold a short lease across days. */
|
|
@@ -573,47 +783,51 @@ export function createJournal(options: CreateJournalOptions): Journal {
|
|
|
573
783
|
if (name.startsWith(JOURNAL_UNDO_PREFIX) && opts?.undo) {
|
|
574
784
|
throw new Error("journal: undo steps cannot register nested undo");
|
|
575
785
|
}
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
const
|
|
579
|
-
if (
|
|
580
|
-
|
|
581
|
-
if (!name.startsWith(JOURNAL_UNDO_PREFIX)) {
|
|
582
|
-
registerUndo(name, e.value as T, opts);
|
|
583
|
-
}
|
|
584
|
-
return e.value as T;
|
|
786
|
+
const replayed = takeReplay((e) => e.kind === "step" && e.name === name, `step ${name}`);
|
|
787
|
+
if (replayed && replayed.kind === "step") {
|
|
788
|
+
const value = reviveJournalValue(replayed.value) as T;
|
|
789
|
+
if (!name.startsWith(JOURNAL_UNDO_PREFIX)) {
|
|
790
|
+
registerUndo(name, value, opts);
|
|
585
791
|
}
|
|
792
|
+
return value;
|
|
586
793
|
}
|
|
587
794
|
assertCanAppendStep(name);
|
|
588
|
-
|
|
795
|
+
stepDepth += 1;
|
|
796
|
+
let stored: unknown;
|
|
797
|
+
try {
|
|
798
|
+
stored = await journalValue(await fn());
|
|
799
|
+
} finally {
|
|
800
|
+
stepDepth -= 1;
|
|
801
|
+
}
|
|
802
|
+
const value = reviveJournalValue(stored) as T;
|
|
589
803
|
const entry: JournalStepEntry = {
|
|
590
804
|
kind: "step",
|
|
591
805
|
name,
|
|
592
|
-
value,
|
|
806
|
+
value: stored,
|
|
593
807
|
at: now(),
|
|
594
808
|
};
|
|
595
809
|
run.entries.push(entry);
|
|
596
810
|
cursor = run.entries.length;
|
|
597
|
-
await persist();
|
|
811
|
+
await persist(entry);
|
|
598
812
|
if (!name.startsWith(JOURNAL_UNDO_PREFIX)) {
|
|
599
813
|
registerUndo(name, value, opts);
|
|
600
814
|
}
|
|
601
815
|
return value;
|
|
602
816
|
},
|
|
603
817
|
async sleep(label, duration, parseMs) {
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
return;
|
|
818
|
+
const replayed = takeReplay(
|
|
819
|
+
(e) => e.kind === "sleep" && e.label === label,
|
|
820
|
+
`sleep ${label}`,
|
|
821
|
+
);
|
|
822
|
+
if (replayed && replayed.kind === "sleep") {
|
|
823
|
+
if (now() < replayed.wakeAt) {
|
|
824
|
+
run.status = "sleeping";
|
|
825
|
+
run.wakeAt = replayed.wakeAt;
|
|
826
|
+
releaseLeaseLocally();
|
|
827
|
+
await persist();
|
|
828
|
+
throw new JournalSuspend(label, replayed.wakeAt);
|
|
616
829
|
}
|
|
830
|
+
return;
|
|
617
831
|
}
|
|
618
832
|
if (registrationPass) {
|
|
619
833
|
throw new JournalRegistrationComplete();
|
|
@@ -632,42 +846,84 @@ export function createJournal(options: CreateJournalOptions): Journal {
|
|
|
632
846
|
run.status = "sleeping";
|
|
633
847
|
run.wakeAt = wakeAt;
|
|
634
848
|
releaseLeaseLocally();
|
|
635
|
-
await persist();
|
|
849
|
+
await persist(entry);
|
|
636
850
|
throw new JournalSuspend(label, wakeAt);
|
|
637
851
|
}
|
|
638
|
-
await persist();
|
|
852
|
+
await persist(entry);
|
|
639
853
|
},
|
|
640
854
|
async effect<T>(
|
|
641
855
|
effectKind: string,
|
|
642
856
|
resource: string,
|
|
643
857
|
execute: () => T | Promise<T>,
|
|
644
858
|
): Promise<T> {
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
859
|
+
// Inside fx.step the step value is the snapshot. A nested effect must
|
|
860
|
+
// not insert a row ahead of that step, or replay would diverge.
|
|
861
|
+
if (stepDepth > 0) return execute();
|
|
862
|
+
const called = `effect ${effectKind} ${resource}`;
|
|
863
|
+
const next = cursor < run.entries.length ? run.entries[cursor] : undefined;
|
|
864
|
+
const matches =
|
|
865
|
+
next?.kind === "effect" && next.effectKind === effectKind && next.resource === resource;
|
|
866
|
+
if (matches && next.kind === "effect") {
|
|
867
|
+
cursor += 1;
|
|
868
|
+
return reviveJournalValue(next.value) as T;
|
|
869
|
+
}
|
|
870
|
+
// A nested call must hit its own row. An outer call re-enters: its row
|
|
871
|
+
// is appended after the nested rows, so it is not next yet.
|
|
872
|
+
if (next !== undefined && effectDepth > 0) {
|
|
873
|
+
throwOke("JOURNAL_REPLAY_DIVERGENCE", {
|
|
874
|
+
runId: run.id,
|
|
875
|
+
expected: entryLabel(next),
|
|
876
|
+
actual: called,
|
|
877
|
+
});
|
|
651
878
|
}
|
|
652
879
|
if (registrationPass) {
|
|
653
880
|
throw new JournalRegistrationComplete();
|
|
654
881
|
}
|
|
655
|
-
|
|
882
|
+
effectDepth += 1;
|
|
883
|
+
let stored: unknown;
|
|
884
|
+
try {
|
|
885
|
+
stored = await journalValue(await execute());
|
|
886
|
+
} finally {
|
|
887
|
+
effectDepth -= 1;
|
|
888
|
+
}
|
|
889
|
+
const after = cursor < run.entries.length ? run.entries[cursor] : undefined;
|
|
890
|
+
if (
|
|
891
|
+
after?.kind === "effect" &&
|
|
892
|
+
after.effectKind === effectKind &&
|
|
893
|
+
after.resource === resource
|
|
894
|
+
) {
|
|
895
|
+
cursor += 1;
|
|
896
|
+
return reviveJournalValue(after.value) as T;
|
|
897
|
+
}
|
|
898
|
+
if (after !== undefined) {
|
|
899
|
+
throwOke("JOURNAL_REPLAY_DIVERGENCE", {
|
|
900
|
+
runId: run.id,
|
|
901
|
+
expected: entryLabel(after),
|
|
902
|
+
actual: called,
|
|
903
|
+
});
|
|
904
|
+
}
|
|
656
905
|
const entry: JournalEffectEntry = {
|
|
657
906
|
kind: "effect",
|
|
658
907
|
effectKind,
|
|
659
908
|
resource,
|
|
660
|
-
value,
|
|
909
|
+
value: stored,
|
|
661
910
|
at: now(),
|
|
662
911
|
};
|
|
663
912
|
run.entries.push(entry);
|
|
664
913
|
cursor = run.entries.length;
|
|
665
|
-
await persist();
|
|
666
|
-
return
|
|
914
|
+
await persist(entry);
|
|
915
|
+
return reviveJournalValue(stored) as T;
|
|
916
|
+
},
|
|
917
|
+
get epoch() {
|
|
918
|
+
return epoch;
|
|
667
919
|
},
|
|
668
920
|
rewind() {
|
|
669
921
|
cursor = 0;
|
|
670
922
|
undos.length = 0;
|
|
923
|
+
epoch += 1;
|
|
924
|
+
},
|
|
925
|
+
recordedBeforeCursor() {
|
|
926
|
+
return run.entries.slice(0, cursor);
|
|
671
927
|
},
|
|
672
928
|
undoStack() {
|
|
673
929
|
return undos;
|
|
@@ -709,6 +965,7 @@ export function createJournal(options: CreateJournalOptions): Journal {
|
|
|
709
965
|
input,
|
|
710
966
|
status: "running",
|
|
711
967
|
entries: [],
|
|
968
|
+
codeVersion,
|
|
712
969
|
createdAt: t,
|
|
713
970
|
updatedAt: t,
|
|
714
971
|
};
|
|
@@ -716,6 +973,7 @@ export function createJournal(options: CreateJournalOptions): Journal {
|
|
|
716
973
|
// Fresh id — insert already holding the lease (no claim race).
|
|
717
974
|
run.lockedBy = lease.instanceId;
|
|
718
975
|
run.leaseExpiresAt = t + (lease.leaseMs ?? JOURNAL_DEFAULT_LEASE_MS);
|
|
976
|
+
run.leaseToken = 1;
|
|
719
977
|
}
|
|
720
978
|
await options.store.put(cloneRun(run));
|
|
721
979
|
return openSession(run, coordinated);
|
|
@@ -740,6 +998,16 @@ export function createJournal(options: CreateJournalOptions): Journal {
|
|
|
740
998
|
}
|
|
741
999
|
throw new Error(`journal: run "${runId}" not found`);
|
|
742
1000
|
}
|
|
1001
|
+
if (run.codeVersion !== undefined && run.codeVersion !== codeVersion) {
|
|
1002
|
+
if (coordinated && lease) {
|
|
1003
|
+
await options.store.releaseLease!(runId, lease.instanceId);
|
|
1004
|
+
}
|
|
1005
|
+
throwOke("JOURNAL_CODE_VERSION", {
|
|
1006
|
+
runId,
|
|
1007
|
+
expected: run.codeVersion,
|
|
1008
|
+
actual: codeVersion,
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
743
1011
|
// Leave status intact — the durable runner parks or continues.
|
|
744
1012
|
run.updatedAt = now();
|
|
745
1013
|
if (coordinated && lease) {
|
|
@@ -755,3 +1023,155 @@ export function createJournal(options: CreateJournalOptions): Journal {
|
|
|
755
1023
|
function cloneRun(run: JournalRun): JournalRun {
|
|
756
1024
|
return structuredClone(run);
|
|
757
1025
|
}
|
|
1026
|
+
|
|
1027
|
+
/**
|
|
1028
|
+
* Bump the fencing token unless this instance already holds a live lease.
|
|
1029
|
+
*
|
|
1030
|
+
* @param run - Run being claimed
|
|
1031
|
+
* @param instanceId - Claimant
|
|
1032
|
+
* @param now - Epoch-ms
|
|
1033
|
+
* @param leaseMs - Lease duration
|
|
1034
|
+
*/
|
|
1035
|
+
function holdLease(run: JournalRun, instanceId: string, now: number, leaseMs: number): void {
|
|
1036
|
+
const renew =
|
|
1037
|
+
run.lockedBy === instanceId && hasLiveLease(run, now) && run.leaseToken !== undefined;
|
|
1038
|
+
if (!renew) run.leaseToken = (run.leaseToken ?? 0) + 1;
|
|
1039
|
+
run.lockedBy = instanceId;
|
|
1040
|
+
const nextExpiry = now + leaseMs;
|
|
1041
|
+
if (run.leaseExpiresAt === undefined || run.leaseExpiresAt < nextExpiry) {
|
|
1042
|
+
run.leaseExpiresAt = nextExpiry;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
/**
|
|
1047
|
+
* Reject a write whose lease token is no longer current.
|
|
1048
|
+
*
|
|
1049
|
+
* @param existing - Stored run, if any
|
|
1050
|
+
* @param fence - Caller fence. Omitted on insert and uncoordinated stores.
|
|
1051
|
+
*/
|
|
1052
|
+
function assertJournalFence(
|
|
1053
|
+
existing: JournalRun | undefined,
|
|
1054
|
+
fence: JournalWriteFence | undefined,
|
|
1055
|
+
): void {
|
|
1056
|
+
if (!fence || !existing) return;
|
|
1057
|
+
const expired = existing.leaseExpiresAt !== undefined && existing.leaseExpiresAt <= fence.now;
|
|
1058
|
+
if (existing.lockedBy !== fence.lockedBy || existing.leaseToken !== fence.leaseToken || expired) {
|
|
1059
|
+
throwOke("JOURNAL_STALE_LEASE", { runId: existing.id });
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
/**
|
|
1064
|
+
* Header replace that keeps appended entries when the snapshot omitted them.
|
|
1065
|
+
*
|
|
1066
|
+
* @param existing - Previous row
|
|
1067
|
+
* @param incoming - Header snapshot
|
|
1068
|
+
*/
|
|
1069
|
+
function mergeJournalPut(existing: JournalRun | undefined, incoming: JournalRun): JournalRun {
|
|
1070
|
+
const next = cloneRun(incoming);
|
|
1071
|
+
if (existing && incoming.entries.length === 0 && existing.entries.length > 0) {
|
|
1072
|
+
(next as { entries: JournalEntry[] }).entries = existing.entries;
|
|
1073
|
+
}
|
|
1074
|
+
return next;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
/**
|
|
1078
|
+
* Append one entry at `seq`, or no-op when that seq is already stored.
|
|
1079
|
+
*
|
|
1080
|
+
* @param run - Stored run
|
|
1081
|
+
* @param seq - Zero-based position
|
|
1082
|
+
* @param entry - JSON-safe entry
|
|
1083
|
+
*/
|
|
1084
|
+
function appendStoredEntry(run: JournalRun, seq: number, entry: JournalEntry): void {
|
|
1085
|
+
const entries = run.entries as JournalEntry[];
|
|
1086
|
+
if (entries.length === seq) {
|
|
1087
|
+
entries.push(structuredClone(entry));
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
if (entries.length === seq + 1) return;
|
|
1091
|
+
throw new Error(`journal: entry seq ${seq} does not append (have ${entries.length})`);
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
/**
|
|
1095
|
+
* JSON snapshot of a journaled value. `Response` becomes a marked object.
|
|
1096
|
+
*
|
|
1097
|
+
* @param value - Step or effect result
|
|
1098
|
+
*/
|
|
1099
|
+
async function journalValue(value: unknown): Promise<unknown> {
|
|
1100
|
+
if (typeof Response !== "undefined" && value instanceof Response) {
|
|
1101
|
+
const bytes = new Uint8Array(await value.arrayBuffer());
|
|
1102
|
+
let body = "";
|
|
1103
|
+
for (const byte of bytes) body += String.fromCharCode(byte);
|
|
1104
|
+
const headers: Record<string, string> = {};
|
|
1105
|
+
value.headers.forEach((header, name) => {
|
|
1106
|
+
headers[name] = header;
|
|
1107
|
+
});
|
|
1108
|
+
return {
|
|
1109
|
+
__oke: "response",
|
|
1110
|
+
status: value.status,
|
|
1111
|
+
statusText: value.statusText,
|
|
1112
|
+
headers,
|
|
1113
|
+
body: btoa(body),
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
if (typeof value === "bigint" || typeof value === "function" || typeof value === "symbol") {
|
|
1117
|
+
throwOke("JOURNAL_VALUE_NOT_JSON", { detail: typeof value });
|
|
1118
|
+
}
|
|
1119
|
+
try {
|
|
1120
|
+
return JSON.parse(JSON.stringify(value ?? null)) as unknown;
|
|
1121
|
+
} catch {
|
|
1122
|
+
throwOke("JOURNAL_VALUE_NOT_JSON", { detail: "unserializable" });
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
/**
|
|
1127
|
+
* Rebuild a `Response` from a journal snapshot. Other values pass through.
|
|
1128
|
+
*
|
|
1129
|
+
* @param value - Stored entry value
|
|
1130
|
+
*/
|
|
1131
|
+
function reviveJournalValue(value: unknown): unknown {
|
|
1132
|
+
if (!value || typeof value !== "object") return value;
|
|
1133
|
+
const record = value as {
|
|
1134
|
+
__oke?: string;
|
|
1135
|
+
status?: number;
|
|
1136
|
+
statusText?: string;
|
|
1137
|
+
headers?: Record<string, string>;
|
|
1138
|
+
body?: string;
|
|
1139
|
+
};
|
|
1140
|
+
if (record.__oke !== "response" || typeof record.body !== "string") return value;
|
|
1141
|
+
const binary = atob(record.body);
|
|
1142
|
+
const bytes = new Uint8Array(binary.length);
|
|
1143
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
1144
|
+
return new Response(bytes, {
|
|
1145
|
+
status: record.status ?? 200,
|
|
1146
|
+
statusText: record.statusText,
|
|
1147
|
+
headers: record.headers,
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
/**
|
|
1152
|
+
* Short label for a journal entry, used in replay errors.
|
|
1153
|
+
*
|
|
1154
|
+
* @param entry - Stored entry
|
|
1155
|
+
*/
|
|
1156
|
+
function entryLabel(entry: JournalEntry): string {
|
|
1157
|
+
if (entry.kind === "step") return `step ${entry.name}`;
|
|
1158
|
+
if (entry.kind === "sleep") return `sleep ${entry.label}`;
|
|
1159
|
+
return `effect ${entry.effectKind} ${entry.resource}`;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
let cachedPackageVersion: string | undefined;
|
|
1163
|
+
|
|
1164
|
+
/**
|
|
1165
|
+
* Package version used when a journal is not given an explicit code version.
|
|
1166
|
+
*/
|
|
1167
|
+
function packageVersion(): string {
|
|
1168
|
+
if (cachedPackageVersion !== undefined) return cachedPackageVersion;
|
|
1169
|
+
try {
|
|
1170
|
+
const raw = readFileSync(new URL("../../package.json", import.meta.url), "utf8");
|
|
1171
|
+
const parsed = JSON.parse(raw) as { version?: string };
|
|
1172
|
+
cachedPackageVersion = parsed.version && parsed.version.length > 0 ? parsed.version : "0";
|
|
1173
|
+
} catch {
|
|
1174
|
+
cachedPackageVersion = "0";
|
|
1175
|
+
}
|
|
1176
|
+
return cachedPackageVersion;
|
|
1177
|
+
}
|