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.
Files changed (130) hide show
  1. package/CHANGELOG.md +295 -0
  2. package/dist/assets/hints/cli-hints-full.md +3 -3
  3. package/dist/assets/improve-strategies/catchup.json +40 -11
  4. package/dist/assets/improve-strategies/thorough.json +45 -7
  5. package/dist/assets/tasks/improve/akm-improve-frequent.yml +2 -2
  6. package/dist/commands/agent/contribute-cli.js +11 -0
  7. package/dist/commands/env/env-cli.js +1 -2
  8. package/dist/commands/env/secret-cli.js +1 -5
  9. package/dist/commands/feedback-cli.js +0 -4
  10. package/dist/commands/health/checks.js +0 -32
  11. package/dist/commands/health/surfaces.js +2 -2
  12. package/dist/commands/health.js +6 -15
  13. package/dist/commands/improve/autonomy-gate.js +1 -1
  14. package/dist/commands/improve/consolidate.js +25 -26
  15. package/dist/commands/improve/distill.js +2 -2
  16. package/dist/commands/improve/extract.js +8 -1
  17. package/dist/commands/improve/improve-cli.js +1 -1
  18. package/dist/commands/improve/improve-strategies.js +0 -4
  19. package/dist/commands/improve/improve.js +3 -5
  20. package/dist/commands/improve/locks.js +13 -5
  21. package/dist/commands/improve/loop-stages.js +8 -9
  22. package/dist/commands/improve/memory/memory-contradiction-detect.js +1 -19
  23. package/dist/commands/improve/memory/memory-improve.js +2 -1
  24. package/dist/commands/improve/outcome-loop.js +0 -6
  25. package/dist/commands/improve/preparation.js +1 -1
  26. package/dist/commands/improve/reflect.js +21 -10
  27. package/dist/commands/lint/base-linter.js +141 -18
  28. package/dist/commands/lint/index.js +21 -10
  29. package/dist/commands/proposal/validators/proposal-quality-validators.js +0 -7
  30. package/dist/commands/read/curate.js +47 -0
  31. package/dist/commands/read/search-cli.js +24 -1
  32. package/dist/commands/sources/bundle-config-ops.js +9 -4
  33. package/dist/commands/sources/info.js +14 -10
  34. package/dist/commands/tasks/tasks.js +12 -16
  35. package/dist/commands/workflow-cli.js +2 -2
  36. package/dist/core/asset/asset-placement.js +13 -2
  37. package/dist/core/asset/frontmatter.js +116 -0
  38. package/dist/core/asset/memory-archive.js +97 -0
  39. package/dist/core/common.js +61 -24
  40. package/dist/core/config/config-io.js +2 -2
  41. package/dist/core/config/config-sources.js +32 -2
  42. package/dist/core/config/config.js +1 -1
  43. package/dist/core/config/engine-semantics.js +0 -2
  44. package/dist/core/config/schema/engines.js +9 -7
  45. package/dist/core/config/schema/primitives.js +0 -5
  46. package/dist/core/config/schema/search.js +2 -1
  47. package/dist/core/file-lock.js +2 -1
  48. package/dist/core/maintenance-barrier.js +2 -14
  49. package/dist/core/paths.js +0 -3
  50. package/dist/core/redaction.js +2 -2
  51. package/dist/core/spawn-env.js +8 -12
  52. package/dist/core/state/migrations.js +1 -12
  53. package/dist/core/state-db.js +9 -27
  54. package/dist/core/write-source.js +12 -19
  55. package/dist/execution/directory-identity.js +36 -10
  56. package/dist/indexer/graph/graph-boost.js +0 -4
  57. package/dist/indexer/index-writer-lock.js +43 -24
  58. package/dist/indexer/index-written-assets.js +5 -6
  59. package/dist/indexer/indexer.js +2 -39
  60. package/dist/indexer/materialize-embeddings.js +85 -41
  61. package/dist/indexer/search/db-search.js +15 -48
  62. package/dist/indexer/search/ranking-contributors.js +0 -25
  63. package/dist/indexer/search/ranking.js +3 -13
  64. package/dist/integrations/agent/builder-shared.js +0 -25
  65. package/dist/integrations/agent/model-map.js +2 -60
  66. package/dist/integrations/harnesses/aider/agent-builder.js +1 -3
  67. package/dist/integrations/harnesses/amazonq/agent-builder.js +2 -7
  68. package/dist/integrations/harnesses/claude/agent-builder.js +1 -4
  69. package/dist/integrations/harnesses/codex/agent-builder.js +1 -4
  70. package/dist/integrations/harnesses/copilot/agent-builder.js +2 -6
  71. package/dist/integrations/harnesses/gemini/agent-builder.js +2 -7
  72. package/dist/integrations/harnesses/opencode/agent-builder.js +1 -4
  73. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +2 -16
  74. package/dist/integrations/harnesses/openhands/agent-builder.js +1 -3
  75. package/dist/integrations/harnesses/pi/agent-builder.js +2 -4
  76. package/dist/integrations/session-logs/index.js +0 -9
  77. package/dist/llm/client.js +75 -42
  78. package/dist/llm/embedder.js +7 -3
  79. package/dist/llm/embedders/remote.js +141 -42
  80. package/dist/registry/network.js +5 -37
  81. package/dist/runtime.js +2 -10
  82. package/dist/scripts/akm-migrate-node.js +73 -133
  83. package/dist/scripts/akm-migrate.js +73 -133
  84. package/dist/setup/engine-config.js +2 -5
  85. package/dist/setup/registry-stash-loader.js +0 -8
  86. package/dist/setup/setup.js +9 -46
  87. package/dist/setup/steps/connection-shared.js +10 -13
  88. package/dist/sources/providers/git-install.js +1 -1
  89. package/dist/sources/snapshot-fetchers/website-ingest.js +126 -0
  90. package/dist/storage/engines/sqlite-migrations.js +20 -1
  91. package/dist/storage/repositories/index-connection.js +45 -3
  92. package/dist/storage/repositories/index-entries-repository.js +0 -15
  93. package/dist/tasks/backends/cron.js +49 -9
  94. package/dist/tasks/backends/launchd.js +15 -20
  95. package/dist/tasks/backends/schtasks.js +18 -8
  96. package/dist/tasks/resolve-akm-bin.js +17 -2
  97. package/dist/tasks/run/run-native-task.js +8 -6
  98. package/dist/tasks/scheduler-invocation.js +8 -1
  99. package/dist/tasks/source/bounded-document.js +2 -4
  100. package/dist/tasks/source/parse-task-source.js +23 -9
  101. package/dist/tasks/source/task-source-v3-frozen.js +5 -7
  102. package/dist/tasks/source/task-source-v4.js +5 -10
  103. package/dist/tasks/source/task-to-v3.js +5 -10
  104. package/dist/tasks/source/task-to-v4.js +1 -4
  105. package/dist/tasks/source-v3.js +6 -6
  106. package/dist/workflows/exec/native-executor.js +21 -31
  107. package/dist/workflows/exec/run-workflow.js +5 -6
  108. package/dist/workflows/exec/scheduler.js +3 -19
  109. package/dist/workflows/exec/step-work.js +1 -4
  110. package/dist/workflows/exec/unit-dispatch.js +2 -2
  111. package/dist/workflows/exec/worktree.js +1 -13
  112. package/dist/workflows/freeze/targets/child-workflow.js +2 -10
  113. package/dist/workflows/ir/plan-hash.js +4 -6
  114. package/dist/workflows/ir/schema-v4.js +0 -12
  115. package/dist/workflows/ir/schema.js +20 -31
  116. package/dist/workflows/parser.js +11 -52
  117. package/dist/workflows/renderer.js +2 -3
  118. package/dist/workflows/resource-limits.js +11 -41
  119. package/dist/workflows/runtime/runs.js +3 -4
  120. package/dist/workflows/source-ir/schema.js +14 -30
  121. package/dist/workflows/validator.js +1 -7
  122. package/docs/reference/cli.md +5 -1
  123. package/docs/reference/configuration.md +1 -1
  124. package/package.json +1 -1
  125. package/schemas/akm-config.json +4 -23
  126. package/schemas/akm-task.json +1 -2
  127. package/schemas/akm-workflow.json +1 -13
  128. package/dist/assets/improve-strategies/frequent.json +0 -15
  129. package/dist/assets/improve-strategies/memory-focus.json +0 -15
  130. 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
