akm-cli 0.9.15-beta.4 → 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 +53 -1
- package/dist/commands/improve/locks.js +9 -2
- package/dist/commands/improve/loop-stages.js +21 -1
- 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 +1 -0
- 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/feature-gate.js +4 -0
- package/dist/llm/rerank-client.js +113 -0
- package/dist/scripts/akm-migrate-node.js +43 -3
- package/dist/scripts/akm-migrate.js +43 -3
- package/dist/tasks/run/task-log.js +59 -0
- package/docs/migration/release-notes/0.9.15.md +16 -0
- package/docs/reference/cli.md +1 -1
- package/docs/reference/configuration.md +17 -0
- package/package.json +1 -1
- package/schemas/akm-config.json +56 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
6
|
|
|
7
|
-
## [0.9.15-beta.
|
|
7
|
+
## [0.9.15-beta.5] - 2026-09-10
|
|
8
8
|
|
|
9
9
|
### Added
|
|
10
10
|
|
|
@@ -159,6 +159,32 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
159
159
|
(#951).** These are akm's resolved data/config/cache/state directories, so a
|
|
160
160
|
script can read `akm info --format json | jq -r .dataDir` instead of hardcoding
|
|
161
161
|
a path that differs between a host install and a container.
|
|
162
|
+
- **A real `curate_rerank` implementation: `search.curateRerank`, a cross-
|
|
163
|
+
encoder rerank pass over `akm curate`'s already-selected candidates (#951).**
|
|
164
|
+
`curate_rerank` was a dead `llm.features.*` key removed in 0.8.0 with no wire
|
|
165
|
+
call ever implemented under it. `search.curateRerank.{enabled, endpoint,
|
|
166
|
+
model, apiKey, timeoutMs, topN}` configures a standalone `/rerank`-style
|
|
167
|
+
endpoint (`src/llm/rerank-client.ts`, `POST {model, query, documents}` →
|
|
168
|
+
`{results: [{index, relevance_score}]}`); when enabled, curate sends its top
|
|
169
|
+
`topN` (default 8) already-ranked candidates and reorders them by the
|
|
170
|
+
endpoint's scores. Disabled by default, and best-effort like every other
|
|
171
|
+
bounded LLM feature: a misconfigured endpoint, network failure, timeout, or
|
|
172
|
+
malformed response falls back to curate's own ranking unchanged rather than
|
|
173
|
+
failing the command. Deliberately its own config arm rather than a third
|
|
174
|
+
`engines` kind alongside `"llm"`/`"agent"` — that union's kinds are
|
|
175
|
+
load-bearing through execution-lowering, runner dispatch, and the harness
|
|
176
|
+
model map, none of which a reranker touches.
|
|
177
|
+
- **Per-run task log files are purged past the retention window (#951).**
|
|
178
|
+
`tasks/logs/<taskId>/<timestamp>.log` (the per-run human-readable tail;
|
|
179
|
+
`logs.db` is the durable, already-purged record) had file separation but no
|
|
180
|
+
cleanup, so old run files accumulated forever. `akm improve`'s existing
|
|
181
|
+
retention pass now also deletes `.log` files under `getTaskLogDir()` older
|
|
182
|
+
than the same `improve.eventRetentionDays` window (default 90d, `0` disables
|
|
183
|
+
it) it already uses for `task_logs`/events/`improve_runs` — bounded to that
|
|
184
|
+
one directory, one level of `<taskId>` subdirectories, `.log` files only.
|
|
185
|
+
Path-agnostic bundled scripts and the `akm show env/<name>`/`akm task list`
|
|
186
|
+
items from the same review were previously confirmed shipped and are
|
|
187
|
+
unchanged here.
|
|
162
188
|
- **`akm index --reembed` forces a full re-embed (#955).** Bypasses the
|
|
163
189
|
compatibility check above entirely and purges + regenerates every stored
|
|
164
190
|
embedding, for the rare case where the check's verdict should not be trusted. A
|
|
@@ -477,6 +503,32 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
477
503
|
|
|
478
504
|
### Fixed
|
|
479
505
|
|
|
506
|
+
- **An alias-level `engine` binding in `models.json` is honoured on every
|
|
507
|
+
platform column (#946).** The per-platform nested form
|
|
508
|
+
(`"fast": {"opencode": {"engine": "local-fast"}}`) already worked, but the
|
|
509
|
+
flat shorthand from this issue's own acceptance criteria
|
|
510
|
+
(`"fast": {"engine": "local-fast"}`) did not: `parseModelMapLayer` treated
|
|
511
|
+
every key under an alias as a platform-column name, so `engine` was parsed
|
|
512
|
+
as a fourth platform holding the model string, the three real columns kept
|
|
513
|
+
their hardcoded cloud defaults, and `akm agent --model fast` silently
|
|
514
|
+
dispatched to a cloud model instead of the configured local engine. The
|
|
515
|
+
flat `model`/`inference`/`engine` keys are now a wildcard default merged
|
|
516
|
+
onto every column, a same-alias per-platform entry still overrides it, and
|
|
517
|
+
an alias naming an unknown engine fails with a `ConfigError` rather than
|
|
518
|
+
falling back.
|
|
519
|
+
- **Two `improve` runs colliding on the lock is transient, not a broken
|
|
520
|
+
config (#948).** The lock-held path threw a `ConfigError`, surfacing as
|
|
521
|
+
exit 78 (`INVALID_CONFIG_FILE`) and telling a supervisor to stop retrying
|
|
522
|
+
an ordinary collision. It is now a `TransientError`/`IMPROVE_LOCK_HELD` at
|
|
523
|
+
exit 75, matching the `MAINTENANCE_BARRIER_BUSY`/`INDEX_DB_CONTENDED`
|
|
524
|
+
treatment the index rebuild lock already had.
|
|
525
|
+
- **The embedding path no longer leaks a raw `database is locked` (#956).**
|
|
526
|
+
`reclassifyIndexDbContention` moved out of `indexer.ts` into a shared
|
|
527
|
+
module and `materialize-embeddings.ts`'s embedding-generation catch now
|
|
528
|
+
routes through it, so contention produces the same "index database is
|
|
529
|
+
busy" wording as every other path instead of the raw driver string.
|
|
530
|
+
Control flow is unchanged: this was always non-fatal.
|
|
531
|
+
|
|
480
532
|
- **Starting a workflow ref that already has an active run in a different scope
|
|
481
533
|
now warns instead of silently duplicating it (#942).** `akm workflow run
|
|
482
534
|
<ref>`'s per-scope concurrency guard is unchanged by design — two unrelated
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import {
|
|
5
|
+
import { TransientError } from "../../core/errors.js";
|
|
6
6
|
import { appendEvent } from "../../core/events.js";
|
|
7
7
|
import { releaseLock } from "../../core/file-lock.js";
|
|
8
8
|
import { tryWithMaintenanceStartBarrier, withMaintenanceStartBarrier } from "../../core/maintenance-barrier.js";
|
|
@@ -65,7 +65,14 @@ function tryAcquireImproveLockUnlocked(lockPath, skipIfLocked, onRecovered) {
|
|
|
65
65
|
warn(`[improve] another improve run holds the lock (PID ${pid}, started ${startedAt}); skipping (--skip-if-locked)`);
|
|
66
66
|
return { state: "skipped" };
|
|
67
67
|
}
|
|
68
|
-
|
|
68
|
+
// #948 field follow-up: two legitimate `improve` invocations colliding on
|
|
69
|
+
// this lock is ordinary, retryable contention — not a broken config file.
|
|
70
|
+
// A `ConfigError` here surfaced as exit 78 (INVALID_CONFIG_FILE) and told
|
|
71
|
+
// a supervisor to stop retrying a normal lock collision. Reclassified to
|
|
72
|
+
// `TransientError`/`IMPROVE_LOCK_HELD` (exit 75), mirroring the
|
|
73
|
+
// `MAINTENANCE_BARRIER_BUSY`/`INDEX_DB_CONTENDED` treatment #956 already
|
|
74
|
+
// applied to the index rebuild lock and the maintenance-start barrier.
|
|
75
|
+
throw new TransientError(`akm improve is already running (PID ${pid}, started ${startedAt}). Delete ${lockPath} to force.`, "IMPROVE_LOCK_HELD");
|
|
69
76
|
}
|
|
70
77
|
export function releaseImproveLock(ownership) {
|
|
71
78
|
releaseLock(ownership);
|
|
@@ -9,7 +9,7 @@ import { DEFAULT_GRAPH_EXTRACTION_BATCH_SIZE, loadConfig } from "../../core/conf
|
|
|
9
9
|
import { UsageError } from "../../core/errors.js";
|
|
10
10
|
import { appendEvent } from "../../core/events.js";
|
|
11
11
|
import { openLogsDatabase, purgeOldTaskLogs } from "../../core/logs-db.js";
|
|
12
|
-
import { getDbPath } from "../../core/paths.js";
|
|
12
|
+
import { getDbPath, getTaskLogDir } from "../../core/paths.js";
|
|
13
13
|
import { withStateDb } from "../../core/state-db.js";
|
|
14
14
|
import { info } from "../../core/warn.js";
|
|
15
15
|
import { DEFAULT_GRAPH_EXTRACTION_INCLUDE_TYPES, runGraphExtractionPass, } from "../../indexer/graph/graph-extraction.js";
|
|
@@ -25,6 +25,7 @@ import { closeDatabase, openIndexDatabase } from "../../storage/repositories/ind
|
|
|
25
25
|
import { getEntryByRef } from "../../storage/repositories/index-entries-repository.js";
|
|
26
26
|
import { clearAssetOutcomeMissing, countAssetOutcomeMissing, deleteAssetOutcomeMissingBefore, listAssetOutcomeMissingState, stampAssetOutcomeMissing, } from "../../storage/repositories/outcome-repository.js";
|
|
27
27
|
import { clearAssetSalienceMissing, countAssetSalienceMissing, deleteAssetSalienceMissingBefore, listAssetSalienceMissingState, stampAssetSalienceMissing, } from "../../storage/repositories/salience-repository.js";
|
|
28
|
+
import { purgeOldTaskLogFiles } from "../../tasks/run/task-log.js";
|
|
28
29
|
import { expireStaleProposals, listProposals, purgeOrphanProposals } from "../proposal/repository.js";
|
|
29
30
|
import { checkDeadUrls } from "../url-checker.js";
|
|
30
31
|
import { DEFAULT_RETENTION_DAYS as CYCLE_METRICS_RETENTION_DAYS, runCollapseDetector } from "./collapse-detector.js";
|
|
@@ -1124,6 +1125,25 @@ export function runRetentionPurgePass(ctx) {
|
|
|
1124
1125
|
}
|
|
1125
1126
|
}
|
|
1126
1127
|
}
|
|
1128
|
+
// Per-run flat log files under getTaskLogDir() (#951): logs.db above is
|
|
1129
|
+
// the durable record and already retention-purged, so the transitional
|
|
1130
|
+
// `<taskId>/<timestamp>.log` tail files can be deleted on the same
|
|
1131
|
+
// window without losing anything. A separate try/catch — a filesystem
|
|
1132
|
+
// problem here must not block the DB purges above.
|
|
1133
|
+
try {
|
|
1134
|
+
const taskLogFilesPurged = purgeOldTaskLogFiles(undefined, retentionDays);
|
|
1135
|
+
if (taskLogFilesPurged > 0) {
|
|
1136
|
+
info(`[improve] task log files purge: ${taskLogFilesPurged} file(s) older than ${retentionDays}d removed from ${getTaskLogDir()}`);
|
|
1137
|
+
}
|
|
1138
|
+
appendEvent({
|
|
1139
|
+
eventType: "task_log_files_purged",
|
|
1140
|
+
ref: "task_log_files/_purge",
|
|
1141
|
+
metadata: { purgedCount: taskLogFilesPurged, retentionDays },
|
|
1142
|
+
}, eventsCtx);
|
|
1143
|
+
}
|
|
1144
|
+
catch (err) {
|
|
1145
|
+
warnings.push(`task log files purge failed: ${errMessage(err)}`);
|
|
1146
|
+
}
|
|
1127
1147
|
}
|
|
1128
1148
|
return { warnings };
|
|
1129
1149
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { isDeepStrictEqual } from "node:util";
|
|
5
5
|
import { defineGroupCommand, defineJsonCommand, output } from "../cli/shared.js";
|
|
6
6
|
import { loadConfig } from "../core/config/config.js";
|
|
7
|
-
import { copyDefaultModelMap, loadModelMapLayers, mergedModelMapProfiles, mergeModelMapLayers, } from "../integrations/agent/model-map.js";
|
|
7
|
+
import { copyDefaultModelMap, loadModelMapLayers, mergedModelMapProfiles, mergeModelMapLayers, WILDCARD_ENGINE_KEY, } from "../integrations/agent/model-map.js";
|
|
8
8
|
/**
|
|
9
9
|
* Compute the effective alias table (#946): every (alias, column) pair from
|
|
10
10
|
* the fully resolved map, labeled with where its value came from.
|
|
@@ -25,6 +25,11 @@ function modelsListRows() {
|
|
|
25
25
|
const rows = [];
|
|
26
26
|
for (const [alias, columns] of Object.entries(resolved.aliases)) {
|
|
27
27
|
for (const [column, profile] of Object.entries(columns)) {
|
|
28
|
+
// The wildcard default (#946) is an alias-wide fallback, not a real
|
|
29
|
+
// platform column an operator would dispatch to; report it folded into
|
|
30
|
+
// the real per-platform rows above instead of as its own row.
|
|
31
|
+
if (column === WILDCARD_ENGINE_KEY)
|
|
32
|
+
continue;
|
|
28
33
|
const raw = rawProfiles[alias]?.[column];
|
|
29
34
|
const via = raw?.engine !== undefined ? "engine" : "literal";
|
|
30
35
|
const defaultProfile = defaultsOnly.aliases[alias]?.[column];
|
|
@@ -25,6 +25,8 @@ import { copySearchHitAttribution, getSearchHitAttribution, usageEventAttributio
|
|
|
25
25
|
import { findSourceForPath, resolveSourceEntries } from "../../indexer/search/search-source.js";
|
|
26
26
|
import { insertUsageEvent } from "../../indexer/usage/usage-events.js";
|
|
27
27
|
import { estimateTokenCount } from "../../llm/embedders/remote.js";
|
|
28
|
+
import { tryLlmFeature } from "../../llm/feature-gate.js";
|
|
29
|
+
import { rerankDocuments } from "../../llm/rerank-client.js";
|
|
28
30
|
import { truncateDescription } from "../../output/shapes/helpers.js";
|
|
29
31
|
import { TELEMETRY_BUSY_TIMEOUT_MS, withIndexDb } from "../../storage/repositories/index-db.js";
|
|
30
32
|
import { findEntryIdByRef, getItemRefById } from "../../storage/repositories/index-entries-repository.js";
|
|
@@ -145,7 +147,7 @@ export async function curateSearchResults(query, result, limit, selectedType, ev
|
|
|
145
147
|
// fixtures) with a `SearchResponse` that was never type-filtered.
|
|
146
148
|
const stashHits = selectedType && selectedType !== "any" ? allStashHits.filter((hit) => hit.type === selectedType) : allStashHits;
|
|
147
149
|
const selected = selectCuratedStashHits(query, stashHits, limit);
|
|
148
|
-
const selectedStashHits = selected.selected;
|
|
150
|
+
const selectedStashHits = await maybeRerankCuratedStashHits(query, selected.selected);
|
|
149
151
|
const supportRefsByRef = selected.supportRefsByRef;
|
|
150
152
|
// F4/R-019: respect `--limit` for registry fill instead of hard-capping it
|
|
151
153
|
// at a bare literal 2 — the remaining slots after stash hits ARE the cap.
|
|
@@ -594,6 +596,37 @@ function appendCurateSupportRef(supportRefsByRef, ownerRef, supportRef) {
|
|
|
594
596
|
return;
|
|
595
597
|
supportRefsByRef.set(ownerRef, [...existing, supportRef]);
|
|
596
598
|
}
|
|
599
|
+
/** Default number of `selectCuratedStashHits` candidates sent to the reranker when `search.curateRerank.topN` isn't set. */
|
|
600
|
+
const DEFAULT_CURATE_RERANK_TOP_N = 8;
|
|
601
|
+
/**
|
|
602
|
+
* Optional cross-encoder rerank pass over curate's already-selected, already-
|
|
603
|
+
* ranked candidates (#951). Disabled by default (`search.curateRerank.enabled`
|
|
604
|
+
* is falsy) and, when enabled, best-effort: any failure (misconfigured
|
|
605
|
+
* endpoint, network error, timeout, malformed response) falls back to
|
|
606
|
+
* `selectCuratedStashHits`'s own ranking unchanged — a reranker outage must
|
|
607
|
+
* never turn into a curate failure.
|
|
608
|
+
*
|
|
609
|
+
* Only the top `topN` (default {@link DEFAULT_CURATE_RERANK_TOP_N}) already-
|
|
610
|
+
* selected hits are sent (bounded request size); anything past that keeps its
|
|
611
|
+
* original position appended after the reranked prefix.
|
|
612
|
+
*/
|
|
613
|
+
async function maybeRerankCuratedStashHits(query, hits) {
|
|
614
|
+
if (hits.length <= 1)
|
|
615
|
+
return hits;
|
|
616
|
+
const config = loadConfig();
|
|
617
|
+
const rerankConfig = config.search?.curateRerank;
|
|
618
|
+
return tryLlmFeature("curate_rerank", config, async () => {
|
|
619
|
+
const topN = rerankConfig?.topN ?? DEFAULT_CURATE_RERANK_TOP_N;
|
|
620
|
+
const head = hits.slice(0, topN);
|
|
621
|
+
const tail = hits.slice(topN);
|
|
622
|
+
const documents = head.map((hit) => [hit.name, hit.description].filter(Boolean).join(" — "));
|
|
623
|
+
const ranked = await rerankDocuments(rerankConfig ?? {}, query, documents);
|
|
624
|
+
const rerankedHead = ranked
|
|
625
|
+
.map(({ index }) => head[index])
|
|
626
|
+
.filter((hit) => hit !== undefined);
|
|
627
|
+
return [...rerankedHead, ...tail];
|
|
628
|
+
}, hits, { timeoutMs: rerankConfig?.timeoutMs ?? null });
|
|
629
|
+
}
|
|
597
630
|
function selectCuratedStashHits(query, hits, limit) {
|
|
598
631
|
const intent = parseCurateIntent(query);
|
|
599
632
|
const collapsed = collapseCurateFamilies(query, hits);
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* former `config-schema.ts` monolith — no behavior change.
|
|
7
7
|
*/
|
|
8
8
|
import { z } from "zod";
|
|
9
|
-
import { nonEmptyString, nonNegativeNumber, positiveInt } from "./primitives.js";
|
|
9
|
+
import { httpUrl, nonEmptyString, nonNegativeNumber, positiveInt, symbolicOrWarnApiKey } from "./primitives.js";
|
|
10
10
|
// ── Search ──────────────────────────────────────────────────────────────────
|
|
11
11
|
const SearchGraphBoostSchema = z
|
|
12
12
|
.object({
|
|
@@ -22,10 +22,41 @@ const SearchGraphBoostSchema = z
|
|
|
22
22
|
confidenceWeight: z.number().finite().min(0).max(1).default(0.2).optional(),
|
|
23
23
|
})
|
|
24
24
|
.passthrough();
|
|
25
|
+
/**
|
|
26
|
+
* `search.curateRerank` (#951) — an optional cross-encoder rerank pass over
|
|
27
|
+
* `akm curate`'s already-selected candidates.
|
|
28
|
+
*
|
|
29
|
+
* Deliberately its own small config arm rather than a third member of the
|
|
30
|
+
* `engines` map (`EngineConfigSchema` in ./engines.ts): that union's "llm" /
|
|
31
|
+
* "agent" kinds are load-bearing all the way through execution-lowering,
|
|
32
|
+
* runner dispatch, and the harness model map (100+ call sites narrow on
|
|
33
|
+
* `engine.kind`). A reranker is neither — it never dispatches an agent or
|
|
34
|
+
* lowers to a chat-completions call — so folding it into that union would
|
|
35
|
+
* force every one of those call sites to account for a kind they can't do
|
|
36
|
+
* anything with. `endpoint` + `model` (+ optional `apiKey`) is the same
|
|
37
|
+
* connection shape as an LLM engine without inheriting that machinery.
|
|
38
|
+
*
|
|
39
|
+
* `curate_rerank` was removed as a dead `llm.features.*` key in 0.8.0 (no
|
|
40
|
+
* implementation ever sent a request); this is a new, real implementation,
|
|
41
|
+
* disabled by default.
|
|
42
|
+
*/
|
|
43
|
+
export const CurateRerankConfigSchema = z
|
|
44
|
+
.object({
|
|
45
|
+
enabled: z.boolean().optional(),
|
|
46
|
+
/** Full URL of the reranker's rerank endpoint, e.g. `http://host:port/rerank`. */
|
|
47
|
+
endpoint: httpUrl.optional(),
|
|
48
|
+
model: nonEmptyString.optional(),
|
|
49
|
+
apiKey: symbolicOrWarnApiKey("search.curateRerank.apiKey").optional(),
|
|
50
|
+
timeoutMs: positiveInt.optional(),
|
|
51
|
+
/** How many of curate's already-ranked candidates to send to the reranker. Default 8. */
|
|
52
|
+
topN: positiveInt.max(50).optional(),
|
|
53
|
+
})
|
|
54
|
+
.passthrough();
|
|
25
55
|
export const SearchConfigSchema = z
|
|
26
56
|
.object({
|
|
27
57
|
minScore: nonNegativeNumber.optional(),
|
|
28
58
|
defaultExcludeTypes: z.array(nonEmptyString).optional(),
|
|
29
59
|
graphBoost: SearchGraphBoostSchema.optional(),
|
|
60
|
+
curateRerank: CurateRerankConfigSchema.optional(),
|
|
30
61
|
})
|
|
31
62
|
.passthrough();
|
package/dist/core/errors.js
CHANGED
|
@@ -84,6 +84,7 @@ const TRANSIENT_HINTS = {
|
|
|
84
84
|
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.",
|
|
85
85
|
INDEX_DB_CONTENDED: "Another akm process is writing index.db; retry shortly, or pass --skip-if-locked on scheduled runs.",
|
|
86
86
|
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.",
|
|
87
|
+
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.",
|
|
87
88
|
};
|
|
88
89
|
/** Default hint for each NotFoundError code. */
|
|
89
90
|
const NOT_FOUND_HINTS = {
|
|
@@ -0,0 +1,56 @@
|
|
|
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
|
+
* Shared index.db contention reclassification (field follow-up to #956).
|
|
6
|
+
*
|
|
7
|
+
* Extracted out of `indexer.ts` so both `akmIndex`'s outer catch AND
|
|
8
|
+
* `generateEmbeddingsForDb`'s own catch (`materialize-embeddings.ts`) can
|
|
9
|
+
* reuse the ONE classifier instead of each building a raw
|
|
10
|
+
* `Semantic search verification failed: <driver message>` string. Living in
|
|
11
|
+
* its own module (rather than one importing the other) avoids the import
|
|
12
|
+
* cycle `indexer.ts` <-> `materialize-embeddings.ts` would otherwise form.
|
|
13
|
+
*/
|
|
14
|
+
import { AkmError, TransientError } from "../core/errors.js";
|
|
15
|
+
import { probeLock } from "../core/file-lock.js";
|
|
16
|
+
import { formatLockHolderPid } from "../core/run-lock.js";
|
|
17
|
+
import { isSqliteContentionError } from "../core/state-db.js";
|
|
18
|
+
import { indexRebuildLockPath } from "./index-rebuild-lock.js";
|
|
19
|
+
/**
|
|
20
|
+
* Read-only description of the rebuild lock's current holder, appended to a
|
|
21
|
+
* reclassified index.db contention message when known (field follow-up to
|
|
22
|
+
* #956). `probeLock` only inspects the sentinel — it never acquires or
|
|
23
|
+
* mutates it — so this is safe to call from inside an error path.
|
|
24
|
+
*/
|
|
25
|
+
function describeIndexRebuildLockHolder() {
|
|
26
|
+
const probe = probeLock(indexRebuildLockPath());
|
|
27
|
+
if (probe.state !== "held")
|
|
28
|
+
return "";
|
|
29
|
+
return ` The rebuild lock is currently held by pid ${formatLockHolderPid({
|
|
30
|
+
pid: probe.holderPid,
|
|
31
|
+
launcherPid: probe.launcherPid ?? null,
|
|
32
|
+
})}.`;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Reclassify a contention-shaped error escaping the walk, index, or
|
|
36
|
+
* embedding phase into a retryable-shortly `TransientError` (field
|
|
37
|
+
* follow-up to #956, dev-team field review 2026-09-10): a concurrent writer
|
|
38
|
+
* (another `akm index`, a source-update embedding pass, the per-command
|
|
39
|
+
* background reindex) can make index.db busy, and the raw SQLite driver
|
|
40
|
+
* error ("database is locked") used to escape as exit 70
|
|
41
|
+
* (internal/unclassified) instead of the "retry shortly" contract exit 75
|
|
42
|
+
* gives a scheduler to branch on — mirroring `STATE_DB_CONTENDED`'s
|
|
43
|
+
* precedent for state.db (`core/state-db.ts`). Reuses the ONE shared
|
|
44
|
+
* classifier, `isSqliteContentionError`, rather than a second one. An error
|
|
45
|
+
* that is already a classified akm error (e.g. a `STATE_DB_CONTENDED`
|
|
46
|
+
* TransientError from an inner state.db write) is never re-wrapped — only a
|
|
47
|
+
* raw, unclassified error matching the shared contention shape is
|
|
48
|
+
* reclassified. Every other error is rethrown unchanged.
|
|
49
|
+
*/
|
|
50
|
+
export function reclassifyIndexDbContention(error) {
|
|
51
|
+
if (error instanceof AkmError || !isSqliteContentionError(error))
|
|
52
|
+
return error;
|
|
53
|
+
const contended = new TransientError(`akm's index database is busy (another akm process is writing it); retry shortly.${describeIndexRebuildLockHolder()}`, "INDEX_DB_CONTENDED");
|
|
54
|
+
contended.cause = error;
|
|
55
|
+
return contended;
|
|
56
|
+
}
|
package/dist/indexer/indexer.js
CHANGED
|
@@ -7,14 +7,12 @@ import { detectAdapterId } from "../core/adapter/detect-adapter.js";
|
|
|
7
7
|
import { adapterForId } from "../core/adapter/registry.js";
|
|
8
8
|
import { isHttpUrl, toErrorMessage } from "../core/common.js";
|
|
9
9
|
import { concurrentMap } from "../core/concurrent.js";
|
|
10
|
-
import {
|
|
11
|
-
import { probeLock } from "../core/file-lock.js";
|
|
10
|
+
import { ConfigError } from "../core/errors.js";
|
|
12
11
|
import { defaultConcurrencyForEndpoint } from "../core/loopback.js";
|
|
13
12
|
import { classifyPathAccess, describeInaccessiblePath } from "../core/path-access.js";
|
|
14
13
|
import { getDbPath } from "../core/paths.js";
|
|
15
14
|
import { SCRIPT_EXTENSIONS } from "../core/recognition-util.js";
|
|
16
|
-
import {
|
|
17
|
-
import { isSqliteContentionError, withStateDb } from "../core/state-db.js";
|
|
15
|
+
import { withStateDb } from "../core/state-db.js";
|
|
18
16
|
import { isVerbose, warn, warnOnce, warnVerbose } from "../core/warn.js";
|
|
19
17
|
import { disposeLoweredExecutionDispatchLease, } from "../integrations/agent/execution-lowering.js";
|
|
20
18
|
import { isLlmFeatureEnabled } from "../llm/feature-gate.js";
|
|
@@ -30,7 +28,7 @@ import { upsertUtilityScore } from "../storage/repositories/index-utility-reposi
|
|
|
30
28
|
import { getEmbeddingCount, isVecAvailable, isVecFastPathReady, warnIfVecMissing, } from "../storage/repositories/index-vec-repository.js";
|
|
31
29
|
import { assertIndexedWorkflowSourceIdentity, WorkflowSourceIdentityError } from "../workflows/source-files.js";
|
|
32
30
|
import { deleteStoredGraph } from "./db/graph-db.js";
|
|
33
|
-
import {
|
|
31
|
+
import { reclassifyIndexDbContention } from "./index-db-contention.js";
|
|
34
32
|
import { deriveEntryProvenance, deriveInstallations } from "./installations.js";
|
|
35
33
|
import { indexedPathMatchesOwner, resolveAdapterConceptOwner, } from "./lookup/adapter-concept-owner.js";
|
|
36
34
|
import { generateEmbeddingsForDb } from "./materialize-embeddings.js";
|
|
@@ -374,44 +372,13 @@ let akmIndexOverride;
|
|
|
374
372
|
export function _setAkmIndexForTests(fake) {
|
|
375
373
|
akmIndexOverride = fake;
|
|
376
374
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
const probe = probeLock(indexRebuildLockPath());
|
|
385
|
-
if (probe.state !== "held")
|
|
386
|
-
return "";
|
|
387
|
-
return ` The rebuild lock is currently held by pid ${formatLockHolderPid({
|
|
388
|
-
pid: probe.holderPid,
|
|
389
|
-
launcherPid: probe.launcherPid ?? null,
|
|
390
|
-
})}.`;
|
|
391
|
-
}
|
|
392
|
-
/**
|
|
393
|
-
* Reclassify a contention-shaped error escaping the walk, index, or
|
|
394
|
-
* embedding phase into a retryable-shortly `TransientError` (field
|
|
395
|
-
* follow-up to #956, dev-team field review 2026-09-10): a concurrent writer
|
|
396
|
-
* (another `akm index`, a source-update embedding pass, the per-command
|
|
397
|
-
* background reindex) can make index.db busy, and the raw SQLite driver
|
|
398
|
-
* error ("database is locked") used to escape as exit 70
|
|
399
|
-
* (internal/unclassified) instead of the "retry shortly" contract exit 75
|
|
400
|
-
* gives a scheduler to branch on — mirroring `STATE_DB_CONTENDED`'s
|
|
401
|
-
* precedent for state.db (`core/state-db.ts`). Reuses the ONE shared
|
|
402
|
-
* classifier, `isSqliteContentionError`, rather than a second one. An error
|
|
403
|
-
* that is already a classified akm error (e.g. a `STATE_DB_CONTENDED`
|
|
404
|
-
* TransientError from an inner state.db write) is never re-wrapped — only a
|
|
405
|
-
* raw, unclassified error matching the shared contention shape is
|
|
406
|
-
* reclassified. Every other error is rethrown unchanged.
|
|
407
|
-
*/
|
|
408
|
-
export function reclassifyIndexDbContention(error) {
|
|
409
|
-
if (error instanceof AkmError || !isSqliteContentionError(error))
|
|
410
|
-
return error;
|
|
411
|
-
const contended = new TransientError(`akm's index database is busy (another akm process is writing it); retry shortly.${describeIndexRebuildLockHolder()}`, "INDEX_DB_CONTENDED");
|
|
412
|
-
contended.cause = error;
|
|
413
|
-
return contended;
|
|
414
|
-
}
|
|
375
|
+
// Moved to its own module (field follow-up to #956) so
|
|
376
|
+
// `generateEmbeddingsForDb` (materialize-embeddings.ts) can reuse the same
|
|
377
|
+
// classifier without an indexer.ts <-> materialize-embeddings.ts import
|
|
378
|
+
// cycle. Re-exported here for back-compat with existing call sites/tests
|
|
379
|
+
// that import it from `./indexer`. See index-db-contention.ts for the full
|
|
380
|
+
// rationale.
|
|
381
|
+
export { reclassifyIndexDbContention };
|
|
415
382
|
export async function akmIndex(options) {
|
|
416
383
|
try {
|
|
417
384
|
const override = akmIndexOverride;
|
|
@@ -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/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
|
+
}
|
|
@@ -7072,7 +7072,8 @@ var init_errors = __esm(() => {
|
|
|
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
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."
|
|
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."
|
|
7076
7077
|
};
|
|
7077
7078
|
NOT_FOUND_HINTS = {
|
|
7078
7079
|
ASSET_NOT_FOUND: "Run `akm search <query>` or `akm index` to refresh the index.",
|
|
@@ -76378,10 +76379,19 @@ var SearchGraphBoostSchema = exports_external.object({
|
|
|
76378
76379
|
confidenceMode: exports_external.enum(["blend"]).default("blend").optional(),
|
|
76379
76380
|
confidenceWeight: exports_external.number().finite().min(0).max(1).default(0.2).optional()
|
|
76380
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();
|
|
76381
76390
|
var SearchConfigSchema = exports_external.object({
|
|
76382
76391
|
minScore: nonNegativeNumber.optional(),
|
|
76383
76392
|
defaultExcludeTypes: exports_external.array(nonEmptyString).optional(),
|
|
76384
|
-
graphBoost: SearchGraphBoostSchema.optional()
|
|
76393
|
+
graphBoost: SearchGraphBoostSchema.optional(),
|
|
76394
|
+
curateRerank: CurateRerankConfigSchema.optional()
|
|
76385
76395
|
}).passthrough();
|
|
76386
76396
|
|
|
76387
76397
|
// src/core/config/schema/setup.ts
|
|
@@ -93483,6 +93493,7 @@ init_warn();
|
|
|
93483
93493
|
var MODEL_MAP_VERSION = 1;
|
|
93484
93494
|
var ENGINE_KEY_PATTERN = new RegExp(ENGINE_NAME_PATTERN_SOURCE);
|
|
93485
93495
|
var RESERVED_MAP_KEYS = new Set(["__proto__", "constructor", "prototype", "tostring"]);
|
|
93496
|
+
var PROFILE_FIELD_KEYS = new Set(["model", "inference", "engine"]);
|
|
93486
93497
|
|
|
93487
93498
|
// src/integrations/harnesses/opencode-sdk/sdk-runner.ts
|
|
93488
93499
|
var _servers = new Map;
|
|
@@ -97472,6 +97483,34 @@ async function embedBatch(texts, embeddingConfig, signal, onSkip, onBatch) {
|
|
|
97472
97483
|
return results;
|
|
97473
97484
|
}
|
|
97474
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
|
+
|
|
97475
97514
|
// src/indexer/materialize-embeddings.ts
|
|
97476
97515
|
function deriveSemanticProviderFingerprint(embedding) {
|
|
97477
97516
|
if (isDeterministicEmbedEnabled()) {
|
|
@@ -97882,7 +97921,8 @@ ${listed}${more}` });
|
|
|
97882
97921
|
clearInterval(heartbeatTimer);
|
|
97883
97922
|
}
|
|
97884
97923
|
} catch (error2) {
|
|
97885
|
-
const
|
|
97924
|
+
const reclassified = reclassifyIndexDbContention(error2);
|
|
97925
|
+
const message = reclassified instanceof Error ? reclassified.message : String(reclassified);
|
|
97886
97926
|
warn("Embedding generation failed, continuing without:", message);
|
|
97887
97927
|
onProgress({ phase: "embeddings", message: `Embedding generation failed: ${message}` });
|
|
97888
97928
|
return {
|
|
@@ -7071,7 +7071,8 @@ var init_errors = __esm(() => {
|
|
|
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
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."
|
|
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."
|
|
7075
7076
|
};
|
|
7076
7077
|
NOT_FOUND_HINTS = {
|
|
7077
7078
|
ASSET_NOT_FOUND: "Run `akm search <query>` or `akm index` to refresh the index.",
|
|
@@ -75706,10 +75707,19 @@ var SearchGraphBoostSchema = exports_external.object({
|
|
|
75706
75707
|
confidenceMode: exports_external.enum(["blend"]).default("blend").optional(),
|
|
75707
75708
|
confidenceWeight: exports_external.number().finite().min(0).max(1).default(0.2).optional()
|
|
75708
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();
|
|
75709
75718
|
var SearchConfigSchema = exports_external.object({
|
|
75710
75719
|
minScore: nonNegativeNumber.optional(),
|
|
75711
75720
|
defaultExcludeTypes: exports_external.array(nonEmptyString).optional(),
|
|
75712
|
-
graphBoost: SearchGraphBoostSchema.optional()
|
|
75721
|
+
graphBoost: SearchGraphBoostSchema.optional(),
|
|
75722
|
+
curateRerank: CurateRerankConfigSchema.optional()
|
|
75713
75723
|
}).passthrough();
|
|
75714
75724
|
|
|
75715
75725
|
// src/core/config/schema/setup.ts
|
|
@@ -93478,6 +93488,7 @@ init_warn();
|
|
|
93478
93488
|
var MODEL_MAP_VERSION = 1;
|
|
93479
93489
|
var ENGINE_KEY_PATTERN = new RegExp(ENGINE_NAME_PATTERN_SOURCE);
|
|
93480
93490
|
var RESERVED_MAP_KEYS = new Set(["__proto__", "constructor", "prototype", "tostring"]);
|
|
93491
|
+
var PROFILE_FIELD_KEYS = new Set(["model", "inference", "engine"]);
|
|
93481
93492
|
|
|
93482
93493
|
// src/integrations/harnesses/opencode-sdk/sdk-runner.ts
|
|
93483
93494
|
var _servers = new Map;
|
|
@@ -97428,6 +97439,34 @@ async function embedBatch(texts, embeddingConfig, signal, onSkip, onBatch) {
|
|
|
97428
97439
|
return results;
|
|
97429
97440
|
}
|
|
97430
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
|
+
|
|
97431
97470
|
// src/indexer/materialize-embeddings.ts
|
|
97432
97471
|
function deriveSemanticProviderFingerprint(embedding) {
|
|
97433
97472
|
if (isDeterministicEmbedEnabled()) {
|
|
@@ -97838,7 +97877,8 @@ ${listed}${more}` });
|
|
|
97838
97877
|
clearInterval(heartbeatTimer);
|
|
97839
97878
|
}
|
|
97840
97879
|
} catch (error2) {
|
|
97841
|
-
const
|
|
97880
|
+
const reclassified = reclassifyIndexDbContention(error2);
|
|
97881
|
+
const message = reclassified instanceof Error ? reclassified.message : String(reclassified);
|
|
97842
97882
|
warn("Embedding generation failed, continuing without:", message);
|
|
97843
97883
|
onProgress({ phase: "embeddings", message: `Embedding generation failed: ${message}` });
|
|
97844
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
|
*
|
|
@@ -22,6 +22,22 @@ fix for a scheduled or opportunistic index run is the same:
|
|
|
22
22
|
`akm index --skip-if-locked` steps aside (exit 0) instead of contending at
|
|
23
23
|
all.
|
|
24
24
|
|
|
25
|
+
Two more contention paths collided on the same "config error instead of
|
|
26
|
+
transient" anti-pattern (#948 field follow-up, 2026-09-10). `akm improve`'s
|
|
27
|
+
whole-run lock (only one `improve` runs at a time) used to fail a losing
|
|
28
|
+
contender with `{"code":"INVALID_CONFIG_FILE"}` at exit 78 when no
|
|
29
|
+
`--skip-if-locked` was passed — a config-error exit for ordinary contention
|
|
30
|
+
between two legitimate `improve` invocations. It is now `TransientError`
|
|
31
|
+
code `IMPROVE_LOCK_HELD` at exit 75, naming the current holder's pid and
|
|
32
|
+
start time; `--skip-if-locked` is unchanged (still exit 0). Separately, the
|
|
33
|
+
index rebuild's embedding-verification step (`[index:verify] Semantic search
|
|
34
|
+
verification failed: ...`) could still surface a raw, unclassified
|
|
35
|
+
`database is locked` message even though the acquisition path was already
|
|
36
|
+
fixed — that message is now built through the same `INDEX_DB_CONTENDED`
|
|
37
|
+
reclassification as the rest of the index path. This step does not throw, so
|
|
38
|
+
it does not change `akm index`'s own exit code; only the message text
|
|
39
|
+
changed.
|
|
40
|
+
|
|
25
41
|
The six shipped scheduled `improve` task templates now run with
|
|
26
42
|
`--require-engines`, which aborts (exit 78) before any index work when a
|
|
27
43
|
process's engine or credential cannot be resolved in the task's own
|
package/docs/reference/cli.md
CHANGED
|
@@ -2362,7 +2362,7 @@ akm improve report --since 7d # ...aggregated over every real run start
|
|
|
2362
2362
|
| `--require-feedback-signal` | Only process assets with recent feedback signals |
|
|
2363
2363
|
| `--strategy <name>` | Override the active improve strategy (a built-in or entry under `improve.strategies`) |
|
|
2364
2364
|
| `--json-to-stdout` | Also emit the full persisted JSON result on stdout for a live run. Without this flag, stdout stays empty. Dry-runs always emit their result and are never persisted. |
|
|
2365
|
-
| `--skip-if-locked` | If another improve run already holds the lock, skip gracefully (exit 0) instead of failing with "already running" (exit
|
|
2365
|
+
| `--skip-if-locked` | If another improve run already holds the lock, skip gracefully (exit 0) instead of failing with "already running" (exit 75, `TransientError`, code `IMPROVE_LOCK_HELD` — field follow-up to #948: two legitimate `improve` invocations colliding on this lock is ordinary, retryable contention, not a broken config file). Use for high-frequency scheduled runs so they don't pile up failures while a longer run is in progress. |
|
|
2366
2366
|
| `--require-engines` | Abort (exit 78, before any indexing, lock, or log side effect) if the active strategy would enable a process whose engine or credential cannot be resolved in this process's environment, OR whose endpoint fails a bounded reachability probe — the same probe `akm health`'s `default-llm-engine`/`configured-engines` checks run, once per distinct endpoint. Without this flag, improve degrades gracefully: it skips the affected processes and reports them in the result's `skippedProcesses`. Recommended alongside `--skip-if-locked` for scheduled runs, since the operator's own shell can pass config validation while a scheduler's stripped-down environment (see #953) cannot. |
|
|
2367
2367
|
| `--show-prompt` | Print the composed reflect prompt (#952) for one asset and exit — before any lock, index write, or engine dispatch. Requires a fully-qualified asset ref as the scope (`akm improve lessons/my-lesson --show-prompt`); rejected with a type or whole-bundle scope. The default output format is JSON, which carries the prompt as a `prompt` field (escaped into one line) alongside the resolved `engine`/`engineKind`; pass `--format text` to print the prompt itself, unwrapped and readable by eye. |
|
|
2368
2368
|
| `--sync` / `--no-sync` | Commit (and optionally push) the git-backed primary bundle when the run finishes. Default: on for git-backed bundles (per profile config). |
|
|
@@ -514,6 +514,23 @@ taking about the same wall time as a single one against a healthy endpoint.
|
|
|
514
514
|
| --- | --- |
|
|
515
515
|
| `search.graphBoost.*` | Entity-graph relevance boost: `directBoostPerEntity`/`directBoostCap` (directly related entities), `hopBoostPerEntity`/`hopBoostCap` (multi-hop, capped at `maxHops` ≤ 3), `confidenceMode` (`blend`, the only supported value), `confidenceWeight` (0–1, default `0.2`) |
|
|
516
516
|
|
|
517
|
+
### Curate rerank (#951)
|
|
518
|
+
|
|
519
|
+
An optional cross-encoder rerank pass over `akm curate`'s already-selected
|
|
520
|
+
candidates, via a standalone `/rerank`-style HTTP endpoint (NOT one of the
|
|
521
|
+
`engines.*` `"llm"`/`"agent"` kinds). Disabled by default; a misconfigured
|
|
522
|
+
endpoint, network failure, timeout, or malformed response falls back to
|
|
523
|
+
curate's own ranking unchanged.
|
|
524
|
+
|
|
525
|
+
| Key | Purpose |
|
|
526
|
+
| --- | --- |
|
|
527
|
+
| `search.curateRerank.enabled` | Turn the rerank pass on (default `false`) |
|
|
528
|
+
| `search.curateRerank.endpoint` | Full URL of the reranker's rerank endpoint |
|
|
529
|
+
| `search.curateRerank.model` | Model name sent to the endpoint (optional) |
|
|
530
|
+
| `search.curateRerank.apiKey` | `$VAR`/`secret://<name>` credential reference (optional) |
|
|
531
|
+
| `search.curateRerank.timeoutMs` | Request timeout (default `10000`) |
|
|
532
|
+
| `search.curateRerank.topN` | How many of curate's ranked candidates to send (default `8`, max `50`) |
|
|
533
|
+
|
|
517
534
|
## Feedback
|
|
518
535
|
|
|
519
536
|
`feedback` shapes the `akm feedback` taxonomy:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akm-cli",
|
|
3
|
-
"version": "0.9.15-beta.
|
|
3
|
+
"version": "0.9.15-beta.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "akm (Agent Knowledge Manager) — a portable, local-first capability library for AI agents. Discover, load, share, and improve reusable skills, scripts, workflows, and knowledge across any shell-capable coding agent, including Claude Code, OpenCode, and Cursor.",
|
|
6
6
|
"keywords": [
|
package/schemas/akm-config.json
CHANGED
|
@@ -596,6 +596,34 @@
|
|
|
596
596
|
}
|
|
597
597
|
},
|
|
598
598
|
"additionalProperties": true
|
|
599
|
+
},
|
|
600
|
+
"curateRerank": {
|
|
601
|
+
"type": "object",
|
|
602
|
+
"properties": {
|
|
603
|
+
"enabled": {
|
|
604
|
+
"type": "boolean"
|
|
605
|
+
},
|
|
606
|
+
"endpoint": {
|
|
607
|
+
"type": "string"
|
|
608
|
+
},
|
|
609
|
+
"model": {
|
|
610
|
+
"type": "string",
|
|
611
|
+
"minLength": 1
|
|
612
|
+
},
|
|
613
|
+
"apiKey": {
|
|
614
|
+
"type": "string"
|
|
615
|
+
},
|
|
616
|
+
"timeoutMs": {
|
|
617
|
+
"type": "integer",
|
|
618
|
+
"exclusiveMinimum": 0
|
|
619
|
+
},
|
|
620
|
+
"topN": {
|
|
621
|
+
"type": "integer",
|
|
622
|
+
"exclusiveMinimum": 0,
|
|
623
|
+
"maximum": 50
|
|
624
|
+
}
|
|
625
|
+
},
|
|
626
|
+
"additionalProperties": true
|
|
599
627
|
}
|
|
600
628
|
},
|
|
601
629
|
"additionalProperties": true
|
|
@@ -2296,6 +2324,34 @@
|
|
|
2296
2324
|
}
|
|
2297
2325
|
},
|
|
2298
2326
|
"additionalProperties": true
|
|
2327
|
+
},
|
|
2328
|
+
"curateRerank": {
|
|
2329
|
+
"type": "object",
|
|
2330
|
+
"properties": {
|
|
2331
|
+
"enabled": {
|
|
2332
|
+
"type": "boolean"
|
|
2333
|
+
},
|
|
2334
|
+
"endpoint": {
|
|
2335
|
+
"type": "string"
|
|
2336
|
+
},
|
|
2337
|
+
"model": {
|
|
2338
|
+
"type": "string",
|
|
2339
|
+
"minLength": 1
|
|
2340
|
+
},
|
|
2341
|
+
"apiKey": {
|
|
2342
|
+
"type": "string"
|
|
2343
|
+
},
|
|
2344
|
+
"timeoutMs": {
|
|
2345
|
+
"type": "integer",
|
|
2346
|
+
"exclusiveMinimum": 0
|
|
2347
|
+
},
|
|
2348
|
+
"topN": {
|
|
2349
|
+
"type": "integer",
|
|
2350
|
+
"exclusiveMinimum": 0,
|
|
2351
|
+
"maximum": 50
|
|
2352
|
+
}
|
|
2353
|
+
},
|
|
2354
|
+
"additionalProperties": true
|
|
2299
2355
|
}
|
|
2300
2356
|
},
|
|
2301
2357
|
"additionalProperties": true
|