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
|
@@ -207,6 +207,122 @@ export function spliceFrontmatterLine(raw, line) {
|
|
|
207
207
|
lines.splice(closeIdx, 0, line);
|
|
208
208
|
return lines.join("\n");
|
|
209
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* Strip one layer of matching quotes — frontmatter list items are often quoted
|
|
212
|
+
* refs. Written as an explicit char compare rather than a backreference regex
|
|
213
|
+
* on purpose: `scripts/lint-repository-sql.ts`'s comment/string stripper has no
|
|
214
|
+
* regex-literal awareness, so a literal holding an ODD number of quote
|
|
215
|
+
* characters desyncs its state machine and corrupts every match after it.
|
|
216
|
+
*/
|
|
217
|
+
function unquote(value) {
|
|
218
|
+
const trimmed = value.trim();
|
|
219
|
+
if (trimmed.length < 2)
|
|
220
|
+
return trimmed;
|
|
221
|
+
const first = trimmed[0];
|
|
222
|
+
if ((first === '"' || first === "'") && trimmed[trimmed.length - 1] === first)
|
|
223
|
+
return trimmed.slice(1, -1);
|
|
224
|
+
return trimmed;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Remove specific VALUES from one frontmatter list key, preserving every other
|
|
228
|
+
* byte — the counterpart to {@link spliceFrontmatterLine} for the
|
|
229
|
+
* `akm lint --prune-dangling-edges` repair (#884).
|
|
230
|
+
*
|
|
231
|
+
* Handles the three spellings a belief channel appears in: a block sequence
|
|
232
|
+
* (`contradictedBy:\n - a`), an inline flow (`contradictedBy: [a, b]`), and a
|
|
233
|
+
* bare scalar (`contradictedBy: a`). When every value under the key is removed
|
|
234
|
+
* the key itself goes too — an empty `contradictedBy: []` is not the same
|
|
235
|
+
* assertion as no edge at all.
|
|
236
|
+
*
|
|
237
|
+
* Returns the rewritten source, or `null` when `raw` has no well-formed
|
|
238
|
+
* frontmatter block or nothing matched, so the caller can leave the file
|
|
239
|
+
* untouched and report the finding unfixed. Deliberately source-preserving:
|
|
240
|
+
* these are user-authored memories, and a repair must not silently reformat
|
|
241
|
+
* the frontmatter it was not asked to touch.
|
|
242
|
+
*/
|
|
243
|
+
export function removeFrontmatterListValues(raw, key, values) {
|
|
244
|
+
const remove = new Set(values.map((v) => unquote(v)));
|
|
245
|
+
if (remove.size === 0)
|
|
246
|
+
return null;
|
|
247
|
+
const lines = raw.split(/\r?\n/);
|
|
248
|
+
if (lines[0]?.trim() !== "---")
|
|
249
|
+
return null;
|
|
250
|
+
const closeIdx = lines.findIndex((l, i) => i > 0 && l.trim() === "---");
|
|
251
|
+
if (closeIdx === -1)
|
|
252
|
+
return null;
|
|
253
|
+
const out = [];
|
|
254
|
+
let changed = false;
|
|
255
|
+
let index = 0;
|
|
256
|
+
while (index < lines.length) {
|
|
257
|
+
const line = lines[index];
|
|
258
|
+
if (index === 0 || index >= closeIdx) {
|
|
259
|
+
out.push(line);
|
|
260
|
+
index += 1;
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
const kv = line.match(/^(\w[\w-]*):\s*(.*)$/);
|
|
264
|
+
if (kv === null || kv[1] !== key) {
|
|
265
|
+
out.push(line);
|
|
266
|
+
index += 1;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
const rest = kv[2].trim();
|
|
270
|
+
// Inline flow: contradictedBy: [a, b]
|
|
271
|
+
const flow = rest.match(/^\[(.*)\]$/);
|
|
272
|
+
if (flow !== null) {
|
|
273
|
+
const kept = flow[1]
|
|
274
|
+
.split(",")
|
|
275
|
+
.map((item) => item.trim())
|
|
276
|
+
.filter(Boolean)
|
|
277
|
+
.filter((item) => !remove.has(unquote(item)));
|
|
278
|
+
const original = flow[1].split(",").filter((s) => s.trim().length > 0).length;
|
|
279
|
+
if (kept.length !== original) {
|
|
280
|
+
changed = true;
|
|
281
|
+
if (kept.length > 0)
|
|
282
|
+
out.push(`${key}: [${kept.join(", ")}]`);
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
out.push(line);
|
|
286
|
+
}
|
|
287
|
+
index += 1;
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
// Bare scalar: contradictedBy: a
|
|
291
|
+
if (rest !== "") {
|
|
292
|
+
if (remove.has(unquote(rest)))
|
|
293
|
+
changed = true;
|
|
294
|
+
else
|
|
295
|
+
out.push(line);
|
|
296
|
+
index += 1;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
// Block sequence: the key line, then ` - value` items.
|
|
300
|
+
const header = line;
|
|
301
|
+
const items = [];
|
|
302
|
+
let cursor = index + 1;
|
|
303
|
+
while (cursor < closeIdx) {
|
|
304
|
+
const itemMatch = lines[cursor].match(/^\s+-\s*(.*)$/);
|
|
305
|
+
if (itemMatch === null)
|
|
306
|
+
break;
|
|
307
|
+
items.push(lines[cursor]);
|
|
308
|
+
cursor += 1;
|
|
309
|
+
}
|
|
310
|
+
const kept = items.filter((item) => !remove.has(unquote(item.replace(/^\s*-\s*/, ""))));
|
|
311
|
+
if (kept.length !== items.length) {
|
|
312
|
+
changed = true;
|
|
313
|
+
if (kept.length > 0) {
|
|
314
|
+
out.push(header);
|
|
315
|
+
out.push(...kept);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
out.push(header);
|
|
320
|
+
out.push(...items);
|
|
321
|
+
}
|
|
322
|
+
index = cursor;
|
|
323
|
+
}
|
|
324
|
+
return changed ? out.join("\n") : null;
|
|
325
|
+
}
|
|
210
326
|
/**
|
|
211
327
|
* Parse a YAML scalar value (string, boolean, or number).
|
|
212
328
|
*
|
|
@@ -0,0 +1,97 @@
|
|
|
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 memory-cleanup archive as a REF-RESOLUTION surface (#884).
|
|
6
|
+
*
|
|
7
|
+
* `analyzeMemoryCleanup`'s prune (`commands/improve/memory/memory-improve.ts`
|
|
8
|
+
* `#archiveMemory`) does not delete a memory: it `rename`s the file under
|
|
9
|
+
* `.akm/memory-cleanup/archive/<stamp>-<ref>/<originalPath>` and writes a
|
|
10
|
+
* sibling `cleanup.md` audit asset carrying `ref` / `originalPath` /
|
|
11
|
+
* `archivedPath`. The bytes and the identity both survive — but the ref stops
|
|
12
|
+
* resolving at its ORIGINAL location, so every inbound belief edge
|
|
13
|
+
* (`contradictedBy` / `supersededBy`) pointing at the pruned memory becomes a
|
|
14
|
+
* `missing-ref` the moment #882 made those channels validatable.
|
|
15
|
+
*
|
|
16
|
+
* That is the #884 defect, and the archive already holds everything needed to
|
|
17
|
+
* fix it: the audit record IS a tombstone. This module reads those tombstones
|
|
18
|
+
* so ref resolution can answer "archived" instead of "missing". Resolution
|
|
19
|
+
* stays non-destructive — pruning never rewrites an unrelated memory's
|
|
20
|
+
* frontmatter, and the contradiction an edge records is preserved rather than
|
|
21
|
+
* erased (the concern #884 raised against a bare edge-scrub).
|
|
22
|
+
*
|
|
23
|
+
* A ref whose target has NO tombstone and no file is genuinely dangling — it
|
|
24
|
+
* was removed by something other than prune (a hand `git rm`, an older
|
|
25
|
+
* release). Those stay reported; clearing them mutates user data and so is
|
|
26
|
+
* gated behind `akm lint --prune-dangling-edges`.
|
|
27
|
+
*/
|
|
28
|
+
import fs from "node:fs";
|
|
29
|
+
import path from "node:path";
|
|
30
|
+
import { parseFrontmatter } from "./frontmatter.js";
|
|
31
|
+
/** Stash-relative root the prune path archives into. Must match `memory-improve.ts#createArchiveDir`. */
|
|
32
|
+
export const MEMORY_ARCHIVE_REL = ".akm/memory-cleanup/archive";
|
|
33
|
+
/** Filename of the per-archive audit asset written alongside the archived memory. */
|
|
34
|
+
const AUDIT_FILENAME = "cleanup.md";
|
|
35
|
+
/**
|
|
36
|
+
* Cache keyed by stash root. A lint sweep resolves thousands of refs against a
|
|
37
|
+
* directory that only the prune path ever writes, so the scan runs once per
|
|
38
|
+
* root instead of once per missing ref.
|
|
39
|
+
*/
|
|
40
|
+
const cache = new Map();
|
|
41
|
+
/** @internal Drop the memoized scans — process-global state needs a reset seam for tests (#785). */
|
|
42
|
+
export function resetMemoryArchiveCache() {
|
|
43
|
+
cache.clear();
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Every stash-relative `originalPath` archived under `root`, i.e. the set of
|
|
47
|
+
* paths that USED to hold a memory and now hold a tombstone instead.
|
|
48
|
+
*
|
|
49
|
+
* Unreadable or malformed audit records are skipped rather than thrown: a
|
|
50
|
+
* corrupt tombstone must degrade to "this ref is missing" (the pre-#884
|
|
51
|
+
* answer), never break the whole lint sweep.
|
|
52
|
+
*/
|
|
53
|
+
export function archivedOriginalPaths(root) {
|
|
54
|
+
const cached = cache.get(root);
|
|
55
|
+
if (cached !== undefined)
|
|
56
|
+
return cached;
|
|
57
|
+
const paths = new Set();
|
|
58
|
+
const archiveRoot = path.join(root, MEMORY_ARCHIVE_REL);
|
|
59
|
+
let entries;
|
|
60
|
+
try {
|
|
61
|
+
entries = fs.readdirSync(archiveRoot, { withFileTypes: true });
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
cache.set(root, paths); // no archive dir — nothing was ever pruned here
|
|
65
|
+
return paths;
|
|
66
|
+
}
|
|
67
|
+
for (const entry of entries) {
|
|
68
|
+
if (!entry.isDirectory())
|
|
69
|
+
continue;
|
|
70
|
+
let raw;
|
|
71
|
+
try {
|
|
72
|
+
raw = fs.readFileSync(path.join(archiveRoot, entry.name, AUDIT_FILENAME), "utf8");
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
let originalPath;
|
|
78
|
+
try {
|
|
79
|
+
originalPath = parseFrontmatter(raw).data.originalPath;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (typeof originalPath === "string" && originalPath.trim().length > 0) {
|
|
85
|
+
paths.add(originalPath.trim().replace(/\\/g, "/"));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
cache.set(root, paths);
|
|
89
|
+
return paths;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* True when `relPath` (stash-relative, POSIX) names a memory that prune
|
|
93
|
+
* archived — the ref resolves to a tombstone rather than to nothing.
|
|
94
|
+
*/
|
|
95
|
+
export function isArchivedRelPath(relPath, root) {
|
|
96
|
+
return archivedOriginalPaths(root).has(relPath.replace(/\\/g, "/"));
|
|
97
|
+
}
|
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)
|
|
@@ -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(),
|
|
@@ -16,7 +16,8 @@ const SearchGraphBoostSchema = z
|
|
|
16
16
|
hopBoostCap: nonNegativeNumber.optional(),
|
|
17
17
|
/** Hard-capped at 3; values > 3 hard-error so users see the typo. */
|
|
18
18
|
maxHops: positiveInt.max(3).optional(),
|
|
19
|
-
|
|
19
|
+
/** Only "blend" is exercised; "off"/"multiply" were never set in practice and were removed. */
|
|
20
|
+
confidenceMode: z.enum(["blend"]).default("blend").optional(),
|
|
20
21
|
/** Range [0, 1]; values > 1 hard-error (no silent clamp). */
|
|
21
22
|
confidenceWeight: z.number().finite().min(0).max(1).default(0.2).optional(),
|
|
22
23
|
})
|
package/dist/core/file-lock.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
5
|
import fs from "node:fs";
|
|
6
6
|
import path from "node:path";
|
|
7
|
+
import { sleepSync } from "../runtime.js";
|
|
7
8
|
import { openDatabase } from "../storage/database.js";
|
|
8
9
|
import { isProcessAlive, MAX_LOCK_METADATA_BYTES, readTextFileDescriptorWithLimit } from "./common.js";
|
|
9
10
|
function readLockSnapshot(lockPath) {
|
|
@@ -51,7 +52,7 @@ function withLockOperationMutex(lockPath, run) {
|
|
|
51
52
|
db.exec("BEGIN IMMEDIATE");
|
|
52
53
|
began = db.inTransaction;
|
|
53
54
|
if (!began)
|
|
54
|
-
|
|
55
|
+
sleepSync(2 ** attempt);
|
|
55
56
|
}
|
|
56
57
|
if (!began)
|
|
57
58
|
throw new Error(`Could not acquire lock operation mutex for ${lockPath}.`);
|
|
@@ -5,6 +5,7 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
6
|
import fs from "node:fs";
|
|
7
7
|
import path from "node:path";
|
|
8
|
+
import { sleepSync } from "../runtime.js";
|
|
8
9
|
import { ConfigError } from "./errors.js";
|
|
9
10
|
import { createLockPayload, probeLock, reclaimStaleLock, releaseLock, tryAcquireLockSync } from "./file-lock.js";
|
|
10
11
|
import { getMaintenanceBarrierPath } from "./paths.js";
|
|
@@ -93,7 +94,7 @@ function withMaintenanceStartBarrierSyncWait(run) {
|
|
|
93
94
|
const deadline = Date.now() + 5_000;
|
|
94
95
|
let release = tryAcquireMaintenanceBarrier();
|
|
95
96
|
while (!release && Date.now() < deadline) {
|
|
96
|
-
|
|
97
|
+
sleepSync(5);
|
|
97
98
|
release = tryAcquireMaintenanceBarrier();
|
|
98
99
|
}
|
|
99
100
|
if (!release)
|
|
@@ -107,19 +108,6 @@ function withMaintenanceStartBarrierSyncWait(run) {
|
|
|
107
108
|
release();
|
|
108
109
|
}
|
|
109
110
|
}
|
|
110
|
-
/** Register a long-lived state operation atomically with other start acquisitions. */
|
|
111
|
-
export async function acquireMaintenanceActivity(name) {
|
|
112
|
-
return withMaintenanceStartBarrierAsync(async () => {
|
|
113
|
-
const directory = path.join(path.dirname(getMaintenanceBarrierPath()), "maintenance-activities");
|
|
114
|
-
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
115
|
-
const lockPath = path.join(directory, `${name}-${process.pid}-${randomUUID()}.lock`);
|
|
116
|
-
const ownership = tryAcquireLockSync(lockPath, createLockPayload({ purpose: name }));
|
|
117
|
-
if (!ownership) {
|
|
118
|
-
throw new ConfigError(`Could not register AKM maintenance activity at ${lockPath}.`, "INVALID_CONFIG_FILE");
|
|
119
|
-
}
|
|
120
|
-
return () => releaseLock(ownership);
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
111
|
/** Synchronous activity registration for synchronous database handle lifetimes. */
|
|
124
112
|
export function acquireMaintenanceActivitySync(name) {
|
|
125
113
|
return withMaintenanceStartBarrierSyncWait(() => {
|
package/dist/core/paths.js
CHANGED
|
@@ -251,9 +251,6 @@ export function getLockfilePath() {
|
|
|
251
251
|
export function getLockfileLockPath() {
|
|
252
252
|
return path.join(getDataDir(), "akm.lock.lck");
|
|
253
253
|
}
|
|
254
|
-
export function getSemanticStatusPath() {
|
|
255
|
-
return path.join(getCacheDir(), "semantic-status.json");
|
|
256
|
-
}
|
|
257
254
|
export function getRegistryCacheDir() {
|
|
258
255
|
return path.join(getCacheDir(), "registry");
|
|
259
256
|
}
|
package/dist/core/redaction.js
CHANGED
|
@@ -373,11 +373,11 @@ const ERROR_BODY_MAX_LEN = 200;
|
|
|
373
373
|
* Make an HTTP error body safe to put in an error message: pattern-redact
|
|
374
374
|
* credential shapes, then clip. Provider bodies can echo the credential that
|
|
375
375
|
* was sent and can be megabytes of HTML, and these messages travel — into
|
|
376
|
-
*
|
|
376
|
+
* `--json` output and agent transcripts.
|
|
377
377
|
*
|
|
378
378
|
* Lives here rather than beside one transport because every HTTP client in the
|
|
379
379
|
* codebase needs it; the embeddings transport originally lacked it and leaked
|
|
380
|
-
* raw 10 MB bodies into
|
|
380
|
+
* raw 10 MB bodies into error messages.
|
|
381
381
|
*/
|
|
382
382
|
export function redactErrorBody(input) {
|
|
383
383
|
if (!input)
|