akm-cli 0.9.15-beta.3 → 0.9.15-beta.5

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.
@@ -12,6 +12,7 @@ import { purgeEmbeddingSalvage, relabelEmbeddingSalvageFingerprint, reuseSalvage
12
12
  import { getEmbeddableEntryCount } from "../storage/repositories/index-entries-repository.js";
13
13
  import { deleteMeta, getMeta, setMeta } from "../storage/repositories/index-meta-repository.js";
14
14
  import { getAllEntriesForEmbedding, getEmbeddingCount, isVecFastPathComplete, isVecFastPathReady, purgeEmbeddings, sampleEmbeddedEntriesForCanary, setVecFastPathReady, upsertEmbedding, } from "../storage/repositories/index-vec-repository.js";
15
+ import { reclassifyIndexDbContention } from "./index-db-contention.js";
15
16
  /** Identifies the embedding provider+model+dimension a stored vector was generated with. */
16
17
  export function deriveSemanticProviderFingerprint(embedding) {
17
18
  if (isDeterministicEmbedEnabled()) {
@@ -732,7 +733,20 @@ export async function generateEmbeddingsForDb(db, config, onProgress, signal, en
732
733
  }
733
734
  }
734
735
  catch (error) {
735
- const message = error instanceof Error ? error.message : String(error);
736
+ // Field follow-up to #956 (dev-team field review 2026-09-10): a
737
+ // contention-shaped error (another akm process writing index.db right
738
+ // now) used to escape this catch as a raw driver string ("database is
739
+ // locked"), reaching this user-facing message unclassified even though
740
+ // the acquisition-time path (`akmIndex`'s outer catch) already
741
+ // reclassifies the same shape into `TransientError("INDEX_DB_CONTENDED")`.
742
+ // Reuses that ONE shared classifier rather than a second one — see
743
+ // `index-db-contention.ts`. This catch stays non-fatal (a caller sees
744
+ // `success: false` and a message, never a thrown error): the run
745
+ // continues through the remaining index phases exactly as it did
746
+ // before, only the message is now classified when the error is
747
+ // contention-shaped.
748
+ const reclassified = reclassifyIndexDbContention(error);
749
+ const message = reclassified instanceof Error ? reclassified.message : String(reclassified);
736
750
  warn("Embedding generation failed, continuing without:", message);
737
751
  onProgress({ phase: "embeddings", message: `Embedding generation failed: ${message}` });
738
752
  return {
@@ -26,6 +26,18 @@ let standaloneInstalledModelMapText;
26
26
  const ENGINE_KEY_PATTERN = new RegExp(ENGINE_NAME_PATTERN_SOURCE);
27
27
  const ALIAS_KEY_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
28
28
  const RESERVED_MAP_KEYS = new Set(["__proto__", "constructor", "prototype", "tostring"]);
29
+ /**
30
+ * A profile's own field names (#946). When one of these appears directly
31
+ * under an alias — e.g. `"fast": { "engine": "local-fast" }` — it is not a
32
+ * per-platform column named `model`/`inference`/`engine`; it is a wildcard
33
+ * default profile applied to every platform column of that alias, exactly
34
+ * the flat shorthand the original proposal asked for. A nested per-platform
35
+ * entry in the same alias (`"fast": { "claude": { ... } }`) still overrides
36
+ * the wildcard for that one column, so both forms compose.
37
+ */
38
+ const PROFILE_FIELD_KEYS = new Set(["model", "inference", "engine"]);
39
+ /** Internal key the wildcard default profile is stored under (never user-writable directly). */
40
+ export const WILDCARD_ENGINE_KEY = "*";
29
41
  function assertSafeMapKey(key, source, jsonPath) {
30
42
  if (RESERVED_MAP_KEYS.has(key.toLowerCase())) {
31
43
  invalid(source, jsonPath, "reserved prototype-like key is not allowed");
@@ -137,7 +149,18 @@ export function parseModelMapLayer(text, source) {
137
149
  }
138
150
  const engines = [];
139
151
  const enginesSeen = new Map();
140
- for (const [rawEngine, rawProfile] of Object.entries(engineRecord)) {
152
+ const wildcardFields = {};
153
+ const platformEntries = [];
154
+ for (const [rawKey, rawValue] of Object.entries(engineRecord)) {
155
+ if (PROFILE_FIELD_KEYS.has(rawKey))
156
+ wildcardFields[rawKey] = rawValue;
157
+ else
158
+ platformEntries.push([rawKey, rawValue]);
159
+ }
160
+ if (Object.keys(wildcardFields).length > 0) {
161
+ engines.push([WILDCARD_ENGINE_KEY, parseProfileLayer(wildcardFields, source, `$.aliases.${rawAlias}`)]);
162
+ }
163
+ for (const [rawEngine, rawProfile] of platformEntries) {
141
164
  const engine = rawEngine.toLowerCase();
142
165
  assertSafeMapKey(engine, source, `$.aliases.${rawAlias}.${rawEngine}`);
143
166
  if (!ENGINE_KEY_PATTERN.test(engine)) {
@@ -238,6 +261,18 @@ function mergeRawProfileLayers(installed, user) {
238
261
  const apply = (layer) => {
239
262
  for (const [alias, layerEngines] of Object.entries(layer.aliases)) {
240
263
  const mergedEngines = aliases.get(alias) ?? new Map();
264
+ const wildcard = ownValue(layerEngines, WILDCARD_ENGINE_KEY);
265
+ if (wildcard !== undefined) {
266
+ // This layer's wildcard default (#946) overlays every platform column
267
+ // already accumulated from earlier layers, before this layer's own
268
+ // explicit per-platform entries (applied below) get a chance to
269
+ // override it for their one column.
270
+ for (const [engineKey, profile] of mergedEngines) {
271
+ if (engineKey === WILDCARD_ENGINE_KEY)
272
+ continue;
273
+ mergedEngines.set(engineKey, mergeProfiles(profile, wildcard));
274
+ }
275
+ }
241
276
  for (const [engineKey, profile] of Object.entries(layerEngines)) {
242
277
  mergedEngines.set(engineKey, mergeProfiles(mergedEngines.get(engineKey), profile));
243
278
  }
@@ -316,7 +351,9 @@ export function resolveModelMapAlias(input, engine, map) {
316
351
  const alias = input.toLowerCase();
317
352
  const selectedEngine = engine.toLowerCase();
318
353
  const tier = ownValue(map.aliases, alias);
319
- const profile = ownValue(tier, selectedEngine);
354
+ // A specific platform column always wins; the alias-level wildcard default
355
+ // (#946) is only a fallback for a column no layer ever set explicitly.
356
+ const profile = ownValue(tier, selectedEngine) ?? ownValue(tier, WILDCARD_ENGINE_KEY);
320
357
  if (profile !== undefined)
321
358
  return selectionFromProfile(input, profile);
322
359
  const knownAliasUnmappedForEngine = tier !== undefined;
@@ -472,3 +472,21 @@ export async function probeLlmEndpoint(config, timeoutMs = 3_000) {
472
472
  return { reachable: false, error: err instanceof Error ? err.message : String(err) };
473
473
  }
474
474
  }
475
+ /**
476
+ * Endpoint-keyed probe memoization (#957): every caller that probes several
477
+ * engine connections in one pass (`akm health`'s engine checks, `akm
478
+ * improve --require-engines`) shares one in-flight probe per distinct
479
+ * endpoint (trailing slashes normalized) instead of firing a duplicate probe
480
+ * when two engines point at the same server. `cache` must be scoped to one
481
+ * invocation and never shared across calls — a stale "reachable" surviving
482
+ * past the run that produced it is the failure mode this exists to avoid.
483
+ */
484
+ export function probeEndpointOnce(connection, cache, probe) {
485
+ const key = connection.endpoint.replace(/\/+$/, "");
486
+ let pending = cache.get(key);
487
+ if (!pending) {
488
+ pending = probe(connection);
489
+ cache.set(key, pending);
490
+ }
491
+ return pending;
492
+ }
@@ -9,6 +9,10 @@ const FEATURE_LOCATION = {
9
9
  memory_inference: (cfg) => cfg.index?.memory?.enabled ?? true,
10
10
  graph_extraction: (cfg) => cfg.index?.graph?.enabled ?? true,
11
11
  metadata_enhance: (cfg) => cfg.index?.metadataEnhance?.enabled ?? false,
12
+ // #951: a real implementation of the dead `curate_rerank` key removed in
13
+ // 0.8.0. Off by default — it requires a `search.curateRerank.endpoint` a
14
+ // caller must explicitly configure.
15
+ curate_rerank: (cfg) => Boolean(cfg.search?.curateRerank?.enabled),
12
16
  // Always on at the LLM-wrapper level. Enablement is decided ONCE at the
13
17
  // extract entry point (`akmExtract`): the `extract.enabled` process toggle
14
18
  // gates extract as a STAGE of `akm improve` (the active improve strategy, per
@@ -0,0 +1,113 @@
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
+ * HTTP client for a standalone cross-encoder rerank endpoint (#951).
6
+ *
7
+ * `curate_rerank` was a dead `llm.features.*` key removed in 0.8.0 — no wire
8
+ * call was ever implemented under it. This is the first real implementation:
9
+ * a small, dedicated client (NOT routed through `llm/client.ts`'s chat-
10
+ * completions transport, since a reranker speaks a different, much smaller
11
+ * contract) for the request/response shape a TEI/Cohere-style `/rerank`
12
+ * endpoint accepts:
13
+ *
14
+ * POST <endpoint>
15
+ * { "model": "<name>", "query": "<text>", "documents": ["<text>", ...] }
16
+ *
17
+ * 200 OK
18
+ * { "results": [ { "index": 0, "relevance_score": 0.83 }, ... ] }
19
+ *
20
+ * `results` need not be sorted or complete — {@link rerankDocuments} sorts by
21
+ * `relevance_score` descending and callers treat a missing index as
22
+ * "unscored" (kept in its original relative position, after every scored
23
+ * document). Deliberately independent of `EngineConfigSchema`'s "llm"/"agent"
24
+ * kinds — see the comment on `CurateRerankConfigSchema` in
25
+ * `core/config/schema/search.ts` for why.
26
+ */
27
+ import { fetchWithTimeout, readBodyWithByteCap } from "../core/common.js";
28
+ import { resolveSecret } from "../core/config/config.js";
29
+ import { isApiKeyReference } from "../core/config/schema/primitives.js";
30
+ import { redactErrorBody, redactSensitiveText } from "../core/redaction.js";
31
+ import { resolveSecretFromStore } from "../sources/snapshot-fetchers/secret-seam.js";
32
+ const DEFAULT_RERANK_TIMEOUT_MS = 10_000;
33
+ export class RerankCallError extends Error {
34
+ code;
35
+ constructor(message, code) {
36
+ super(message);
37
+ this.name = "RerankCallError";
38
+ this.code = code;
39
+ }
40
+ }
41
+ /**
42
+ * Call a configured rerank endpoint and return documents ordered by
43
+ * relevance to `query`, most relevant first. Any document the endpoint
44
+ * didn't return a score for keeps its original relative order, appended
45
+ * after every scored document (never dropped).
46
+ *
47
+ * Throws {@link RerankCallError} on any transport/parse failure — callers
48
+ * that want a graceful fallback should use `tryLlmFeature("curate_rerank", ...)`
49
+ * (`llm/feature-gate.ts`), matching every other bounded in-tree LLM/rerank
50
+ * call site.
51
+ */
52
+ export async function rerankDocuments(config, query, documents) {
53
+ if (!config.endpoint) {
54
+ throw new RerankCallError("search.curateRerank.endpoint is not configured.", "provider_error");
55
+ }
56
+ if (documents.length === 0)
57
+ return [];
58
+ const resolvedKey = config.apiKey && isApiKeyReference(config.apiKey)
59
+ ? resolveSecret(config.apiKey, resolveSecretFromStore)
60
+ : config.apiKey;
61
+ const headers = { "Content-Type": "application/json" };
62
+ if (resolvedKey)
63
+ headers.Authorization = `Bearer ${resolvedKey}`;
64
+ const timeoutMs = config.timeoutMs ?? DEFAULT_RERANK_TIMEOUT_MS;
65
+ const requestBody = JSON.stringify({
66
+ ...(config.model ? { model: config.model } : {}),
67
+ query,
68
+ documents,
69
+ });
70
+ let response;
71
+ try {
72
+ response = await fetchWithTimeout(config.endpoint, { method: "POST", headers, body: requestBody }, timeoutMs);
73
+ }
74
+ catch (err) {
75
+ const msg = err instanceof Error ? err.message : String(err);
76
+ if (msg.includes("timed out")) {
77
+ throw new RerankCallError(`Rerank request timed out after ${timeoutMs}ms`, "timeout");
78
+ }
79
+ throw new RerankCallError(`Rerank network error: ${msg}`, "network_error");
80
+ }
81
+ if (!response.ok) {
82
+ const rawBody = await readBodyWithByteCap(response).catch(() => "");
83
+ const safeBody = redactSensitiveText(redactErrorBody(rawBody), resolvedKey ? [resolvedKey] : []);
84
+ throw new RerankCallError(`Rerank request failed (${response.status}) ${config.endpoint}: ${safeBody}`, "provider_error");
85
+ }
86
+ const rawBody = await readBodyWithByteCap(response);
87
+ let json;
88
+ try {
89
+ json = JSON.parse(rawBody);
90
+ }
91
+ catch {
92
+ throw new RerankCallError(`Rerank response was not valid JSON ${config.endpoint}: ${redactSensitiveText(redactErrorBody(rawBody), resolvedKey ? [resolvedKey] : [])}`, "parse_error");
93
+ }
94
+ if (!Array.isArray(json.results)) {
95
+ throw new RerankCallError(`Rerank response from ${config.endpoint} has no "results" array.`, "parse_error");
96
+ }
97
+ const scored = new Map();
98
+ for (const entry of json.results) {
99
+ if (typeof entry.index !== "number")
100
+ continue;
101
+ const score = typeof entry.relevance_score === "number" ? entry.relevance_score : entry.score;
102
+ if (typeof score === "number")
103
+ scored.set(entry.index, score);
104
+ }
105
+ const rankedScored = [...scored.entries()]
106
+ .map(([index, score]) => ({ index, score }))
107
+ .sort((a, b) => b.score - a.score);
108
+ const unscored = documents
109
+ .map((_, index) => index)
110
+ .filter((index) => !scored.has(index))
111
+ .map((index) => ({ index, score: Number.NEGATIVE_INFINITY }));
112
+ return [...rankedScored, ...unscored];
113
+ }
@@ -67,12 +67,18 @@ function toEventMetadata(record) {
67
67
  * opened for its other events. A getter is resolved for every append so a
68
68
  * caller can replace its context binding without replacing this owning sink.
69
69
  * When omitted, `appendEvent` falls back to its default open-insert-close path.
70
+ *
71
+ * `onRecord`, when supplied, runs synchronously for every terminal record
72
+ * BEFORE persistence — improve's first-engine-response heartbeat (#957) uses
73
+ * it to know the run is no longer silent, without this module taking on any
74
+ * dependency of its own on improve's lifecycle.
70
75
  */
71
- export function installLlmUsagePersistence(ctx) {
76
+ export function installLlmUsagePersistence(ctx, onRecord) {
72
77
  let expectedTerminalRecords = 0;
73
78
  let disposed = false;
74
79
  setLlmUsageSink((record) => {
75
80
  expectedTerminalRecords += 1;
81
+ onRecord?.(record);
76
82
  appendEvent({ eventType: LLM_USAGE_EVENT, metadata: toEventMetadata(record) }, typeof ctx === "function" ? ctx() : ctx);
77
83
  });
78
84
  return () => {
@@ -7071,7 +7071,9 @@ var init_errors = __esm(() => {
7071
7071
  TRANSIENT_HINTS = {
7072
7072
  RUN_LEASE_HELD: "Wait for the named engine invocation to finish or for the lease to expire, then retry. `akm workflow status <id>` shows the current lease.",
7073
7073
  STATE_DB_CONTENDED: "Another akm process is writing state.db right now. Wait a few seconds and retry; commands that support --skip-if-locked can skip instead of failing.",
7074
- INDEX_DB_CONTENDED: "Another akm process is writing index.db; retry shortly, or pass --skip-if-locked on scheduled runs."
7074
+ INDEX_DB_CONTENDED: "Another akm process is writing index.db; retry shortly, or pass --skip-if-locked on scheduled runs.",
7075
+ MAINTENANCE_BARRIER_BUSY: "Another akm process is registering a lock or lease right now. Retry shortly, or pass --skip-if-locked on scheduled index/improve/workflow runs.",
7076
+ IMPROVE_LOCK_HELD: "Another akm improve run holds the whole-run lock right now. Wait for it to finish and retry, or pass --skip-if-locked on scheduled runs."
7075
7077
  };
7076
7078
  NOT_FOUND_HINTS = {
7077
7079
  ASSET_NOT_FOUND: "Run `akm search <query>` or `akm index` to refresh the index.",
@@ -76377,10 +76379,19 @@ var SearchGraphBoostSchema = exports_external.object({
76377
76379
  confidenceMode: exports_external.enum(["blend"]).default("blend").optional(),
76378
76380
  confidenceWeight: exports_external.number().finite().min(0).max(1).default(0.2).optional()
76379
76381
  }).passthrough();
76382
+ var CurateRerankConfigSchema = exports_external.object({
76383
+ enabled: exports_external.boolean().optional(),
76384
+ endpoint: httpUrl.optional(),
76385
+ model: nonEmptyString.optional(),
76386
+ apiKey: symbolicOrWarnApiKey("search.curateRerank.apiKey").optional(),
76387
+ timeoutMs: positiveInt.optional(),
76388
+ topN: positiveInt.max(50).optional()
76389
+ }).passthrough();
76380
76390
  var SearchConfigSchema = exports_external.object({
76381
76391
  minScore: nonNegativeNumber.optional(),
76382
76392
  defaultExcludeTypes: exports_external.array(nonEmptyString).optional(),
76383
- graphBoost: SearchGraphBoostSchema.optional()
76393
+ graphBoost: SearchGraphBoostSchema.optional(),
76394
+ curateRerank: CurateRerankConfigSchema.optional()
76384
76395
  }).passthrough();
76385
76396
 
76386
76397
  // src/core/config/schema/setup.ts
@@ -77923,10 +77934,13 @@ import { AsyncLocalStorage } from "node:async_hooks";
77923
77934
  import { randomUUID as randomUUID2 } from "node:crypto";
77924
77935
  import fs7 from "node:fs";
77925
77936
  import path12 from "node:path";
77937
+ init_common();
77926
77938
  init_errors();
77927
77939
  init_paths();
77928
77940
  var heldBarrierContext = new AsyncLocalStorage;
77929
77941
  var MAINTENANCE_BARRIER_STALE_AFTER_MS = 5 * 60 * 1000;
77942
+ var MAINTENANCE_BARRIER_BUSY_RETRY_BOUND_MS = 1500;
77943
+ var busyRetryBoundMsForTests;
77930
77944
  function tryAcquireMaintenanceBarrier() {
77931
77945
  const lockPath = getMaintenanceBarrierPath();
77932
77946
  fs7.mkdirSync(path12.dirname(lockPath), { recursive: true });
@@ -77942,10 +77956,18 @@ function tryAcquireMaintenanceBarrier() {
77942
77956
  return;
77943
77957
  }
77944
77958
  function acquireMaintenanceBarrier() {
77945
- const release = tryAcquireMaintenanceBarrier();
77946
- if (release)
77947
- return release;
77948
- throw new ConfigError(`AKM maintenance is in progress (barrier ${getMaintenanceBarrierPath()}); retry after it completes. ` + `A sentinel older than ${MAINTENANCE_BARRIER_STALE_AFTER_MS / 60000} minute(s) is reclaimed automatically on the next attempt.`, "INVALID_CONFIG_FILE");
77959
+ const boundMs = busyRetryBoundMsForTests ?? MAINTENANCE_BARRIER_BUSY_RETRY_BOUND_MS;
77960
+ const deadline = Date.now() + boundMs;
77961
+ for (let attempt = 0;; attempt += 1) {
77962
+ const release = tryAcquireMaintenanceBarrier();
77963
+ if (release)
77964
+ return release;
77965
+ const remainingMs = deadline - Date.now();
77966
+ if (remainingMs <= 0)
77967
+ break;
77968
+ sleepSync(Math.min(backoffDelay(attempt), remainingMs));
77969
+ }
77970
+ throw new TransientError(`AKM maintenance is in progress (barrier ${getMaintenanceBarrierPath()}); retry shortly. ` + `A sentinel older than ${MAINTENANCE_BARRIER_STALE_AFTER_MS / 60000} minute(s) is reclaimed automatically on the next attempt.`, "MAINTENANCE_BARRIER_BUSY");
77949
77971
  }
77950
77972
  function withMaintenanceStartBarrier(run) {
77951
77973
  if (heldBarrierContext.getStore()?.active)
@@ -93471,6 +93493,7 @@ init_warn();
93471
93493
  var MODEL_MAP_VERSION = 1;
93472
93494
  var ENGINE_KEY_PATTERN = new RegExp(ENGINE_NAME_PATTERN_SOURCE);
93473
93495
  var RESERVED_MAP_KEYS = new Set(["__proto__", "constructor", "prototype", "tostring"]);
93496
+ var PROFILE_FIELD_KEYS = new Set(["model", "inference", "engine"]);
93474
93497
 
93475
93498
  // src/integrations/harnesses/opencode-sdk/sdk-runner.ts
93476
93499
  var _servers = new Map;
@@ -97460,6 +97483,34 @@ async function embedBatch(texts, embeddingConfig, signal, onSkip, onBatch) {
97460
97483
  return results;
97461
97484
  }
97462
97485
 
97486
+ // src/indexer/index-db-contention.ts
97487
+ init_errors();
97488
+
97489
+ // src/indexer/index-rebuild-lock.ts
97490
+ init_paths();
97491
+ init_warn();
97492
+ function indexRebuildLockPath() {
97493
+ return getIndexRebuildLockPath();
97494
+ }
97495
+
97496
+ // src/indexer/index-db-contention.ts
97497
+ function describeIndexRebuildLockHolder() {
97498
+ const probe = probeLock(indexRebuildLockPath());
97499
+ if (probe.state !== "held")
97500
+ return "";
97501
+ return ` The rebuild lock is currently held by pid ${formatLockHolderPid({
97502
+ pid: probe.holderPid,
97503
+ launcherPid: probe.launcherPid ?? null
97504
+ })}.`;
97505
+ }
97506
+ function reclassifyIndexDbContention(error2) {
97507
+ if (error2 instanceof AkmError || !isSqliteContentionError(error2))
97508
+ return error2;
97509
+ const contended = new TransientError(`akm's index database is busy (another akm process is writing it); retry shortly.${describeIndexRebuildLockHolder()}`, "INDEX_DB_CONTENDED");
97510
+ contended.cause = error2;
97511
+ return contended;
97512
+ }
97513
+
97463
97514
  // src/indexer/materialize-embeddings.ts
97464
97515
  function deriveSemanticProviderFingerprint(embedding) {
97465
97516
  if (isDeterministicEmbedEnabled()) {
@@ -97870,7 +97921,8 @@ ${listed}${more}` });
97870
97921
  clearInterval(heartbeatTimer);
97871
97922
  }
97872
97923
  } catch (error2) {
97873
- const message = error2 instanceof Error ? error2.message : String(error2);
97924
+ const reclassified = reclassifyIndexDbContention(error2);
97925
+ const message = reclassified instanceof Error ? reclassified.message : String(reclassified);
97874
97926
  warn("Embedding generation failed, continuing without:", message);
97875
97927
  onProgress({ phase: "embeddings", message: `Embedding generation failed: ${message}` });
97876
97928
  return {
@@ -7070,7 +7070,9 @@ var init_errors = __esm(() => {
7070
7070
  TRANSIENT_HINTS = {
7071
7071
  RUN_LEASE_HELD: "Wait for the named engine invocation to finish or for the lease to expire, then retry. `akm workflow status <id>` shows the current lease.",
7072
7072
  STATE_DB_CONTENDED: "Another akm process is writing state.db right now. Wait a few seconds and retry; commands that support --skip-if-locked can skip instead of failing.",
7073
- INDEX_DB_CONTENDED: "Another akm process is writing index.db; retry shortly, or pass --skip-if-locked on scheduled runs."
7073
+ INDEX_DB_CONTENDED: "Another akm process is writing index.db; retry shortly, or pass --skip-if-locked on scheduled runs.",
7074
+ MAINTENANCE_BARRIER_BUSY: "Another akm process is registering a lock or lease right now. Retry shortly, or pass --skip-if-locked on scheduled index/improve/workflow runs.",
7075
+ IMPROVE_LOCK_HELD: "Another akm improve run holds the whole-run lock right now. Wait for it to finish and retry, or pass --skip-if-locked on scheduled runs."
7074
7076
  };
7075
7077
  NOT_FOUND_HINTS = {
7076
7078
  ASSET_NOT_FOUND: "Run `akm search <query>` or `akm index` to refresh the index.",
@@ -75705,10 +75707,19 @@ var SearchGraphBoostSchema = exports_external.object({
75705
75707
  confidenceMode: exports_external.enum(["blend"]).default("blend").optional(),
75706
75708
  confidenceWeight: exports_external.number().finite().min(0).max(1).default(0.2).optional()
75707
75709
  }).passthrough();
75710
+ var CurateRerankConfigSchema = exports_external.object({
75711
+ enabled: exports_external.boolean().optional(),
75712
+ endpoint: httpUrl.optional(),
75713
+ model: nonEmptyString.optional(),
75714
+ apiKey: symbolicOrWarnApiKey("search.curateRerank.apiKey").optional(),
75715
+ timeoutMs: positiveInt.optional(),
75716
+ topN: positiveInt.max(50).optional()
75717
+ }).passthrough();
75708
75718
  var SearchConfigSchema = exports_external.object({
75709
75719
  minScore: nonNegativeNumber.optional(),
75710
75720
  defaultExcludeTypes: exports_external.array(nonEmptyString).optional(),
75711
- graphBoost: SearchGraphBoostSchema.optional()
75721
+ graphBoost: SearchGraphBoostSchema.optional(),
75722
+ curateRerank: CurateRerankConfigSchema.optional()
75712
75723
  }).passthrough();
75713
75724
 
75714
75725
  // src/core/config/schema/setup.ts
@@ -77251,10 +77262,13 @@ import { AsyncLocalStorage } from "async_hooks";
77251
77262
  import { randomUUID as randomUUID2 } from "crypto";
77252
77263
  import fs7 from "fs";
77253
77264
  import path12 from "path";
77265
+ init_common();
77254
77266
  init_errors();
77255
77267
  init_paths();
77256
77268
  var heldBarrierContext = new AsyncLocalStorage;
77257
77269
  var MAINTENANCE_BARRIER_STALE_AFTER_MS = 5 * 60 * 1000;
77270
+ var MAINTENANCE_BARRIER_BUSY_RETRY_BOUND_MS = 1500;
77271
+ var busyRetryBoundMsForTests;
77258
77272
  function tryAcquireMaintenanceBarrier() {
77259
77273
  const lockPath = getMaintenanceBarrierPath();
77260
77274
  fs7.mkdirSync(path12.dirname(lockPath), { recursive: true });
@@ -77270,10 +77284,18 @@ function tryAcquireMaintenanceBarrier() {
77270
77284
  return;
77271
77285
  }
77272
77286
  function acquireMaintenanceBarrier() {
77273
- const release = tryAcquireMaintenanceBarrier();
77274
- if (release)
77275
- return release;
77276
- throw new ConfigError(`AKM maintenance is in progress (barrier ${getMaintenanceBarrierPath()}); retry after it completes. ` + `A sentinel older than ${MAINTENANCE_BARRIER_STALE_AFTER_MS / 60000} minute(s) is reclaimed automatically on the next attempt.`, "INVALID_CONFIG_FILE");
77287
+ const boundMs = busyRetryBoundMsForTests ?? MAINTENANCE_BARRIER_BUSY_RETRY_BOUND_MS;
77288
+ const deadline = Date.now() + boundMs;
77289
+ for (let attempt = 0;; attempt += 1) {
77290
+ const release = tryAcquireMaintenanceBarrier();
77291
+ if (release)
77292
+ return release;
77293
+ const remainingMs = deadline - Date.now();
77294
+ if (remainingMs <= 0)
77295
+ break;
77296
+ sleepSync(Math.min(backoffDelay(attempt), remainingMs));
77297
+ }
77298
+ throw new TransientError(`AKM maintenance is in progress (barrier ${getMaintenanceBarrierPath()}); retry shortly. ` + `A sentinel older than ${MAINTENANCE_BARRIER_STALE_AFTER_MS / 60000} minute(s) is reclaimed automatically on the next attempt.`, "MAINTENANCE_BARRIER_BUSY");
77277
77299
  }
77278
77300
  function withMaintenanceStartBarrier(run) {
77279
77301
  if (heldBarrierContext.getStore()?.active)
@@ -93466,6 +93488,7 @@ init_warn();
93466
93488
  var MODEL_MAP_VERSION = 1;
93467
93489
  var ENGINE_KEY_PATTERN = new RegExp(ENGINE_NAME_PATTERN_SOURCE);
93468
93490
  var RESERVED_MAP_KEYS = new Set(["__proto__", "constructor", "prototype", "tostring"]);
93491
+ var PROFILE_FIELD_KEYS = new Set(["model", "inference", "engine"]);
93469
93492
 
93470
93493
  // src/integrations/harnesses/opencode-sdk/sdk-runner.ts
93471
93494
  var _servers = new Map;
@@ -97416,6 +97439,34 @@ async function embedBatch(texts, embeddingConfig, signal, onSkip, onBatch) {
97416
97439
  return results;
97417
97440
  }
97418
97441
 
97442
+ // src/indexer/index-db-contention.ts
97443
+ init_errors();
97444
+
97445
+ // src/indexer/index-rebuild-lock.ts
97446
+ init_paths();
97447
+ init_warn();
97448
+ function indexRebuildLockPath() {
97449
+ return getIndexRebuildLockPath();
97450
+ }
97451
+
97452
+ // src/indexer/index-db-contention.ts
97453
+ function describeIndexRebuildLockHolder() {
97454
+ const probe = probeLock(indexRebuildLockPath());
97455
+ if (probe.state !== "held")
97456
+ return "";
97457
+ return ` The rebuild lock is currently held by pid ${formatLockHolderPid({
97458
+ pid: probe.holderPid,
97459
+ launcherPid: probe.launcherPid ?? null
97460
+ })}.`;
97461
+ }
97462
+ function reclassifyIndexDbContention(error2) {
97463
+ if (error2 instanceof AkmError || !isSqliteContentionError(error2))
97464
+ return error2;
97465
+ const contended = new TransientError(`akm's index database is busy (another akm process is writing it); retry shortly.${describeIndexRebuildLockHolder()}`, "INDEX_DB_CONTENDED");
97466
+ contended.cause = error2;
97467
+ return contended;
97468
+ }
97469
+
97419
97470
  // src/indexer/materialize-embeddings.ts
97420
97471
  function deriveSemanticProviderFingerprint(embedding) {
97421
97472
  if (isDeterministicEmbedEnabled()) {
@@ -97826,7 +97877,8 @@ ${listed}${more}` });
97826
97877
  clearInterval(heartbeatTimer);
97827
97878
  }
97828
97879
  } catch (error2) {
97829
- const message = error2 instanceof Error ? error2.message : String(error2);
97880
+ const reclassified = reclassifyIndexDbContention(error2);
97881
+ const message = reclassified instanceof Error ? reclassified.message : String(reclassified);
97830
97882
  warn("Embedding generation failed, continuing without:", message);
97831
97883
  onProgress({ phase: "embeddings", message: `Embedding generation failed: ${message}` });
97832
97884
  return {
@@ -36,6 +36,65 @@ export function resolveTaskLogPath(logDir, taskId, startedAtIso) {
36
36
  return "";
37
37
  }
38
38
  }
39
+ /**
40
+ * Delete per-run flat log files (`<logDir>/<taskId>/<timestamp>.log`) older
41
+ * than `retentionDays` (#951). These files are a transitional human-readable
42
+ * tail only — the durable record lives in logs.db, already purged by
43
+ * {@link purgeOldTaskLogs} in ../../core/logs-db — so deleting an old file
44
+ * here loses nothing that isn't already retained (and separately purged)
45
+ * there.
46
+ *
47
+ * Bounded by construction: only ever reads/deletes inside `logDir` (defaults
48
+ * to {@link getTaskLogDir}), one level of `<taskId>` subdirectories, `.log`
49
+ * files only. A missing/unreadable directory is a no-op, not an error —
50
+ * mirrors the best-effort contract the rest of this module holds for log
51
+ * persistence.
52
+ */
53
+ export function purgeOldTaskLogFiles(logDir, retentionDays = 90) {
54
+ if (!Number.isFinite(retentionDays) || retentionDays <= 0)
55
+ return 0;
56
+ const dir = logDir ?? getTaskLogDir();
57
+ const cutoffMs = Date.now() - retentionDays * 86_400_000;
58
+ let deleted = 0;
59
+ let taskDirs;
60
+ try {
61
+ taskDirs = fs.readdirSync(dir, { withFileTypes: true });
62
+ }
63
+ catch (error) {
64
+ rethrowIfTestIsolationError(error);
65
+ return 0;
66
+ }
67
+ for (const taskDirEntry of taskDirs) {
68
+ if (!taskDirEntry.isDirectory())
69
+ continue;
70
+ const taskDirPath = path.join(dir, taskDirEntry.name);
71
+ let logFiles;
72
+ try {
73
+ logFiles = fs.readdirSync(taskDirPath, { withFileTypes: true });
74
+ }
75
+ catch (error) {
76
+ rethrowIfTestIsolationError(error);
77
+ continue;
78
+ }
79
+ for (const logFile of logFiles) {
80
+ if (!logFile.isFile() || !logFile.name.endsWith(".log"))
81
+ continue;
82
+ const logFilePath = path.join(taskDirPath, logFile.name);
83
+ try {
84
+ const stat = fs.statSync(logFilePath);
85
+ if (stat.mtimeMs < cutoffMs) {
86
+ fs.unlinkSync(logFilePath);
87
+ deleted++;
88
+ }
89
+ }
90
+ catch (error) {
91
+ rethrowIfTestIsolationError(error);
92
+ // Best-effort: a file that vanished or can't be stat'd/removed is skipped.
93
+ }
94
+ }
95
+ }
96
+ return deleted;
97
+ }
39
98
  /**
40
99
  * Redact logs.db rows against the SAME contiguous text the file sink sees.
41
100
  *