akm-cli 0.9.0-rc.1 → 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 +223 -9
- 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/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/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/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/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 +358 -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/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 +116 -1
- 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/integrations/agent/builder-shared.js +7 -0
- package/dist/integrations/agent/builders.js +5 -52
- 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 +7 -2
- package/dist/integrations/agent/profiles.js +6 -99
- package/dist/integrations/agent/runner-dispatch.js +76 -13
- package/dist/integrations/agent/runner.js +76 -207
- package/dist/integrations/agent/spawn.js +4 -6
- package/dist/integrations/harnesses/aider/agent-builder.js +2 -3
- package/dist/integrations/harnesses/aider/index.js +0 -1
- package/dist/integrations/harnesses/amazonq/agent-builder.js +2 -3
- package/dist/integrations/harnesses/amazonq/index.js +0 -1
- package/dist/integrations/harnesses/claude/agent-builder.js +2 -3
- package/dist/integrations/harnesses/claude/index.js +0 -2
- package/dist/integrations/harnesses/codex/agent-builder.js +2 -3
- package/dist/integrations/harnesses/codex/index.js +0 -1
- package/dist/integrations/harnesses/copilot/agent-builder.js +2 -3
- package/dist/integrations/harnesses/copilot/index.js +0 -1
- package/dist/integrations/harnesses/gemini/agent-builder.js +2 -3
- package/dist/integrations/harnesses/gemini/index.js +0 -1
- package/dist/integrations/harnesses/index.js +1 -24
- package/dist/integrations/harnesses/opencode/agent-builder.js +2 -3
- package/dist/integrations/harnesses/opencode/index.js +0 -6
- package/dist/integrations/harnesses/opencode-sdk/harness.js +1 -6
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +274 -191
- package/dist/integrations/harnesses/openhands/agent-builder.js +2 -3
- package/dist/integrations/harnesses/openhands/index.js +0 -1
- package/dist/integrations/harnesses/pi/agent-builder.js +2 -3
- package/dist/integrations/harnesses/pi/index.js +0 -1
- package/dist/integrations/harnesses/types.js +1 -32
- package/dist/integrations/lockfile.js +32 -21
- 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/shapes/helpers.js +2 -1
- package/dist/output/shapes/passthrough.js +2 -0
- package/dist/output/text/helpers.js +3 -1
- package/dist/schemas/akm-config.json +11013 -8600
- package/dist/schemas/akm-task.json +87 -0
- package/dist/schemas/akm-workflow.json +57 -13
- package/dist/scripts/migrate-storage.js +8591 -509
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +8623 -484
- 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 +9 -8
- 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/wiki/wiki.js +9 -8
- package/dist/workflows/authoring/workflow-program-template.yaml +3 -3
- package/dist/workflows/concurrency-policy.js +15 -0
- package/dist/workflows/db.js +65 -13
- package/dist/workflows/exec/brief.js +18 -24
- package/dist/workflows/exec/frozen-judge.js +47 -0
- package/dist/workflows/exec/native-executor.js +124 -65
- package/dist/workflows/exec/report.js +93 -33
- package/dist/workflows/exec/run-workflow.js +38 -25
- package/dist/workflows/exec/scheduler.js +12 -41
- package/dist/workflows/exec/step-work.js +91 -35
- package/dist/workflows/ir/compile.js +13 -26
- package/dist/workflows/ir/freeze.js +243 -0
- package/dist/workflows/ir/plan-hash.js +40 -5
- package/dist/workflows/ir/schema.js +542 -1
- package/dist/workflows/parser.js +7 -0
- package/dist/workflows/program/parser.js +132 -23
- package/dist/workflows/program/project.js +3 -4
- package/dist/workflows/program/schema.js +2 -2
- package/dist/workflows/resource-limits.js +20 -0
- package/dist/workflows/runtime/plan-classifier.js +187 -0
- package/dist/workflows/runtime/runs.js +54 -47
- package/dist/workflows/runtime/workflow-asset-loader.js +0 -22
- package/dist/workflows/validator.js +25 -0
- package/docs/data-and-telemetry.md +2 -2
- 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/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
|
@@ -41,13 +41,17 @@ import { randomUUID } from "node:crypto";
|
|
|
41
41
|
import { UsageError } from "../../core/errors.js";
|
|
42
42
|
import { appendEvent } from "../../core/events.js";
|
|
43
43
|
import { validateJsonSchemaSubset } from "../../core/json-schema.js";
|
|
44
|
+
import { acquireMaintenanceActivity } from "../../core/maintenance-barrier.js";
|
|
45
|
+
import { collectSensitiveValues, isEnvPassthroughValueSafeToExpose, redactSensitiveText, redactSensitiveValue, } from "../../core/redaction.js";
|
|
44
46
|
import { withWorkflowRunsRepo, } from "../../storage/repositories/workflow-runs-repository.js";
|
|
45
47
|
import { assertRunParamsSatisfyPlan } from "../ir/params.js";
|
|
46
48
|
import { PROGRAM_RETRY_REASONS } from "../program/schema.js";
|
|
49
|
+
import { requireExecutableWorkflowPlan } from "../runtime/plan-classifier.js";
|
|
47
50
|
import { completeWorkflowStep, getNextWorkflowStep, snapshotRunForDriver, } from "../runtime/runs.js";
|
|
48
51
|
import { UNIT_STALE_MS } from "../runtime/unit-checkin.js";
|
|
49
52
|
import { buildLease, resolveRunId } from "./brief.js";
|
|
50
|
-
import {
|
|
53
|
+
import { frozenSummaryJudge } from "./frozen-judge.js";
|
|
54
|
+
import { activeGateLoop, cascadeSkippedRouter, computeStepWorkList, finalizeExecutedStep, isRetryEligibleFailure, recoverGateFeedback, reduceEmptyStep, reduceStepOutcomes, seedJournaledRouteDecisions, selectUnitAttemptRow, stepOutputsFromEvidence, unitOutcomeFromRow, unitStillNeedsReport, } from "./step-work.js";
|
|
51
55
|
/**
|
|
52
56
|
* Claim time-to-live. Equal to the unit-checkin stale window
|
|
53
57
|
* ({@link UNIT_STALE_MS}) so a claim expires at exactly the moment
|
|
@@ -66,14 +70,21 @@ const CLAIM_TTL_MS = UNIT_STALE_MS;
|
|
|
66
70
|
const FINALIZE_LOCK_TTL_MS = 90_000;
|
|
67
71
|
// ── Entry point ──────────────────────────────────────────────────────────────
|
|
68
72
|
export async function reportWorkflowUnit(input) {
|
|
73
|
+
const release = await acquireMaintenanceActivity("workflow-report");
|
|
74
|
+
try {
|
|
75
|
+
return await reportWorkflowUnitWithBarrier(input);
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
release();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async function reportWorkflowUnitWithBarrier(input) {
|
|
69
82
|
const nowFn = input.now ?? (() => new Date());
|
|
70
83
|
const nowIso = nowFn().toISOString();
|
|
71
84
|
const runId = await resolveRunId(input.target);
|
|
72
85
|
// #14: read the spine, run row, and unit journal in ONE snapshot so the guards
|
|
73
86
|
// below see a consistent point-in-time state (same fix as `brief`).
|
|
74
87
|
const { next, run: runRow, units } = await snapshotRunForDriver(runId);
|
|
75
|
-
const planJson = runRow.plan_json;
|
|
76
|
-
const planHash = runRow.plan_hash;
|
|
77
88
|
const leaseHolder = runRow.engine_lease_holder;
|
|
78
89
|
const leaseUntil = runRow.engine_lease_until;
|
|
79
90
|
// Refuse a non-active run: there is no work-list to report against.
|
|
@@ -100,7 +111,7 @@ export async function reportWorkflowUnit(input) {
|
|
|
100
111
|
`moved since you briefed it — a concurrent report/run/manual completion advanced the run. Re-run ` +
|
|
101
112
|
`\`akm workflow brief ${runId}\` and report against the current step.`, "INVALID_FLAG_VALUE");
|
|
102
113
|
}
|
|
103
|
-
const plan =
|
|
114
|
+
const plan = requireExecutableWorkflowPlan(runRow);
|
|
104
115
|
// Reviewer #12: the journaled params row must still satisfy the frozen param
|
|
105
116
|
// schemas before report resolves any unit prompt from it — a violation is
|
|
106
117
|
// post-start corruption, refused loudly (mirrors the frozen-plan hash check
|
|
@@ -140,14 +151,21 @@ export async function reportWorkflowUnit(input) {
|
|
|
140
151
|
throw new UsageError(`Unit "${input.unitId}" cannot be resolved (${workUnit.resolved.error}) — it has no dispatchable input to ` +
|
|
141
152
|
`report a result against. This is an authoring/data error in the workflow; fix it and start a new run.`);
|
|
142
153
|
}
|
|
154
|
+
if (!workUnit.engine || !workUnit.invocation) {
|
|
155
|
+
throw new UsageError(`Unit "${workUnit.unitId}" has no complete frozen engine attribution.`);
|
|
156
|
+
}
|
|
157
|
+
const engineName = workUnit.engine.name;
|
|
158
|
+
const exactModel = workUnit.invocation.model;
|
|
143
159
|
const inputHash = workUnit.resolved.inputHash;
|
|
144
160
|
const journalId = workUnit.journalBaseId;
|
|
161
|
+
const sensitiveValues = await collectReportedUnitSensitiveValues(workUnit);
|
|
162
|
+
const sessionId = input.sessionId === undefined ? undefined : redactSensitiveText(input.sessionId, sensitiveValues);
|
|
145
163
|
// ── running: claim / heartbeat, never advances the spine ───────────────────
|
|
146
164
|
if (input.status === "running") {
|
|
147
165
|
// The claim holder: the driver's --session-id, else a token we mint and
|
|
148
166
|
// return so the driver can reuse it (as --session-id) to heartbeat and
|
|
149
167
|
// finish the SAME claim. First unexpired claim wins.
|
|
150
|
-
const holder =
|
|
168
|
+
const holder = sessionId ?? `claim:${randomUUID()}`;
|
|
151
169
|
const claimExpiresAt = new Date(nowFn().getTime() + CLAIM_TTL_MS).toISOString();
|
|
152
170
|
const claimed = await withWorkflowRunsRepo((repo) => repo.transaction(() => {
|
|
153
171
|
const existing = repo.getUnit(runId, journalId);
|
|
@@ -175,7 +193,8 @@ export async function reportWorkflowUnit(input) {
|
|
|
175
193
|
parentUnitId: workUnit.isFanOut ? `${stepState.id}.map` : null,
|
|
176
194
|
phase: null,
|
|
177
195
|
runner: workUnit.runner,
|
|
178
|
-
|
|
196
|
+
engine: engineName,
|
|
197
|
+
model: exactModel,
|
|
179
198
|
inputHash,
|
|
180
199
|
startedAt: nowIso,
|
|
181
200
|
claimHolder: holder,
|
|
@@ -208,7 +227,7 @@ export async function reportWorkflowUnit(input) {
|
|
|
208
227
|
};
|
|
209
228
|
}
|
|
210
229
|
// ── completed / failed: validate, guard, write, maybe finalize ─────────────
|
|
211
|
-
const { resultJson, failureReason } = prepareResult(input, workUnit);
|
|
230
|
+
const { resultJson, failureReason } = prepareResult(input, workUnit, sensitiveValues);
|
|
212
231
|
const thisTokens = input.tokens ?? 0;
|
|
213
232
|
// Guarded write: the idempotent re-report / replay-divergence check and the
|
|
214
233
|
// budget ceiling are evaluated INSIDE the same SQLite transaction as the
|
|
@@ -216,7 +235,7 @@ export async function reportWorkflowUnit(input) {
|
|
|
216
235
|
// one budgeted step) serialize on the write lock — each sees the other's row
|
|
217
236
|
// and the row is always internally consistent.
|
|
218
237
|
const status = input.status;
|
|
219
|
-
const holder =
|
|
238
|
+
const holder = sessionId ?? null;
|
|
220
239
|
const writeResult = await withWorkflowRunsRepo((repo) => repo.transaction(() => {
|
|
221
240
|
const existing = repo.getUnit(runId, journalId);
|
|
222
241
|
if (existing?.status === "completed") {
|
|
@@ -279,7 +298,8 @@ export async function reportWorkflowUnit(input) {
|
|
|
279
298
|
parentUnitId: workUnit.isFanOut ? `${stepState.id}.map` : null,
|
|
280
299
|
phase: null,
|
|
281
300
|
runner: workUnit.runner,
|
|
282
|
-
|
|
301
|
+
engine: engineName,
|
|
302
|
+
model: exactModel,
|
|
283
303
|
inputHash,
|
|
284
304
|
startedAt: existing?.started_at ?? nowIso,
|
|
285
305
|
});
|
|
@@ -306,7 +326,7 @@ export async function reportWorkflowUnit(input) {
|
|
|
306
326
|
resultJson,
|
|
307
327
|
tokens: carriedTokens,
|
|
308
328
|
failureReason,
|
|
309
|
-
sessionId:
|
|
329
|
+
sessionId: sessionId ?? null,
|
|
310
330
|
finishedAt: nowIso,
|
|
311
331
|
});
|
|
312
332
|
// A `tokens-cross` verdict: this unit's OWN tokens push the run total over
|
|
@@ -381,7 +401,7 @@ export async function reportWorkflowUnit(input) {
|
|
|
381
401
|
byUnit,
|
|
382
402
|
gateLoop,
|
|
383
403
|
priorEvidence,
|
|
384
|
-
summaryJudge: input.summaryJudge,
|
|
404
|
+
summaryJudge: input.summaryJudge === undefined ? frozenSummaryJudge(plan, stepPlan.gate.judge) : input.summaryJudge,
|
|
385
405
|
now: nowFn,
|
|
386
406
|
written: { unitId: journalId, status },
|
|
387
407
|
recorded: "written",
|
|
@@ -444,7 +464,7 @@ export async function reportWorkflowUnit(input) {
|
|
|
444
464
|
byUnit,
|
|
445
465
|
gateLoop,
|
|
446
466
|
priorEvidence,
|
|
447
|
-
summaryJudge: input.summaryJudge,
|
|
467
|
+
summaryJudge: input.summaryJudge === undefined ? frozenSummaryJudge(plan, stepPlan.gate.judge) : input.summaryJudge,
|
|
448
468
|
now: nowFn,
|
|
449
469
|
written: { unitId: journalId, status },
|
|
450
470
|
recorded: idempotent ? "idempotent" : "written",
|
|
@@ -471,6 +491,15 @@ export async function reportWorkflowUnit(input) {
|
|
|
471
491
|
* `finalizeStep` uses) so two concurrent settles/reports cannot double-journal.
|
|
472
492
|
*/
|
|
473
493
|
export async function settleWorkflowSpine(input) {
|
|
494
|
+
const release = await acquireMaintenanceActivity("workflow-report-settle");
|
|
495
|
+
try {
|
|
496
|
+
return await settleWorkflowSpineWithBarrier(input);
|
|
497
|
+
}
|
|
498
|
+
finally {
|
|
499
|
+
release();
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
async function settleWorkflowSpineWithBarrier(input) {
|
|
474
503
|
const nowFn = input.now ?? (() => new Date());
|
|
475
504
|
const runId = await resolveRunId(input.target);
|
|
476
505
|
const { next, run: runRow, units } = await snapshotRunForDriver(runId);
|
|
@@ -491,7 +520,7 @@ export async function settleWorkflowSpine(input) {
|
|
|
491
520
|
throw new UsageError(`Workflow run ${runId} is now on step "${next.step.id}", not "${input.expectStep}" (--expect-step). The spine ` +
|
|
492
521
|
`moved since you briefed it — re-run \`akm workflow brief ${runId}\` and settle against the current step.`, "INVALID_FLAG_VALUE");
|
|
493
522
|
}
|
|
494
|
-
const plan =
|
|
523
|
+
const plan = requireExecutableWorkflowPlan(runRow);
|
|
495
524
|
assertRunParamsSatisfyPlan(runId, plan, next.run.params ?? {});
|
|
496
525
|
// A step with resolvable units is settled ONLY when its work-list is FULLY
|
|
497
526
|
// TERMINAL — every resolvable unit run to a terminal state, nothing left to
|
|
@@ -611,6 +640,7 @@ function buildStepContext(runId, plan, next, units) {
|
|
|
611
640
|
runId,
|
|
612
641
|
params: next.run.params ?? {},
|
|
613
642
|
stepOutputs,
|
|
643
|
+
engines: plan.execution.engines,
|
|
614
644
|
gateLoop,
|
|
615
645
|
...(gateFeedback ? { gateFeedback } : {}),
|
|
616
646
|
});
|
|
@@ -778,7 +808,7 @@ async function finalizeStep(args) {
|
|
|
778
808
|
workflowRef: next.run.workflowRef,
|
|
779
809
|
stepPlan,
|
|
780
810
|
stepId: stepState.id,
|
|
781
|
-
completionCriteria:
|
|
811
|
+
completionCriteria: stepPlan.gate.criteria,
|
|
782
812
|
gateLoop,
|
|
783
813
|
reduced,
|
|
784
814
|
priorEvidence: args.priorEvidence,
|
|
@@ -860,6 +890,7 @@ async function settleSpine(args) {
|
|
|
860
890
|
const sp = plan.steps.find((s) => s.stepId === step.id);
|
|
861
891
|
if (!sp)
|
|
862
892
|
break;
|
|
893
|
+
const stepJudge = summaryJudge === undefined ? frozenSummaryJudge(plan, sp.gate.judge) : summaryJudge;
|
|
863
894
|
// A route-skipped target: complete it as skipped, cascading if it is itself
|
|
864
895
|
// a router (identical to the engine loop's skip handling).
|
|
865
896
|
const skipInfo = routeUnselected.get(step.id);
|
|
@@ -883,7 +914,7 @@ async function settleSpine(args) {
|
|
|
883
914
|
workflowRef: state.run.workflowRef,
|
|
884
915
|
stepId: step.id,
|
|
885
916
|
stepPlan: sp,
|
|
886
|
-
completionCriteria:
|
|
917
|
+
completionCriteria: sp.gate.criteria,
|
|
887
918
|
gateLoop: 1,
|
|
888
919
|
loopsRemaining: false,
|
|
889
920
|
result: {
|
|
@@ -896,7 +927,7 @@ async function settleSpine(args) {
|
|
|
896
927
|
params: state.run.params ?? {},
|
|
897
928
|
routeSelected,
|
|
898
929
|
routeUnselected,
|
|
899
|
-
summaryJudge,
|
|
930
|
+
summaryJudge: stepJudge,
|
|
900
931
|
...leaseArg,
|
|
901
932
|
});
|
|
902
933
|
if (fin.kind !== "advanced")
|
|
@@ -919,6 +950,7 @@ async function settleSpine(args) {
|
|
|
919
950
|
runId,
|
|
920
951
|
params: state.run.params ?? {},
|
|
921
952
|
stepOutputs,
|
|
953
|
+
engines: plan.execution.engines,
|
|
922
954
|
gateLoop,
|
|
923
955
|
...(gateFeedback ? { gateFeedback } : {}),
|
|
924
956
|
});
|
|
@@ -946,14 +978,14 @@ async function settleSpine(args) {
|
|
|
946
978
|
workflowRef: state.run.workflowRef,
|
|
947
979
|
stepPlan: sp,
|
|
948
980
|
stepId: step.id,
|
|
949
|
-
completionCriteria:
|
|
981
|
+
completionCriteria: sp.gate.criteria,
|
|
950
982
|
gateLoop,
|
|
951
983
|
reduced,
|
|
952
984
|
priorEvidence,
|
|
953
985
|
params: state.run.params ?? {},
|
|
954
986
|
routeSelected,
|
|
955
987
|
routeUnselected,
|
|
956
|
-
summaryJudge,
|
|
988
|
+
summaryJudge: stepJudge,
|
|
957
989
|
...leaseArg,
|
|
958
990
|
});
|
|
959
991
|
if (completion.kind === "advanced") {
|
|
@@ -977,13 +1009,6 @@ async function settleSpine(args) {
|
|
|
977
1009
|
return getNextWorkflowStep(runId);
|
|
978
1010
|
}
|
|
979
1011
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
980
|
-
function loadFrozenPlan(runId, planJson, planHash) {
|
|
981
|
-
if (!planJson) {
|
|
982
|
-
throw new UsageError(`Workflow run ${runId} predates frozen plans (no plan_json on the run row) and cannot be driven by ` +
|
|
983
|
-
`\`akm workflow report\`. Use engine-driven mode: \`akm workflow run ${runId}\`.`);
|
|
984
|
-
}
|
|
985
|
-
return parseFrozenPlan(runId, planJson, planHash);
|
|
986
|
-
}
|
|
987
1012
|
/**
|
|
988
1013
|
* Normalize a driver-supplied `--failure-reason` to the persisted taxonomy (PR
|
|
989
1014
|
* #714 review round 2, #16). An external driver can type ANY string; storing it
|
|
@@ -1001,10 +1026,12 @@ function loadFrozenPlan(runId, planJson, planHash) {
|
|
|
1001
1026
|
* unknown external reason is recorded for observability without ever
|
|
1002
1027
|
* triggering retry.
|
|
1003
1028
|
*/
|
|
1004
|
-
export function normalizeFailureReason(raw) {
|
|
1029
|
+
export function normalizeFailureReason(raw, sensitiveValues = []) {
|
|
1005
1030
|
const trimmed = raw?.trim();
|
|
1006
1031
|
if (!trimmed)
|
|
1007
1032
|
return "reported_failure";
|
|
1033
|
+
if (redactSensitiveText(trimmed, sensitiveValues) !== trimmed)
|
|
1034
|
+
return "reported_failure";
|
|
1008
1035
|
if (PROGRAM_RETRY_REASONS.includes(trimmed))
|
|
1009
1036
|
return trimmed;
|
|
1010
1037
|
const slug = trimmed
|
|
@@ -1015,11 +1042,13 @@ export function normalizeFailureReason(raw) {
|
|
|
1015
1042
|
return `external:${slug || "unknown"}`;
|
|
1016
1043
|
}
|
|
1017
1044
|
/** Validate + shape the reported result into what `finishUnit` persists. */
|
|
1018
|
-
function prepareResult(input, workUnit) {
|
|
1045
|
+
function prepareResult(input, workUnit, sensitiveValues) {
|
|
1019
1046
|
if (input.status === "failed") {
|
|
1020
1047
|
return {
|
|
1021
|
-
resultJson: input.resultRaw !== undefined && input.resultRaw !== ""
|
|
1022
|
-
|
|
1048
|
+
resultJson: input.resultRaw !== undefined && input.resultRaw !== ""
|
|
1049
|
+
? JSON.stringify(redactSensitiveText(input.resultRaw, sensitiveValues))
|
|
1050
|
+
: null,
|
|
1051
|
+
failureReason: normalizeFailureReason(input.failureReason, sensitiveValues),
|
|
1023
1052
|
};
|
|
1024
1053
|
}
|
|
1025
1054
|
// completed
|
|
@@ -1034,13 +1063,13 @@ function prepareResult(input, workUnit) {
|
|
|
1034
1063
|
parsed = JSON.parse(raw);
|
|
1035
1064
|
}
|
|
1036
1065
|
catch (err) {
|
|
1037
|
-
throw new UsageError(`Unit "${input.unitId}" result is not valid JSON (its output schema requires a JSON value): ${err instanceof Error ? err.message : String(err)}`, "INVALID_FLAG_VALUE");
|
|
1066
|
+
throw new UsageError(`Unit "${input.unitId}" result is not valid JSON (its output schema requires a JSON value): ${redactSensitiveText(err instanceof Error ? err.message : String(err), sensitiveValues)}`, "INVALID_FLAG_VALUE");
|
|
1038
1067
|
}
|
|
1039
1068
|
const errors = validateJsonSchemaSubset(parsed, workUnit.schema);
|
|
1040
1069
|
if (errors.length > 0) {
|
|
1041
|
-
throw new UsageError(`Unit "${input.unitId}" result failed validation against its declared output schema: ${errors.join("; ")}.`, "INVALID_FLAG_VALUE");
|
|
1070
|
+
throw new UsageError(`Unit "${input.unitId}" result failed validation against its declared output schema: ${redactSensitiveText(errors.join("; "), sensitiveValues)}.`, "INVALID_FLAG_VALUE");
|
|
1042
1071
|
}
|
|
1043
|
-
return { resultJson: JSON.stringify(parsed), failureReason: null };
|
|
1072
|
+
return { resultJson: JSON.stringify(redactSensitiveValue(parsed, sensitiveValues)), failureReason: null };
|
|
1044
1073
|
}
|
|
1045
1074
|
// Free-text unit: journal the text as a JSON string EXACTLY as the executor
|
|
1046
1075
|
// does — `native-executor.ts` finishUnit uses `outcome.text ? JSON.stringify… :
|
|
@@ -1048,7 +1077,38 @@ function prepareResult(input, workUnit) {
|
|
|
1048
1077
|
// Matching that keeps the promoted artifact and the dispatch row byte-identical
|
|
1049
1078
|
// across the engine and report surfaces (the cardinal graph-parity rule), and
|
|
1050
1079
|
// stays consistent with the FAILED branch above which also maps ""→null.
|
|
1051
|
-
return {
|
|
1080
|
+
return {
|
|
1081
|
+
resultJson: input.resultRaw ? JSON.stringify(redactSensitiveText(input.resultRaw, sensitiveValues)) : null,
|
|
1082
|
+
failureReason: null,
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
async function collectReportedUnitSensitiveValues(workUnit) {
|
|
1086
|
+
const values = new Set();
|
|
1087
|
+
for (const ref of workUnit.env ?? []) {
|
|
1088
|
+
const { resolveEnvBinding } = await import("../../commands/env/env-binding.js");
|
|
1089
|
+
for (const value of Object.values(resolveEnvBinding(ref).values))
|
|
1090
|
+
values.add(value);
|
|
1091
|
+
}
|
|
1092
|
+
const collectEngine = (engine) => {
|
|
1093
|
+
if (!engine)
|
|
1094
|
+
return;
|
|
1095
|
+
if (engine.kind === "llm") {
|
|
1096
|
+
for (const name of engine.credential?.names ?? []) {
|
|
1097
|
+
const value = process.env[name]?.trim();
|
|
1098
|
+
if (value)
|
|
1099
|
+
values.add(value);
|
|
1100
|
+
}
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
for (const name of engine.envPassthrough) {
|
|
1104
|
+
const value = process.env[name];
|
|
1105
|
+
if (!isEnvPassthroughValueSafeToExpose(name, value) && value)
|
|
1106
|
+
values.add(value);
|
|
1107
|
+
}
|
|
1108
|
+
};
|
|
1109
|
+
collectEngine(workUnit.engine);
|
|
1110
|
+
collectEngine(workUnit.fallbackEngine);
|
|
1111
|
+
return collectSensitiveValues(values);
|
|
1052
1112
|
}
|
|
1053
1113
|
/**
|
|
1054
1114
|
* Assess the frozen plan's declared budget ceilings for ONE report, seeded from
|
|
@@ -62,20 +62,33 @@
|
|
|
62
62
|
*/
|
|
63
63
|
import { randomUUID } from "node:crypto";
|
|
64
64
|
import { UsageError } from "../../core/errors.js";
|
|
65
|
-
import {
|
|
65
|
+
import { withMaintenanceStartBarrierAsync } from "../../core/maintenance-barrier.js";
|
|
66
66
|
import { disposeDispatchResources } from "../../integrations/agent/runner-dispatch.js";
|
|
67
67
|
import { withWorkflowRunsRepo } from "../../storage/repositories/workflow-runs-repository.js";
|
|
68
68
|
import { assertRunParamsSatisfyPlan } from "../ir/params.js";
|
|
69
|
+
import { computePlanHash } from "../ir/plan-hash.js";
|
|
70
|
+
import { decodeWorkflowPlanV3 } from "../ir/schema.js";
|
|
71
|
+
import { requireExecutableWorkflowPlan } from "../runtime/plan-classifier.js";
|
|
69
72
|
import { completeWorkflowStep, getNextWorkflowStep } from "../runtime/runs.js";
|
|
70
|
-
import {
|
|
73
|
+
import { frozenSummaryJudge } from "./frozen-judge.js";
|
|
71
74
|
import { executeStepPlan } from "./native-executor.js";
|
|
72
75
|
// Shared step semantics — route evaluation + cascaded-skip bookkeeping,
|
|
73
76
|
// gate-evaluation journaling, and the whole step-completion path
|
|
74
77
|
// (`finalizeExecutedStep`) live in step-work.ts so the engine loop and the R3
|
|
75
78
|
// brief/report driver protocol share ONE implementation (no drift).
|
|
76
|
-
import { activeGateLoop, cascadeSkippedRouter, finalizeExecutedStep, GATE_EVALUATION_PHASE,
|
|
79
|
+
import { activeGateLoop, cascadeSkippedRouter, finalizeExecutedStep, GATE_EVALUATION_PHASE, recoverGateFeedback, seedJournaledRouteDecisions, } from "./step-work.js";
|
|
77
80
|
export async function runWorkflowSteps(options) {
|
|
78
81
|
const next = await getNextWorkflowStep(options.target, options.params);
|
|
82
|
+
// Version/canonical/hash validation precedes every executable mutation,
|
|
83
|
+
// including lease acquisition. Historical rows remain inspectable/abandonable.
|
|
84
|
+
if (!next.done && !options.loadPlan) {
|
|
85
|
+
await withWorkflowRunsRepo((repo) => {
|
|
86
|
+
const row = repo.getRunById(next.run.id);
|
|
87
|
+
if (!row)
|
|
88
|
+
throw new UsageError(`Workflow run ${next.run.id} was not found.`);
|
|
89
|
+
requireExecutableWorkflowPlan(row);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
79
92
|
// Refuse non-active runs BEFORE any dispatch — completeWorkflowStep would
|
|
80
93
|
// reject the completion anyway, but only after the units already ran (and
|
|
81
94
|
// cost money). Mirror its preflight up front.
|
|
@@ -128,7 +141,7 @@ export async function runWorkflowSteps(options) {
|
|
|
128
141
|
// hanging on the leaked handle. Runs even if lease release itself fails;
|
|
129
142
|
// a teardown-time repository error must not skip dispatch cleanup.
|
|
130
143
|
try {
|
|
131
|
-
(options.disposeDispatchResources ?? disposeDispatchResources)();
|
|
144
|
+
await (options.disposeDispatchResources ?? disposeDispatchResources)();
|
|
132
145
|
}
|
|
133
146
|
catch {
|
|
134
147
|
/* disposal is best-effort; never let cleanup mask the run outcome */
|
|
@@ -148,14 +161,14 @@ function leaseExpiry() {
|
|
|
148
161
|
* arbiter — two racing invocations cannot both win.
|
|
149
162
|
*/
|
|
150
163
|
async function acquireRunLease(runId, holder) {
|
|
151
|
-
await withWorkflowRunsRepo((repo) => {
|
|
164
|
+
await withMaintenanceStartBarrierAsync(() => withWorkflowRunsRepo((repo) => {
|
|
152
165
|
if (repo.acquireEngineLease(runId, holder, leaseExpiry(), new Date().toISOString()))
|
|
153
166
|
return;
|
|
154
167
|
const row = repo.getRunById(runId);
|
|
155
168
|
throw new UsageError(`Workflow run ${runId} is already being driven by engine ${row?.engine_lease_holder ?? "(unknown)"} ` +
|
|
156
169
|
`(run lease expires ${row?.engine_lease_until ?? "(unknown)"}). A second \`akm workflow run\` would race it — ` +
|
|
157
170
|
`wait for that invocation to finish or for the lease to expire.`);
|
|
158
|
-
});
|
|
171
|
+
}));
|
|
159
172
|
}
|
|
160
173
|
/**
|
|
161
174
|
* Renew the lease between steps. Losing the lease mid-run (it expired during
|
|
@@ -333,11 +346,14 @@ async function driveRun(options, initial, leaseHolder, heartbeat) {
|
|
|
333
346
|
const journaledDispatches = journaledUnits.filter((row) => row.phase !== GATE_EVALUATION_PHASE);
|
|
334
347
|
let unitsDispatched = journaledDispatches.reduce((sum, row) => sum + row.attempts, 0);
|
|
335
348
|
let tokensUsed = journaledDispatches.reduce((sum, row) => sum + (row.tokens ?? 0), 0);
|
|
336
|
-
//
|
|
337
|
-
//
|
|
338
|
-
const plan =
|
|
339
|
-
|
|
340
|
-
|
|
349
|
+
// The decoded/hash-verified row plan is the sole execution authority. The
|
|
350
|
+
// loader seam may assert an expected plan in tests, but can never replace it.
|
|
351
|
+
const plan = await loadFrozenPlan(next.run.id, next.run.workflowRef);
|
|
352
|
+
if (options.loadPlan) {
|
|
353
|
+
const expected = decodeWorkflowPlanV3(await options.loadPlan(next.run.workflowRef));
|
|
354
|
+
if (computePlanHash(expected) !== computePlanHash(plan))
|
|
355
|
+
throw new UsageError(`Injected workflow plan for run ${next.run.id} differs from its frozen plan.`);
|
|
356
|
+
}
|
|
341
357
|
// Reviewer #12: the journaled params row must still satisfy the frozen param
|
|
342
358
|
// schemas before the engine resolves any unit prompt from it. Applied on ALL
|
|
343
359
|
// THREE driver surfaces (engine here, brief, report) so schema-violating
|
|
@@ -475,13 +491,14 @@ async function driveRun(options, initial, leaseHolder, heartbeat) {
|
|
|
475
491
|
// Budget ceilings ride the FROZEN plan (addendum R2): a mid-run
|
|
476
492
|
// asset edit can never loosen or tighten a run's budget.
|
|
477
493
|
...(plan.budget ? { budget: plan.budget } : {}),
|
|
494
|
+
...(plan.execution ? { engines: plan.execution.engines } : {}),
|
|
478
495
|
gateLoop,
|
|
479
496
|
...(gateFeedback ? { gateFeedback } : {}),
|
|
480
497
|
// The heartbeat's signal is the effective dispatch signal: a lost
|
|
481
498
|
// lease (or a caller abort) aborts in-flight units promptly.
|
|
482
499
|
...(dispatchSignal ? { signal: dispatchSignal } : {}),
|
|
483
500
|
...(options.dispatcher ? { dispatcher: options.dispatcher } : {}),
|
|
484
|
-
|
|
501
|
+
maxConcurrency: Math.min(options.maxConcurrency ?? Number.POSITIVE_INFINITY, plan.execution?.maxConcurrency ?? 1),
|
|
485
502
|
});
|
|
486
503
|
// If the heartbeat lost the lease WHILE this step dispatched, another
|
|
487
504
|
// engine now owns the run — stop loudly BEFORE finalizing the step
|
|
@@ -510,7 +527,7 @@ async function driveRun(options, initial, leaseHolder, heartbeat) {
|
|
|
510
527
|
workflowRef: next.run.workflowRef,
|
|
511
528
|
stepId: step.id,
|
|
512
529
|
stepPlan,
|
|
513
|
-
completionCriteria:
|
|
530
|
+
completionCriteria: stepPlan.gate.criteria,
|
|
514
531
|
gateLoop,
|
|
515
532
|
loopsRemaining: gateLoop < maxLoops,
|
|
516
533
|
result,
|
|
@@ -518,7 +535,7 @@ async function driveRun(options, initial, leaseHolder, heartbeat) {
|
|
|
518
535
|
params: next.run.params ?? {},
|
|
519
536
|
routeSelected,
|
|
520
537
|
routeUnselected,
|
|
521
|
-
summaryJudge: options.summaryJudge,
|
|
538
|
+
summaryJudge: options.summaryJudge === undefined ? frozenSummaryJudge(plan, stepPlan.gate.judge) : options.summaryJudge,
|
|
522
539
|
...(options.requireGates ? { requireGates: true } : {}),
|
|
523
540
|
leaseHolder,
|
|
524
541
|
});
|
|
@@ -578,19 +595,15 @@ async function driveRun(options, initial, leaseHolder, heartbeat) {
|
|
|
578
595
|
* canonical JSON). A mismatch means the journaled plan was tampered with
|
|
579
596
|
* or corrupted — fail loudly, never silently recompile. The workflow
|
|
580
597
|
* asset file is NEVER touched on this path.
|
|
581
|
-
*
|
|
582
|
-
*
|
|
583
|
-
* preserving pre-006 behavior for in-flight legacy runs.
|
|
598
|
+
* Historical v2/null rows are inspection-only in the engine cutover. They are
|
|
599
|
+
* never recompiled from a mutable source asset.
|
|
584
600
|
*/
|
|
585
|
-
async function loadFrozenPlan(runId,
|
|
601
|
+
async function loadFrozenPlan(runId, _workflowRef) {
|
|
586
602
|
const row = await withWorkflowRunsRepo((repo) => {
|
|
587
603
|
const run = repo.getRunById(runId);
|
|
588
|
-
return run
|
|
604
|
+
return run;
|
|
589
605
|
});
|
|
590
|
-
if (row
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
warn(`Workflow run ${runId} predates frozen plans (no plan_json on the run row); ` +
|
|
594
|
-
`compiling the plan from the live asset ${workflowRef}. New runs freeze their plan at start.`);
|
|
595
|
-
return compileWorkflowAssetPlan(await loadWorkflowAsset(workflowRef));
|
|
606
|
+
if (!row)
|
|
607
|
+
throw new UsageError(`Workflow run ${runId} was not found.`);
|
|
608
|
+
return requireExecutableWorkflowPlan(row);
|
|
596
609
|
}
|
|
@@ -6,13 +6,12 @@
|
|
|
6
6
|
*
|
|
7
7
|
* A thin policy layer over `core/concurrent.ts` (`concurrentMap`) — the
|
|
8
8
|
* existing semaphore-bounded pool is generalized, not forked. The scheduler
|
|
9
|
-
* owns the
|
|
9
|
+
* owns the limits the plan requires:
|
|
10
10
|
*
|
|
11
|
-
* -
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* `min(16, cores − 2)` (the original Claude-Code-matching formula).
|
|
11
|
+
* - The effective width is the minimum of the map request, the frozen
|
|
12
|
+
* workflow cap, the selected LLM engine's frozen cap (when applicable),
|
|
13
|
+
* and the current host's CPU-derived safety cap. Reapplying host safety at
|
|
14
|
+
* dispatch matters when a frozen run resumes on a smaller machine.
|
|
16
15
|
* - Cooperative cancellation via AbortSignal (workers stop claiming items;
|
|
17
16
|
* the same signal is passed into each dispatch so in-flight units can be
|
|
18
17
|
* preempted too).
|
|
@@ -24,9 +23,10 @@
|
|
|
24
23
|
* units impossible to resume (peer review R1). Only work that really
|
|
25
24
|
* dispatches consumes the cap.
|
|
26
25
|
*/
|
|
27
|
-
import os from "node:os";
|
|
28
26
|
import { concurrentMap } from "../../core/concurrent.js";
|
|
29
|
-
import {
|
|
27
|
+
import { cpuDerivedUnitConcurrency, workflowMaxConcurrency } from "../concurrency-policy.js";
|
|
28
|
+
import { WORKFLOW_MAX_MAP_EXPANSION } from "../resource-limits.js";
|
|
29
|
+
export { clampMaxConcurrency, cpuDerivedUnitConcurrency, WORKFLOW_MAX_CONCURRENCY_CEILING, } from "../concurrency-policy.js";
|
|
30
30
|
/**
|
|
31
31
|
* Hard ceiling on an EXPLICIT `workflow.maxConcurrency`. A user value above
|
|
32
32
|
* this is clamped down (not rejected) so a config shared across machines with
|
|
@@ -34,32 +34,6 @@ import { loadConfig } from "../../core/config/config.js";
|
|
|
34
34
|
* 64 is deliberately far above any sane fan-out width — it exists only to keep
|
|
35
35
|
* a fat-fingered `100000` from spawning a runaway pool.
|
|
36
36
|
*/
|
|
37
|
-
export const WORKFLOW_MAX_CONCURRENCY_CEILING = 64;
|
|
38
|
-
/**
|
|
39
|
-
* CPU-derived engine cap used when `workflow.maxConcurrency` is unset — the
|
|
40
|
-
* original `min(16, cores−2)` formula (matching Claude Code), floored at 1.
|
|
41
|
-
*/
|
|
42
|
-
export function cpuDerivedUnitConcurrency(cpuCount = os.cpus()?.length ?? 4) {
|
|
43
|
-
return Math.min(16, Math.max(1, cpuCount - 2));
|
|
44
|
-
}
|
|
45
|
-
/** Clamp an explicit configured value into `[1, WORKFLOW_MAX_CONCURRENCY_CEILING]`. */
|
|
46
|
-
export function clampMaxConcurrency(value) {
|
|
47
|
-
return Math.min(WORKFLOW_MAX_CONCURRENCY_CEILING, Math.max(1, Math.floor(value)));
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Read `workflow.maxConcurrency` from config, fail-open to `undefined` (use the
|
|
51
|
-
* CPU default) on any load error or a non-numeric value. Kept side-effect-free
|
|
52
|
-
* and defensive: the scheduler must never fail a run because config is unwell.
|
|
53
|
-
*/
|
|
54
|
-
function configuredMaxConcurrency() {
|
|
55
|
-
try {
|
|
56
|
-
const value = loadConfig().workflow?.maxConcurrency;
|
|
57
|
-
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
58
|
-
}
|
|
59
|
-
catch {
|
|
60
|
-
return undefined;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
37
|
/**
|
|
64
38
|
* Engine-wide ceiling on concurrent units. Precedence:
|
|
65
39
|
* 1. An explicit `workflow.maxConcurrency` config value, clamped to
|
|
@@ -72,13 +46,11 @@ function configuredMaxConcurrency() {
|
|
|
72
46
|
* @param configured Explicit config value seam (defaults to reading config);
|
|
73
47
|
* pass `undefined` explicitly to force the CPU path in a test.
|
|
74
48
|
*/
|
|
75
|
-
export function maxUnitConcurrency(cpuCount
|
|
76
|
-
|
|
77
|
-
return clampMaxConcurrency(configured);
|
|
78
|
-
return cpuDerivedUnitConcurrency(cpuCount);
|
|
49
|
+
export function maxUnitConcurrency(cpuCount, configured) {
|
|
50
|
+
return workflowMaxConcurrency(configured, cpuCount);
|
|
79
51
|
}
|
|
80
52
|
/** Lifetime unit cap per run — a runaway-loop backstop, far above real use. */
|
|
81
|
-
export const LIFETIME_UNIT_CAP =
|
|
53
|
+
export const LIFETIME_UNIT_CAP = WORKFLOW_MAX_MAP_EXPANSION;
|
|
82
54
|
export class UnitCapExceededError extends Error {
|
|
83
55
|
constructor(cap) {
|
|
84
56
|
super(`workflow run exceeded the lifetime unit cap (${cap}). Aborting dispatch — check for a runaway fan-out.`);
|
|
@@ -94,7 +66,6 @@ export class UnitCapExceededError extends Error {
|
|
|
94
66
|
* resume stay free.
|
|
95
67
|
*/
|
|
96
68
|
export async function scheduleUnits(items, dispatch, options = {}) {
|
|
97
|
-
const
|
|
98
|
-
const concurrency = Math.max(1, Math.min(options.concurrency ?? 1, cap));
|
|
69
|
+
const concurrency = Math.max(1, Math.min(options.concurrency ?? 1, options.maxConcurrency ?? Number.POSITIVE_INFINITY, options.llmConcurrency ?? Number.POSITIVE_INFINITY, options.hostConcurrency ?? cpuDerivedUnitConcurrency()));
|
|
99
70
|
return concurrentMap(items, dispatch, concurrency, { signal: options.signal });
|
|
100
71
|
}
|