akm-cli 0.9.17-alpha.2 → 0.9.17-alpha.4
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 +756 -0
- package/dist/akm +94 -196
- package/dist/cli/shared.js +6 -2
- package/dist/cli.js +22 -9
- package/dist/commands/agent/agent-dispatch.js +1 -1
- package/dist/commands/command/command-execution.js +24 -62
- package/dist/commands/feedback-cli.js +0 -1
- package/dist/commands/health/accept-rate.js +2 -2
- package/dist/commands/health/checks.js +30 -75
- package/dist/commands/health/config-skew.js +38 -0
- package/dist/commands/health/egress.js +54 -0
- package/dist/commands/health/html-report.js +0 -38
- package/dist/commands/health/improve-metrics.js +123 -562
- package/dist/commands/health/plugin-staleness.js +53 -3
- package/dist/commands/health/renderers.js +12 -4
- package/dist/commands/health/report-view-model.js +11 -106
- package/dist/commands/health/types-improve.js +4 -19
- package/dist/commands/health/windows.js +64 -73
- package/dist/commands/health.js +122 -143
- package/dist/commands/improve/consolidate/chunking.js +25 -100
- package/dist/commands/improve/consolidate/sanitize.js +54 -149
- package/dist/commands/improve/consolidate.js +538 -1075
- package/dist/commands/improve/content-hash.js +16 -24
- package/dist/commands/improve/distill/content-repair.js +18 -100
- package/dist/commands/improve/distill-guards.js +20 -81
- package/dist/commands/improve/distill-promotion-policy.js +23 -243
- package/dist/commands/improve/distill.js +608 -1075
- package/dist/commands/improve/eligibility.js +126 -400
- package/dist/commands/improve/execution.js +3 -5
- package/dist/commands/improve/extract.js +487 -1046
- package/dist/commands/improve/feedback-valence.js +0 -25
- package/dist/commands/improve/improve-cli.js +29 -166
- package/dist/commands/improve/improve-result-file.js +10 -66
- package/dist/commands/improve/improve-strategies.js +12 -7
- package/dist/commands/improve/improve-usage-report.js +18 -64
- package/dist/commands/improve/improve.js +443 -1063
- package/dist/commands/improve/ledger.js +114 -0
- package/dist/commands/improve/locks.js +2 -8
- package/dist/commands/improve/loop-stages.js +459 -1172
- package/dist/commands/improve/memory/derived-ref.js +12 -77
- package/dist/commands/improve/memory/memory-belief.js +14 -118
- package/dist/commands/improve/memory/memory-improve.js +4 -3
- package/dist/commands/improve/outcome-loop.js +28 -156
- package/dist/commands/improve/planner.js +5 -10
- package/dist/commands/improve/preparation.js +851 -2339
- package/dist/commands/improve/proactive-maintenance.js +34 -101
- package/dist/commands/improve/reflect-noise.js +104 -280
- package/dist/commands/improve/reflect.js +621 -1367
- package/dist/commands/improve/salience.js +46 -232
- package/dist/commands/improve/session-asset.js +19 -100
- package/dist/commands/improve/stage.js +323 -0
- package/dist/commands/proposal/drain.js +251 -644
- package/dist/commands/proposal/proposal-cli.js +3 -18
- package/dist/commands/proposal/proposal-types.js +20 -41
- package/dist/commands/proposal/proposal.js +1 -2
- package/dist/commands/proposal/propose.js +134 -160
- package/dist/commands/proposal/repository.js +502 -1487
- package/dist/commands/proposal/validators/proposal-quality-validators.js +71 -174
- package/dist/commands/proposal/validators/proposal-validators.js +1 -1
- package/dist/commands/proposal/validators/proposals.js +13 -89
- package/dist/commands/read/curate.js +63 -413
- package/dist/commands/read/search-cli.js +16 -33
- package/dist/commands/read/search.js +17 -23
- package/dist/commands/read/show.js +2 -13
- package/dist/commands/sources/bundle-cli.js +25 -2
- package/dist/commands/sources/bundle-config-ops.js +7 -0
- package/dist/commands/sources/dangerous-env-audit.js +1 -2
- package/dist/commands/sources/info.js +2 -11
- package/dist/commands/sources/installed-stashes.js +197 -746
- package/dist/commands/sources/schema-repair.js +98 -129
- package/dist/commands/sources/source-add.js +62 -12
- package/dist/commands/sources/stash-cli.js +1 -1
- package/dist/commands/tasks/explain.js +10 -13
- package/dist/commands/tasks/tasks-cli.js +9 -8
- package/dist/commands/tasks/tasks.js +326 -930
- package/dist/commands/tasks/validate.js +42 -21
- package/dist/commands/workflow/plan.js +22 -29
- package/dist/commands/workflow-cli.js +4 -4
- package/dist/core/adapter/adapters/akm-adapter.js +0 -1
- package/dist/core/adapter/adapters/akm-lint.js +2 -3
- package/dist/core/adapter/adapters/akm-metadata.js +11 -12
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +1 -1
- package/dist/core/adapter/execution-source.js +17 -29
- package/dist/core/asset/resolve-ref.js +1 -1
- package/dist/core/bundle-id.js +42 -5
- package/dist/core/bundle-rename.js +291 -0
- package/dist/core/config/config-io.js +1 -2
- package/dist/core/config/config-schema.js +1 -33
- package/dist/core/config/config-walker.js +1 -1
- package/dist/core/config/config.js +163 -68
- package/dist/core/config/legacy-source-shape-shim.js +38 -9
- package/dist/core/config/schema/embedding.js +20 -5
- package/dist/core/config/schema/engines.js +5 -0
- package/dist/core/config/schema/execution.js +1 -1
- package/dist/core/config/schema/experimental.js +1 -1
- package/dist/core/config/schema/improve-processes.js +21 -95
- package/dist/core/config/schema/improve.js +4 -42
- package/dist/core/config/schema/scheduler.js +12 -12
- package/dist/core/config/schema/search.js +6 -22
- package/dist/core/env-secret-ref.js +0 -1
- package/dist/core/errors.js +8 -9
- package/dist/core/file-lock.js +76 -173
- package/dist/core/logs-db.js +2 -2
- package/dist/core/paths.js +0 -27
- package/dist/core/redaction.js +109 -2
- package/dist/core/run-lock.js +2 -5
- package/dist/core/spawn-env.js +1 -1
- package/dist/core/state/migrations.js +108 -61
- package/dist/core/state-db-scope.js +2 -4
- package/dist/core/state-db.js +126 -692
- package/dist/core/type-presentation.js +1 -9
- package/dist/core/write-source.js +293 -1012
- package/dist/execution/input-contract.js +1 -1
- package/dist/execution/resolved-request.js +135 -689
- package/dist/execution/source.js +63 -257
- package/dist/execution/target-ref.js +1 -1
- package/dist/indexer/bundle-identity-guard.js +2 -2
- package/dist/indexer/db/graph-db.js +106 -46
- package/dist/indexer/ensure-index.js +44 -85
- package/dist/indexer/graph/graph-extraction.js +340 -562
- package/dist/indexer/graph/graph-related.js +130 -0
- package/dist/indexer/index-rebuild-lock.js +3 -11
- package/dist/indexer/index-writer-lock.js +8 -17
- package/dist/indexer/index-written-assets.js +139 -151
- package/dist/indexer/indexer.js +524 -846
- package/dist/indexer/materialize-embeddings.js +60 -397
- package/dist/indexer/passes/memory-inference.js +81 -90
- package/dist/indexer/passes/metadata.js +132 -200
- package/dist/indexer/read-preflight.js +0 -7
- package/dist/indexer/scan/doc-to-entry.js +1 -3
- package/dist/indexer/scan/drain-dir.js +1 -1
- package/dist/indexer/search/db-search.js +181 -590
- package/dist/indexer/search/fts-query.js +30 -41
- package/dist/indexer/search/ranking.js +28 -154
- package/dist/indexer/search/search-attribution.js +12 -32
- package/dist/indexer/search/search-fields.js +11 -15
- package/dist/indexer/search/search-hit-enrichers.js +54 -85
- package/dist/indexer/search/search-source.js +1 -4
- package/dist/indexer/usage/usage-events.js +2 -7
- package/dist/integrations/agent/engine-fallback.js +23 -40
- package/dist/integrations/agent/engine-resolution.js +93 -183
- package/dist/integrations/agent/execution.js +507 -0
- package/dist/integrations/agent/model-map.js +28 -156
- package/dist/integrations/agent/request-lowering.js +66 -141
- package/dist/integrations/agent/runner-dispatch.js +143 -321
- package/dist/integrations/agent/runner.js +54 -14
- package/dist/integrations/lockfile.js +53 -101
- package/dist/llm/embedders/deterministic.js +2 -3
- package/dist/llm/embedders/profile.js +71 -0
- package/dist/llm/embedders/remote.js +10 -15
- package/dist/llm/graph-extract.js +3 -12
- package/dist/llm/index-passes.js +3 -5
- package/dist/llm/memory-infer.js +1 -2
- package/dist/llm/metadata-enhance.js +1 -2
- package/dist/llm/structured-call.js +5 -24
- package/dist/output/generic-render.js +23 -11
- package/dist/output/html-render.js +13 -10
- package/dist/output/render-registry.js +3 -32
- package/dist/output/shapes/helpers.js +2 -34
- package/dist/output/shapes/passthrough.js +1 -9
- package/dist/{indexer/search/ranking-types.js → output/text/bundle-rename.js} +4 -1
- package/dist/output/text/command-format.js +60 -23
- package/dist/output/text/helpers.js +1 -1
- package/dist/output/text/migrate.js +5 -14
- package/dist/output/text/proposal-format.js +1 -2
- package/dist/output/text/workflow-format.js +0 -32
- package/dist/output/text.js +2 -0
- package/dist/registry/factory.js +4 -19
- package/dist/registry/network.js +66 -220
- package/dist/registry/providers/index.js +0 -2
- package/dist/registry/providers/skills-sh.js +3 -14
- package/dist/registry/providers/static-index.js +24 -26
- package/dist/registry/resolve.js +55 -131
- package/dist/scripts/akm-migrate-node.js +43937 -93313
- package/dist/scripts/akm-migrate.js +43697 -93071
- package/dist/setup/registry-stash-loader.js +4 -13
- package/dist/setup/semantic-assets.js +3 -44
- package/dist/setup/setup.js +1 -1
- package/dist/setup/steps/tasks.js +25 -15
- package/dist/sources/provider-factory.js +17 -18
- package/dist/sources/providers/filesystem.js +2 -3
- package/dist/sources/providers/git-install.js +7 -1
- package/dist/sources/providers/git-provider.js +0 -3
- package/dist/sources/providers/git-stash.js +0 -17
- package/dist/sources/providers/npm.js +2 -4
- package/dist/sources/providers/provider-utils.js +5 -10
- package/dist/sources/providers/website.js +0 -2
- package/dist/sources/snapshot-fetchers/website-ingest.js +1 -1
- package/dist/sources/website-url.js +2 -2
- package/dist/storage/database.js +9 -35
- package/dist/storage/repositories/improve-ledger-repository.js +168 -0
- package/dist/storage/repositories/index-connection.js +34 -70
- package/dist/storage/repositories/index-entries-repository.js +69 -111
- package/dist/storage/repositories/index-entry-mapper.js +1 -2
- package/dist/storage/repositories/index-entry-schema.js +83 -269
- package/dist/storage/repositories/index-fts-repository.js +86 -256
- package/dist/storage/repositories/index-llm-cache-repository.js +17 -0
- package/dist/storage/repositories/index-meta-repository.js +6 -4
- package/dist/storage/repositories/index-schema.js +192 -220
- package/dist/storage/repositories/index-utility-repository.js +8 -29
- package/dist/storage/repositories/index-vec-repository.js +133 -414
- package/dist/storage/repositories/outcome-repository.js +2 -1
- package/dist/storage/repositories/proposals-repository.js +35 -0
- package/dist/storage/repositories/registry-index-cache-repository.js +100 -0
- package/dist/storage/repositories/task-history-repository.js +26 -4
- package/dist/storage/repositories/workflow-runs-repository.js +53 -244
- package/dist/storage/sqlite-migrations.js +136 -0
- package/dist/storage/sqlite-pragmas.js +11 -9
- package/dist/storage/sqlite-transaction.js +170 -0
- package/dist/storage/state-db-integrity.js +34 -27
- package/dist/tasks/activation-config.js +134 -62
- package/dist/tasks/backends/cron.js +129 -277
- package/dist/tasks/backends/exec-utils.js +2 -5
- package/dist/tasks/backends/launchd.js +125 -745
- package/dist/tasks/backends/schtasks.js +101 -620
- package/dist/tasks/prepare/prepare-support.js +5 -15
- package/dist/tasks/prepare/prepare.js +0 -2
- package/dist/tasks/resolve-akm-bin.js +20 -79
- package/dist/tasks/run/attempt-lifecycle.js +0 -1
- package/dist/tasks/scheduler-binding.js +18 -238
- package/dist/tasks/scheduler-invocation.js +52 -52
- package/dist/tasks/scheduler-lock.js +53 -0
- package/dist/tasks/scheduler-sync.js +363 -679
- package/dist/tasks/source/parse-task-source.js +160 -10
- package/dist/tasks/source/task-source-v3-frozen.js +3 -4
- package/dist/tasks/source/task-to-v4.js +2 -2
- package/dist/workflows/authoring/authoring.js +3 -12
- package/dist/workflows/compile.js +211 -0
- package/dist/workflows/concurrency-policy.js +13 -74
- package/dist/workflows/exec/child-invocation.js +3 -17
- package/dist/workflows/exec/child-workflow.js +32 -141
- package/dist/workflows/exec/dispatch-redaction.js +13 -53
- package/dist/workflows/exec/environment.js +98 -0
- package/dist/workflows/exec/exec-unit.js +33 -140
- package/dist/workflows/exec/frozen-judge.js +7 -59
- package/dist/workflows/exec/native-executor.js +82 -341
- package/dist/workflows/exec/param-secrets.js +29 -47
- package/dist/workflows/exec/run-workflow.js +154 -387
- package/dist/workflows/exec/scheduler.js +9 -36
- package/dist/workflows/exec/step-work.js +127 -430
- package/dist/workflows/exec/unit-dispatch.js +11 -63
- package/dist/workflows/exec/unit-writer.js +8 -52
- package/dist/workflows/exec/worktree.js +39 -273
- package/dist/workflows/freeze/child-output-references.js +4 -15
- package/dist/workflows/freeze/environment.js +99 -92
- package/dist/workflows/freeze/freeze.js +172 -0
- package/dist/workflows/freeze/step-values.js +19 -21
- package/dist/workflows/freeze/targets/child-workflow.js +23 -92
- package/dist/workflows/freeze/targets/command.js +10 -33
- package/dist/workflows/freeze/targets/script.js +5 -12
- package/dist/workflows/freeze/targets/shell.js +3 -6
- package/dist/workflows/freeze/targets/task.js +25 -80
- package/dist/workflows/freeze/task-bindings.js +20 -67
- package/dist/workflows/{source-ir/github-yaml.js → github-yaml.js} +88 -206
- package/dist/workflows/ir/params.js +6 -51
- package/dist/workflows/ir/plan-hash.js +2 -34
- package/dist/workflows/parser.js +140 -43
- package/dist/{commands/improve/consolidate/types.js → workflows/plan.js} +2 -1
- package/dist/workflows/renderer.js +36 -69
- package/dist/workflows/resource-limits.js +12 -120
- package/dist/workflows/runtime/agent-identity.js +8 -40
- package/dist/workflows/runtime/run-outputs.js +3 -6
- package/dist/workflows/runtime/run-plan.js +316 -0
- package/dist/workflows/runtime/runs.js +48 -200
- package/dist/workflows/runtime/workflow-asset-loader.js +24 -57
- package/dist/workflows/{source-ir/semantics.js → source-semantics.js} +16 -20
- package/dist/workflows/validate-summary.js +2 -7
- package/docs/integration/bundling-akm.md +49 -42
- package/docs/migration/README.md +1 -0
- package/docs/migration/release-notes/0.9.17.md +41 -0
- package/docs/migration/v0.9.1-to-v0.9.2.md +19 -7
- package/docs/reference/cli.md +182 -125
- package/docs/reference/configuration.md +49 -56
- package/docs/reference/data-and-telemetry.md +19 -20
- package/docs/reference/tasks.md +86 -38
- package/docs/reference/workflow-schema.md +14 -18
- package/docs/reference/workflows.md +6 -9
- package/package.json +1 -1
- package/schemas/akm-config.json +87 -406
- package/dist/commands/health/advisories.js +0 -150
- package/dist/commands/health/metrics.js +0 -329
- package/dist/commands/health/surfaces.js +0 -102
- package/dist/commands/improve/anti-collapse.js +0 -83
- package/dist/commands/improve/collapse-detector.js +0 -432
- package/dist/commands/improve/consolidate/eligibility.js +0 -48
- package/dist/commands/improve/consolidate/merge.js +0 -146
- package/dist/commands/improve/distill/promote-memory.js +0 -329
- package/dist/commands/improve/distill/quality-gate.js +0 -500
- package/dist/commands/improve/memory/memory-contradiction-detect.js +0 -291
- package/dist/commands/improve/proposal-envelope.js +0 -31
- package/dist/commands/improve/run-context.js +0 -123
- package/dist/commands/improve/shared.js +0 -21
- package/dist/commands/improve/source-identity.js +0 -28
- package/dist/commands/improve/triage.js +0 -96
- package/dist/commands/proposal/drain-policies.js +0 -151
- package/dist/commands/sources/update-transaction.js +0 -220
- package/dist/core/action-contributors.js +0 -28
- package/dist/core/config/config-version-shim.js +0 -101
- package/dist/core/config/retired-experimental-keys-shim.js +0 -62
- package/dist/core/fs-txn.js +0 -405
- package/dist/core/lexical-score.js +0 -25
- package/dist/core/maintenance-barrier.js +0 -167
- package/dist/execution/executable-identity.js +0 -105
- package/dist/execution/guarded-source.js +0 -427
- package/dist/indexer/graph/graph-boost.js +0 -427
- package/dist/indexer/graph/graph-dedup.js +0 -95
- package/dist/indexer/search/name-match.js +0 -35
- package/dist/indexer/search/ranking-contributors.js +0 -515
- package/dist/indexer/walk/project-context.js +0 -192
- package/dist/integrations/agent/execution-cascade.js +0 -566
- package/dist/integrations/agent/execution-definitions.js +0 -202
- package/dist/integrations/agent/execution-lowering.js +0 -841
- package/dist/integrations/agent/execution-preparation.js +0 -98
- package/dist/integrations/agent/inline-execution.js +0 -74
- package/dist/registry/create-provider-registry.js +0 -29
- package/dist/registry/pinned-request-helper.js +0 -247
- package/dist/registry/pinned-transport.js +0 -717
- package/dist/sources/providers/index.js +0 -14
- package/dist/storage/engines/sqlite-migrations.js +0 -271
- package/dist/storage/repositories/canaries-repository.js +0 -107
- package/dist/storage/repositories/embedding-salvage-repository.js +0 -184
- package/dist/storage/repositories/registry-cache.js +0 -113
- package/dist/tasks/scheduler-sync-preview.js +0 -52
- package/dist/workflows/freeze/resolve-steps.js +0 -86
- package/dist/workflows/freeze/source-freeze.js +0 -64
- package/dist/workflows/ir/compile.js +0 -321
- package/dist/workflows/ir/environment-v4.js +0 -330
- package/dist/workflows/ir/freeze-v4.js +0 -153
- package/dist/workflows/ir/schema-v4.js +0 -745
- package/dist/workflows/ir/schema.js +0 -354
- package/dist/workflows/program/schema.js +0 -78
- package/dist/workflows/runtime/checkin.js +0 -57
- package/dist/workflows/runtime/plan-classifier.js +0 -196
- package/dist/workflows/runtime/unit-checkin.js +0 -45
- package/dist/workflows/runtime/unit-phases.js +0 -20
- package/dist/workflows/schema.js +0 -4
- package/dist/workflows/source-ir/compile.js +0 -200
- package/dist/workflows/source-ir/program.js +0 -50
- package/dist/workflows/source-ir/result.js +0 -26
- package/dist/workflows/source-ir/schema.js +0 -786
- package/dist/workflows/source-ir/triggers.js +0 -79
- package/dist/workflows/source-ir/uses.js +0 -40
- package/dist/workflows/validator.js +0 -60
package/dist/core/state-db.js
CHANGED
|
@@ -4,93 +4,70 @@
|
|
|
4
4
|
/**
|
|
5
5
|
* state.db — Durable SQLite database for non-regenerable akm state.
|
|
6
6
|
*
|
|
7
|
-
* This module OWNS the state database's shared infrastructure: path
|
|
8
|
-
* the
|
|
9
|
-
*
|
|
7
|
+
* This module OWNS the state database's shared infrastructure: path
|
|
8
|
+
* resolution, the open/loan wrappers and schema introspection. The
|
|
9
|
+
* table-specific query helpers live by domain in
|
|
10
10
|
* `src/storage/repositories/*-repository.ts` (events, proposals, task-history,
|
|
11
|
-
* improve-runs, extract-sessions,
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* The state DB stores non-regenerable events, proposals, task history, workflow
|
|
16
|
-
* runs, and improve-pipeline ledgers.
|
|
11
|
+
* improve-runs, improve-ledger, extract-sessions, embeddings); the
|
|
12
|
+
* migration registry lives in `./state/migrations` and the runner in
|
|
13
|
+
* `storage/sqlite-migrations`. The `BEGIN IMMEDIATE` helpers are
|
|
14
|
+
* re-exported from `storage/sqlite-transaction`.
|
|
17
15
|
*
|
|
18
16
|
* ## Why a separate database from index.db
|
|
19
17
|
*
|
|
20
18
|
* index.db is a derived cache built by an idempotent baseline schema; it is fully
|
|
21
19
|
* regenerable from the stash on disk, so a corrupt index is recovered by deleting
|
|
22
|
-
* it and re-running `akm index
|
|
23
|
-
*
|
|
24
|
-
* live in a database whose released migration ledger is
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* the
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* - CREATE TABLE IF NOT EXISTS … (additive new tables)
|
|
20
|
+
* it and re-running `akm index`. Events, proposals, task history, workflow runs
|
|
21
|
+
* and improve-pipeline ledgers are NON-REGENERABLE — losing them is data loss.
|
|
22
|
+
* They live in a database whose released migration ledger is append-only.
|
|
23
|
+
*
|
|
24
|
+
* ## Open sequence
|
|
25
|
+
*
|
|
26
|
+
* {@link openStateDatabase} opens ONE connection: mkdir the parent, open, apply
|
|
27
|
+
* the standard pragmas (`busy_timeout` 30 s first, `journal_mode` WAL with the
|
|
28
|
+
* network-filesystem fallback, `foreign_keys` ON — `storage/sqlite-pragmas`),
|
|
29
|
+
* read the `schema_migrations` ledger on that connection, and run every
|
|
30
|
+
* migration not yet applied inside one `BEGIN IMMEDIATE` transaction. A
|
|
31
|
+
* missing, empty or table-less file is simply a fresh database and gets the
|
|
32
|
+
* whole registry. A ledger carrying ids this akm does not know was written by
|
|
33
|
+
* a newer akm: the open continues with the schema this version knows and warns
|
|
34
|
+
* once ({@link warnNewerStateLedger}). Before a released migration that drops
|
|
35
|
+
* schema (`018-drop-dead-lane-schema`, `028-improve-ledger`; see
|
|
36
|
+
* `STATE_MIGRATION_SAFETY_BY_ID`) runs against an existing database, the
|
|
37
|
+
* database is copied beside itself to `state.db.pre-<id of the first such
|
|
38
|
+
* pending migration>.bak` with `VACUUM INTO` — a plain sibling copy the
|
|
39
|
+
* operator can open, nothing more. A database this open created from scratch is
|
|
40
|
+
* compacted once after its migrations, since the registry creates tables that
|
|
41
|
+
* later migrations drop. Only a ledger that DIVERGES from this akm's registry
|
|
42
|
+
* is refused (`assertMigrationLedger`).
|
|
46
43
|
*
|
|
47
44
|
* ## Schema design: indexed columns vs. metadata_json
|
|
48
45
|
*
|
|
49
46
|
* Each table holds only the columns needed for indexed queries as first-class
|
|
50
|
-
* columns. All other fields live in a `metadata_json TEXT` column (a JSON
|
|
51
|
-
*
|
|
52
|
-
* DDL. This is the same pattern used by `usage_events.metadata` in index.db and
|
|
53
|
-
* by the original events.jsonl format (the `metadata` field was always free-form
|
|
54
|
-
* JSON).
|
|
47
|
+
* columns. All other fields live in a `metadata_json TEXT` column (a JSON
|
|
48
|
+
* object), so new fields can be appended without touching the DDL.
|
|
55
49
|
*
|
|
56
|
-
* ## WAL mode
|
|
50
|
+
* ## WAL mode and writer contention
|
|
57
51
|
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* ## Writer contention (#948)
|
|
64
|
-
*
|
|
65
|
-
* Every open already carries a 30s `busy_timeout` (`sqlite-pragmas.ts`), so a
|
|
66
|
-
* single blocked statement waits before failing. `withImmediateTransaction`
|
|
67
|
-
* additionally retries `BEGIN IMMEDIATE` itself
|
|
68
|
-
* ({@link WITH_IMMEDIATE_TX_MAX_ATTEMPTS} attempts) for the rarer case of two
|
|
69
|
-
* writers racing the BEGIN statement back-to-back. If every attempt is still
|
|
70
|
-
* contention-shaped ({@link isSqliteContentionError} — SQLITE_BUSY/LOCKED, the
|
|
71
|
-
* matching message text, or the phantom-BEGIN marker), the exhaustion throw is
|
|
72
|
-
* reclassified into `TransientError("STATE_DB_CONTENDED")` (exit 75, #948
|
|
73
|
-
* addendum) instead of surfacing the raw driver text: another akm process (an
|
|
74
|
-
* unrelated `improve`, `workflow run`, or task run — not necessarily
|
|
75
|
-
* contending for the same row) is writing state.db right now. A genuinely
|
|
76
|
-
* unrelated error (real corruption, a body-thrown failure) is never
|
|
77
|
-
* reclassified and rethrows exactly as raised.
|
|
52
|
+
* WAL lets readers proceed while a writer is active and replays after a crash;
|
|
53
|
+
* CLI commands are almost always single-writer. Writer contention that
|
|
54
|
+
* outlasts every `BEGIN IMMEDIATE` retry surfaces as
|
|
55
|
+
* `TransientError("STATE_DB_CONTENDED")`, exit 75 (#948) — the one place that
|
|
56
|
+
* mapping lives is `storage/sqlite-transaction`.
|
|
78
57
|
*
|
|
79
58
|
* @module state-db
|
|
80
59
|
*/
|
|
81
|
-
import { randomUUID } from "node:crypto";
|
|
82
60
|
import fs from "node:fs";
|
|
83
61
|
import path from "node:path";
|
|
84
|
-
import { sleepSync } from "../runtime.js";
|
|
85
62
|
import { openDatabase } from "../storage/database.js";
|
|
86
|
-
import { assertMigrationLedger } from "../storage/engines/sqlite-migrations.js";
|
|
87
63
|
import { openManagedDatabase, withManagedDb } from "../storage/managed-db.js";
|
|
64
|
+
import { assertMigrationLedger, runMigrations, } from "../storage/sqlite-migrations.js";
|
|
65
|
+
import { applyReadonlyPragmas } from "../storage/sqlite-pragmas.js";
|
|
88
66
|
import { pkgVersion } from "../version.js";
|
|
89
|
-
import { TransientError } from "./errors.js";
|
|
90
|
-
import { acquireMaintenanceActivitySync } from "./maintenance-barrier.js";
|
|
91
67
|
import { getDataDir } from "./paths.js";
|
|
92
|
-
import {
|
|
68
|
+
import { getStateMigrationSafety, STATE_MIGRATIONS } from "./state/migrations.js";
|
|
93
69
|
import { warnOnce } from "./warn.js";
|
|
70
|
+
export { beginImmediateTransaction, isSqliteContentionError, withImmediateTransaction, } from "../storage/sqlite-transaction.js";
|
|
94
71
|
// ── Path helper ──────────────────────────────────────────────────────────────
|
|
95
72
|
/**
|
|
96
73
|
* Default path: `<dataDir>/state.db`.
|
|
@@ -101,294 +78,7 @@ import { warnOnce } from "./warn.js";
|
|
|
101
78
|
export function getStateDbPath() {
|
|
102
79
|
return path.join(getDataDir(), "state.db");
|
|
103
80
|
}
|
|
104
|
-
function safetyCopyTimestamp() {
|
|
105
|
-
return new Date().toISOString().replaceAll(/[^0-9]/g, "");
|
|
106
|
-
}
|
|
107
|
-
function noFollowFlag() {
|
|
108
|
-
return process.platform !== "win32" && typeof fs.constants.O_NOFOLLOW === "number" ? fs.constants.O_NOFOLLOW : 0;
|
|
109
|
-
}
|
|
110
|
-
function samePhysicalFile(left, right) {
|
|
111
|
-
if (left.dev !== 0n || left.ino !== 0n || right.dev !== 0n || right.ino !== 0n) {
|
|
112
|
-
return left.dev === right.dev && left.ino === right.ino;
|
|
113
|
-
}
|
|
114
|
-
return left.birthtimeNs === right.birthtimeNs && left.rdev === right.rdev;
|
|
115
|
-
}
|
|
116
|
-
function assertOwnedFileReservation(reservation, label) {
|
|
117
|
-
const descriptorStat = fs.fstatSync(reservation.fd, { bigint: true });
|
|
118
|
-
let pathStat;
|
|
119
|
-
try {
|
|
120
|
-
pathStat = fs.lstatSync(reservation.path, { bigint: true });
|
|
121
|
-
}
|
|
122
|
-
catch (error) {
|
|
123
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
124
|
-
throw new Error(`${label} ownership/inode verification failed because its path disappeared: ${detail}`);
|
|
125
|
-
}
|
|
126
|
-
if (pathStat.isSymbolicLink() ||
|
|
127
|
-
!pathStat.isFile() ||
|
|
128
|
-
!descriptorStat.isFile() ||
|
|
129
|
-
!samePhysicalFile(reservation.identity, descriptorStat) ||
|
|
130
|
-
!samePhysicalFile(descriptorStat, pathStat)) {
|
|
131
|
-
throw new Error(`${label} ownership/inode verification failed: its path is a symlink or was replaced.`);
|
|
132
|
-
}
|
|
133
|
-
if (process.platform !== "win32" && typeof process.geteuid === "function") {
|
|
134
|
-
const expectedUid = BigInt(process.geteuid());
|
|
135
|
-
if (descriptorStat.uid !== expectedUid || pathStat.uid !== expectedUid) {
|
|
136
|
-
throw new Error(`${label} ownership verification failed: the reserved file is not owned by the current user.`);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
return pathStat;
|
|
140
|
-
}
|
|
141
|
-
function assertStateDatabaseSource(source) {
|
|
142
|
-
const descriptorStat = fs.fstatSync(source.fd, { bigint: true });
|
|
143
|
-
let pathStat;
|
|
144
|
-
try {
|
|
145
|
-
pathStat = fs.lstatSync(source.path, { bigint: true });
|
|
146
|
-
}
|
|
147
|
-
catch (error) {
|
|
148
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
149
|
-
throw new Error(`state.db source inode verification failed because its path disappeared: ${detail}`);
|
|
150
|
-
}
|
|
151
|
-
if (pathStat.isSymbolicLink() ||
|
|
152
|
-
!pathStat.isFile() ||
|
|
153
|
-
!descriptorStat.isFile() ||
|
|
154
|
-
!samePhysicalFile(source.identity, descriptorStat) ||
|
|
155
|
-
!samePhysicalFile(descriptorStat, pathStat) ||
|
|
156
|
-
descriptorStat.uid !== source.identity.uid ||
|
|
157
|
-
pathStat.uid !== source.identity.uid) {
|
|
158
|
-
throw new Error("state.db source ownership/inode verification failed: its path is a symlink or was replaced.");
|
|
159
|
-
}
|
|
160
|
-
return descriptorStat;
|
|
161
|
-
}
|
|
162
|
-
function descriptorAlias(handle) {
|
|
163
|
-
const candidates = process.platform === "linux"
|
|
164
|
-
? [`/proc/self/fd/${handle.fd}`, `/dev/fd/${handle.fd}`]
|
|
165
|
-
: process.platform === "win32"
|
|
166
|
-
? []
|
|
167
|
-
: [`/dev/fd/${handle.fd}`];
|
|
168
|
-
for (const candidate of candidates) {
|
|
169
|
-
try {
|
|
170
|
-
const stat = fs.statSync(candidate, { bigint: true });
|
|
171
|
-
if (samePhysicalFile(handle.identity, stat))
|
|
172
|
-
return candidate;
|
|
173
|
-
}
|
|
174
|
-
catch {
|
|
175
|
-
// The caller verifies the pathname immediately around SQLite open on
|
|
176
|
-
// platforms without a SQLite-openable descriptor alias.
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
return undefined;
|
|
180
|
-
}
|
|
181
|
-
function sqliteBoundFilePath(handle) {
|
|
182
|
-
// A descriptor-backed alias (/proc/self/fd, /dev/fd) lets SQLite open the
|
|
183
|
-
// exact held inode even if its path gets swapped out from under it. It is
|
|
184
|
-
// an optimization, not the actual protection: every caller re-verifies the
|
|
185
|
-
// held identity (dev/ino/uid) immediately before and after every open that
|
|
186
|
-
// uses this path, so a plain path is safe whenever no alias is available —
|
|
187
|
-
// Windows never has one, and macOS's /dev/fd is a small fixed-size devfs
|
|
188
|
-
// table that a process holding higher fd numbers (as a bundled standalone
|
|
189
|
-
// binary routinely does) can miss entirely. Either way, a swap in that
|
|
190
|
-
// window is still caught by the surrounding identity checks.
|
|
191
|
-
return descriptorAlias(handle) ?? handle.path;
|
|
192
|
-
}
|
|
193
|
-
function closeFileIdentity(handle) {
|
|
194
|
-
try {
|
|
195
|
-
fs.closeSync(handle.fd);
|
|
196
|
-
}
|
|
197
|
-
catch {
|
|
198
|
-
// Preserve the authoritative operation failure.
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
function reserveFreshStateDatabase(dbPath) {
|
|
202
|
-
let fd;
|
|
203
|
-
try {
|
|
204
|
-
fd = fs.openSync(dbPath, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_RDWR | noFollowFlag(), 0o666);
|
|
205
|
-
}
|
|
206
|
-
catch (error) {
|
|
207
|
-
if (error?.code === "EEXIST")
|
|
208
|
-
return undefined;
|
|
209
|
-
throw error;
|
|
210
|
-
}
|
|
211
|
-
try {
|
|
212
|
-
return {
|
|
213
|
-
path: dbPath,
|
|
214
|
-
fd,
|
|
215
|
-
identity: fs.fstatSync(fd, { bigint: true }),
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
catch (error) {
|
|
219
|
-
try {
|
|
220
|
-
fs.closeSync(fd);
|
|
221
|
-
}
|
|
222
|
-
catch {
|
|
223
|
-
// Preserve the fstat failure.
|
|
224
|
-
}
|
|
225
|
-
throw error;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
function openExistingStateDatabaseSource(dbPath) {
|
|
229
|
-
let fd;
|
|
230
|
-
try {
|
|
231
|
-
fd = fs.openSync(dbPath, fs.constants.O_RDONLY | noFollowFlag());
|
|
232
|
-
}
|
|
233
|
-
catch (error) {
|
|
234
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
235
|
-
throw new Error(`Could not bind the existing state.db source inode: ${detail}`);
|
|
236
|
-
}
|
|
237
|
-
try {
|
|
238
|
-
return {
|
|
239
|
-
path: dbPath,
|
|
240
|
-
fd,
|
|
241
|
-
identity: fs.fstatSync(fd, { bigint: true }),
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
catch (error) {
|
|
245
|
-
try {
|
|
246
|
-
fs.closeSync(fd);
|
|
247
|
-
}
|
|
248
|
-
catch {
|
|
249
|
-
// Preserve the fstat failure.
|
|
250
|
-
}
|
|
251
|
-
throw error;
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
function reserveHistoricalSafetyCopy(source, migrationId) {
|
|
255
|
-
const sourceStat = assertStateDatabaseSource(source);
|
|
256
|
-
const finalMode = Number(sourceStat.mode & 384n);
|
|
257
|
-
const prefix = `${source.path}.pre-${migrationId}.${safetyCopyTimestamp()}`;
|
|
258
|
-
for (let attempt = 0; attempt < 32; attempt += 1) {
|
|
259
|
-
const candidate = `${prefix}.${randomUUID()}.bak`;
|
|
260
|
-
let fd;
|
|
261
|
-
try {
|
|
262
|
-
fd = fs.openSync(candidate, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_RDWR | noFollowFlag(), 0o600);
|
|
263
|
-
}
|
|
264
|
-
catch (error) {
|
|
265
|
-
if (error?.code === "EEXIST")
|
|
266
|
-
continue;
|
|
267
|
-
throw error;
|
|
268
|
-
}
|
|
269
|
-
let reservation;
|
|
270
|
-
try {
|
|
271
|
-
reservation = {
|
|
272
|
-
path: candidate,
|
|
273
|
-
fd,
|
|
274
|
-
identity: fs.fstatSync(fd, { bigint: true }),
|
|
275
|
-
};
|
|
276
|
-
// Keep recovery bytes owner-only throughout creation. The source-derived
|
|
277
|
-
// (never broader) final mode is restored only after verification.
|
|
278
|
-
fs.fchmodSync(fd, 0o600);
|
|
279
|
-
assertOwnedFileReservation(reservation, "Reserved state.db safety copy");
|
|
280
|
-
return { reservation, finalMode };
|
|
281
|
-
}
|
|
282
|
-
catch (error) {
|
|
283
|
-
try {
|
|
284
|
-
fs.closeSync(fd);
|
|
285
|
-
}
|
|
286
|
-
catch {
|
|
287
|
-
// Preserve the reservation/ownership failure.
|
|
288
|
-
}
|
|
289
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
290
|
-
throw new Error(`Could not secure reserved state.db safety-copy path ${candidate}: ${detail}. ` +
|
|
291
|
-
"The reserved pathname was not removed.");
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
throw new Error("Could not reserve a unique randomized state.db safety-copy path after 32 attempts.");
|
|
295
|
-
}
|
|
296
|
-
function fsyncDirectory(directory) {
|
|
297
|
-
if (process.platform === "win32")
|
|
298
|
-
return;
|
|
299
|
-
const fd = fs.openSync(directory, fs.constants.O_RDONLY);
|
|
300
|
-
try {
|
|
301
|
-
fs.fsyncSync(fd);
|
|
302
|
-
}
|
|
303
|
-
finally {
|
|
304
|
-
fs.closeSync(fd);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
/**
|
|
308
|
-
* Create one verified, standalone SQLite snapshot immediately before released
|
|
309
|
-
* migration 018 removes its retired tables/column. `VACUUM INTO` includes
|
|
310
|
-
* committed WAL content in one consistent sibling database; a raw file copy
|
|
311
|
-
* would not.
|
|
312
|
-
*/
|
|
313
|
-
function createHistoricalStateSafetyCopy(source, migrationId) {
|
|
314
|
-
const { reservation, finalMode } = reserveHistoricalSafetyCopy(source, migrationId);
|
|
315
|
-
let reader;
|
|
316
|
-
try {
|
|
317
|
-
assertOwnedFileReservation(reservation, "Reserved state.db safety copy");
|
|
318
|
-
assertStateDatabaseSource(source);
|
|
319
|
-
// The migration connection already holds BEGIN IMMEDIATE. A distinct
|
|
320
|
-
// read-only connection bound to the held source inode can snapshot the
|
|
321
|
-
// committed WAL view without trying to VACUUM from inside that transaction.
|
|
322
|
-
reader = openDatabase(sqliteBoundFilePath(source), { readonly: true });
|
|
323
|
-
assertStateDatabaseSource(source);
|
|
324
|
-
reader.prepare("VACUUM INTO ?").run(sqliteBoundFilePath(reservation));
|
|
325
|
-
assertStateDatabaseSource(source);
|
|
326
|
-
reader.close();
|
|
327
|
-
reader = undefined;
|
|
328
|
-
assertOwnedFileReservation(reservation, "Reserved state.db safety copy");
|
|
329
|
-
fs.fsyncSync(reservation.fd);
|
|
330
|
-
assertOwnedFileReservation(reservation, "Reserved state.db safety copy");
|
|
331
|
-
const verified = openDatabase(sqliteBoundFilePath(reservation), {
|
|
332
|
-
readonly: true,
|
|
333
|
-
});
|
|
334
|
-
try {
|
|
335
|
-
const quickCheck = verified.prepare("PRAGMA quick_check").get();
|
|
336
|
-
if (!quickCheck || Object.values(quickCheck)[0] !== "ok") {
|
|
337
|
-
throw new Error("SQLite quick_check did not report ok");
|
|
338
|
-
}
|
|
339
|
-
assertMigrationLedger(verified, STATE_MIGRATIONS);
|
|
340
|
-
}
|
|
341
|
-
finally {
|
|
342
|
-
verified.close();
|
|
343
|
-
}
|
|
344
|
-
assertOwnedFileReservation(reservation, "Reserved state.db safety copy");
|
|
345
|
-
fs.fchmodSync(reservation.fd, finalMode);
|
|
346
|
-
fs.fsyncSync(reservation.fd);
|
|
347
|
-
assertOwnedFileReservation(reservation, "Reserved state.db safety copy");
|
|
348
|
-
fsyncDirectory(path.dirname(reservation.path));
|
|
349
|
-
closeFileIdentity(reservation);
|
|
350
|
-
return reservation.path;
|
|
351
|
-
}
|
|
352
|
-
catch (error) {
|
|
353
|
-
try {
|
|
354
|
-
reader?.close();
|
|
355
|
-
}
|
|
356
|
-
catch {
|
|
357
|
-
// Preserve the snapshot/verification failure below.
|
|
358
|
-
}
|
|
359
|
-
closeFileIdentity(reservation);
|
|
360
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
361
|
-
throw new Error(`Could not create a verified state.db safety copy before ${migrationId}: ${detail}. ` +
|
|
362
|
-
`The reserved safety-copy pathname was not removed: ${reservation.path}`);
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
81
|
// ── Database open ────────────────────────────────────────────────────────────
|
|
366
|
-
/**
|
|
367
|
-
* Open (and initialise / migrate) the state database.
|
|
368
|
-
*
|
|
369
|
-
* @param dbPath - Override the database file path. Pass a tmpdir path in tests
|
|
370
|
-
* to avoid touching the real user cache. Mirrors the `filePath` test seam
|
|
371
|
-
* on `EventsContext`.
|
|
372
|
-
*
|
|
373
|
-
* PRAGMA rationale:
|
|
374
|
-
*
|
|
375
|
-
* journal_mode = WAL
|
|
376
|
-
* Write-Ahead Logging: readers never block writers and vice-versa. Crashes
|
|
377
|
-
* are safe — the WAL is replayed on next open. Required for concurrent CLI
|
|
378
|
-
* invocations that may read while another writes.
|
|
379
|
-
*
|
|
380
|
-
* foreign_keys = ON
|
|
381
|
-
* Enforces FK constraints at runtime. SQLite disables them by default for
|
|
382
|
-
* backwards compatibility; enabling them prevents orphaned rows in tables
|
|
383
|
-
* that reference each other (not used in v1 schema but guards future ones).
|
|
384
|
-
*
|
|
385
|
-
* busy_timeout = 30000
|
|
386
|
-
* When another connection holds a write lock, SQLite retries for up to
|
|
387
|
-
* 30 000 ms before returning SQLITE_BUSY. Without this, the default timeout
|
|
388
|
-
* is 0 ms — any concurrent writer causes an immediate error. 30 s (#589)
|
|
389
|
-
* matches the value used in openDatabase() for index.db; 5 s proved too
|
|
390
|
-
* narrow when a post-inference reindex overlapped a parallel event write.
|
|
391
|
-
*/
|
|
392
82
|
/**
|
|
393
83
|
* Tell the operator once when state.db was migrated by a newer akm than the
|
|
394
84
|
* one running. The open proceeds: every migration this binary knows is already
|
|
@@ -402,193 +92,102 @@ function warnNewerStateLedger(ledger) {
|
|
|
402
92
|
warnOnce("state-db-newer-ledger", `[state.db] This akm (v${pkgVersion}) is older than the state database: ${ledger.detail}. ` +
|
|
403
93
|
"Continuing with the schema this version knows; upgrade akm if its output looks incomplete.");
|
|
404
94
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
95
|
+
/**
|
|
96
|
+
* Copy the database beside itself before the first pending migration that
|
|
97
|
+
* drops schema runs. `VACUUM INTO` writes one consistent standalone file
|
|
98
|
+
* (committed WAL content included — a raw file copy would miss it) through
|
|
99
|
+
* the same connection, into a temporary name that is renamed over the final
|
|
100
|
+
* one only once the copy is complete, so a crash never leaves a half-written
|
|
101
|
+
* `.bak` behind. Returns the copy's path, or undefined when nothing pending
|
|
102
|
+
* is destructive.
|
|
103
|
+
*/
|
|
104
|
+
function backupBeforeDestructiveMigration(db, dbPath, pending) {
|
|
105
|
+
const destructive = pending.find((migration) => getStateMigrationSafety(migration.id) === "historical-destructive");
|
|
106
|
+
if (!destructive)
|
|
107
|
+
return undefined;
|
|
108
|
+
const backupPath = `${dbPath}.pre-${destructive.id}.bak`;
|
|
109
|
+
const partialPath = `${backupPath}.tmp`;
|
|
110
|
+
fs.rmSync(partialPath, { force: true });
|
|
111
|
+
db.prepare("VACUUM INTO ?").run(partialPath);
|
|
112
|
+
fs.renameSync(partialPath, backupPath);
|
|
113
|
+
return backupPath;
|
|
114
|
+
}
|
|
115
|
+
/** Read the ledger, warn on a newer one, back up before destructive DDL, then run what is pending. */
|
|
116
|
+
function migrateStateDatabase(db, dbPath) {
|
|
117
|
+
const ledger = assertMigrationLedger(db, STATE_MIGRATIONS);
|
|
118
|
+
warnNewerStateLedger(ledger);
|
|
119
|
+
const pending = STATE_MIGRATIONS.slice(ledger.migrationIds.length);
|
|
120
|
+
if (pending.length === 0)
|
|
121
|
+
return { applied: [] };
|
|
122
|
+
// A database with no applied migration holds nothing akm wrote: a fresh
|
|
123
|
+
// file, `:memory:`, or an empty file left by an interrupted first open.
|
|
124
|
+
const fresh = ledger.migrationIds.length === 0;
|
|
125
|
+
const backupPath = fresh ? undefined : backupBeforeDestructiveMigration(db, dbPath, pending);
|
|
126
|
+
const applied = runMigrations(db, STATE_MIGRATIONS);
|
|
127
|
+
// The registry creates tables that later migrations drop (018, 028), which
|
|
128
|
+
// leaves free pages behind; a database created by this open starts compact.
|
|
129
|
+
if (fresh && applied.length > 0)
|
|
130
|
+
db.exec("VACUUM");
|
|
131
|
+
return backupPath ? { applied, backupPath } : { applied };
|
|
132
|
+
}
|
|
133
|
+
function openAndMigrate(dbPath) {
|
|
134
|
+
let report = { applied: [] };
|
|
135
|
+
const db = openManagedDatabase({
|
|
136
|
+
path: dbPath,
|
|
137
|
+
pragmas: { dataDir: path.dirname(dbPath) },
|
|
138
|
+
init: (handle) => {
|
|
139
|
+
report = migrateStateDatabase(handle, dbPath);
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
return { db, ...report };
|
|
410
143
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
init: (db) => runMigrations(db, { freshDatabase: true }),
|
|
425
|
-
});
|
|
426
|
-
}
|
|
427
|
-
const isCanonical = path.resolve(resolvedPath) === path.resolve(canonicalPath);
|
|
428
|
-
const releaseActivity = isCanonical ? acquireMaintenanceActivitySync("state-db") : undefined;
|
|
429
|
-
let freshReservation;
|
|
430
|
-
let existingSource;
|
|
431
|
-
let openedDb;
|
|
432
|
-
let existingUnversionedDatabase = false;
|
|
433
|
-
let treatUnversionedAsFresh = false;
|
|
434
|
-
let stateSafetyCopyCreated = false;
|
|
435
|
-
try {
|
|
436
|
-
fs.mkdirSync(path.dirname(resolvedPath), { recursive: true });
|
|
437
|
-
freshReservation = reserveFreshStateDatabase(resolvedPath);
|
|
438
|
-
if (!freshReservation) {
|
|
439
|
-
existingSource = openExistingStateDatabaseSource(resolvedPath);
|
|
440
|
-
const preflight = openDatabase(sqliteBoundFilePath(existingSource), {
|
|
441
|
-
readonly: true,
|
|
442
|
-
});
|
|
443
|
-
try {
|
|
444
|
-
preflight.exec("PRAGMA busy_timeout = 30000");
|
|
445
|
-
const ledger = assertMigrationLedger(preflight, STATE_MIGRATIONS);
|
|
446
|
-
warnNewerStateLedger(ledger);
|
|
447
|
-
existingUnversionedDatabase = ledger.migrationIds.length === 0;
|
|
448
|
-
if (existingUnversionedDatabase && unversionedDatabaseHasNoTables(preflight)) {
|
|
449
|
-
existingUnversionedDatabase = false;
|
|
450
|
-
treatUnversionedAsFresh = true;
|
|
451
|
-
}
|
|
452
|
-
if (existingUnversionedDatabase && !options?.allowHistoricalDestructiveStateUpgrade) {
|
|
453
|
-
throw new Error("Refusing to migrate an existing unversioned state.db during an ordinary managed open. " +
|
|
454
|
-
"Run `akm upgrade` (or `akm migrate apply`) to create a verified snapshot before migration 001 " +
|
|
455
|
-
"and apply it deliberately.");
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
finally {
|
|
459
|
-
preflight.close();
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
const boundSource = existingSource;
|
|
463
|
-
openedDb = openManagedDatabase({
|
|
464
|
-
path: boundSource ? sqliteBoundFilePath(boundSource) : resolvedPath,
|
|
465
|
-
pragmas: { dataDir: path.dirname(resolvedPath) },
|
|
466
|
-
init: (db) => {
|
|
467
|
-
runMigrations(db, {
|
|
468
|
-
freshDatabase: !!freshReservation || treatUnversionedAsFresh,
|
|
469
|
-
existingUnversionedDatabase,
|
|
470
|
-
allowHistoricalDestructiveStateUpgrade: options?.allowHistoricalDestructiveStateUpgrade,
|
|
471
|
-
beforeExistingUnversionedStateMigration: options?.allowHistoricalDestructiveStateUpgrade
|
|
472
|
-
? (migration) => {
|
|
473
|
-
if (!boundSource)
|
|
474
|
-
throw new Error("An existing unversioned state.db has no bound source inode.");
|
|
475
|
-
const safetyCopyPath = createHistoricalStateSafetyCopy(boundSource, migration.id);
|
|
476
|
-
stateSafetyCopyCreated = true;
|
|
477
|
-
options.onHistoricalStateSafetyCopy?.(safetyCopyPath);
|
|
478
|
-
}
|
|
479
|
-
: undefined,
|
|
480
|
-
beforeHistoricalDestructiveMigration: options?.allowHistoricalDestructiveStateUpgrade
|
|
481
|
-
? (migration) => {
|
|
482
|
-
if (stateSafetyCopyCreated)
|
|
483
|
-
return;
|
|
484
|
-
if (!boundSource)
|
|
485
|
-
throw new Error("Historical state migration has no bound source inode.");
|
|
486
|
-
const safetyCopyPath = createHistoricalStateSafetyCopy(boundSource, migration.id);
|
|
487
|
-
stateSafetyCopyCreated = true;
|
|
488
|
-
options.onHistoricalStateSafetyCopy?.(safetyCopyPath);
|
|
489
|
-
}
|
|
490
|
-
: undefined,
|
|
491
|
-
});
|
|
492
|
-
},
|
|
493
|
-
});
|
|
494
|
-
if (existingSource) {
|
|
495
|
-
closeFileIdentity(existingSource);
|
|
496
|
-
existingSource = undefined;
|
|
497
|
-
}
|
|
498
|
-
if (freshReservation) {
|
|
499
|
-
closeFileIdentity(freshReservation);
|
|
500
|
-
freshReservation = undefined;
|
|
501
|
-
}
|
|
502
|
-
const db = openedDb;
|
|
503
|
-
if (!releaseActivity)
|
|
504
|
-
return db;
|
|
505
|
-
let closed = false;
|
|
506
|
-
return {
|
|
507
|
-
prepare: db.prepare.bind(db),
|
|
508
|
-
exec: db.exec.bind(db),
|
|
509
|
-
run: db.run.bind(db),
|
|
510
|
-
transaction: db.transaction.bind(db),
|
|
511
|
-
loadExtension: db.loadExtension.bind(db),
|
|
512
|
-
get inTransaction() {
|
|
513
|
-
return db.inTransaction;
|
|
514
|
-
},
|
|
515
|
-
close() {
|
|
516
|
-
if (closed)
|
|
517
|
-
return;
|
|
518
|
-
closed = true;
|
|
519
|
-
try {
|
|
520
|
-
db.close();
|
|
521
|
-
}
|
|
522
|
-
finally {
|
|
523
|
-
releaseActivity();
|
|
524
|
-
}
|
|
525
|
-
},
|
|
526
|
-
};
|
|
527
|
-
}
|
|
528
|
-
catch (error) {
|
|
529
|
-
if (openedDb) {
|
|
530
|
-
try {
|
|
531
|
-
openedDb.close();
|
|
532
|
-
}
|
|
533
|
-
catch {
|
|
534
|
-
// Preserve the open/migration ownership failure.
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
if (existingSource)
|
|
538
|
-
closeFileIdentity(existingSource);
|
|
539
|
-
if (freshReservation)
|
|
540
|
-
closeFileIdentity(freshReservation);
|
|
541
|
-
releaseActivity?.();
|
|
542
|
-
throw error;
|
|
543
|
-
}
|
|
144
|
+
/**
|
|
145
|
+
* Open (and initialise / migrate) the state database on one connection — see
|
|
146
|
+
* the module header for the sequence.
|
|
147
|
+
*
|
|
148
|
+
* @param dbPath - Override the database file path. Pass a tmpdir path (or
|
|
149
|
+
* `:memory:`) in tests to avoid touching the real user data dir.
|
|
150
|
+
*/
|
|
151
|
+
export function openStateDatabase(dbPath = getStateDbPath()) {
|
|
152
|
+
return openAndMigrate(dbPath).db;
|
|
153
|
+
}
|
|
154
|
+
/** {@link openStateDatabase}, also reporting which migrations this open applied (`akm health`). */
|
|
155
|
+
export function openStateDatabaseWithReport(dbPath = getStateDbPath()) {
|
|
156
|
+
return openAndMigrate(dbPath);
|
|
544
157
|
}
|
|
545
158
|
/**
|
|
546
159
|
* Read-only: the state migration IDs the running akm would apply to `dbPath`,
|
|
547
|
-
* in ledger order. Empty when the database is missing
|
|
548
|
-
*
|
|
549
|
-
* same refusal
|
|
160
|
+
* in ledger order. Empty when the database is missing, current, or was
|
|
161
|
+
* migrated by a newer akm. Never applies anything. A ledger that diverges from
|
|
162
|
+
* this akm's registry throws the same refusal the open does.
|
|
550
163
|
*/
|
|
551
164
|
export function listPendingStateMigrations(dbPath = getStateDbPath()) {
|
|
552
165
|
if (!fs.existsSync(dbPath))
|
|
553
166
|
return [];
|
|
554
|
-
const
|
|
167
|
+
const db = openDatabase(dbPath, { readonly: true, create: false });
|
|
555
168
|
try {
|
|
556
|
-
|
|
557
|
-
const ledger = assertMigrationLedger(
|
|
169
|
+
applyReadonlyPragmas(db);
|
|
170
|
+
const ledger = assertMigrationLedger(db, STATE_MIGRATIONS);
|
|
558
171
|
return STATE_MIGRATIONS.slice(ledger.migrationIds.length).map((migration) => migration.id);
|
|
559
172
|
}
|
|
560
173
|
finally {
|
|
561
|
-
|
|
174
|
+
db.close();
|
|
562
175
|
}
|
|
563
176
|
}
|
|
564
177
|
/**
|
|
565
|
-
* Apply every pending state migration
|
|
566
|
-
*
|
|
567
|
-
*
|
|
568
|
-
* design). Missing/current databases are no-ops. A pre-018 exact ledger, or an
|
|
569
|
-
* unversioned database, is snapshotted beside state.db and verified before the
|
|
570
|
-
* immutable released migration runs.
|
|
178
|
+
* Apply every pending state migration now and report what ran — the state
|
|
179
|
+
* step of `akm migrate apply`. Any open does exactly the same work; this one
|
|
180
|
+
* just says what happened. Missing and current databases are no-ops.
|
|
571
181
|
*/
|
|
572
182
|
export function upgradeHistoricalStateDatabase(dbPath = getStateDbPath()) {
|
|
573
183
|
const pending = listPendingStateMigrations(dbPath);
|
|
574
184
|
if (pending.length === 0)
|
|
575
185
|
return { upgraded: false, applied: [] };
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
safetyCopyPath = copyPath;
|
|
582
|
-
},
|
|
583
|
-
});
|
|
584
|
-
db.close();
|
|
585
|
-
}
|
|
586
|
-
catch (error) {
|
|
587
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
588
|
-
const recovery = safetyCopyPath ? ` Verified safety copy: ${safetyCopyPath}.` : "";
|
|
589
|
-
throw new Error(`${detail}${recovery}`);
|
|
590
|
-
}
|
|
591
|
-
return safetyCopyPath ? { upgraded: true, applied: pending, safetyCopyPath } : { upgraded: true, applied: pending };
|
|
186
|
+
const { db, backupPath } = openAndMigrate(dbPath);
|
|
187
|
+
db.close();
|
|
188
|
+
return backupPath
|
|
189
|
+
? { upgraded: true, applied: pending, safetyCopyPath: backupPath }
|
|
190
|
+
: { upgraded: true, applied: pending };
|
|
592
191
|
}
|
|
593
192
|
/**
|
|
594
193
|
* Run `fn` against state.db, owning the handle unless one is borrowed. The loan
|
|
@@ -601,14 +200,14 @@ export function withStateDb(fn, opts) {
|
|
|
601
200
|
return withManagedDb(() => openStateDatabase(opts?.path), fn, opts);
|
|
602
201
|
}
|
|
603
202
|
/**
|
|
604
|
-
* Fire-and-forget telemetry write to state.db (
|
|
605
|
-
*
|
|
606
|
-
*
|
|
607
|
-
*
|
|
608
|
-
*
|
|
609
|
-
*
|
|
610
|
-
*
|
|
611
|
-
*
|
|
203
|
+
* Fire-and-forget telemetry write to state.db (usage_events' durable home).
|
|
204
|
+
* Skips entirely when state.db does not exist yet (never fabricates an
|
|
205
|
+
* un-migrated DB); otherwise opens it and lowers `busy_timeout` to a short
|
|
206
|
+
* window so a contended state.db (e.g. a reindex finalize holding the write
|
|
207
|
+
* lock) never stalls a hot path — mirrors `withIndexDb`'s
|
|
208
|
+
* `TELEMETRY_BUSY_TIMEOUT_MS`. On a current database the open itself only
|
|
209
|
+
* reads, so it does not wait behind a writer. Callers wrap this in their own
|
|
210
|
+
* try/catch.
|
|
612
211
|
*/
|
|
613
212
|
export function withStateDbTelemetry(fn, busyTimeoutMs = 250) {
|
|
614
213
|
if (!fs.existsSync(getStateDbPath()))
|
|
@@ -622,171 +221,6 @@ export function withStateDbTelemetry(fn, busyTimeoutMs = 250) {
|
|
|
622
221
|
db.close();
|
|
623
222
|
}
|
|
624
223
|
}
|
|
625
|
-
// ── Migration engine ─────────────────────────────────────────────────────────
|
|
626
|
-
//
|
|
627
|
-
// The MIGRATIONS registry + runMigrations live in ./state/migrations (the single
|
|
628
|
-
// append-only ordered source of truth). Imported for internal use by
|
|
629
|
-
// openStateDatabase.
|
|
630
|
-
// ── BEGIN IMMEDIATE transaction helper ───────────────────────────────────────
|
|
631
|
-
/**
|
|
632
|
-
* Run `fn` inside a `BEGIN IMMEDIATE` transaction.
|
|
633
|
-
*
|
|
634
|
-
* `db.transaction()` is DEFERRED by default on both Bun and better-sqlite3,
|
|
635
|
-
* which means two writers can both perform stale preflight reads and only race
|
|
636
|
-
* when they finally attempt the write. Proposal creation and queue mutation
|
|
637
|
-
* need the write lock BEFORE those reads so concurrent processes serialize on
|
|
638
|
-
* the live queue state rather than clobbering each other.
|
|
639
|
-
*/
|
|
640
|
-
/**
|
|
641
|
-
* Whether `err` is one of the SQLite conditions a concurrent-writer race can
|
|
642
|
-
* throw that are transient — the statement did NOT corrupt anything, another
|
|
643
|
-
* writer just holds the lock right now — and therefore safe to retry or
|
|
644
|
-
* reclassify as ordinary contention rather than a genuine failure:
|
|
645
|
-
* - `SQLITE_BUSY` / `SQLITE_LOCKED` (either driver's `.code`).
|
|
646
|
-
* - "database is locked" / "database table is locked" message text.
|
|
647
|
-
* - the phantom-BEGIN marker synthesized below when `BEGIN IMMEDIATE`
|
|
648
|
-
* returns without actually opening a transaction.
|
|
649
|
-
*
|
|
650
|
-
* This is the single shared classifier for "is this ordinary state.db
|
|
651
|
-
* contention" (#948) — `isRetryableBeginError` below delegates to it, and so
|
|
652
|
-
* does {@link WorkflowRunsRepository}'s lease-contention classifier (which
|
|
653
|
-
* additionally matches a couple of corruption-shaped texts specific to its
|
|
654
|
-
* own narrower cross-process race and keeps its own live-lease confirmation
|
|
655
|
-
* before reclassifying). Matching this set alone is never sufficient to
|
|
656
|
-
* declare something DEFINITELY contention when a caller needs corroborating
|
|
657
|
-
* evidence (see the lease path); for `beginImmediateTransaction`'s own
|
|
658
|
-
* exhaustion case there is no such evidence available, so the five 30s
|
|
659
|
-
* `busy_timeout` waits already spent stand as the evidence instead.
|
|
660
|
-
*/
|
|
661
|
-
export function isSqliteContentionError(err) {
|
|
662
|
-
const code = err?.code;
|
|
663
|
-
if (code === "SQLITE_BUSY" || code === "SQLITE_LOCKED")
|
|
664
|
-
return true;
|
|
665
|
-
const msg = (err instanceof Error ? err.message : String(err)).toLowerCase();
|
|
666
|
-
return (msg.includes("database is locked") ||
|
|
667
|
-
msg.includes("database table is locked") ||
|
|
668
|
-
// Phantom BEGIN (see below) — synthesized when BEGIN IMMEDIATE returns
|
|
669
|
-
// without opening a transaction. Safe to retry: fn() has not run.
|
|
670
|
-
msg.includes("did not open a transaction"));
|
|
671
|
-
}
|
|
672
|
-
/**
|
|
673
|
-
* Errors `BEGIN IMMEDIATE` can throw under concurrent-writer contention that
|
|
674
|
-
* are transient (the statement did NOT start a usable transaction) and safe
|
|
675
|
-
* to retry. An error thrown by `fn` is a real failure and is NEVER retried.
|
|
676
|
-
*
|
|
677
|
-
* "cannot start a transaction within a transaction" is deliberately NOT
|
|
678
|
-
* retryable: it means a transaction is already open on this connection (a
|
|
679
|
-
* re-entrant call — handled by the entry guard in withImmediateTransaction),
|
|
680
|
-
* and "retrying" it with a ROLLBACK would destroy the caller's transaction
|
|
681
|
-
* (issue #686).
|
|
682
|
-
*/
|
|
683
|
-
function isRetryableBeginError(err) {
|
|
684
|
-
return isSqliteContentionError(err);
|
|
685
|
-
}
|
|
686
|
-
const WITH_IMMEDIATE_TX_MAX_ATTEMPTS = 5;
|
|
687
|
-
/** Portable synchronous sleep (works under both Bun and Node). Delegates to the runtime boundary's `sleepSync`. */
|
|
688
|
-
function sleepSyncMs(ms) {
|
|
689
|
-
if (ms <= 0)
|
|
690
|
-
return;
|
|
691
|
-
sleepSync(ms);
|
|
692
|
-
}
|
|
693
|
-
/**
|
|
694
|
-
* Open, but deliberately do not finish, an immediate transaction.
|
|
695
|
-
*
|
|
696
|
-
* This is the split-phase counterpart to {@link withImmediateTransaction} for
|
|
697
|
-
* the source-update coordinator: index finalization must mutate state.db in a
|
|
698
|
-
* transaction that remains pending until content, lockfile, and index
|
|
699
|
-
* publication have all succeeded. The caller that asked for this split phase
|
|
700
|
-
* owns the matching COMMIT/ROLLBACK.
|
|
701
|
-
*/
|
|
702
|
-
/**
|
|
703
|
-
* Reclassify an exhausted-retry BEGIN failure that is still contention-shaped
|
|
704
|
-
* (#948) into a `TransientError("STATE_DB_CONTENDED")`, mirroring the
|
|
705
|
-
* RUN_LEASE_HELD precedent (`WorkflowRunsRepository.acquireEngineLease`): the
|
|
706
|
-
* driver text is accurate but unhelpful (`{"ok":false,"error":"database is
|
|
707
|
-
* locked"}`, exit 70/INTERNAL) — this instead reads as a retryable-shortly
|
|
708
|
-
* signal (exit 75, sysexits EX_TEMPFAIL — #948 addendum) with the original
|
|
709
|
-
* error preserved as `cause` for `--verbose`/debugging. A genuinely unrelated
|
|
710
|
-
* error (not contention-shaped) is rethrown exactly as raised, never
|
|
711
|
-
* reclassified.
|
|
712
|
-
*/
|
|
713
|
-
function throwBeginFailure(err) {
|
|
714
|
-
if (isSqliteContentionError(err)) {
|
|
715
|
-
const contended = new TransientError("akm's state database is busy (another akm process is writing it); retry shortly.", "STATE_DB_CONTENDED");
|
|
716
|
-
contended.cause = err;
|
|
717
|
-
throw contended;
|
|
718
|
-
}
|
|
719
|
-
throw err;
|
|
720
|
-
}
|
|
721
|
-
export function beginImmediateTransaction(db) {
|
|
722
|
-
if (db.inTransaction) {
|
|
723
|
-
throw new Error("beginImmediateTransaction requires a connection with no active transaction");
|
|
724
|
-
}
|
|
725
|
-
let lastBeginErr;
|
|
726
|
-
for (let attempt = 1; attempt <= WITH_IMMEDIATE_TX_MAX_ATTEMPTS; attempt++) {
|
|
727
|
-
try {
|
|
728
|
-
db.exec("BEGIN IMMEDIATE");
|
|
729
|
-
if (!db.inTransaction) {
|
|
730
|
-
throw new Error("BEGIN IMMEDIATE did not open a transaction (phantom contention state)");
|
|
731
|
-
}
|
|
732
|
-
return;
|
|
733
|
-
}
|
|
734
|
-
catch (err) {
|
|
735
|
-
lastBeginErr = err;
|
|
736
|
-
if (isRetryableBeginError(err) && attempt < WITH_IMMEDIATE_TX_MAX_ATTEMPTS) {
|
|
737
|
-
if (db.inTransaction) {
|
|
738
|
-
try {
|
|
739
|
-
db.exec("ROLLBACK");
|
|
740
|
-
}
|
|
741
|
-
catch {
|
|
742
|
-
// Transaction already gone — safe to retry BEGIN.
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
sleepSyncMs(2 ** (attempt - 1));
|
|
746
|
-
continue;
|
|
747
|
-
}
|
|
748
|
-
throwBeginFailure(err);
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
throwBeginFailure(lastBeginErr);
|
|
752
|
-
}
|
|
753
|
-
export function withImmediateTransaction(db, fn) {
|
|
754
|
-
// Re-entrancy guard (issue #686): if a transaction is already open on this
|
|
755
|
-
// connection (e.g. a nested withImmediateTransaction call inside an outer
|
|
756
|
-
// frame's fn), join it — run fn directly with no BEGIN/COMMIT/ROLLBACK of
|
|
757
|
-
// our own. Without this, the nested BEGIN throws "cannot start a transaction
|
|
758
|
-
// within a transaction", which the old retry path answered with an
|
|
759
|
-
// unconditional ROLLBACK — destroying the OUTER transaction and leaving its
|
|
760
|
-
// COMMIT to fail with "cannot commit - no transaction is active".
|
|
761
|
-
if (db.inTransaction) {
|
|
762
|
-
return fn();
|
|
763
|
-
}
|
|
764
|
-
beginImmediateTransaction(db);
|
|
765
|
-
try {
|
|
766
|
-
const result = fn();
|
|
767
|
-
if (!db.inTransaction) {
|
|
768
|
-
// The transaction we opened vanished while fn() ran (e.g. an
|
|
769
|
-
// auto-rollback or a stray ROLLBACK inside fn). fn's writes may have
|
|
770
|
-
// escaped serialization, so retrying is unsafe — fail loudly instead of
|
|
771
|
-
// letting COMMIT throw the opaque "cannot commit - no transaction is
|
|
772
|
-
// active" SQLiteError.
|
|
773
|
-
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)");
|
|
774
|
-
}
|
|
775
|
-
db.exec("COMMIT");
|
|
776
|
-
return result;
|
|
777
|
-
}
|
|
778
|
-
catch (err) {
|
|
779
|
-
if (db.inTransaction) {
|
|
780
|
-
try {
|
|
781
|
-
db.exec("ROLLBACK");
|
|
782
|
-
}
|
|
783
|
-
catch {
|
|
784
|
-
// Ignore rollback failures so the original error is preserved.
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
throw err;
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
224
|
// ── schema introspection ─────────────────────────────────────────────────────
|
|
791
225
|
/**
|
|
792
226
|
* Return the subset of `names` that exist as TABLEs in this database, ordered
|