akm-cli 0.9.16 → 0.9.17-alpha.10
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 +2101 -0
- package/STABILITY.md +11 -10
- package/dist/akm +124 -193
- package/dist/akm-migrate +38 -19
- package/dist/assets/hints/cli-hints-full.md +6 -7
- package/dist/assets/improve-strategies/catchup.json +0 -3
- package/dist/assets/improve-strategies/consolidate.json +0 -1
- package/dist/assets/improve-strategies/default.json +1 -2
- package/dist/assets/improve-strategies/proactive-maintenance.json +1 -2
- package/dist/assets/improve-strategies/quick.json +1 -2
- package/dist/assets/improve-strategies/reflect-distill.json +1 -2
- package/dist/assets/improve-strategies/thorough.json +0 -3
- package/dist/assets/prompts/consolidate-pair.md +20 -0
- package/dist/assets/prompts/consolidate-system.md +4 -11
- package/dist/assets/prompts/retrieval-relevance-judge.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +20 -20
- package/dist/assets/stash-skeleton/facts/conventions/domains.md +2 -2
- package/dist/assets/templates/html/health.html +3 -5
- package/dist/cli/retired-commands.js +1 -1
- package/dist/cli/shared.js +6 -2
- package/dist/cli/unknown-flags.js +24 -1
- package/dist/cli.js +68 -10
- 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 +6 -0
- package/dist/commands/health/archive-usage.js +92 -0
- package/dist/commands/health/checks.js +83 -74
- package/dist/commands/health/config-skew.js +38 -0
- package/dist/commands/health/data-dir-usage.js +25 -13
- package/dist/commands/health/egress.js +54 -0
- package/dist/commands/health/html-report.js +1 -42
- package/dist/commands/health/improve-metrics.js +136 -591
- package/dist/commands/health/md-report.js +1 -6
- 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 +14 -120
- package/dist/commands/health/types-improve.js +4 -19
- package/dist/commands/health/windows.js +64 -74
- package/dist/commands/health.js +145 -143
- package/dist/commands/improve/consolidate/chunking.js +26 -117
- package/dist/commands/improve/consolidate/continuity-check.js +137 -0
- package/dist/commands/improve/consolidate/pair-pass.js +791 -0
- package/dist/commands/improve/consolidate/sanitize.js +54 -149
- package/dist/commands/improve/consolidate.js +589 -1127
- 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 -1041
- package/dist/commands/improve/eligibility.js +126 -390
- package/dist/commands/improve/execution.js +8 -10
- package/dist/commands/improve/extract-prompt.js +1 -2
- package/dist/commands/improve/extract.js +487 -1046
- package/dist/commands/improve/feedback-valence.js +0 -25
- package/dist/commands/improve/improve-cli.js +75 -169
- package/dist/commands/improve/improve-result-file.js +10 -66
- package/dist/commands/improve/improve-strategies.js +52 -4
- package/dist/commands/improve/improve-usage-report.js +18 -64
- package/dist/commands/improve/improve.js +480 -1074
- package/dist/commands/improve/ledger.js +119 -0
- package/dist/commands/improve/locks.js +2 -8
- package/dist/commands/improve/loop-stages.js +415 -1073
- package/dist/commands/improve/memory/derived-ref.js +12 -77
- package/dist/commands/improve/memory/memory-belief.js +16 -118
- package/dist/commands/improve/memory/memory-improve.js +266 -14
- package/dist/commands/improve/outcome-loop.js +28 -156
- package/dist/commands/improve/planner.js +5 -15
- package/dist/commands/improve/preparation.js +779 -2319
- package/dist/commands/improve/proactive-maintenance.js +34 -101
- package/dist/commands/improve/reflect-noise.js +104 -280
- package/dist/commands/improve/reflect.js +642 -1353
- package/dist/commands/improve/retrieval-gate.js +127 -0
- package/dist/commands/improve/retrieval-scope.js +92 -0
- package/dist/commands/improve/salience.js +41 -240
- package/dist/commands/improve/session-asset.js +19 -100
- package/dist/commands/improve/stage.js +322 -0
- package/dist/commands/lint/base-linter.js +37 -15
- package/dist/commands/proposal/drain.js +261 -578
- package/dist/commands/proposal/proposal-cli.js +19 -20
- package/dist/commands/proposal/proposal-types.js +31 -24
- package/dist/commands/proposal/proposal.js +38 -8
- package/dist/commands/proposal/propose.js +134 -160
- package/dist/commands/proposal/repository.js +1097 -1394
- 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 +22 -89
- package/dist/commands/read/curate.js +105 -462
- package/dist/commands/read/knowledge.js +3 -2
- package/dist/commands/read/search-cli.js +16 -33
- package/dist/commands/read/search.js +17 -23
- package/dist/commands/read/show.js +57 -108
- package/dist/commands/sources/bundle-cli.js +25 -2
- package/dist/commands/sources/bundle-config-ops.js +4 -0
- package/dist/commands/sources/dangerous-env-audit.js +1 -2
- package/dist/commands/sources/info.js +127 -29
- 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/source-manage.js +9 -2
- package/dist/commands/sources/stash-cli.js +24 -4
- package/dist/commands/tasks/explain.js +10 -13
- package/dist/commands/tasks/tasks-cli.js +12 -13
- package/dist/commands/tasks/tasks.js +350 -936
- package/dist/commands/tasks/validate.js +26 -24
- package/dist/commands/workflow/plan.js +22 -29
- package/dist/commands/workflow-cli.js +4 -4
- package/dist/core/adapter/adapters/akm-adapter.js +2 -1
- package/dist/core/adapter/adapters/akm-lint.js +2 -3
- package/dist/core/adapter/adapters/akm-metadata.js +42 -12
- package/dist/core/adapter/adapters/akm-task-adapter.js +29 -8
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +1 -1
- package/dist/core/adapter/execution-source.js +17 -29
- package/dist/core/asset/asset-placement.js +4 -13
- package/dist/core/asset/frontmatter.js +106 -1
- package/dist/core/asset/resolve-ref.js +1 -1
- package/dist/core/bundle-id.js +42 -5
- package/dist/core/bundle-rename.js +285 -0
- package/dist/core/config/config-io.js +1 -2
- package/dist/core/config/config-schema.js +9 -34
- package/dist/core/config/config-walker.js +1 -1
- package/dist/core/config/config.js +184 -111
- package/dist/core/config/engine-semantics.js +0 -2
- 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 +54 -125
- package/dist/core/config/schema/improve.js +4 -42
- package/dist/core/config/schema/index-config.js +9 -48
- 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-change.js +13 -5
- package/dist/core/file-lock.js +76 -173
- package/dist/core/improve-result.js +35 -7
- package/dist/core/improve-types.js +0 -1
- package/dist/core/logs-db.js +2 -2
- package/dist/core/loopback.js +7 -12
- package/dist/core/non-task-input.js +20 -0
- package/dist/core/parse.js +13 -16
- package/dist/core/paths.js +0 -24
- 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 +123 -61
- package/dist/core/state-db-scope.js +2 -4
- package/dist/core/state-db.js +126 -692
- package/dist/core/time.js +0 -20
- package/dist/core/type-presentation.js +1 -9
- package/dist/core/write-source.js +294 -1005
- 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/llm-cache.js +2 -2
- package/dist/indexer/ensure-index.js +77 -73
- 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 +141 -154
- package/dist/indexer/indexer.js +400 -1124
- package/dist/indexer/links/declared-links.js +90 -0
- package/dist/indexer/materialize-embeddings.js +60 -397
- package/dist/indexer/passes/memory-inference.js +96 -90
- package/dist/indexer/passes/metadata.js +132 -219
- package/dist/indexer/read-preflight.js +0 -7
- package/dist/indexer/scan/doc-to-entry.js +2 -3
- package/dist/indexer/scan/drain-dir.js +1 -1
- package/dist/indexer/search/db-search.js +190 -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 +36 -7
- package/dist/indexer/walk/walker.js +3 -4
- 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/client.js +18 -6
- package/dist/llm/embedders/deterministic.js +2 -3
- package/dist/llm/embedders/profile.js +71 -0
- package/dist/llm/embedders/remote.js +11 -17
- package/dist/llm/feature-gate.js +0 -8
- package/dist/llm/index-passes.js +3 -5
- package/dist/llm/memory-infer.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 +25 -38
- package/dist/output/shapes/passthrough.js +1 -9
- package/dist/{indexer/graph/graph-types.js → output/text/bundle-rename.js} +4 -1
- package/dist/output/text/command-format.js +69 -31
- package/dist/output/text/helpers.js +1 -1
- package/dist/output/text/migrate.js +5 -14
- package/dist/output/text/proposal-format.js +48 -3
- package/dist/output/text/show-format.js +13 -17
- 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 +42948 -92369
- package/dist/scripts/akm-migrate.js +42935 -92354
- 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/connection.js +5 -6
- package/dist/setup/steps/platforms.js +2 -2
- 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 +83 -21
- 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 +209 -0
- package/dist/storage/repositories/index-connection.js +39 -72
- package/dist/storage/repositories/index-entries-repository.js +131 -129
- package/dist/storage/repositories/index-entry-mapper.js +1 -2
- package/dist/storage/repositories/index-entry-schema.js +101 -268
- package/dist/storage/repositories/index-fts-repository.js +86 -256
- package/dist/storage/repositories/index-links-repository.js +143 -0
- package/dist/storage/repositories/index-llm-cache-repository.js +7 -9
- package/dist/storage/repositories/index-meta-repository.js +6 -4
- package/dist/storage/repositories/index-schema.js +257 -325
- 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 +104 -1
- package/dist/storage/repositories/registry-index-cache-repository.js +100 -0
- package/dist/storage/repositories/salience-repository.js +1 -19
- 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 +130 -0
- package/dist/tasks/activation-config.js +134 -62
- package/dist/tasks/backends/cron.js +191 -302
- package/dist/tasks/backends/exec-utils.js +2 -5
- package/dist/tasks/backends/launchd.js +141 -748
- package/dist/tasks/backends/schtasks.js +119 -623
- 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/run/load-task.js +1 -1
- package/dist/tasks/scheduler-binding.js +20 -238
- package/dist/tasks/scheduler-invocation.js +136 -244
- package/dist/tasks/scheduler-lock.js +53 -0
- package/dist/tasks/scheduler-sync.js +368 -679
- package/dist/tasks/source/parse-task-source.js +55 -9
- package/dist/tasks/source/task-source-v3-frozen.js +3 -4
- package/dist/tasks/source/task-to-v4.js +464 -88
- 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 +43 -0
- package/docs/migration/v0.9.1-to-v0.9.2.md +23 -7
- package/docs/reference/cli.md +232 -135
- package/docs/reference/configuration.md +71 -57
- package/docs/reference/data-and-telemetry.md +20 -21
- package/docs/reference/tasks.md +105 -39
- 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 +115 -738
- package/schemas/akm-workflow.json +1 -0
- package/dist/assets/improve-strategies/graph-refresh.json +0 -15
- package/dist/assets/prompts/contradiction-judge.md +0 -33
- package/dist/assets/prompts/graph-extract-system.md +0 -1
- package/dist/assets/prompts/graph-extract-user-prompt.md +0 -35
- package/dist/assets/prompts/metadata-enhance-system.md +0 -1
- package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +0 -4
- 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 -149
- package/dist/commands/improve/distill/promote-memory.js +0 -291
- package/dist/commands/improve/distill/quality-gate.js +0 -337
- package/dist/commands/improve/eval-cases.js +0 -52
- 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 -31
- 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/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/db/graph-db.js +0 -444
- package/dist/indexer/graph/graph-boost.js +0 -427
- package/dist/indexer/graph/graph-dedup.js +0 -95
- package/dist/indexer/graph/graph-extraction.js +0 -1108
- package/dist/indexer/search/name-match.js +0 -35
- package/dist/indexer/search/ranking-contributors.js +0 -515
- package/dist/indexer/search/ranking-types.js +0 -4
- 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/llm/graph-extract.js +0 -728
- package/dist/llm/metadata-enhance.js +0 -96
- 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/tasks/source/task-to-v3.js +0 -507
- 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 -77
- 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
|
@@ -2,69 +2,16 @@
|
|
|
2
2
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
|
-
import { NotFoundError,
|
|
6
|
-
import {
|
|
5
|
+
import { NotFoundError, UsageError } from "../../core/errors.js";
|
|
6
|
+
import { openStateDatabase, withImmediateTransaction } from "../../core/state-db.js";
|
|
7
7
|
import { borrowScopedStateDb, withStateDbScope } from "../../core/state-db-scope.js";
|
|
8
|
-
import {
|
|
8
|
+
import { WORKFLOW_PLAN_VERSION } from "../../workflows/plan.js";
|
|
9
9
|
import { escapeLikePattern } from "../like-pattern.js";
|
|
10
10
|
import { resolveStorageLocations } from "../locations.js";
|
|
11
11
|
import { insertEventOnce, insertEventStrict } from "./events-repository.js";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
return;
|
|
16
|
-
throw new UsageError(`Workflow run ${input.runId} is leased by another engine; direct durable dispatch reservation is forbidden.`, "RESOURCE_ALREADY_EXISTS");
|
|
17
|
-
}
|
|
18
|
-
if (run.engine_lease_holder !== input.claimHolder) {
|
|
19
|
-
throw new UsageError(`Workflow run ${input.runId} lease holder changed; refusing a stale durable dispatch reservation.`, "RESOURCE_ALREADY_EXISTS");
|
|
20
|
-
}
|
|
21
|
-
if (run.engine_lease_until === null || run.engine_lease_until < input.now) {
|
|
22
|
-
throw new UsageError(`Workflow run ${input.runId} engine lease expired before durable dispatch reservation.`, "RESOURCE_ALREADY_EXISTS");
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Whether `error` is one of the specific SQLite conditions a run-lease
|
|
27
|
-
* statement can throw under real cross-process contention on the same row:
|
|
28
|
-
* the shared {@link isSqliteContentionError} classifier (SQLITE_BUSY/LOCKED,
|
|
29
|
-
* "database is locked", "database table is locked", the phantom-BEGIN
|
|
30
|
-
* marker) plus two corruption-shaped message texts a transient contention
|
|
31
|
-
* blip has been observed producing on this specific race, "disk I/O error"
|
|
32
|
-
* and "database disk image is malformed". Matching on this set alone is
|
|
33
|
-
* never sufficient to call something lease contention — see
|
|
34
|
-
* {@link WorkflowRunsRepository.acquireEngineLease}, which additionally
|
|
35
|
-
* requires a fresh read confirming a live lease before substituting the
|
|
36
|
-
* lease-held message for the original error.
|
|
37
|
-
*/
|
|
38
|
-
function isLeaseContentionSqliteError(error) {
|
|
39
|
-
if (isSqliteContentionError(error))
|
|
40
|
-
return true;
|
|
41
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
42
|
-
return message.includes("disk I/O error") || message.includes("database disk image is malformed");
|
|
43
|
-
}
|
|
44
|
-
const LEASE_RETRY_ATTEMPTS = 4;
|
|
45
|
-
const LEASE_RETRY_BASE_DELAY_MS = 15;
|
|
46
|
-
/**
|
|
47
|
-
* Retry a single lease statement across a short, bounded set of attempts when
|
|
48
|
-
* it throws one of {@link isLeaseContentionSqliteError}'s conditions —
|
|
49
|
-
* absorbing a blip that a fresh attempt on the same connection clears on its
|
|
50
|
-
* own. Any other error, or the same error surviving every attempt, propagates
|
|
51
|
-
* unchanged; this never converts a persistent failure into a false success.
|
|
52
|
-
*/
|
|
53
|
-
function runLeaseStatementWithRetry(fn) {
|
|
54
|
-
let lastError;
|
|
55
|
-
for (let attempt = 0; attempt < LEASE_RETRY_ATTEMPTS; attempt += 1) {
|
|
56
|
-
try {
|
|
57
|
-
return fn();
|
|
58
|
-
}
|
|
59
|
-
catch (error) {
|
|
60
|
-
if (!isLeaseContentionSqliteError(error))
|
|
61
|
-
throw error;
|
|
62
|
-
lastError = error;
|
|
63
|
-
if (attempt < LEASE_RETRY_ATTEMPTS - 1)
|
|
64
|
-
sleepSync(LEASE_RETRY_BASE_DELAY_MS * 2 ** attempt);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
throw lastError;
|
|
12
|
+
/** Informational `claim_holder` value stamped on attempt rows: the dispatching process. */
|
|
13
|
+
function dispatchingProcess() {
|
|
14
|
+
return `pid:${process.pid}`;
|
|
68
15
|
}
|
|
69
16
|
/**
|
|
70
17
|
* Repository owning every raw SQL statement against `workflow_runs` and
|
|
@@ -245,7 +192,7 @@ export class WorkflowRunsRepository {
|
|
|
245
192
|
// ── writes ─────────────────────────────────────────────────────────────────
|
|
246
193
|
insertRun(input) {
|
|
247
194
|
// R-R3 (P3a Review log; docs/plans/specs/p4-deletions-closeout.md §8):
|
|
248
|
-
// this
|
|
195
|
+
// this 12-column list is hand-duplicated by publishChildWorkflowRun's own
|
|
249
196
|
// INSERT below, which extends it with parent_run_id/parent_unit_id/
|
|
250
197
|
// invocation_key. A signature refactor to share one INSERT builder was
|
|
251
198
|
// considered and deliberately deferred — see
|
|
@@ -254,9 +201,9 @@ export class WorkflowRunsRepository {
|
|
|
254
201
|
this.db
|
|
255
202
|
.prepare(`INSERT INTO workflow_runs (
|
|
256
203
|
id, workflow_ref, scope_key, workflow_entry_id, workflow_title, status, params_json, current_step_id, created_at, updated_at,
|
|
257
|
-
agent_harness, agent_session_id
|
|
258
|
-
) VALUES (?, ?, ?, ?, ?, 'active', ?, ?, ?, ?, ?,
|
|
259
|
-
.run(input.id, input.workflowRef, input.scopeKey, input.workflowEntryId, input.workflowTitle, input.paramsJson, input.currentStepId, input.createdAt, input.updatedAt, input.agentHarness, input.agentSessionId
|
|
204
|
+
agent_harness, agent_session_id
|
|
205
|
+
) VALUES (?, ?, ?, ?, ?, 'active', ?, ?, ?, ?, ?, ?)`)
|
|
206
|
+
.run(input.id, input.workflowRef, input.scopeKey, input.workflowEntryId, input.workflowTitle, input.paramsJson, input.currentStepId, input.createdAt, input.updatedAt, input.agentHarness, input.agentSessionId);
|
|
260
207
|
}
|
|
261
208
|
insertSteps(steps) {
|
|
262
209
|
const insertStep = this.db.prepare(`INSERT INTO workflow_run_steps (
|
|
@@ -274,9 +221,7 @@ export class WorkflowRunsRepository {
|
|
|
274
221
|
.run(runId, currentStepId);
|
|
275
222
|
}
|
|
276
223
|
markRunActive(runId, updatedAt) {
|
|
277
|
-
this.db
|
|
278
|
-
.prepare("UPDATE workflow_runs SET status = 'active', updated_at = ?, engine_lease_holder = NULL, engine_lease_until = NULL WHERE id = ?")
|
|
279
|
-
.run(updatedAt, runId);
|
|
224
|
+
this.db.prepare("UPDATE workflow_runs SET status = 'active', updated_at = ? WHERE id = ?").run(updatedAt, runId);
|
|
280
225
|
}
|
|
281
226
|
updateStepCompletion(input) {
|
|
282
227
|
this.db
|
|
@@ -288,29 +233,25 @@ export class WorkflowRunsRepository {
|
|
|
288
233
|
updateRunState(input) {
|
|
289
234
|
this.db
|
|
290
235
|
.prepare(`UPDATE workflow_runs
|
|
291
|
-
SET status = ?, current_step_id = ?, updated_at = ?, completed_at =
|
|
236
|
+
SET status = ?, current_step_id = ?, updated_at = ?, completed_at = ?
|
|
292
237
|
WHERE id = ?`)
|
|
293
|
-
.run(input.status, input.currentStepId, input.updatedAt, input.completedAt, input.
|
|
238
|
+
.run(input.status, input.currentStepId, input.updatedAt, input.completedAt, input.runId);
|
|
294
239
|
}
|
|
295
240
|
markRunAbandoned(runId, updatedAt) {
|
|
296
241
|
const result = this.db
|
|
297
242
|
.prepare(`UPDATE workflow_runs
|
|
298
|
-
SET status = 'failed', updated_at = ?, completed_at =
|
|
243
|
+
SET status = 'failed', updated_at = ?, completed_at = ?
|
|
299
244
|
WHERE id = ? AND status IN ('active', 'blocked')`)
|
|
300
|
-
.run(updatedAt, updatedAt,
|
|
245
|
+
.run(updatedAt, updatedAt, runId);
|
|
301
246
|
return Number(result.changes) === 1;
|
|
302
247
|
}
|
|
303
|
-
rearmCheckin(runId, checkinArmedAt) {
|
|
304
|
-
this.db.prepare("UPDATE workflow_runs SET checkin_armed_at = ? WHERE id = ?").run(checkinArmedAt, runId);
|
|
305
|
-
}
|
|
306
248
|
/**
|
|
307
|
-
* Atomically publish the entire
|
|
308
|
-
*
|
|
309
|
-
*
|
|
249
|
+
* Atomically publish the entire run spine. No run row, partial spine, plan
|
|
250
|
+
* attachment, or started event can escape independently across a crash or
|
|
251
|
+
* statement failure.
|
|
310
252
|
*/
|
|
311
253
|
publishWorkflowRunV4(input) {
|
|
312
254
|
this.immediateTransaction((db) => {
|
|
313
|
-
input.revalidateSources();
|
|
314
255
|
if (!input.force) {
|
|
315
256
|
// The uniqueness guard must never silently skip its scope predicate
|
|
316
257
|
// (#942) — every real caller (`startWorkflowRun`) stamps a concrete
|
|
@@ -329,7 +270,7 @@ export class WorkflowRunsRepository {
|
|
|
329
270
|
}
|
|
330
271
|
this.insertRun(input.run);
|
|
331
272
|
this.insertSteps(input.steps);
|
|
332
|
-
db.prepare("UPDATE workflow_runs SET plan_json = ?, plan_hash = ?, plan_ir_version =
|
|
273
|
+
db.prepare("UPDATE workflow_runs SET plan_json = ?, plan_hash = ?, plan_ir_version = ? WHERE id = ?").run(input.planJson, input.planHash, WORKFLOW_PLAN_VERSION, input.run.id);
|
|
333
274
|
insertEventOnce(db, {
|
|
334
275
|
eventType: "workflow_started",
|
|
335
276
|
ts: input.run.createdAt,
|
|
@@ -350,15 +291,12 @@ export class WorkflowRunsRepository {
|
|
|
350
291
|
* `(parent_run_id, invocation_key)` and return the existing child if
|
|
351
292
|
* present; otherwise INSERT the child run row (parentage columns +
|
|
352
293
|
* `invocation_key`), its step rows, attach the embedded frozen child plan
|
|
353
|
-
* (`plan_ir_version =
|
|
354
|
-
* return the freshly-inserted row.
|
|
294
|
+
* (`plan_ir_version` = the current plan version), and append its
|
|
295
|
+
* `workflow_started` event — then return the freshly-inserted row.
|
|
355
296
|
*
|
|
356
|
-
* Deliberately does NOT
|
|
297
|
+
* Deliberately does NOT call {@link findActiveRunForScope} or raise
|
|
357
298
|
* `RESOURCE_ALREADY_EXISTS` (top-level scope-conflict rules do not apply to
|
|
358
|
-
* a child,
|
|
359
|
-
* way (the child plan was frozen and CAS'd into the PARENT's read set at
|
|
360
|
-
* parent freeze, C-11); or touch {@link publishWorkflowRunV4} /
|
|
361
|
-
* `startWorkflowRun` (untouched, C-12).
|
|
299
|
+
* a child), and reads no source: the child plan was frozen into the parent's.
|
|
362
300
|
*
|
|
363
301
|
* This method's serialization guarantee holds only when it is the
|
|
364
302
|
* OUTERMOST transaction on the connection (spec Review log R10,
|
|
@@ -394,16 +332,16 @@ export class WorkflowRunsRepository {
|
|
|
394
332
|
if (existing)
|
|
395
333
|
return existing;
|
|
396
334
|
// R-R3 (P3a Review log; docs/plans/specs/p4-deletions-closeout.md §8):
|
|
397
|
-
// the first
|
|
335
|
+
// the first 12 columns here must stay byte-identical to insertRun's own
|
|
398
336
|
// column list above (hand-duplicated, not shared, by deliberate choice
|
|
399
337
|
// — see docs/architecture/decisions/0009-child-run-publication-column-parity.md).
|
|
400
338
|
// Keep both column lists in sync by hand.
|
|
401
339
|
db.prepare(`INSERT INTO workflow_runs (
|
|
402
340
|
id, workflow_ref, scope_key, workflow_entry_id, workflow_title, status, params_json, current_step_id, created_at, updated_at,
|
|
403
|
-
agent_harness, agent_session_id,
|
|
404
|
-
) VALUES (?, ?, ?, ?, ?, 'active', ?, ?, ?, ?, ?, ?, ?, ?,
|
|
341
|
+
agent_harness, agent_session_id, parent_run_id, parent_unit_id, invocation_key
|
|
342
|
+
) VALUES (?, ?, ?, ?, ?, 'active', ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(input.run.id, input.run.workflowRef, input.run.scopeKey, input.run.workflowEntryId, input.run.workflowTitle, input.run.paramsJson, input.run.currentStepId, input.run.createdAt, input.run.updatedAt, input.run.agentHarness, input.run.agentSessionId, input.parentRunId, input.spawnedByUnitId, input.invocationKey);
|
|
405
343
|
this.insertSteps(input.steps);
|
|
406
|
-
db.prepare("UPDATE workflow_runs SET plan_json = ?, plan_hash = ?, plan_ir_version =
|
|
344
|
+
db.prepare("UPDATE workflow_runs SET plan_json = ?, plan_hash = ?, plan_ir_version = ? WHERE id = ?").run(input.planJson, input.planHash, WORKFLOW_PLAN_VERSION, input.run.id);
|
|
407
345
|
insertEventOnce(db, {
|
|
408
346
|
eventType: "workflow_started",
|
|
409
347
|
ts: input.run.createdAt,
|
|
@@ -436,111 +374,6 @@ export class WorkflowRunsRepository {
|
|
|
436
374
|
.prepare("SELECT * FROM workflow_runs WHERE parent_run_id = ? AND invocation_key = ?")
|
|
437
375
|
.get(parentRunId, key) ?? undefined);
|
|
438
376
|
}
|
|
439
|
-
// ── engine run lease (migration 006 columns, R2 enforcement) ──────────────
|
|
440
|
-
//
|
|
441
|
-
// Single-driver invariant: at most one `akm workflow run` invocation drives
|
|
442
|
-
// a run at a time. The lease is (holder id, expiry); all timestamps are
|
|
443
|
-
// ISO-8601 UTC strings, which compare correctly with SQL `<` (lexicographic
|
|
444
|
-
// order matches chronological order for a fixed-format UTC ISO string).
|
|
445
|
-
/**
|
|
446
|
-
* Atomically claim the run lease: succeeds when the run is unleased OR the
|
|
447
|
-
* existing lease has expired (`engine_lease_until < now` — crash recovery).
|
|
448
|
-
* A live lease held by anyone (including a stale copy of the same holder)
|
|
449
|
-
* is NOT reclaimable through this method; the single UPDATE is the whole
|
|
450
|
-
* claim, so two racing invocations cannot both win.
|
|
451
|
-
*
|
|
452
|
-
* The UPDATE can throw instead of cleanly returning `changes: 0` under real
|
|
453
|
-
* cross-process contention on this row: a `SQLITE_BUSY`/`SQLITE_LOCKED`
|
|
454
|
-
* from two engines racing the same statement, occasionally surfacing as
|
|
455
|
-
* "database is locked" or even "database disk image is malformed" text that
|
|
456
|
-
* reads as corruption but is not. `runLeaseStatementWithRetry` absorbs a
|
|
457
|
-
* blip that a fresh attempt clears on its own. If it is still failing after
|
|
458
|
-
* every retry, the row is read fresh (a plain SELECT, far less likely to
|
|
459
|
-
* trip whatever the write hit) to get independent evidence of what is
|
|
460
|
-
* actually going on: a live lease there means this really was contention,
|
|
461
|
-
* so the caller gets the same lease-held message `akm workflow run` already
|
|
462
|
-
* shows for the clean (non-throwing) case, now with `RUN_LEASE_HELD`. No
|
|
463
|
-
* live lease — or the verifying read itself fails — means the error was
|
|
464
|
-
* never actually about the lease, so it is rethrown exactly as raised.
|
|
465
|
-
* Nothing here invents a diagnosis from error text alone or suppresses a
|
|
466
|
-
* genuine SQLite failure.
|
|
467
|
-
*/
|
|
468
|
-
acquireEngineLease(runId, holder, until, now) {
|
|
469
|
-
try {
|
|
470
|
-
const result = runLeaseStatementWithRetry(() => this.db
|
|
471
|
-
.prepare(`UPDATE workflow_runs
|
|
472
|
-
SET engine_lease_holder = ?, engine_lease_until = ?
|
|
473
|
-
WHERE id = ? AND status = 'active'
|
|
474
|
-
AND (engine_lease_holder IS NULL OR engine_lease_until IS NULL OR engine_lease_until < ?)`)
|
|
475
|
-
.run(holder, until, runId, now));
|
|
476
|
-
return Number(result.changes) > 0;
|
|
477
|
-
}
|
|
478
|
-
catch (error) {
|
|
479
|
-
if (!isLeaseContentionSqliteError(error))
|
|
480
|
-
throw error;
|
|
481
|
-
const row = this.tryReadLeaseColumns(runId);
|
|
482
|
-
if (row?.engine_lease_holder && row.engine_lease_until && row.engine_lease_until >= now) {
|
|
483
|
-
throw new TransientError(`Workflow run ${runId} is already being driven by engine ${row.engine_lease_holder} ` +
|
|
484
|
-
`(run lease expires ${row.engine_lease_until}). A second \`akm workflow run\` would race it — ` +
|
|
485
|
-
`wait for that invocation to finish or for the lease to expire.`, "RUN_LEASE_HELD");
|
|
486
|
-
}
|
|
487
|
-
throw error;
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
/**
|
|
491
|
-
* Extend the lease expiry — only while `holder` still owns it. Returns
|
|
492
|
-
* false when the lease was lost (expired and claimed by another engine),
|
|
493
|
-
* so the caller can stop driving instead of racing the new owner. Wrapped
|
|
494
|
-
* in the same transient-error retry as {@link acquireEngineLease}; a
|
|
495
|
-
* renewal that still fails after retries is rethrown as-is (no confirmed
|
|
496
|
-
* "lost lease" diagnosis to substitute, unlike the acquire case above).
|
|
497
|
-
*/
|
|
498
|
-
renewEngineLease(runId, holder, until) {
|
|
499
|
-
const result = runLeaseStatementWithRetry(() => this.db
|
|
500
|
-
.prepare("UPDATE workflow_runs SET engine_lease_until = ? WHERE id = ? AND engine_lease_holder = ? AND status = 'active'")
|
|
501
|
-
.run(until, runId, holder));
|
|
502
|
-
return Number(result.changes) > 0;
|
|
503
|
-
}
|
|
504
|
-
/**
|
|
505
|
-
* Clear the lease only while `holder` still owns a non-failed run. Failed
|
|
506
|
-
* runs retain the final holder/expiry for forensics until explicit resume.
|
|
507
|
-
* Releasing an already-lost lease is a harmless no-op.
|
|
508
|
-
*/
|
|
509
|
-
releaseEngineLease(runId, holder) {
|
|
510
|
-
this.db
|
|
511
|
-
.prepare("UPDATE workflow_runs SET engine_lease_holder = NULL, engine_lease_until = NULL WHERE id = ? AND engine_lease_holder = ? AND status <> 'failed'")
|
|
512
|
-
.run(runId, holder);
|
|
513
|
-
}
|
|
514
|
-
/**
|
|
515
|
-
* Self-heal an engine lease its holder crashed without releasing: once
|
|
516
|
-
* `engine_lease_until` has passed, clear it so a read (`workflow status`,
|
|
517
|
-
* `workflow list`) stops reporting a run as engine-driven when the engine is
|
|
518
|
-
* long gone — mirroring the maintenance barrier's self-reclaim of a wedged
|
|
519
|
-
* sentinel (`tryAcquireMaintenanceBarrier`) rather than a bespoke mechanism.
|
|
520
|
-
* The WHERE clause repeats the exact (holder, until) snapshot the caller
|
|
521
|
-
* read, so a lease renewed or re-acquired in between never gets clobbered —
|
|
522
|
-
* same compare-and-swap shape as the claim above. Never touches a lease
|
|
523
|
-
* that is still live.
|
|
524
|
-
*/
|
|
525
|
-
reclaimExpiredEngineLease(runId, holder, until, now) {
|
|
526
|
-
if (until >= now)
|
|
527
|
-
return false;
|
|
528
|
-
const result = this.db
|
|
529
|
-
.prepare(`UPDATE workflow_runs
|
|
530
|
-
SET engine_lease_holder = NULL, engine_lease_until = NULL
|
|
531
|
-
WHERE id = ? AND engine_lease_holder = ? AND engine_lease_until = ? AND engine_lease_until < ?`)
|
|
532
|
-
.run(runId, holder, until, now);
|
|
533
|
-
return Number(result.changes) > 0;
|
|
534
|
-
}
|
|
535
|
-
/** Best-effort lease-column read used only to confirm genuine contention after {@link acquireEngineLease} exhausts its retries. `undefined` on any failure — never a diagnosis, just "couldn't confirm". */
|
|
536
|
-
tryReadLeaseColumns(runId) {
|
|
537
|
-
try {
|
|
538
|
-
return (this.db.prepare("SELECT engine_lease_holder, engine_lease_until FROM workflow_runs WHERE id = ?").get(runId) ?? undefined);
|
|
539
|
-
}
|
|
540
|
-
catch {
|
|
541
|
-
return undefined;
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
377
|
// ── durable v4 append-only dispatch attempts (migration 022) ─────────────
|
|
545
378
|
getUnitAttempts(runId, unitId) {
|
|
546
379
|
return this.db
|
|
@@ -571,61 +404,37 @@ export class WorkflowRunsRepository {
|
|
|
571
404
|
};
|
|
572
405
|
}
|
|
573
406
|
/**
|
|
574
|
-
* Reserve or reclaim one
|
|
575
|
-
* projection, and directly-paired started event share one IMMEDIATE
|
|
576
|
-
* transaction.
|
|
577
|
-
*
|
|
407
|
+
* Reserve or reclaim one dispatch attempt. The attempt row, the unit
|
|
408
|
+
* projection, and the directly-paired started event share one IMMEDIATE
|
|
409
|
+
* transaction. A latest attempt still `running` is reclaimed in place —
|
|
410
|
+
* same attempt number, same stable dispatch id, no duplicate start event —
|
|
411
|
+
* so a re-dispatch after a crash stays explicitly at-least-once with an
|
|
412
|
+
* idempotency key a downstream can dedupe on. One process drives a run at a
|
|
413
|
+
* time (the per-run lock file in `workflows/exec/run-workflow.ts`), so a
|
|
414
|
+
* `running` attempt found here is never another live driver's.
|
|
578
415
|
*/
|
|
579
416
|
reserveUnitAttempt(input) {
|
|
417
|
+
const holder = dispatchingProcess();
|
|
580
418
|
return this.immediateTransaction((db) => {
|
|
581
|
-
const run = db
|
|
582
|
-
.prepare("SELECT workflow_ref, status, engine_lease_holder, engine_lease_until FROM workflow_runs WHERE id = ?")
|
|
583
|
-
.get(input.runId);
|
|
419
|
+
const run = db.prepare("SELECT workflow_ref, status FROM workflow_runs WHERE id = ?").get(input.runId);
|
|
584
420
|
if (!run || run.status !== "active") {
|
|
585
421
|
throw new UsageError(`Workflow run ${input.runId} is not active; refusing to reserve a durable dispatch attempt.`, "RESOURCE_ALREADY_EXISTS");
|
|
586
422
|
}
|
|
587
|
-
assertAttemptReservationLease(input, run);
|
|
588
423
|
const latest = db
|
|
589
424
|
.prepare(`SELECT * FROM workflow_run_unit_attempts
|
|
590
425
|
WHERE run_id = ? AND unit_id = ?
|
|
591
426
|
ORDER BY attempt DESC
|
|
592
427
|
LIMIT 1`)
|
|
593
428
|
.get(input.runId, input.unitId);
|
|
594
|
-
if (latest?.status === "running")
|
|
595
|
-
|
|
596
|
-
return { kind: "existing", attempt: latest };
|
|
597
|
-
}
|
|
598
|
-
const currentRunLeaseDisplacedClaim = run.engine_lease_holder === input.claimHolder;
|
|
599
|
-
const expired = latest.claim_expires_at < input.now;
|
|
600
|
-
if (!expired && !currentRunLeaseDisplacedClaim) {
|
|
601
|
-
return { kind: "busy", attempt: latest };
|
|
602
|
-
}
|
|
603
|
-
const reclaimed = db
|
|
604
|
-
.prepare(`UPDATE workflow_run_unit_attempts
|
|
605
|
-
SET claim_holder = ?, claim_expires_at = ?
|
|
606
|
-
WHERE run_id = ? AND unit_id = ? AND attempt = ?
|
|
607
|
-
AND status = 'running' AND dispatch_id = ? AND claim_holder = ?
|
|
608
|
-
AND claim_expires_at = ?`)
|
|
609
|
-
.run(input.claimHolder, input.claimExpiresAt, input.runId, input.unitId, latest.attempt, latest.dispatch_id, latest.claim_holder, latest.claim_expires_at);
|
|
610
|
-
if (Number(reclaimed.changes) !== 1) {
|
|
611
|
-
throw new Error(`Durable attempt ${input.unitId} changed while its displaced claim was reclaimed.`);
|
|
612
|
-
}
|
|
613
|
-
db.prepare(`UPDATE workflow_run_units
|
|
614
|
-
SET claim_holder = ?, claim_expires_at = ?, last_checkin_at = ?
|
|
615
|
-
WHERE run_id = ? AND unit_id = ? AND status = 'running'`).run(input.claimHolder, input.claimExpiresAt, input.now, input.runId, input.unitId);
|
|
616
|
-
const attempt = db
|
|
617
|
-
.prepare(`SELECT * FROM workflow_run_unit_attempts
|
|
618
|
-
WHERE run_id = ? AND unit_id = ? AND attempt = ?`)
|
|
619
|
-
.get(input.runId, input.unitId, latest.attempt);
|
|
620
|
-
return { kind: "reclaimed", attempt };
|
|
621
|
-
}
|
|
429
|
+
if (latest?.status === "running")
|
|
430
|
+
return { kind: "reclaimed", attempt: latest };
|
|
622
431
|
const attemptNumber = (latest?.attempt ?? 0) + 1;
|
|
623
432
|
const dispatchId = randomUUID();
|
|
624
433
|
db.prepare(`INSERT INTO workflow_run_unit_attempts (
|
|
625
434
|
run_id, unit_id, attempt, dispatch_id, step_id, node_id, phase,
|
|
626
435
|
runner, engine, model, input_hash, status, worktree_path, started_at,
|
|
627
436
|
claim_holder, claim_expires_at
|
|
628
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'running', ?, ?, ?, ?)`).run(input.runId, input.unitId, attemptNumber, dispatchId, input.stepId, input.nodeId, input.phase, input.runner, input.engine, input.model, input.inputHash, input.worktreePath ?? null, input.now,
|
|
437
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'running', ?, ?, ?, ?)`).run(input.runId, input.unitId, attemptNumber, dispatchId, input.stepId, input.nodeId, input.phase, input.runner, input.engine, input.model, input.inputHash, input.worktreePath ?? null, input.now, holder, input.now);
|
|
629
438
|
db.prepare(`INSERT INTO workflow_run_units (
|
|
630
439
|
run_id, unit_id, step_id, node_id, parent_unit_id, phase, runner, engine, model,
|
|
631
440
|
status, input_hash, worktree_path, started_at, claim_holder, claim_expires_at, attempts
|
|
@@ -649,8 +458,7 @@ export class WorkflowRunsRepository {
|
|
|
649
458
|
failure_reason = NULL,
|
|
650
459
|
session_id = NULL,
|
|
651
460
|
finished_at = NULL,
|
|
652
|
-
|
|
653
|
-
attempts = excluded.attempts`).run(input.runId, input.unitId, input.stepId, input.nodeId, input.parentUnitId ?? null, input.phase, input.runner, input.engine, input.model, input.inputHash, input.worktreePath ?? null, input.now, input.claimHolder, input.claimExpiresAt, attemptNumber);
|
|
461
|
+
attempts = excluded.attempts`).run(input.runId, input.unitId, input.stepId, input.nodeId, input.parentUnitId ?? null, input.phase, input.runner, input.engine, input.model, input.inputHash, input.worktreePath ?? null, input.now, holder, input.now, attemptNumber);
|
|
654
462
|
insertEventStrict(db, {
|
|
655
463
|
eventType: "workflow_unit_started",
|
|
656
464
|
ts: input.now,
|
|
@@ -672,16 +480,19 @@ export class WorkflowRunsRepository {
|
|
|
672
480
|
return { kind: "reserved", attempt };
|
|
673
481
|
});
|
|
674
482
|
}
|
|
675
|
-
/**
|
|
483
|
+
/**
|
|
484
|
+
* Commit one terminal result, known usage, and finish event. Returns false
|
|
485
|
+
* when the attempt is no longer `running` (already finished) — a duplicate
|
|
486
|
+
* terminal callback never adds usage or a second event.
|
|
487
|
+
*/
|
|
676
488
|
finishUnitAttempt(input) {
|
|
677
489
|
return this.immediateTransaction((db) => {
|
|
678
490
|
const changed = db
|
|
679
491
|
.prepare(`UPDATE workflow_run_unit_attempts
|
|
680
492
|
SET status = ?, result_json = ?, tokens = ?, failure_reason = ?,
|
|
681
493
|
session_id = ?, finished_at = ?
|
|
682
|
-
WHERE run_id = ? AND unit_id = ? AND attempt = ? AND dispatch_id = ?
|
|
683
|
-
|
|
684
|
-
.run(input.status, input.resultJson, input.tokens, input.failureReason, input.sessionId ?? null, input.finishedAt, input.runId, input.unitId, input.attempt, input.dispatchId, input.claimHolder);
|
|
494
|
+
WHERE run_id = ? AND unit_id = ? AND attempt = ? AND dispatch_id = ? AND status = 'running'`)
|
|
495
|
+
.run(input.status, input.resultJson, input.tokens, input.failureReason, input.sessionId ?? null, input.finishedAt, input.runId, input.unitId, input.attempt, input.dispatchId);
|
|
685
496
|
if (Number(changed.changes) !== 1)
|
|
686
497
|
return false;
|
|
687
498
|
const attempt = db
|
|
@@ -692,9 +503,8 @@ export class WorkflowRunsRepository {
|
|
|
692
503
|
.prepare(`UPDATE workflow_run_units
|
|
693
504
|
SET status = ?, result_json = ?, tokens = ?, failure_reason = ?,
|
|
694
505
|
session_id = ?, finished_at = ?
|
|
695
|
-
WHERE run_id = ? AND unit_id = ? AND status = 'running'
|
|
696
|
-
|
|
697
|
-
.run(input.status, input.resultJson, input.tokens, input.failureReason, input.sessionId ?? null, input.finishedAt, input.runId, input.unitId, input.attempt, input.claimHolder);
|
|
506
|
+
WHERE run_id = ? AND unit_id = ? AND status = 'running' AND attempts = ?`)
|
|
507
|
+
.run(input.status, input.resultJson, input.tokens, input.failureReason, input.sessionId ?? null, input.finishedAt, input.runId, input.unitId, input.attempt);
|
|
698
508
|
if (Number(projection.changes) !== 1) {
|
|
699
509
|
throw new Error(`Durable attempt ${input.unitId} has no matching live workflow_run_units projection.`);
|
|
700
510
|
}
|
|
@@ -754,9 +564,8 @@ export class WorkflowRunsRepository {
|
|
|
754
564
|
* - Inside a {@link withWorkflowRunsConnection} scope, the ambient handle is
|
|
755
565
|
* BORROWED and left open for the rest of the scope. A wide `map` fan-out
|
|
756
566
|
* therefore opens ONE connection for the whole step instead of two per unit
|
|
757
|
-
* (insert + finish) — `openStateDatabase`
|
|
758
|
-
*
|
|
759
|
-
* the per-call cost is milliseconds, not microseconds.
|
|
567
|
+
* (insert + finish) — `openStateDatabase` opens a read-only ledger-preflight
|
|
568
|
+
* handle on every call, so the per-call cost is milliseconds, not microseconds.
|
|
760
569
|
* - Outside a scope the behaviour is unchanged: open a fresh connection, run
|
|
761
570
|
* `fn`, close it in a `finally`.
|
|
762
571
|
*
|
|
@@ -789,7 +598,7 @@ export async function withWorkflowRunsRepo(fn) {
|
|
|
789
598
|
* logically concurrent units cannot interleave statements on the shared handle
|
|
790
599
|
* in a single-threaded event loop — sharing REMOVES in-process writer
|
|
791
600
|
* contention instead of creating it. Cross-process arbitration (WAL,
|
|
792
|
-
* `busy_timeout`, the run
|
|
601
|
+
* `busy_timeout`, the per-run lock file) is untouched. See `core/state-db-scope.ts` for
|
|
793
602
|
* the escaped-async-work guard.
|
|
794
603
|
*/
|
|
795
604
|
export function withWorkflowRunsConnection(fn) {
|
|
@@ -0,0 +1,136 @@
|
|
|
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
|
+
import { withImmediateTransaction } from "./sqlite-transaction.js";
|
|
5
|
+
/**
|
|
6
|
+
* Reject a `MIGRATIONS` array containing a duplicate `id`.
|
|
7
|
+
*
|
|
8
|
+
* A compiled-in registry can't diverge at runtime, so this is a dev-time
|
|
9
|
+
* invariant, not a per-open guard: each consumer (state.db, logs.db) calls it
|
|
10
|
+
* once on its own array at module load, and `tests/storage/sqlite-migrations.test.ts`
|
|
11
|
+
* pins the duplicate-detection behavior directly. {@link inspectMigrationLedger}
|
|
12
|
+
* does NOT call this — re-scanning the same compiled-in array on every DB open
|
|
13
|
+
* added no safety over the module-load check, only repeated O(n) cost.
|
|
14
|
+
*/
|
|
15
|
+
export function assertMigrationRegistry(migrations) {
|
|
16
|
+
const seen = new Set();
|
|
17
|
+
for (const migration of migrations) {
|
|
18
|
+
if (seen.has(migration.id))
|
|
19
|
+
throw new Error(`Migration registry contains duplicate ID ${migration.id}.`);
|
|
20
|
+
seen.add(migration.id);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function migrationLedgerExists(db) {
|
|
24
|
+
return !!db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'schema_migrations'").get();
|
|
25
|
+
}
|
|
26
|
+
/** Inspect the database's applied IDs against the exact ordered registry prefix. */
|
|
27
|
+
function inspectLedgerAgainst(db, registryIds) {
|
|
28
|
+
if (!migrationLedgerExists(db))
|
|
29
|
+
return { status: registryIds.length === 0 ? "current" : "old", migrationIds: [] };
|
|
30
|
+
const rows = db.prepare("SELECT id FROM schema_migrations ORDER BY rowid").all();
|
|
31
|
+
const migrationIds = rows.map((row) => row.id);
|
|
32
|
+
for (const [index, row] of rows.entries()) {
|
|
33
|
+
const expectedId = registryIds[index];
|
|
34
|
+
// Every id this binary knows matched in order and the ledger carries more:
|
|
35
|
+
// the database was migrated by a newer akm. Nothing here is applicable —
|
|
36
|
+
// this binary's whole registry is already applied — so this is version
|
|
37
|
+
// skew, not divergence.
|
|
38
|
+
if (!expectedId) {
|
|
39
|
+
return {
|
|
40
|
+
status: "newer",
|
|
41
|
+
migrationIds,
|
|
42
|
+
detail: `applied migration ID${rows.length - registryIds.length === 1 ? "" : "s"} ${migrationIds
|
|
43
|
+
.slice(registryIds.length)
|
|
44
|
+
.join(", ")} unknown to this akm`,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
// A mismatch at a position this binary has a migration for is divergence,
|
|
48
|
+
// whether or not the id is one this binary knows later: this binary's
|
|
49
|
+
// migration at `index` was never applied, and something else was.
|
|
50
|
+
if (row.id !== expectedId) {
|
|
51
|
+
return {
|
|
52
|
+
status: "inconsistent",
|
|
53
|
+
migrationIds,
|
|
54
|
+
detail: `migration ledger is not an exact ordered prefix at position ${index + 1} (found '${row.id}', expected '${expectedId}'). ` +
|
|
55
|
+
`Applied, in order: [${migrationIds.join(", ")}]. This akm's expected order: [${registryIds.join(", ")}].`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
status: rows.length === registryIds.length ? "current" : "old",
|
|
61
|
+
migrationIds,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export function inspectMigrationLedger(db, migrations) {
|
|
65
|
+
return inspectLedgerAgainst(db, migrations.map((migration) => migration.id));
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Reject only a ledger this binary cannot reason about at all.
|
|
69
|
+
*
|
|
70
|
+
* A `newer` ledger — an exact ordered prefix of this binary's registry plus
|
|
71
|
+
* migrations a later akm added — is NOT rejected. Two akm versions sharing one
|
|
72
|
+
* data directory is a supported deployment (a bundled CLI alongside a newer
|
|
73
|
+
* global install), and refusing the open bricked the older one for every
|
|
74
|
+
* command while protecting nothing: its entire registry is already applied, so
|
|
75
|
+
* it has no pending migration to run. Callers that want to tell an operator
|
|
76
|
+
* about the skew read {@link MigrationLedgerState.status}.
|
|
77
|
+
*
|
|
78
|
+
* An `inconsistent` ledger is different: this binary has a migration that was
|
|
79
|
+
* never applied and something else was applied in its place, so running the
|
|
80
|
+
* pending set could conflict with schema it cannot see. That still refuses.
|
|
81
|
+
*/
|
|
82
|
+
export function assertMigrationLedger(db, migrations) {
|
|
83
|
+
const state = inspectMigrationLedger(db, migrations);
|
|
84
|
+
if (state.status === "inconsistent") {
|
|
85
|
+
throw new Error(`Refusing a database whose migrations are not an exact ordered prefix: ${state.detail} ` +
|
|
86
|
+
"Applying this binary's missing migration now could run it against a schema a later migration already " +
|
|
87
|
+
"changed underneath it, which is a real risk of producing a wrong schema — not something akm can guess " +
|
|
88
|
+
"its way out of safely. This usually means the database was migrated by an incompatible akm build or " +
|
|
89
|
+
"fork, or schema_migrations was edited by hand. Restore this file from a backup taken before the " +
|
|
90
|
+
"divergence, or — if there is no backup and the data is not needed — delete it and let akm rebuild it " +
|
|
91
|
+
"from scratch (a derived index.db regenerates from your sources on the next 'akm index'; state.db loses " +
|
|
92
|
+
"durable history such as improve/proposal state and must be treated as a last resort).");
|
|
93
|
+
}
|
|
94
|
+
return state;
|
|
95
|
+
}
|
|
96
|
+
/** Create the migrations ledger table if it does not exist. */
|
|
97
|
+
export function ensureMigrationsTable(db) {
|
|
98
|
+
db.exec(`
|
|
99
|
+
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
100
|
+
id TEXT PRIMARY KEY,
|
|
101
|
+
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
102
|
+
);
|
|
103
|
+
`);
|
|
104
|
+
}
|
|
105
|
+
/** The registry entries not yet recorded in the ledger, in order. Throws on a divergent ledger. */
|
|
106
|
+
export function pendingMigrations(db, migrations) {
|
|
107
|
+
return migrations.slice(assertMigrationLedger(db, migrations).migrationIds.length);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Apply every pending migration in one `BEGIN IMMEDIATE` transaction.
|
|
111
|
+
*
|
|
112
|
+
* A database with nothing pending is only read, never write-locked. Otherwise
|
|
113
|
+
* the write lock is taken up front — a second process bootstrapping the same
|
|
114
|
+
* database WAITS for the first to commit instead of racing it — and the
|
|
115
|
+
* pending set is re-read under that lock, so the process that lost the race
|
|
116
|
+
* finds nothing left to do rather than re-running DDL. Each migration's ledger
|
|
117
|
+
* row is inserted right after its SQL inside the same transaction: a failing
|
|
118
|
+
* migration rolls back every migration this call applied, and their ledger
|
|
119
|
+
* rows with them. Contention that outlasts every BEGIN retry surfaces as
|
|
120
|
+
* `TransientError("STATE_DB_CONTENDED")` (`../sqlite-transaction`). Returns the
|
|
121
|
+
* IDs this call applied, in order — empty when another process got there first.
|
|
122
|
+
*/
|
|
123
|
+
export function runMigrations(db, migrations) {
|
|
124
|
+
if (pendingMigrations(db, migrations).length === 0)
|
|
125
|
+
return [];
|
|
126
|
+
return withImmediateTransaction(db, () => {
|
|
127
|
+
ensureMigrationsTable(db);
|
|
128
|
+
const applied = [];
|
|
129
|
+
for (const migration of pendingMigrations(db, migrations)) {
|
|
130
|
+
db.exec(migration.up);
|
|
131
|
+
db.prepare("INSERT INTO schema_migrations (id) VALUES (?)").run(migration.id);
|
|
132
|
+
applied.push(migration.id);
|
|
133
|
+
}
|
|
134
|
+
return applied;
|
|
135
|
+
});
|
|
136
|
+
}
|
|
@@ -95,17 +95,19 @@ export function isNetworkFilesystem(fsType) {
|
|
|
95
95
|
return false;
|
|
96
96
|
return NETWORK_FS_MAGICS.has(fsType);
|
|
97
97
|
}
|
|
98
|
-
/**
|
|
98
|
+
/** How long a statement waits for a lock before failing with SQLITE_BUSY. */
|
|
99
|
+
export const SQLITE_BUSY_TIMEOUT_MS = 30_000;
|
|
99
100
|
/**
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
* the
|
|
105
|
-
*
|
|
106
|
-
* fallback and `AKM_SQLITE_JOURNAL_MODE` can select, where readers do block.
|
|
101
|
+
* The standard set for a READ-ONLY handle: just `busy_timeout`. A read-only
|
|
102
|
+
* connection cannot run the rest (journal_mode and foreign_keys are write
|
|
103
|
+
* operations), but SQLite's default timeout of 0 makes reads fail INSTANTLY
|
|
104
|
+
* under writer contention — which matters in the DELETE/TRUNCATE journal modes
|
|
105
|
+
* the network-filesystem fallback and `AKM_SQLITE_JOURNAL_MODE` can select,
|
|
106
|
+
* where readers do block.
|
|
107
107
|
*/
|
|
108
|
-
export
|
|
108
|
+
export function applyReadonlyPragmas(db) {
|
|
109
|
+
db.exec(`PRAGMA busy_timeout = ${SQLITE_BUSY_TIMEOUT_MS}`);
|
|
110
|
+
}
|
|
109
111
|
/**
|
|
110
112
|
* Apply AKM's standard opening PRAGMAs to `db`, in order:
|
|
111
113
|
* 1. `journal_mode` = the configured mode (with WAL→DELETE network-FS fallback)
|