+ }
@@ -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 readTextFileWithLimit(filePath, maxBytes, label = "File") {
52
+ export function readTextFile(filePath, label = "File") {
55
53
  const fd = fs.openSync(filePath, "r");
56
54
  try {
57
- return readTextFileDescriptorWithLimit(fd, maxBytes, label, filePath);
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 = readTextFileWithLimit(configPath, MAX_CONFIG_FILE_BYTES, "Config file");
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 for 429 responses.
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 retryAfter = parseRetryAfter(response);
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
- const delay = baseDelay * 2 ** attempt * (0.5 + Math.random() * 0.5);
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("Aborted"));
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("Aborted"));
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 { MAX_CONFIG_FILE_BYTES, readTextFileWithLimit, stripJsonComments, writeFileAtomic } from "../common.js";
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 readTextFileWithLimit(configPath, MAX_CONFIG_FILE_BYTES, "Config file");
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}
@@ -6,9 +6,7 @@ export const BUILTIN_IMPROVE_STRATEGY_NAMES = [
6
6
  "default",
7
7
  "quick",
8
8
  "thorough",
9
- "memory-focus",
10
9
  "graph-refresh",
11
- "frequent",
12
10
  "consolidate",
13
11
  "catchup",
14
12
  "reflect-distill",
@@ -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, LlmCapabilitiesSchema, nonEmptyString, positiveInt, } from "./primitives.js";
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
- capabilities: LlmCapabilitiesSchema.optional(),
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.extend({
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
- confidenceMode: z.enum(["off", "blend", "multiply"]).default("blend").optional(),
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
  })
@@ -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
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 2 ** attempt);
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
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5);
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(() => {
@@ -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
  }
@@ -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
- * persisted status files, `--json` output, and agent transcripts.
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 `semantic-status.json`.
380
+ * raw 10 MB bodies into error messages.
381
381
  */
382
382
  export function redactErrorBody(input) {
383
383
  if (!input)