akm-cli 0.9.5 → 0.9.6
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 +105 -0
- package/dist/commands/env/env-cli.js +1 -2
- package/dist/commands/env/secret-cli.js +1 -5
- package/dist/commands/feedback-cli.js +0 -4
- package/dist/commands/health/checks.js +0 -32
- package/dist/commands/health/surfaces.js +2 -2
- package/dist/commands/health.js +6 -15
- package/dist/commands/improve/autonomy-gate.js +1 -1
- package/dist/commands/improve/consolidate.js +25 -26
- package/dist/commands/improve/distill.js +2 -2
- package/dist/commands/improve/extract.js +8 -1
- package/dist/commands/improve/improve.js +3 -5
- package/dist/commands/improve/locks.js +13 -5
- package/dist/commands/improve/loop-stages.js +8 -9
- package/dist/commands/improve/memory/memory-contradiction-detect.js +1 -19
- package/dist/commands/improve/outcome-loop.js +0 -6
- package/dist/commands/improve/reflect.js +20 -9
- package/dist/commands/lint/index.js +4 -6
- package/dist/commands/proposal/validators/proposal-quality-validators.js +0 -7
- package/dist/commands/sources/bundle-config-ops.js +9 -4
- package/dist/commands/sources/info.js +14 -10
- package/dist/commands/tasks/tasks.js +12 -16
- package/dist/commands/workflow-cli.js +2 -2
- package/dist/core/common.js +61 -24
- package/dist/core/config/config-io.js +2 -2
- package/dist/core/config/config-sources.js +32 -2
- package/dist/core/config/config.js +1 -1
- package/dist/core/config/schema/engines.js +9 -7
- package/dist/core/config/schema/primitives.js +0 -5
- package/dist/core/config/schema/search.js +2 -1
- package/dist/core/file-lock.js +2 -1
- package/dist/core/maintenance-barrier.js +2 -14
- package/dist/core/paths.js +0 -3
- package/dist/core/redaction.js +2 -2
- package/dist/core/spawn-env.js +8 -12
- package/dist/core/state/migrations.js +1 -12
- package/dist/core/state-db.js +9 -27
- package/dist/core/write-source.js +12 -19
- package/dist/execution/directory-identity.js +36 -10
- package/dist/indexer/graph/graph-boost.js +0 -4
- package/dist/indexer/index-writer-lock.js +43 -24
- package/dist/indexer/index-written-assets.js +5 -6
- package/dist/indexer/indexer.js +2 -39
- package/dist/indexer/materialize-embeddings.js +85 -41
- package/dist/indexer/search/db-search.js +15 -48
- package/dist/indexer/search/ranking-contributors.js +0 -25
- package/dist/indexer/search/ranking.js +3 -13
- package/dist/integrations/agent/builder-shared.js +0 -25
- package/dist/integrations/agent/model-map.js +2 -60
- package/dist/integrations/harnesses/aider/agent-builder.js +1 -3
- package/dist/integrations/harnesses/amazonq/agent-builder.js +2 -7
- package/dist/integrations/harnesses/claude/agent-builder.js +1 -4
- package/dist/integrations/harnesses/codex/agent-builder.js +1 -4
- package/dist/integrations/harnesses/copilot/agent-builder.js +2 -6
- package/dist/integrations/harnesses/gemini/agent-builder.js +2 -7
- package/dist/integrations/harnesses/opencode/agent-builder.js +1 -4
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +2 -16
- package/dist/integrations/harnesses/openhands/agent-builder.js +1 -3
- package/dist/integrations/harnesses/pi/agent-builder.js +2 -4
- package/dist/integrations/session-logs/index.js +0 -9
- package/dist/llm/client.js +75 -42
- package/dist/llm/embedder.js +7 -3
- package/dist/llm/embedders/remote.js +141 -42
- package/dist/registry/network.js +5 -37
- package/dist/runtime.js +2 -10
- package/dist/scripts/akm-migrate-node.js +60 -126
- package/dist/scripts/akm-migrate.js +60 -126
- package/dist/setup/engine-config.js +2 -5
- package/dist/setup/registry-stash-loader.js +0 -8
- package/dist/setup/setup.js +9 -46
- package/dist/setup/steps/connection-shared.js +10 -13
- package/dist/sources/providers/git-install.js +1 -1
- package/dist/storage/engines/sqlite-migrations.js +20 -1
- package/dist/storage/repositories/index-entries-repository.js +0 -15
- package/dist/tasks/backends/launchd.js +15 -20
- package/dist/tasks/backends/schtasks.js +18 -8
- package/dist/tasks/run/run-native-task.js +8 -6
- package/dist/tasks/source/bounded-document.js +2 -4
- package/dist/tasks/source/task-source-v3-frozen.js +5 -7
- package/dist/tasks/source/task-source-v4.js +5 -10
- package/dist/tasks/source/task-to-v3.js +5 -10
- package/dist/tasks/source/task-to-v4.js +1 -4
- package/dist/tasks/source-v3.js +6 -6
- package/dist/workflows/exec/native-executor.js +21 -31
- package/dist/workflows/exec/run-workflow.js +5 -6
- package/dist/workflows/exec/scheduler.js +3 -19
- package/dist/workflows/exec/step-work.js +1 -4
- package/dist/workflows/exec/unit-dispatch.js +2 -2
- package/dist/workflows/exec/worktree.js +1 -13
- package/dist/workflows/freeze/targets/child-workflow.js +2 -10
- package/dist/workflows/ir/plan-hash.js +4 -6
- package/dist/workflows/ir/schema-v4.js +0 -12
- package/dist/workflows/ir/schema.js +20 -31
- package/dist/workflows/parser.js +11 -52
- package/dist/workflows/renderer.js +2 -3
- package/dist/workflows/resource-limits.js +11 -41
- package/dist/workflows/runtime/runs.js +3 -4
- package/dist/workflows/source-ir/schema.js +14 -30
- package/dist/workflows/validator.js +1 -7
- package/docs/reference/configuration.md +1 -1
- package/package.json +1 -1
- package/schemas/akm-config.json +4 -23
- package/schemas/akm-task.json +1 -2
- package/schemas/akm-workflow.json +1 -13
- package/dist/indexer/search/semantic-status.js +0 -142
|
@@ -115,7 +115,7 @@ import { appendEvent } from "../../core/events.js";
|
|
|
115
115
|
import { validateJsonSchemaSubset } from "../../core/json-schema.js";
|
|
116
116
|
import { runStructured } from "../../core/structured.js";
|
|
117
117
|
import { warn } from "../../core/warn.js";
|
|
118
|
-
import {
|
|
118
|
+
import { assertFrozenDirectoryContained } from "../../execution/directory-identity.js";
|
|
119
119
|
import { assertFrozenExecutableIdentity } from "../../execution/executable-identity.js";
|
|
120
120
|
import { withWorkflowRunsConnection, withWorkflowRunsRepo, } from "../../storage/repositories/workflow-runs-repository.js";
|
|
121
121
|
import { materializeFrozenWorkflowEnvironment } from "../ir/environment-v4.js";
|
|
@@ -132,7 +132,7 @@ import { collectWorkflowDispatchSensitiveValues, redactUnitOutcome } from "./dis
|
|
|
132
132
|
// and the process-outcome → failure-reason mapping.
|
|
133
133
|
import { runExecUnit } from "./exec-unit.js";
|
|
134
134
|
import { mergeLoweringNotices } from "./lowering-notices.js";
|
|
135
|
-
import {
|
|
135
|
+
import { scheduleUnits } from "./scheduler.js";
|
|
136
136
|
// Shared step semantics — the ONE implementation consumed by the engine
|
|
137
137
|
// (this module + run-workflow.ts) on both the fresh-execution and the resume
|
|
138
138
|
// path. This module dispatches; step-work.ts owns the pure decisions.
|
|
@@ -142,13 +142,13 @@ import { cleanupFrozenScript, frozenScriptCommand, materializeFrozenScript } fro
|
|
|
142
142
|
import { enqueueUnitWrite } from "./unit-writer.js";
|
|
143
143
|
import { assertGitWorkTree, cleanupUnitWorktree, createUnitWorktree } from "./worktree.js";
|
|
144
144
|
/**
|
|
145
|
-
* Mutable per-step dispatch budget: the
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
145
|
+
* Mutable per-step dispatch budget: the declared run-level budget ceilings
|
|
146
|
+
* (`budget.max_units` / `budget.max_tokens`, addendum R2). Consumed once per
|
|
147
|
+
* journaled dispatch attempt (including retries); durable-row reuses never
|
|
148
|
+
* touch it — the peer-review fix that keeps large partially-completed
|
|
149
|
+
* fan-outs resumable instead of tripping a pre-batch check on
|
|
150
|
+
* `journaled + items`. Token usage accumulates per actual dispatch on top of
|
|
151
|
+
* the journal-seeded run total (reused rows' tokens are already in the
|
|
152
152
|
* seed). Check-and-increment is synchronous, so concurrent units cannot race
|
|
153
153
|
* it; crossing a declared ceiling fires `onExceeded` ONCE (the executor's
|
|
154
154
|
* chained AbortController), aborting pending and in-flight dispatches.
|
|
@@ -157,8 +157,6 @@ class DispatchBudget {
|
|
|
157
157
|
used;
|
|
158
158
|
/** Run-total tokens: journal-seeded input + this step's dispatch usage. */
|
|
159
159
|
tokens;
|
|
160
|
-
/** Set (once) when a dispatch was refused; the step fails with this message. */
|
|
161
|
-
capMessage;
|
|
162
160
|
/** Set (once) when a declared budget ceiling was hit; the step fails hard with it. */
|
|
163
161
|
budgetMessage;
|
|
164
162
|
maxUnits;
|
|
@@ -185,10 +183,6 @@ class DispatchBudget {
|
|
|
185
183
|
`against the workflow's declared budget.max_tokens of ${this.maxTokens} — refusing further dispatch.`);
|
|
186
184
|
return false;
|
|
187
185
|
}
|
|
188
|
-
if (this.used >= LIFETIME_UNIT_CAP) {
|
|
189
|
-
this.capMessage ??= new UnitCapExceededError(LIFETIME_UNIT_CAP).message;
|
|
190
|
-
return false;
|
|
191
|
-
}
|
|
192
186
|
this.used++;
|
|
193
187
|
return true;
|
|
194
188
|
}
|
|
@@ -343,7 +337,7 @@ async function prepareStepDispatchPrerequisites(input) {
|
|
|
343
337
|
const target = workUnits[0]?.frozenTarget;
|
|
344
338
|
const frozenCwd = target && "cwdIdentity" in target ? target.cwdIdentity : undefined;
|
|
345
339
|
if (frozenCwd)
|
|
346
|
-
|
|
340
|
+
assertFrozenDirectoryContained(frozenCwd);
|
|
347
341
|
const base = frozenCwd?.realRoot ?? ctx.workDir ?? process.cwd();
|
|
348
342
|
const preflightWorktree = ctx.preflightWorktree ?? assertGitWorkTree;
|
|
349
343
|
const gitError = preflightWorktree(base);
|
|
@@ -464,15 +458,12 @@ async function executeStepPlanInConnection(plan, ctx) {
|
|
|
464
458
|
// journal-write failures must not erase notices already observed from real
|
|
465
459
|
// dispatches; durable row reuses naturally contribute none.
|
|
466
460
|
const notices = mergeLoweringNotices(...outcomes.map((outcome) => outcome?.notices));
|
|
467
|
-
//
|
|
468
|
-
//
|
|
469
|
-
//
|
|
461
|
+
// A declared budget ceiling is a hard backstop: a step that hit one FAILS
|
|
462
|
+
// regardless of on_error policy (a capped run must never quietly pass its
|
|
463
|
+
// gate). The budget message names WHICH ceiling tripped.
|
|
470
464
|
if (budget.budgetMessage) {
|
|
471
465
|
return { ...failedStep(budget.used, budget.budgetMessage, notices), tokensUsed: budget.tokens };
|
|
472
466
|
}
|
|
473
|
-
if (budget.capMessage) {
|
|
474
|
-
return { ...failedStep(budget.used, budget.capMessage, notices), tokensUsed: budget.tokens };
|
|
475
|
-
}
|
|
476
467
|
const units = outcomes.map((outcome, index) => outcome ?? {
|
|
477
468
|
unitId: workUnits[index].unitId,
|
|
478
469
|
ok: false,
|
|
@@ -597,17 +588,16 @@ async function runUnit(input) {
|
|
|
597
588
|
});
|
|
598
589
|
}
|
|
599
590
|
const attemptId = attemptIdFor(attempt);
|
|
600
|
-
//
|
|
601
|
-
//
|
|
602
|
-
//
|
|
603
|
-
//
|
|
591
|
+
// Declared budget ceilings, consumed per ACTUAL dispatch (reuses above
|
|
592
|
+
// returned before reaching here). Refusal fails this unit without
|
|
593
|
+
// journaling a row — nothing was dispatched — and the sticky
|
|
594
|
+
// budgetMessage fails the step.
|
|
604
595
|
if (!input.budget.tryConsume()) {
|
|
605
|
-
const budgetHit = input.budget.budgetMessage !== undefined;
|
|
606
596
|
return (outcome ?? {
|
|
607
597
|
unitId,
|
|
608
598
|
ok: false,
|
|
609
|
-
failureReason:
|
|
610
|
-
error: input.budget.budgetMessage ??
|
|
599
|
+
failureReason: "budget_exceeded",
|
|
600
|
+
error: input.budget.budgetMessage ?? "budget exceeded",
|
|
611
601
|
});
|
|
612
602
|
}
|
|
613
603
|
outcome = await dispatchJournaledAttempt({
|
|
@@ -1055,7 +1045,7 @@ async function dispatchUnit(request, dispatcher) {
|
|
|
1055
1045
|
export const defaultUnitDispatcher = async (request, feedback) => {
|
|
1056
1046
|
const frozenTarget = request.frozenTarget;
|
|
1057
1047
|
if (frozenTarget.kind === "script") {
|
|
1058
|
-
|
|
1048
|
+
assertFrozenDirectoryContained(frozenTarget.cwdIdentity);
|
|
1059
1049
|
if (frozenTarget.executable) {
|
|
1060
1050
|
assertFrozenExecutableIdentity(frozenTarget.executable, `unit ${request.unitId} executable`);
|
|
1061
1051
|
}
|
|
@@ -1099,7 +1089,7 @@ export const defaultUnitDispatcher = async (request, feedback) => {
|
|
|
1099
1089
|
}
|
|
1100
1090
|
if (frozenTarget.kind === "shell") {
|
|
1101
1091
|
if (frozenTarget.cwdIdentity)
|
|
1102
|
-
|
|
1092
|
+
assertFrozenDirectoryContained(frozenTarget.cwdIdentity);
|
|
1103
1093
|
if (frozenTarget.executable) {
|
|
1104
1094
|
assertFrozenExecutableIdentity(frozenTarget.executable, `unit ${request.unitId} executable`);
|
|
1105
1095
|
}
|
|
@@ -332,12 +332,11 @@ function workflowSummaryJudge(options, stepPlan, signal, owner) {
|
|
|
332
332
|
return frozenSummaryJudge(stepPlan.gate.frozenJudge, signal, options.dispatcher ?? defaultUnitDispatcher, owner, options.eventSource);
|
|
333
333
|
}
|
|
334
334
|
/**
|
|
335
|
-
* Seed the
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
* `budget.
|
|
340
|
-
* `budget.max_tokens`. Durable result reuse is free.
|
|
335
|
+
* Seed the declared budget ceilings from the journal so they are truly
|
|
336
|
+
* per-RUN: a resumed or re-invoked run must not restart a declared `budget`
|
|
337
|
+
* at zero. The append-only attempt journal is authoritative: dispatch
|
|
338
|
+
* attempts count against `budget.max_units`, and their known tokens count
|
|
339
|
+
* against `budget.max_tokens`. Durable result reuse is free.
|
|
341
340
|
*
|
|
342
341
|
* Gate-evaluation rows (`phase = "gate"`, journaled by the completion-gate
|
|
343
342
|
* judge) are EXCLUDED from the seed: the live path never consumes
|
|
@@ -15,17 +15,9 @@
|
|
|
15
15
|
* - Cooperative cancellation via AbortSignal (workers stop claiming items;
|
|
16
16
|
* the same signal is passed into each dispatch so in-flight units can be
|
|
17
17
|
* preempted too).
|
|
18
|
-
*
|
|
19
|
-
* The lifetime unit cap ({@link LIFETIME_UNIT_CAP}) is DECLARED here but
|
|
20
|
-
* enforced per actual dispatch by the native executor: a pre-batch check
|
|
21
|
-
* (`journaled + items.length`) counted durable-row REUSES as new dispatches,
|
|
22
|
-
* which made any partially-completed fan-out with more than ~cap/2 journaled
|
|
23
|
-
* units impossible to resume (peer review R1). Only work that really
|
|
24
|
-
* dispatches consumes the cap.
|
|
25
18
|
*/
|
|
26
19
|
import { concurrentMap } from "../../core/concurrent.js";
|
|
27
20
|
import { cpuDerivedUnitConcurrency, workflowMaxConcurrency } from "../concurrency-policy.js";
|
|
28
|
-
import { WORKFLOW_MAX_MAP_EXPANSION } from "../resource-limits.js";
|
|
29
21
|
export { clampMaxConcurrency, cpuDerivedUnitConcurrency, WORKFLOW_MAX_CONCURRENCY_CEILING, } from "../concurrency-policy.js";
|
|
30
22
|
/**
|
|
31
23
|
* Hard ceiling on an EXPLICIT `workflow.maxConcurrency`. A user value above
|
|
@@ -49,21 +41,13 @@ export { clampMaxConcurrency, cpuDerivedUnitConcurrency, WORKFLOW_MAX_CONCURRENC
|
|
|
49
41
|
export function maxUnitConcurrency(cpuCount, configured) {
|
|
50
42
|
return workflowMaxConcurrency(configured, cpuCount);
|
|
51
43
|
}
|
|
52
|
-
/** Lifetime unit cap per run — a runaway-loop backstop, far above real use. */
|
|
53
|
-
export const LIFETIME_UNIT_CAP = WORKFLOW_MAX_MAP_EXPANSION;
|
|
54
|
-
export class UnitCapExceededError extends Error {
|
|
55
|
-
constructor(cap) {
|
|
56
|
-
super(`workflow run exceeded the lifetime unit cap (${cap}). Aborting dispatch — check for a runaway fan-out.`);
|
|
57
|
-
this.name = "UnitCapExceededError";
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
44
|
/**
|
|
61
45
|
* Run `dispatch` over `items` under the engine concurrency caps. Individual
|
|
62
46
|
* failures do not cancel siblings (allSettled semantics from `concurrentMap`);
|
|
63
47
|
* a slot whose dispatch threw or that was never claimed after an abort stays
|
|
64
|
-
* `undefined` in the result array.
|
|
65
|
-
* — the executor consumes it per actual dispatch, so
|
|
66
|
-
* resume stay free.
|
|
48
|
+
* `undefined` in the result array. A declared `budget.max_units` ceiling is
|
|
49
|
+
* NOT checked here — the executor consumes it per actual dispatch, so
|
|
50
|
+
* durable-row reuses on resume stay free.
|
|
67
51
|
*/
|
|
68
52
|
export async function scheduleUnits(items, dispatch, options = {}) {
|
|
69
53
|
const concurrency = Math.max(1, Math.min(options.concurrency ?? 1, options.maxConcurrency ?? Number.POSITIVE_INFINITY, options.llmConcurrency ?? Number.POSITIVE_INFINITY, options.hostConcurrency ?? cpuDerivedUnitConcurrency()));
|
|
@@ -22,7 +22,7 @@ import { canonicalInputJson, validateInputs } from "../../execution/input-contra
|
|
|
22
22
|
import { withWorkflowRunsRepo, } from "../../storage/repositories/workflow-runs-repository.js";
|
|
23
23
|
import { canonicalJson as canonicalJsonString } from "../ir/plan-hash.js";
|
|
24
24
|
import { parseReference, resolveReferenceString, } from "../program/expressions.js";
|
|
25
|
-
import { clip,
|
|
25
|
+
import { clip, WORKFLOW_UNIT_DIAGNOSTIC_CLIP } from "../resource-limits.js";
|
|
26
26
|
import { completeWorkflowStep, isTruncatedEvidence, } from "../runtime/runs.js";
|
|
27
27
|
import { GATE_EVALUATION_PHASE } from "../runtime/unit-phases.js";
|
|
28
28
|
import { parseJudgeVerdict } from "../validate-summary.js";
|
|
@@ -60,9 +60,6 @@ const GATE_ARTIFACT_CLIP = 4_000;
|
|
|
60
60
|
* deterministically, before dispatch.
|
|
61
61
|
*/
|
|
62
62
|
function validateFanOutItems(stepId, items) {
|
|
63
|
-
if (items.length > WORKFLOW_MAX_MAP_EXPANSION) {
|
|
64
|
-
return `Step "${stepId}" fan-out expands to ${items.length} units, exceeding the ${WORKFLOW_MAX_MAP_EXPANSION}-unit resource limit.`;
|
|
65
|
-
}
|
|
66
63
|
const nullIndex = items.findIndex((item) => item === null || item === undefined);
|
|
67
64
|
if (nullIndex !== -1) {
|
|
68
65
|
return (`Step "${stepId}" fan-out list contains a null item (index ${nullIndex}). ` +
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
4
|
import { ConfigError } from "../../core/errors.js";
|
|
5
|
-
import {
|
|
5
|
+
import { assertFrozenDirectoryContained } from "../../execution/directory-identity.js";
|
|
6
6
|
import { assertFrozenExecutableIdentity } from "../../execution/executable-identity.js";
|
|
7
7
|
import { canonicalResolvedExecutionRequest, decodeResolvedExecutionRequest, } from "../../execution/resolved-request.js";
|
|
8
8
|
import { dispatchLoweredExecutionRequest, lowerResolvedExecutionRequestWithRunner, } from "../../integrations/agent/execution-lowering.js";
|
|
@@ -11,7 +11,7 @@ import { getHarness } from "../../integrations/harnesses/index.js";
|
|
|
11
11
|
export function prepareWorkflowExecution(request, prompt = request.prompt) {
|
|
12
12
|
const target = request.frozenTarget;
|
|
13
13
|
if (target.cwdIdentity)
|
|
14
|
-
|
|
14
|
+
assertFrozenDirectoryContained(target.cwdIdentity);
|
|
15
15
|
if (target.executable)
|
|
16
16
|
assertFrozenExecutableIdentity(target.executable, `unit ${request.unitId} executable`);
|
|
17
17
|
const wire = JSON.parse(canonicalResolvedExecutionRequest(target.request));
|
|
@@ -67,7 +67,7 @@ import { spawnSync } from "node:child_process";
|
|
|
67
67
|
import fsp from "node:fs/promises";
|
|
68
68
|
import os from "node:os";
|
|
69
69
|
import path from "node:path";
|
|
70
|
-
import { isWithinAsync, safeRealpathAsync } from "../../core/common.js";
|
|
70
|
+
import { isProcessAlive, isWithinAsync, safeRealpathAsync } from "../../core/common.js";
|
|
71
71
|
import { serializeByKey } from "../../core/concurrent.js";
|
|
72
72
|
import { runManagedSubprocess } from "../../core/subprocess.js";
|
|
73
73
|
import { warn } from "../../core/warn.js";
|
|
@@ -417,18 +417,6 @@ async function isWorktreeLeaseLive(candidate) {
|
|
|
417
417
|
return false;
|
|
418
418
|
return isProcessAlive(lease.pid);
|
|
419
419
|
}
|
|
420
|
-
/** `kill(pid, 0)` liveness probe. EPERM proves the process exists but is not ours. */
|
|
421
|
-
function isProcessAlive(pid) {
|
|
422
|
-
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0)
|
|
423
|
-
return false;
|
|
424
|
-
try {
|
|
425
|
-
process.kill(pid, 0);
|
|
426
|
-
return true;
|
|
427
|
-
}
|
|
428
|
-
catch (err) {
|
|
429
|
-
return err.code === "EPERM";
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
420
|
// ── Garbage collection ──────────────────────────────────────────────────────
|
|
433
421
|
/**
|
|
434
422
|
* Drop the run-scoped root once its last unit worktree is gone. `rmdir`
|
|
@@ -31,7 +31,7 @@ import { UsageError } from "../../../core/errors.js";
|
|
|
31
31
|
import { GuardedExecutionSourceCollector } from "../../../execution/guarded-source.js";
|
|
32
32
|
import { workflowParamContract } from "../../ir/params.js";
|
|
33
33
|
import { canonicalJson, canonicalPlanJson } from "../../ir/plan-hash.js";
|
|
34
|
-
import { utf8Bytes,
|
|
34
|
+
import { utf8Bytes, WORKFLOW_MAX_EMBEDDED_CHILD_PLAN_BYTES } from "../../resource-limits.js";
|
|
35
35
|
import { loadWorkflowAsset } from "../../runtime/workflow-asset-loader.js";
|
|
36
36
|
import { resolveOwnedAsset } from "../environment.js";
|
|
37
37
|
import { declaredParamNames, earlierStepIds } from "../step-values.js";
|
|
@@ -91,13 +91,6 @@ function assertNoCompositionCycle(stepId, childRef, refPath) {
|
|
|
91
91
|
`Path: ${compositionPath(refPath, childRef)}.`, "COMPOSITION_INVALID", "Break the cycle: remove or redirect one of the compositions in the path above so no workflow ends up " +
|
|
92
92
|
"composing itself, directly or through intermediates.");
|
|
93
93
|
}
|
|
94
|
-
function assertCompositionDepthAllowed(stepId, childRef, childDepth, refPath) {
|
|
95
|
-
if (childDepth <= WORKFLOW_MAX_COMPOSITION_DEPTH)
|
|
96
|
-
return;
|
|
97
|
-
throw new UsageError(`Workflow step ${stepId} cannot compose ${childRef}: workflow composition is limited to ` +
|
|
98
|
-
`${WORKFLOW_MAX_COMPOSITION_DEPTH} levels. Path: ${compositionPath(refPath, childRef)}.`, "COMPOSITION_INVALID", `Flatten the composition chain to ${WORKFLOW_MAX_COMPOSITION_DEPTH} levels or fewer — inline one of the ` +
|
|
99
|
-
"intermediate workflows, or restructure the chain so fewer child compositions are nested.");
|
|
100
|
-
}
|
|
101
94
|
/**
|
|
102
95
|
* Add the child's embedded plan bytes to the shared, tree-wide budget
|
|
103
96
|
* (A-N6) and fail before publication if the AGGREGATE crosses the cap.
|
|
@@ -126,10 +119,9 @@ export async function childWorkflowDispatch(input) {
|
|
|
126
119
|
// path to reach the child run and must reject, not vanish (see
|
|
127
120
|
// assertNoStepEnvironment's doc comment).
|
|
128
121
|
assertNoStepEnvironment(source.id, childRef, source);
|
|
129
|
-
// §4.2
|
|
122
|
+
// §4.2 step 2: the composition cycle check, before any child compilation.
|
|
130
123
|
assertNoCompositionCycle(source.id, childRef, context.composition.refPath);
|
|
131
124
|
const childDepth = context.composition.depth + 1;
|
|
132
|
-
assertCompositionDepthAllowed(source.id, childRef, childDepth, context.composition.refPath);
|
|
133
125
|
// §4.2 step 4: freeze the child COMPLETELY (compile -> validate -> freeze),
|
|
134
126
|
// with its OWN fresh collector (A-N7) so its plan is a pure function of its
|
|
135
127
|
// own source, and the SAME mutable budget object so the aggregate bound
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* Pure module: no IO beyond node:crypto, no engine imports.
|
|
14
14
|
*/
|
|
15
15
|
import { createHash } from "node:crypto";
|
|
16
|
-
import { utf8Bytes,
|
|
16
|
+
import { utf8Bytes, WORKFLOW_MAX_PLAN_BYTES } from "../resource-limits.js";
|
|
17
17
|
import { decodeWorkflowPlanV4, WORKFLOW_IR_V5_VERSION } from "./schema-v4.js";
|
|
18
18
|
/** sha256 hex of the canonical (recursively sorted-keys) JSON of the plan. */
|
|
19
19
|
export function computePlanHash(plan) {
|
|
@@ -53,9 +53,7 @@ export function decodeCanonicalPlan(runId, planJson, planHash, expectedVersion)
|
|
|
53
53
|
throw new Error(`Workflow run ${runId} has noncanonical frozen plan JSON.`);
|
|
54
54
|
return plan;
|
|
55
55
|
}
|
|
56
|
-
function sortKeys(value
|
|
57
|
-
if (depth > WORKFLOW_MAX_JSON_DEPTH)
|
|
58
|
-
throw new TypeError("JSON value exceeds maximum depth");
|
|
56
|
+
function sortKeys(value) {
|
|
59
57
|
if (value === null || typeof value === "string" || typeof value === "boolean")
|
|
60
58
|
return value;
|
|
61
59
|
if (typeof value === "number") {
|
|
@@ -64,11 +62,11 @@ function sortKeys(value, depth = 0) {
|
|
|
64
62
|
return value;
|
|
65
63
|
}
|
|
66
64
|
if (Array.isArray(value))
|
|
67
|
-
return value.map((item) => sortKeys(item
|
|
65
|
+
return value.map((item) => sortKeys(item));
|
|
68
66
|
if (value && typeof value === "object") {
|
|
69
67
|
return Object.fromEntries(Object.entries(value)
|
|
70
68
|
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
71
|
-
.map(([k, v]) => [k, sortKeys(v
|
|
69
|
+
.map(([k, v]) => [k, sortKeys(v)]));
|
|
72
70
|
}
|
|
73
71
|
throw new TypeError("JSON value contains a non-JSON value");
|
|
74
72
|
}
|
|
@@ -182,15 +182,6 @@ function decodeFrozenTarget(value, unit, environment, requiredSources, depth, bu
|
|
|
182
182
|
return decodeChildWorkflowTarget(target, unit, depth, budget);
|
|
183
183
|
fail(`unit ${unit.id} frozenTarget has unsupported kind ${String(target.kind)}`);
|
|
184
184
|
}
|
|
185
|
-
/**
|
|
186
|
-
* Composition-depth corruption bound (spec §3.6 step 6, §4.5, row A-23).
|
|
187
|
-
* Mirrors `WORKFLOW_MAX_COMPOSITION_DEPTH` (= 8), which Lane B adds to
|
|
188
|
-
* `src/workflows/resource-limits.ts` in a later commit as the freeze-time
|
|
189
|
-
* `COMPOSITION_INVALID` gate. Reproduced here — the decode-time corruption
|
|
190
|
-
* gate — rather than imported across the lane boundary, matching
|
|
191
|
-
* `tests/workflows/plan-v5-schema.test.ts`'s own header comment.
|
|
192
|
-
*/
|
|
193
|
-
const CHILD_WORKFLOW_DECODE_MAX_DEPTH = 8;
|
|
194
185
|
/**
|
|
195
186
|
* Decode a `kind: "child-workflow"` frozen target (§3.5): the embedded
|
|
196
187
|
* COMPLETE child plan, re-verified against its own `planHash` and this
|
|
@@ -241,9 +232,6 @@ function decodeChildWorkflowTarget(target, unit, depth, budget) {
|
|
|
241
232
|
fail(`unit ${unit.id} child workflow contentHash does not match its frozen dispatch`);
|
|
242
233
|
}
|
|
243
234
|
const childDepth = depth + 1;
|
|
244
|
-
if (childDepth > CHILD_WORKFLOW_DECODE_MAX_DEPTH) {
|
|
245
|
-
fail(`unit ${unit.id} child workflow ${target.ref} exceeds the max composition depth of ${CHILD_WORKFLOW_DECODE_MAX_DEPTH}`);
|
|
246
|
-
}
|
|
247
235
|
const frozenPlan = decodeWorkflowPlanV4(target.frozenPlan, {}, childDepth, budget);
|
|
248
236
|
const embeddedPlanJson = canonicalJsonLocal(frozenPlan);
|
|
249
237
|
const actualPlanHash = sha256(embeddedPlanJson);
|
|
@@ -5,12 +5,11 @@ import { isContainedRelativePath } from "../../core/common.js";
|
|
|
5
5
|
import { UsageError } from "../../core/errors.js";
|
|
6
6
|
import { parseReference } from "../program/expressions.js";
|
|
7
7
|
import { PROGRAM_PARAM_NAME_PATTERN, PROGRAM_RETRY_REASONS, PROGRAM_STEP_ID_PATTERN } from "../program/schema.js";
|
|
8
|
-
import { jsonBytes, utf8Bytes, WORKFLOW_ENV_VAR_NAME_PATTERN, WORKFLOW_MAX_CONCURRENCY,
|
|
8
|
+
import { jsonBytes, utf8Bytes, WORKFLOW_ENV_VAR_NAME_PATTERN, WORKFLOW_MAX_CONCURRENCY, WORKFLOW_MAX_INSTRUCTION_BYTES, WORKFLOW_MAX_PLAN_BYTES, WORKFLOW_MAX_SCHEMA_BYTES, WORKFLOW_MAX_TIMEOUT_MS, } from "../resource-limits.js";
|
|
9
9
|
// Shared dispatch-significant bounds now live in `../resource-limits` so the
|
|
10
10
|
// parser, the published JSON Schema, and this decoder enforce identical
|
|
11
11
|
// values. Re-exported here for existing importers (e.g. `commands/workflow-cli.ts`).
|
|
12
|
-
export { WORKFLOW_MAX_CONCURRENCY,
|
|
13
|
-
export const WORKFLOW_MAX_UNITS = WORKFLOW_MAX_MAP_EXPANSION;
|
|
12
|
+
export { WORKFLOW_MAX_CONCURRENCY, WORKFLOW_MAX_TIMEOUT_MS };
|
|
14
13
|
const MAX_LIST_ITEMS = 1024;
|
|
15
14
|
const MAX_STRING_LENGTH = 1_000_000;
|
|
16
15
|
/** Validate the current durable execution graph. */
|
|
@@ -33,8 +32,8 @@ export function validateWorkflowPlanStructure(input, options, hooks = {}) {
|
|
|
33
32
|
fail(`execution.maxConcurrency must be an integer from 1 through ${WORKFLOW_MAX_CONCURRENCY}`);
|
|
34
33
|
}
|
|
35
34
|
assertKeys(plan.execution, ["maxConcurrency"], "execution");
|
|
36
|
-
if (!Array.isArray(plan.steps) || plan.steps.length === 0
|
|
37
|
-
fail("steps must
|
|
35
|
+
if (!Array.isArray(plan.steps) || plan.steps.length === 0)
|
|
36
|
+
fail("steps must be a non-empty array");
|
|
38
37
|
const stepIds = new Set();
|
|
39
38
|
const nodeIds = new Set();
|
|
40
39
|
for (let index = 0; index < plan.steps.length; index++) {
|
|
@@ -126,7 +125,7 @@ function validateNode(node, stepId, nodeIds, unitExtraKeys) {
|
|
|
126
125
|
validateSchema(node.schema, `unit ${node.id} schema`);
|
|
127
126
|
validateRetry(node.retry, node.id);
|
|
128
127
|
validateStringArray(node.env, `unit ${node.id} env`, MAX_LIST_ITEMS, true);
|
|
129
|
-
validateStringArray(node.inputs, `unit ${node.id} inputs`,
|
|
128
|
+
validateStringArray(node.inputs, `unit ${node.id} inputs`, Infinity, true);
|
|
130
129
|
validateSource(node.source, `unit ${node.id} source`);
|
|
131
130
|
}
|
|
132
131
|
/**
|
|
@@ -143,15 +142,11 @@ function validateExecSpec(value, label) {
|
|
|
143
142
|
const command = value.command;
|
|
144
143
|
if (!Array.isArray(command) ||
|
|
145
144
|
command.length === 0 ||
|
|
146
|
-
command.length >
|
|
147
|
-
|
|
148
|
-
fail(`${label}.command must be an argv array of 1 through ${WORKFLOW_MAX_EXEC_ARGV} bounded non-empty strings`);
|
|
145
|
+
!command.every((arg) => typeof arg === "string" && arg.length > 0)) {
|
|
146
|
+
fail(`${label}.command must be an argv array of non-empty strings`);
|
|
149
147
|
}
|
|
150
148
|
if (value.cwd !== undefined) {
|
|
151
|
-
if (typeof value.cwd !== "string" ||
|
|
152
|
-
value.cwd.length === 0 ||
|
|
153
|
-
value.cwd.length > WORKFLOW_MAX_EXEC_CWD_LENGTH ||
|
|
154
|
-
!isContainedRelativePath(value.cwd)) {
|
|
149
|
+
if (typeof value.cwd !== "string" || value.cwd.length === 0 || !isContainedRelativePath(value.cwd)) {
|
|
155
150
|
fail(`${label}.cwd must be a relative path contained in the unit working directory`);
|
|
156
151
|
}
|
|
157
152
|
}
|
|
@@ -183,11 +178,10 @@ function validateExecEnvScope(value, label) {
|
|
|
183
178
|
const passEnv = value.passEnv;
|
|
184
179
|
if (!Array.isArray(passEnv) ||
|
|
185
180
|
passEnv.length === 0 ||
|
|
186
|
-
passEnv.length > WORKFLOW_MAX_EXEC_PASS_ENV ||
|
|
187
181
|
!passEnv.every((name) => typeof name === "string" && WORKFLOW_ENV_VAR_NAME_PATTERN.test(name)) ||
|
|
188
182
|
new Set(passEnv).size !== passEnv.length) {
|
|
189
|
-
fail(`${label}.passEnv must be
|
|
190
|
-
|
|
183
|
+
fail(`${label}.passEnv must be a non-empty list of distinct environment variable names matching ` +
|
|
184
|
+
`${WORKFLOW_ENV_VAR_NAME_PATTERN.source}`);
|
|
191
185
|
}
|
|
192
186
|
}
|
|
193
187
|
function validateGate(gate, stepId, nodeIds, gateExtraKeys) {
|
|
@@ -199,8 +193,7 @@ function validateGate(gate, stepId, nodeIds, gateExtraKeys) {
|
|
|
199
193
|
gate.criteria.length > MAX_LIST_ITEMS ||
|
|
200
194
|
!gate.criteria.every((x) => typeof x === "string" && x.length > 0 && x.length <= MAX_STRING_LENGTH) ||
|
|
201
195
|
!Number.isInteger(gate.maxLoops) ||
|
|
202
|
-
gate.maxLoops < 1
|
|
203
|
-
gate.maxLoops > WORKFLOW_MAX_GATE_LOOPS)
|
|
196
|
+
gate.maxLoops < 1)
|
|
204
197
|
fail(`gate for step ${stepId} is invalid`);
|
|
205
198
|
if (nodeIds.has(gate.id))
|
|
206
199
|
fail(`gate id ${gate.id} collides with a node`);
|
|
@@ -213,7 +206,6 @@ function validateRoute(route, stepId) {
|
|
|
213
206
|
!route.input ||
|
|
214
207
|
!isRecord(route.when) ||
|
|
215
208
|
Object.keys(route.when).length === 0 ||
|
|
216
|
-
Object.keys(route.when).length > WORKFLOW_MAX_ROUTE_BRANCHES ||
|
|
217
209
|
!Object.keys(route.when).every((match) => match.length > 0 && match.length <= MAX_STRING_LENGTH) ||
|
|
218
210
|
!Object.values(route.when).every((target) => typeof target === "string" && target))
|
|
219
211
|
fail(`route for step ${stepId} is invalid`);
|
|
@@ -226,9 +218,7 @@ function assertKeys(value, allowed, label) {
|
|
|
226
218
|
if (!allowed.includes(key))
|
|
227
219
|
fail(`${label} contains unknown key ${key}`);
|
|
228
220
|
}
|
|
229
|
-
function assertJson(value
|
|
230
|
-
if (depth > WORKFLOW_MAX_JSON_DEPTH)
|
|
231
|
-
fail("plan exceeds JSON depth limit of 64");
|
|
221
|
+
function assertJson(value) {
|
|
232
222
|
if (value === null || typeof value === "boolean")
|
|
233
223
|
return;
|
|
234
224
|
if (typeof value === "string") {
|
|
@@ -243,23 +233,23 @@ function assertJson(value, depth = 0) {
|
|
|
243
233
|
}
|
|
244
234
|
if (Array.isArray(value)) {
|
|
245
235
|
for (const item of value)
|
|
246
|
-
assertJson(item
|
|
236
|
+
assertJson(item);
|
|
247
237
|
return;
|
|
248
238
|
}
|
|
249
239
|
if (isRecord(value)) {
|
|
250
240
|
for (const item of Object.values(value))
|
|
251
|
-
assertJson(item
|
|
241
|
+
assertJson(item);
|
|
252
242
|
return;
|
|
253
243
|
}
|
|
254
244
|
fail("plan contains a non-JSON value");
|
|
255
245
|
}
|
|
256
246
|
function validateParams(params, schemas) {
|
|
257
|
-
validateStringArray(params, "params",
|
|
247
|
+
validateStringArray(params, "params", Infinity, true);
|
|
258
248
|
if (params?.some((name) => !PROGRAM_PARAM_NAME_PATTERN.test(name)))
|
|
259
249
|
fail("params contains an invalid name");
|
|
260
250
|
if (schemas !== undefined) {
|
|
261
|
-
if (!isRecord(schemas)
|
|
262
|
-
fail("paramSchemas must be
|
|
251
|
+
if (!isRecord(schemas))
|
|
252
|
+
fail("paramSchemas must be an object");
|
|
263
253
|
for (const [name, schema] of Object.entries(schemas)) {
|
|
264
254
|
if (!PROGRAM_PARAM_NAME_PATTERN.test(name))
|
|
265
255
|
fail(`paramSchemas.${name} is invalid`);
|
|
@@ -284,9 +274,8 @@ function validateBudget(budget) {
|
|
|
284
274
|
fail("budget must declare a ceiling");
|
|
285
275
|
validateOptionalPositiveInteger(budget.maxTokens, "budget.maxTokens");
|
|
286
276
|
const maxUnits = budget.maxUnits;
|
|
287
|
-
if (maxUnits !== undefined &&
|
|
288
|
-
(
|
|
289
|
-
fail(`budget.maxUnits must be an integer from 1 through ${WORKFLOW_MAX_UNITS}`);
|
|
277
|
+
if (maxUnits !== undefined && (!Number.isSafeInteger(maxUnits) || maxUnits < 1))
|
|
278
|
+
fail("budget.maxUnits must be a positive integer");
|
|
290
279
|
}
|
|
291
280
|
function validateRetry(retry, nodeId) {
|
|
292
281
|
if (retry === undefined)
|
|
@@ -294,7 +283,7 @@ function validateRetry(retry, nodeId) {
|
|
|
294
283
|
if (!isRecord(retry))
|
|
295
284
|
fail(`unit ${nodeId} retry must be an object`);
|
|
296
285
|
assertKeys(retry, ["max", "on"], `unit ${nodeId} retry`);
|
|
297
|
-
if (!Number.isSafeInteger(retry.max) || retry.max < 0
|
|
286
|
+
if (!Number.isSafeInteger(retry.max) || retry.max < 0)
|
|
298
287
|
fail(`unit ${nodeId} retry.max is invalid`);
|
|
299
288
|
validateStringArray(retry.on, `unit ${nodeId} retry.on`, PROGRAM_RETRY_REASONS.length, true);
|
|
300
289
|
if (retry.on === undefined ||
|