akm-cli 0.9.0-rc.0 → 0.9.0-rc.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 +339 -2
- package/SECURITY.md +23 -24
- package/dist/assets/help/help-improve.md +10 -10
- package/dist/assets/hints/cli-hints-full.md +44 -10
- package/dist/assets/hints/cli-hints-short.md +6 -2
- package/dist/assets/{profiles → improve-strategies}/default.json +1 -0
- package/dist/assets/{profiles → improve-strategies}/graph-refresh.json +1 -1
- package/dist/assets/{profiles → improve-strategies}/proactive-maintenance.json +2 -3
- package/dist/assets/{profiles → improve-strategies}/reflect-distill.json +3 -4
- package/dist/assets/prompts/workflow-unit-preamble.md +26 -0
- package/dist/assets/stash-skeleton/README.md +28 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +12 -1
- package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +11 -1
- package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +9 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +7 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +7 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +98 -0
- package/dist/assets/stash-skeleton/facts/conventions/domains.md +63 -0
- package/dist/assets/stash-skeleton/facts/conventions/organization.md +127 -0
- package/dist/assets/tasks/core/backup.yml +1 -0
- package/dist/assets/tasks/core/extract.yml +1 -0
- package/dist/assets/tasks/core/improve.yml +1 -0
- package/dist/assets/tasks/core/index-refresh.yml +1 -0
- package/dist/assets/tasks/core/sync.yml +1 -0
- package/dist/assets/tasks/core/version-check.yml +1 -0
- package/dist/assets/tasks/graph-refresh-weekly.yml +4 -4
- package/dist/assets/templates/html/health.html +5 -1
- package/dist/cli/config-migrate.js +31 -138
- package/dist/cli/config-validate.js +10 -8
- package/dist/cli.js +48 -14
- package/dist/commands/agent/agent-dispatch.js +17 -16
- package/dist/commands/agent/agent-support.js +0 -24
- package/dist/commands/agent/contribute-cli.js +5 -15
- package/dist/commands/backup-cli.js +54 -0
- package/dist/commands/config-cli.js +45 -159
- package/dist/commands/env/env-binding.js +95 -0
- package/dist/commands/env/env-cli.js +8 -65
- package/dist/commands/env/secret.js +8 -5
- package/dist/commands/health/checks.js +130 -83
- package/dist/commands/health/html-report.js +4 -0
- package/dist/commands/health/improve-metrics.js +30 -32
- package/dist/commands/health/llm-usage.js +19 -19
- package/dist/commands/health/md-report.js +4 -0
- package/dist/commands/health/metrics.js +2 -1
- package/dist/commands/health/surfaces.js +5 -4
- package/dist/commands/health.js +1 -1
- package/dist/commands/improve/consolidate/chunking.js +2 -2
- package/dist/commands/improve/consolidate.js +28 -25
- package/dist/commands/improve/distill/promote-memory.js +5 -12
- package/dist/commands/improve/distill/quality-gate.js +5 -7
- package/dist/commands/improve/distill.js +16 -5
- package/dist/commands/improve/eligibility.js +22 -12
- package/dist/commands/improve/extract-cli.js +47 -19
- package/dist/commands/improve/extract.js +110 -62
- package/dist/commands/improve/improve-cli.js +38 -16
- package/dist/commands/improve/improve-result-file.js +30 -24
- package/dist/commands/improve/improve-strategies.js +137 -0
- package/dist/commands/improve/improve.js +60 -30
- package/dist/commands/improve/locks.js +66 -45
- package/dist/commands/improve/loop-stages.js +75 -33
- package/dist/commands/improve/memory/memory-belief.js +79 -7
- package/dist/commands/improve/memory/memory-contradiction-detect.js +12 -4
- package/dist/commands/improve/preparation.js +71 -73
- package/dist/commands/improve/procedural.js +3 -2
- package/dist/commands/improve/recombine.js +2 -1
- package/dist/commands/improve/reflect.js +119 -214
- package/dist/commands/improve/shared.js +11 -5
- package/dist/commands/lint/base-linter.js +152 -42
- package/dist/commands/mv-cli.js +809 -0
- package/dist/commands/proposal/proposal-cli.js +18 -8
- package/dist/commands/proposal/propose.js +64 -69
- package/dist/commands/read/knowledge.js +436 -4
- package/dist/commands/read/remember-cli.js +39 -2
- package/dist/commands/read/search-cli.js +6 -1
- package/dist/commands/registry-cli.js +29 -14
- package/dist/commands/remember.js +2 -0
- package/dist/commands/sources/init.js +13 -14
- package/dist/commands/sources/migration-help.js +7 -4
- package/dist/commands/sources/schema-repair.js +2 -4
- package/dist/commands/sources/source-add.js +62 -73
- package/dist/commands/sources/source-manage.js +50 -46
- package/dist/commands/sources/stash-cli.js +41 -4
- package/dist/commands/tasks/default-tasks.js +12 -12
- package/dist/commands/tasks/tasks-cli.js +7 -3
- package/dist/commands/tasks/tasks.js +113 -18
- package/dist/commands/wiki-cli.js +9 -10
- package/dist/commands/workflow-cli.js +276 -12
- package/dist/core/asset/asset-spec.js +58 -1
- package/dist/core/asset/frontmatter.js +12 -2
- package/dist/core/common.js +5 -3
- package/dist/core/config/config-io.js +28 -17
- package/dist/core/config/config-schema.js +379 -66
- package/dist/core/config/config-types.js +3 -3
- package/dist/core/config/config-version.js +29 -0
- package/dist/core/config/config-walker.js +98 -27
- package/dist/core/config/config.js +132 -266
- package/dist/core/config/deep-merge.js +41 -0
- package/dist/core/config/engine-semantics.js +32 -0
- package/dist/core/errors.js +2 -2
- package/dist/core/extra-params.js +61 -0
- package/dist/core/file-lock.js +201 -56
- package/dist/core/improve-result.js +178 -0
- package/dist/core/json-schema.js +142 -0
- package/dist/core/maintenance-barrier.js +119 -0
- package/dist/core/migration-backup.js +416 -0
- package/dist/core/paths.js +3 -0
- package/dist/core/redaction.js +358 -0
- package/dist/core/state/migrations.js +17 -2
- package/dist/core/state-db.js +44 -1
- package/dist/indexer/db/db.js +118 -2
- package/dist/indexer/graph/graph-extraction.js +28 -16
- package/dist/indexer/index-writer-lock.js +31 -24
- package/dist/indexer/index-written-assets.js +15 -6
- package/dist/indexer/indexer.js +47 -2
- package/dist/indexer/passes/memory-inference.js +10 -6
- package/dist/indexer/passes/metadata.js +250 -0
- package/dist/indexer/search/db-search.js +111 -44
- package/dist/indexer/search/fts-query.js +41 -0
- package/dist/indexer/search/ranking-contributors.js +48 -0
- package/dist/indexer/search/ranking.js +36 -23
- package/dist/indexer/search/search-fields.js +11 -1
- package/dist/indexer/walk/matchers.js +39 -0
- package/dist/integrations/agent/builder-shared.js +7 -0
- package/dist/integrations/agent/builders.js +5 -50
- package/dist/integrations/agent/config.js +3 -143
- package/dist/integrations/agent/detect.js +17 -2
- package/dist/integrations/agent/engine-resolution.js +202 -0
- package/dist/integrations/agent/index.js +1 -2
- package/dist/integrations/agent/model-aliases.js +16 -2
- package/dist/integrations/agent/profiles.js +36 -62
- package/dist/integrations/agent/runner-dispatch.js +91 -4
- package/dist/integrations/agent/runner.js +76 -207
- package/dist/integrations/agent/spawn.js +141 -20
- package/dist/integrations/harnesses/aider/agent-builder.js +112 -0
- package/dist/integrations/harnesses/aider/index.js +57 -0
- package/dist/integrations/harnesses/aider/result-extractor.js +53 -0
- package/dist/integrations/harnesses/amazonq/agent-builder.js +152 -0
- package/dist/integrations/harnesses/amazonq/index.js +58 -0
- package/dist/integrations/harnesses/amazonq/result-extractor.js +48 -0
- package/dist/integrations/harnesses/claude/agent-builder.js +46 -8
- package/dist/integrations/harnesses/claude/index.js +25 -25
- package/dist/integrations/harnesses/claude/result-extractor.js +52 -0
- package/dist/integrations/harnesses/codex/agent-builder.js +136 -0
- package/dist/integrations/harnesses/codex/index.js +62 -0
- package/dist/integrations/harnesses/codex/result-extractor.js +73 -0
- package/dist/integrations/harnesses/copilot/agent-builder.js +121 -0
- package/dist/integrations/harnesses/copilot/index.js +59 -0
- package/dist/integrations/harnesses/copilot/result-extractor.js +151 -0
- package/dist/integrations/harnesses/gemini/agent-builder.js +120 -0
- package/dist/integrations/harnesses/gemini/index.js +59 -0
- package/dist/integrations/harnesses/gemini/result-extractor.js +121 -0
- package/dist/integrations/harnesses/index.js +27 -28
- package/dist/integrations/harnesses/opencode/agent-builder.js +2 -3
- package/dist/integrations/harnesses/opencode/index.js +15 -22
- package/dist/integrations/harnesses/opencode-sdk/harness.js +60 -0
- package/dist/integrations/harnesses/opencode-sdk/index.js +8 -32
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +681 -108
- package/dist/integrations/harnesses/openhands/agent-builder.js +125 -0
- package/dist/integrations/harnesses/openhands/index.js +57 -0
- package/dist/integrations/harnesses/openhands/result-extractor.js +103 -0
- package/dist/integrations/harnesses/pi/agent-builder.js +103 -0
- package/dist/integrations/harnesses/pi/index.js +57 -0
- package/dist/integrations/harnesses/pi/result-extractor.js +135 -0
- package/dist/integrations/harnesses/types.js +8 -32
- package/dist/integrations/lockfile.js +32 -21
- package/dist/integrations/session-logs/index.js +24 -11
- package/dist/llm/client.js +48 -14
- package/dist/llm/feature-gate.js +15 -47
- package/dist/llm/graph-extract.js +1 -1
- package/dist/llm/index-passes.js +8 -42
- package/dist/llm/memory-infer-impl.js +1 -1
- package/dist/llm/usage-persist.js +4 -0
- package/dist/llm/usage-telemetry.js +35 -5
- package/dist/output/renderers.js +3 -2
- package/dist/output/shapes/helpers.js +2 -1
- package/dist/output/shapes/passthrough.js +6 -0
- package/dist/output/text/helpers.js +215 -2
- package/dist/output/text/workflow.js +3 -1
- package/dist/schemas/akm-config.json +16638 -0
- package/dist/schemas/akm-task.json +87 -0
- package/dist/schemas/akm-workflow.json +372 -0
- package/dist/scripts/migrate-storage.js +10944 -8801
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +9247 -350
- package/dist/setup/detected-engines.js +142 -0
- package/dist/setup/engine-config.js +89 -0
- package/dist/setup/setup.js +236 -132
- package/dist/setup/steps/connection.js +61 -32
- package/dist/setup/steps/platforms.js +4 -4
- package/dist/setup/steps.js +3 -2
- package/dist/storage/database.js +13 -1
- package/dist/storage/engines/sqlite-migrations.js +1 -0
- package/dist/storage/repositories/improve-runs-repository.js +5 -5
- package/dist/storage/repositories/task-history-repository.js +78 -0
- package/dist/storage/repositories/workflow-runs-repository.js +190 -1
- package/dist/tasks/parser.js +138 -52
- package/dist/tasks/runner.js +71 -75
- package/dist/tasks/schema.js +1 -1
- package/dist/tasks/validator.js +11 -6
- package/dist/text-import-hook.mjs +1 -1
- package/dist/wiki/wiki.js +9 -8
- package/dist/workflows/authoring/authoring.js +123 -10
- package/dist/workflows/authoring/workflow-program-template.yaml +31 -0
- package/dist/workflows/cli.js +4 -0
- package/dist/workflows/concurrency-policy.js +15 -0
- package/dist/workflows/db.js +200 -13
- package/dist/workflows/exec/brief.js +478 -0
- package/dist/workflows/exec/frozen-judge.js +47 -0
- package/dist/workflows/exec/native-executor.js +1034 -0
- package/dist/workflows/exec/param-secrets.js +115 -0
- package/dist/workflows/exec/report.js +1355 -0
- package/dist/workflows/exec/run-workflow.js +609 -0
- package/dist/workflows/exec/scheduler.js +71 -0
- package/dist/workflows/exec/step-work.js +1212 -0
- package/dist/workflows/exec/unit-writer.js +23 -0
- package/dist/workflows/exec/watch.js +116 -0
- package/dist/workflows/exec/worktree.js +171 -0
- package/dist/workflows/ir/compile.js +375 -0
- package/dist/workflows/ir/freeze.js +243 -0
- package/dist/workflows/ir/params.js +54 -0
- package/dist/workflows/ir/plan-hash.js +68 -0
- package/dist/workflows/ir/schema.js +545 -0
- package/dist/workflows/parser.js +10 -1
- package/dist/workflows/program/expressions.js +369 -0
- package/dist/workflows/program/parser.js +869 -0
- package/dist/workflows/program/project.js +104 -0
- package/dist/workflows/program/schema.js +54 -0
- package/dist/workflows/renderer.js +82 -5
- package/dist/workflows/resource-limits.js +20 -0
- package/dist/workflows/runtime/agent-identity.js +59 -14
- package/dist/workflows/runtime/plan-classifier.js +187 -0
- package/dist/workflows/runtime/runs.js +246 -69
- package/dist/workflows/runtime/unit-checkin.js +45 -0
- package/dist/workflows/runtime/workflow-asset-loader.js +42 -1
- package/dist/workflows/validate-summary.js +24 -3
- package/dist/workflows/validator.js +26 -1
- package/docs/data-and-telemetry.md +4 -3
- package/docs/migration/release-notes/0.6.0.md +1 -1
- package/docs/migration/release-notes/0.7.0.md +5 -4
- package/docs/migration/release-notes/0.9.0-beta.60.md +19 -0
- package/docs/migration/v0.8-to-v0.9.md +401 -0
- package/package.json +4 -2
- package/schemas/akm-config.json +16638 -0
- package/schemas/akm-task.json +87 -0
- package/schemas/akm-workflow.json +372 -0
- package/dist/commands/improve/improve-profiles.js +0 -168
- package/dist/core/config/config-migration.js +0 -602
- package/dist/core/deep-merge.js +0 -38
- package/dist/llm/call-ai.js +0 -62
- package/dist/setup/legacy-config.js +0 -106
- package/docs/README.md +0 -104
- /package/dist/assets/{profiles → improve-strategies}/catchup.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/consolidate.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/frequent.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/memory-focus.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/quick.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/recombine-only.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/synthesize.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/thorough.json +0 -0
|
@@ -9,7 +9,8 @@ import * as p from "../../cli/clack.js";
|
|
|
9
9
|
import { detectAgentCliProfiles, pickDefaultAgentProfile } from "../../integrations/agent/index.js";
|
|
10
10
|
import { probeLlmCapabilities } from "../../llm/client.js";
|
|
11
11
|
import { detectLMStudio, detectOllama } from "../detect.js";
|
|
12
|
-
import {
|
|
12
|
+
import { verifyOpenAiCompatibleEndpoint } from "../detected-engines.js";
|
|
13
|
+
import { cloneLlmConnection, readAgentEngineSelection, readCurrentLlmEngine, } from "../engine-config.js";
|
|
13
14
|
import { prompt, promptOrBack } from "../prompt.js";
|
|
14
15
|
export async function stepOllama(current) {
|
|
15
16
|
const spin = p.spinner();
|
|
@@ -154,7 +155,7 @@ export async function stepLlm(current, ollamaEndpoint, ollamaChatModels, lmStudi
|
|
|
154
155
|
options.push({ value: "lmstudio", label: "LM Studio / local server", hint: lmStudioHint });
|
|
155
156
|
options.push({ value: "custom", label: "Custom OpenAI-compatible endpoint" });
|
|
156
157
|
options.push({ value: "none", label: "Skip LLM", hint: "no metadata enhancement during indexing" });
|
|
157
|
-
const currentLlm =
|
|
158
|
+
const currentLlm = readCurrentLlmEngine(current);
|
|
158
159
|
if (currentLlm) {
|
|
159
160
|
options.push({
|
|
160
161
|
value: "keep",
|
|
@@ -169,7 +170,7 @@ export async function stepLlm(current, ollamaEndpoint, ollamaChatModels, lmStudi
|
|
|
169
170
|
initialValue,
|
|
170
171
|
}));
|
|
171
172
|
if (choice === "keep")
|
|
172
|
-
return
|
|
173
|
+
return cloneLlmConnection(currentLlm);
|
|
173
174
|
if (choice === "none")
|
|
174
175
|
return undefined;
|
|
175
176
|
// Handle "Import from <Harness>" choices
|
|
@@ -180,16 +181,28 @@ export async function stepLlm(current, ollamaEndpoint, ollamaChatModels, lmStudi
|
|
|
180
181
|
// Show a summary before accepting
|
|
181
182
|
p.log.info(`Importing LLM config from ${harness.harnessName}: ` +
|
|
182
183
|
[harness.provider, harness.model, harness.baseUrl].filter(Boolean).join(", "));
|
|
184
|
+
if (!harness.baseUrl || !harness.model) {
|
|
185
|
+
p.log.warn(`Skipping ${harness.harnessName}: no complete endpoint/model was detected.`);
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
const verified = await verifyOpenAiCompatibleEndpoint({
|
|
189
|
+
endpoint: harness.baseUrl,
|
|
190
|
+
model: harness.model,
|
|
191
|
+
apiKeyEnvVar: harness.apiKeyEnvVar,
|
|
192
|
+
});
|
|
193
|
+
if (!verified.ok) {
|
|
194
|
+
p.log.warn(`Skipping ${harness.harnessName}: ${verified.reason}. Fix its endpoint/credential and retry setup.`);
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
183
197
|
const llmConfig = {
|
|
184
|
-
endpoint:
|
|
198
|
+
endpoint: verified.endpoint,
|
|
185
199
|
model: harness.model ?? "",
|
|
186
200
|
temperature: 0.3,
|
|
187
201
|
maxTokens: 1024,
|
|
202
|
+
...(harness.apiKeyEnvVar ? { apiKey: `\${${harness.apiKeyEnvVar}}` } : {}),
|
|
188
203
|
};
|
|
189
204
|
if (harness.provider)
|
|
190
205
|
llmConfig.provider = harness.provider;
|
|
191
|
-
if (harness.baseUrl)
|
|
192
|
-
llmConfig.endpoint = harness.baseUrl;
|
|
193
206
|
return llmConfig;
|
|
194
207
|
}
|
|
195
208
|
let llm;
|
|
@@ -376,7 +389,7 @@ export async function stepSmallModelConnection(current) {
|
|
|
376
389
|
? `${lmStudio.models.length} model${lmStudio.models.length === 1 ? "" : "s"} detected`
|
|
377
390
|
: "http://localhost:1234";
|
|
378
391
|
providerOptions.push({ value: "openai", label: "OpenAI", hint: "requires AKM_LLM_API_KEY" }, { value: "lmstudio", label: "LM Studio / local server", hint: lmStudioHint }, { value: "custom", label: "Custom OpenAI-compatible endpoint" }, { value: "skip", label: "Skip — disable enrichment features" });
|
|
379
|
-
const currentLlmSmall =
|
|
392
|
+
const currentLlmSmall = readCurrentLlmEngine(current);
|
|
380
393
|
if (currentLlmSmall) {
|
|
381
394
|
providerOptions.push({
|
|
382
395
|
value: "keep",
|
|
@@ -391,7 +404,7 @@ export async function stepSmallModelConnection(current) {
|
|
|
391
404
|
initialValue,
|
|
392
405
|
}));
|
|
393
406
|
if (providerChoice === "keep") {
|
|
394
|
-
return { llm:
|
|
407
|
+
return { llm: cloneLlmConnection(currentLlmSmall), skipped: false, ollamaEndpoint };
|
|
395
408
|
}
|
|
396
409
|
if (providerChoice === "skip") {
|
|
397
410
|
p.note([
|
|
@@ -539,8 +552,11 @@ export async function stepSmallModelConnection(current) {
|
|
|
539
552
|
validate: (v) => (!v?.trim() ? "Model name cannot be empty" : undefined),
|
|
540
553
|
}));
|
|
541
554
|
const apiKeyInput = await promptOrBack(() => p.text({
|
|
542
|
-
message: "API key
|
|
543
|
-
placeholder: "",
|
|
555
|
+
message: "API key environment variable name (optional):",
|
|
556
|
+
placeholder: "CUSTOM_LLM_API_KEY",
|
|
557
|
+
validate: (value) => value && !/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)
|
|
558
|
+
? "Use an environment variable name, not a key value"
|
|
559
|
+
: undefined,
|
|
544
560
|
}));
|
|
545
561
|
llm = {
|
|
546
562
|
provider: "custom",
|
|
@@ -548,7 +564,7 @@ export async function stepSmallModelConnection(current) {
|
|
|
548
564
|
model: model.trim(),
|
|
549
565
|
temperature: 0.3,
|
|
550
566
|
maxTokens: 1024,
|
|
551
|
-
...(apiKeyInput?.trim() ? { apiKey: apiKeyInput.trim() } : {}),
|
|
567
|
+
...(apiKeyInput?.trim() ? { apiKey: `\${${apiKeyInput.trim()}}` } : {}),
|
|
552
568
|
};
|
|
553
569
|
}
|
|
554
570
|
// Best-effort probe — never blocks setup.
|
|
@@ -584,7 +600,7 @@ export async function stepAgentConnection(current, smallModel) {
|
|
|
584
600
|
].join("\n"));
|
|
585
601
|
// Detect available CLI agents.
|
|
586
602
|
const detections = detectAgentCliProfiles(current);
|
|
587
|
-
const currentAgentBlock =
|
|
603
|
+
const currentAgentBlock = readAgentEngineSelection(current);
|
|
588
604
|
const availableClis = detections.filter((d) => d.available);
|
|
589
605
|
const agentOptions = [];
|
|
590
606
|
if (!smallModel.skipped && smallModel.llm) {
|
|
@@ -606,8 +622,8 @@ export async function stepAgentConnection(current, smallModel) {
|
|
|
606
622
|
if (currentAgentBlock) {
|
|
607
623
|
const currentDesc = currentAgentBlock.default
|
|
608
624
|
? `CLI: ${currentAgentBlock.default}`
|
|
609
|
-
: currentAgentBlock.
|
|
610
|
-
? `SDK: ${currentAgentBlock.
|
|
625
|
+
: currentAgentBlock.engines?.default?.model
|
|
626
|
+
? `SDK: ${currentAgentBlock.engines.default.model}`
|
|
611
627
|
: "configured";
|
|
612
628
|
agentOptions.push({ value: "keep", label: `Keep current: ${currentDesc}` });
|
|
613
629
|
}
|
|
@@ -647,9 +663,9 @@ export async function stepAgentConnection(current, smallModel) {
|
|
|
647
663
|
else {
|
|
648
664
|
const baseEndpoint = smallModel.llm.endpoint.replace("/v1/chat/completions", "");
|
|
649
665
|
p.log.info(`Endpoint: ${baseEndpoint} (from Step 1)`);
|
|
650
|
-
const profileName = smallModel.llm.provider ?? "default"
|
|
666
|
+
const profileName = `${smallModel.llm.provider ?? "default"}-agent`;
|
|
651
667
|
// Pre-populate from existing agent profile for this provider, if any.
|
|
652
|
-
const existingAgentModel = currentAgentBlock?.
|
|
668
|
+
const existingAgentModel = currentAgentBlock?.engines?.[profileName]?.model ?? smallModel.llm.model ?? undefined;
|
|
653
669
|
const agentModel = await prompt(() => p.text({
|
|
654
670
|
message: "Model to use for agent tasks (same model is fine, larger models work better):",
|
|
655
671
|
placeholder: existingAgentModel ?? "qwen2.5-coder:32b",
|
|
@@ -658,13 +674,14 @@ export async function stepAgentConnection(current, smallModel) {
|
|
|
658
674
|
}));
|
|
659
675
|
return {
|
|
660
676
|
...(currentAgentBlock ?? {}),
|
|
661
|
-
|
|
662
|
-
...(currentAgentBlock?.
|
|
677
|
+
engines: {
|
|
678
|
+
...(currentAgentBlock?.engines ?? {}),
|
|
663
679
|
[profileName]: {
|
|
664
|
-
...(currentAgentBlock?.
|
|
665
|
-
|
|
680
|
+
...(currentAgentBlock?.engines?.[profileName] ?? {}),
|
|
681
|
+
kind: "agent",
|
|
682
|
+
platform: "opencode-sdk",
|
|
666
683
|
model: agentModel.trim(),
|
|
667
|
-
|
|
684
|
+
...(current.defaults?.llmEngine ? { llmEngine: current.defaults.llmEngine } : {}),
|
|
668
685
|
},
|
|
669
686
|
},
|
|
670
687
|
default: profileName,
|
|
@@ -693,8 +710,8 @@ export async function stepAgentConnection(current, smallModel) {
|
|
|
693
710
|
}
|
|
694
711
|
// "new-connection" (also fall-through from "same-provider" when Step 1 was skipped)
|
|
695
712
|
// Pre-populate from current "custom" agent profile if available.
|
|
696
|
-
const currentCustomAgentProfile = currentAgentBlock?.
|
|
697
|
-
const currentNewEndpoint =
|
|
713
|
+
const currentCustomAgentProfile = currentAgentBlock?.engines?.["custom-agent"];
|
|
714
|
+
const currentNewEndpoint = undefined;
|
|
698
715
|
const currentNewModel = currentCustomAgentProfile?.model ?? undefined;
|
|
699
716
|
const newEndpoint = await prompt(() => p.text({
|
|
700
717
|
message: "OpenAI-compatible chat completions endpoint:",
|
|
@@ -708,8 +725,11 @@ export async function stepAgentConnection(current, smallModel) {
|
|
|
708
725
|
},
|
|
709
726
|
}));
|
|
710
727
|
const newApiKeyInput = await promptOrBack(() => p.text({
|
|
711
|
-
message: "API key
|
|
712
|
-
placeholder: "",
|
|
728
|
+
message: "API key environment variable name (optional):",
|
|
729
|
+
placeholder: "CUSTOM_LLM_API_KEY",
|
|
730
|
+
validate: (value) => value && !/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)
|
|
731
|
+
? "Use an environment variable name, not a key value"
|
|
732
|
+
: undefined,
|
|
713
733
|
}));
|
|
714
734
|
const newModel = await prompt(() => p.text({
|
|
715
735
|
message: "Model name (larger is better, e.g. gpt-4o):",
|
|
@@ -717,18 +737,27 @@ export async function stepAgentConnection(current, smallModel) {
|
|
|
717
737
|
...(currentNewModel ? { defaultValue: currentNewModel } : {}),
|
|
718
738
|
validate: (v) => (!v?.trim() ? "Model name cannot be empty" : undefined),
|
|
719
739
|
}));
|
|
740
|
+
const llmEngineName = "custom-llm";
|
|
741
|
+
const agentEngineName = "custom-agent";
|
|
720
742
|
const customProfile = {
|
|
721
|
-
|
|
722
|
-
|
|
743
|
+
kind: "agent",
|
|
744
|
+
platform: "opencode-sdk",
|
|
745
|
+
llmEngine: llmEngineName,
|
|
723
746
|
model: newModel.trim(),
|
|
724
|
-
...(newApiKeyInput?.trim() ? { apiKey: newApiKeyInput.trim() } : {}),
|
|
725
747
|
};
|
|
726
748
|
return {
|
|
727
749
|
...(currentAgentBlock ?? {}),
|
|
728
|
-
|
|
729
|
-
...(currentAgentBlock?.
|
|
730
|
-
|
|
750
|
+
engines: {
|
|
751
|
+
...(currentAgentBlock?.engines ?? {}),
|
|
752
|
+
[llmEngineName]: {
|
|
753
|
+
kind: "llm",
|
|
754
|
+
provider: "custom",
|
|
755
|
+
endpoint: newEndpoint.trim(),
|
|
756
|
+
model: newModel.trim(),
|
|
757
|
+
...(newApiKeyInput?.trim() ? { apiKey: `\${${newApiKeyInput.trim()}}` } : {}),
|
|
758
|
+
},
|
|
759
|
+
[agentEngineName]: customProfile,
|
|
731
760
|
},
|
|
732
|
-
default:
|
|
761
|
+
default: agentEngineName,
|
|
733
762
|
};
|
|
734
763
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import * as p from "../../cli/clack.js";
|
|
9
9
|
import { detectAgentCliProfiles, pickDefaultAgentProfile } from "../../integrations/agent/index.js";
|
|
10
10
|
import { detectAgentPlatforms } from "../detect.js";
|
|
11
|
-
import {
|
|
11
|
+
import { readAgentEngineSelection } from "../engine-config.js";
|
|
12
12
|
import { prompt } from "../prompt.js";
|
|
13
13
|
export async function stepAgentPlatforms(current) {
|
|
14
14
|
const platforms = detectAgentPlatforms();
|
|
@@ -66,7 +66,7 @@ export function printCapabilitySummary(smallModelSkipped, agentConfigured) {
|
|
|
66
66
|
p.note(lines.join("\n"), "Feature Summary");
|
|
67
67
|
}
|
|
68
68
|
export async function stepAgentSelection(current, detections) {
|
|
69
|
-
const currentAgentBlock =
|
|
69
|
+
const currentAgentBlock = readAgentEngineSelection(current);
|
|
70
70
|
const available = detections.filter((d) => d.available);
|
|
71
71
|
if (available.length === 0) {
|
|
72
72
|
return currentAgentBlock;
|
|
@@ -85,7 +85,7 @@ export async function stepAgentSelection(current, detections) {
|
|
|
85
85
|
initialValue,
|
|
86
86
|
}));
|
|
87
87
|
if (selectedDefault === "disabled") {
|
|
88
|
-
if (!currentAgentBlock?.
|
|
88
|
+
if (!currentAgentBlock?.engines) {
|
|
89
89
|
return undefined;
|
|
90
90
|
}
|
|
91
91
|
return {
|
|
@@ -110,7 +110,7 @@ export async function stepAgentSelection(current, detections) {
|
|
|
110
110
|
*/
|
|
111
111
|
export function stepAgentCliDetection(current, detectFn = detectAgentCliProfiles) {
|
|
112
112
|
const detections = detectFn(current);
|
|
113
|
-
const currentAgentBlock =
|
|
113
|
+
const currentAgentBlock = readAgentEngineSelection(current);
|
|
114
114
|
const defaultName = pickDefaultAgentProfile(detections, currentAgentBlock?.default);
|
|
115
115
|
// No installed agents found and no existing config → leave block absent.
|
|
116
116
|
if (!defaultName && !currentAgentBlock) {
|
package/dist/setup/steps.js
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
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 { deepMergeConfig } from "../core/config/deep-merge.js";
|
|
4
5
|
/**
|
|
5
6
|
* Build a fresh `SetupContext` over a starting config. The returned context
|
|
6
7
|
* applies deltas in-place onto an internal accumulator and exposes the
|
|
7
8
|
* latest snapshot via `ctx.config`.
|
|
8
9
|
*/
|
|
9
10
|
export function createSetupContext(initial, options) {
|
|
10
|
-
let acc = {
|
|
11
|
+
let acc = deepMergeConfig({}, initial);
|
|
11
12
|
return {
|
|
12
13
|
get config() {
|
|
13
14
|
return acc;
|
|
14
15
|
},
|
|
15
16
|
nonInteractive: options.nonInteractive,
|
|
16
17
|
apply(delta) {
|
|
17
|
-
acc =
|
|
18
|
+
acc = deepMergeConfig(acc, delta);
|
|
18
19
|
},
|
|
19
20
|
};
|
|
20
21
|
}
|
package/dist/storage/database.js
CHANGED
|
@@ -133,5 +133,17 @@ function openNodeDatabase(path, opts) {
|
|
|
133
133
|
if (opts?.create === false)
|
|
134
134
|
options.fileMustExist = true;
|
|
135
135
|
const db = opts ? new BetterSqlite3(path, options) : new BetterSqlite3(path);
|
|
136
|
-
return
|
|
136
|
+
return {
|
|
137
|
+
prepare: db.prepare.bind(db),
|
|
138
|
+
exec: db.exec.bind(db),
|
|
139
|
+
// better-sqlite3 exposes mutations on prepared statements, while
|
|
140
|
+
// bun:sqlite also provides db.run(). Normalize the latter at the provider
|
|
141
|
+
// boundary so callers and maintenance wrappers can rely on one contract.
|
|
142
|
+
run: (sql, ...params) => db.prepare(sql).run(...params),
|
|
143
|
+
transaction: db.transaction.bind(db),
|
|
144
|
+
get inTransaction() {
|
|
145
|
+
return db.inTransaction;
|
|
146
|
+
},
|
|
147
|
+
close: db.close.bind(db),
|
|
148
|
+
};
|
|
137
149
|
}
|
|
@@ -34,6 +34,7 @@ export function runMigrations(db, migrations, opts) {
|
|
|
34
34
|
for (const migration of migrations) {
|
|
35
35
|
if (applied.has(migration.id))
|
|
36
36
|
continue;
|
|
37
|
+
opts?.beforeMigration?.(migration);
|
|
37
38
|
db.transaction(() => {
|
|
38
39
|
db.exec(migration.up);
|
|
39
40
|
db.prepare("INSERT INTO schema_migrations (id) VALUES (?)").run(migration.id);
|
|
@@ -103,10 +103,10 @@ export function recordImproveRun(db, input) {
|
|
|
103
103
|
const metricsObj = input.metrics ?? computeImproveRunMetrics(input.result);
|
|
104
104
|
db.prepare(`
|
|
105
105
|
INSERT INTO improve_runs
|
|
106
|
-
|
|
106
|
+
(id, started_at, completed_at, stash_dir, dry_run, profile, strategy,
|
|
107
107
|
scope_mode, scope_value, guidance, ok, result_json, metrics_json, metadata_json)
|
|
108
|
-
|
|
109
|
-
`).run(input.id, input.startedAt, input.completedAt, input.stashDir, input.dryRun ? 1 : 0, input.
|
|
108
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
109
|
+
`).run(input.id, input.startedAt, input.completedAt, input.stashDir, input.dryRun ? 1 : 0, input.legacyProfile ?? null, input.strategy ?? null, input.scopeMode, input.scopeValue, input.guidance, input.ok ? 1 : 0, JSON.stringify(input.result), JSON.stringify(metricsObj), JSON.stringify(input.metadata ?? {}));
|
|
110
110
|
}
|
|
111
111
|
/**
|
|
112
112
|
* Read real (non-dry-run) improve_runs rows whose `started_at` falls in the
|
|
@@ -124,8 +124,8 @@ export function recordImproveRun(db, input) {
|
|
|
124
124
|
*/
|
|
125
125
|
export function queryImproveRuns(db, since, until) {
|
|
126
126
|
const sql = until
|
|
127
|
-
? "SELECT id, started_at, completed_at, ok, scope_mode, scope_value, result_json FROM improve_runs WHERE started_at >= ? AND started_at < ? AND dry_run = 0 ORDER BY started_at DESC"
|
|
128
|
-
: "SELECT id, started_at, completed_at, ok, scope_mode, scope_value, result_json FROM improve_runs WHERE started_at >= ? AND dry_run = 0 ORDER BY started_at DESC";
|
|
127
|
+
? "SELECT id, started_at, completed_at, ok, scope_mode, scope_value, profile AS legacyProfile, strategy, result_json FROM improve_runs WHERE started_at >= ? AND started_at < ? AND dry_run = 0 ORDER BY started_at DESC"
|
|
128
|
+
: "SELECT id, started_at, completed_at, ok, scope_mode, scope_value, profile AS legacyProfile, strategy, result_json FROM improve_runs WHERE started_at >= ? AND dry_run = 0 ORDER BY started_at DESC";
|
|
129
129
|
return (until ? db.prepare(sql).all(since, until) : db.prepare(sql).all(since));
|
|
130
130
|
}
|
|
131
131
|
/**
|
|
@@ -1,6 +1,84 @@
|
|
|
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
|
+
function metadataError(message) {
|
|
5
|
+
throw new Error(`invalid task_history metadata_json: ${message}`);
|
|
6
|
+
}
|
|
7
|
+
function isRecord(value) {
|
|
8
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9
|
+
}
|
|
10
|
+
function validateDetail(value) {
|
|
11
|
+
if (value === undefined || value === null)
|
|
12
|
+
return;
|
|
13
|
+
if (!isRecord(value))
|
|
14
|
+
metadataError("detail must be an object or null");
|
|
15
|
+
const allowed = new Set(["runId", "reason", "error", "exitCode"]);
|
|
16
|
+
const unknown = Object.keys(value).filter((key) => !allowed.has(key));
|
|
17
|
+
if (unknown.length > 0)
|
|
18
|
+
metadataError(`unknown detail fields: ${unknown.sort().join(", ")}`);
|
|
19
|
+
for (const field of ["runId", "reason", "error"]) {
|
|
20
|
+
if (value[field] !== undefined && typeof value[field] !== "string")
|
|
21
|
+
metadataError(`detail.${field} must be a string`);
|
|
22
|
+
}
|
|
23
|
+
if (value.exitCode !== undefined && value.exitCode !== null && typeof value.exitCode !== "number") {
|
|
24
|
+
metadataError("detail.exitCode must be a number or null");
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** Decode historical v1 and current v2 metadata without cross-version aliases. */
|
|
28
|
+
export function decodeTaskHistoryMetadata(input) {
|
|
29
|
+
let parsed = input;
|
|
30
|
+
if (typeof input === "string") {
|
|
31
|
+
try {
|
|
32
|
+
parsed = JSON.parse(input);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
metadataError("not valid JSON");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (!isRecord(parsed))
|
|
39
|
+
metadataError("root must be an object");
|
|
40
|
+
const version = parsed.metadataVersion ?? 1;
|
|
41
|
+
if (version === 1) {
|
|
42
|
+
const allowed = new Set(["metadataVersion", "durationMs", "detail", "profile"]);
|
|
43
|
+
const unknown = Object.keys(parsed).filter((key) => !allowed.has(key));
|
|
44
|
+
if (unknown.length > 0)
|
|
45
|
+
metadataError(`unknown v1 fields: ${unknown.sort().join(", ")}`);
|
|
46
|
+
if (parsed.metadataVersion !== undefined && parsed.metadataVersion !== 1)
|
|
47
|
+
metadataError("invalid v1 metadataVersion");
|
|
48
|
+
if (parsed.durationMs !== undefined && typeof parsed.durationMs !== "number")
|
|
49
|
+
metadataError("durationMs must be a number");
|
|
50
|
+
if (parsed.profile !== undefined && typeof parsed.profile !== "string")
|
|
51
|
+
metadataError("profile must be a string");
|
|
52
|
+
validateDetail(parsed.detail);
|
|
53
|
+
return {
|
|
54
|
+
metadataVersion: 1,
|
|
55
|
+
...(typeof parsed.durationMs === "number" ? { durationMs: parsed.durationMs } : {}),
|
|
56
|
+
...(parsed.detail !== undefined && parsed.detail !== null ? { detail: parsed.detail } : {}),
|
|
57
|
+
...(typeof parsed.profile === "string" ? { legacyProfile: parsed.profile } : {}),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
if (version === 2) {
|
|
61
|
+
const allowed = new Set(["metadataVersion", "durationMs", "detail", "engine"]);
|
|
62
|
+
const unknown = Object.keys(parsed).filter((key) => !allowed.has(key));
|
|
63
|
+
if (unknown.length > 0)
|
|
64
|
+
metadataError(`unknown v2 fields: ${unknown.sort().join(", ")}`);
|
|
65
|
+
if (typeof parsed.durationMs !== "number")
|
|
66
|
+
metadataError("durationMs must be a number");
|
|
67
|
+
if (!("detail" in parsed))
|
|
68
|
+
metadataError("detail is required in v2");
|
|
69
|
+
if (parsed.engine !== undefined && parsed.engine !== null && typeof parsed.engine !== "string") {
|
|
70
|
+
metadataError("engine must be a string or null");
|
|
71
|
+
}
|
|
72
|
+
validateDetail(parsed.detail);
|
|
73
|
+
return {
|
|
74
|
+
metadataVersion: 2,
|
|
75
|
+
durationMs: parsed.durationMs,
|
|
76
|
+
detail: parsed.detail ?? null,
|
|
77
|
+
...(parsed.engine !== undefined ? { engine: parsed.engine } : {}),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
metadataError(`unsupported metadataVersion: ${String(version)}`);
|
|
81
|
+
}
|
|
4
82
|
/**
|
|
5
83
|
* Upsert a task history row.
|
|
6
84
|
*/
|
|
@@ -56,7 +56,17 @@ export class WorkflowRunsRepository {
|
|
|
56
56
|
params.push(filter.workflowRef);
|
|
57
57
|
}
|
|
58
58
|
if (filter.activeOnly) {
|
|
59
|
-
|
|
59
|
+
// `activeOnly` means EXACTLY status='active' — a run currently
|
|
60
|
+
// executable. A `blocked` run is NOT active (it is parked awaiting a
|
|
61
|
+
// human `resume`), so it must never appear under `--active`, or a script
|
|
62
|
+
// treating `--active` output as executable work would pick up a blocked
|
|
63
|
+
// run (owner manual-validation finding 1). Blocked runs stay visible in
|
|
64
|
+
// plain `list` (all statuses) with their `blocked` status. The
|
|
65
|
+
// active-OR-blocked scope semantics some call sites want (the `akm show`
|
|
66
|
+
// scope guard, which surfaces a blocked run as the scope's occupant) live
|
|
67
|
+
// in the SEPARATE {@link findActiveOrBlockedRunForScope} — never folded
|
|
68
|
+
// into this shared list filter.
|
|
69
|
+
filters.push("status = 'active'");
|
|
60
70
|
}
|
|
61
71
|
const where = filters.length > 0 ? `WHERE ${filters.join(" AND ")}` : "";
|
|
62
72
|
return this.db
|
|
@@ -120,6 +130,185 @@ export class WorkflowRunsRepository {
|
|
|
120
130
|
rearmCheckin(runId, checkinArmedAt) {
|
|
121
131
|
this.db.prepare("UPDATE workflow_runs SET checkin_armed_at = ? WHERE id = ?").run(checkinArmedAt, runId);
|
|
122
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* Freeze the compiled plan on the run row (migration 006, redesign addendum
|
|
135
|
+
* R1): `planJson` is the CANONICAL plan JSON (`ir/plan-hash.ts`), `planHash`
|
|
136
|
+
* its sha256. Called by `startWorkflowRun` inside the same transaction as
|
|
137
|
+
* `insertRun`, so a run row never exists without its frozen plan. Read back
|
|
138
|
+
* via {@link getRunById} (`plan_json` / `plan_hash` on the row).
|
|
139
|
+
*/
|
|
140
|
+
setRunPlan(runId, planJson, planHash, planIrVersion = 3) {
|
|
141
|
+
this.db
|
|
142
|
+
.prepare("UPDATE workflow_runs SET plan_json = ?, plan_hash = ?, plan_ir_version = ? WHERE id = ?")
|
|
143
|
+
.run(planJson, planHash, planIrVersion, runId);
|
|
144
|
+
}
|
|
145
|
+
// ── engine run lease (migration 006 columns, R2 enforcement) ──────────────
|
|
146
|
+
//
|
|
147
|
+
// Single-driver invariant: at most one `akm workflow run` invocation drives
|
|
148
|
+
// a run at a time. The lease is (holder id, expiry); all timestamps are
|
|
149
|
+
// ISO-8601 UTC strings, which compare correctly with SQL `<` (lexicographic
|
|
150
|
+
// order matches chronological order for a fixed-format UTC ISO string).
|
|
151
|
+
/**
|
|
152
|
+
* Atomically claim the run lease: succeeds when the run is unleased OR the
|
|
153
|
+
* existing lease has expired (`engine_lease_until < now` — crash recovery).
|
|
154
|
+
* A live lease held by anyone (including a stale copy of the same holder)
|
|
155
|
+
* is NOT reclaimable through this method; the single UPDATE is the whole
|
|
156
|
+
* claim, so two racing invocations cannot both win.
|
|
157
|
+
*/
|
|
158
|
+
acquireEngineLease(runId, holder, until, now) {
|
|
159
|
+
const result = this.db
|
|
160
|
+
.prepare(`UPDATE workflow_runs
|
|
161
|
+
SET engine_lease_holder = ?, engine_lease_until = ?
|
|
162
|
+
WHERE id = ? AND (engine_lease_holder IS NULL OR engine_lease_until IS NULL OR engine_lease_until < ?)`)
|
|
163
|
+
.run(holder, until, runId, now);
|
|
164
|
+
return Number(result.changes) > 0;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Extend the lease expiry — only while `holder` still owns it. Returns
|
|
168
|
+
* false when the lease was lost (expired and claimed by another engine),
|
|
169
|
+
* so the caller can stop driving instead of racing the new owner.
|
|
170
|
+
*/
|
|
171
|
+
renewEngineLease(runId, holder, until) {
|
|
172
|
+
const result = this.db
|
|
173
|
+
.prepare("UPDATE workflow_runs SET engine_lease_until = ? WHERE id = ? AND engine_lease_holder = ?")
|
|
174
|
+
.run(until, runId, holder);
|
|
175
|
+
return Number(result.changes) > 0;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Clear the lease — only while `holder` still owns it, so a crashed-then-
|
|
179
|
+
* recovered invocation can never release a lease another engine has since
|
|
180
|
+
* claimed. Releasing an already-lost lease is a harmless no-op.
|
|
181
|
+
*/
|
|
182
|
+
releaseEngineLease(runId, holder) {
|
|
183
|
+
this.db
|
|
184
|
+
.prepare("UPDATE workflow_runs SET engine_lease_holder = NULL, engine_lease_until = NULL WHERE id = ? AND engine_lease_holder = ?")
|
|
185
|
+
.run(runId, holder);
|
|
186
|
+
}
|
|
187
|
+
// ── unit rows (migration 004) ──────────────────────────────────────────────
|
|
188
|
+
//
|
|
189
|
+
// Writes to `workflow_run_units` should go through the serialized writer
|
|
190
|
+
// queue (`src/workflows/exec/unit-writer.ts`) when N units may complete
|
|
191
|
+
// concurrently — SQLite has a single writer and `withWorkflowRunsRepo`
|
|
192
|
+
// opens a fresh connection per call.
|
|
193
|
+
getUnitsForRun(runId) {
|
|
194
|
+
return this.db
|
|
195
|
+
.prepare("SELECT * FROM workflow_run_units WHERE run_id = ? ORDER BY started_at ASC, unit_id ASC")
|
|
196
|
+
.all(runId);
|
|
197
|
+
}
|
|
198
|
+
getUnitsForStep(runId, stepId) {
|
|
199
|
+
return this.db
|
|
200
|
+
.prepare("SELECT * FROM workflow_run_units WHERE run_id = ? AND step_id = ? ORDER BY started_at ASC, unit_id ASC")
|
|
201
|
+
.all(runId, stepId);
|
|
202
|
+
}
|
|
203
|
+
/** One unit row by primary key, or undefined. Used by the R3 report path's
|
|
204
|
+
* guarded read-then-write (idempotency / replay-divergence / claim checks). */
|
|
205
|
+
getUnit(runId, unitId) {
|
|
206
|
+
return this.db.prepare("SELECT * FROM workflow_run_units WHERE run_id = ? AND unit_id = ?").get(runId, unitId);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Stamp a unit-level check-in heartbeat (migration 007, R3 driver protocol):
|
|
210
|
+
* a driver executing a unit calls `report --status running` to claim (first
|
|
211
|
+
* call) or heartbeat (subsequent) it. Sets `status = 'running'` and
|
|
212
|
+
* `last_checkin_at`; NEVER touches `started_at` (the first-claim marker set by
|
|
213
|
+
* {@link insertUnit}) so the heartbeat window advances without resetting the
|
|
214
|
+
* claim time. The caller guards against re-claiming a terminal unit inside its
|
|
215
|
+
* transaction.
|
|
216
|
+
*/
|
|
217
|
+
updateUnitCheckin(runId, unitId, lastCheckinAt) {
|
|
218
|
+
this.db
|
|
219
|
+
.prepare("UPDATE workflow_run_units SET status = 'running', last_checkin_at = ? WHERE run_id = ? AND unit_id = ?")
|
|
220
|
+
.run(lastCheckinAt, runId, unitId);
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Insert a unit row in `running` state (a dispatch is starting now).
|
|
224
|
+
*
|
|
225
|
+
* Upsert on the (run_id, unit_id) primary key: durable-row resume
|
|
226
|
+
* re-dispatches units whose previous attempt never reached a terminal status
|
|
227
|
+
* (a crash mid-step leaves `running` rows). The fresh dispatch REPLACES the
|
|
228
|
+
* stale row — value columns (`result_json`/`tokens`/`failure_reason`/
|
|
229
|
+
* `session_id`/`finished_at`/`last_checkin_at`) are reset exactly as the old
|
|
230
|
+
* `INSERT OR REPLACE` reset them, and the dispatch metadata is overwritten —
|
|
231
|
+
* but `attempts` is INCREMENTED rather than reset (migration 008). A first
|
|
232
|
+
* insert lands `attempts = 1` (column default); each re-dispatch bumps it, so
|
|
233
|
+
* budget/lifetime seeds that sum `attempts` charge every crash-retried
|
|
234
|
+
* dispatch instead of collapsing them into one row. Using `ON CONFLICT DO
|
|
235
|
+
* UPDATE` (not `INSERT OR REPLACE`, which deletes+reinserts) is what lets the
|
|
236
|
+
* increment read the prior row's counter.
|
|
237
|
+
*/
|
|
238
|
+
insertUnit(input) {
|
|
239
|
+
this.db
|
|
240
|
+
.prepare(`INSERT INTO workflow_run_units (
|
|
241
|
+
run_id, unit_id, step_id, node_id, parent_unit_id, phase, runner, engine, model,
|
|
242
|
+
status, input_hash, worktree_path, started_at, claim_holder, claim_expires_at
|
|
243
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'running', ?, ?, ?, ?, ?)
|
|
244
|
+
ON CONFLICT(run_id, unit_id) DO UPDATE SET
|
|
245
|
+
step_id = excluded.step_id,
|
|
246
|
+
node_id = excluded.node_id,
|
|
247
|
+
parent_unit_id = excluded.parent_unit_id,
|
|
248
|
+
phase = excluded.phase,
|
|
249
|
+
runner = excluded.runner,
|
|
250
|
+
engine = excluded.engine,
|
|
251
|
+
model = excluded.model,
|
|
252
|
+
status = excluded.status,
|
|
253
|
+
input_hash = excluded.input_hash,
|
|
254
|
+
worktree_path = excluded.worktree_path,
|
|
255
|
+
started_at = excluded.started_at,
|
|
256
|
+
claim_holder = excluded.claim_holder,
|
|
257
|
+
claim_expires_at = excluded.claim_expires_at,
|
|
258
|
+
result_json = NULL,
|
|
259
|
+
tokens = NULL,
|
|
260
|
+
failure_reason = NULL,
|
|
261
|
+
session_id = NULL,
|
|
262
|
+
finished_at = NULL,
|
|
263
|
+
last_checkin_at = NULL,
|
|
264
|
+
attempts = workflow_run_units.attempts + 1`)
|
|
265
|
+
.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);
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Stamp a `--status running` claim + heartbeat on a unit row (migration 009,
|
|
269
|
+
* PR #714 review round 2). Sets `status = 'running'`, refreshes the heartbeat
|
|
270
|
+
* (`last_checkin_at`), and (re)writes the claim owner + expiry. Called inside
|
|
271
|
+
* the report path's running-claim transaction AFTER it has validated that the
|
|
272
|
+
* claim is free / expired / already held by this holder, so the write is the
|
|
273
|
+
* final step of a checked reclaim. Never touches `started_at` (the first-claim
|
|
274
|
+
* marker set by {@link insertUnit}).
|
|
275
|
+
*/
|
|
276
|
+
updateUnitClaim(runId, unitId, holder, expiresAt, lastCheckinAt) {
|
|
277
|
+
this.db
|
|
278
|
+
.prepare(`UPDATE workflow_run_units
|
|
279
|
+
SET status = 'running', last_checkin_at = ?, claim_holder = ?, claim_expires_at = ?
|
|
280
|
+
WHERE run_id = ? AND unit_id = ?`)
|
|
281
|
+
.run(lastCheckinAt, holder, expiresAt, runId, unitId);
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Record a unit's terminal state (completed / failed / skipped).
|
|
285
|
+
*
|
|
286
|
+
* ## Loud contract (must update exactly one row)
|
|
287
|
+
*
|
|
288
|
+
* A finish ALWAYS targets a row a prior dispatch/claim inserted: every caller
|
|
289
|
+
* ({@link insertUnit} in the executor and the report path,
|
|
290
|
+
* {@link journalGateEvaluationStart} for gate rows) writes the `running` row
|
|
291
|
+
* before finishing it, inside the same writer-queue task or SQLite
|
|
292
|
+
* transaction. If the UPDATE matches NO `(run_id, unit_id)` row the journal is
|
|
293
|
+
* inconsistent — a finish against a missing or mismatched unit id — and the
|
|
294
|
+
* unit's terminal state would silently vanish (the row stays `running`, or no
|
|
295
|
+
* row exists at all). That is a journaling BUG, so we throw loudly at the
|
|
296
|
+
* source instead of no-oping: a stuck-`running` unit would wedge resume
|
|
297
|
+
* (durable-row reuse never matches it) or corrupt budget/gate accounting.
|
|
298
|
+
*/
|
|
299
|
+
finishUnit(input) {
|
|
300
|
+
const result = this.db
|
|
301
|
+
.prepare(`UPDATE workflow_run_units
|
|
302
|
+
SET status = ?, result_json = ?, tokens = ?, failure_reason = ?, session_id = ?, finished_at = ?
|
|
303
|
+
WHERE run_id = ? AND unit_id = ?`)
|
|
304
|
+
.run(input.status, input.resultJson, input.tokens, input.failureReason, input.sessionId ?? null, input.finishedAt, input.runId, input.unitId);
|
|
305
|
+
if (Number(result.changes) === 0) {
|
|
306
|
+
throw new Error(`finishUnit updated no row: no unit "${input.unitId}" exists for run "${input.runId}". ` +
|
|
307
|
+
`A unit must be inserted (dispatched or claimed) before it can be finished — a finish that ` +
|
|
308
|
+
`matches no row indicates a journaling bug (mismatched unit id or a lost dispatch row), not a ` +
|
|
309
|
+
`recoverable state.`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
123
312
|
}
|
|
124
313
|
/**
|
|
125
314
|
* Open the workflow database (bound to {@link StorageLocations.workflowDb}),
|