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.
- package/CHANGELOG.md +109 -3
- package/dist/commands/health/checks.js +2 -7
- package/dist/commands/improve/improve-cli.js +107 -6
- package/dist/commands/improve/improve.js +47 -3
- package/dist/commands/improve/locks.js +9 -2
- package/dist/commands/improve/loop-stages.js +21 -1
- package/dist/commands/improve/reflect.js +159 -64
- package/dist/commands/models-cli.js +6 -1
- package/dist/commands/read/curate.js +34 -1
- package/dist/core/config/schema/search.js +32 -1
- package/dist/core/errors.js +2 -0
- package/dist/core/maintenance-barrier.js +35 -6
- package/dist/indexer/index-db-contention.js +56 -0
- package/dist/indexer/indexer.js +10 -43
- package/dist/indexer/materialize-embeddings.js +15 -1
- package/dist/integrations/agent/model-map.js +39 -2
- package/dist/llm/client.js +18 -0
- package/dist/llm/feature-gate.js +4 -0
- package/dist/llm/rerank-client.js +113 -0
- package/dist/llm/usage-persist.js +7 -1
- package/dist/scripts/akm-migrate-node.js +59 -7
- package/dist/scripts/akm-migrate.js +59 -7
- package/dist/tasks/run/task-log.js +59 -0
- package/docs/migration/release-notes/0.9.15.md +81 -0
- package/docs/reference/cli.md +34 -2
- package/docs/reference/configuration.md +51 -0
- package/package.json +1 -1
- package/schemas/akm-config.json +56 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
package/dist/llm/client.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/llm/feature-gate.js
CHANGED
|
@@ -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
|
|
77946
|
-
|
|
77947
|
-
|
|
77948
|
-
|
|
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
|
|
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
|
|
77274
|
-
|
|
77275
|
-
|
|
77276
|
-
|
|
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
|
|
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
|
*
|