akm-cli 0.9.5 → 0.9.7
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 +295 -0
- package/dist/assets/hints/cli-hints-full.md +3 -3
- package/dist/assets/improve-strategies/catchup.json +40 -11
- package/dist/assets/improve-strategies/thorough.json +45 -7
- package/dist/assets/tasks/improve/akm-improve-frequent.yml +2 -2
- package/dist/commands/agent/contribute-cli.js +11 -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-cli.js +1 -1
- package/dist/commands/improve/improve-strategies.js +0 -4
- 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/memory/memory-improve.js +2 -1
- package/dist/commands/improve/outcome-loop.js +0 -6
- package/dist/commands/improve/preparation.js +1 -1
- package/dist/commands/improve/reflect.js +21 -10
- package/dist/commands/lint/base-linter.js +141 -18
- package/dist/commands/lint/index.js +21 -10
- package/dist/commands/proposal/validators/proposal-quality-validators.js +0 -7
- package/dist/commands/read/curate.js +47 -0
- package/dist/commands/read/search-cli.js +24 -1
- 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/asset/asset-placement.js +13 -2
- package/dist/core/asset/frontmatter.js +116 -0
- package/dist/core/asset/memory-archive.js +97 -0
- 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/engine-semantics.js +0 -2
- 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 +73 -133
- package/dist/scripts/akm-migrate.js +73 -133
- 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/sources/snapshot-fetchers/website-ingest.js +126 -0
- package/dist/storage/engines/sqlite-migrations.js +20 -1
- package/dist/storage/repositories/index-connection.js +45 -3
- package/dist/storage/repositories/index-entries-repository.js +0 -15
- package/dist/tasks/backends/cron.js +49 -9
- package/dist/tasks/backends/launchd.js +15 -20
- package/dist/tasks/backends/schtasks.js +18 -8
- package/dist/tasks/resolve-akm-bin.js +17 -2
- package/dist/tasks/run/run-native-task.js +8 -6
- package/dist/tasks/scheduler-invocation.js +8 -1
- package/dist/tasks/source/bounded-document.js +2 -4
- package/dist/tasks/source/parse-task-source.js +23 -9
- 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/cli.md +5 -1
- 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/assets/improve-strategies/frequent.json +0 -15
- package/dist/assets/improve-strategies/memory-focus.json +0 -15
- package/dist/indexer/search/semantic-status.js +0 -142
|
@@ -10,7 +10,11 @@
|
|
|
10
10
|
* Resolution order:
|
|
11
11
|
*
|
|
12
12
|
* 1. `process.execPath` alone for a Bun standalone executable.
|
|
13
|
-
* 2. Absolute Node plus the public `dist/akm` package launcher
|
|
13
|
+
* 2. Absolute Node plus the public `dist/akm` package launcher — eligible
|
|
14
|
+
* when it is the active npm global install, or when this process
|
|
15
|
+
* cannot write to the directory containing it (a read-only mount, e.g.
|
|
16
|
+
* an image-baked install, gives the same "won't change out from under
|
|
17
|
+
* the scheduler" guarantee npm-global ownership does).
|
|
14
18
|
* 3. Absolute runtime plus the source/build CLI entry, classified as a
|
|
15
19
|
* checkout that requires explicit `--rebind` for scheduler writes.
|
|
16
20
|
*
|
|
@@ -51,12 +55,13 @@ export function resolveAkmInvocation(options = {}) {
|
|
|
51
55
|
}
|
|
52
56
|
}
|
|
53
57
|
const npmGlobal = !checkout && packageBelongsToNpmGlobalRoot(launcherPath, npmGlobalRoot);
|
|
58
|
+
const readOnlyInstall = !checkout && !npmGlobal && !(options.isPathWritable ?? isPathWritable)(path.dirname(launcherPath));
|
|
54
59
|
const kind = checkout ? "checkout" : npmGlobal ? "npm" : "package-local";
|
|
55
60
|
return {
|
|
56
61
|
argv: [absoluteInvocationPath(nodePath), absoluteInvocationPath(launcherPath)],
|
|
57
62
|
via: kind,
|
|
58
63
|
kind,
|
|
59
|
-
eligible: npmGlobal,
|
|
64
|
+
eligible: npmGlobal || readOnlyInstall,
|
|
60
65
|
};
|
|
61
66
|
}
|
|
62
67
|
const checkoutEntry = resolveCheckoutEntry(options.cliEntryUrl ?? import.meta.url, runtime, mainPath);
|
|
@@ -117,6 +122,16 @@ function isCheckoutLauncher(file) {
|
|
|
117
122
|
const packageRoot = path.dirname(path.dirname(launcher));
|
|
118
123
|
return fs.existsSync(path.join(packageRoot, ".git"));
|
|
119
124
|
}
|
|
125
|
+
/** Whether this process can write to `dir` — false also covers a read-only mount. */
|
|
126
|
+
function isPathWritable(dir) {
|
|
127
|
+
try {
|
|
128
|
+
fs.accessSync(dir, fs.constants.W_OK);
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
120
135
|
function packageBelongsToNpmGlobalRoot(launcherPath, npmGlobalRoot) {
|
|
121
136
|
if (!npmGlobalRoot)
|
|
122
137
|
return false;
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
import path from "node:path";
|
|
21
21
|
import { assertNever } from "../../core/assert.js";
|
|
22
22
|
import { runManagedSubprocess, streamCaptureFailure } from "../../core/subprocess.js";
|
|
23
|
-
import {
|
|
23
|
+
import { assertFrozenDirectoryContained } from "../../execution/directory-identity.js";
|
|
24
24
|
import { cleanupFrozenScript, frozenScriptCommand, materializeFrozenScript } from "../frozen-script.js";
|
|
25
25
|
import { resolveAkmInvocation } from "../resolve-akm-bin.js";
|
|
26
26
|
import { finishAttempt } from "./attempt-lifecycle.js";
|
|
@@ -146,11 +146,13 @@ export async function runNativeTask(input) {
|
|
|
146
146
|
const dbLines = [{ line: header }];
|
|
147
147
|
let exitCode = null;
|
|
148
148
|
try {
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
|
|
149
|
+
// Re-resolve the authored root/cwd immediately before spawn so a
|
|
150
|
+
// symlink, ancestor, bundle-root, or directory/file swap cannot redirect
|
|
151
|
+
// execution outside that physical workspace. This is a live containment
|
|
152
|
+
// check, not an identity comparison — it does not care whether the
|
|
153
|
+
// underlying device/inode changed (e.g. a remount), only whether the
|
|
154
|
+
// resolved cwd still lives inside its root.
|
|
155
|
+
assertFrozenDirectoryContained(task.cwdIdentity);
|
|
154
156
|
if (task.kind === "script") {
|
|
155
157
|
materialized = materializeFrozenScript(task);
|
|
156
158
|
cmd = frozenScriptCommand(task, materialized.file);
|
|
@@ -189,7 +189,14 @@ export function parseScheduledBindingArgv(argv) {
|
|
|
189
189
|
...(publicInvocation.target !== undefined ? { target: publicInvocation.target } : {}),
|
|
190
190
|
};
|
|
191
191
|
}
|
|
192
|
-
|
|
192
|
+
/**
|
|
193
|
+
* Parse just the public `task run …` / `workflow run …` tail, with no
|
|
194
|
+
* `--scheduler-context` wrapper. Exported so a backend can recognize a
|
|
195
|
+
* pre-`--scheduler-context` invocation still sitting inside akm's own
|
|
196
|
+
* ownership-marked block (see `extractCronInvocation` in
|
|
197
|
+
* `src/tasks/backends/cron.ts`) without re-implementing this grammar.
|
|
198
|
+
*/
|
|
199
|
+
export function parsePublicSchedulerInvocation(invocation) {
|
|
193
200
|
if (invocation[0] === "task" && invocation[1] === "run" && invocation[2]) {
|
|
194
201
|
try {
|
|
195
202
|
if (normaliseTaskConceptId(invocation[2]) !== invocation[2])
|
|
@@ -53,13 +53,14 @@ import { EXECUTION_MAX_TIMEOUT_MS } from "../../execution/limits.js";
|
|
|
53
53
|
import { snapshotStrictRecord } from "../../execution/record.js";
|
|
54
54
|
import { WORKFLOW_ENV_VAR_NAME_PATTERN } from "../../workflows/resource-limits.js";
|
|
55
55
|
// ── Resource bounds (D2-N4: owned here, re-exported by source-v3.ts) ───────
|
|
56
|
-
export const TASK_V3_MAX_SOURCE_BYTES = 1024 * 1024;
|
|
57
56
|
export const TASK_V3_MAX_JSON_DEPTH = 64;
|
|
58
57
|
export const TASK_V3_MAX_JSON_NODES = 10_000;
|
|
59
58
|
export const TASK_V3_MAX_COLLECTION_ITEMS = 1024;
|
|
60
59
|
export const TASK_V3_MAX_OBJECT_KEYS = 256;
|
|
61
60
|
export const TASK_V3_MAX_STRING_BYTES = 256 * 1024;
|
|
62
61
|
export const TASK_V3_MAX_SCHEDULES = 64;
|
|
62
|
+
/** Max entries in a task's `redact:` env-name list. */
|
|
63
|
+
export const TASK_V3_MAX_REDACT_NAMES = 32;
|
|
63
64
|
export function own(value, key) {
|
|
64
65
|
return Object.hasOwn(value, key);
|
|
65
66
|
}
|
|
@@ -414,9 +415,6 @@ export function readBoundedTaskSourceYaml(input, options) {
|
|
|
414
415
|
if (typeof input.yaml !== "string") {
|
|
415
416
|
throw new UsageError(`Invalid ${sourceLabel} at ${input.filePath}: source must be a string.`, "TASK_SOURCE_INVALID");
|
|
416
417
|
}
|
|
417
|
-
if (utf8Bytes(input.yaml) > TASK_V3_MAX_SOURCE_BYTES) {
|
|
418
|
-
throw new UsageError(`Invalid ${sourceLabel} at ${input.filePath}: source exceeds the 1 MiB (${TASK_V3_MAX_SOURCE_BYTES}-byte) resource limit.`, "TASK_SOURCE_INVALID");
|
|
419
|
-
}
|
|
420
418
|
const lineCounter = new LineCounter();
|
|
421
419
|
let document;
|
|
422
420
|
try {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* | root `version` | outcome |
|
|
18
18
|
* |------------------------|-----------------------------------------------------------------|
|
|
19
19
|
* | `4` | `parseTaskSourceV4Document` — the new grammar (row B-13) |
|
|
20
|
-
* | `2` or `3` | in-memory read shim (below): the SAME pure planners `akm migrate apply` uses (`./task-to-v3.ts`, `./task-to-v4.ts`) convert the bytes already in hand to v4 in memory; the result is parsed and returned with a one-line stderr deprecation warning. If the deterministic conversion itself fails (an unmigratable shape), falls back to `TASK_SCHEMA_VERSION_UNSUPPORTED
|
|
20
|
+
* | `2` or `3` | in-memory read shim (below): the SAME pure planners `akm migrate apply` uses (`./task-to-v3.ts`, `./task-to-v4.ts`) convert the bytes already in hand to v4 in memory; the result is parsed and returned with a one-line stderr deprecation warning. If the deterministic conversion itself fails (an unmigratable shape — the file needs a human decision, not a re-run), falls back to `TASK_SCHEMA_VERSION_UNSUPPORTED` naming the specific blocked reason (issue #869) — the shim removes friction for the deterministic case, it never hides a real problem |
|
|
21
21
|
* | any other number | `TASK_SCHEMA_VERSION_UNSUPPORTED`, naming the migrator (B-14/B-15) |
|
|
22
22
|
* | absent / not a number | `parseTaskSourceV4Document` — its own `TASK_SOURCE_INVALID` "version is required and must be 4" / "must be exactly 4" wording (row B-16) |
|
|
23
23
|
*
|
|
@@ -57,6 +57,18 @@ export function peekTaskSourceVersion(root) {
|
|
|
57
57
|
return typeof value === "number" ? value : undefined;
|
|
58
58
|
}
|
|
59
59
|
const TASK_MIGRATE_HINT = "Run `akm migrate apply --dry-run` to preview the task-v3 to task-source-v4 conversion, then run `akm migrate apply`.";
|
|
60
|
+
/**
|
|
61
|
+
* Thrown only when the deterministic conversion itself could not produce a
|
|
62
|
+
* task source v4 document — a case where a person must decide the intended
|
|
63
|
+
* behavior (e.g. an ambiguous shell command), not one the migrator can just
|
|
64
|
+
* be re-run to fix. `reason`/`detail` are the SAME blocked outcome
|
|
65
|
+
* `akm migrate status`/`apply` reports for this file, so the message names
|
|
66
|
+
* the actual decision instead of pointing at a command that will report the
|
|
67
|
+
* identical block.
|
|
68
|
+
*/
|
|
69
|
+
function unmigratableVersionError(filePath, version, reason, detail) {
|
|
70
|
+
return new UsageError(`TASK_SCHEMA_VERSION_UNSUPPORTED: Task at ${filePath} uses task schema version ${version} and needs a human decision before it can run — the deterministic migrator cannot convert it automatically (${reason}${detail ? `: ${detail}` : ""}).`, "TASK_SCHEMA_VERSION_UNSUPPORTED", "Review the file and resolve the ambiguity by hand, then it will convert normally; `akm migrate status` reports the same reason.");
|
|
71
|
+
}
|
|
60
72
|
function unsupportedVersionError(filePath, version) {
|
|
61
73
|
return new UsageError(`TASK_SCHEMA_VERSION_UNSUPPORTED: Task at ${filePath} uses task schema version ${version}, which this release does not accept.`, "TASK_SCHEMA_VERSION_UNSUPPORTED", TASK_MIGRATE_HINT);
|
|
62
74
|
}
|
|
@@ -64,9 +76,10 @@ function unsupportedVersionError(filePath, version) {
|
|
|
64
76
|
* Plan the SAME bytes already in hand through the pure v3->v4 (and, for v2,
|
|
65
77
|
* chained v2->v3->v4) migration planner(s) — never touches disk, never
|
|
66
78
|
* writes the file, never re-reads it from disk. Returns the produced v4
|
|
67
|
-
* YAML text, or
|
|
68
|
-
* proceed (an unmigratable v2/v3 shape) — the caller falls back to
|
|
69
|
-
* hard error this gate threw before the shim existed
|
|
79
|
+
* YAML text, or the blocked reason/detail when the deterministic conversion
|
|
80
|
+
* cannot proceed (an unmigratable v2/v3 shape) — the caller falls back to
|
|
81
|
+
* the same hard error this gate threw before the shim existed, now naming
|
|
82
|
+
* that reason.
|
|
70
83
|
*/
|
|
71
84
|
function planInMemoryV4Bytes(version, yaml, filePath, workspaceRoot) {
|
|
72
85
|
const bytes = Buffer.from(yaml, "utf8");
|
|
@@ -89,12 +102,12 @@ function planInMemoryV4Bytes(version, yaml, filePath, workspaceRoot) {
|
|
|
89
102
|
else {
|
|
90
103
|
const v3Outcome = planTaskToV3File(baseInput);
|
|
91
104
|
if (v3Outcome.status !== "changed")
|
|
92
|
-
return
|
|
105
|
+
return { reason: v3Outcome.reason, detail: v3Outcome.detail };
|
|
93
106
|
v3Bytes = v3Outcome.after;
|
|
94
107
|
}
|
|
95
108
|
const v4Outcome = planTaskToV4File({ ...baseInput, bytes: v3Bytes });
|
|
96
109
|
if (v4Outcome.status !== "changed")
|
|
97
|
-
return
|
|
110
|
+
return { reason: v4Outcome.reason, detail: v4Outcome.detail };
|
|
98
111
|
return v4Outcome.after.toString("utf8");
|
|
99
112
|
}
|
|
100
113
|
/** Parse task source YAML, routing per the terminal table above. */
|
|
@@ -103,16 +116,17 @@ export function parseTaskSource(input) {
|
|
|
103
116
|
const version = peekTaskSourceVersion(root);
|
|
104
117
|
if (version !== undefined && version !== TASK_SOURCE_V4_VERSION) {
|
|
105
118
|
if (version === 2 || version === 3) {
|
|
106
|
-
const
|
|
107
|
-
if (
|
|
119
|
+
const shimmed = planInMemoryV4Bytes(version, input.yaml, input.filePath, input.workspaceRoot);
|
|
120
|
+
if (typeof shimmed === "string") {
|
|
108
121
|
const v4 = parseTaskSourceV4({
|
|
109
|
-
yaml:
|
|
122
|
+
yaml: shimmed,
|
|
110
123
|
filePath: input.filePath,
|
|
111
124
|
...(input.workspaceRoot ? { workspaceRoot: input.workspaceRoot } : {}),
|
|
112
125
|
});
|
|
113
126
|
warn(`akm: task ${input.filePath} uses schema v${version} — auto-read as v4; run \`akm migrate apply\` to rewrite it and silence this`);
|
|
114
127
|
return Object.freeze({ version: 4, v4 });
|
|
115
128
|
}
|
|
129
|
+
throw unmigratableVersionError(input.filePath, version, shimmed.reason, shimmed.detail);
|
|
116
130
|
}
|
|
117
131
|
throw unsupportedVersionError(input.filePath, version);
|
|
118
132
|
}
|
|
@@ -42,8 +42,8 @@ import { parseBuiltinCommandAction } from "../../commands/command/builtin-action
|
|
|
42
42
|
import { bundleRefToString, parseBundleRef } from "../../core/asset/asset-ref.js";
|
|
43
43
|
import { UsageError } from "../../core/errors.js";
|
|
44
44
|
import { checkJsonSchemaDefinition } from "../../core/json-schema.js";
|
|
45
|
-
import { WORKFLOW_ENV_VAR_NAME_PATTERN
|
|
46
|
-
import { asRecord, checkKeys, cloneBoundedJson, noGithubExpression, own, parseEnvironment, parseStringArray, parseTimeout, parseTools, presentJsonValue, readBoundedTaskSourceYaml, sourceError, stringField, TASK_V3_MAX_SCHEDULES, validateWorkingDirectory, } from "./bounded-document.js";
|
|
45
|
+
import { WORKFLOW_ENV_VAR_NAME_PATTERN } from "../../workflows/resource-limits.js";
|
|
46
|
+
import { asRecord, checkKeys, cloneBoundedJson, noGithubExpression, own, parseEnvironment, parseStringArray, parseTimeout, parseTools, presentJsonValue, readBoundedTaskSourceYaml, sourceError, stringField, TASK_V3_MAX_REDACT_NAMES, TASK_V3_MAX_SCHEDULES, validateWorkingDirectory, } from "./bounded-document.js";
|
|
47
47
|
export const TASK_V3_SCHEMA_VERSION = 3;
|
|
48
48
|
/** Closed authoring vocabulary. Arbitrary GitHub `{0}` shell templates are not accepted. */
|
|
49
49
|
export const TASK_V3_HOST_SHELLS = ["bash", "sh", "zsh", "pwsh", "powershell", "cmd"];
|
|
@@ -146,7 +146,7 @@ function parseAkm(value, ctx) {
|
|
|
146
146
|
out.timeout = parseTimeout(input.timeout, ctx);
|
|
147
147
|
if (own(input, "redact")) {
|
|
148
148
|
const names = parseStringArray(input.redact, ctx, ["akm", "redact"], {
|
|
149
|
-
max:
|
|
149
|
+
max: TASK_V3_MAX_REDACT_NAMES,
|
|
150
150
|
pattern: WORKFLOW_ENV_VAR_NAME_PATTERN,
|
|
151
151
|
});
|
|
152
152
|
if (new Set(names).size !== names.length)
|
|
@@ -160,10 +160,8 @@ function parseAkm(value, ctx) {
|
|
|
160
160
|
out.maxSteps = input.maxSteps;
|
|
161
161
|
}
|
|
162
162
|
if (own(input, "maxRetries")) {
|
|
163
|
-
if (!Number.isSafeInteger(input.maxRetries) ||
|
|
164
|
-
|
|
165
|
-
input.maxRetries > WORKFLOW_MAX_RETRIES) {
|
|
166
|
-
sourceError(ctx, ["akm", "maxRetries"], `must be an integer from 0 through ${WORKFLOW_MAX_RETRIES}.`);
|
|
163
|
+
if (!Number.isSafeInteger(input.maxRetries) || input.maxRetries < 0) {
|
|
164
|
+
sourceError(ctx, ["akm", "maxRetries"], "must be a non-negative safe integer.");
|
|
167
165
|
}
|
|
168
166
|
out.maxRetries = input.maxRetries;
|
|
169
167
|
}
|
|
@@ -42,10 +42,10 @@ import { applyInputDefaults, INPUT_NAME_PATTERN, validateInputs, } from "../../e
|
|
|
42
42
|
import { EXECUTION_MAX_TIMEOUT_MS } from "../../execution/limits.js";
|
|
43
43
|
import { classifyTargetRef } from "../../execution/target-ref.js";
|
|
44
44
|
import { detectSecretShapedParams } from "../../workflows/exec/param-secrets.js";
|
|
45
|
-
import { WORKFLOW_ENV_VAR_NAME_PATTERN,
|
|
45
|
+
import { WORKFLOW_ENV_VAR_NAME_PATTERN, WORKFLOW_MAX_SCHEMA_BYTES } from "../../workflows/resource-limits.js";
|
|
46
46
|
import { TASK_V3_HOST_SHELLS, TASK_V3_MAX_SCHEDULES } from "../source-v3.js";
|
|
47
47
|
import { TASK_RUN_RESERVED_FLAG_NAMES, TASK_RUN_SELF_DIAGNOSED_FLAGS } from "../task-run-reserved-flags.js";
|
|
48
|
-
import { asRecord, checkKeys, cloneBoundedJson, noGithubExpression, own, parseEnvironment, parseStringArray, presentJsonValue, readBoundedTaskSourceYaml, sourceError, stringField, utf8Bytes, validateWorkingDirectory, } from "./bounded-document.js";
|
|
48
|
+
import { asRecord, checkKeys, cloneBoundedJson, noGithubExpression, own, parseEnvironment, parseStringArray, presentJsonValue, readBoundedTaskSourceYaml, sourceError, stringField, TASK_V3_MAX_REDACT_NAMES, utf8Bytes, validateWorkingDirectory, } from "./bounded-document.js";
|
|
49
49
|
// ── Closed constants (D1, D2-N3, D2-N7) ─────────────────────────────────────
|
|
50
50
|
export const TASK_SOURCE_V4_VERSION = 4;
|
|
51
51
|
/** The exact, closed top-level key set (D2-N7) — `akm` and `on` are deliberately absent. */
|
|
@@ -298,7 +298,7 @@ function parseExecutionControls(input, ctx) {
|
|
|
298
298
|
out.timeout = parseTimeoutTopLevel(input.timeout, ctx);
|
|
299
299
|
if (own(input, "redact")) {
|
|
300
300
|
const names = parseStringArray(input.redact, ctx, ["redact"], {
|
|
301
|
-
max:
|
|
301
|
+
max: TASK_V3_MAX_REDACT_NAMES,
|
|
302
302
|
pattern: WORKFLOW_ENV_VAR_NAME_PATTERN,
|
|
303
303
|
});
|
|
304
304
|
if (new Set(names).size !== names.length)
|
|
@@ -312,10 +312,8 @@ function parseExecutionControls(input, ctx) {
|
|
|
312
312
|
out.maxSteps = input.maxSteps;
|
|
313
313
|
}
|
|
314
314
|
if (own(input, "maxRetries")) {
|
|
315
|
-
if (!Number.isSafeInteger(input.maxRetries) ||
|
|
316
|
-
|
|
317
|
-
input.maxRetries > WORKFLOW_MAX_RETRIES) {
|
|
318
|
-
sourceError(ctx, ["maxRetries"], `must be an integer from 0 through ${WORKFLOW_MAX_RETRIES}.`);
|
|
315
|
+
if (!Number.isSafeInteger(input.maxRetries) || input.maxRetries < 0) {
|
|
316
|
+
sourceError(ctx, ["maxRetries"], "must be a non-negative safe integer.");
|
|
319
317
|
}
|
|
320
318
|
out.maxRetries = input.maxRetries;
|
|
321
319
|
}
|
|
@@ -376,9 +374,6 @@ function parseInputDeclaration(name, raw, ctx) {
|
|
|
376
374
|
function parseInputDeclarations(value, ctx) {
|
|
377
375
|
const input = asRecord(value, ctx, ["inputs"]);
|
|
378
376
|
const names = Object.keys(input);
|
|
379
|
-
if (names.length > WORKFLOW_MAX_PARAMS) {
|
|
380
|
-
sourceError(ctx, ["inputs"], `accepts at most ${WORKFLOW_MAX_PARAMS} declared inputs.`);
|
|
381
|
-
}
|
|
382
377
|
const result = {};
|
|
383
378
|
for (const name of names) {
|
|
384
379
|
if (!INPUT_NAME_PATTERN.test(name)) {
|
|
@@ -7,9 +7,9 @@ import path from "node:path";
|
|
|
7
7
|
import { LineCounter, parseDocument, stringify as stringifyYaml } from "yaml";
|
|
8
8
|
import { bundleRefToString, parseBundleRef } from "../../core/asset/asset-ref.js";
|
|
9
9
|
import { formatExtraParamsIssue, validateExtraParams } from "../../core/extra-params.js";
|
|
10
|
-
import { WORKFLOW_ENV_VAR_NAME_PATTERN,
|
|
10
|
+
import { WORKFLOW_ENV_VAR_NAME_PATTERN, WORKFLOW_MAX_TIMEOUT_MS } from "../../workflows/resource-limits.js";
|
|
11
11
|
import { validateTaskId } from "../task-id.js";
|
|
12
|
-
import { assertBoundedTaskYamlDocument,
|
|
12
|
+
import { assertBoundedTaskYamlDocument, TASK_V3_MAX_REDACT_NAMES } from "./bounded-document.js";
|
|
13
13
|
import { classifyTaskV3Uses, parseTaskV3Yaml } from "./task-source-v3-frozen.js";
|
|
14
14
|
import { parseTaskSourceV4 } from "./task-source-v4.js";
|
|
15
15
|
const V2_KEYS = new Set([
|
|
@@ -135,9 +135,6 @@ function optionalString(value, label) {
|
|
|
135
135
|
return exactString(value, label);
|
|
136
136
|
}
|
|
137
137
|
function parseLegacyTaskYaml(input) {
|
|
138
|
-
if (input.bytes.byteLength > TASK_V3_MAX_SOURCE_BYTES) {
|
|
139
|
-
throw new Error(`task YAML exceeds the 1 MiB (${TASK_V3_MAX_SOURCE_BYTES}-byte) source resource limit`);
|
|
140
|
-
}
|
|
141
138
|
let source;
|
|
142
139
|
try {
|
|
143
140
|
source = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(input.bytes);
|
|
@@ -189,7 +186,7 @@ function validateCommonV2(data) {
|
|
|
189
186
|
}
|
|
190
187
|
if (data.redact !== undefined && data.redact !== null) {
|
|
191
188
|
if (!Array.isArray(data.redact) ||
|
|
192
|
-
data.redact.length >
|
|
189
|
+
data.redact.length > TASK_V3_MAX_REDACT_NAMES ||
|
|
193
190
|
data.redact.some((entry) => typeof entry !== "string" || !WORKFLOW_ENV_VAR_NAME_PATTERN.test(entry))) {
|
|
194
191
|
throw new Error("redact must contain only bounded environment variable names");
|
|
195
192
|
}
|
|
@@ -321,10 +318,8 @@ function migratedObject(data) {
|
|
|
321
318
|
akm.maxSteps = data.maxSteps;
|
|
322
319
|
}
|
|
323
320
|
if (data.maxRetries !== undefined && data.maxRetries !== null) {
|
|
324
|
-
if (!Number.isSafeInteger(data.maxRetries) ||
|
|
325
|
-
|
|
326
|
-
data.maxRetries > WORKFLOW_MAX_RETRIES) {
|
|
327
|
-
throw new Error(`maxRetries must be from 0 through ${WORKFLOW_MAX_RETRIES}`);
|
|
321
|
+
if (!Number.isSafeInteger(data.maxRetries) || data.maxRetries < 0) {
|
|
322
|
+
throw new Error("maxRetries must be a non-negative integer");
|
|
328
323
|
}
|
|
329
324
|
akm.maxRetries = data.maxRetries;
|
|
330
325
|
}
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import crypto from "node:crypto";
|
|
19
19
|
import path from "node:path";
|
|
20
20
|
import { LineCounter, parseDocument, stringify as stringifyYaml } from "yaml";
|
|
21
|
-
import { assertBoundedTaskYamlDocument
|
|
21
|
+
import { assertBoundedTaskYamlDocument } from "./bounded-document.js";
|
|
22
22
|
import { classifyTaskV3Uses } from "./task-source-v3-frozen.js";
|
|
23
23
|
import { parseTaskSourceV4 } from "./task-source-v4.js";
|
|
24
24
|
/** The closed v3 top-level key set (`src/tasks/source-v3.ts`'s own, vendored — not exported there). */
|
|
@@ -114,9 +114,6 @@ function exactString(value, label, nonempty = false) {
|
|
|
114
114
|
* away (C-N1).
|
|
115
115
|
*/
|
|
116
116
|
function parseV3RawYaml(input) {
|
|
117
|
-
if (input.bytes.byteLength > TASK_V3_MAX_SOURCE_BYTES) {
|
|
118
|
-
throw new Error(`task YAML exceeds the 1 MiB (${TASK_V3_MAX_SOURCE_BYTES}-byte) source resource limit`);
|
|
119
|
-
}
|
|
120
117
|
let source;
|
|
121
118
|
try {
|
|
122
119
|
source = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(input.bytes);
|
package/dist/tasks/source-v3.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
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 { TASK_V3_MAX_SCHEDULES
|
|
4
|
+
import { TASK_V3_MAX_SCHEDULES } from "./source/bounded-document.js";
|
|
5
5
|
// D2-N4 (spec docs/plans/specs/p2a-task-source-v4.md §3.1, §9): re-exported
|
|
6
|
-
// at
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
export { TASK_V3_MAX_SCHEDULES
|
|
6
|
+
// at its EXISTING name so no importer changes. The one name a surviving
|
|
7
|
+
// `src` consumer still imports (task-source-v4.ts) — every other
|
|
8
|
+
// bounded-document re-export this file used to carry was dropped in P4
|
|
9
|
+
// §3.2.3.
|
|
10
|
+
export { TASK_V3_MAX_SCHEDULES };
|
|
11
11
|
export const TASK_EXTENSION = ".yml";
|
|
12
12
|
export const TASK_NEAR_MISS_EXTENSION = ".yaml";
|
|
13
13
|
/** Explain why the near-miss `.yaml` spelling is not a task source. */
|
|
@@ -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`
|