akm-cli 0.9.14 → 0.9.15-beta.1
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 +397 -0
- package/STABILITY.md +6 -3
- package/dist/assets/prompts/reflect-feedback-framing.md +1 -0
- package/dist/assets/prompts/reflect-llm-framed-contract.md +2 -0
- package/dist/assets/prompts/reflect-llm-schema-contract.md +2 -0
- package/dist/assets/tasks/core/improve.yml +1 -1
- package/dist/assets/tasks/core/index-refresh.yml +1 -1
- package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +1 -1
- package/dist/assets/tasks/improve/akm-improve-catchup.yml +1 -1
- package/dist/assets/tasks/improve/akm-improve-consolidate.yml +1 -1
- package/dist/assets/tasks/improve/akm-improve-frequent.yml +1 -1
- package/dist/assets/tasks/improve/akm-improve-nightly.yml +1 -1
- package/dist/cli/retired-commands.js +0 -1
- package/dist/cli/shared.js +9 -0
- package/dist/cli/unknown-flags.js +1 -0
- package/dist/cli.js +3 -2
- package/dist/commands/config-cli.js +85 -3
- package/dist/commands/env/env-cli.js +1 -42
- package/dist/commands/env/env.js +1 -1
- package/dist/commands/env/secret-cli.js +1 -2
- package/dist/commands/health/checks.js +357 -63
- package/dist/commands/health/engine-usage.js +45 -0
- package/dist/commands/health/improve-metrics.js +18 -0
- package/dist/commands/health/llm-usage.js +41 -1
- package/dist/commands/health/plugin-staleness.js +7 -3
- package/dist/commands/health/version-drift.js +93 -0
- package/dist/commands/health/windows.js +3 -1
- package/dist/commands/health.js +44 -9
- package/dist/commands/improve/consolidate/chunking.js +4 -2
- package/dist/commands/improve/improve-cli.js +99 -5
- package/dist/commands/improve/improve-report.js +154 -0
- package/dist/commands/improve/improve-result-file.js +45 -33
- package/dist/commands/improve/improve-strategies.js +133 -3
- package/dist/commands/improve/improve-usage-report.js +182 -0
- package/dist/commands/improve/improve.js +40 -3
- package/dist/commands/improve/locks.js +27 -78
- package/dist/commands/improve/planner.js +1 -0
- package/dist/commands/improve/preparation.js +9 -1
- package/dist/commands/improve/reflect.js +44 -4
- package/dist/commands/models-cli.js +50 -1
- package/dist/commands/proposal/repository.js +8 -3
- package/dist/commands/proposal/validators/proposal-quality-validators.js +41 -6
- package/dist/commands/proposal/validators/proposal-validators.js +24 -0
- package/dist/commands/read/search-cli.js +38 -2
- package/dist/commands/read/show.js +103 -4
- package/dist/commands/sources/info.js +5 -1
- package/dist/commands/sources/self-update.js +2 -2
- package/dist/commands/sources/stash-cli.js +31 -0
- package/dist/commands/tasks/tasks-cli.js +49 -2
- package/dist/commands/workflow-cli.js +86 -12
- package/dist/core/asset/markdown-fragments.js +35 -0
- package/dist/core/config/config-schema.js +14 -0
- package/dist/core/config/config.js +302 -24
- package/dist/core/env-secret-ref.js +58 -5
- package/dist/core/errors.js +30 -0
- package/dist/core/improve-result.js +51 -0
- package/dist/core/loopback.js +17 -0
- package/dist/core/paths.js +11 -0
- package/dist/core/run-lock.js +96 -0
- package/dist/core/sensitive-marker-path.js +19 -0
- package/dist/core/state-db.js +74 -14
- package/dist/indexer/index-rebuild-lock.js +73 -0
- package/dist/indexer/index-writer-lock.js +40 -1
- package/dist/indexer/index-written-assets.js +21 -1
- package/dist/indexer/indexer.js +18 -17
- package/dist/indexer/materialize-embeddings.js +282 -32
- package/dist/indexer/search/db-search.js +49 -2
- package/dist/integrations/agent/engine-resolution.js +96 -6
- package/dist/integrations/agent/execution-definitions.js +6 -15
- package/dist/integrations/agent/execution-lowering.js +6 -1
- package/dist/integrations/agent/execution-preparation.js +1 -1
- package/dist/integrations/agent/model-map.js +123 -20
- package/dist/integrations/agent/prompts.js +40 -8
- package/dist/integrations/agent/runner-dispatch.js +9 -3
- package/dist/integrations/agent/runner.js +2 -0
- package/dist/llm/client.js +8 -3
- package/dist/llm/embedder.js +20 -8
- package/dist/llm/embedders/local.js +10 -2
- package/dist/llm/embedders/remote.js +188 -21
- package/dist/output/shapes/helpers.js +38 -2
- package/dist/output/shapes/models-list.js +16 -0
- package/dist/output/shapes/passthrough.js +2 -0
- package/dist/output/shapes.js +4 -0
- package/dist/output/text/command-format.js +29 -0
- package/dist/output/text/helpers.js +1 -1
- package/dist/output/text/improve-report.js +27 -0
- package/dist/{commands/env/marker-path.js → output/text/models.js} +4 -3
- package/dist/output/text/show-format.js +4 -0
- package/dist/output/text.js +4 -0
- package/dist/scripts/akm-migrate-node.js +24798 -21732
- package/dist/scripts/akm-migrate.js +23408 -20343
- package/dist/storage/repositories/improve-runs-repository.js +34 -0
- package/dist/storage/repositories/index-fts-repository.js +49 -6
- package/dist/storage/repositories/index-vec-repository.js +30 -0
- package/dist/storage/repositories/workflow-runs-repository.js +55 -18
- package/dist/tasks/backends/cron.js +14 -7
- package/dist/tasks/run/run-workflow-task.js +16 -0
- package/dist/workflows/exec/child-workflow.js +2 -2
- package/dist/workflows/exec/dispatch-redaction.js +21 -9
- package/dist/workflows/exec/run-workflow.js +6 -5
- package/dist/workflows/runtime/runs.js +33 -5
- package/docs/migration/release-notes/0.9.15.md +52 -0
- package/docs/migration/release-notes/README.md +4 -0
- package/docs/reference/cli.md +245 -29
- package/docs/reference/configuration.md +180 -19
- package/docs/reference/data-and-telemetry.md +8 -0
- package/docs/reference/tasks.md +16 -1
- package/docs/reference/workflow-schema.md +5 -1
- package/package.json +1 -1
- package/schemas/akm-config.json +8 -0
package/dist/indexer/indexer.js
CHANGED
|
@@ -8,7 +8,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
10
|
import { ConfigError } from "../core/errors.js";
|
|
11
|
-
import {
|
|
11
|
+
import { defaultConcurrencyForEndpoint } from "../core/loopback.js";
|
|
12
12
|
import { classifyPathAccess, describeInaccessiblePath } from "../core/path-access.js";
|
|
13
13
|
import { getDbPath } from "../core/paths.js";
|
|
14
14
|
import { SCRIPT_EXTENSIONS } from "../core/recognition-util.js";
|
|
@@ -54,20 +54,17 @@ function throwIfAborted(signal) {
|
|
|
54
54
|
export function getDefaultLlmConcurrency(llmConfig) {
|
|
55
55
|
if (typeof llmConfig?.concurrency === "number")
|
|
56
56
|
return llmConfig.concurrency;
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
// does NOT copy it into the resolved connection, so on the enrichment path the
|
|
69
|
-
// auto-derived 1/2 is what runs (see docs/architecture/internals/indexing.md).
|
|
70
|
-
return 2;
|
|
57
|
+
// ONE classifier decides the local-vs-remote default (`core/loopback.ts`'s
|
|
58
|
+
// `defaultConcurrencyForEndpoint`), shared with the embedding pool
|
|
59
|
+
// (`resolveEmbeddingConcurrency`, `src/llm/embedders/remote.ts`) and the
|
|
60
|
+
// workflow engine's frozen concurrency default.
|
|
61
|
+
//
|
|
62
|
+
// The explicit-override branch above only fires for callers that put
|
|
63
|
+
// `concurrency` on the connection themselves — `engines.<name>.concurrency`
|
|
64
|
+
// is a valid schema field but `resolveLlmEngineUse` does NOT copy it into
|
|
65
|
+
// the resolved connection, so on the enrichment path the auto-derived 1/2
|
|
66
|
+
// is what runs (see docs/architecture/internals/indexing.md).
|
|
67
|
+
return defaultConcurrencyForEndpoint(llmConfig?.endpoint);
|
|
71
68
|
}
|
|
72
69
|
function sourceOwners(sources) {
|
|
73
70
|
const installations = deriveInstallations([...sources]);
|
|
@@ -192,14 +189,16 @@ async function runWalkPhase(ctx) {
|
|
|
192
189
|
* entries. Writes `ctx.embeddingResult` for the finalize phase.
|
|
193
190
|
*/
|
|
194
191
|
async function runEmbeddingPhase(ctx) {
|
|
195
|
-
const { db, config, signal, onProgress } = ctx;
|
|
192
|
+
const { db, config, signal, onProgress, reembed } = ctx;
|
|
196
193
|
throwIfAborted(signal);
|
|
197
194
|
// Forward the signal. Without it generateEmbeddingsForDb's abort machinery was
|
|
198
195
|
// inert — its throwIfAborted checks and the signal it threads into embedBatch
|
|
199
196
|
// (which RemoteEmbedder passes to every fetch and LocalEmbedder honours between
|
|
200
197
|
// chunks) never saw a controller. Ctrl-C and the improve budget abort could not
|
|
201
198
|
// stop the embedding phase, the longest phase of an index run.
|
|
202
|
-
ctx.embeddingResult = await generateEmbeddingsForDb(db, config, onProgress, signal
|
|
199
|
+
ctx.embeddingResult = await generateEmbeddingsForDb(db, config, onProgress, signal, undefined, {
|
|
200
|
+
forceReembed: reembed,
|
|
201
|
+
});
|
|
203
202
|
ctx.timing.tEmbedEnd = Date.now();
|
|
204
203
|
}
|
|
205
204
|
/**
|
|
@@ -489,6 +488,7 @@ async function akmIndexReal(options) {
|
|
|
489
488
|
const full = options?.full === true;
|
|
490
489
|
const clean = options?.clean === true;
|
|
491
490
|
const dryRun = options?.dryRun === true;
|
|
491
|
+
const reembed = options?.reembed === true;
|
|
492
492
|
// Load config and resolve all stash sources
|
|
493
493
|
const { loadConfig, mutateConfig } = await import("../core/config/config.js");
|
|
494
494
|
let config = loadConfig();
|
|
@@ -548,6 +548,7 @@ async function akmIndexReal(options) {
|
|
|
548
548
|
sourceDirs: allSourceDirs,
|
|
549
549
|
full,
|
|
550
550
|
clean,
|
|
551
|
+
reembed,
|
|
551
552
|
stashDir,
|
|
552
553
|
onProgress,
|
|
553
554
|
signal,
|
|
@@ -6,9 +6,10 @@ import { embedBatch } from "../llm/embedder.js";
|
|
|
6
6
|
import { DETERMINISTIC_EMBED_MODEL_ID, isDeterministicEmbedEnabled } from "../llm/embedders/deterministic.js";
|
|
7
7
|
import { DEFAULT_LOCAL_MODEL } from "../llm/embedders/local.js";
|
|
8
8
|
import { buildTokenBoundedBatches, DEFAULT_REMOTE_BATCH_SIZE, DEFAULT_TOKEN_BUDGET, estimateTokenCount, hasRemoteEndpoint, } from "../llm/embedders/remote.js";
|
|
9
|
+
import { cosineSimilarity } from "../llm/embedders/types.js";
|
|
9
10
|
import { getEmbeddableEntryCount } from "../storage/repositories/index-entries-repository.js";
|
|
10
11
|
import { deleteMeta, getMeta, setMeta } from "../storage/repositories/index-meta-repository.js";
|
|
11
|
-
import { getAllEntriesForEmbedding, getEmbeddingCount, isVecFastPathComplete, isVecFastPathReady, purgeEmbeddings, setVecFastPathReady, upsertEmbedding, } from "../storage/repositories/index-vec-repository.js";
|
|
12
|
+
import { getAllEntriesForEmbedding, getEmbeddingCount, isVecFastPathComplete, isVecFastPathReady, purgeEmbeddings, sampleEmbeddedEntriesForCanary, setVecFastPathReady, upsertEmbedding, } from "../storage/repositories/index-vec-repository.js";
|
|
12
13
|
/** Identifies the embedding provider+model+dimension a stored vector was generated with. */
|
|
13
14
|
export function deriveSemanticProviderFingerprint(embedding) {
|
|
14
15
|
if (isDeterministicEmbedEnabled()) {
|
|
@@ -22,12 +23,159 @@ export function deriveSemanticProviderFingerprint(embedding) {
|
|
|
22
23
|
}
|
|
23
24
|
return `local:${embedding?.localModel ?? DEFAULT_LOCAL_MODEL}`;
|
|
24
25
|
}
|
|
26
|
+
/** How often (in stored entries) to emit a progress line during a large embedding run (#954). */
|
|
27
|
+
const PROGRESS_INTERVAL = 500;
|
|
28
|
+
/**
|
|
29
|
+
* Number of already-embedded entries sampled for the fingerprint-rename
|
|
30
|
+
* canary (#955) — small and cheap even against a slow local server; a
|
|
31
|
+
* handful of chunks is a strong compatibility signal (a different model
|
|
32
|
+
* cannot plausibly land near-identical vectors by chance).
|
|
33
|
+
*/
|
|
34
|
+
const CANARY_SAMPLE_SIZE = 8;
|
|
35
|
+
/**
|
|
36
|
+
* Minimum median cosine similarity between stored and freshly re-embedded
|
|
37
|
+
* canary vectors for a fingerprint-string change to be treated as a
|
|
38
|
+
* same-model rename rather than a real model change (#955).
|
|
39
|
+
*/
|
|
40
|
+
const CANARY_SIMILARITY_THRESHOLD = 0.999;
|
|
41
|
+
/**
|
|
42
|
+
* Pure decision: do stored vectors remain valid against freshly re-embedded
|
|
43
|
+
* canary samples? The ONE place that computes the canary's similarity
|
|
44
|
+
* numbers — callers must use this result rather than recomputing it (#955).
|
|
45
|
+
*
|
|
46
|
+
* An empty sample means nothing is stored to lose or verify against, so
|
|
47
|
+
* there is nothing to decide — keep.
|
|
48
|
+
*
|
|
49
|
+
* A sample whose re-embed FAILED (`fresh === undefined`, e.g. a provider
|
|
50
|
+
* sub-batch that was skipped) is EXCLUDED from the similarity computation
|
|
51
|
+
* entirely, not scored as zero: a partial provider failure is not evidence
|
|
52
|
+
* of a different model (#955). A dimension mismatch on a successful
|
|
53
|
+
* re-embed still counts as zero similarity via {@link cosineSimilarity}'s
|
|
54
|
+
* own dimension-mismatch guard — that IS evidence. When half or fewer of
|
|
55
|
+
* the sampled entries re-embedded successfully, the sample is too thin to
|
|
56
|
+
* trust either verdict — the outcome is `unverifiable`, the same outcome a
|
|
57
|
+
* total canary failure already produces.
|
|
58
|
+
*
|
|
59
|
+
* Otherwise the MEDIAN pairwise cosine similarity of the verified samples
|
|
60
|
+
* must clear {@link CANARY_SIMILARITY_THRESHOLD}; the median (not the
|
|
61
|
+
* minimum or mean) tolerates one stale or lightly-edited sample without
|
|
62
|
+
* either discarding a real match or being fooled by it.
|
|
63
|
+
*/
|
|
64
|
+
export function decideEmbeddingCompatibility(pairs) {
|
|
65
|
+
if (pairs.length === 0)
|
|
66
|
+
return { outcome: "keep", medianSimilarity: undefined, verifiedSamples: 0 };
|
|
67
|
+
const verified = pairs.filter((pair) => pair.fresh !== undefined);
|
|
68
|
+
if (verified.length * 2 <= pairs.length) {
|
|
69
|
+
return { outcome: "unverifiable", medianSimilarity: undefined, verifiedSamples: verified.length };
|
|
70
|
+
}
|
|
71
|
+
const similarities = verified.map((pair) => cosineSimilarity(pair.stored, pair.fresh));
|
|
72
|
+
const medianSimilarity = medianOf(similarities);
|
|
73
|
+
return {
|
|
74
|
+
outcome: medianSimilarity >= CANARY_SIMILARITY_THRESHOLD ? "keep" : "rebuild",
|
|
75
|
+
medianSimilarity,
|
|
76
|
+
verifiedSamples: verified.length,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function medianOf(values) {
|
|
80
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
81
|
+
const mid = Math.floor(sorted.length / 2);
|
|
82
|
+
return sorted.length % 2 === 0
|
|
83
|
+
? (sorted[mid - 1] + sorted[mid]) / 2
|
|
84
|
+
: sorted[mid];
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Identity of the embedding vectors actually observed on a run — as opposed
|
|
88
|
+
* to {@link deriveSemanticProviderFingerprint}'s CONFIG-derived string. Keys
|
|
89
|
+
* on what the server (or local model) actually reported plus the observed
|
|
90
|
+
* vector width, so a gateway/transport change that keeps returning the same
|
|
91
|
+
* underlying model can be told apart from a genuine model change without
|
|
92
|
+
* relying on the operator's config string (#955 field-review addendum).
|
|
93
|
+
* Returns undefined when nothing was actually observed this call (no vector
|
|
94
|
+
* to measure yet).
|
|
95
|
+
*/
|
|
96
|
+
function deriveObservedEmbeddingIdentity(embedding, observedModel, observedVectorLen) {
|
|
97
|
+
if (isDeterministicEmbedEnabled()) {
|
|
98
|
+
return `deterministic:${DETERMINISTIC_EMBED_MODEL_ID}`;
|
|
99
|
+
}
|
|
100
|
+
if (observedVectorLen === undefined)
|
|
101
|
+
return undefined;
|
|
102
|
+
if (embedding?.endpoint) {
|
|
103
|
+
return `remote:${observedModel ?? embedding.model ?? "unknown"}|${observedVectorLen}`;
|
|
104
|
+
}
|
|
105
|
+
return `local:${embedding?.localModel ?? DEFAULT_LOCAL_MODEL}|${observedVectorLen}`;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Run the fingerprint-rename canary: re-embed a small sample of already-
|
|
109
|
+
* stored entries with the CURRENT config and decide whether the stored
|
|
110
|
+
* index survives. Goes through the standard {@link embedBatch} facade (not a
|
|
111
|
+
* direct `RemoteEmbedder`) so every embedder branch — remote, local,
|
|
112
|
+
* deterministic, and test overrides via `_setEmbedderForTests` — is
|
|
113
|
+
* exercised identically to the main embedding pass.
|
|
114
|
+
*/
|
|
115
|
+
async function runEmbeddingCanary(db, config, signal) {
|
|
116
|
+
const samples = sampleEmbeddedEntriesForCanary(db, CANARY_SAMPLE_SIZE);
|
|
117
|
+
if (samples.length === 0) {
|
|
118
|
+
return { outcome: "keep", verified: false, viaIdentityMatch: false };
|
|
119
|
+
}
|
|
120
|
+
let observedModel;
|
|
121
|
+
const skips = [];
|
|
122
|
+
let canaryVectors;
|
|
123
|
+
try {
|
|
124
|
+
canaryVectors = await embedBatch(samples.map((sample) => sample.searchText), config.embedding, signal, (skip) => skips.push(skip), (_indices, _embeddings, model) => {
|
|
125
|
+
if (model)
|
|
126
|
+
observedModel = model;
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
131
|
+
return {
|
|
132
|
+
outcome: "unverifiable",
|
|
133
|
+
message: `could not verify embedding compatibility (${message}); keeping existing vectors — rerun akm index when the endpoint is reachable`,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
const observedVectorLen = canaryVectors.find((vector) => vector !== undefined)?.length;
|
|
137
|
+
const observedIdentity = deriveObservedEmbeddingIdentity(config.embedding, observedModel, observedVectorLen);
|
|
138
|
+
const storedIdentity = getMeta(db, "embeddingIdentity");
|
|
139
|
+
if (storedIdentity && observedIdentity && storedIdentity === observedIdentity) {
|
|
140
|
+
// The server reports the same model identity as last time — no need to
|
|
141
|
+
// even look at the cosines; the config string alone was misleading.
|
|
142
|
+
return { outcome: "keep", verified: true, identity: observedIdentity, viaIdentityMatch: true };
|
|
143
|
+
}
|
|
144
|
+
const pairs = samples.map((sample, i) => ({ stored: sample.vector, fresh: canaryVectors[i] }));
|
|
145
|
+
const decision = decideEmbeddingCompatibility(pairs);
|
|
146
|
+
if (decision.outcome === "unverifiable") {
|
|
147
|
+
// Covers both a total provider failure (RemoteEmbedder skips a failing
|
|
148
|
+
// request rather than throwing, #874, so an unreachable endpoint
|
|
149
|
+
// surfaces here as an all-`undefined` canary result, not a caught
|
|
150
|
+
// exception) and a partial one thin enough that neither verdict can be
|
|
151
|
+
// trusted (#955) — same message path either way.
|
|
152
|
+
const message = skips[0]?.message ?? "embedding provider returned no vectors for the canary sample";
|
|
153
|
+
return {
|
|
154
|
+
outcome: "unverifiable",
|
|
155
|
+
message: `could not verify embedding compatibility (${message}); keeping existing vectors — rerun akm index when the endpoint is reachable`,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
if (decision.outcome === "keep") {
|
|
159
|
+
return {
|
|
160
|
+
outcome: "keep",
|
|
161
|
+
verified: true,
|
|
162
|
+
identity: observedIdentity,
|
|
163
|
+
viaIdentityMatch: false,
|
|
164
|
+
medianSimilarity: decision.medianSimilarity,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
outcome: "rebuild",
|
|
169
|
+
identity: observedIdentity,
|
|
170
|
+
reason: `vectors differ (median similarity ${decision.medianSimilarity?.toFixed(3)})`,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
25
173
|
function throwIfAborted(signal) {
|
|
26
174
|
if (signal?.aborted) {
|
|
27
175
|
throw signal.reason instanceof Error ? signal.reason : new Error("index interrupted");
|
|
28
176
|
}
|
|
29
177
|
}
|
|
30
|
-
export async function generateEmbeddingsForDb(db, config, onProgress, signal, entryIds) {
|
|
178
|
+
export async function generateEmbeddingsForDb(db, config, onProgress, signal, entryIds, opts) {
|
|
31
179
|
throwIfAborted(signal);
|
|
32
180
|
if (config.semanticSearchMode === "off") {
|
|
33
181
|
onProgress({ phase: "embeddings", message: "Semantic search disabled; skipping embeddings." });
|
|
@@ -41,12 +189,69 @@ export async function generateEmbeddingsForDb(db, config, onProgress, signal, en
|
|
|
41
189
|
const currentFingerprint = deriveSemanticProviderFingerprint(config.embedding);
|
|
42
190
|
const storedFingerprint = getMeta(db, "embeddingFingerprint");
|
|
43
191
|
let targetEntryIds = entryIds;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
//
|
|
48
|
-
//
|
|
192
|
+
/** Set only on an actual rebuild, so the up-front "Re-embedding N entries" line names why. */
|
|
193
|
+
let rebuildReason;
|
|
194
|
+
if (opts?.forceReembed) {
|
|
195
|
+
// `akm index --reembed`: an explicit operator override, skips the canary
|
|
196
|
+
// entirely. The new fingerprint (and identity, now stale/unknown until
|
|
197
|
+
// the next successful pass observes it) is written in the SAME
|
|
198
|
+
// transaction as the purge, before any embedding request — a restart
|
|
199
|
+
// then sees a matching fingerprint and only heals what is still missing
|
|
200
|
+
// instead of purging again from zero (#955/#956).
|
|
201
|
+
db.transaction(() => {
|
|
202
|
+
purgeEmbeddings(db, { dropVecTable: true });
|
|
203
|
+
deleteMeta(db, "embeddingDim");
|
|
204
|
+
setMeta(db, "embeddingFingerprint", currentFingerprint);
|
|
205
|
+
deleteMeta(db, "embeddingIdentity");
|
|
206
|
+
})();
|
|
49
207
|
targetEntryIds = undefined;
|
|
208
|
+
rebuildReason = "forced by --reembed";
|
|
209
|
+
}
|
|
210
|
+
else if (storedFingerprint && storedFingerprint !== currentFingerprint) {
|
|
211
|
+
const decision = await runEmbeddingCanary(db, config, signal);
|
|
212
|
+
if (decision.outcome === "unverifiable") {
|
|
213
|
+
// Destroying a good index because the server happens to be down right
|
|
214
|
+
// now is worse than leaving a rename unverified until the next run —
|
|
215
|
+
// keep the vectors AND the old fingerprint so the next `akm index`
|
|
216
|
+
// retries the canary instead of silently treating this as resolved.
|
|
217
|
+
warn(`[embed] ${decision.message}`);
|
|
218
|
+
onProgress({ phase: "embeddings", message: decision.message });
|
|
219
|
+
return { success: false, message: decision.message };
|
|
220
|
+
}
|
|
221
|
+
if (decision.outcome === "rebuild") {
|
|
222
|
+
db.transaction(() => {
|
|
223
|
+
purgeEmbeddings(db, { dropVecTable: true });
|
|
224
|
+
deleteMeta(db, "embeddingDim");
|
|
225
|
+
setMeta(db, "embeddingFingerprint", currentFingerprint);
|
|
226
|
+
if (decision.identity)
|
|
227
|
+
setMeta(db, "embeddingIdentity", decision.identity);
|
|
228
|
+
else
|
|
229
|
+
deleteMeta(db, "embeddingIdentity");
|
|
230
|
+
})();
|
|
231
|
+
targetEntryIds = undefined;
|
|
232
|
+
rebuildReason = decision.reason;
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
// Keep: adopt the new fingerprint (and identity, when observed)
|
|
236
|
+
// immediately rather than deferring to end-of-run — nothing was
|
|
237
|
+
// purged, so there is nothing an interruption could lose, and an
|
|
238
|
+
// immediate write means a crash right after this decision does not
|
|
239
|
+
// re-run the canary needlessly on the next attempt.
|
|
240
|
+
setMeta(db, "embeddingFingerprint", currentFingerprint);
|
|
241
|
+
if (decision.identity)
|
|
242
|
+
setMeta(db, "embeddingIdentity", decision.identity);
|
|
243
|
+
if (decision.verified) {
|
|
244
|
+
const keptCount = getEmbeddingCount(db);
|
|
245
|
+
const detail = decision.viaIdentityMatch
|
|
246
|
+
? "server-reported model unchanged"
|
|
247
|
+
: `stored vectors are compatible (median similarity ${decision.medianSimilarity?.toFixed(3)})`;
|
|
248
|
+
const message = `[embed] embedding model renamed (${storedFingerprint} → ${currentFingerprint}); ${detail}, keeping ${keptCount} embedding${keptCount === 1 ? "" : "s"}.`;
|
|
249
|
+
warn(message);
|
|
250
|
+
onProgress({ phase: "embeddings", message });
|
|
251
|
+
}
|
|
252
|
+
// Empty-sample case (decision.verified === false): nothing stored to
|
|
253
|
+
// lose or verify against — adopt the label silently, no purge line.
|
|
254
|
+
}
|
|
50
255
|
}
|
|
51
256
|
try {
|
|
52
257
|
throwIfAborted(signal);
|
|
@@ -56,6 +261,11 @@ export async function generateEmbeddingsForDb(db, config, onProgress, signal, en
|
|
|
56
261
|
setMeta(db, "embeddingFingerprint", currentFingerprint);
|
|
57
262
|
return { success: true };
|
|
58
263
|
}
|
|
264
|
+
if (rebuildReason) {
|
|
265
|
+
const message = `[embed] Re-embedding ${allEntries.length} entr${allEntries.length === 1 ? "y" : "ies"} because ${rebuildReason}`;
|
|
266
|
+
warn(message);
|
|
267
|
+
onProgress({ phase: "embeddings", message });
|
|
268
|
+
}
|
|
59
269
|
onProgress({
|
|
60
270
|
phase: "embeddings",
|
|
61
271
|
message: `Generating embeddings for ${allEntries.length} entr${allEntries.length === 1 ? "y" : "ies"}.`,
|
|
@@ -94,42 +304,77 @@ export async function generateEmbeddingsForDb(db, config, onProgress, signal, en
|
|
|
94
304
|
}
|
|
95
305
|
}
|
|
96
306
|
let heartbeatTimer;
|
|
307
|
+
let storedCount = 0;
|
|
308
|
+
let skippedCount = 0;
|
|
309
|
+
let embedFailedCount = 0;
|
|
310
|
+
let vecFailedCount = 0;
|
|
311
|
+
let vecUnavailableCount = 0;
|
|
312
|
+
let storedTokens = 0;
|
|
313
|
+
let lastProgressBucket = 0;
|
|
97
314
|
try {
|
|
98
315
|
heartbeatTimer = setInterval(() => {
|
|
99
316
|
onProgress({
|
|
100
317
|
phase: "embeddings",
|
|
101
|
-
message: `Still generating embeddings
|
|
318
|
+
message: `Still generating embeddings: ${storedCount}/${allEntries.length} stored; waiting on embedding provider.`,
|
|
102
319
|
});
|
|
103
320
|
}, 15000);
|
|
104
321
|
// A failing sub-batch or an oversized document is SKIPPED by embedBatch,
|
|
105
322
|
// not thrown (#874) — collect what couldn't be embedded and why, so a
|
|
106
323
|
// few bad documents don't discard every other entry's embedding.
|
|
107
324
|
const skips = [];
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
325
|
+
const embedStart = Date.now();
|
|
326
|
+
// Commit each provider batch in its own short transaction as it lands,
|
|
327
|
+
// rather than buffering the whole run in memory for one transaction at
|
|
328
|
+
// the very end (#954) — a competing-process lock error or any other
|
|
329
|
+
// interruption partway through now keeps whatever already committed
|
|
330
|
+
// instead of losing the entire pass.
|
|
331
|
+
// Tracks what this run actually observed, so a successful pass can
|
|
332
|
+
// record `embeddingIdentity` from real data rather than the config
|
|
333
|
+
// string alone (#955) — only the first non-empty batch's vector width
|
|
334
|
+
// is kept; every batch from one run shares the same provider/model.
|
|
335
|
+
let observedModel;
|
|
336
|
+
let observedVectorLen;
|
|
337
|
+
const onBatch = (indices, batchEmbeddings, model) => {
|
|
338
|
+
if (model)
|
|
339
|
+
observedModel = model;
|
|
340
|
+
db.transaction(() => {
|
|
341
|
+
for (let k = 0; k < indices.length; k++) {
|
|
342
|
+
const entry = allEntries[indices[k]];
|
|
343
|
+
if (!entry)
|
|
344
|
+
continue;
|
|
345
|
+
const embedding = batchEmbeddings[k];
|
|
346
|
+
if (!embedding) {
|
|
347
|
+
embedFailedCount++;
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
if (observedVectorLen === undefined)
|
|
351
|
+
observedVectorLen = embedding.length;
|
|
352
|
+
const result = upsertEmbedding(db, entry.id, embedding);
|
|
353
|
+
if (result.stored) {
|
|
354
|
+
storedCount++;
|
|
355
|
+
storedTokens += estimateTokenCount(entry.searchText);
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
skippedCount++;
|
|
359
|
+
}
|
|
360
|
+
if (result.vec === "failed")
|
|
361
|
+
vecFailedCount++;
|
|
362
|
+
if (result.vec === "unavailable")
|
|
363
|
+
vecUnavailableCount++;
|
|
121
364
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
vecUnavailableCount++;
|
|
365
|
+
})();
|
|
366
|
+
const bucket = Math.floor(storedCount / PROGRESS_INTERVAL);
|
|
367
|
+
if (bucket > lastProgressBucket) {
|
|
368
|
+
lastProgressBucket = bucket;
|
|
369
|
+
onProgress({
|
|
370
|
+
phase: "embeddings",
|
|
371
|
+
message: `Embedded ${storedCount}/${allEntries.length} entries.`,
|
|
372
|
+
});
|
|
131
373
|
}
|
|
132
|
-
}
|
|
374
|
+
};
|
|
375
|
+
await embedBatch(texts, config.embedding, signal, (skip) => skips.push(skip), onBatch);
|
|
376
|
+
throwIfAborted(signal);
|
|
377
|
+
const elapsedSeconds = Math.max((Date.now() - embedStart) / 1000, 0.001);
|
|
133
378
|
if (skippedCount > 0) {
|
|
134
379
|
warn(`[embed] ${skippedCount} embedding${skippedCount === 1 ? "" : "s"} skipped (entry deleted between queue and write)`);
|
|
135
380
|
}
|
|
@@ -148,11 +393,16 @@ export async function generateEmbeddingsForDb(db, config, onProgress, signal, en
|
|
|
148
393
|
"semantic search will use the slower JS-cosine fallback over stored embeddings. " +
|
|
149
394
|
"Rebuild with 'akm index --full' after resolving the vec table (often a vector-dimension mismatch).");
|
|
150
395
|
}
|
|
396
|
+
const entriesPerSec = storedCount / elapsedSeconds;
|
|
397
|
+
const tokensPerSec = storedTokens / elapsedSeconds;
|
|
151
398
|
onProgress({
|
|
152
399
|
phase: "embeddings",
|
|
153
|
-
message: `Stored ${storedCount} embedding${storedCount === 1 ? "" : "s"}.`,
|
|
400
|
+
message: `Stored ${storedCount} embedding${storedCount === 1 ? "" : "s"} in ${elapsedSeconds.toFixed(1)}s (${entriesPerSec.toFixed(1)} entries/s, ~${Math.round(tokensPerSec)} tokens/s).`,
|
|
154
401
|
});
|
|
155
402
|
setMeta(db, "embeddingFingerprint", currentFingerprint);
|
|
403
|
+
const observedIdentity = deriveObservedEmbeddingIdentity(config.embedding, observedModel, observedVectorLen);
|
|
404
|
+
if (observedIdentity)
|
|
405
|
+
setMeta(db, "embeddingIdentity", observedIdentity);
|
|
156
406
|
// Only a total failure (nothing at all embedded, despite having entries
|
|
157
407
|
// to embed) turns into a phase failure. Any partial success — the vast
|
|
158
408
|
// majority of a large bundle embedding fine around a handful of skips —
|
|
@@ -25,7 +25,7 @@ import { allowsFragmentRef, defaultRendererRegistry } from "../../core/type-pres
|
|
|
25
25
|
import { normalizeEmbeddingEndpoint } from "../../llm/embedders/remote.js";
|
|
26
26
|
import { assertIndexPathReadable, closeDatabase, openExistingDatabase, } from "../../storage/repositories/index-connection.js";
|
|
27
27
|
import { getAllEntries, getBaseBeliefStatesForDerivedTwins, getEntryById, getEntryCount, getPositiveFeedbackCountsByIds, } from "../../storage/repositories/index-entries-repository.js";
|
|
28
|
-
import { searchFts } from "../../storage/repositories/index-fts-repository.js";
|
|
28
|
+
import { getIndexedMarkdownFragment, getIndexedMarkdownFragments, searchFts, } from "../../storage/repositories/index-fts-repository.js";
|
|
29
29
|
import { getMeta } from "../../storage/repositories/index-meta-repository.js";
|
|
30
30
|
import { getEmbeddingCount, searchVec } from "../../storage/repositories/index-vec-repository.js";
|
|
31
31
|
import { getCurrentWorkflowScopeKey } from "../../workflows/authoring/scope-key.js";
|
|
@@ -451,6 +451,14 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allSourceD
|
|
|
451
451
|
});
|
|
452
452
|
const rankMs = Date.now() - tRank0;
|
|
453
453
|
const selected = beliefFiltered.slice(0, limit);
|
|
454
|
+
const fragmentSelections = selected.flatMap((ranked) => ranked.fragmentId && allowsFragmentRef(ranked.entry.type) && hasIndexedProvenance(ranked)
|
|
455
|
+
? [{ entryId: ranked.id, itemRef: ranked.itemRef, fragmentId: ranked.fragmentId }]
|
|
456
|
+
: []);
|
|
457
|
+
const selectedFragments = getIndexedMarkdownFragments(db, fragmentSelections);
|
|
458
|
+
const selectedFragmentByEntryId = new Map();
|
|
459
|
+
fragmentSelections.forEach((selection, index) => {
|
|
460
|
+
selectedFragmentByEntryId.set(selection.entryId, selectedFragments[index]);
|
|
461
|
+
});
|
|
454
462
|
const hits = await Promise.all(selected.map((ranked) => {
|
|
455
463
|
const { entry, filePath, score, rankingMode, utilityBoosted } = ranked;
|
|
456
464
|
// CLAUDE.md locks SearchHit.score in [0,1]. The boost loop deliberately
|
|
@@ -467,6 +475,7 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allSourceD
|
|
|
467
475
|
rankingMode,
|
|
468
476
|
lexicalMatch: ranked.lexicalMatch,
|
|
469
477
|
fragmentId: ranked.fragmentId,
|
|
478
|
+
indexedFragment: ranked.fragmentId ? (selectedFragmentByEntryId.get(ranked.id) ?? null) : undefined,
|
|
470
479
|
defaultStashDir: stashDir,
|
|
471
480
|
allSourceDirs,
|
|
472
481
|
sources,
|
|
@@ -788,7 +797,19 @@ export async function buildDbHit(input) {
|
|
|
788
797
|
// The central type-presentation contract opts those types out explicitly.
|
|
789
798
|
const ref = input.fragmentId && allowsFragmentRef(input.entry.type) ? `${parentRef}#${input.fragmentId}` : parentRef;
|
|
790
799
|
const editable = isEditable(absolutePath, input.config, input.sources);
|
|
791
|
-
const
|
|
800
|
+
const indexedFragment = input.indexedFragment === undefined
|
|
801
|
+
? input.fragmentId && input.db
|
|
802
|
+
? getIndexedMarkdownFragment(input.db, input.itemRef, input.fragmentId)
|
|
803
|
+
: undefined
|
|
804
|
+
: (input.indexedFragment ?? undefined);
|
|
805
|
+
const selectedRef = input.fragmentId && ref !== parentRef ? `${parentRef}#${input.fragmentId}` : undefined;
|
|
806
|
+
const parentEstimatedTokens = typeof input.entry.fileSize === "number"
|
|
807
|
+
? Math.round(input.entry.fileSize / 4)
|
|
808
|
+
: indexedFragment
|
|
809
|
+
? Math.round(indexedFragment.parentChars / 4)
|
|
810
|
+
: undefined;
|
|
811
|
+
const fragmentEstimatedTokens = indexedFragment ? Math.round(indexedFragment.fragmentChars / 4) : undefined;
|
|
812
|
+
const estimatedTokens = selectedRef === ref && fragmentEstimatedTokens !== undefined ? fragmentEstimatedTokens : parentEstimatedTokens;
|
|
792
813
|
const hit = {
|
|
793
814
|
type: input.entry.type,
|
|
794
815
|
name: input.entry.name,
|
|
@@ -804,6 +825,32 @@ export async function buildDbHit(input) {
|
|
|
804
825
|
score,
|
|
805
826
|
whyMatched,
|
|
806
827
|
...(estimatedTokens !== undefined ? { estimatedTokens } : {}),
|
|
828
|
+
...(selectedRef
|
|
829
|
+
? {
|
|
830
|
+
selectedRef,
|
|
831
|
+
parentRef,
|
|
832
|
+
...(indexedFragment
|
|
833
|
+
? {
|
|
834
|
+
fragmentOrdinal: indexedFragment.ordinal + 1,
|
|
835
|
+
fragmentCount: indexedFragment.count,
|
|
836
|
+
startLine: indexedFragment.startLine,
|
|
837
|
+
endLine: indexedFragment.endLine,
|
|
838
|
+
...(indexedFragment.previousFragmentId
|
|
839
|
+
? { previousRef: `${parentRef}#${indexedFragment.previousFragmentId}` }
|
|
840
|
+
: {}),
|
|
841
|
+
...(indexedFragment.nextFragmentId
|
|
842
|
+
? { nextRef: `${parentRef}#${indexedFragment.nextFragmentId}` }
|
|
843
|
+
: {}),
|
|
844
|
+
fragmentChars: indexedFragment.fragmentChars,
|
|
845
|
+
fragmentEstimatedTokens,
|
|
846
|
+
parentChars: indexedFragment.parentChars,
|
|
847
|
+
...(parentEstimatedTokens !== undefined ? { parentEstimatedTokens } : {}),
|
|
848
|
+
}
|
|
849
|
+
: parentEstimatedTokens !== undefined
|
|
850
|
+
? { parentEstimatedTokens }
|
|
851
|
+
: {}),
|
|
852
|
+
}
|
|
853
|
+
: {}),
|
|
807
854
|
// Surface optional quality (v1 spec §4.2). Omitted when entry has
|
|
808
855
|
// no `quality` field so payloads stay compact for the common case.
|
|
809
856
|
...(input.entry.quality ? { quality: input.entry.quality } : {}),
|