akm-cli 0.9.1 → 0.9.2-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +103 -28
- package/README.md +3 -1
- package/SECURITY.md +1 -1
- package/STABILITY.md +1 -1
- package/dist/akm +2 -2
- package/dist/akm-migrate +2 -2
- package/dist/assets/hints/cli-hints-full.md +14 -9
- package/dist/assets/improve-strategies/proactive-maintenance.json +1 -1
- package/dist/assets/improve-strategies/reflect-distill.json +1 -1
- package/dist/assets/models.json +35 -0
- package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +3 -4
- package/dist/assets/stash-skeleton/facts/conventions/organization.md +1 -3
- package/dist/assets/tasks/core/extract.yml +6 -5
- package/dist/assets/tasks/core/improve.yml +6 -5
- package/dist/assets/tasks/core/index-refresh.yml +6 -5
- package/dist/assets/tasks/core/sync.yml +6 -5
- package/dist/assets/tasks/core/version-check.yml +6 -5
- package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-catchup.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-consolidate.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-frequent.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-nightly.yml +6 -5
- package/dist/cli/confirm.js +2 -2
- package/dist/cli/parse-args.js +3 -24
- package/dist/cli/retired-commands.js +1 -1
- package/dist/cli/shared.js +2 -2
- package/dist/cli.js +11 -9
- package/dist/commands/agent/agent-dispatch.js +55 -89
- package/dist/commands/agent/contribute-cli.js +12 -45
- package/dist/commands/command/builtin-action.js +32 -0
- package/dist/commands/command/command-cli.js +99 -0
- package/dist/commands/command/command-execution.js +308 -0
- package/dist/commands/command/execution-source-loader.js +176 -0
- package/dist/commands/command/portable-template.js +60 -0
- package/dist/commands/config-cli.js +10 -4
- package/dist/commands/env/env.js +4 -2
- package/dist/commands/feedback-cli.js +1 -1
- package/dist/commands/health/checks.js +241 -29
- package/dist/commands/health/html-report.js +0 -14
- package/dist/commands/health/report-view-model.js +0 -1
- package/dist/commands/health/surfaces.js +6 -7
- package/dist/commands/health/types.js +0 -2
- package/dist/commands/health.js +63 -18
- package/dist/commands/improve/collapse-detector.js +5 -6
- package/dist/commands/improve/consolidate.js +251 -214
- package/dist/commands/improve/distill/promote-memory.js +71 -34
- package/dist/commands/improve/distill/quality-gate.js +17 -5
- package/dist/commands/improve/distill.js +232 -155
- package/dist/commands/improve/eligibility.js +112 -79
- package/dist/commands/improve/execution.js +57 -0
- package/dist/commands/improve/extract-cli.js +5 -5
- package/dist/commands/improve/extract-prompt.js +64 -22
- package/dist/commands/improve/extract.js +608 -360
- package/dist/commands/improve/improve-strategies.js +43 -14
- package/dist/commands/improve/improve.js +249 -29
- package/dist/commands/improve/loop-stages.js +11 -17
- package/dist/commands/improve/memory/memory-contradiction-detect.js +90 -66
- package/dist/commands/improve/outcome-loop.js +22 -38
- package/dist/commands/improve/planner.js +134 -0
- package/dist/commands/improve/preparation.js +730 -409
- package/dist/commands/improve/reflect.js +386 -223
- package/dist/commands/improve/run-context.js +3 -4
- package/dist/commands/improve/salience.js +6 -58
- package/dist/commands/improve/session-asset.js +12 -12
- package/dist/commands/lint/index.js +101 -29
- package/dist/commands/migrate-cli.js +11 -69
- package/dist/commands/migration-tool.js +6 -9
- package/dist/commands/models-cli.js +27 -0
- package/dist/commands/proposal/drain.js +258 -186
- package/dist/commands/proposal/proposal-cli.js +32 -10
- package/dist/commands/proposal/proposal.js +2 -5
- package/dist/commands/proposal/propose.js +192 -172
- package/dist/commands/proposal/repository.js +54 -91
- package/dist/commands/proposal/validators/proposal-validators.js +9 -7
- package/dist/commands/read/curate.js +53 -22
- package/dist/commands/read/registry-search.js +25 -9
- package/dist/commands/read/remember-cli.js +14 -2
- package/dist/commands/read/search.js +10 -4
- package/dist/commands/read/show.js +139 -153
- package/dist/commands/registry-cli.js +16 -7
- package/dist/commands/remember.js +33 -18
- package/dist/commands/sources/add-cli.js +19 -178
- package/dist/commands/sources/bundle-cli.js +15 -3
- package/dist/commands/sources/dangerous-env-audit.js +135 -0
- package/dist/commands/sources/info.js +2 -1
- package/dist/commands/sources/installed-stashes.js +901 -177
- package/dist/commands/sources/schema-repair.js +174 -95
- package/dist/commands/sources/self-update.js +30 -74
- package/dist/commands/sources/source-add.js +3 -5
- package/dist/commands/sources/sources-cli.js +2 -15
- package/dist/commands/sources/update-transaction.js +220 -0
- package/dist/commands/tasks/tasks-cli.js +3 -3
- package/dist/commands/tasks/tasks.js +736 -317
- package/dist/commands/workflow-cli.js +2 -2
- package/dist/core/adapter/adapters/agent-skills-adapter.js +3 -0
- package/dist/core/adapter/adapters/akm-adapter.js +85 -35
- package/dist/core/adapter/adapters/akm-lint.js +54 -39
- package/dist/core/adapter/adapters/akm-metadata.js +45 -45
- package/dist/core/adapter/adapters/akm-task-adapter.js +32 -49
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +38 -23
- package/dist/core/adapter/adapters/dotenv-adapter.js +30 -1
- package/dist/core/adapter/adapters/generic-files-adapter.js +11 -0
- package/dist/core/adapter/adapters/index.js +0 -9
- package/dist/core/adapter/adapters/llm-wiki-adapter.js +4 -0
- package/dist/core/adapter/adapters/okf-adapter.js +4 -0
- package/dist/core/adapter/adapters/opencode-adapter.js +5 -8
- package/dist/core/adapter/adapters/tool-dir-shared.js +63 -6
- package/dist/core/adapter/adapters/website-snapshot-adapter.js +4 -0
- package/dist/core/adapter/execution-source.js +308 -0
- package/dist/core/adapter/recognize-match.js +36 -13
- package/dist/core/adapter/registry.js +0 -9
- package/dist/core/asset/stash-meta.js +94 -4
- package/dist/core/common.js +6 -11
- package/dist/core/config/config-io.js +3 -3
- package/dist/core/config/config-schema.js +18 -40
- package/dist/core/config/config-sources.js +11 -21
- package/dist/core/config/config-walker.js +31 -13
- package/dist/core/config/config.js +23 -26
- package/dist/core/config/schema/engines.js +8 -7
- package/dist/core/config/schema/improve-processes.js +29 -5
- package/dist/core/config/schema/index-config.js +0 -27
- package/dist/core/config/schema/primitives.js +1 -23
- package/dist/core/config/schema/sources-bundles.js +13 -16
- package/dist/core/errors.js +2 -0
- package/dist/core/events.js +68 -32
- package/dist/core/extra-params.js +1 -0
- package/dist/core/improve-result.js +315 -0
- package/dist/core/lesson-lint.js +0 -6
- package/dist/core/maintenance-barrier.js +4 -4
- package/dist/core/network-policy.js +152 -0
- package/dist/core/paths.js +1 -1
- package/dist/core/recognition-util.js +4 -4
- package/dist/core/registry-url.js +456 -0
- package/dist/core/state/migrations.js +161 -47
- package/dist/core/state-db.js +453 -80
- package/dist/core/system-error.js +32 -0
- package/dist/core/time.js +2 -12
- package/dist/core/write-source.js +0 -18
- package/dist/execution/directory-identity.js +52 -0
- package/dist/execution/executable-identity.js +107 -0
- package/dist/execution/guarded-source.js +398 -0
- package/dist/execution/json.js +95 -0
- package/dist/{commands/health/types-session-log.js → execution/limits.js} +2 -1
- package/dist/execution/record.js +55 -0
- package/dist/execution/resolved-request.js +730 -0
- package/dist/execution/source.js +320 -0
- package/dist/indexer/bundle-identity-guard.js +5 -4
- package/dist/indexer/db/graph-db.js +33 -0
- package/dist/indexer/graph/graph-boost.js +3 -4
- package/dist/indexer/graph/graph-extraction.js +562 -373
- package/dist/indexer/index-written-assets.js +78 -39
- package/dist/indexer/indexer.js +471 -432
- package/dist/indexer/installations.js +6 -0
- package/dist/indexer/lookup/adapter-concept-owner.js +283 -0
- package/dist/indexer/materialize-embeddings.js +155 -0
- package/dist/indexer/passes/memory-inference.js +227 -174
- package/dist/indexer/passes/metadata.js +263 -118
- package/dist/indexer/scan/doc-to-entry.js +7 -10
- package/dist/indexer/scan/drain-dir.js +51 -23
- package/dist/indexer/search/db-search.js +156 -50
- package/dist/indexer/search/fts-query.js +40 -40
- package/dist/indexer/search/ranking.js +36 -1
- package/dist/indexer/search/search-attribution.js +3 -1
- package/dist/indexer/search/search-fields.js +23 -14
- package/dist/indexer/search/search-hit-enrichers.js +1 -1
- package/dist/indexer/search/search-source.js +7 -16
- package/dist/indexer/search/semantic-status.js +10 -1
- package/dist/indexer/usage/show-usage.js +105 -0
- package/dist/indexer/usage/usage-events.js +7 -2
- package/dist/indexer/walk/matchers.js +40 -10
- package/dist/indexer/walk/path-resolver.js +5 -2
- package/dist/indexer/walk/walker.js +20 -2
- package/dist/integrations/agent/builder-shared.js +3 -6
- package/dist/integrations/agent/conversation-fallback.js +16 -0
- package/dist/integrations/agent/engine-resolution.js +87 -87
- package/dist/integrations/agent/execution-cascade.js +566 -0
- package/dist/integrations/agent/execution-definitions.js +211 -0
- package/dist/integrations/agent/execution-lowering.js +811 -0
- package/dist/integrations/agent/execution-preparation.js +67 -0
- package/dist/integrations/agent/index.js +0 -2
- package/dist/integrations/agent/inline-execution.js +74 -0
- package/dist/integrations/agent/model-map.js +515 -0
- package/dist/integrations/agent/persona-fallback.js +30 -0
- package/dist/integrations/agent/request-lowering.js +186 -0
- package/dist/integrations/agent/runner-dispatch.js +230 -37
- package/dist/integrations/agent/runner.js +12 -83
- package/dist/integrations/harnesses/aider/agent-builder.js +8 -0
- package/dist/integrations/harnesses/aider/index.js +0 -1
- package/dist/integrations/harnesses/amazonq/agent-builder.js +8 -0
- package/dist/integrations/harnesses/amazonq/index.js +0 -1
- package/dist/integrations/harnesses/claude/agent-builder.js +14 -1
- package/dist/integrations/harnesses/claude/index.js +1 -5
- package/dist/integrations/harnesses/claude/session-log.js +3 -33
- package/dist/integrations/harnesses/codex/agent-builder.js +8 -0
- package/dist/integrations/harnesses/codex/index.js +0 -1
- package/dist/integrations/harnesses/copilot/agent-builder.js +8 -0
- package/dist/integrations/harnesses/copilot/index.js +0 -1
- package/dist/integrations/harnesses/gemini/agent-builder.js +8 -0
- package/dist/integrations/harnesses/gemini/index.js +0 -1
- package/dist/integrations/harnesses/index.js +4 -44
- package/dist/integrations/harnesses/opencode/agent-builder.js +16 -9
- package/dist/integrations/harnesses/opencode/index.js +0 -2
- package/dist/integrations/harnesses/opencode/session-log.js +14 -204
- package/dist/integrations/harnesses/opencode-sdk/harness.js +12 -1
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +40 -42
- package/dist/integrations/harnesses/openhands/agent-builder.js +8 -0
- package/dist/integrations/harnesses/openhands/index.js +0 -1
- package/dist/integrations/harnesses/pi/agent-builder.js +8 -0
- package/dist/integrations/harnesses/pi/index.js +0 -1
- package/dist/integrations/harnesses/shared.js +0 -1
- package/dist/integrations/harnesses/types.js +1 -3
- package/dist/integrations/lockfile.js +82 -79
- package/dist/integrations/session-logs/index.js +6 -17
- package/dist/integrations/session-logs/provider-base.js +1 -29
- package/dist/llm/client.js +10 -5
- package/dist/llm/embedder.js +6 -7
- package/dist/llm/embedders/local.js +37 -88
- package/dist/llm/embedders/types.js +1 -1
- package/dist/llm/graph-extract.js +75 -50
- package/dist/llm/index-passes.js +43 -5
- package/dist/llm/memory-infer.js +8 -6
- package/dist/llm/metadata-enhance.js +5 -3
- package/dist/llm/structured-call.js +122 -25
- package/dist/output/format-exempt.js +1 -1
- package/dist/output/render-registry.js +0 -16
- package/dist/output/renderers.js +12 -7
- package/dist/output/shapes/curate.js +1 -0
- package/dist/output/shapes/helpers.js +10 -2
- package/dist/output/shapes/passthrough.js +2 -0
- package/dist/output/text/command-format.js +31 -33
- package/dist/output/text/health-format.js +1 -29
- package/dist/output/text/migrate.js +6 -56
- package/dist/output/text/proposal-format.js +16 -1
- package/dist/output/text/workflow-format.js +16 -0
- package/dist/registry/network.js +279 -0
- package/dist/registry/pinned-request-helper.js +247 -0
- package/dist/registry/pinned-transport.js +717 -0
- package/dist/registry/providers/skills-sh.js +18 -6
- package/dist/registry/providers/static-index.js +20 -7
- package/dist/registry/resolve.js +53 -28
- package/dist/scripts/akm-migrate-node.js +19334 -52269
- package/dist/scripts/akm-migrate.js +19270 -51612
- package/dist/setup/registry-stash-loader.js +64 -20
- package/dist/setup/semantic-assets.js +9 -34
- package/dist/setup/setup.js +12 -30
- package/dist/setup/source-identity.js +17 -0
- package/dist/setup/steps/sources.js +36 -15
- package/dist/setup/steps/tasks.js +39 -11
- package/dist/sources/providers/git-provider.js +3 -3
- package/dist/sources/providers/npm.js +2 -2
- package/dist/sources/providers/provider-utils.js +4 -3
- package/dist/sources/providers/website.js +11 -7
- package/dist/sources/snapshot-fetchers/host-guard.js +9 -136
- package/dist/sources/snapshot-fetchers/website-ingest.js +25 -109
- package/dist/sources/website-url.js +73 -0
- package/dist/storage/engines/sqlite-migrations.js +81 -26
- package/dist/storage/managed-db.js +27 -24
- package/dist/storage/repositories/events-repository.js +3 -0
- package/dist/storage/repositories/index-connection.js +42 -10
- package/dist/storage/repositories/index-entries-repository.js +203 -229
- package/dist/storage/repositories/index-entry-mapper.js +8 -12
- package/dist/storage/repositories/index-entry-schema.js +255 -0
- package/dist/storage/repositories/index-fts-repository.js +64 -71
- package/dist/storage/repositories/index-llm-cache-repository.js +8 -13
- package/dist/storage/repositories/index-meta-repository.js +0 -11
- package/dist/storage/repositories/index-schema.js +74 -350
- package/dist/storage/repositories/index-utility-repository.js +12 -17
- package/dist/storage/repositories/index-vec-repository.js +56 -7
- package/dist/storage/repositories/proposals-repository.js +4 -127
- package/dist/storage/repositories/registry-cache.js +2 -1
- package/dist/storage/repositories/task-history-repository.js +20 -40
- package/dist/storage/repositories/workflow-runs-repository.js +228 -129
- package/dist/storage/sqlite-read-snapshot.js +148 -0
- package/dist/tasks/backends/cron.js +170 -42
- package/dist/tasks/backends/index.js +1 -1
- package/dist/tasks/backends/launchd.js +787 -202
- package/dist/tasks/backends/schtasks.js +282 -83
- package/dist/tasks/embedded.js +7 -7
- package/dist/tasks/frozen-script.js +50 -0
- package/dist/tasks/resolve-akm-bin.js +5 -1
- package/dist/tasks/runner.js +239 -251
- package/dist/tasks/runtime-v3.js +281 -0
- package/dist/tasks/scheduler-binding.js +272 -0
- package/dist/tasks/scheduler-invocation.js +57 -43
- package/dist/tasks/scheduler-sync.js +654 -0
- package/dist/tasks/source-v3.js +752 -0
- package/dist/tasks/standalone-script-entry.js +5 -0
- package/dist/tasks/task-id.js +29 -0
- package/dist/workflows/authoring/authoring.js +15 -32
- package/dist/workflows/exec/dispatch-redaction.js +14 -8
- package/dist/workflows/exec/exec-unit.js +7 -28
- package/dist/workflows/exec/frozen-judge.js +57 -89
- package/dist/workflows/exec/lowering-notices.js +23 -0
- package/dist/workflows/exec/native-executor.js +301 -458
- package/dist/workflows/exec/param-secrets.js +4 -3
- package/dist/workflows/exec/run-workflow.js +26 -32
- package/dist/workflows/exec/step-work.js +105 -109
- package/dist/workflows/exec/unit-dispatch.js +103 -27
- package/dist/workflows/exec/unit-writer.js +3 -3
- package/dist/workflows/exec/worktree.js +2 -2
- package/dist/workflows/ir/compile.js +86 -72
- package/dist/workflows/ir/environment-v4.js +328 -0
- package/dist/workflows/ir/freeze-v4.js +122 -0
- package/dist/workflows/ir/plan-hash.js +13 -7
- package/dist/workflows/ir/schema-v4.js +525 -0
- package/dist/workflows/ir/schema.js +25 -284
- package/dist/workflows/ir/source-freeze-v4.js +506 -0
- package/dist/workflows/parser.js +27 -24
- package/dist/workflows/program/schema.js +1 -2
- package/dist/workflows/renderer.js +42 -29
- package/dist/workflows/resource-limits.js +4 -5
- package/dist/workflows/runtime/agent-identity.js +11 -13
- package/dist/workflows/runtime/plan-classifier.js +8 -8
- package/dist/workflows/runtime/runs.js +27 -43
- package/dist/workflows/runtime/workflow-asset-loader.js +45 -205
- package/dist/workflows/source-files.js +373 -0
- package/dist/workflows/source-ir/compile.js +196 -0
- package/dist/workflows/source-ir/github-yaml.js +577 -0
- package/dist/workflows/source-ir/ordering.js +38 -0
- package/dist/workflows/source-ir/program.js +50 -0
- package/dist/workflows/source-ir/result.js +26 -0
- package/dist/workflows/source-ir/schema.js +772 -0
- package/dist/workflows/source-ir/semantics.js +242 -0
- package/dist/workflows/source-ir/uses.js +14 -0
- package/docs/README.md +2 -0
- package/docs/migration/README.md +3 -1
- package/docs/migration/release-notes/0.9.2.md +55 -0
- package/docs/migration/release-notes/README.md +5 -0
- package/docs/migration/v0.8-to-v0.9.md +76 -1077
- package/docs/migration/v0.9.0-troubleshooting.md +104 -516
- package/docs/migration/v0.9.1-to-v0.9.2.md +150 -0
- package/docs/reference/README.md +1 -0
- package/docs/reference/cli.md +230 -98
- package/docs/reference/configuration.md +159 -36
- package/docs/reference/data-and-telemetry.md +19 -1
- package/docs/reference/supported-formats.md +23 -3
- package/docs/reference/tasks.md +182 -0
- package/docs/reference/workflow-schema.md +91 -40
- package/docs/reference/workflows.md +33 -6
- package/package.json +10 -6
- package/schemas/akm-config.json +372 -224
- package/schemas/akm-task.json +324 -80
- package/schemas/akm-workflow.json +6 -9
- package/dist/core/migration-operation.js +0 -75
- package/dist/integrations/agent/model-aliases.js +0 -74
- package/dist/tasks/parser.js +0 -380
- package/dist/tasks/schema.js +0 -123
- package/dist/tasks/validator.js +0 -80
- package/dist/workflows/ir/freeze.js +0 -320
- package/dist/workflows/runtime/document-cache.js +0 -13
|
@@ -183,10 +183,8 @@ export function proposalRowToProposal(row) {
|
|
|
183
183
|
throw new Error("Proposal row has invalid metadata_json.", { cause: error });
|
|
184
184
|
}
|
|
185
185
|
validatePresentMetadata(meta);
|
|
186
|
-
const changes =
|
|
187
|
-
|
|
188
|
-
: [{ path: "", op: "update", after: row.content }];
|
|
189
|
-
const proposedTarget = Object.hasOwn(meta, "proposedTarget") ? currentProposalTarget(meta.proposedTarget) : undefined;
|
|
186
|
+
const changes = storedToChanges(meta.changes, row.content);
|
|
187
|
+
const proposedTarget = currentProposalTarget(meta.proposedTarget);
|
|
190
188
|
return {
|
|
191
189
|
id: row.id,
|
|
192
190
|
ref: currentProposalRef(row.ref),
|
|
@@ -200,7 +198,7 @@ export function proposalRowToProposal(row) {
|
|
|
200
198
|
...(frontmatter !== undefined ? { frontmatter } : {}),
|
|
201
199
|
},
|
|
202
200
|
changes,
|
|
203
|
-
|
|
201
|
+
proposedTarget,
|
|
204
202
|
...(typeof meta.beforeHash === "string" ? { beforeHash: meta.beforeHash } : {}),
|
|
205
203
|
...(meta.review !== undefined ? { review: meta.review } : {}),
|
|
206
204
|
...(typeof meta.confidence === "number" ? { confidence: meta.confidence } : {}),
|
|
@@ -212,110 +210,6 @@ export function proposalRowToProposal(row) {
|
|
|
212
210
|
: {}),
|
|
213
211
|
};
|
|
214
212
|
}
|
|
215
|
-
/**
|
|
216
|
-
* Lenient counterpart to {@link proposalRowToProposal} for the terminal-status
|
|
217
|
-
* (reject/archive) read paths (repository.ts `archiveProposal` /
|
|
218
|
-
* `rejectProposalDurably`). Those paths must succeed even on legacy rows
|
|
219
|
-
* minted before a field was required — e.g. 0.8-era rows missing
|
|
220
|
-
* `proposedTarget` and/or carrying empty/missing `changes[].path` metadata —
|
|
221
|
-
* so an operator can reject them instead of resorting to manual SQL.
|
|
222
|
-
*
|
|
223
|
-
* Per-field decode failures are swallowed: the offending field degrades to a
|
|
224
|
-
* safe placeholder (matching the strict decoder's own "no changes recorded"
|
|
225
|
-
* fallback) and the failure is appended to `decodeWarnings` instead of
|
|
226
|
-
* throwing. `id` / `status` / timestamps / `source` are trusted verbatim (the
|
|
227
|
-
* schema guarantees they're non-null strings). The strict decoder remains the
|
|
228
|
-
* default everywhere else, including every other read of a `pending` or
|
|
229
|
-
* `accepted` proposal (accept/revert must keep failing exactly as today on a
|
|
230
|
-
* malformed row).
|
|
231
|
-
*/
|
|
232
|
-
export function proposalRowToProposalLenient(row) {
|
|
233
|
-
const warnings = [];
|
|
234
|
-
let meta = {};
|
|
235
|
-
try {
|
|
236
|
-
const parsed = JSON.parse(row.metadata_json);
|
|
237
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
238
|
-
throw new Error("metadata_json must contain an object.");
|
|
239
|
-
}
|
|
240
|
-
meta = parsed;
|
|
241
|
-
validatePresentMetadata(meta);
|
|
242
|
-
}
|
|
243
|
-
catch (error) {
|
|
244
|
-
warnings.push(`metadata_json is invalid (${error instanceof Error ? error.message : String(error)}); optional fields dropped.`);
|
|
245
|
-
meta = {};
|
|
246
|
-
}
|
|
247
|
-
let frontmatter;
|
|
248
|
-
if (row.frontmatter_json !== null) {
|
|
249
|
-
try {
|
|
250
|
-
const parsed = JSON.parse(row.frontmatter_json);
|
|
251
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
252
|
-
throw new Error("frontmatter_json must contain an object.");
|
|
253
|
-
}
|
|
254
|
-
frontmatter = parsed;
|
|
255
|
-
}
|
|
256
|
-
catch (error) {
|
|
257
|
-
warnings.push(`frontmatter_json is invalid (${error instanceof Error ? error.message : String(error)}); frontmatter dropped.`);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
let ref;
|
|
261
|
-
try {
|
|
262
|
-
ref = currentProposalRef(row.ref);
|
|
263
|
-
}
|
|
264
|
-
catch (error) {
|
|
265
|
-
warnings.push(`ref is invalid (${error instanceof Error ? error.message : String(error)}); keeping the raw stored value.`);
|
|
266
|
-
ref = row.ref;
|
|
267
|
-
}
|
|
268
|
-
let changes;
|
|
269
|
-
try {
|
|
270
|
-
changes = Object.hasOwn(meta, "changes")
|
|
271
|
-
? storedToChanges(meta.changes, row.content)
|
|
272
|
-
: [{ path: "", op: "update", after: row.content }];
|
|
273
|
-
}
|
|
274
|
-
catch (error) {
|
|
275
|
-
warnings.push(`changes metadata is invalid (${error instanceof Error ? error.message : String(error)}); degraded to a single placeholder change.`);
|
|
276
|
-
changes = [{ path: "", op: "update", after: row.content }];
|
|
277
|
-
}
|
|
278
|
-
let proposedTarget;
|
|
279
|
-
if (Object.hasOwn(meta, "proposedTarget")) {
|
|
280
|
-
try {
|
|
281
|
-
proposedTarget = currentProposalTarget(meta.proposedTarget);
|
|
282
|
-
}
|
|
283
|
-
catch (error) {
|
|
284
|
-
warnings.push(`proposedTarget is invalid (${error instanceof Error ? error.message : String(error)}); omitted.`);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
else {
|
|
288
|
-
warnings.push("proposedTarget is missing (pre-WI-6.2 legacy row).");
|
|
289
|
-
}
|
|
290
|
-
if (!["pending", "accepted", "rejected", "reverted"].includes(row.status)) {
|
|
291
|
-
warnings.push(`status "${row.status}" is not a recognized value.`);
|
|
292
|
-
}
|
|
293
|
-
return {
|
|
294
|
-
id: row.id,
|
|
295
|
-
ref,
|
|
296
|
-
status: row.status,
|
|
297
|
-
source: row.source,
|
|
298
|
-
...(typeof meta.sourceRun === "string" ? { sourceRun: meta.sourceRun } : {}),
|
|
299
|
-
createdAt: row.created_at,
|
|
300
|
-
updatedAt: row.updated_at,
|
|
301
|
-
payload: {
|
|
302
|
-
content: row.content,
|
|
303
|
-
...(frontmatter !== undefined ? { frontmatter } : {}),
|
|
304
|
-
},
|
|
305
|
-
changes,
|
|
306
|
-
...(proposedTarget !== undefined ? { proposedTarget } : {}),
|
|
307
|
-
...(typeof meta.beforeHash === "string" ? { beforeHash: meta.beforeHash } : {}),
|
|
308
|
-
...(meta.review !== undefined ? { review: meta.review } : {}),
|
|
309
|
-
...(typeof meta.confidence === "number" ? { confidence: meta.confidence } : {}),
|
|
310
|
-
...(meta.gateDecision !== undefined ? { gateDecision: meta.gateDecision } : {}),
|
|
311
|
-
...(typeof meta.backupContent === "string" ? { backupContent: meta.backupContent } : {}),
|
|
312
|
-
...(meta.acceptedTarget !== undefined ? { acceptedTarget: meta.acceptedTarget } : {}),
|
|
313
|
-
...(typeof meta.eligibilitySource === "string"
|
|
314
|
-
? { eligibilitySource: meta.eligibilitySource }
|
|
315
|
-
: {}),
|
|
316
|
-
...(warnings.length > 0 ? { decodeWarnings: warnings } : {}),
|
|
317
|
-
};
|
|
318
|
-
}
|
|
319
213
|
/**
|
|
320
214
|
* Convert a public `Proposal` to column values ready for an INSERT/UPDATE.
|
|
321
215
|
* The `stash_dir` comes from the call site (proposals.ts has it in scope).
|
|
@@ -413,12 +307,7 @@ export function listStateProposals(db, options = {}) {
|
|
|
413
307
|
content, frontmatter_json, metadata_json
|
|
414
308
|
FROM proposals ${where} ORDER BY created_at ASC, rowid ASC`)
|
|
415
309
|
.all(...params);
|
|
416
|
-
|
|
417
|
-
// terminal-status row that was rejected via the lenient path (or predates
|
|
418
|
-
// a required field) remains listable — a strict throw here would defeat
|
|
419
|
-
// the point of tolerating it at reject/archive time. Pending rows keep the
|
|
420
|
-
// strict decoder; nothing about the live queue changes here.
|
|
421
|
-
return rows.map((row) => (row.status === "pending" ? proposalRowToProposal(row) : proposalRowToProposalLenient(row)));
|
|
310
|
+
return rows.map(proposalRowToProposal);
|
|
422
311
|
}
|
|
423
312
|
/**
|
|
424
313
|
* Look up a single proposal by id, optionally scoped to one stash root.
|
|
@@ -431,18 +320,6 @@ export function getStateProposal(db, id, stashDir) {
|
|
|
431
320
|
const row = (stashDir ? db.prepare(sql).get(id, stashDir) : db.prepare(sql).get(id));
|
|
432
321
|
return row ? proposalRowToProposal(row) : undefined;
|
|
433
322
|
}
|
|
434
|
-
/**
|
|
435
|
-
* Lenient counterpart to {@link getStateProposal} — used ONLY by the
|
|
436
|
-
* terminal-status (reject/archive) read paths in repository.ts that must
|
|
437
|
-
* tolerate a malformed row (see {@link proposalRowToProposalLenient}).
|
|
438
|
-
*/
|
|
439
|
-
export function getStateProposalLenient(db, id, stashDir) {
|
|
440
|
-
const sql = `SELECT id, stash_dir, ref, status, source, created_at, updated_at,
|
|
441
|
-
content, frontmatter_json, metadata_json
|
|
442
|
-
FROM proposals WHERE id = ?${stashDir ? " AND stash_dir = ?" : ""}`;
|
|
443
|
-
const row = (stashDir ? db.prepare(sql).get(id, stashDir) : db.prepare(sql).get(id));
|
|
444
|
-
return row ? proposalRowToProposalLenient(row) : undefined;
|
|
445
|
-
}
|
|
446
323
|
/**
|
|
447
324
|
* Find PENDING proposal ids in one stash whose id starts with `idPrefix`.
|
|
448
325
|
* Backs the UUID-prefix form of `akm proposal show/accept/... <prefix>` —
|
|
@@ -2,6 +2,7 @@
|
|
|
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 { rethrowIfTestIsolationError } from "../../core/errors.js";
|
|
5
|
+
import { formatRegistryError } from "../../core/registry-url.js";
|
|
5
6
|
import { warn } from "../../core/warn.js";
|
|
6
7
|
import { closeDatabase, openIndexDatabase } from "./index-connection.js";
|
|
7
8
|
import { getRegistryIndexCache, upsertRegistryIndexCache } from "./registry-index-cache-repository.js";
|
|
@@ -96,7 +97,7 @@ export async function fetchCachedJson(opts) {
|
|
|
96
97
|
if (expiredRow) {
|
|
97
98
|
const stale = parseCache(expiredRow.indexJson, { stale: true });
|
|
98
99
|
if (stale !== undefined) {
|
|
99
|
-
warn(`Registry fetch failed (${
|
|
100
|
+
warn(`Registry fetch failed (${formatRegistryError(err)}); ` +
|
|
100
101
|
"serving the last cached index, which is past its refresh interval.");
|
|
101
102
|
return stale;
|
|
102
103
|
}
|
|
@@ -24,7 +24,7 @@ function validateDetail(value) {
|
|
|
24
24
|
metadataError("detail.exitCode must be a number or null");
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
|
-
/** Decode
|
|
27
|
+
/** Decode the current task-history metadata shape. */
|
|
28
28
|
export function decodeTaskHistoryMetadata(input) {
|
|
29
29
|
let parsed = input;
|
|
30
30
|
if (typeof input === "string") {
|
|
@@ -37,46 +37,26 @@ export function decodeTaskHistoryMetadata(input) {
|
|
|
37
37
|
}
|
|
38
38
|
if (!isRecord(parsed))
|
|
39
39
|
metadataError("root must be an object");
|
|
40
|
-
if (parsed.metadataVersion
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return {
|
|
53
|
-
metadataVersion: 1,
|
|
54
|
-
durationMs: parsed.durationMs ?? 0,
|
|
55
|
-
detail: parsed.detail ?? null,
|
|
56
|
-
...(parsed.profile !== undefined ? { legacyProfile: parsed.profile } : {}),
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
if (parsed.metadataVersion === 2) {
|
|
60
|
-
const allowed = new Set(["metadataVersion", "durationMs", "detail", "engine"]);
|
|
61
|
-
const unknown = Object.keys(parsed).filter((key) => !allowed.has(key));
|
|
62
|
-
if (unknown.length > 0)
|
|
63
|
-
metadataError(`unknown v2 fields: ${unknown.sort().join(", ")}`);
|
|
64
|
-
if (typeof parsed.durationMs !== "number")
|
|
65
|
-
metadataError("durationMs must be a number");
|
|
66
|
-
if (!("detail" in parsed))
|
|
67
|
-
metadataError("detail is required in v2");
|
|
68
|
-
if (parsed.engine !== undefined && parsed.engine !== null && typeof parsed.engine !== "string") {
|
|
69
|
-
metadataError("engine must be a string or null");
|
|
70
|
-
}
|
|
71
|
-
validateDetail(parsed.detail);
|
|
72
|
-
return {
|
|
73
|
-
metadataVersion: 2,
|
|
74
|
-
durationMs: parsed.durationMs,
|
|
75
|
-
detail: parsed.detail ?? null,
|
|
76
|
-
...(parsed.engine !== undefined ? { engine: parsed.engine } : {}),
|
|
77
|
-
};
|
|
40
|
+
if (parsed.metadataVersion !== 2)
|
|
41
|
+
metadataError(`unsupported metadataVersion: ${String(parsed.metadataVersion)}`);
|
|
42
|
+
const allowed = new Set(["metadataVersion", "durationMs", "detail", "engine"]);
|
|
43
|
+
const unknown = Object.keys(parsed).filter((key) => !allowed.has(key));
|
|
44
|
+
if (unknown.length > 0)
|
|
45
|
+
metadataError(`unknown fields: ${unknown.sort().join(", ")}`);
|
|
46
|
+
if (typeof parsed.durationMs !== "number")
|
|
47
|
+
metadataError("durationMs must be a number");
|
|
48
|
+
if (!("detail" in parsed))
|
|
49
|
+
metadataError("detail is required");
|
|
50
|
+
if (parsed.engine !== undefined && parsed.engine !== null && typeof parsed.engine !== "string") {
|
|
51
|
+
metadataError("engine must be a string or null");
|
|
78
52
|
}
|
|
79
|
-
|
|
53
|
+
validateDetail(parsed.detail);
|
|
54
|
+
return {
|
|
55
|
+
metadataVersion: 2,
|
|
56
|
+
durationMs: parsed.durationMs,
|
|
57
|
+
detail: parsed.detail ?? null,
|
|
58
|
+
...(parsed.engine !== undefined ? { engine: parsed.engine } : {}),
|
|
59
|
+
};
|
|
80
60
|
}
|
|
81
61
|
/**
|
|
82
62
|
* Atomically reserve one task-attempt identity.
|
|
@@ -1,16 +1,30 @@
|
|
|
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 { randomUUID } from "node:crypto";
|
|
5
|
+
import { UsageError } from "../../core/errors.js";
|
|
4
6
|
import { openStateDatabase, withImmediateTransaction } from "../../core/state-db.js";
|
|
5
7
|
import { borrowScopedStateDb, withStateDbScope } from "../../core/state-db-scope.js";
|
|
6
8
|
import { resolveStorageLocations } from "../locations.js";
|
|
9
|
+
import { insertEventOnce, insertEventStrict } from "./events-repository.js";
|
|
10
|
+
function assertAttemptReservationLease(input, run) {
|
|
11
|
+
if (input.leaseMode === "direct") {
|
|
12
|
+
if (run.engine_lease_holder === null)
|
|
13
|
+
return;
|
|
14
|
+
throw new UsageError(`Workflow run ${input.runId} is leased by another engine; direct durable dispatch reservation is forbidden.`, "RESOURCE_ALREADY_EXISTS");
|
|
15
|
+
}
|
|
16
|
+
if (run.engine_lease_holder !== input.claimHolder) {
|
|
17
|
+
throw new UsageError(`Workflow run ${input.runId} lease holder changed; refusing a stale durable dispatch reservation.`, "RESOURCE_ALREADY_EXISTS");
|
|
18
|
+
}
|
|
19
|
+
if (run.engine_lease_until === null || run.engine_lease_until < input.now) {
|
|
20
|
+
throw new UsageError(`Workflow run ${input.runId} engine lease expired before durable dispatch reservation.`, "RESOURCE_ALREADY_EXISTS");
|
|
21
|
+
}
|
|
22
|
+
}
|
|
7
23
|
/**
|
|
8
24
|
* Repository owning every raw SQL statement against `workflow_runs` and
|
|
9
25
|
* `workflow_run_steps`. It is DB-location-agnostic: the lifecycle helper
|
|
10
|
-
* {@link withWorkflowRunsRepo} binds it to {@link StorageLocations.stateDb}
|
|
11
|
-
*
|
|
12
|
-
* state migration `020-three-db-cutover`) so a future storage move (#489)
|
|
13
|
-
* changes only `locations.ts`.
|
|
26
|
+
* {@link withWorkflowRunsRepo} binds it to {@link StorageLocations.stateDb}, so
|
|
27
|
+
* a future storage move changes only `locations.ts`.
|
|
14
28
|
*
|
|
15
29
|
* ## Connection-lifetime contract (WS5)
|
|
16
30
|
*
|
|
@@ -44,7 +58,8 @@ export class WorkflowRunsRepository {
|
|
|
44
58
|
.get(...refs, scopeKey);
|
|
45
59
|
}
|
|
46
60
|
getRunById(runId) {
|
|
47
|
-
return this.db.prepare("SELECT * FROM workflow_runs WHERE id = ?").get(runId)
|
|
61
|
+
return (this.db.prepare("SELECT * FROM workflow_runs WHERE id = ?").get(runId) ??
|
|
62
|
+
undefined);
|
|
48
63
|
}
|
|
49
64
|
getActiveRunRowForScope(workflowRefs, scopeKey) {
|
|
50
65
|
const refs = typeof workflowRefs === "string" ? [workflowRefs] : [...workflowRefs];
|
|
@@ -157,16 +172,34 @@ export class WorkflowRunsRepository {
|
|
|
157
172
|
this.db.prepare("UPDATE workflow_runs SET checkin_armed_at = ? WHERE id = ?").run(checkinArmedAt, runId);
|
|
158
173
|
}
|
|
159
174
|
/**
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
* `insertRun`, so a run row never exists without its frozen plan. Read back
|
|
164
|
-
* via {@link getRunById} (`plan_json` / `plan_hash` on the row).
|
|
175
|
+
* Atomically publish the entire durable-v4 run spine after the final source
|
|
176
|
+
* CAS. No run row, partial spine, plan attachment, or started event can
|
|
177
|
+
* escape independently across a crash or statement failure.
|
|
165
178
|
*/
|
|
166
|
-
|
|
167
|
-
this.db
|
|
168
|
-
.
|
|
169
|
-
.
|
|
179
|
+
publishWorkflowRunV4(input) {
|
|
180
|
+
this.immediateTransaction((db) => {
|
|
181
|
+
input.revalidateSources();
|
|
182
|
+
if (!input.force) {
|
|
183
|
+
const existing = this.findActiveRunForScope(input.workflowRefs, input.run.scopeKey);
|
|
184
|
+
if (existing) {
|
|
185
|
+
throw new UsageError(`Workflow ${input.run.workflowRef} already has an active run in this scope ` +
|
|
186
|
+
`(id=${existing.id}, step=${existing.current_step_id ?? "—"}). ` +
|
|
187
|
+
`Use 'akm workflow run ${input.run.workflowRef}' to resume it or ` +
|
|
188
|
+
`'akm workflow abandon ${existing.id}' to give up on it.`, "RESOURCE_ALREADY_EXISTS");
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
this.insertRun(input.run);
|
|
192
|
+
this.insertSteps(input.steps);
|
|
193
|
+
db.prepare("UPDATE workflow_runs SET plan_json = ?, plan_hash = ?, plan_ir_version = 4 WHERE id = ?").run(input.planJson, input.planHash, input.run.id);
|
|
194
|
+
insertEventOnce(db, {
|
|
195
|
+
eventType: "workflow_started",
|
|
196
|
+
ts: input.run.createdAt,
|
|
197
|
+
ref: input.run.workflowRef,
|
|
198
|
+
metadata: { runId: input.run.id, status: "active" },
|
|
199
|
+
idempotencyKey: input.run.id,
|
|
200
|
+
idempotencyMetadataKey: "runId",
|
|
201
|
+
});
|
|
202
|
+
});
|
|
170
203
|
}
|
|
171
204
|
// ── engine run lease (migration 006 columns, R2 enforcement) ──────────────
|
|
172
205
|
//
|
|
@@ -211,7 +244,186 @@ export class WorkflowRunsRepository {
|
|
|
211
244
|
.prepare("UPDATE workflow_runs SET engine_lease_holder = NULL, engine_lease_until = NULL WHERE id = ? AND engine_lease_holder = ? AND status <> 'failed'")
|
|
212
245
|
.run(runId, holder);
|
|
213
246
|
}
|
|
214
|
-
// ──
|
|
247
|
+
// ── durable v4 append-only dispatch attempts (migration 022) ─────────────
|
|
248
|
+
getUnitAttempts(runId, unitId) {
|
|
249
|
+
return this.db
|
|
250
|
+
.prepare(`SELECT * FROM workflow_run_unit_attempts
|
|
251
|
+
WHERE run_id = ? AND unit_id = ?
|
|
252
|
+
ORDER BY attempt`)
|
|
253
|
+
.all(runId, unitId);
|
|
254
|
+
}
|
|
255
|
+
getAttemptAccounting(runId) {
|
|
256
|
+
const row = this.db
|
|
257
|
+
.prepare(`SELECT
|
|
258
|
+
COUNT(*) AS total_attempts,
|
|
259
|
+
COALESCE(SUM(tokens), 0) AS total_tokens,
|
|
260
|
+
COALESCE(SUM(CASE WHEN phase = 'unit' THEN 1 ELSE 0 END), 0) AS dispatch_attempts,
|
|
261
|
+
COALESCE(SUM(CASE WHEN phase = 'unit' THEN COALESCE(tokens, 0) ELSE 0 END), 0) AS dispatch_tokens,
|
|
262
|
+
COALESCE(SUM(CASE WHEN phase = 'gate' THEN 1 ELSE 0 END), 0) AS gate_attempts,
|
|
263
|
+
COALESCE(SUM(CASE WHEN phase = 'gate' THEN COALESCE(tokens, 0) ELSE 0 END), 0) AS gate_tokens
|
|
264
|
+
FROM workflow_run_unit_attempts
|
|
265
|
+
WHERE run_id = ?`)
|
|
266
|
+
.get(runId);
|
|
267
|
+
return {
|
|
268
|
+
totalAttempts: Number(row.total_attempts),
|
|
269
|
+
totalTokens: Number(row.total_tokens),
|
|
270
|
+
dispatchAttempts: Number(row.dispatch_attempts),
|
|
271
|
+
dispatchTokens: Number(row.dispatch_tokens),
|
|
272
|
+
gateAttempts: Number(row.gate_attempts),
|
|
273
|
+
gateTokens: Number(row.gate_tokens),
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Reserve or reclaim one v4 external dispatch. The attempt row, legacy
|
|
278
|
+
* projection, and directly-paired started event share one IMMEDIATE
|
|
279
|
+
* transaction. Reclaim keeps the stable dispatch id and emits no duplicate
|
|
280
|
+
* start event: the external effect remains explicitly at-least-once.
|
|
281
|
+
*/
|
|
282
|
+
reserveUnitAttempt(input) {
|
|
283
|
+
return this.immediateTransaction((db) => {
|
|
284
|
+
const run = db
|
|
285
|
+
.prepare("SELECT workflow_ref, status, engine_lease_holder, engine_lease_until FROM workflow_runs WHERE id = ?")
|
|
286
|
+
.get(input.runId);
|
|
287
|
+
if (!run || run.status !== "active") {
|
|
288
|
+
throw new UsageError(`Workflow run ${input.runId} is not active; refusing to reserve a durable dispatch attempt.`, "RESOURCE_ALREADY_EXISTS");
|
|
289
|
+
}
|
|
290
|
+
assertAttemptReservationLease(input, run);
|
|
291
|
+
const latest = db
|
|
292
|
+
.prepare(`SELECT * FROM workflow_run_unit_attempts
|
|
293
|
+
WHERE run_id = ? AND unit_id = ?
|
|
294
|
+
ORDER BY attempt DESC
|
|
295
|
+
LIMIT 1`)
|
|
296
|
+
.get(input.runId, input.unitId);
|
|
297
|
+
if (latest?.status === "running") {
|
|
298
|
+
if (latest.claim_holder === input.claimHolder) {
|
|
299
|
+
return { kind: "existing", attempt: latest };
|
|
300
|
+
}
|
|
301
|
+
const currentRunLeaseDisplacedClaim = run.engine_lease_holder === input.claimHolder;
|
|
302
|
+
const expired = latest.claim_expires_at < input.now;
|
|
303
|
+
if (!expired && !currentRunLeaseDisplacedClaim) {
|
|
304
|
+
return { kind: "busy", attempt: latest };
|
|
305
|
+
}
|
|
306
|
+
const reclaimed = db
|
|
307
|
+
.prepare(`UPDATE workflow_run_unit_attempts
|
|
308
|
+
SET claim_holder = ?, claim_expires_at = ?
|
|
309
|
+
WHERE run_id = ? AND unit_id = ? AND attempt = ?
|
|
310
|
+
AND status = 'running' AND dispatch_id = ? AND claim_holder = ?
|
|
311
|
+
AND claim_expires_at = ?`)
|
|
312
|
+
.run(input.claimHolder, input.claimExpiresAt, input.runId, input.unitId, latest.attempt, latest.dispatch_id, latest.claim_holder, latest.claim_expires_at);
|
|
313
|
+
if (Number(reclaimed.changes) !== 1) {
|
|
314
|
+
throw new Error(`Durable attempt ${input.unitId} changed while its displaced claim was reclaimed.`);
|
|
315
|
+
}
|
|
316
|
+
db.prepare(`UPDATE workflow_run_units
|
|
317
|
+
SET claim_holder = ?, claim_expires_at = ?, last_checkin_at = ?
|
|
318
|
+
WHERE run_id = ? AND unit_id = ? AND status = 'running'`).run(input.claimHolder, input.claimExpiresAt, input.now, input.runId, input.unitId);
|
|
319
|
+
const attempt = db
|
|
320
|
+
.prepare(`SELECT * FROM workflow_run_unit_attempts
|
|
321
|
+
WHERE run_id = ? AND unit_id = ? AND attempt = ?`)
|
|
322
|
+
.get(input.runId, input.unitId, latest.attempt);
|
|
323
|
+
return { kind: "reclaimed", attempt };
|
|
324
|
+
}
|
|
325
|
+
const attemptNumber = (latest?.attempt ?? 0) + 1;
|
|
326
|
+
const dispatchId = randomUUID();
|
|
327
|
+
db.prepare(`INSERT INTO workflow_run_unit_attempts (
|
|
328
|
+
run_id, unit_id, attempt, dispatch_id, step_id, node_id, phase,
|
|
329
|
+
runner, engine, model, input_hash, status, worktree_path, started_at,
|
|
330
|
+
claim_holder, claim_expires_at
|
|
331
|
+
) 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, input.claimHolder, input.claimExpiresAt);
|
|
332
|
+
db.prepare(`INSERT INTO workflow_run_units (
|
|
333
|
+
run_id, unit_id, step_id, node_id, parent_unit_id, phase, runner, engine, model,
|
|
334
|
+
status, input_hash, worktree_path, started_at, claim_holder, claim_expires_at, attempts
|
|
335
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'running', ?, ?, ?, ?, ?, ?)
|
|
336
|
+
ON CONFLICT(run_id, unit_id) DO UPDATE SET
|
|
337
|
+
step_id = excluded.step_id,
|
|
338
|
+
node_id = excluded.node_id,
|
|
339
|
+
parent_unit_id = excluded.parent_unit_id,
|
|
340
|
+
phase = excluded.phase,
|
|
341
|
+
runner = excluded.runner,
|
|
342
|
+
engine = excluded.engine,
|
|
343
|
+
model = excluded.model,
|
|
344
|
+
status = 'running',
|
|
345
|
+
input_hash = excluded.input_hash,
|
|
346
|
+
worktree_path = excluded.worktree_path,
|
|
347
|
+
started_at = excluded.started_at,
|
|
348
|
+
claim_holder = excluded.claim_holder,
|
|
349
|
+
claim_expires_at = excluded.claim_expires_at,
|
|
350
|
+
result_json = NULL,
|
|
351
|
+
tokens = NULL,
|
|
352
|
+
failure_reason = NULL,
|
|
353
|
+
session_id = NULL,
|
|
354
|
+
finished_at = NULL,
|
|
355
|
+
last_checkin_at = NULL,
|
|
356
|
+
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);
|
|
357
|
+
insertEventStrict(db, {
|
|
358
|
+
eventType: "workflow_unit_started",
|
|
359
|
+
ts: input.now,
|
|
360
|
+
ref: run.workflow_ref,
|
|
361
|
+
metadata: {
|
|
362
|
+
runId: input.runId,
|
|
363
|
+
stepId: input.stepId,
|
|
364
|
+
unitId: input.unitId,
|
|
365
|
+
attempt: attemptNumber,
|
|
366
|
+
dispatchId,
|
|
367
|
+
phase: input.phase,
|
|
368
|
+
status: "running",
|
|
369
|
+
},
|
|
370
|
+
});
|
|
371
|
+
const attempt = db
|
|
372
|
+
.prepare(`SELECT * FROM workflow_run_unit_attempts
|
|
373
|
+
WHERE run_id = ? AND unit_id = ? AND attempt = ?`)
|
|
374
|
+
.get(input.runId, input.unitId, attemptNumber);
|
|
375
|
+
return { kind: "reserved", attempt };
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
/** Commit one CAS-valid v4 terminal result, known usage, and finish event. */
|
|
379
|
+
finishUnitAttempt(input) {
|
|
380
|
+
return this.immediateTransaction((db) => {
|
|
381
|
+
const changed = db
|
|
382
|
+
.prepare(`UPDATE workflow_run_unit_attempts
|
|
383
|
+
SET status = ?, result_json = ?, tokens = ?, failure_reason = ?,
|
|
384
|
+
session_id = ?, finished_at = ?
|
|
385
|
+
WHERE run_id = ? AND unit_id = ? AND attempt = ? AND dispatch_id = ?
|
|
386
|
+
AND claim_holder = ? AND status = 'running'`)
|
|
387
|
+
.run(input.status, input.resultJson, input.tokens, input.failureReason, input.sessionId ?? null, input.finishedAt, input.runId, input.unitId, input.attempt, input.dispatchId, input.claimHolder);
|
|
388
|
+
if (Number(changed.changes) !== 1)
|
|
389
|
+
return false;
|
|
390
|
+
const attempt = db
|
|
391
|
+
.prepare(`SELECT * FROM workflow_run_unit_attempts
|
|
392
|
+
WHERE run_id = ? AND unit_id = ? AND attempt = ?`)
|
|
393
|
+
.get(input.runId, input.unitId, input.attempt);
|
|
394
|
+
const projection = db
|
|
395
|
+
.prepare(`UPDATE workflow_run_units
|
|
396
|
+
SET status = ?, result_json = ?, tokens = ?, failure_reason = ?,
|
|
397
|
+
session_id = ?, finished_at = ?
|
|
398
|
+
WHERE run_id = ? AND unit_id = ? AND status = 'running'
|
|
399
|
+
AND attempts = ? AND claim_holder = ?`)
|
|
400
|
+
.run(input.status, input.resultJson, input.tokens, input.failureReason, input.sessionId ?? null, input.finishedAt, input.runId, input.unitId, input.attempt, input.claimHolder);
|
|
401
|
+
if (Number(projection.changes) !== 1) {
|
|
402
|
+
throw new Error(`Durable attempt ${input.unitId} has no matching live workflow_run_units projection.`);
|
|
403
|
+
}
|
|
404
|
+
const run = db.prepare("SELECT workflow_ref FROM workflow_runs WHERE id = ?").get(input.runId);
|
|
405
|
+
if (!run)
|
|
406
|
+
throw new Error(`Durable attempt ${input.unitId} has no owning workflow run.`);
|
|
407
|
+
insertEventStrict(db, {
|
|
408
|
+
eventType: "workflow_unit_finished",
|
|
409
|
+
ts: input.finishedAt,
|
|
410
|
+
ref: run.workflow_ref,
|
|
411
|
+
metadata: {
|
|
412
|
+
runId: input.runId,
|
|
413
|
+
stepId: attempt.step_id,
|
|
414
|
+
unitId: input.unitId,
|
|
415
|
+
attempt: input.attempt,
|
|
416
|
+
dispatchId: input.dispatchId,
|
|
417
|
+
phase: attempt.phase,
|
|
418
|
+
status: input.status,
|
|
419
|
+
...(input.failureReason ? { failureReason: input.failureReason } : {}),
|
|
420
|
+
...(input.tokens !== null ? { tokens: input.tokens } : {}),
|
|
421
|
+
},
|
|
422
|
+
});
|
|
423
|
+
return true;
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
// ── current unit status projection ─────────────────────────────────────────
|
|
215
427
|
//
|
|
216
428
|
// Writes to `workflow_run_units` should go through the serialized writer
|
|
217
429
|
// queue (`src/workflows/exec/unit-writer.ts`) when N units may complete
|
|
@@ -229,123 +441,10 @@ export class WorkflowRunsRepository {
|
|
|
229
441
|
.prepare("SELECT * FROM workflow_run_units WHERE run_id = ? AND step_id = ? ORDER BY started_at ASC, unit_id ASC")
|
|
230
442
|
.all(runId, stepId);
|
|
231
443
|
}
|
|
232
|
-
/** One unit row by primary key, or undefined.
|
|
233
|
-
* (idempotency / replay-divergence / claim checks). */
|
|
444
|
+
/** One unit row by primary key, or undefined. */
|
|
234
445
|
getUnit(runId, unitId) {
|
|
235
446
|
return this.db.prepare("SELECT * FROM workflow_run_units WHERE run_id = ? AND unit_id = ?").get(runId, unitId);
|
|
236
447
|
}
|
|
237
|
-
/**
|
|
238
|
-
* Insert a unit row in `running` state (a dispatch is starting now).
|
|
239
|
-
*
|
|
240
|
-
* Upsert on the (run_id, unit_id) primary key: durable-row resume
|
|
241
|
-
* re-dispatches units whose previous attempt never reached a terminal status
|
|
242
|
-
* (a crash mid-step leaves `running` rows). The fresh dispatch REPLACES the
|
|
243
|
-
* stale row — value columns (`result_json`/`tokens`/`failure_reason`/
|
|
244
|
-
* `session_id`/`finished_at`/`last_checkin_at`) are reset exactly as the old
|
|
245
|
-
* `INSERT OR REPLACE` reset them, and the dispatch metadata is overwritten —
|
|
246
|
-
* but `attempts` is INCREMENTED rather than reset (migration 008). A first
|
|
247
|
-
* insert lands `attempts = 1` (column default); each re-dispatch bumps it, so
|
|
248
|
-
* budget/lifetime seeds that sum `attempts` charge every crash-retried
|
|
249
|
-
* dispatch instead of collapsing them into one row. Using `ON CONFLICT DO
|
|
250
|
-
* UPDATE` (not `INSERT OR REPLACE`, which deletes+reinserts) is what lets the
|
|
251
|
-
* increment read the prior row's counter.
|
|
252
|
-
*/
|
|
253
|
-
insertUnit(input) {
|
|
254
|
-
this.db
|
|
255
|
-
.prepare(`INSERT INTO workflow_run_units (
|
|
256
|
-
run_id, unit_id, step_id, node_id, parent_unit_id, phase, runner, engine, model,
|
|
257
|
-
status, input_hash, worktree_path, started_at, claim_holder, claim_expires_at
|
|
258
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'running', ?, ?, ?, ?, ?)
|
|
259
|
-
ON CONFLICT(run_id, unit_id) DO UPDATE SET
|
|
260
|
-
step_id = excluded.step_id,
|
|
261
|
-
node_id = excluded.node_id,
|
|
262
|
-
parent_unit_id = excluded.parent_unit_id,
|
|
263
|
-
phase = excluded.phase,
|
|
264
|
-
runner = excluded.runner,
|
|
265
|
-
engine = excluded.engine,
|
|
266
|
-
model = excluded.model,
|
|
267
|
-
status = excluded.status,
|
|
268
|
-
input_hash = excluded.input_hash,
|
|
269
|
-
worktree_path = excluded.worktree_path,
|
|
270
|
-
started_at = excluded.started_at,
|
|
271
|
-
claim_holder = excluded.claim_holder,
|
|
272
|
-
claim_expires_at = excluded.claim_expires_at,
|
|
273
|
-
result_json = NULL,
|
|
274
|
-
tokens = NULL,
|
|
275
|
-
failure_reason = NULL,
|
|
276
|
-
session_id = NULL,
|
|
277
|
-
finished_at = NULL,
|
|
278
|
-
last_checkin_at = NULL,
|
|
279
|
-
attempts = workflow_run_units.attempts + 1`)
|
|
280
|
-
.run(input.runId, input.unitId, input.stepId, input.nodeId, input.parentUnitId, input.phase, input.runner, input.engine ?? null, input.model, input.inputHash, input.worktreePath ?? null, input.startedAt, input.claimHolder ?? null, input.claimExpiresAt ?? null);
|
|
281
|
-
}
|
|
282
|
-
/**
|
|
283
|
-
* Stamp a `running` claim + heartbeat on a unit row (migration 009, PR #714
|
|
284
|
-
* review round 2). Sets `status = 'running'`, refreshes the heartbeat
|
|
285
|
-
* (`last_checkin_at`), and (re)writes the claim owner + expiry. Must be called
|
|
286
|
-
* inside a transaction that has ALREADY validated the claim is free / expired
|
|
287
|
-
* / already held by this holder, so the write is the final step of a checked
|
|
288
|
-
* reclaim. Never touches `started_at` (the first-claim marker set by
|
|
289
|
-
* {@link insertUnit}).
|
|
290
|
-
*/
|
|
291
|
-
updateUnitClaim(runId, unitId, holder, expiresAt, lastCheckinAt) {
|
|
292
|
-
const result = this.db
|
|
293
|
-
.prepare(`UPDATE workflow_run_units
|
|
294
|
-
SET status = 'running', last_checkin_at = ?, claim_holder = ?, claim_expires_at = ?
|
|
295
|
-
WHERE run_id = ? AND unit_id = ?
|
|
296
|
-
AND EXISTS (SELECT 1 FROM workflow_runs WHERE id = ? AND status = 'active')`)
|
|
297
|
-
.run(lastCheckinAt, holder, expiresAt, runId, unitId, runId);
|
|
298
|
-
return Number(result.changes) === 1;
|
|
299
|
-
}
|
|
300
|
-
/**
|
|
301
|
-
* Record a unit's terminal state (completed / failed / skipped).
|
|
302
|
-
*
|
|
303
|
-
* ## Loud contract (must update exactly one row)
|
|
304
|
-
*
|
|
305
|
-
* A finish ALWAYS targets a row a prior dispatch/claim inserted: every caller
|
|
306
|
-
* ({@link insertUnit} in the executor and the report path,
|
|
307
|
-
* {@link journalGateEvaluationStart} for gate rows) writes the `running` row
|
|
308
|
-
* before finishing it, inside the same writer-queue task or SQLite
|
|
309
|
-
* transaction. If the UPDATE matches NO `(run_id, unit_id)` row the journal is
|
|
310
|
-
* inconsistent — a finish against a missing or mismatched unit id — and the
|
|
311
|
-
* unit's terminal state would silently vanish (the row stays `running`, or no
|
|
312
|
-
* row exists at all). That is a journaling BUG, so we throw loudly at the
|
|
313
|
-
* source instead of no-oping: a stuck-`running` unit would wedge resume
|
|
314
|
-
* (durable-row reuse never matches it) or corrupt budget/gate accounting.
|
|
315
|
-
*/
|
|
316
|
-
finishUnit(input) {
|
|
317
|
-
const result = this.db
|
|
318
|
-
.prepare(`UPDATE workflow_run_units
|
|
319
|
-
SET status = ?, result_json = ?, tokens = ?, failure_reason = ?, session_id = ?, finished_at = ?
|
|
320
|
-
WHERE run_id = ? AND unit_id = ?`)
|
|
321
|
-
.run(input.status, input.resultJson, input.tokens, input.failureReason, input.sessionId ?? null, input.finishedAt, input.runId, input.unitId);
|
|
322
|
-
if (Number(result.changes) === 0) {
|
|
323
|
-
throw new Error(`finishUnit updated no row: no unit "${input.unitId}" exists for run "${input.runId}". ` +
|
|
324
|
-
`A unit must be inserted (dispatched or claimed) before it can be finished — a finish that ` +
|
|
325
|
-
`matches no row indicates a journaling bug (mismatched unit id or a lost dispatch row), not a ` +
|
|
326
|
-
`recoverable state.`);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
/**
|
|
330
|
-
* Finish a unit row ONLY while it is still the exact row a specific dispatch
|
|
331
|
-
* inserted: `running`, with that dispatch's `started_at`. The native
|
|
332
|
-
* executor's guarded finish (single-driver invariant): a run stolen by
|
|
333
|
-
* another engine re-dispatches the unit through {@link insertUnit}, which
|
|
334
|
-
* REPLACES the row (fresh `started_at`, bumped `attempts`) — the stale
|
|
335
|
-
* driver's finish then matches NOTHING instead of clobbering the new
|
|
336
|
-
* driver's live dispatch. Returns whether the row was finished; a zero-row
|
|
337
|
-
* match is a caller-classified outcome here (the executor distinguishes
|
|
338
|
-
* "replaced by another driver" from "row vanished"), unlike
|
|
339
|
-
* {@link finishUnit}'s loud throw, whose callers guarantee their row exists.
|
|
340
|
-
*/
|
|
341
|
-
finishUnitFromDispatch(input) {
|
|
342
|
-
const result = this.db
|
|
343
|
-
.prepare(`UPDATE workflow_run_units
|
|
344
|
-
SET status = ?, result_json = ?, tokens = ?, failure_reason = ?, session_id = ?, finished_at = ?
|
|
345
|
-
WHERE run_id = ? AND unit_id = ? AND status = 'running' AND started_at = ?`)
|
|
346
|
-
.run(input.status, input.resultJson, input.tokens, input.failureReason, input.sessionId ?? null, input.finishedAt, input.runId, input.unitId, input.dispatchStartedAt);
|
|
347
|
-
return Number(result.changes) === 1;
|
|
348
|
-
}
|
|
349
448
|
}
|
|
350
449
|
/**
|
|
351
450
|
* Run `fn` against a {@link WorkflowRunsRepository} bound to state.db
|