akm-cli 0.9.1 → 0.9.2-alpha.2
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/CHANGELOG.md +103 -28
- package/README.md +3 -1
- package/SECURITY.md +1 -1
- package/STABILITY.md +1 -1
- package/dist/akm +2 -2
- package/dist/akm-migrate +2 -2
- package/dist/assets/hints/cli-hints-full.md +14 -9
- package/dist/assets/improve-strategies/proactive-maintenance.json +1 -1
- package/dist/assets/improve-strategies/reflect-distill.json +1 -1
- package/dist/assets/models.json +35 -0
- package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +3 -4
- package/dist/assets/stash-skeleton/facts/conventions/organization.md +1 -3
- package/dist/assets/tasks/core/extract.yml +6 -5
- package/dist/assets/tasks/core/improve.yml +6 -5
- package/dist/assets/tasks/core/index-refresh.yml +6 -5
- package/dist/assets/tasks/core/sync.yml +6 -5
- package/dist/assets/tasks/core/version-check.yml +6 -5
- package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-catchup.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-consolidate.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-frequent.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-nightly.yml +6 -5
- package/dist/cli/confirm.js +2 -2
- package/dist/cli/parse-args.js +3 -24
- package/dist/cli/retired-commands.js +1 -1
- package/dist/cli/shared.js +2 -2
- package/dist/cli.js +11 -9
- package/dist/commands/agent/agent-dispatch.js +55 -89
- package/dist/commands/agent/contribute-cli.js +12 -45
- package/dist/commands/command/builtin-action.js +32 -0
- package/dist/commands/command/command-cli.js +99 -0
- package/dist/commands/command/command-execution.js +308 -0
- package/dist/commands/command/execution-source-loader.js +176 -0
- package/dist/commands/command/portable-template.js +60 -0
- package/dist/commands/config-cli.js +10 -4
- package/dist/commands/env/env.js +4 -2
- package/dist/commands/feedback-cli.js +1 -1
- package/dist/commands/health/checks.js +241 -29
- package/dist/commands/health/html-report.js +0 -14
- package/dist/commands/health/report-view-model.js +0 -1
- package/dist/commands/health/surfaces.js +6 -7
- package/dist/commands/health/types.js +0 -2
- package/dist/commands/health.js +63 -18
- package/dist/commands/improve/collapse-detector.js +5 -6
- package/dist/commands/improve/consolidate.js +251 -214
- package/dist/commands/improve/distill/promote-memory.js +71 -34
- package/dist/commands/improve/distill/quality-gate.js +17 -5
- package/dist/commands/improve/distill.js +232 -155
- package/dist/commands/improve/eligibility.js +112 -79
- package/dist/commands/improve/execution.js +57 -0
- package/dist/commands/improve/extract-cli.js +5 -5
- package/dist/commands/improve/extract-prompt.js +64 -22
- package/dist/commands/improve/extract.js +608 -360
- package/dist/commands/improve/improve-strategies.js +43 -14
- package/dist/commands/improve/improve.js +249 -29
- package/dist/commands/improve/loop-stages.js +11 -17
- package/dist/commands/improve/memory/memory-contradiction-detect.js +90 -66
- package/dist/commands/improve/outcome-loop.js +22 -38
- package/dist/commands/improve/planner.js +134 -0
- package/dist/commands/improve/preparation.js +730 -409
- package/dist/commands/improve/reflect.js +386 -223
- package/dist/commands/improve/run-context.js +3 -4
- package/dist/commands/improve/salience.js +6 -58
- package/dist/commands/improve/session-asset.js +12 -12
- package/dist/commands/lint/index.js +101 -29
- package/dist/commands/migrate-cli.js +11 -69
- package/dist/commands/migration-tool.js +6 -9
- package/dist/commands/models-cli.js +27 -0
- package/dist/commands/proposal/drain.js +258 -186
- package/dist/commands/proposal/proposal-cli.js +32 -10
- package/dist/commands/proposal/proposal.js +2 -5
- package/dist/commands/proposal/propose.js +192 -172
- package/dist/commands/proposal/repository.js +54 -91
- package/dist/commands/proposal/validators/proposal-validators.js +9 -7
- package/dist/commands/read/curate.js +53 -22
- package/dist/commands/read/registry-search.js +25 -9
- package/dist/commands/read/remember-cli.js +14 -2
- package/dist/commands/read/search.js +10 -4
- package/dist/commands/read/show.js +139 -153
- package/dist/commands/registry-cli.js +16 -7
- package/dist/commands/remember.js +33 -18
- package/dist/commands/sources/add-cli.js +19 -178
- package/dist/commands/sources/bundle-cli.js +15 -3
- package/dist/commands/sources/dangerous-env-audit.js +135 -0
- package/dist/commands/sources/info.js +2 -1
- package/dist/commands/sources/installed-stashes.js +901 -177
- package/dist/commands/sources/schema-repair.js +174 -95
- package/dist/commands/sources/self-update.js +30 -74
- package/dist/commands/sources/source-add.js +3 -5
- package/dist/commands/sources/sources-cli.js +2 -15
- package/dist/commands/sources/update-transaction.js +220 -0
- package/dist/commands/tasks/tasks-cli.js +3 -3
- package/dist/commands/tasks/tasks.js +736 -317
- package/dist/commands/workflow-cli.js +2 -2
- package/dist/core/adapter/adapters/agent-skills-adapter.js +3 -0
- package/dist/core/adapter/adapters/akm-adapter.js +85 -35
- package/dist/core/adapter/adapters/akm-lint.js +54 -39
- package/dist/core/adapter/adapters/akm-metadata.js +45 -45
- package/dist/core/adapter/adapters/akm-task-adapter.js +32 -49
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +38 -23
- package/dist/core/adapter/adapters/dotenv-adapter.js +30 -1
- package/dist/core/adapter/adapters/generic-files-adapter.js +11 -0
- package/dist/core/adapter/adapters/index.js +0 -9
- package/dist/core/adapter/adapters/llm-wiki-adapter.js +4 -0
- package/dist/core/adapter/adapters/okf-adapter.js +4 -0
- package/dist/core/adapter/adapters/opencode-adapter.js +5 -8
- package/dist/core/adapter/adapters/tool-dir-shared.js +63 -6
- package/dist/core/adapter/adapters/website-snapshot-adapter.js +4 -0
- package/dist/core/adapter/execution-source.js +308 -0
- package/dist/core/adapter/recognize-match.js +36 -13
- package/dist/core/adapter/registry.js +0 -9
- package/dist/core/asset/stash-meta.js +94 -4
- package/dist/core/common.js +6 -11
- package/dist/core/config/config-io.js +3 -3
- package/dist/core/config/config-schema.js +18 -40
- package/dist/core/config/config-sources.js +11 -21
- package/dist/core/config/config-walker.js +31 -13
- package/dist/core/config/config.js +23 -26
- package/dist/core/config/schema/engines.js +8 -7
- package/dist/core/config/schema/improve-processes.js +29 -5
- package/dist/core/config/schema/index-config.js +0 -27
- package/dist/core/config/schema/primitives.js +1 -23
- package/dist/core/config/schema/sources-bundles.js +13 -16
- package/dist/core/errors.js +2 -0
- package/dist/core/events.js +68 -32
- package/dist/core/extra-params.js +1 -0
- package/dist/core/improve-result.js +315 -0
- package/dist/core/lesson-lint.js +0 -6
- package/dist/core/maintenance-barrier.js +4 -4
- package/dist/core/network-policy.js +152 -0
- package/dist/core/paths.js +1 -1
- package/dist/core/recognition-util.js +4 -4
- package/dist/core/registry-url.js +456 -0
- package/dist/core/state/migrations.js +161 -47
- package/dist/core/state-db.js +453 -80
- package/dist/core/system-error.js +32 -0
- package/dist/core/time.js +2 -12
- package/dist/core/write-source.js +0 -18
- package/dist/execution/directory-identity.js +52 -0
- package/dist/execution/executable-identity.js +107 -0
- package/dist/execution/guarded-source.js +398 -0
- package/dist/execution/json.js +95 -0
- package/dist/{commands/health/types-session-log.js → execution/limits.js} +2 -1
- package/dist/execution/record.js +55 -0
- package/dist/execution/resolved-request.js +730 -0
- package/dist/execution/source.js +320 -0
- package/dist/indexer/bundle-identity-guard.js +5 -4
- package/dist/indexer/db/graph-db.js +33 -0
- package/dist/indexer/graph/graph-boost.js +3 -4
- package/dist/indexer/graph/graph-extraction.js +562 -373
- package/dist/indexer/index-written-assets.js +78 -39
- package/dist/indexer/indexer.js +471 -432
- package/dist/indexer/installations.js +6 -0
- package/dist/indexer/lookup/adapter-concept-owner.js +283 -0
- package/dist/indexer/materialize-embeddings.js +155 -0
- package/dist/indexer/passes/memory-inference.js +227 -174
- package/dist/indexer/passes/metadata.js +263 -118
- package/dist/indexer/scan/doc-to-entry.js +7 -10
- package/dist/indexer/scan/drain-dir.js +51 -23
- package/dist/indexer/search/db-search.js +156 -50
- package/dist/indexer/search/fts-query.js +40 -40
- package/dist/indexer/search/ranking.js +36 -1
- package/dist/indexer/search/search-attribution.js +3 -1
- package/dist/indexer/search/search-fields.js +23 -14
- package/dist/indexer/search/search-hit-enrichers.js +1 -1
- package/dist/indexer/search/search-source.js +7 -16
- package/dist/indexer/search/semantic-status.js +10 -1
- package/dist/indexer/usage/show-usage.js +105 -0
- package/dist/indexer/usage/usage-events.js +7 -2
- package/dist/indexer/walk/matchers.js +40 -10
- package/dist/indexer/walk/path-resolver.js +5 -2
- package/dist/indexer/walk/walker.js +20 -2
- package/dist/integrations/agent/builder-shared.js +3 -6
- package/dist/integrations/agent/conversation-fallback.js +16 -0
- package/dist/integrations/agent/engine-resolution.js +87 -87
- package/dist/integrations/agent/execution-cascade.js +566 -0
- package/dist/integrations/agent/execution-definitions.js +211 -0
- package/dist/integrations/agent/execution-lowering.js +811 -0
- package/dist/integrations/agent/execution-preparation.js +67 -0
- package/dist/integrations/agent/index.js +0 -2
- package/dist/integrations/agent/inline-execution.js +74 -0
- package/dist/integrations/agent/model-map.js +515 -0
- package/dist/integrations/agent/persona-fallback.js +30 -0
- package/dist/integrations/agent/request-lowering.js +186 -0
- package/dist/integrations/agent/runner-dispatch.js +230 -37
- package/dist/integrations/agent/runner.js +12 -83
- package/dist/integrations/harnesses/aider/agent-builder.js +8 -0
- package/dist/integrations/harnesses/aider/index.js +0 -1
- package/dist/integrations/harnesses/amazonq/agent-builder.js +8 -0
- package/dist/integrations/harnesses/amazonq/index.js +0 -1
- package/dist/integrations/harnesses/claude/agent-builder.js +14 -1
- package/dist/integrations/harnesses/claude/index.js +1 -5
- package/dist/integrations/harnesses/claude/session-log.js +3 -33
- package/dist/integrations/harnesses/codex/agent-builder.js +8 -0
- package/dist/integrations/harnesses/codex/index.js +0 -1
- package/dist/integrations/harnesses/copilot/agent-builder.js +8 -0
- package/dist/integrations/harnesses/copilot/index.js +0 -1
- package/dist/integrations/harnesses/gemini/agent-builder.js +8 -0
- package/dist/integrations/harnesses/gemini/index.js +0 -1
- package/dist/integrations/harnesses/index.js +4 -44
- package/dist/integrations/harnesses/opencode/agent-builder.js +16 -9
- package/dist/integrations/harnesses/opencode/index.js +0 -2
- package/dist/integrations/harnesses/opencode/session-log.js +14 -204
- package/dist/integrations/harnesses/opencode-sdk/harness.js +12 -1
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +40 -42
- package/dist/integrations/harnesses/openhands/agent-builder.js +8 -0
- package/dist/integrations/harnesses/openhands/index.js +0 -1
- package/dist/integrations/harnesses/pi/agent-builder.js +8 -0
- package/dist/integrations/harnesses/pi/index.js +0 -1
- package/dist/integrations/harnesses/shared.js +0 -1
- package/dist/integrations/harnesses/types.js +1 -3
- package/dist/integrations/lockfile.js +82 -79
- package/dist/integrations/session-logs/index.js +6 -17
- package/dist/integrations/session-logs/provider-base.js +1 -29
- package/dist/llm/client.js +10 -5
- package/dist/llm/embedder.js +6 -7
- package/dist/llm/embedders/local.js +37 -88
- package/dist/llm/embedders/types.js +1 -1
- package/dist/llm/graph-extract.js +75 -50
- package/dist/llm/index-passes.js +43 -5
- package/dist/llm/memory-infer.js +8 -6
- package/dist/llm/metadata-enhance.js +5 -3
- package/dist/llm/structured-call.js +122 -25
- package/dist/output/format-exempt.js +1 -1
- package/dist/output/render-registry.js +0 -16
- package/dist/output/renderers.js +12 -7
- package/dist/output/shapes/curate.js +1 -0
- package/dist/output/shapes/helpers.js +10 -2
- package/dist/output/shapes/passthrough.js +2 -0
- package/dist/output/text/command-format.js +31 -33
- package/dist/output/text/health-format.js +1 -29
- package/dist/output/text/migrate.js +6 -56
- package/dist/output/text/proposal-format.js +16 -1
- package/dist/output/text/workflow-format.js +16 -0
- package/dist/registry/network.js +279 -0
- package/dist/registry/pinned-request-helper.js +247 -0
- package/dist/registry/pinned-transport.js +717 -0
- package/dist/registry/providers/skills-sh.js +18 -6
- package/dist/registry/providers/static-index.js +20 -7
- package/dist/registry/resolve.js +53 -28
- package/dist/scripts/akm-migrate-node.js +19334 -52269
- package/dist/scripts/akm-migrate.js +19270 -51612
- package/dist/setup/registry-stash-loader.js +64 -20
- package/dist/setup/semantic-assets.js +9 -34
- package/dist/setup/setup.js +12 -30
- package/dist/setup/source-identity.js +17 -0
- package/dist/setup/steps/sources.js +36 -15
- package/dist/setup/steps/tasks.js +39 -11
- package/dist/sources/providers/git-provider.js +3 -3
- package/dist/sources/providers/npm.js +2 -2
- package/dist/sources/providers/provider-utils.js +4 -3
- package/dist/sources/providers/website.js +11 -7
- package/dist/sources/snapshot-fetchers/host-guard.js +9 -136
- package/dist/sources/snapshot-fetchers/website-ingest.js +25 -109
- package/dist/sources/website-url.js +73 -0
- package/dist/storage/engines/sqlite-migrations.js +81 -26
- package/dist/storage/managed-db.js +27 -24
- package/dist/storage/repositories/events-repository.js +3 -0
- package/dist/storage/repositories/index-connection.js +42 -10
- package/dist/storage/repositories/index-entries-repository.js +203 -229
- package/dist/storage/repositories/index-entry-mapper.js +8 -12
- package/dist/storage/repositories/index-entry-schema.js +255 -0
- package/dist/storage/repositories/index-fts-repository.js +64 -71
- package/dist/storage/repositories/index-llm-cache-repository.js +8 -13
- package/dist/storage/repositories/index-meta-repository.js +0 -11
- package/dist/storage/repositories/index-schema.js +74 -350
- package/dist/storage/repositories/index-utility-repository.js +12 -17
- package/dist/storage/repositories/index-vec-repository.js +56 -7
- package/dist/storage/repositories/proposals-repository.js +4 -127
- package/dist/storage/repositories/registry-cache.js +2 -1
- package/dist/storage/repositories/task-history-repository.js +20 -40
- package/dist/storage/repositories/workflow-runs-repository.js +228 -129
- package/dist/storage/sqlite-read-snapshot.js +148 -0
- package/dist/tasks/backends/cron.js +170 -42
- package/dist/tasks/backends/index.js +1 -1
- package/dist/tasks/backends/launchd.js +787 -202
- package/dist/tasks/backends/schtasks.js +282 -83
- package/dist/tasks/embedded.js +7 -7
- package/dist/tasks/frozen-script.js +50 -0
- package/dist/tasks/resolve-akm-bin.js +5 -1
- package/dist/tasks/runner.js +239 -251
- package/dist/tasks/runtime-v3.js +281 -0
- package/dist/tasks/scheduler-binding.js +272 -0
- package/dist/tasks/scheduler-invocation.js +57 -43
- package/dist/tasks/scheduler-sync.js +654 -0
- package/dist/tasks/source-v3.js +752 -0
- package/dist/tasks/standalone-script-entry.js +5 -0
- package/dist/tasks/task-id.js +29 -0
- package/dist/workflows/authoring/authoring.js +15 -32
- package/dist/workflows/exec/dispatch-redaction.js +14 -8
- package/dist/workflows/exec/exec-unit.js +7 -28
- package/dist/workflows/exec/frozen-judge.js +57 -89
- package/dist/workflows/exec/lowering-notices.js +23 -0
- package/dist/workflows/exec/native-executor.js +301 -458
- package/dist/workflows/exec/param-secrets.js +4 -3
- package/dist/workflows/exec/run-workflow.js +26 -32
- package/dist/workflows/exec/step-work.js +105 -109
- package/dist/workflows/exec/unit-dispatch.js +103 -27
- package/dist/workflows/exec/unit-writer.js +3 -3
- package/dist/workflows/exec/worktree.js +2 -2
- package/dist/workflows/ir/compile.js +86 -72
- package/dist/workflows/ir/environment-v4.js +328 -0
- package/dist/workflows/ir/freeze-v4.js +122 -0
- package/dist/workflows/ir/plan-hash.js +13 -7
- package/dist/workflows/ir/schema-v4.js +525 -0
- package/dist/workflows/ir/schema.js +25 -284
- package/dist/workflows/ir/source-freeze-v4.js +506 -0
- package/dist/workflows/parser.js +27 -24
- package/dist/workflows/program/schema.js +1 -2
- package/dist/workflows/renderer.js +42 -29
- package/dist/workflows/resource-limits.js +4 -5
- package/dist/workflows/runtime/agent-identity.js +11 -13
- package/dist/workflows/runtime/plan-classifier.js +8 -8
- package/dist/workflows/runtime/runs.js +27 -43
- package/dist/workflows/runtime/workflow-asset-loader.js +45 -205
- package/dist/workflows/source-files.js +373 -0
- package/dist/workflows/source-ir/compile.js +196 -0
- package/dist/workflows/source-ir/github-yaml.js +577 -0
- package/dist/workflows/source-ir/ordering.js +38 -0
- package/dist/workflows/source-ir/program.js +50 -0
- package/dist/workflows/source-ir/result.js +26 -0
- package/dist/workflows/source-ir/schema.js +772 -0
- package/dist/workflows/source-ir/semantics.js +242 -0
- package/dist/workflows/source-ir/uses.js +14 -0
- package/docs/README.md +2 -0
- package/docs/migration/README.md +3 -1
- package/docs/migration/release-notes/0.9.2.md +55 -0
- package/docs/migration/release-notes/README.md +5 -0
- package/docs/migration/v0.8-to-v0.9.md +76 -1077
- package/docs/migration/v0.9.0-troubleshooting.md +104 -516
- package/docs/migration/v0.9.1-to-v0.9.2.md +150 -0
- package/docs/reference/README.md +1 -0
- package/docs/reference/cli.md +230 -98
- package/docs/reference/configuration.md +159 -36
- package/docs/reference/data-and-telemetry.md +19 -1
- package/docs/reference/supported-formats.md +23 -3
- package/docs/reference/tasks.md +182 -0
- package/docs/reference/workflow-schema.md +91 -40
- package/docs/reference/workflows.md +33 -6
- package/package.json +10 -6
- package/schemas/akm-config.json +372 -224
- package/schemas/akm-task.json +324 -80
- package/schemas/akm-workflow.json +6 -9
- package/dist/core/migration-operation.js +0 -75
- package/dist/integrations/agent/model-aliases.js +0 -74
- package/dist/tasks/parser.js +0 -380
- package/dist/tasks/schema.js +0 -123
- package/dist/tasks/validator.js +0 -80
- package/dist/workflows/ir/freeze.js +0 -320
- package/dist/workflows/runtime/document-cache.js +0 -13
package/dist/core/state-db.js
CHANGED
|
@@ -12,9 +12,8 @@
|
|
|
12
12
|
* importers reference those modules directly. The migration engine
|
|
13
13
|
* lives in `./state/migrations`.
|
|
14
14
|
*
|
|
15
|
-
* The state DB
|
|
16
|
-
*
|
|
17
|
-
* (per-task JSONL), and the improve-pipeline ledgers.
|
|
15
|
+
* The state DB stores non-regenerable events, proposals, task history, workflow
|
|
16
|
+
* runs, and improve-pipeline ledgers.
|
|
18
17
|
*
|
|
19
18
|
* ## Why a separate database from index.db
|
|
20
19
|
*
|
|
@@ -22,34 +21,29 @@
|
|
|
22
21
|
* regenerable from the stash on disk, so a corrupt index is recovered by deleting
|
|
23
22
|
* it and re-running `akm index` (no destructive version-bump rebuild). Events,
|
|
24
23
|
* proposals, and task history are NON-REGENERABLE — losing them is data loss. They
|
|
25
|
-
*
|
|
26
|
-
*
|
|
24
|
+
* live in a database whose released migration ledger is immutable and whose
|
|
25
|
+
* application policy is explicit.
|
|
27
26
|
*
|
|
28
27
|
* ## Migration-safety contract
|
|
29
28
|
*
|
|
30
29
|
* The `schema_migrations` table records every applied migration by a stable string
|
|
31
|
-
* ID.
|
|
32
|
-
*
|
|
33
|
-
*
|
|
30
|
+
* ID. New installs run all migrations in order. Existing exact-prefix ledgers
|
|
31
|
+
* automatically apply additive migrations and the verified data-preserving 002
|
|
32
|
+
* table rebuild. Released migration 018 contains destructive cleanup DDL and is
|
|
33
|
+
* never applied by an ordinary managed open. The successful `akm upgrade` path
|
|
34
|
+
* must first create and verify a sibling `VACUUM INTO` snapshot, then supplies
|
|
35
|
+
* the narrow explicit intent that admits 018. A pre-existing file with no
|
|
36
|
+
* applied migration IDs (whether the ledger table is absent or empty) is also
|
|
37
|
+
* rejected without writes; explicit upgrade snapshots its exact inode before
|
|
38
|
+
* creating the ledger or applying migration 001, then retains the same writer
|
|
39
|
+
* lock through migration 002's rebuild. Unknown and divergent ledgers fail
|
|
40
|
+
* closed.
|
|
34
41
|
*
|
|
35
|
-
*
|
|
42
|
+
* Normal automatic schema evolution uses:
|
|
36
43
|
* - ALTER TABLE … ADD COLUMN <name> <type> DEFAULT <value>
|
|
37
44
|
* - CREATE INDEX IF NOT EXISTS …
|
|
38
45
|
* - CREATE TABLE IF NOT EXISTS … (additive new tables)
|
|
39
46
|
*
|
|
40
|
-
* ## Three-DB cutover carve-out (Chunk 8, migration `020-three-db-cutover`)
|
|
41
|
-
*
|
|
42
|
-
* The 0.9.0 three-DB merge folds workflow.db and index.db's durable rows
|
|
43
|
-
* (`usage_events`, `legacy_state`) into state.db. That migration is still pure
|
|
44
|
-
* additive DDL and DROPS NOTHING — it only `CREATE TABLE IF NOT EXISTS`es the
|
|
45
|
-
* merge-target tables at their final shape. The one-time, filesystem-derived,
|
|
46
|
-
* fail-closed DATA movement (the workflow.db merge, the usage_events rescue, the
|
|
47
|
-
* full old-ref→item_ref re-key, and the workflow.db unlink / index.db quarantine
|
|
48
|
-
* rename) runs as idempotent code in the migrate-apply coordinator
|
|
49
|
-
* (`scripts/akm-migrate/migrate/legacy/three-db-cutover.ts`). So the no-DROP
|
|
50
|
-
* contract here is intact: physical workflow.db deletion happens outside the
|
|
51
|
-
* ledger DDL, after a verified backup and committed data transaction.
|
|
52
|
-
*
|
|
53
47
|
* ## Schema design: indexed columns vs. metadata_json
|
|
54
48
|
*
|
|
55
49
|
* Each table holds only the columns needed for indexed queries as first-class
|
|
@@ -68,13 +62,13 @@
|
|
|
68
62
|
*
|
|
69
63
|
* @module state-db
|
|
70
64
|
*/
|
|
65
|
+
import { randomUUID } from "node:crypto";
|
|
71
66
|
import fs from "node:fs";
|
|
72
67
|
import path from "node:path";
|
|
73
68
|
import { openDatabase } from "../storage/database.js";
|
|
74
|
-
import {
|
|
69
|
+
import { assertMigrationLedger } from "../storage/engines/sqlite-migrations.js";
|
|
75
70
|
import { openManagedDatabase, withManagedDb } from "../storage/managed-db.js";
|
|
76
71
|
import { acquireMaintenanceActivitySync } from "./maintenance-barrier.js";
|
|
77
|
-
import { assertNoPendingMigrationOperation } from "./migration-operation.js";
|
|
78
72
|
import { getDataDir } from "./paths.js";
|
|
79
73
|
import { runMigrations, STATE_MIGRATIONS } from "./state/migrations.js";
|
|
80
74
|
// ── Path helper ──────────────────────────────────────────────────────────────
|
|
@@ -87,6 +81,269 @@ import { runMigrations, STATE_MIGRATIONS } from "./state/migrations.js";
|
|
|
87
81
|
export function getStateDbPath() {
|
|
88
82
|
return path.join(getDataDir(), "state.db");
|
|
89
83
|
}
|
|
84
|
+
function safetyCopyTimestamp() {
|
|
85
|
+
return new Date().toISOString().replaceAll(/[^0-9]/g, "");
|
|
86
|
+
}
|
|
87
|
+
function noFollowFlag() {
|
|
88
|
+
return process.platform !== "win32" && typeof fs.constants.O_NOFOLLOW === "number" ? fs.constants.O_NOFOLLOW : 0;
|
|
89
|
+
}
|
|
90
|
+
function samePhysicalFile(left, right) {
|
|
91
|
+
if (left.dev !== 0n || left.ino !== 0n || right.dev !== 0n || right.ino !== 0n) {
|
|
92
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
93
|
+
}
|
|
94
|
+
return left.birthtimeNs === right.birthtimeNs && left.rdev === right.rdev;
|
|
95
|
+
}
|
|
96
|
+
function assertOwnedFileReservation(reservation, label) {
|
|
97
|
+
const descriptorStat = fs.fstatSync(reservation.fd, { bigint: true });
|
|
98
|
+
let pathStat;
|
|
99
|
+
try {
|
|
100
|
+
pathStat = fs.lstatSync(reservation.path, { bigint: true });
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
104
|
+
throw new Error(`${label} ownership/inode verification failed because its path disappeared: ${detail}`);
|
|
105
|
+
}
|
|
106
|
+
if (pathStat.isSymbolicLink() ||
|
|
107
|
+
!pathStat.isFile() ||
|
|
108
|
+
!descriptorStat.isFile() ||
|
|
109
|
+
!samePhysicalFile(reservation.identity, descriptorStat) ||
|
|
110
|
+
!samePhysicalFile(descriptorStat, pathStat)) {
|
|
111
|
+
throw new Error(`${label} ownership/inode verification failed: its path is a symlink or was replaced.`);
|
|
112
|
+
}
|
|
113
|
+
if (process.platform !== "win32" && typeof process.geteuid === "function") {
|
|
114
|
+
const expectedUid = BigInt(process.geteuid());
|
|
115
|
+
if (descriptorStat.uid !== expectedUid || pathStat.uid !== expectedUid) {
|
|
116
|
+
throw new Error(`${label} ownership verification failed: the reserved file is not owned by the current user.`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return pathStat;
|
|
120
|
+
}
|
|
121
|
+
function assertStateDatabaseSource(source) {
|
|
122
|
+
const descriptorStat = fs.fstatSync(source.fd, { bigint: true });
|
|
123
|
+
let pathStat;
|
|
124
|
+
try {
|
|
125
|
+
pathStat = fs.lstatSync(source.path, { bigint: true });
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
129
|
+
throw new Error(`state.db source inode verification failed because its path disappeared: ${detail}`);
|
|
130
|
+
}
|
|
131
|
+
if (pathStat.isSymbolicLink() ||
|
|
132
|
+
!pathStat.isFile() ||
|
|
133
|
+
!descriptorStat.isFile() ||
|
|
134
|
+
!samePhysicalFile(source.identity, descriptorStat) ||
|
|
135
|
+
!samePhysicalFile(descriptorStat, pathStat) ||
|
|
136
|
+
descriptorStat.uid !== source.identity.uid ||
|
|
137
|
+
pathStat.uid !== source.identity.uid) {
|
|
138
|
+
throw new Error("state.db source ownership/inode verification failed: its path is a symlink or was replaced.");
|
|
139
|
+
}
|
|
140
|
+
return descriptorStat;
|
|
141
|
+
}
|
|
142
|
+
function descriptorAlias(handle) {
|
|
143
|
+
const candidates = process.platform === "linux"
|
|
144
|
+
? [`/proc/self/fd/${handle.fd}`, `/dev/fd/${handle.fd}`]
|
|
145
|
+
: process.platform === "win32"
|
|
146
|
+
? []
|
|
147
|
+
: [`/dev/fd/${handle.fd}`];
|
|
148
|
+
for (const candidate of candidates) {
|
|
149
|
+
try {
|
|
150
|
+
const stat = fs.statSync(candidate, { bigint: true });
|
|
151
|
+
if (samePhysicalFile(handle.identity, stat))
|
|
152
|
+
return candidate;
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// The caller verifies the pathname immediately around SQLite open on
|
|
156
|
+
// platforms without a SQLite-openable descriptor alias.
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
function sqliteBoundFilePath(handle, label) {
|
|
162
|
+
const alias = descriptorAlias(handle);
|
|
163
|
+
if (alias)
|
|
164
|
+
return alias;
|
|
165
|
+
// SQLite's Windows VFS keeps an open database pathname from being replaced;
|
|
166
|
+
// the caller still performs identity checks immediately around every open.
|
|
167
|
+
if (process.platform === "win32")
|
|
168
|
+
return handle.path;
|
|
169
|
+
throw new Error(`${label} cannot be bound to its held inode: this platform has no descriptor-backed path.`);
|
|
170
|
+
}
|
|
171
|
+
function closeFileIdentity(handle) {
|
|
172
|
+
try {
|
|
173
|
+
fs.closeSync(handle.fd);
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
// Preserve the authoritative operation failure.
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function reserveFreshStateDatabase(dbPath) {
|
|
180
|
+
let fd;
|
|
181
|
+
try {
|
|
182
|
+
fd = fs.openSync(dbPath, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_RDWR | noFollowFlag(), 0o666);
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
if (error?.code === "EEXIST")
|
|
186
|
+
return undefined;
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
const reservation = {
|
|
191
|
+
path: dbPath,
|
|
192
|
+
fd,
|
|
193
|
+
identity: fs.fstatSync(fd, { bigint: true }),
|
|
194
|
+
};
|
|
195
|
+
assertOwnedFileReservation(reservation, "Fresh state.db");
|
|
196
|
+
return reservation;
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
try {
|
|
200
|
+
fs.closeSync(fd);
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
// Preserve the ownership failure.
|
|
204
|
+
}
|
|
205
|
+
throw error;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function openExistingStateDatabaseSource(dbPath) {
|
|
209
|
+
let fd;
|
|
210
|
+
try {
|
|
211
|
+
fd = fs.openSync(dbPath, fs.constants.O_RDONLY | noFollowFlag());
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
215
|
+
throw new Error(`Could not bind the existing state.db source inode: ${detail}`);
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
const source = {
|
|
219
|
+
path: dbPath,
|
|
220
|
+
fd,
|
|
221
|
+
identity: fs.fstatSync(fd, { bigint: true }),
|
|
222
|
+
};
|
|
223
|
+
assertStateDatabaseSource(source);
|
|
224
|
+
return source;
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
try {
|
|
228
|
+
fs.closeSync(fd);
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
// Preserve the source identity failure.
|
|
232
|
+
}
|
|
233
|
+
throw error;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function reserveHistoricalSafetyCopy(source, migrationId) {
|
|
237
|
+
const sourceStat = assertStateDatabaseSource(source);
|
|
238
|
+
const finalMode = Number(sourceStat.mode & 384n);
|
|
239
|
+
const prefix = `${source.path}.pre-${migrationId}.${safetyCopyTimestamp()}`;
|
|
240
|
+
for (let attempt = 0; attempt < 32; attempt += 1) {
|
|
241
|
+
const candidate = `${prefix}.${randomUUID()}.bak`;
|
|
242
|
+
let fd;
|
|
243
|
+
try {
|
|
244
|
+
fd = fs.openSync(candidate, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_RDWR | noFollowFlag(), 0o600);
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
if (error?.code === "EEXIST")
|
|
248
|
+
continue;
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
251
|
+
let reservation;
|
|
252
|
+
try {
|
|
253
|
+
reservation = {
|
|
254
|
+
path: candidate,
|
|
255
|
+
fd,
|
|
256
|
+
identity: fs.fstatSync(fd, { bigint: true }),
|
|
257
|
+
};
|
|
258
|
+
// Keep recovery bytes owner-only throughout creation. The source-derived
|
|
259
|
+
// (never broader) final mode is restored only after verification.
|
|
260
|
+
fs.fchmodSync(fd, 0o600);
|
|
261
|
+
assertOwnedFileReservation(reservation, "Reserved state.db safety copy");
|
|
262
|
+
return { reservation, finalMode };
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
try {
|
|
266
|
+
fs.closeSync(fd);
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
// Preserve the reservation/ownership failure.
|
|
270
|
+
}
|
|
271
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
272
|
+
throw new Error(`Could not secure reserved state.db safety-copy path ${candidate}: ${detail}. ` +
|
|
273
|
+
"The reserved pathname was not removed.");
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
throw new Error("Could not reserve a unique randomized state.db safety-copy path after 32 attempts.");
|
|
277
|
+
}
|
|
278
|
+
function fsyncDirectory(directory) {
|
|
279
|
+
if (process.platform === "win32")
|
|
280
|
+
return;
|
|
281
|
+
const fd = fs.openSync(directory, fs.constants.O_RDONLY);
|
|
282
|
+
try {
|
|
283
|
+
fs.fsyncSync(fd);
|
|
284
|
+
}
|
|
285
|
+
finally {
|
|
286
|
+
fs.closeSync(fd);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Create one verified, standalone SQLite snapshot immediately before released
|
|
291
|
+
* migration 018 removes its retired tables/column. `VACUUM INTO` includes
|
|
292
|
+
* committed WAL content in one consistent sibling database; a raw file copy
|
|
293
|
+
* would not.
|
|
294
|
+
*/
|
|
295
|
+
function createHistoricalStateSafetyCopy(source, migrationId) {
|
|
296
|
+
const { reservation, finalMode } = reserveHistoricalSafetyCopy(source, migrationId);
|
|
297
|
+
let reader;
|
|
298
|
+
try {
|
|
299
|
+
assertOwnedFileReservation(reservation, "Reserved state.db safety copy");
|
|
300
|
+
assertStateDatabaseSource(source);
|
|
301
|
+
// The migration connection already holds BEGIN IMMEDIATE. A distinct
|
|
302
|
+
// read-only connection bound to the held source inode can snapshot the
|
|
303
|
+
// committed WAL view without trying to VACUUM from inside that transaction.
|
|
304
|
+
reader = openDatabase(sqliteBoundFilePath(source, "state.db snapshot source"), { readonly: true });
|
|
305
|
+
assertStateDatabaseSource(source);
|
|
306
|
+
reader.prepare("VACUUM INTO ?").run(sqliteBoundFilePath(reservation, "Reserved state.db safety-copy target"));
|
|
307
|
+
assertStateDatabaseSource(source);
|
|
308
|
+
reader.close();
|
|
309
|
+
reader = undefined;
|
|
310
|
+
assertOwnedFileReservation(reservation, "Reserved state.db safety copy");
|
|
311
|
+
fs.fsyncSync(reservation.fd);
|
|
312
|
+
assertOwnedFileReservation(reservation, "Reserved state.db safety copy");
|
|
313
|
+
const verified = openDatabase(sqliteBoundFilePath(reservation, "Reserved state.db safety-copy target"), {
|
|
314
|
+
readonly: true,
|
|
315
|
+
});
|
|
316
|
+
try {
|
|
317
|
+
const quickCheck = verified.prepare("PRAGMA quick_check").get();
|
|
318
|
+
if (!quickCheck || Object.values(quickCheck)[0] !== "ok") {
|
|
319
|
+
throw new Error("SQLite quick_check did not report ok");
|
|
320
|
+
}
|
|
321
|
+
assertMigrationLedger(verified, STATE_MIGRATIONS);
|
|
322
|
+
}
|
|
323
|
+
finally {
|
|
324
|
+
verified.close();
|
|
325
|
+
}
|
|
326
|
+
assertOwnedFileReservation(reservation, "Reserved state.db safety copy");
|
|
327
|
+
fs.fchmodSync(reservation.fd, finalMode);
|
|
328
|
+
fs.fsyncSync(reservation.fd);
|
|
329
|
+
assertOwnedFileReservation(reservation, "Reserved state.db safety copy");
|
|
330
|
+
fsyncDirectory(path.dirname(reservation.path));
|
|
331
|
+
closeFileIdentity(reservation);
|
|
332
|
+
return reservation.path;
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
try {
|
|
336
|
+
reader?.close();
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
// Preserve the snapshot/verification failure below.
|
|
340
|
+
}
|
|
341
|
+
closeFileIdentity(reservation);
|
|
342
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
343
|
+
throw new Error(`Could not create a verified state.db safety copy before ${migrationId}: ${detail}. ` +
|
|
344
|
+
`The reserved safety-copy pathname was not removed: ${reservation.path}`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
90
347
|
// ── Database open ────────────────────────────────────────────────────────────
|
|
91
348
|
/**
|
|
92
349
|
* Open (and initialise / migrate) the state database.
|
|
@@ -114,34 +371,106 @@ export function getStateDbPath() {
|
|
|
114
371
|
* matches the value used in openDatabase() for index.db; 5 s proved too
|
|
115
372
|
* narrow when a post-inference reindex overlapped a parallel event write.
|
|
116
373
|
*/
|
|
117
|
-
export function openStateDatabase(dbPath) {
|
|
374
|
+
export function openStateDatabase(dbPath, options) {
|
|
118
375
|
const canonicalPath = getStateDbPath();
|
|
119
376
|
const resolvedPath = dbPath ?? canonicalPath;
|
|
377
|
+
// `:memory:` is a SQLite connection identity, not a filesystem pathname.
|
|
378
|
+
// Never pass it through the durable-file reservation/inode/snapshot path:
|
|
379
|
+
// doing so creates a literal `:memory:` file and makes later in-process
|
|
380
|
+
// opens look like an unversioned durable database. Each in-memory handle is
|
|
381
|
+
// fresh and cannot be path-swapped, so the ownership proof is intrinsically
|
|
382
|
+
// satisfied for this explicit test/internal seam.
|
|
383
|
+
if (resolvedPath === ":memory:") {
|
|
384
|
+
return openManagedDatabase({
|
|
385
|
+
path: resolvedPath,
|
|
386
|
+
pragmas: { dataDir: path.dirname(resolvedPath) },
|
|
387
|
+
init: (db) => runMigrations(db, {
|
|
388
|
+
freshDatabase: true,
|
|
389
|
+
verifyFreshDatabaseOwnership: () => { },
|
|
390
|
+
}),
|
|
391
|
+
});
|
|
392
|
+
}
|
|
120
393
|
const isCanonical = path.resolve(resolvedPath) === path.resolve(canonicalPath);
|
|
121
|
-
if (isCanonical)
|
|
122
|
-
assertNoPendingMigrationOperation();
|
|
123
394
|
const releaseActivity = isCanonical ? acquireMaintenanceActivitySync("state-db") : undefined;
|
|
395
|
+
let freshReservation;
|
|
396
|
+
let existingSource;
|
|
397
|
+
let openedDb;
|
|
398
|
+
let existingUnversionedDatabase = false;
|
|
399
|
+
let stateSafetyCopyCreated = false;
|
|
124
400
|
try {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
401
|
+
fs.mkdirSync(path.dirname(resolvedPath), { recursive: true });
|
|
402
|
+
freshReservation = reserveFreshStateDatabase(resolvedPath);
|
|
403
|
+
if (!freshReservation) {
|
|
404
|
+
existingSource = openExistingStateDatabaseSource(resolvedPath);
|
|
405
|
+
assertStateDatabaseSource(existingSource);
|
|
406
|
+
const preflight = openDatabase(sqliteBoundFilePath(existingSource, "Existing state.db preflight"), {
|
|
407
|
+
readonly: true,
|
|
408
|
+
});
|
|
130
409
|
try {
|
|
410
|
+
assertStateDatabaseSource(existingSource);
|
|
131
411
|
preflight.exec("PRAGMA busy_timeout = 30000");
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
412
|
+
const ledger = assertMigrationLedger(preflight, STATE_MIGRATIONS);
|
|
413
|
+
existingUnversionedDatabase = ledger.migrationIds.length === 0;
|
|
414
|
+
if (existingUnversionedDatabase && !options?.allowHistoricalDestructiveStateUpgrade) {
|
|
415
|
+
throw new Error("Refusing to migrate an existing unversioned state.db during an ordinary managed open. " +
|
|
416
|
+
"Run `akm upgrade --force` to create a verified snapshot before migration 001.");
|
|
417
|
+
}
|
|
136
418
|
}
|
|
137
419
|
finally {
|
|
138
420
|
preflight.close();
|
|
139
421
|
}
|
|
140
422
|
}
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
423
|
+
const ownedFresh = freshReservation;
|
|
424
|
+
const boundSource = existingSource;
|
|
425
|
+
if (boundSource)
|
|
426
|
+
assertStateDatabaseSource(boundSource);
|
|
427
|
+
openedDb = openManagedDatabase({
|
|
428
|
+
path: boundSource ? sqliteBoundFilePath(boundSource, "Managed state.db writer") : resolvedPath,
|
|
429
|
+
pragmas: { dataDir: path.dirname(resolvedPath) },
|
|
430
|
+
init: (db) => {
|
|
431
|
+
if (boundSource)
|
|
432
|
+
assertStateDatabaseSource(boundSource);
|
|
433
|
+
runMigrations(db, {
|
|
434
|
+
freshDatabase: !!ownedFresh,
|
|
435
|
+
verifyFreshDatabaseOwnership: ownedFresh
|
|
436
|
+
? () => assertOwnedFileReservation(ownedFresh, "Fresh state.db")
|
|
437
|
+
: undefined,
|
|
438
|
+
existingUnversionedDatabase,
|
|
439
|
+
allowHistoricalDestructiveStateUpgrade: options?.allowHistoricalDestructiveStateUpgrade,
|
|
440
|
+
beforeExistingUnversionedStateMigration: options?.allowHistoricalDestructiveStateUpgrade
|
|
441
|
+
? (migration) => {
|
|
442
|
+
if (!boundSource)
|
|
443
|
+
throw new Error("An existing unversioned state.db has no bound source inode.");
|
|
444
|
+
const safetyCopyPath = createHistoricalStateSafetyCopy(boundSource, migration.id);
|
|
445
|
+
stateSafetyCopyCreated = true;
|
|
446
|
+
options.onHistoricalStateSafetyCopy?.(safetyCopyPath);
|
|
447
|
+
}
|
|
448
|
+
: undefined,
|
|
449
|
+
beforeHistoricalDestructiveMigration: options?.allowHistoricalDestructiveStateUpgrade
|
|
450
|
+
? (migration) => {
|
|
451
|
+
if (stateSafetyCopyCreated)
|
|
452
|
+
return;
|
|
453
|
+
if (!boundSource)
|
|
454
|
+
throw new Error("Historical state migration has no bound source inode.");
|
|
455
|
+
const safetyCopyPath = createHistoricalStateSafetyCopy(boundSource, migration.id);
|
|
456
|
+
stateSafetyCopyCreated = true;
|
|
457
|
+
options.onHistoricalStateSafetyCopy?.(safetyCopyPath);
|
|
458
|
+
}
|
|
459
|
+
: undefined,
|
|
460
|
+
});
|
|
461
|
+
},
|
|
144
462
|
});
|
|
463
|
+
if (existingSource) {
|
|
464
|
+
assertStateDatabaseSource(existingSource);
|
|
465
|
+
closeFileIdentity(existingSource);
|
|
466
|
+
existingSource = undefined;
|
|
467
|
+
}
|
|
468
|
+
if (freshReservation) {
|
|
469
|
+
assertOwnedFileReservation(freshReservation, "Fresh state.db");
|
|
470
|
+
closeFileIdentity(freshReservation);
|
|
471
|
+
freshReservation = undefined;
|
|
472
|
+
}
|
|
473
|
+
const db = openedDb;
|
|
145
474
|
if (!releaseActivity)
|
|
146
475
|
return db;
|
|
147
476
|
let closed = false;
|
|
@@ -168,10 +497,47 @@ export function openStateDatabase(dbPath) {
|
|
|
168
497
|
};
|
|
169
498
|
}
|
|
170
499
|
catch (error) {
|
|
500
|
+
if (openedDb) {
|
|
501
|
+
try {
|
|
502
|
+
openedDb.close();
|
|
503
|
+
}
|
|
504
|
+
catch {
|
|
505
|
+
// Preserve the open/migration ownership failure.
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
if (existingSource)
|
|
509
|
+
closeFileIdentity(existingSource);
|
|
510
|
+
if (freshReservation)
|
|
511
|
+
closeFileIdentity(freshReservation);
|
|
171
512
|
releaseActivity?.();
|
|
172
513
|
throw error;
|
|
173
514
|
}
|
|
174
515
|
}
|
|
516
|
+
/**
|
|
517
|
+
* Narrow state-schema step owned by `akm upgrade` after executable replacement.
|
|
518
|
+
* Missing/current databases are no-ops. A pre-018 exact ledger is snapshotted
|
|
519
|
+
* beside state.db and verified before the immutable released migration runs.
|
|
520
|
+
*/
|
|
521
|
+
export function upgradeHistoricalStateDatabase(dbPath = getStateDbPath()) {
|
|
522
|
+
if (!fs.existsSync(dbPath))
|
|
523
|
+
return { upgraded: false };
|
|
524
|
+
let safetyCopyPath;
|
|
525
|
+
try {
|
|
526
|
+
const db = openStateDatabase(dbPath, {
|
|
527
|
+
allowHistoricalDestructiveStateUpgrade: true,
|
|
528
|
+
onHistoricalStateSafetyCopy(copyPath) {
|
|
529
|
+
safetyCopyPath = copyPath;
|
|
530
|
+
},
|
|
531
|
+
});
|
|
532
|
+
db.close();
|
|
533
|
+
}
|
|
534
|
+
catch (error) {
|
|
535
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
536
|
+
const recovery = safetyCopyPath ? ` Verified safety copy: ${safetyCopyPath}.` : "";
|
|
537
|
+
throw new Error(`${detail}${recovery}`);
|
|
538
|
+
}
|
|
539
|
+
return safetyCopyPath ? { upgraded: true, safetyCopyPath } : { upgraded: false };
|
|
540
|
+
}
|
|
175
541
|
/**
|
|
176
542
|
* Run `fn` against state.db, owning the handle unless one is borrowed. The loan
|
|
177
543
|
* helper for state.db, mirroring `withIndexDb` / `withWorkflowRunsRepo`. Pass
|
|
@@ -248,42 +614,37 @@ function sleepSyncMs(ms) {
|
|
|
248
614
|
return;
|
|
249
615
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
250
616
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
617
|
+
/**
|
|
618
|
+
* Open, but deliberately do not finish, an immediate transaction.
|
|
619
|
+
*
|
|
620
|
+
* This is the split-phase counterpart to {@link withImmediateTransaction} for
|
|
621
|
+
* the source-update coordinator: index finalization must mutate state.db in a
|
|
622
|
+
* transaction that remains pending until content, lockfile, and index
|
|
623
|
+
* publication have all succeeded. The caller that asked for this split phase
|
|
624
|
+
* owns the matching COMMIT/ROLLBACK.
|
|
625
|
+
*/
|
|
626
|
+
export function beginImmediateTransaction(db) {
|
|
259
627
|
if (db.inTransaction) {
|
|
260
|
-
|
|
628
|
+
throw new Error("beginImmediateTransaction requires a connection with no active transaction");
|
|
261
629
|
}
|
|
262
630
|
let lastBeginErr;
|
|
263
631
|
for (let attempt = 1; attempt <= WITH_IMMEDIATE_TX_MAX_ATTEMPTS; attempt++) {
|
|
264
632
|
try {
|
|
265
633
|
db.exec("BEGIN IMMEDIATE");
|
|
266
|
-
// bun:sqlite can return from BEGIN IMMEDIATE under writer contention WITHOUT
|
|
267
|
-
// actually opening a transaction (no throw). That phantom state otherwise
|
|
268
|
-
// surfaces as "cannot commit - no transaction is active" at COMMIT — AFTER
|
|
269
|
-
// fn() has already run in autocommit, so its writes escaped the intended
|
|
270
|
-
// serialization (the concurrent proposal-queue race). Detect it here, before
|
|
271
|
-
// fn(), and route it through the same retry path as a contended BEGIN.
|
|
272
634
|
if (!db.inTransaction) {
|
|
273
635
|
throw new Error("BEGIN IMMEDIATE did not open a transaction (phantom contention state)");
|
|
274
636
|
}
|
|
637
|
+
return;
|
|
275
638
|
}
|
|
276
639
|
catch (err) {
|
|
277
640
|
lastBeginErr = err;
|
|
278
641
|
if (isRetryableBeginError(err) && attempt < WITH_IMMEDIATE_TX_MAX_ATTEMPTS) {
|
|
279
|
-
// Only roll back a transaction we can see — never blind-ROLLBACK, since
|
|
280
|
-
// that could destroy a transaction this frame does not own.
|
|
281
642
|
if (db.inTransaction) {
|
|
282
643
|
try {
|
|
283
644
|
db.exec("ROLLBACK");
|
|
284
645
|
}
|
|
285
646
|
catch {
|
|
286
|
-
// Transaction already gone —
|
|
647
|
+
// Transaction already gone — safe to retry BEGIN.
|
|
287
648
|
}
|
|
288
649
|
}
|
|
289
650
|
sleepSyncMs(2 ** (attempt - 1));
|
|
@@ -291,33 +652,45 @@ export function withImmediateTransaction(db, fn) {
|
|
|
291
652
|
}
|
|
292
653
|
throw err;
|
|
293
654
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
655
|
+
}
|
|
656
|
+
throw lastBeginErr;
|
|
657
|
+
}
|
|
658
|
+
export function withImmediateTransaction(db, fn) {
|
|
659
|
+
// Re-entrancy guard (issue #686): if a transaction is already open on this
|
|
660
|
+
// connection (e.g. a nested withImmediateTransaction call inside an outer
|
|
661
|
+
// frame's fn), join it — run fn directly with no BEGIN/COMMIT/ROLLBACK of
|
|
662
|
+
// our own. Without this, the nested BEGIN throws "cannot start a transaction
|
|
663
|
+
// within a transaction", which the old retry path answered with an
|
|
664
|
+
// unconditional ROLLBACK — destroying the OUTER transaction and leaving its
|
|
665
|
+
// COMMIT to fail with "cannot commit - no transaction is active".
|
|
666
|
+
if (db.inTransaction) {
|
|
667
|
+
return fn();
|
|
668
|
+
}
|
|
669
|
+
beginImmediateTransaction(db);
|
|
670
|
+
try {
|
|
671
|
+
const result = fn();
|
|
672
|
+
if (!db.inTransaction) {
|
|
673
|
+
// The transaction we opened vanished while fn() ran (e.g. an
|
|
674
|
+
// auto-rollback or a stray ROLLBACK inside fn). fn's writes may have
|
|
675
|
+
// escaped serialization, so retrying is unsafe — fail loudly instead of
|
|
676
|
+
// letting COMMIT throw the opaque "cannot commit - no transaction is
|
|
677
|
+
// active" SQLiteError.
|
|
678
|
+
throw new Error("withImmediateTransaction invariant violated: transaction opened by BEGIN IMMEDIATE was no longer active after the transaction body ran; refusing to COMMIT (writes may have escaped serialization)");
|
|
306
679
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
680
|
+
db.exec("COMMIT");
|
|
681
|
+
return result;
|
|
682
|
+
}
|
|
683
|
+
catch (err) {
|
|
684
|
+
if (db.inTransaction) {
|
|
685
|
+
try {
|
|
686
|
+
db.exec("ROLLBACK");
|
|
687
|
+
}
|
|
688
|
+
catch {
|
|
689
|
+
// Ignore rollback failures so the original error is preserved.
|
|
315
690
|
}
|
|
316
|
-
throw err; // a real error inside the transaction body — never retried.
|
|
317
691
|
}
|
|
692
|
+
throw err;
|
|
318
693
|
}
|
|
319
|
-
// Exhausted retries on transient begin failures.
|
|
320
|
-
throw lastBeginErr;
|
|
321
694
|
}
|
|
322
695
|
// ── schema introspection ─────────────────────────────────────────────────────
|
|
323
696
|
/**
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* Return a Node-style system error code from an error or one of its causes.
|
|
6
|
+
* The bounded walk handles wrappers without trusting or parsing error text.
|
|
7
|
+
*/
|
|
8
|
+
export function systemErrorCode(error) {
|
|
9
|
+
let current = error;
|
|
10
|
+
for (let depth = 0; depth < 8; depth += 1) {
|
|
11
|
+
if (!current || typeof current !== "object")
|
|
12
|
+
return undefined;
|
|
13
|
+
const candidate = current;
|
|
14
|
+
if (typeof candidate.code === "string")
|
|
15
|
+
return candidate.code;
|
|
16
|
+
current = candidate.cause;
|
|
17
|
+
}
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
/** Read-only telemetry writes are expected to disappear without diagnostics. */
|
|
21
|
+
export function isReadOnlyFilesystemError(error) {
|
|
22
|
+
let current = error;
|
|
23
|
+
for (let depth = 0; depth < 8; depth += 1) {
|
|
24
|
+
if (!current || typeof current !== "object")
|
|
25
|
+
return false;
|
|
26
|
+
const candidate = current;
|
|
27
|
+
if (candidate.code === "EROFS" || candidate.code === "EACCES")
|
|
28
|
+
return true;
|
|
29
|
+
current = candidate.cause;
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
}
|