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
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
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
|
+
import { rethrowIfTestIsolationError } from "../../core/errors.js";
|
|
5
|
+
import { formatRegistryError } from "../../core/registry-url.js";
|
|
6
|
+
import { warn } from "../../core/warn.js";
|
|
7
|
+
import { closeDatabase, openIndexDatabase } from "./index-connection.js";
|
|
4
8
|
/**
|
|
5
9
|
* Upsert a registry index cache entry in index.db.
|
|
6
10
|
*
|
|
@@ -44,3 +48,99 @@ export function getRegistryIndexCache(db, registryUrl, maxAgeMs = 3_600_000 /* 1
|
|
|
44
48
|
return undefined;
|
|
45
49
|
return { indexJson: row.index_json, etag: row.etag, lastModified: row.last_modified };
|
|
46
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Open the cache DB (a failed open yields `db = undefined`; the bun-test
|
|
53
|
+
* isolation guard is re-thrown), run `fn`, and close the DB only after `fn`
|
|
54
|
+
* has fully settled — the callbacks are async, and closing earlier would tear
|
|
55
|
+
* the DB down mid-write.
|
|
56
|
+
*/
|
|
57
|
+
export async function withRegistryCacheDb(fn) {
|
|
58
|
+
let db;
|
|
59
|
+
try {
|
|
60
|
+
db = openIndexDatabase();
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
rethrowIfTestIsolationError(err);
|
|
64
|
+
db = undefined;
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
return await fn(db);
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
if (db) {
|
|
71
|
+
try {
|
|
72
|
+
closeDatabase(db);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
/* ignore */
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Returns a fresh cache hit when present, otherwise fetches live (writing the
|
|
82
|
+
* result back best-effort), and falls back to a stale cache row when the
|
|
83
|
+
* fetch fails.
|
|
84
|
+
*/
|
|
85
|
+
export async function fetchCachedJson(opts) {
|
|
86
|
+
const { cacheKey, ttlMs, parseCache, fetchFresh } = opts;
|
|
87
|
+
return withRegistryCacheDb(async (db) => {
|
|
88
|
+
let dbCacheResult;
|
|
89
|
+
try {
|
|
90
|
+
if (db) {
|
|
91
|
+
dbCacheResult = getRegistryIndexCache(db, cacheKey, ttlMs);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
// Never mask the bun-test isolation guard as "DB unavailable" — see
|
|
96
|
+
// rethrowIfTestIsolationError in src/core/errors.ts.
|
|
97
|
+
rethrowIfTestIsolationError(err);
|
|
98
|
+
// index.db read failed (pre-migration install or test env) — fall through
|
|
99
|
+
}
|
|
100
|
+
if (dbCacheResult) {
|
|
101
|
+
const cached = parseCache(dbCacheResult.indexJson, { stale: false });
|
|
102
|
+
if (cached !== undefined) {
|
|
103
|
+
return cached;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
const { value, cacheJson } = await fetchFresh();
|
|
108
|
+
if (db) {
|
|
109
|
+
try {
|
|
110
|
+
upsertRegistryIndexCache(db, cacheKey, cacheJson);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
/* best-effort */
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return value;
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
// Fetch failed — use stale DB cache if available.
|
|
120
|
+
if (dbCacheResult) {
|
|
121
|
+
const stale = parseCache(dbCacheResult.indexJson, { stale: true });
|
|
122
|
+
if (stale !== undefined)
|
|
123
|
+
return stale;
|
|
124
|
+
}
|
|
125
|
+
// No in-TTL row — consult the cache PAST its TTL before giving up: a
|
|
126
|
+
// briefly unreachable registry should degrade to the last-known index,
|
|
127
|
+
// loudly, not hard-fail the command.
|
|
128
|
+
try {
|
|
129
|
+
const expiredRow = db ? getRegistryIndexCache(db, cacheKey, Number.POSITIVE_INFINITY) : undefined;
|
|
130
|
+
if (expiredRow) {
|
|
131
|
+
const stale = parseCache(expiredRow.indexJson, { stale: true });
|
|
132
|
+
if (stale !== undefined) {
|
|
133
|
+
warn(`Registry fetch failed (${formatRegistryError(err)}); ` +
|
|
134
|
+
"serving the last cached index, which is past its refresh interval.");
|
|
135
|
+
return stale;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch (cacheErr) {
|
|
140
|
+
rethrowIfTestIsolationError(cacheErr);
|
|
141
|
+
// cache read failed — fall through to the original fetch error
|
|
142
|
+
}
|
|
143
|
+
throw err;
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { isRecord } from "../../core/common.js";
|
|
13
13
|
import { warnOnce } from "../../core/warn.js";
|
|
14
|
+
import { escapeLikePattern } from "../like-pattern.js";
|
|
14
15
|
function metadataError(message) {
|
|
15
16
|
throw new Error(`invalid task_history metadata_json: ${message}`);
|
|
16
17
|
}
|
|
@@ -63,9 +64,9 @@ export function decodeTaskHistoryMetadata(input) {
|
|
|
63
64
|
if (typeof parsed.durationMs !== "number")
|
|
64
65
|
metadataError("durationMs must be a number");
|
|
65
66
|
const detail = "detail" in parsed ? parsed.detail : null;
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
// A malformed `engine` (wrong type) is dropped like any other unrecognized
|
|
68
|
+
// field rather than rejecting the whole row over a non-essential value.
|
|
69
|
+
const engine = parsed.engine === null || typeof parsed.engine === "string" ? parsed.engine : undefined;
|
|
69
70
|
if (parsed.targetVocab !== undefined && parsed.targetVocab !== 2) {
|
|
70
71
|
warnOnce(`task-history-target-vocab:${String(parsed.targetVocab)}`, `task_history row has targetVocab ${String(parsed.targetVocab)}, newer than this akm's 2 — falling back to ` +
|
|
71
72
|
"the legacy target_kind mapping rather than rejecting the row.");
|
|
@@ -83,7 +84,7 @@ export function decodeTaskHistoryMetadata(input) {
|
|
|
83
84
|
metadataVersion: 2,
|
|
84
85
|
durationMs: parsed.durationMs,
|
|
85
86
|
detail: cleanDetail,
|
|
86
|
-
...(
|
|
87
|
+
...(engine !== undefined ? { engine } : {}),
|
|
87
88
|
...(parsed.targetVocab === 2 ? { targetVocab: 2 } : {}),
|
|
88
89
|
};
|
|
89
90
|
}
|
|
@@ -207,3 +208,24 @@ export function queryCompletedTaskIntervals(db, since, until) {
|
|
|
207
208
|
: "SELECT started_at, completed_at FROM task_history WHERE task_id = 'akm-improve' AND started_at >= ? AND completed_at IS NOT NULL ORDER BY started_at";
|
|
208
209
|
return (until ? db.prepare(sql).all(since, until) : db.prepare(sql).all(since));
|
|
209
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* Preview counterpart of {@link renameTaskHistoryTargetRefs} for `akm bundle
|
|
213
|
+
* rename --dry-run`: the same `WHERE` predicate, read-only.
|
|
214
|
+
*/
|
|
215
|
+
export function countTaskHistoryTargetRefs(db, oldBundleId) {
|
|
216
|
+
const prefix = `${escapeLikePattern(oldBundleId)}//`;
|
|
217
|
+
return db.prepare(`SELECT COUNT(*) AS n FROM task_history WHERE target_ref LIKE ? ESCAPE '\\'`).get(`${prefix}%`).n;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Rewrite every `task_history.target_ref` naming `oldBundleId` (a workflow
|
|
221
|
+
* run's fully-qualified `<bundle>//conceptId` target) to `newBundleId`
|
|
222
|
+
* (`akm bundle rename`, D6). A row whose `target_ref` is unset or not
|
|
223
|
+
* bundle-qualified is not matched by the `LIKE` prefix and is left alone.
|
|
224
|
+
* Returns the number of rows rewritten.
|
|
225
|
+
*/
|
|
226
|
+
export function renameTaskHistoryTargetRefs(db, oldBundleId, newBundleId) {
|
|
227
|
+
const prefix = `${escapeLikePattern(oldBundleId)}//`;
|
|
228
|
+
return Number(db
|
|
229
|
+
.prepare(`UPDATE task_history SET target_ref = ? || substr(target_ref, ?) WHERE target_ref LIKE ? ESCAPE '\\'`)
|
|
230
|
+
.run(newBundleId, oldBundleId.length + 1, `${prefix}%`).changes);
|
|
231
|
+
}
|
|
@@ -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) {
|