akm-cli 0.9.15-beta.1 → 0.9.15-beta.3
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 +234 -13
- package/dist/akm +54 -1
- package/dist/akm-migrate +34 -1
- package/dist/cli.js +40 -4
- package/dist/commands/health/checks.js +11 -2
- package/dist/commands/health/scheduler-binary.js +120 -0
- package/dist/commands/health.js +9 -0
- package/dist/commands/improve/locks.js +3 -2
- package/dist/commands/sources/installed-stashes.js +58 -16
- package/dist/commands/sources/stash-cli.js +17 -0
- package/dist/core/config/schema/embedding.js +41 -1
- package/dist/core/errors.js +1 -0
- package/dist/core/file-lock.js +49 -15
- package/dist/core/parent-watchdog.js +64 -0
- package/dist/core/run-lock.js +13 -2
- package/dist/indexer/index-rebuild-lock.js +4 -4
- package/dist/indexer/index-written-assets.js +9 -1
- package/dist/indexer/indexer.js +123 -19
- package/dist/indexer/materialize-embeddings.js +345 -37
- package/dist/indexer/search/search-source.js +23 -1
- package/dist/llm/embedders/remote.js +443 -47
- package/dist/scripts/akm-migrate-node.js +454 -85
- package/dist/scripts/akm-migrate.js +454 -85
- package/dist/storage/repositories/embedding-salvage-repository.js +184 -0
- package/dist/storage/repositories/index-schema.js +16 -0
- package/dist/tasks/run/run-native-task.js +23 -1
- package/docs/migration/release-notes/0.9.15.md +103 -4
- package/docs/migration/release-notes/README.md +3 -2
- package/docs/reference/cli.md +55 -8
- package/docs/reference/configuration.md +83 -15
- package/package.json +1 -1
- package/schemas/akm-config.json +36 -9
package/dist/indexer/indexer.js
CHANGED
|
@@ -7,18 +7,21 @@ 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 { ConfigError } from "../core/errors.js";
|
|
10
|
+
import { AkmError, ConfigError, TransientError } from "../core/errors.js";
|
|
11
|
+
import { probeLock } from "../core/file-lock.js";
|
|
11
12
|
import { defaultConcurrencyForEndpoint } from "../core/loopback.js";
|
|
12
13
|
import { classifyPathAccess, describeInaccessiblePath } from "../core/path-access.js";
|
|
13
14
|
import { getDbPath } from "../core/paths.js";
|
|
14
15
|
import { SCRIPT_EXTENSIONS } from "../core/recognition-util.js";
|
|
15
|
-
import {
|
|
16
|
+
import { formatLockHolderPid } from "../core/run-lock.js";
|
|
17
|
+
import { isSqliteContentionError, withStateDb } from "../core/state-db.js";
|
|
16
18
|
import { isVerbose, warn, warnOnce, warnVerbose } from "../core/warn.js";
|
|
17
19
|
import { disposeLoweredExecutionDispatchLease, } from "../integrations/agent/execution-lowering.js";
|
|
18
20
|
import { isLlmFeatureEnabled } from "../llm/feature-gate.js";
|
|
19
21
|
import { resolveIndexPassExecution } from "../llm/index-passes.js";
|
|
20
22
|
import { preflightStructuredLlmRunner } from "../llm/structured-call.js";
|
|
21
23
|
import { resolveSourcesForOrigin } from "../registry/origin-resolve.js";
|
|
24
|
+
import { salvageEmbeddingsBeforeDiscard } from "../storage/repositories/embedding-salvage-repository.js";
|
|
22
25
|
import { closeDatabase, openExistingDatabase, openIndexDatabase, openReadonlyExistingDatabase, } from "../storage/repositories/index-connection.js";
|
|
23
26
|
import { deleteAllEntries, deleteEntriesByBundle, deleteEntriesByDirAndBundle, deleteEntriesByDirExceptRefs, deleteEntriesByIds, deleteUsageEventsByEntryIds, findEntryIdByRef, getAllEntries, getEmbeddableEntryCount, getEntryCount, getIndexedBundleIdsByDir, getIndexedDirPathsByBundleId, relinkUsageEvents, upsertEntry, } from "../storage/repositories/index-entries-repository.js";
|
|
24
27
|
import { clearStaleCacheEntries, computeBodyHash, getLlmCacheEntry, } from "../storage/repositories/index-llm-cache-repository.js";
|
|
@@ -27,6 +30,7 @@ import { upsertUtilityScore } from "../storage/repositories/index-utility-reposi
|
|
|
27
30
|
import { getEmbeddingCount, isVecAvailable, isVecFastPathReady, warnIfVecMissing, } from "../storage/repositories/index-vec-repository.js";
|
|
28
31
|
import { assertIndexedWorkflowSourceIdentity, WorkflowSourceIdentityError } from "../workflows/source-files.js";
|
|
29
32
|
import { deleteStoredGraph } from "./db/graph-db.js";
|
|
33
|
+
import { indexRebuildLockPath } from "./index-rebuild-lock.js";
|
|
30
34
|
import { deriveEntryProvenance, deriveInstallations } from "./installations.js";
|
|
31
35
|
import { indexedPathMatchesOwner, resolveAdapterConceptOwner, } from "./lookup/adapter-concept-owner.js";
|
|
32
36
|
import { generateEmbeddingsForDb } from "./materialize-embeddings.js";
|
|
@@ -184,21 +188,54 @@ async function runWalkPhase(ctx) {
|
|
|
184
188
|
});
|
|
185
189
|
ctx.timing.tLlmEnd = Date.now();
|
|
186
190
|
}
|
|
191
|
+
/**
|
|
192
|
+
* The ONE embedding-phase implementation (#954): generate and
|
|
193
|
+
* store vectors for every entry missing one, then compute the `hasEmbeddings`
|
|
194
|
+
* fact and the semantic-search verification off the result. `akmIndex`'s own
|
|
195
|
+
* (non-deferred) run calls this from {@link runEmbeddingPhase} below; `akm
|
|
196
|
+
* bundle update`'s coordinator calls it directly on its own connection AFTER
|
|
197
|
+
* its unified update transaction commits, since the ambient-transaction drift
|
|
198
|
+
* guard (and the whole point of per-batch commit, #954) requires `db` to have
|
|
199
|
+
* no ambient transaction open.
|
|
200
|
+
*/
|
|
201
|
+
export async function runEmbeddingPass(params) {
|
|
202
|
+
const { db, config, onProgress, signal, reembed } = params;
|
|
203
|
+
const embeddingResult = await generateEmbeddingsForDb(db, config, onProgress, signal, undefined, {
|
|
204
|
+
forceReembed: reembed,
|
|
205
|
+
});
|
|
206
|
+
setMeta(db, "hasEmbeddings", embeddingResult.success ? "1" : "0");
|
|
207
|
+
const semanticEntryCount = getEmbeddableEntryCount(db);
|
|
208
|
+
onProgress({ phase: "finalize", message: "Verifying semantic search state." });
|
|
209
|
+
const verification = verifyIndexState(db, config, semanticEntryCount, embeddingResult);
|
|
210
|
+
onProgress({ phase: "verify", message: verification.message });
|
|
211
|
+
return { embeddingResult, verification };
|
|
212
|
+
}
|
|
187
213
|
/**
|
|
188
214
|
* Embedding phase: generate and store vector embeddings for all unembedded
|
|
189
|
-
* entries. Writes `ctx.embeddingResult` for the
|
|
215
|
+
* entries. Writes `ctx.embeddingResult` and `ctx.verification` for the
|
|
216
|
+
* finalize phase / caller.
|
|
190
217
|
*/
|
|
191
218
|
async function runEmbeddingPhase(ctx) {
|
|
192
|
-
const { db, config, signal, onProgress, reembed } = ctx;
|
|
219
|
+
const { db, config, signal, onProgress, reembed, deferredUpdateTransaction } = ctx;
|
|
193
220
|
throwIfAborted(signal);
|
|
221
|
+
if (deferredUpdateTransaction) {
|
|
222
|
+
// `akm bundle update`'s deferred pass (#954): the embedding
|
|
223
|
+
// phase runs AFTER the coordinator's own commit, on its own connection,
|
|
224
|
+
// via the coordinator's direct `runEmbeddingPass` call — never here,
|
|
225
|
+
// inside the borrowed transaction (the ambient-transaction drift guard
|
|
226
|
+
// would reject it anyway). `runFinalizePhase` records semantic state as
|
|
227
|
+
// "pending".
|
|
228
|
+
ctx.timing.tEmbedEnd = Date.now();
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
194
231
|
// Forward the signal. Without it generateEmbeddingsForDb's abort machinery was
|
|
195
232
|
// inert — its throwIfAborted checks and the signal it threads into embedBatch
|
|
196
233
|
// (which RemoteEmbedder passes to every fetch and LocalEmbedder honours between
|
|
197
234
|
// chunks) never saw a controller. Ctrl-C and the improve budget abort could not
|
|
198
235
|
// stop the embedding phase, the longest phase of an index run.
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
236
|
+
const { embeddingResult, verification } = await runEmbeddingPass({ db, config, onProgress, signal, reembed });
|
|
237
|
+
ctx.embeddingResult = embeddingResult;
|
|
238
|
+
ctx.verification = verification;
|
|
202
239
|
ctx.timing.tEmbedEnd = Date.now();
|
|
203
240
|
}
|
|
204
241
|
/**
|
|
@@ -206,8 +243,8 @@ async function runEmbeddingPhase(ctx) {
|
|
|
206
243
|
* usage events, recompute utility scores, update index metadata, and emit the
|
|
207
244
|
* verify event.
|
|
208
245
|
*/
|
|
209
|
-
async function runFinalizePhase(ctx
|
|
210
|
-
const { db, config, sources, sourceDirs, stashDir, signal, onProgress } = ctx;
|
|
246
|
+
async function runFinalizePhase(ctx) {
|
|
247
|
+
const { db, config, sources, sourceDirs, stashDir, signal, onProgress, deferredUpdateTransaction } = ctx;
|
|
211
248
|
ctx.timing.tFinalizeStart = Date.now();
|
|
212
249
|
// `upsertEntry` and every canonical delete own their FTS projection. This is
|
|
213
250
|
// an observation point, not a second materialization pass.
|
|
@@ -249,22 +286,37 @@ async function runFinalizePhase(ctx, deferredUpdateTransaction) {
|
|
|
249
286
|
// An incomplete run preserves the prior freshness watermark. Advancing it
|
|
250
287
|
// could make a recovered source look unchanged even though this run never
|
|
251
288
|
// persisted its files.
|
|
252
|
-
const embeddingResult = ctx.embeddingResult ?? { success: false };
|
|
253
289
|
if (ctx.scanComplete) {
|
|
254
290
|
setMeta(db, "builtAt", new Date().toISOString());
|
|
255
291
|
setMeta(db, "stashDir", stashDir);
|
|
256
292
|
setMeta(db, "stashDirs", JSON.stringify(sourceDirs));
|
|
257
293
|
setMeta(db, "sourceOwners", JSON.stringify(sourceOwners(sources)));
|
|
258
294
|
}
|
|
259
|
-
setMeta(db, "hasEmbeddings", embeddingResult.success ? "1" : "0");
|
|
260
295
|
warnIfVecMissing(db);
|
|
261
296
|
const totalEntries = getEntryCount(db);
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
297
|
+
if (deferredUpdateTransaction) {
|
|
298
|
+
// #954: the embedding phase was skipped for this borrowed
|
|
299
|
+
// transaction — record semantic state as pending, never ready, until the
|
|
300
|
+
// coordinator's own post-commit `runEmbeddingPass` call reports the
|
|
301
|
+
// truth on a fresh connection.
|
|
302
|
+
setMeta(db, "hasEmbeddings", "0");
|
|
303
|
+
const semanticEntryCount = getEmbeddableEntryCount(db);
|
|
304
|
+
const message = "Semantic index update deferred until after the source-update commit.";
|
|
305
|
+
onProgress({ phase: "verify", message });
|
|
306
|
+
ctx.verification = {
|
|
307
|
+
ok: true,
|
|
308
|
+
message,
|
|
309
|
+
semanticSearchEnabled: config.semanticSearchMode === "auto",
|
|
310
|
+
semanticSearchMode: config.semanticSearchMode,
|
|
311
|
+
semanticStatus: config.semanticSearchMode === "off" ? "disabled" : "pending",
|
|
312
|
+
embeddingProvider: getEmbeddingProvider(config.embedding),
|
|
313
|
+
entryCount: semanticEntryCount,
|
|
314
|
+
embeddingCount: getEmbeddingCount(db),
|
|
315
|
+
vecAvailable: isVecAvailable(db),
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
// Non-deferred: ctx.verification was already populated by runEmbeddingPhase
|
|
319
|
+
// (via the shared runEmbeddingPass).
|
|
268
320
|
ctx.totalEntries = totalEntries;
|
|
269
321
|
ctx.timing.tFinalizeEnd = Date.now();
|
|
270
322
|
// suppress unused warning — sources was previously used inline
|
|
@@ -322,6 +374,44 @@ let akmIndexOverride;
|
|
|
322
374
|
export function _setAkmIndexForTests(fake) {
|
|
323
375
|
akmIndexOverride = fake;
|
|
324
376
|
}
|
|
377
|
+
/**
|
|
378
|
+
* Read-only description of the rebuild lock's current holder, appended to a
|
|
379
|
+
* reclassified index.db contention message when known (field follow-up to
|
|
380
|
+
* #956). `probeLock` only inspects the sentinel — it never acquires or
|
|
381
|
+
* mutates it — so this is safe to call from inside an error path.
|
|
382
|
+
*/
|
|
383
|
+
function describeIndexRebuildLockHolder() {
|
|
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
|
+
}
|
|
325
415
|
export async function akmIndex(options) {
|
|
326
416
|
try {
|
|
327
417
|
const override = akmIndexOverride;
|
|
@@ -338,7 +428,7 @@ export async function akmIndex(options) {
|
|
|
338
428
|
// rollback before closing its borrowed unified handle.
|
|
339
429
|
}
|
|
340
430
|
}
|
|
341
|
-
throw error;
|
|
431
|
+
throw reclassifyIndexDbContention(error);
|
|
342
432
|
}
|
|
343
433
|
}
|
|
344
434
|
let indexTransactionHookForTests;
|
|
@@ -513,6 +603,11 @@ async function akmIndexReal(options) {
|
|
|
513
603
|
force: full,
|
|
514
604
|
materialize: options.hydrateSources !== false,
|
|
515
605
|
secrets: storeSecretResolver,
|
|
606
|
+
// Same progress channel as every other phase (#954) — a
|
|
607
|
+
// stalled clone/fetch here runs BEFORE index.db is even opened, so
|
|
608
|
+
// without this it looked identical to "no database open, nothing
|
|
609
|
+
// written".
|
|
610
|
+
onProgress: (message) => onProgress({ phase: "preflight", message }),
|
|
516
611
|
});
|
|
517
612
|
const sourceCacheEnd = Date.now();
|
|
518
613
|
const allSourceEntries = resolveSourceEntries(stashDir, config);
|
|
@@ -553,6 +648,7 @@ async function akmIndexReal(options) {
|
|
|
553
648
|
onProgress,
|
|
554
649
|
signal,
|
|
555
650
|
t0,
|
|
651
|
+
deferredUpdateTransaction: options.deferredUpdateTransaction,
|
|
556
652
|
});
|
|
557
653
|
indexRunContext = ctx;
|
|
558
654
|
onProgress({
|
|
@@ -592,7 +688,7 @@ async function akmIndexReal(options) {
|
|
|
592
688
|
}
|
|
593
689
|
cleanEnd = Date.now();
|
|
594
690
|
await runEmbeddingPhase(ctx);
|
|
595
|
-
await runFinalizePhase(ctx
|
|
691
|
+
await runFinalizePhase(ctx);
|
|
596
692
|
// ────────────────────────────────────────────────────────────────────────
|
|
597
693
|
// runFinalizePhase always populates these before returning.
|
|
598
694
|
const verification = ctx.verification;
|
|
@@ -1159,6 +1255,14 @@ function persistDirRecords(db, dirRecords, doFullDelete, warnings, sourceRoots,
|
|
|
1159
1255
|
// transaction so delete and re-insert are atomic — a concurrent reader
|
|
1160
1256
|
// never observes an empty database between the two operations.
|
|
1161
1257
|
if (fullDelete) {
|
|
1258
|
+
// #955: copy every (search_text hash, embedding) pair about to be
|
|
1259
|
+
// discarded wholesale into `embedding_salvage`, tagged with the
|
|
1260
|
+
// fingerprint the discarded vectors were generated under, BEFORE the
|
|
1261
|
+
// wipe below — inside the SAME transaction so the copy and the
|
|
1262
|
+
// discard commit or roll back together. The embedding phase later in
|
|
1263
|
+
// this run hands salvaged vectors back to unchanged content instead
|
|
1264
|
+
// of re-embedding the whole corpus.
|
|
1265
|
+
salvageEmbeddingsBeforeDiscard(db);
|
|
1162
1266
|
// Entries and every child materialization share one deletion authority.
|
|
1163
1267
|
// Usage events live in state.db and survive so finalize can relink them
|
|
1164
1268
|
// to the replacement generation's row ids.
|