akm-cli 0.9.4 → 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 +265 -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/anti-collapse.js +4 -91
- 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/proposal/validators/proposal-validators.js +12 -0
- package/dist/commands/read/search.js +14 -24
- package/dist/commands/sources/bundle-config-ops.js +9 -4
- package/dist/commands/sources/info.js +14 -10
- package/dist/commands/tasks/tasks-cli.js +56 -3
- package/dist/commands/tasks/tasks.js +101 -18
- package/dist/commands/workflow-cli.js +2 -2
- package/dist/core/adapter/adapters/akm-adapter.js +2 -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-version-shim.js +101 -0
- package/dist/core/config/config.js +7 -7
- 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/improve-result.js +35 -14
- 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/execution/guarded-source.js +0 -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/passes/metadata.js +12 -4
- package/dist/indexer/scan/doc-to-entry.js +2 -0
- 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/indexer/search/search-fields.js +16 -1
- 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/output/shapes/passthrough.js +17 -5
- package/dist/registry/network.js +5 -37
- package/dist/runtime.js +2 -10
- package/dist/scripts/akm-migrate-node.js +180 -238
- package/dist/scripts/akm-migrate.js +180 -238
- 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/storage/repositories/proposals-repository.js +32 -6
- 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/scheduler-binding.js +15 -5
- package/dist/tasks/scheduler-sync-preview.js +4 -2
- package/dist/tasks/scheduler-sync.js +77 -42
- 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 +29 -22
- package/dist/tasks/source/task-to-v4.js +1 -14
- 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 +25 -3
- package/docs/reference/configuration.md +28 -7
- package/docs/reference/tasks.md +10 -0
- 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
|
@@ -35,11 +35,10 @@ import { exitCodeForStatus } from "../../tasks/run/task-result.js";
|
|
|
35
35
|
import { parseSchedule, SCHEDULE_SUPPORTED_SUBSET_HINT } from "../../tasks/schedule.js";
|
|
36
36
|
import { assertSchedulerMutationArtifact, assertSchedulerNativeArtifactCardinality, compileTaskSchedulerBindings, schedulerBindingNativeId, schedulerBindingOrdinal, schedulerNativeArtifactKey, schedulerNativeBindingId, } from "../../tasks/scheduler-binding.js";
|
|
37
37
|
import { schedulerContextDescriptor, schedulerContextPath, validateSchedulerContextDescriptor, writeSchedulerContextDescriptor, } from "../../tasks/scheduler-invocation.js";
|
|
38
|
-
import { assertSchedulerNativeArtifactOwnership, assertSchedulerSourceSnapshot, finalizeSchedulerSyncPlan, prepareSchedulerSyncSourceSet, } from "../../tasks/scheduler-sync.js";
|
|
39
|
-
import { renderSchedulerSyncPlanPreview } from "../../tasks/scheduler-sync-preview.js";
|
|
38
|
+
import { assertSchedulerNativeArtifactOwnership, assertSchedulerSourceSnapshot, buildSchedulerRemoveOperation, finalizeSchedulerSyncPlan, prepareSchedulerSyncSourceSet, } from "../../tasks/scheduler-sync.js";
|
|
39
|
+
import { renderSchedulerPlanPreview, renderSchedulerSyncPlanPreview, } from "../../tasks/scheduler-sync-preview.js";
|
|
40
40
|
import { parseTaskSource } from "../../tasks/source/parse-task-source.js";
|
|
41
41
|
import { projectTaskSourceV4 } from "../../tasks/source/project-v4.js";
|
|
42
|
-
import { TASK_V3_MAX_SOURCE_BYTES } from "../../tasks/source-v3.js";
|
|
43
42
|
import { normaliseTaskConceptId, normaliseTaskId } from "../../tasks/task-id.js";
|
|
44
43
|
import { applyAutonomyGate, configuredDirectAutonomyLanes, describeGatedLanes } from "../improve/autonomy-gate.js";
|
|
45
44
|
import { resolveImproveStrategy } from "../improve/improve-strategies.js";
|
|
@@ -417,6 +416,7 @@ export async function akmTasksSync(deps = {}, bundleTarget, options = {}) {
|
|
|
417
416
|
unchanged: [...plan.unchanged],
|
|
418
417
|
skipped: [],
|
|
419
418
|
backend: sched.name,
|
|
419
|
+
failed: plan.failures.map((failure) => ({ ...failure })),
|
|
420
420
|
...(warnings.length > 0 ? { warnings } : {}),
|
|
421
421
|
};
|
|
422
422
|
}
|
|
@@ -434,6 +434,92 @@ export async function akmTasksSyncPlan(deps = {}, bundleTarget, options = {}) {
|
|
|
434
434
|
const { sched, plan } = await buildSchedulerSyncPlan(deps, bundleTarget, options);
|
|
435
435
|
return renderSchedulerSyncPlanPreview(sched.name, plan);
|
|
436
436
|
}
|
|
437
|
+
/**
|
|
438
|
+
* Classify one installed scheduler binding as a prune candidate (#851), using
|
|
439
|
+
* the same two signals `doctor`'s `inspectInstalledBinding` already computes
|
|
440
|
+
* — deliberately narrower than that function's full `status` set. Only an
|
|
441
|
+
* entry whose ownership can NEVER be resolved (`invalid-context`) or whose
|
|
442
|
+
* resolved owner no longer exists on disk (`dead-bundle-path`) is a
|
|
443
|
+
* candidate; `missing-path` (e.g. the akm binary itself moved) is a
|
|
444
|
+
* different failure mode and is intentionally NOT folded in here, per the
|
|
445
|
+
* scoping in #851 — an entry that still resolves to a live bundle is never a
|
|
446
|
+
* candidate, full stop.
|
|
447
|
+
*/
|
|
448
|
+
function classifyPruneCandidate(entry) {
|
|
449
|
+
let ownerBundlePath;
|
|
450
|
+
try {
|
|
451
|
+
ownerBundlePath = validateSchedulerContextDescriptor(entry.contextPath).environment.AKM_BUNDLE_DIR;
|
|
452
|
+
}
|
|
453
|
+
catch {
|
|
454
|
+
return "invalid-context";
|
|
455
|
+
}
|
|
456
|
+
if (ownerBundlePath !== undefined && !fs.existsSync(ownerBundlePath))
|
|
457
|
+
return "dead-bundle-path";
|
|
458
|
+
return undefined;
|
|
459
|
+
}
|
|
460
|
+
async function buildTaskPrunePlan(deps = {}, options = {}) {
|
|
461
|
+
const sched = deps.backend ?? selectBackend();
|
|
462
|
+
if (!sched.inspectBindings) {
|
|
463
|
+
throw new ConfigError(`Scheduler backend "${sched.name}" cannot provide one coherent inspection for prune.`, "INVALID_CONFIG_FILE");
|
|
464
|
+
}
|
|
465
|
+
const inspection = await sched.inspectBindings({});
|
|
466
|
+
const candidates = new Map();
|
|
467
|
+
for (const entry of inspection.installed) {
|
|
468
|
+
const reason = classifyPruneCandidate(entry);
|
|
469
|
+
if (reason)
|
|
470
|
+
candidates.set(entry.id, reason);
|
|
471
|
+
}
|
|
472
|
+
const requestedIds = options.id?.filter((id) => id.length > 0) ?? [];
|
|
473
|
+
for (const id of requestedIds) {
|
|
474
|
+
if (!candidates.has(id)) {
|
|
475
|
+
throw new UsageError(`Scheduler binding ${JSON.stringify(id)} is not an orphaned prune candidate ` +
|
|
476
|
+
"(either not installed, or it still resolves to a live bundle) — refusing to prune it.", "INVALID_FLAG_VALUE");
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
const idFilter = requestedIds.length > 0 ? new Set(requestedIds) : undefined;
|
|
480
|
+
const resolved = resolveTaskReadBundle(undefined, undefined);
|
|
481
|
+
const bundleContext = {
|
|
482
|
+
adapterId: resolved.source.adapterId ?? detectAdapterId(resolved.source.path),
|
|
483
|
+
bundleName: resolved.source.name,
|
|
484
|
+
};
|
|
485
|
+
const operations = [];
|
|
486
|
+
for (const entry of inspection.installed) {
|
|
487
|
+
const reason = candidates.get(entry.id);
|
|
488
|
+
if (!reason)
|
|
489
|
+
continue;
|
|
490
|
+
if (idFilter && !idFilter.has(entry.id))
|
|
491
|
+
continue;
|
|
492
|
+
const operation = buildSchedulerRemoveOperation(entry.id, entry, inspection.artifacts, bundleContext);
|
|
493
|
+
operations.push(Object.freeze({ ...operation, reason }));
|
|
494
|
+
}
|
|
495
|
+
return { sched, operations: Object.freeze(operations) };
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* `akm task prune` (#851): remove installed scheduler bindings `sync` can
|
|
499
|
+
* never reclaim because their own `--scheduler-context` descriptor doesn't
|
|
500
|
+
* resolve to a live bundle. Defaults to dry-run — no `--yes` and no `--id`
|
|
501
|
+
* means zero backend calls that could mutate anything, matching
|
|
502
|
+
* `akmTasksSyncPlan`'s zero-write guarantee. `--id` (one or more) narrows
|
|
503
|
+
* execution to exactly those bindings; `--yes` alone executes every
|
|
504
|
+
* currently-computed candidate. Both still return the full preview so the
|
|
505
|
+
* plan is never silent about what it did.
|
|
506
|
+
*/
|
|
507
|
+
export async function akmTasksPrune(deps = {}, options = {}) {
|
|
508
|
+
const { sched, operations } = await buildTaskPrunePlan(deps, options);
|
|
509
|
+
const preview = renderSchedulerPlanPreview(sched.name, operations);
|
|
510
|
+
if (!options.yes) {
|
|
511
|
+
return { backend: sched.name, dryRun: true, preview, removed: [] };
|
|
512
|
+
}
|
|
513
|
+
await applySchedulerTransaction(sched, operations, {
|
|
514
|
+
initialExpectations: operations.map((operation) => operation.expected),
|
|
515
|
+
});
|
|
516
|
+
return {
|
|
517
|
+
backend: sched.name,
|
|
518
|
+
dryRun: false,
|
|
519
|
+
preview,
|
|
520
|
+
removed: operations.map((operation) => operation.id),
|
|
521
|
+
};
|
|
522
|
+
}
|
|
437
523
|
export async function akmTasksDoctor(deps = {}) {
|
|
438
524
|
const warnings = [];
|
|
439
525
|
let invocation = {
|
|
@@ -884,13 +970,7 @@ function captureTaskSourceExpectation(filePathInput, rootInput) {
|
|
|
884
970
|
if (!rootStat.isDirectory()) {
|
|
885
971
|
throw new UsageError(`${root} is not a task source directory.`, "INVALID_FLAG_VALUE");
|
|
886
972
|
}
|
|
887
|
-
const common = {
|
|
888
|
-
filePath,
|
|
889
|
-
rootRealPath,
|
|
890
|
-
rootPhysicalIdentity: rootStat.ino === 0n ? `path:${rootRealPath}` : `inode:${rootStat.dev}:${rootStat.ino}`,
|
|
891
|
-
rootMtimeNs: String(rootStat.mtimeNs),
|
|
892
|
-
rootCtimeNs: String(rootStat.ctimeNs),
|
|
893
|
-
};
|
|
973
|
+
const common = { filePath, rootRealPath };
|
|
894
974
|
let descriptor;
|
|
895
975
|
try {
|
|
896
976
|
const noFollow = "O_NOFOLLOW" in fs.constants ? fs.constants.O_NOFOLLOW : 0;
|
|
@@ -899,9 +979,6 @@ function captureTaskSourceExpectation(filePathInput, rootInput) {
|
|
|
899
979
|
if (!before.isFile()) {
|
|
900
980
|
throw new UsageError(`${filePath} is not a regular task source.`, "INVALID_FLAG_VALUE");
|
|
901
981
|
}
|
|
902
|
-
if (before.size > BigInt(TASK_V3_MAX_SOURCE_BYTES)) {
|
|
903
|
-
throw new UsageError(`${filePath} exceeds the 1 MiB (${TASK_V3_MAX_SOURCE_BYTES}-byte) task source limit.`, "INVALID_FLAG_VALUE");
|
|
904
|
-
}
|
|
905
982
|
const bytes = fs.readFileSync(descriptor);
|
|
906
983
|
const after = fs.fstatSync(descriptor, { bigint: true });
|
|
907
984
|
if (!sameTaskSourceStat(before, after) || BigInt(bytes.byteLength) !== before.size) {
|
|
@@ -923,10 +1000,7 @@ function captureTaskSourceExpectation(filePathInput, rootInput) {
|
|
|
923
1000
|
state: "present",
|
|
924
1001
|
...common,
|
|
925
1002
|
realPath,
|
|
926
|
-
physicalIdentity: before.ino === 0n ? `path:${realPath}` : `inode:${before.dev}:${before.ino}`,
|
|
927
1003
|
size: bytes.byteLength,
|
|
928
|
-
mtimeNs: String(before.mtimeNs),
|
|
929
|
-
ctimeNs: String(before.ctimeNs),
|
|
930
1004
|
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
931
1005
|
bytesBase64: bytes.toString("base64"),
|
|
932
1006
|
content,
|
|
@@ -948,6 +1022,11 @@ function captureTaskSourceExpectation(filePathInput, rootInput) {
|
|
|
948
1022
|
fs.closeSync(descriptor);
|
|
949
1023
|
}
|
|
950
1024
|
}
|
|
1025
|
+
// TOCTOU note: this compares CONTENT (state + sha256), not filesystem
|
|
1026
|
+
// identity (inode/mtime/ctime/directory timestamps) — the same split already
|
|
1027
|
+
// applied to the task migrator in 0.9.5. An unrelated touch to the file or
|
|
1028
|
+
// its containing directory must not trip a "changed after planning" refusal;
|
|
1029
|
+
// only a real content change should.
|
|
951
1030
|
function assertTaskSourceExpectation(expected) {
|
|
952
1031
|
const actual = captureTaskSourceExpectation(expected.filePath, expected.rootRealPath);
|
|
953
1032
|
if (!sameTaskSourceExpectation(actual, expected)) {
|
|
@@ -955,12 +1034,16 @@ function assertTaskSourceExpectation(expected) {
|
|
|
955
1034
|
}
|
|
956
1035
|
}
|
|
957
1036
|
function sameTaskSourceExpectation(left, right) {
|
|
958
|
-
|
|
1037
|
+
if (left.state !== right.state || left.filePath !== right.filePath || left.rootRealPath !== right.rootRealPath) {
|
|
1038
|
+
return false;
|
|
1039
|
+
}
|
|
1040
|
+
if (left.state === "absent")
|
|
1041
|
+
return true;
|
|
1042
|
+
return left.sha256 === right.sha256;
|
|
959
1043
|
}
|
|
960
1044
|
function assertTaskSourceRestored(expected) {
|
|
961
1045
|
const actual = captureTaskSourceExpectation(expected.filePath, expected.rootRealPath);
|
|
962
1046
|
const restored = actual.state === expected.state &&
|
|
963
|
-
actual.rootPhysicalIdentity === expected.rootPhysicalIdentity &&
|
|
964
1047
|
(actual.state === "absent" ||
|
|
965
1048
|
(expected.state === "present" && actual.sha256 === expected.sha256 && actual.content === expected.content));
|
|
966
1049
|
if (!restored) {
|
|
@@ -20,7 +20,7 @@ import { deliverRendered } from "../output/html-render.js";
|
|
|
20
20
|
import { shapeForCommand } from "../output/shapes.js";
|
|
21
21
|
import { formatPlain } from "../output/text.js";
|
|
22
22
|
import { assertWorkflowMarkdownName, createWorkflowAsset, getWorkflowTemplate } from "../workflows/authoring/authoring.js";
|
|
23
|
-
import {
|
|
23
|
+
import { WORKFLOW_MAX_TIMEOUT_MS } from "../workflows/ir/schema.js";
|
|
24
24
|
import { abandonWorkflowRun, getWorkflowStatus, hasWorkflowRun, listWorkflowRuns, resumeWorkflowRun, } from "../workflows/runtime/runs.js";
|
|
25
25
|
import { akmWorkflowPlan } from "./workflow/plan.js";
|
|
26
26
|
const workflowStatusCommand = defineJsonCommand({
|
|
@@ -168,7 +168,7 @@ const workflowRunCommand = defineJsonCommand({
|
|
|
168
168
|
const { runWorkflowSteps } = await import("../workflows/exec/run-workflow.js");
|
|
169
169
|
const parameterFlags = parseWorkflowParameterFlags(rawArgs, args.target);
|
|
170
170
|
const maxSteps = parseIntegerFlag(getStringArg(args, "max-steps"), "--max-steps", 1);
|
|
171
|
-
const maxRetries = parseIntegerFlag(getStringArg(args, "max-retries"), "--max-retries", 0
|
|
171
|
+
const maxRetries = parseIntegerFlag(getStringArg(args, "max-retries"), "--max-retries", 0);
|
|
172
172
|
const timeoutMs = parseWorkflowTimeout(getStringArg(args, "timeout"));
|
|
173
173
|
const controller = new AbortController();
|
|
174
174
|
let signalExitCode;
|
|
@@ -223,6 +223,8 @@ function indexDocumentFromEntry(entry, base, rendererName) {
|
|
|
223
223
|
doc.tags = entry.tags;
|
|
224
224
|
if (entry.content !== undefined)
|
|
225
225
|
doc.content = entry.content;
|
|
226
|
+
if (entry.contentTruncated !== undefined)
|
|
227
|
+
doc.contentTruncated = entry.contentTruncated;
|
|
226
228
|
if (entry.aliases !== undefined)
|
|
227
229
|
doc.aliases = entry.aliases;
|
|
228
230
|
if (entry.searchHints !== undefined)
|
package/dist/core/common.js
CHANGED
|
@@ -10,8 +10,6 @@ import { getConfigPath, getDefaultStashDir, getRegistryCacheDir, getRegistryInde
|
|
|
10
10
|
// Moved to the platform leaf so paths.ts can use it without a common↔paths
|
|
11
11
|
// cycle (chunk-8 WI-8.6, DoD 11); re-exported here for the existing surface.
|
|
12
12
|
export { IS_WINDOWS } from "./platform.js";
|
|
13
|
-
export const MAX_CONFIG_FILE_BYTES = 1024 * 1024;
|
|
14
|
-
export const MAX_LOCAL_METADATA_BYTES = 1024 * 1024;
|
|
15
13
|
export const MAX_LOCK_METADATA_BYTES = 64 * 1024;
|
|
16
14
|
export function isHttpUrl(value) {
|
|
17
15
|
return !!value && /^https?:\/\//.test(value);
|
|
@@ -51,10 +49,13 @@ export function readTextFileDescriptorWithLimit(fd, maxBytes, label = "File", di
|
|
|
51
49
|
}
|
|
52
50
|
return buffer.subarray(0, total).toString("utf8");
|
|
53
51
|
}
|
|
54
|
-
export function
|
|
52
|
+
export function readTextFile(filePath, label = "File") {
|
|
55
53
|
const fd = fs.openSync(filePath, "r");
|
|
56
54
|
try {
|
|
57
|
-
|
|
55
|
+
const stat = fs.fstatSync(fd);
|
|
56
|
+
if (!stat.isFile())
|
|
57
|
+
throw new ConfigError(`${label} is not a regular file: ${filePath}.`, "INVALID_CONFIG_FILE");
|
|
58
|
+
return fs.readFileSync(fd, "utf8");
|
|
58
59
|
}
|
|
59
60
|
finally {
|
|
60
61
|
fs.closeSync(fd);
|
|
@@ -290,7 +291,7 @@ function isValidDirectory(dir) {
|
|
|
290
291
|
function readStashDirFromConfig() {
|
|
291
292
|
try {
|
|
292
293
|
const configPath = getConfigPath();
|
|
293
|
-
const text =
|
|
294
|
+
const text = readTextFile(configPath, "Config file");
|
|
294
295
|
// The config loader accepts JSONC, so a commented config.json is valid and
|
|
295
296
|
// in use. Parsing it raw here threw, the catch swallowed it, and every
|
|
296
297
|
// caller silently fell back — operating on the wrong bundle or failing with
|
|
@@ -544,21 +545,59 @@ export async function fetchWithTimeout(url, opts, timeoutMs = 30_000, signal) {
|
|
|
544
545
|
clearTimeout(timer);
|
|
545
546
|
}
|
|
546
547
|
}
|
|
548
|
+
/**
|
|
549
|
+
* Cap on how long a retry loop will wait between attempts, even when a
|
|
550
|
+
* server-supplied `Retry-After` claims a longer delay. Prevents an
|
|
551
|
+
* attacker-controlled or misconfigured server from parking a caller
|
|
552
|
+
* indefinitely.
|
|
553
|
+
*/
|
|
554
|
+
export const DEFAULT_RETRY_MAX_DELAY_MS = 30_000;
|
|
555
|
+
export function shouldRetry(status) {
|
|
556
|
+
return status === 429 || status >= 500;
|
|
557
|
+
}
|
|
558
|
+
/** Jittered exponential backoff, capped at `maxDelayMs`. */
|
|
559
|
+
export function backoffDelay(attempt, baseDelay = 500, maxDelayMs = DEFAULT_RETRY_MAX_DELAY_MS) {
|
|
560
|
+
return Math.min(maxDelayMs, baseDelay * 2 ** attempt * (0.5 + Math.random() * 0.5));
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Determine the delay before the next retry attempt.
|
|
564
|
+
*
|
|
565
|
+
* Honors a server-supplied `Retry-After` header in both its numeric-seconds
|
|
566
|
+
* and HTTP-date forms, but always clamps the result to `maxDelayMs` — an
|
|
567
|
+
* unclamped `Retry-After` lets an attacker/misconfigured server park a
|
|
568
|
+
* caller for an arbitrarily long time. Falls back to jittered exponential
|
|
569
|
+
* backoff when the header is absent or unparseable.
|
|
570
|
+
*/
|
|
571
|
+
export function computeRetryDelay(response, attempt, options) {
|
|
572
|
+
const maxDelayMs = options?.maxDelayMs ?? DEFAULT_RETRY_MAX_DELAY_MS;
|
|
573
|
+
const baseDelay = options?.baseDelay ?? 500;
|
|
574
|
+
const retryAfter = response.headers.get("retry-after");
|
|
575
|
+
if (retryAfter) {
|
|
576
|
+
const seconds = Number(retryAfter);
|
|
577
|
+
if (Number.isFinite(seconds)) {
|
|
578
|
+
return seconds >= 0 ? Math.min(maxDelayMs, seconds * 1_000) : backoffDelay(attempt, baseDelay, maxDelayMs);
|
|
579
|
+
}
|
|
580
|
+
const date = Date.parse(retryAfter);
|
|
581
|
+
if (Number.isFinite(date))
|
|
582
|
+
return Math.min(maxDelayMs, Math.max(0, date - Date.now()));
|
|
583
|
+
}
|
|
584
|
+
return backoffDelay(attempt, baseDelay, maxDelayMs);
|
|
585
|
+
}
|
|
547
586
|
/**
|
|
548
587
|
* Fetch with retry and exponential backoff.
|
|
549
588
|
* Retries on network errors, 429, and 5xx responses.
|
|
550
|
-
* Honors Retry-After header
|
|
589
|
+
* Honors Retry-After header, capped at `maxDelayMs`.
|
|
551
590
|
*/
|
|
552
591
|
export async function fetchWithRetry(url, init, options) {
|
|
553
592
|
const maxRetries = options?.retries ?? 3;
|
|
554
593
|
const baseDelay = options?.baseDelay ?? 500;
|
|
594
|
+
const maxDelayMs = options?.maxDelayMs ?? DEFAULT_RETRY_MAX_DELAY_MS;
|
|
555
595
|
const timeout = options?.timeout ?? 30_000;
|
|
556
596
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
557
597
|
try {
|
|
558
598
|
const response = await fetchWithTimeout(url, init, timeout, init?.signal ?? undefined);
|
|
559
599
|
if (attempt < maxRetries && shouldRetry(response.status)) {
|
|
560
|
-
const
|
|
561
|
-
const delay = retryAfter ?? baseDelay * 2 ** attempt * (0.5 + Math.random() * 0.5);
|
|
600
|
+
const delay = computeRetryDelay(response, attempt, { baseDelay, maxDelayMs });
|
|
562
601
|
await response.body?.cancel().catch(() => undefined);
|
|
563
602
|
await abortableDelay(delay, init?.signal);
|
|
564
603
|
continue;
|
|
@@ -571,8 +610,7 @@ export async function fetchWithRetry(url, init, options) {
|
|
|
571
610
|
// A caller-supplied abort is terminal: never keep retrying past it.
|
|
572
611
|
if (init?.signal?.aborted)
|
|
573
612
|
throw err;
|
|
574
|
-
|
|
575
|
-
await abortableDelay(delay, init?.signal);
|
|
613
|
+
await abortableDelay(backoffDelay(attempt, baseDelay, maxDelayMs), init?.signal);
|
|
576
614
|
}
|
|
577
615
|
}
|
|
578
616
|
throw new Error("fetchWithRetry: unreachable");
|
|
@@ -584,17 +622,19 @@ export async function fetchWithRetry(url, init, options) {
|
|
|
584
622
|
* large. Sleeping it out with a bare `setTimeout` ignored the caller's abort
|
|
585
623
|
* signal entirely, so a single `429` could park an operation far past any
|
|
586
624
|
* deadline its caller believed it had imposed — the request timeout bounds
|
|
587
|
-
* only the request, never the wait between attempts.
|
|
625
|
+
* only the request, never the wait between attempts. Callers using this for
|
|
626
|
+
* a retry delay should pass a `maxDelayMs`-capped `ms` (see
|
|
627
|
+
* {@link computeRetryDelay}) to bound the wait itself.
|
|
588
628
|
*/
|
|
589
|
-
function abortableDelay(ms, signal) {
|
|
629
|
+
export function abortableDelay(ms, signal, abortMessage = "Aborted") {
|
|
590
630
|
if (!signal)
|
|
591
631
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
592
632
|
if (signal.aborted)
|
|
593
|
-
return Promise.reject(signal.reason ?? new Error(
|
|
633
|
+
return Promise.reject(signal.reason ?? new Error(abortMessage));
|
|
594
634
|
return new Promise((resolve, reject) => {
|
|
595
635
|
const onAbort = () => {
|
|
596
636
|
clearTimeout(timer);
|
|
597
|
-
reject(signal.reason ?? new Error(
|
|
637
|
+
reject(signal.reason ?? new Error(abortMessage));
|
|
598
638
|
};
|
|
599
639
|
const timer = setTimeout(() => {
|
|
600
640
|
signal.removeEventListener("abort", onAbort);
|
|
@@ -603,9 +643,6 @@ function abortableDelay(ms, signal) {
|
|
|
603
643
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
604
644
|
});
|
|
605
645
|
}
|
|
606
|
-
function shouldRetry(status) {
|
|
607
|
-
return status === 429 || status >= 500;
|
|
608
|
-
}
|
|
609
646
|
/**
|
|
610
647
|
* Read stdin as UTF-8 text if something is piped in. Returns `undefined`
|
|
611
648
|
* when stdin is a TTY (no pipe) or when the piped content is empty.
|
|
@@ -789,13 +826,6 @@ export async function jsonWithByteCap(response, maxBytes = DEFAULT_RESPONSE_BYTE
|
|
|
789
826
|
const text = await readBodyWithByteCap(response, maxBytes, limits);
|
|
790
827
|
return JSON.parse(text);
|
|
791
828
|
}
|
|
792
|
-
function parseRetryAfter(response) {
|
|
793
|
-
const header = response.headers.get("retry-after");
|
|
794
|
-
if (!header)
|
|
795
|
-
return undefined;
|
|
796
|
-
const seconds = parseInt(header, 10);
|
|
797
|
-
return Number.isNaN(seconds) ? undefined : seconds * 1000;
|
|
798
|
-
}
|
|
799
829
|
export function toErrorMessage(error) {
|
|
800
830
|
return error instanceof Error ? error.message : String(error);
|
|
801
831
|
}
|
|
@@ -871,8 +901,15 @@ export function stringArray(value) {
|
|
|
871
901
|
* reported alive. Treating it as dead let a lock held by a live process in a
|
|
872
902
|
* shared data dir (agent sandboxes, containers, service accounts — a
|
|
873
903
|
* configuration managed-db.ts explicitly supports) be reclaimed as stale.
|
|
904
|
+
*
|
|
905
|
+
* `pid` is `unknown` because callers reading it out of untrusted on-disk
|
|
906
|
+
* JSON (e.g. a lease file) cannot guarantee it parsed as a valid PID; a
|
|
907
|
+
* non-positive-integer value is reported dead without ever reaching
|
|
908
|
+
* `process.kill`.
|
|
874
909
|
*/
|
|
875
910
|
export function isProcessAlive(pid) {
|
|
911
|
+
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0)
|
|
912
|
+
return false;
|
|
876
913
|
try {
|
|
877
914
|
process.kill(pid, 0);
|
|
878
915
|
return true;
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import fs from "node:fs";
|
|
16
16
|
import path from "node:path";
|
|
17
17
|
import { sleepSync } from "../../runtime.js";
|
|
18
|
-
import {
|
|
18
|
+
import { readTextFile, stripJsonComments, writeFileAtomic } from "../common.js";
|
|
19
19
|
import { ConfigError } from "../errors.js";
|
|
20
20
|
import { createLockPayload, probeLock, reclaimStaleLock, releaseLock, tryAcquireLockSync } from "../file-lock.js";
|
|
21
21
|
import { getCacheDir, getConfigDir } from "../paths.js";
|
|
@@ -25,7 +25,7 @@ import { getCacheDir, getConfigDir } from "../paths.js";
|
|
|
25
25
|
*/
|
|
26
26
|
export function readConfigText(configPath) {
|
|
27
27
|
try {
|
|
28
|
-
return
|
|
28
|
+
return readTextFile(configPath, "Config file");
|
|
29
29
|
}
|
|
30
30
|
catch (err) {
|
|
31
31
|
if (err.code === "ENOENT")
|
|
@@ -23,6 +23,16 @@ export function bundleComponentConfig(bundle) {
|
|
|
23
23
|
* `defaultBundle` first, then map insertion order. Each entry's `name` is its
|
|
24
24
|
* bundle key. Returns `undefined` when no bundles map is configured.
|
|
25
25
|
*/
|
|
26
|
+
/**
|
|
27
|
+
* A bundle's true identity: its configured `path` plus its component's
|
|
28
|
+
* `root` (default `"."`), fully resolved. Two bundle entries whose bare
|
|
29
|
+
* `path` differs (relative vs. absolute, trailing slash, `~` vs. expanded)
|
|
30
|
+
* can still resolve to this same directory — this is the identity akm
|
|
31
|
+
* compares before registering or reconciling a bundle (issue #870).
|
|
32
|
+
*/
|
|
33
|
+
export function bundleContentRoot(entryPath, componentRoot) {
|
|
34
|
+
return path.resolve(entryPath, componentRoot ?? ".");
|
|
35
|
+
}
|
|
26
36
|
/**
|
|
27
37
|
* The resolved primary stash path — the `defaultBundle`'s filesystem `path`
|
|
28
38
|
* (spec §10.1) — or `undefined` when no filesystem primary is configured.
|
|
@@ -36,9 +46,9 @@ export function primaryBundlePath(config) {
|
|
|
36
46
|
if (!entry || typeof entry.path !== "string" || entry.path.length === 0)
|
|
37
47
|
return undefined;
|
|
38
48
|
const componentRoot = bundleComponentConfig(entry)?.root;
|
|
39
|
-
if (!componentRoot || componentRoot === ".")
|
|
40
|
-
return entry.path;
|
|
41
49
|
const bundleRoot = path.resolve(entry.path);
|
|
50
|
+
if (!componentRoot || componentRoot === ".")
|
|
51
|
+
return bundleRoot;
|
|
42
52
|
const resolved = path.resolve(bundleRoot, componentRoot);
|
|
43
53
|
const relative = path.relative(bundleRoot, resolved);
|
|
44
54
|
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
@@ -46,6 +56,26 @@ export function primaryBundlePath(config) {
|
|
|
46
56
|
}
|
|
47
57
|
return resolved;
|
|
48
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Every configured filesystem bundle's id paired with its resolved content
|
|
61
|
+
* root. Used to detect two bundle ids that resolve to the same directory
|
|
62
|
+
* (issue #870) and to find the id that already owns a given root before a
|
|
63
|
+
* new one is registered.
|
|
64
|
+
*/
|
|
65
|
+
export function bundleContentRoots(config) {
|
|
66
|
+
const bundles = config.bundles ?? {};
|
|
67
|
+
const out = [];
|
|
68
|
+
for (const [id, entry] of Object.entries(bundles)) {
|
|
69
|
+
if (typeof entry.path !== "string" || entry.path.length === 0)
|
|
70
|
+
continue;
|
|
71
|
+
out.push({ id, contentRoot: bundleContentRoot(entry.path, bundleComponentConfig(entry)?.root) });
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
/** The bundle id whose resolved content root already matches `resolvedContentRoot`, if any. */
|
|
76
|
+
export function bundleKeyForContentRoot(config, resolvedContentRoot) {
|
|
77
|
+
return bundleContentRoots(config).find((entry) => entry.contentRoot === resolvedContentRoot)?.id;
|
|
78
|
+
}
|
|
49
79
|
export function bundlesToSourceEntries(config) {
|
|
50
80
|
const bundles = config.bundles;
|
|
51
81
|
if (!bundles)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* The `configVersion` read shim (#863).
|
|
6
|
+
*
|
|
7
|
+
* `configVersion` used to be a hard `z.literal(CURRENT_CONFIG_VERSION)` gate:
|
|
8
|
+
* every value other than the exact current string threw
|
|
9
|
+
* `UNSUPPORTED_CONFIG_VERSION` for every command, since every akm invocation
|
|
10
|
+
* loads config first. That is the same shape of break #858/#859 (proposal
|
|
11
|
+
* rows), the `task_history` `metadataVersion` gate, and the task-source v2/v3
|
|
12
|
+
* gate each caused in 0.9.x — a version gate with no read shim — except
|
|
13
|
+
* `configVersion`'s blast radius is the whole CLI rather than one subsystem.
|
|
14
|
+
*
|
|
15
|
+
* This module mirrors the in-tree template for that fix,
|
|
16
|
+
* `src/tasks/source/parse-task-source.ts`'s v2/v3 -> v4 shim: a known-old
|
|
17
|
+
* version routes through a pure, in-memory upgrade function to the current
|
|
18
|
+
* shape, with a one-line stderr deprecation warning; the result is never
|
|
19
|
+
* written back to disk (the on-disk rewrite already happens for free — every
|
|
20
|
+
* `saveConfig`/`mutateConfig` write forces `configVersion` to
|
|
21
|
+
* {@link CURRENT_CONFIG_VERSION}, so the very next `akm config set` or any
|
|
22
|
+
* other mutating command silences the warning permanently). A version that is
|
|
23
|
+
* neither current nor a known old version — including anything NEWER than
|
|
24
|
+
* current — still fails closed with `UNSUPPORTED_CONFIG_VERSION`:
|
|
25
|
+
* forward-incompatibility is a real hazard (an older binary must not guess at
|
|
26
|
+
* a newer, unknown shape) and this shim does not soften that.
|
|
27
|
+
*
|
|
28
|
+
* IMPORTANT — as of this writing, `"0.9.0"` is the only `configVersion` akm
|
|
29
|
+
* has ever shipped; there is no real prior release to shim. `"0.0.1"` below
|
|
30
|
+
* is a SYNTHETIC placeholder entry that exists solely to stand up and
|
|
31
|
+
* exercise this mechanism — the known-versions list, the dispatch table, the
|
|
32
|
+
* warn-once-and-upgrade behavior, the fail-closed behavior for anything
|
|
33
|
+
* else — before a real bump ever needs it (see
|
|
34
|
+
* `tests/integration/config-version-shim.test.ts` and the
|
|
35
|
+
* `previous-release-corpus.test.ts` fixture). When the first genuine
|
|
36
|
+
* `configVersion` bump ships, add its real old shape as its own entry the
|
|
37
|
+
* same way and delete the synthetic `"0.0.1"` entry (and this paragraph) in
|
|
38
|
+
* the same change.
|
|
39
|
+
*/
|
|
40
|
+
import { ConfigError } from "../errors.js";
|
|
41
|
+
import { warn } from "../warn.js";
|
|
42
|
+
import { CURRENT_CONFIG_VERSION } from "./schema/primitives.js";
|
|
43
|
+
/**
|
|
44
|
+
* Every `configVersion` this binary can still READ, other than
|
|
45
|
+
* {@link CURRENT_CONFIG_VERSION} itself — each with an in-memory upgrade
|
|
46
|
+
* function in {@link CONFIG_VERSION_UPGRADES}. Anything not in this list (and
|
|
47
|
+
* not equal to current) fails closed.
|
|
48
|
+
*/
|
|
49
|
+
export const KNOWN_OLD_CONFIG_VERSIONS = ["0.0.1"];
|
|
50
|
+
function isKnownOldConfigVersion(value) {
|
|
51
|
+
return typeof value === "string" && KNOWN_OLD_CONFIG_VERSIONS.includes(value);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* SYNTHETIC 0.0.1 -> 0.9.0 upgrade (placeholder — see module doc). Per this
|
|
55
|
+
* placeholder, 0.0.1 kept the default LLM engine name at the config root as
|
|
56
|
+
* `defaultEngine`; 0.9.0 moved it under `defaults.llmEngine`. Pure function:
|
|
57
|
+
* takes the raw parsed JSON object, returns a new raw object with the 0.9.0
|
|
58
|
+
* shape. Never touches disk.
|
|
59
|
+
*/
|
|
60
|
+
function upgradeFrom080(raw) {
|
|
61
|
+
const { defaultEngine, defaults, ...rest } = raw;
|
|
62
|
+
if (typeof defaultEngine !== "string" || defaultEngine.length === 0) {
|
|
63
|
+
return { ...rest, ...(defaults !== undefined ? { defaults } : {}), configVersion: CURRENT_CONFIG_VERSION };
|
|
64
|
+
}
|
|
65
|
+
const existingDefaults = defaults !== null && typeof defaults === "object" ? defaults : {};
|
|
66
|
+
return {
|
|
67
|
+
...rest,
|
|
68
|
+
configVersion: CURRENT_CONFIG_VERSION,
|
|
69
|
+
// An explicit `defaults.llmEngine` already present in the raw 0.0.1
|
|
70
|
+
// document (should never happen for a real 0.0.1 file, but a malformed
|
|
71
|
+
// one is possible) wins over the root-level field being migrated in.
|
|
72
|
+
defaults: { llmEngine: defaultEngine, ...existingDefaults },
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const CONFIG_VERSION_UPGRADES = {
|
|
76
|
+
"0.0.1": upgradeFrom080,
|
|
77
|
+
};
|
|
78
|
+
function unsupportedConfigVersionError(rawVersion, sourcePath) {
|
|
79
|
+
const where = sourcePath ? ` at ${sourcePath}` : "";
|
|
80
|
+
const supported = [CURRENT_CONFIG_VERSION, ...KNOWN_OLD_CONFIG_VERSIONS].map((v) => `"${v}"`).join(", ");
|
|
81
|
+
return new ConfigError(`Unsupported configVersion${where}: got ${JSON.stringify(rawVersion)}, expected one of ${supported}.`, "UNSUPPORTED_CONFIG_VERSION", "Recreate engines and improve.strategies manually for AKM 0.9.0; profile-based configuration is not translated automatically.");
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Route a raw parsed config object through the version shim before schema
|
|
85
|
+
* validation. Returns a raw object whose `configVersion` is
|
|
86
|
+
* {@link CURRENT_CONFIG_VERSION} — either unchanged (already current),
|
|
87
|
+
* in-memory-upgraded (a known old version, with a one-line stderr warning),
|
|
88
|
+
* or this throws `UNSUPPORTED_CONFIG_VERSION` (unknown, newer, missing, or
|
|
89
|
+
* malformed).
|
|
90
|
+
*/
|
|
91
|
+
export function upgradeConfigVersion(raw, sourcePath) {
|
|
92
|
+
const version = raw.configVersion;
|
|
93
|
+
if (version === CURRENT_CONFIG_VERSION)
|
|
94
|
+
return raw;
|
|
95
|
+
if (isKnownOldConfigVersion(version)) {
|
|
96
|
+
const upgraded = CONFIG_VERSION_UPGRADES[version](raw);
|
|
97
|
+
warn(`Config${sourcePath ? ` at ${sourcePath}` : ""} uses configVersion "${version}" — auto-upgraded to ${CURRENT_CONFIG_VERSION} in memory; the next config write (e.g. \`akm config set\`) persists this and silences the warning.`);
|
|
98
|
+
return upgraded;
|
|
99
|
+
}
|
|
100
|
+
throw unsupportedConfigVersionError(version, sourcePath);
|
|
101
|
+
}
|
|
@@ -8,6 +8,7 @@ import { liftLegacyEngineExtraParams } from "../extra-params.js";
|
|
|
8
8
|
import { acquireConfigLock, backupExistingConfig, parseConfigText, readConfigText, withConfigLock, writeConfigAtomic, } from "./config-io.js";
|
|
9
9
|
import { AkmConfigSchema, CURRENT_CONFIG_VERSION } from "./config-schema.js";
|
|
10
10
|
import { bundlesToSourceEntries } from "./config-sources.js";
|
|
11
|
+
import { upgradeConfigVersion } from "./config-version-shim.js";
|
|
11
12
|
import { deepMergeConfig } from "./deep-merge.js";
|
|
12
13
|
export { stripJsonComments } from "./config-io.js";
|
|
13
14
|
import { getConfigPath } from "../paths.js";
|
|
@@ -138,14 +139,13 @@ export function acquireConfigReadFence() {
|
|
|
138
139
|
* Parse raw config text and validate via Zod.
|
|
139
140
|
* ({@link AkmConfigSchema}). Returns the merged-with-defaults AkmConfig.
|
|
140
141
|
*
|
|
141
|
-
* The schema accepts only the current config version
|
|
142
|
-
*
|
|
142
|
+
* The schema accepts only the current config version. A known older version
|
|
143
|
+
* is auto-upgraded in memory first (see `./config-version-shim`); anything
|
|
144
|
+
* else — including anything newer — is rejected before the canonical shape
|
|
145
|
+
* is validated.
|
|
143
146
|
*/
|
|
144
147
|
export function parseAndValidateConfigText(text, sourcePath) {
|
|
145
|
-
const parsedRaw = parseConfigText(text, sourcePath);
|
|
146
|
-
if (parsedRaw.configVersion !== CURRENT_CONFIG_VERSION) {
|
|
147
|
-
throw new ConfigError(`Unsupported configVersion${sourcePath ? ` at ${sourcePath}` : ""}: expected "${CURRENT_CONFIG_VERSION}".`, "UNSUPPORTED_CONFIG_VERSION", "Recreate engines and improve.strategies manually for AKM 0.9.0; profile-based configuration is not translated automatically.");
|
|
148
|
-
}
|
|
148
|
+
const parsedRaw = upgradeConfigVersion(parseConfigText(text, sourcePath), sourcePath);
|
|
149
149
|
// #852 (following #815): lift legacy `extraParams` keys — e.g.
|
|
150
150
|
// `reasoning_effort`, a documented 0.9.1 workaround — onto the first-class
|
|
151
151
|
// engine field they now shadow, before the protected-key check in
|
|
@@ -376,7 +376,7 @@ export function getIndexPassConfig(config, passName) {
|
|
|
376
376
|
return entry;
|
|
377
377
|
}
|
|
378
378
|
// Re-export source runtime helpers — implementation lives in config-sources.ts.
|
|
379
|
-
export { bundleComponentConfig, bundleEntryToSourceEntry, bundlesToSourceEntries, installedSourceDescriptor, parseSourceSpec, primaryBundlePath, resolveConfiguredSources, } from "./config-sources.js";
|
|
379
|
+
export { bundleComponentConfig, bundleContentRoot, bundleContentRoots, bundleEntryToSourceEntry, bundleKeyForContentRoot, bundlesToSourceEntries, installedSourceDescriptor, parseSourceSpec, primaryBundlePath, resolveConfiguredSources, } from "./config-sources.js";
|
|
380
380
|
/**
|
|
381
381
|
* Merge a partial user-config override onto a base config. Used by
|
|
382
382
|
* {@link loadUserConfig} (DEFAULT_CONFIG + on-disk) and {@link updateConfig}
|
|
@@ -15,7 +15,7 @@ import { z } from "zod";
|
|
|
15
15
|
// a config-schema ↔ config-types type cycle that collapses inference.
|
|
16
16
|
import { HARNESS_AGENT_DISPATCH_IDS, VALID_HARNESS_IDS } from "../../../integrations/harnesses/ids.js";
|
|
17
17
|
import { WORKFLOW_MAX_TIMEOUT_MS } from "../../../workflows/resource-limits.js";
|
|
18
|
-
import { chatCompletionsEndpoint, ENV_REFERENCE_PATTERN, ExtraParamsSchema, engineName,
|
|
18
|
+
import { chatCompletionsEndpoint, ENV_REFERENCE_PATTERN, ExtraParamsSchema, engineName, nonEmptyString, positiveInt, } from "./primitives.js";
|
|
19
19
|
/**
|
|
20
20
|
* Engine-config timeouts share the workflow ceiling.
|
|
21
21
|
*
|
|
@@ -43,16 +43,20 @@ export const LlmConnectionConfigSchema = z
|
|
|
43
43
|
maxTokens: positiveInt.optional(),
|
|
44
44
|
timeoutMs: timeoutMsField,
|
|
45
45
|
concurrency: positiveInt.optional(),
|
|
46
|
-
|
|
46
|
+
// User-settable override, not a cached probe verdict: attempt-then-
|
|
47
|
+
// fallback in llm/client.ts tries `response_format: json_schema` whenever
|
|
48
|
+
// a schema is supplied, degrading to plain text on an unsupported-4xx and
|
|
49
|
+
// remembering that in-memory for the rest of the process. `false` here
|
|
50
|
+
// opts a known-incompatible endpoint out of even the first attempt;
|
|
51
|
+
// `true` is advisory only.
|
|
52
|
+
supportsJsonSchema: z.boolean().optional(),
|
|
47
53
|
extraParams: ExtraParamsSchema.optional(),
|
|
48
54
|
contextLength: positiveInt.optional(),
|
|
49
55
|
enableThinking: z.boolean().optional(),
|
|
50
56
|
reasoningEffort: nonEmptyString.optional(),
|
|
51
57
|
})
|
|
52
58
|
.passthrough();
|
|
53
|
-
export const LlmProfileConfigSchema = LlmConnectionConfigSchema.
|
|
54
|
-
supportsJsonSchema: z.boolean().optional(),
|
|
55
|
-
}).passthrough();
|
|
59
|
+
export const LlmProfileConfigSchema = LlmConnectionConfigSchema.passthrough();
|
|
56
60
|
// ── Agent engines ───────────────────────────────────────────────────────────
|
|
57
61
|
// Derives from the canonical VALID_HARNESS_IDS (#565) so the Zod gate cannot
|
|
58
62
|
// drift from the TS union / parse check / setup detection.
|
|
@@ -68,7 +72,6 @@ const LlmEngineSchema = z
|
|
|
68
72
|
maxTokens: positiveInt.optional(),
|
|
69
73
|
timeoutMs: timeoutMsField,
|
|
70
74
|
concurrency: positiveInt.optional(),
|
|
71
|
-
supportsJsonSchema: z.boolean().optional(),
|
|
72
75
|
extraParams: ExtraParamsSchema.optional(),
|
|
73
76
|
contextLength: positiveInt.optional(),
|
|
74
77
|
enableThinking: z.boolean().optional(),
|
|
@@ -103,7 +106,6 @@ const AgentEngineSchema = z
|
|
|
103
106
|
"temperature",
|
|
104
107
|
"maxTokens",
|
|
105
108
|
"concurrency",
|
|
106
|
-
"supportsJsonSchema",
|
|
107
109
|
"extraParams",
|
|
108
110
|
"contextLength",
|
|
109
111
|
"enableThinking",
|
|
@@ -54,11 +54,6 @@ export const ExtraParamsSchema = z.record(z.unknown()).superRefine((value, ctx)
|
|
|
54
54
|
}
|
|
55
55
|
});
|
|
56
56
|
// ── Shared connection/invocation building blocks ────────────────────────────
|
|
57
|
-
export const LlmCapabilitiesSchema = z
|
|
58
|
-
.object({
|
|
59
|
-
structuredOutput: z.boolean().optional(),
|
|
60
|
-
})
|
|
61
|
-
.passthrough();
|
|
62
57
|
export const LlmInvocationOverridesSchema = z
|
|
63
58
|
.object({
|
|
64
59
|
temperature: z.number().finite().optional(),
|