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
|
@@ -7069,7 +7069,8 @@ var init_errors = __esm(() => {
|
|
|
7069
7069
|
};
|
|
7070
7070
|
TRANSIENT_HINTS = {
|
|
7071
7071
|
RUN_LEASE_HELD: "Wait for the named engine invocation to finish or for the lease to expire, then retry. `akm workflow status <id>` shows the current lease.",
|
|
7072
|
-
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."
|
|
7072
|
+
STATE_DB_CONTENDED: "Another akm process is writing state.db right now. Wait a few seconds and retry; commands that support --skip-if-locked can skip instead of failing.",
|
|
7073
|
+
INDEX_DB_CONTENDED: "Another akm process is writing index.db; retry shortly, or pass --skip-if-locked on scheduled runs."
|
|
7073
7074
|
};
|
|
7074
7075
|
NOT_FOUND_HINTS = {
|
|
7075
7076
|
ASSET_NOT_FOUND: "Run `akm search <query>` or `akm index` to refresh the index.",
|
|
@@ -35703,7 +35704,7 @@ var require_libvips = __commonJS((exports, module) => {
|
|
|
35703
35704
|
SPDX-License-Identifier: Apache-2.0
|
|
35704
35705
|
*/
|
|
35705
35706
|
var { spawnSync: spawnSync6 } = __require("child_process");
|
|
35706
|
-
var { createHash:
|
|
35707
|
+
var { createHash: createHash11 } = __require("crypto");
|
|
35707
35708
|
var semverCoerce = require_coerce2();
|
|
35708
35709
|
var semverGreaterThanOrEqualTo = require_gte2();
|
|
35709
35710
|
var semverSatisfies = require_satisfies2();
|
|
@@ -35791,7 +35792,7 @@ var require_libvips = __commonJS((exports, module) => {
|
|
|
35791
35792
|
}
|
|
35792
35793
|
return false;
|
|
35793
35794
|
};
|
|
35794
|
-
var sha512 = (s) =>
|
|
35795
|
+
var sha512 = (s) => createHash11("sha512").update(s).digest("hex");
|
|
35795
35796
|
var yarnLocator = () => {
|
|
35796
35797
|
try {
|
|
35797
35798
|
const identHash = sha512(`imgsharp-libvips-${buildPlatformArch()}`);
|
|
@@ -75245,11 +75246,13 @@ var EmbeddingConnectionConfigSchema = exports_external.object({
|
|
|
75245
75246
|
apiKey: symbolicOrWarnApiKey("embedding.apiKey").optional(),
|
|
75246
75247
|
dimension: positiveInt.max(4096).optional(),
|
|
75247
75248
|
localModel: exports_external.string().min(1).optional(),
|
|
75249
|
+
maxInputTokens: positiveInt.optional(),
|
|
75248
75250
|
maxTokens: positiveInt.optional(),
|
|
75249
75251
|
batchSize: positiveInt.optional(),
|
|
75250
|
-
chunkSize: positiveInt.optional(),
|
|
75251
75252
|
contextLength: positiveInt.optional(),
|
|
75252
|
-
ollamaOptions: EmbeddingOllamaOptionsSchema.optional()
|
|
75253
|
+
ollamaOptions: EmbeddingOllamaOptionsSchema.optional(),
|
|
75254
|
+
timeoutMs: positiveInt.optional(),
|
|
75255
|
+
concurrency: positiveInt.max(16).optional()
|
|
75253
75256
|
}).passthrough();
|
|
75254
75257
|
|
|
75255
75258
|
// src/integrations/harnesses/ids.ts
|
|
@@ -76322,8 +76325,21 @@ function releaseLockRaw(lockPath) {
|
|
|
76322
76325
|
function tryAcquireLockSync(lockPath, payload) {
|
|
76323
76326
|
return withLockOperationMutex(lockPath, () => tryAcquireLockRaw(lockPath, payload));
|
|
76324
76327
|
}
|
|
76328
|
+
function launcherPidFromEnv() {
|
|
76329
|
+
const raw = process.env.AKM_LAUNCHER_PID;
|
|
76330
|
+
if (!raw)
|
|
76331
|
+
return;
|
|
76332
|
+
const pid = Number.parseInt(raw, 10);
|
|
76333
|
+
return Number.isInteger(pid) && pid > 0 ? pid : undefined;
|
|
76334
|
+
}
|
|
76325
76335
|
function createLockPayload(metadata = {}) {
|
|
76326
|
-
|
|
76336
|
+
const launcherPid = launcherPidFromEnv();
|
|
76337
|
+
return JSON.stringify({
|
|
76338
|
+
...metadata,
|
|
76339
|
+
pid: process.pid,
|
|
76340
|
+
...launcherPid !== undefined ? { launcherPid } : {},
|
|
76341
|
+
lockId: randomUUID()
|
|
76342
|
+
});
|
|
76327
76343
|
}
|
|
76328
76344
|
function probeLock(lockPath, opts) {
|
|
76329
76345
|
let snapshot;
|
|
@@ -76339,17 +76355,17 @@ function probeLock(lockPath, opts) {
|
|
|
76339
76355
|
return { state: "absent" };
|
|
76340
76356
|
const { rawContent, identity: identity3 } = snapshot;
|
|
76341
76357
|
const ageMs = Date.now() - identity3.mtimeMs;
|
|
76342
|
-
const holderPid =
|
|
76358
|
+
const { holderPid, launcherPid } = extractLockIdentity(rawContent);
|
|
76343
76359
|
if (holderPid === undefined) {
|
|
76344
76360
|
return { state: "stale", reason: "invalid_pid", ageMs, rawContent, identity: identity3 };
|
|
76345
76361
|
}
|
|
76346
76362
|
if (!isProcessAlive(holderPid)) {
|
|
76347
|
-
return { state: "stale", reason: "pid_dead", holderPid, ageMs, rawContent, identity: identity3 };
|
|
76363
|
+
return { state: "stale", reason: "pid_dead", holderPid, launcherPid, ageMs, rawContent, identity: identity3 };
|
|
76348
76364
|
}
|
|
76349
76365
|
if (opts?.staleAfterMs !== undefined && ageMs > opts.staleAfterMs) {
|
|
76350
|
-
return { state: "stale", reason: "age_exceeded", holderPid, ageMs, rawContent, identity: identity3 };
|
|
76366
|
+
return { state: "stale", reason: "age_exceeded", holderPid, launcherPid, ageMs, rawContent, identity: identity3 };
|
|
76351
76367
|
}
|
|
76352
|
-
return { state: "held", holderPid, ageMs, rawContent, identity: identity3 };
|
|
76368
|
+
return { state: "held", holderPid, launcherPid, ageMs, rawContent, identity: identity3 };
|
|
76353
76369
|
}
|
|
76354
76370
|
function reclaimStaleLock(lockPath, probe, options) {
|
|
76355
76371
|
if (probe.rawContent === undefined || probe.identity === undefined)
|
|
@@ -76417,21 +76433,25 @@ function releaseLock(ownership) {
|
|
|
76417
76433
|
}
|
|
76418
76434
|
});
|
|
76419
76435
|
}
|
|
76420
|
-
function
|
|
76436
|
+
function extractLockIdentity(content) {
|
|
76421
76437
|
const trimmed = content.trim();
|
|
76422
76438
|
if (!trimmed)
|
|
76423
|
-
return;
|
|
76439
|
+
return {};
|
|
76424
76440
|
if (trimmed.startsWith("{")) {
|
|
76425
76441
|
try {
|
|
76426
76442
|
const parsed = JSON.parse(trimmed);
|
|
76427
76443
|
const pid2 = typeof parsed.pid === "number" ? parsed.pid : Number.NaN;
|
|
76428
|
-
|
|
76444
|
+
const rawLauncherPid = typeof parsed.launcherPid === "number" ? parsed.launcherPid : Number.NaN;
|
|
76445
|
+
return {
|
|
76446
|
+
holderPid: Number.isInteger(pid2) && pid2 > 0 ? pid2 : undefined,
|
|
76447
|
+
launcherPid: Number.isInteger(rawLauncherPid) && rawLauncherPid > 0 ? rawLauncherPid : undefined
|
|
76448
|
+
};
|
|
76429
76449
|
} catch {
|
|
76430
|
-
return;
|
|
76450
|
+
return {};
|
|
76431
76451
|
}
|
|
76432
76452
|
}
|
|
76433
76453
|
const pid = Number.parseInt(trimmed, 10);
|
|
76434
|
-
return Number.isInteger(pid) && pid > 0 ? pid : undefined;
|
|
76454
|
+
return { holderPid: Number.isInteger(pid) && pid > 0 ? pid : undefined };
|
|
76435
76455
|
}
|
|
76436
76456
|
|
|
76437
76457
|
// src/core/config/config-io.ts
|
|
@@ -95693,7 +95713,7 @@ function listTxnJournalsTolerant(predicate) {
|
|
|
95693
95713
|
}
|
|
95694
95714
|
|
|
95695
95715
|
// src/commands/proposal/repository.ts
|
|
95696
|
-
import { createHash as
|
|
95716
|
+
import { createHash as createHash11, randomUUID as randomUUID6 } from "crypto";
|
|
95697
95717
|
import fs55 from "fs";
|
|
95698
95718
|
init_dist();
|
|
95699
95719
|
import path66 from "path";
|
|
@@ -95759,6 +95779,16 @@ import fs53 from "fs";
|
|
|
95759
95779
|
import path64 from "path";
|
|
95760
95780
|
init_errors();
|
|
95761
95781
|
init_paths();
|
|
95782
|
+
|
|
95783
|
+
// src/core/run-lock.ts
|
|
95784
|
+
init_errors();
|
|
95785
|
+
function formatLockHolderPid(holder) {
|
|
95786
|
+
if (holder.pid === null)
|
|
95787
|
+
return "unknown";
|
|
95788
|
+
return holder.launcherPid !== null ? `${holder.pid} (launcher ${holder.launcherPid})` : String(holder.pid);
|
|
95789
|
+
}
|
|
95790
|
+
|
|
95791
|
+
// src/indexer/index-written-assets.ts
|
|
95762
95792
|
init_warn();
|
|
95763
95793
|
|
|
95764
95794
|
// src/storage/repositories/index-connection.ts
|
|
@@ -96013,6 +96043,15 @@ function isCanonicalIndexGeneration(db) {
|
|
|
96013
96043
|
init_errors();
|
|
96014
96044
|
init_warn();
|
|
96015
96045
|
|
|
96046
|
+
// src/storage/repositories/embedding-salvage-repository.ts
|
|
96047
|
+
import { createHash as createHash9 } from "crypto";
|
|
96048
|
+
|
|
96049
|
+
// src/storage/repositories/embeddings-repository.ts
|
|
96050
|
+
function blobToEmbedding(blob) {
|
|
96051
|
+
const f32 = new Float32Array(blob.buffer, blob.byteOffset, blob.byteLength / 4);
|
|
96052
|
+
return Array.from(f32);
|
|
96053
|
+
}
|
|
96054
|
+
|
|
96016
96055
|
// src/storage/repositories/index-meta-repository.ts
|
|
96017
96056
|
function getMeta(db, key) {
|
|
96018
96057
|
const row = db.prepare("SELECT value FROM index_meta WHERE key = ?").get(key);
|
|
@@ -96025,6 +96064,64 @@ function deleteMeta(db, key) {
|
|
|
96025
96064
|
db.prepare("DELETE FROM index_meta WHERE key = ?").run(key);
|
|
96026
96065
|
}
|
|
96027
96066
|
|
|
96067
|
+
// src/storage/repositories/index-sql.ts
|
|
96068
|
+
var SQLITE_CHUNK_SIZE = 500;
|
|
96069
|
+
|
|
96070
|
+
// src/storage/repositories/embedding-salvage-repository.ts
|
|
96071
|
+
function hashEmbeddableText(searchText) {
|
|
96072
|
+
return createHash9("sha256").update(searchText, "utf8").digest("hex");
|
|
96073
|
+
}
|
|
96074
|
+
function purgeEmbeddingSalvage(db) {
|
|
96075
|
+
db.exec("DELETE FROM embedding_salvage");
|
|
96076
|
+
}
|
|
96077
|
+
function relabelEmbeddingSalvageFingerprint(db, fromFingerprint, toFingerprint) {
|
|
96078
|
+
db.prepare("UPDATE embedding_salvage SET fingerprint = ? WHERE fingerprint = ?").run(toFingerprint, fromFingerprint);
|
|
96079
|
+
}
|
|
96080
|
+
function reuseSalvagedEmbeddings(db, entries, fingerprint, writeReused) {
|
|
96081
|
+
if (entries.length === 0)
|
|
96082
|
+
return { reusedCount: 0, remaining: [] };
|
|
96083
|
+
const anySalvageForFingerprint = db.prepare("SELECT 1 FROM embedding_salvage WHERE fingerprint = ? LIMIT 1").get(fingerprint);
|
|
96084
|
+
if (!anySalvageForFingerprint)
|
|
96085
|
+
return { reusedCount: 0, remaining: [...entries] };
|
|
96086
|
+
const hashes = entries.map((entry) => hashEmbeddableText(entry.searchText));
|
|
96087
|
+
const salvageByHash = new Map;
|
|
96088
|
+
const uniqueHashes = [...new Set(hashes)];
|
|
96089
|
+
for (let offset = 0;offset < uniqueHashes.length; offset += SQLITE_CHUNK_SIZE) {
|
|
96090
|
+
const chunk = uniqueHashes.slice(offset, offset + SQLITE_CHUNK_SIZE);
|
|
96091
|
+
const placeholders = chunk.map(() => "?").join(",");
|
|
96092
|
+
const rows = db.prepare(`SELECT content_hash AS contentHash, embedding FROM embedding_salvage WHERE fingerprint = ? AND content_hash IN (${placeholders})`).all(fingerprint, ...chunk);
|
|
96093
|
+
for (const row of rows)
|
|
96094
|
+
salvageByHash.set(row.contentHash, row.embedding);
|
|
96095
|
+
}
|
|
96096
|
+
if (salvageByHash.size === 0)
|
|
96097
|
+
return { reusedCount: 0, remaining: [...entries] };
|
|
96098
|
+
let reusedCount = 0;
|
|
96099
|
+
const remaining = [];
|
|
96100
|
+
for (let offset = 0;offset < entries.length; offset += SQLITE_CHUNK_SIZE) {
|
|
96101
|
+
const end = Math.min(offset + SQLITE_CHUNK_SIZE, entries.length);
|
|
96102
|
+
const chunkMatches = [];
|
|
96103
|
+
for (let i = offset;i < end; i++) {
|
|
96104
|
+
const entry = entries[i];
|
|
96105
|
+
const blob = salvageByHash.get(hashes[i]);
|
|
96106
|
+
if (blob)
|
|
96107
|
+
chunkMatches.push({ entry, blob });
|
|
96108
|
+
else
|
|
96109
|
+
remaining.push(entry);
|
|
96110
|
+
}
|
|
96111
|
+
if (chunkMatches.length === 0)
|
|
96112
|
+
continue;
|
|
96113
|
+
db.transaction(() => {
|
|
96114
|
+
for (const { entry, blob } of chunkMatches) {
|
|
96115
|
+
if (writeReused(entry, blobToEmbedding(blob)))
|
|
96116
|
+
reusedCount++;
|
|
96117
|
+
else
|
|
96118
|
+
remaining.push(entry);
|
|
96119
|
+
}
|
|
96120
|
+
})();
|
|
96121
|
+
}
|
|
96122
|
+
return { reusedCount, remaining };
|
|
96123
|
+
}
|
|
96124
|
+
|
|
96028
96125
|
// src/storage/repositories/index-vec-repository.ts
|
|
96029
96126
|
import { createRequire as createRequire3 } from "module";
|
|
96030
96127
|
|
|
@@ -96066,9 +96163,6 @@ function cosineSimilarity(a, b) {
|
|
|
96066
96163
|
return denom === 0 ? 0 : dot / denom;
|
|
96067
96164
|
}
|
|
96068
96165
|
|
|
96069
|
-
// src/storage/repositories/index-sql.ts
|
|
96070
|
-
var SQLITE_CHUNK_SIZE = 500;
|
|
96071
|
-
|
|
96072
96166
|
// src/storage/repositories/index-vec-repository.ts
|
|
96073
96167
|
var vecStatus = new WeakMap;
|
|
96074
96168
|
function loadVecExtension(db) {
|
|
@@ -96328,11 +96422,11 @@ init_warn();
|
|
|
96328
96422
|
|
|
96329
96423
|
// src/core/asset/markdown-fragments.ts
|
|
96330
96424
|
init_markdown();
|
|
96331
|
-
import { createHash as
|
|
96425
|
+
import { createHash as createHash10 } from "crypto";
|
|
96332
96426
|
var MARKDOWN_FRAGMENT_MAX_CHARS = 1600;
|
|
96333
96427
|
var MARKDOWN_FRAGMENT_PREFIX = "akm-fragment-";
|
|
96334
96428
|
function hash3(text) {
|
|
96335
|
-
return
|
|
96429
|
+
return createHash10("sha256").update(text).digest("hex");
|
|
96336
96430
|
}
|
|
96337
96431
|
function uniqueSlugs(body) {
|
|
96338
96432
|
const out = new Map;
|
|
@@ -96613,6 +96707,7 @@ function getEmbeddableEntryCount(db) {
|
|
|
96613
96707
|
}
|
|
96614
96708
|
|
|
96615
96709
|
// src/indexer/materialize-embeddings.ts
|
|
96710
|
+
init_paths();
|
|
96616
96711
|
init_warn();
|
|
96617
96712
|
|
|
96618
96713
|
// src/llm/embedders/cache.ts
|
|
@@ -96848,10 +96943,43 @@ function defaultConcurrencyForEndpoint(endpoint) {
|
|
|
96848
96943
|
// src/llm/embedders/remote.ts
|
|
96849
96944
|
init_warn();
|
|
96850
96945
|
var DEFAULT_REMOTE_BATCH_SIZE = 100;
|
|
96851
|
-
var DEFAULT_TOKEN_BUDGET =
|
|
96946
|
+
var DEFAULT_TOKEN_BUDGET = 6000;
|
|
96852
96947
|
function estimateTokenCount(text) {
|
|
96853
96948
|
return Math.round(text.length / 4);
|
|
96854
96949
|
}
|
|
96950
|
+
var DEFAULT_MAX_INPUT_TOKENS = 512;
|
|
96951
|
+
function capEmbeddingText(text, maxTokens) {
|
|
96952
|
+
if (estimateTokenCount(text) <= maxTokens)
|
|
96953
|
+
return { text, truncated: false };
|
|
96954
|
+
const charBudget = Math.max(0, maxTokens * 4);
|
|
96955
|
+
let cut = Math.min(charBudget, text.length);
|
|
96956
|
+
if (cut > 0 && cut < text.length) {
|
|
96957
|
+
const code = text.charCodeAt(cut);
|
|
96958
|
+
if (code >= 56320 && code <= 57343)
|
|
96959
|
+
cut -= 1;
|
|
96960
|
+
}
|
|
96961
|
+
return { text: text.slice(0, cut), truncated: true };
|
|
96962
|
+
}
|
|
96963
|
+
var DEFAULT_EMBEDDING_TIMEOUT_MS = 120000;
|
|
96964
|
+
function resolveEmbeddingTimeoutMs(config) {
|
|
96965
|
+
return config.timeoutMs ?? DEFAULT_EMBEDDING_TIMEOUT_MS;
|
|
96966
|
+
}
|
|
96967
|
+
function scaleEmbeddingTimeoutMs(timeoutMs, requestTokens, tokenBudget) {
|
|
96968
|
+
const scaled = tokenBudget > 0 ? timeoutMs * (requestTokens / tokenBudget) : timeoutMs;
|
|
96969
|
+
return Math.min(Math.max(scaled, 30000), timeoutMs);
|
|
96970
|
+
}
|
|
96971
|
+
function isEmbeddingTimeoutError(err) {
|
|
96972
|
+
if (!(err instanceof Error))
|
|
96973
|
+
return false;
|
|
96974
|
+
if (err.name === "BodyReadTimeoutError")
|
|
96975
|
+
return true;
|
|
96976
|
+
return err.message.startsWith("Request timed out after ");
|
|
96977
|
+
}
|
|
96978
|
+
var embeddingTimeoutBackoffOverrideForTests;
|
|
96979
|
+
function embeddingTimeoutRetryBackoffMs(timeoutAttempt = 0) {
|
|
96980
|
+
const { baseMs, maxMs } = embeddingTimeoutBackoffOverrideForTests ?? { baseMs: 5000, maxMs: 60000 };
|
|
96981
|
+
return backoffDelay(timeoutAttempt, baseMs, maxMs);
|
|
96982
|
+
}
|
|
96855
96983
|
|
|
96856
96984
|
class ContextExceededError extends Error {
|
|
96857
96985
|
constructor(message) {
|
|
@@ -96859,13 +96987,15 @@ class ContextExceededError extends Error {
|
|
|
96859
96987
|
this.name = "ContextExceededError";
|
|
96860
96988
|
}
|
|
96861
96989
|
}
|
|
96862
|
-
var CONTEXT_EXCEEDED_PATTERN = /exceed_context_size_error|context size|context length|too many tokens/i;
|
|
96990
|
+
var CONTEXT_EXCEEDED_PATTERN = /exceed_context_size_error|context size|context length|too many tokens|input is too large to process|physical batch size|ubatch/i;
|
|
96863
96991
|
function isContextExceededResponse(status, body) {
|
|
96864
96992
|
if (status === 413)
|
|
96865
96993
|
return true;
|
|
96866
96994
|
return CONTEXT_EXCEEDED_PATTERN.test(body);
|
|
96867
96995
|
}
|
|
96868
96996
|
function resolveEmbeddingConcurrency(config) {
|
|
96997
|
+
if (typeof config.concurrency === "number")
|
|
96998
|
+
return config.concurrency;
|
|
96869
96999
|
return defaultConcurrencyForEndpoint(config.endpoint);
|
|
96870
97000
|
}
|
|
96871
97001
|
function buildTokenBoundedBatches(texts, tokenBudget, maxCount) {
|
|
@@ -96895,6 +97025,8 @@ function buildTokenBoundedBatches(texts, tokenBudget, maxCount) {
|
|
|
96895
97025
|
flush();
|
|
96896
97026
|
return batches;
|
|
96897
97027
|
}
|
|
97028
|
+
var ADAPTIVE_BUDGET_SHRINK_FACTOR = 0.75;
|
|
97029
|
+
var ADAPTIVE_BUDGET_FLOOR_MULTIPLIER = 2;
|
|
96898
97030
|
|
|
96899
97031
|
class RemoteEmbedder {
|
|
96900
97032
|
config;
|
|
@@ -96921,20 +97053,21 @@ class RemoteEmbedder {
|
|
|
96921
97053
|
if (ollamaOpts) {
|
|
96922
97054
|
body.options = ollamaOpts;
|
|
96923
97055
|
}
|
|
97056
|
+
const timeoutMs = resolveEmbeddingTimeoutMs(this.config);
|
|
96924
97057
|
const response = await fetchWithTimeout(normalizeEmbeddingEndpoint(this.endpoint), {
|
|
96925
97058
|
method: "POST",
|
|
96926
97059
|
headers,
|
|
96927
97060
|
body: JSON.stringify(body)
|
|
96928
|
-
},
|
|
97061
|
+
}, timeoutMs, signal);
|
|
96929
97062
|
if (!response.ok) {
|
|
96930
|
-
const errBody = await readBodyWithByteCap(response, undefined, { bodyTimeoutMs:
|
|
97063
|
+
const errBody = await readBodyWithByteCap(response, undefined, { bodyTimeoutMs: timeoutMs, signal }).catch((err) => {
|
|
96931
97064
|
if (signal?.aborted)
|
|
96932
97065
|
throw err;
|
|
96933
97066
|
return "";
|
|
96934
97067
|
});
|
|
96935
97068
|
throw new Error(`Embedding request failed (${response.status}): ${this.safeErrorBody(errBody)}`);
|
|
96936
97069
|
}
|
|
96937
|
-
const json = JSON.parse(await readBodyWithByteCap(response, undefined, { bodyTimeoutMs:
|
|
97070
|
+
const json = JSON.parse(await readBodyWithByteCap(response, undefined, { bodyTimeoutMs: timeoutMs, signal }));
|
|
96938
97071
|
if (!json.data?.[0]?.embedding) {
|
|
96939
97072
|
throw new Error(`Unexpected embedding response format: missing data[0].embedding.${embeddingEndpointPathHint(this.endpoint)}`);
|
|
96940
97073
|
}
|
|
@@ -96946,9 +97079,42 @@ class RemoteEmbedder {
|
|
|
96946
97079
|
const results = new Array(texts.length).fill(undefined);
|
|
96947
97080
|
const headers = this.buildHeaders();
|
|
96948
97081
|
const ollamaOpts = resolveOllamaOptions(this.config);
|
|
96949
|
-
|
|
97082
|
+
let effectiveTokenBudget = this.config.maxTokens ?? DEFAULT_TOKEN_BUDGET;
|
|
96950
97083
|
const maxCount = this.config.batchSize ?? DEFAULT_REMOTE_BATCH_SIZE;
|
|
96951
|
-
const
|
|
97084
|
+
const maxInputTokens = this.config.maxInputTokens ?? DEFAULT_MAX_INPUT_TOKENS;
|
|
97085
|
+
const textBatches = buildTokenBoundedBatches(texts, effectiveTokenBudget, maxCount);
|
|
97086
|
+
const configuredTimeoutMs = resolveEmbeddingTimeoutMs(this.config);
|
|
97087
|
+
let dispatchedBatchCount = 0;
|
|
97088
|
+
let budgetShrunk = false;
|
|
97089
|
+
const maybeShrinkBudget = (rejectedIndices, rejectedBatchIndex, rejectedRequestTokens) => {
|
|
97090
|
+
if (budgetShrunk)
|
|
97091
|
+
return;
|
|
97092
|
+
budgetShrunk = true;
|
|
97093
|
+
const floor2 = ADAPTIVE_BUDGET_FLOOR_MULTIPLIER * maxInputTokens;
|
|
97094
|
+
effectiveTokenBudget = Math.max(Math.round(effectiveTokenBudget * ADAPTIVE_BUDGET_SHRINK_FACTOR), floor2);
|
|
97095
|
+
const notYetDispatched = textBatches.slice(dispatchedBatchCount);
|
|
97096
|
+
const remainingIndices = notYetDispatched.flatMap((batch) => batch.indices);
|
|
97097
|
+
if (remainingIndices.length > 0) {
|
|
97098
|
+
const remainingTexts = remainingIndices.map((i) => texts[i]);
|
|
97099
|
+
const replanned = buildTokenBoundedBatches(remainingTexts, effectiveTokenBudget, maxCount).map((batch) => ({
|
|
97100
|
+
indices: batch.indices.map((localIndex) => remainingIndices[localIndex]),
|
|
97101
|
+
oversized: batch.oversized
|
|
97102
|
+
}));
|
|
97103
|
+
textBatches.splice(dispatchedBatchCount, textBatches.length - dispatchedBatchCount, ...replanned);
|
|
97104
|
+
}
|
|
97105
|
+
warnVerbose(`[embed] provider rejected a ${rejectedRequestTokens}-token request as over its context; request budget lowered to ${effectiveTokenBudget.toLocaleString()} for the rest of this run`);
|
|
97106
|
+
commitBatch(rejectedIndices, rejectedIndices.map(() => {
|
|
97107
|
+
return;
|
|
97108
|
+
}), undefined, {
|
|
97109
|
+
batchIndex: rejectedBatchIndex,
|
|
97110
|
+
batchCount: textBatches.length,
|
|
97111
|
+
docCount: rejectedIndices.length,
|
|
97112
|
+
requestTokens: rejectedRequestTokens,
|
|
97113
|
+
elapsedMs: 0,
|
|
97114
|
+
outcome: "budget-lowered",
|
|
97115
|
+
reason: `provider rejected ${rejectedRequestTokens.toLocaleString()} tokens as over its context; request budget lowered to ${effectiveTokenBudget.toLocaleString()} for the rest of this run`
|
|
97116
|
+
});
|
|
97117
|
+
};
|
|
96952
97118
|
const dispatchAbort = new AbortController;
|
|
96953
97119
|
const stopDispatch = (reason) => {
|
|
96954
97120
|
if (!dispatchAbort.signal.aborted)
|
|
@@ -96964,66 +97130,136 @@ class RemoteEmbedder {
|
|
|
96964
97130
|
}
|
|
96965
97131
|
}
|
|
96966
97132
|
let firstOnBatchError;
|
|
96967
|
-
const commitBatch = (indices, embeddings, model) => {
|
|
97133
|
+
const commitBatch = (indices, embeddings, model, outcome) => {
|
|
96968
97134
|
if (!onBatch)
|
|
96969
97135
|
return;
|
|
96970
97136
|
if (firstOnBatchError !== undefined)
|
|
96971
97137
|
return;
|
|
96972
97138
|
try {
|
|
96973
|
-
onBatch(indices, embeddings, model);
|
|
97139
|
+
onBatch(indices, embeddings, model, outcome);
|
|
96974
97140
|
} catch (err) {
|
|
96975
97141
|
firstOnBatchError = err;
|
|
96976
97142
|
stopDispatch(err);
|
|
96977
97143
|
}
|
|
96978
97144
|
};
|
|
96979
|
-
const requestAndCommit = async (indices) => {
|
|
97145
|
+
const requestAndCommit = async (indices, batchIndex, isTimeoutRetry = false, timeoutAttempt = 0) => {
|
|
97146
|
+
if (dispatchAbort.signal.aborted)
|
|
97147
|
+
return;
|
|
96980
97148
|
const batch = indices.map((i) => texts[i]);
|
|
97149
|
+
const requestTokens = batch.reduce((sum, text) => sum + estimateTokenCount(text), 0);
|
|
97150
|
+
const requestTimeoutMs = scaleEmbeddingTimeoutMs(configuredTimeoutMs, requestTokens, effectiveTokenBudget);
|
|
97151
|
+
const requestStart = Date.now();
|
|
96981
97152
|
let batchEmbeddings;
|
|
96982
97153
|
let responseModel;
|
|
97154
|
+
let outcome;
|
|
97155
|
+
let failureReason;
|
|
96983
97156
|
try {
|
|
96984
|
-
const { vectors, model } = await this.requestBatch(batch, headers, ollamaOpts, signal);
|
|
97157
|
+
const { vectors, model } = await this.requestBatch(batch, headers, ollamaOpts, requestTimeoutMs, signal);
|
|
96985
97158
|
for (let k2 = 0;k2 < indices.length; k2++) {
|
|
96986
97159
|
results[indices[k2]] = vectors[k2];
|
|
96987
97160
|
}
|
|
96988
97161
|
batchEmbeddings = indices.map((i) => results[i]);
|
|
96989
97162
|
responseModel = model;
|
|
97163
|
+
outcome = "stored";
|
|
96990
97164
|
} catch (err) {
|
|
96991
97165
|
if (signal?.aborted)
|
|
96992
97166
|
throw err;
|
|
97167
|
+
if (err instanceof ContextExceededError) {
|
|
97168
|
+
maybeShrinkBudget(indices, batchIndex, requestTokens);
|
|
97169
|
+
}
|
|
96993
97170
|
if (err instanceof ContextExceededError && indices.length > 1) {
|
|
96994
97171
|
const mid = Math.ceil(indices.length / 2);
|
|
96995
|
-
await requestAndCommit(indices.slice(0, mid));
|
|
96996
|
-
await requestAndCommit(indices.slice(mid));
|
|
97172
|
+
await requestAndCommit(indices.slice(0, mid), batchIndex, false, timeoutAttempt);
|
|
97173
|
+
await requestAndCommit(indices.slice(mid), batchIndex, false, timeoutAttempt);
|
|
97174
|
+
return;
|
|
97175
|
+
}
|
|
97176
|
+
const timedOut = isEmbeddingTimeoutError(err);
|
|
97177
|
+
if (timedOut && !isTimeoutRetry) {
|
|
97178
|
+
const backoffMs = embeddingTimeoutRetryBackoffMs(timeoutAttempt);
|
|
97179
|
+
warnVerbose(`[embed] batch of ${batch.length} document(s) timed out after ${requestTimeoutMs}ms; retrying once after a ${Math.round(backoffMs)}ms backoff`);
|
|
97180
|
+
commitBatch(indices, indices.map(() => {
|
|
97181
|
+
return;
|
|
97182
|
+
}), undefined, {
|
|
97183
|
+
batchIndex,
|
|
97184
|
+
batchCount: textBatches.length,
|
|
97185
|
+
docCount: indices.length,
|
|
97186
|
+
requestTokens,
|
|
97187
|
+
elapsedMs: backoffMs,
|
|
97188
|
+
outcome: "retrying",
|
|
97189
|
+
reason: "timed out"
|
|
97190
|
+
});
|
|
97191
|
+
await abortableDelay(backoffMs, signal, "embedding interrupted during retry backoff");
|
|
97192
|
+
if (!dispatchAbort.signal.aborted) {
|
|
97193
|
+
return requestAndCommit(indices, batchIndex, true, timeoutAttempt);
|
|
97194
|
+
}
|
|
97195
|
+
} else if (timedOut && indices.length > 1) {
|
|
97196
|
+
const mid = Math.ceil(indices.length / 2);
|
|
97197
|
+
await requestAndCommit(indices.slice(0, mid), batchIndex, false, timeoutAttempt + 1);
|
|
97198
|
+
await requestAndCommit(indices.slice(mid), batchIndex, false, timeoutAttempt + 1);
|
|
96997
97199
|
return;
|
|
96998
97200
|
}
|
|
96999
97201
|
const message = err instanceof Error ? err.message : String(err);
|
|
97000
97202
|
const reason = err instanceof ContextExceededError ? "context-window-exceeded" : "batch-request-failed";
|
|
97001
|
-
|
|
97002
|
-
|
|
97003
|
-
|
|
97004
|
-
|
|
97203
|
+
const failureKind = reason === "batch-request-failed" ? timedOut ? "timeout" : "network-error" : undefined;
|
|
97204
|
+
let stopRequested = false;
|
|
97205
|
+
for (const [k2, idx] of indices.entries()) {
|
|
97206
|
+
if (onSkip?.({
|
|
97207
|
+
index: idx,
|
|
97208
|
+
reason,
|
|
97209
|
+
message,
|
|
97210
|
+
batchStart: k2 === 0,
|
|
97211
|
+
batchSize: indices.length,
|
|
97212
|
+
failureKind
|
|
97213
|
+
}) === false)
|
|
97214
|
+
stopRequested = true;
|
|
97215
|
+
}
|
|
97216
|
+
if (stopRequested)
|
|
97217
|
+
stopDispatch();
|
|
97005
97218
|
batchEmbeddings = indices.map(() => {
|
|
97006
97219
|
return;
|
|
97007
97220
|
});
|
|
97008
|
-
|
|
97009
|
-
|
|
97221
|
+
outcome = "failed";
|
|
97222
|
+
failureReason = message;
|
|
97223
|
+
}
|
|
97224
|
+
commitBatch(indices, batchEmbeddings, responseModel, {
|
|
97225
|
+
batchIndex,
|
|
97226
|
+
batchCount: textBatches.length,
|
|
97227
|
+
docCount: indices.length,
|
|
97228
|
+
requestTokens,
|
|
97229
|
+
elapsedMs: Date.now() - requestStart,
|
|
97230
|
+
outcome,
|
|
97231
|
+
reason: failureReason
|
|
97232
|
+
});
|
|
97010
97233
|
};
|
|
97011
|
-
const runProviderBatch = async (textBatch) => {
|
|
97234
|
+
const runProviderBatch = async (textBatch, batchIndex) => {
|
|
97235
|
+
dispatchedBatchCount = batchIndex;
|
|
97012
97236
|
if (textBatch.oversized) {
|
|
97013
97237
|
const idx = textBatch.indices[0];
|
|
97014
97238
|
const estTokens = estimateTokenCount(texts[idx]);
|
|
97015
97239
|
onSkip?.({
|
|
97016
97240
|
index: idx,
|
|
97017
97241
|
reason: "context-window-exceeded",
|
|
97018
|
-
message: `Document estimated at ${estTokens} tokens exceeds the ${
|
|
97242
|
+
message: `Document estimated at ${estTokens} tokens exceeds the ${effectiveTokenBudget}-token embedding budget; skipped.`,
|
|
97243
|
+
batchStart: true,
|
|
97244
|
+
batchSize: 1
|
|
97245
|
+
});
|
|
97246
|
+
commitBatch([idx], [undefined], undefined, {
|
|
97247
|
+
batchIndex,
|
|
97248
|
+
batchCount: textBatches.length,
|
|
97249
|
+
docCount: 1,
|
|
97250
|
+
requestTokens: estTokens,
|
|
97251
|
+
elapsedMs: 0,
|
|
97252
|
+
outcome: "failed",
|
|
97253
|
+
reason: "oversized"
|
|
97019
97254
|
});
|
|
97020
|
-
commitBatch([idx], [undefined]);
|
|
97021
97255
|
return;
|
|
97022
97256
|
}
|
|
97023
|
-
await requestAndCommit(textBatch.indices);
|
|
97257
|
+
await requestAndCommit(textBatch.indices, batchIndex);
|
|
97024
97258
|
};
|
|
97025
97259
|
const concurrency = resolveEmbeddingConcurrency(this.config);
|
|
97026
|
-
await concurrentMap(textBatches, runProviderBatch, concurrency, {
|
|
97260
|
+
await concurrentMap(textBatches, (textBatch, i) => runProviderBatch(textBatch, i + 1), concurrency, {
|
|
97261
|
+
signal: dispatchAbort.signal
|
|
97262
|
+
});
|
|
97027
97263
|
if (callerAbortListener)
|
|
97028
97264
|
signal?.removeEventListener("abort", callerAbortListener);
|
|
97029
97265
|
if (signal?.aborted) {
|
|
@@ -97034,7 +97270,7 @@ class RemoteEmbedder {
|
|
|
97034
97270
|
}
|
|
97035
97271
|
return results;
|
|
97036
97272
|
}
|
|
97037
|
-
async requestBatch(batch, headers, ollamaOpts, signal) {
|
|
97273
|
+
async requestBatch(batch, headers, ollamaOpts, timeoutMs, signal) {
|
|
97038
97274
|
const body = {
|
|
97039
97275
|
input: batch,
|
|
97040
97276
|
model: this.model
|
|
@@ -97049,9 +97285,9 @@ class RemoteEmbedder {
|
|
|
97049
97285
|
method: "POST",
|
|
97050
97286
|
headers,
|
|
97051
97287
|
body: JSON.stringify(body)
|
|
97052
|
-
},
|
|
97288
|
+
}, timeoutMs, signal);
|
|
97053
97289
|
if (!response.ok) {
|
|
97054
|
-
const respBody = await readBodyWithByteCap(response, undefined, { bodyTimeoutMs:
|
|
97290
|
+
const respBody = await readBodyWithByteCap(response, undefined, { bodyTimeoutMs: timeoutMs, signal }).catch((err) => {
|
|
97055
97291
|
if (signal?.aborted)
|
|
97056
97292
|
throw err;
|
|
97057
97293
|
return "";
|
|
@@ -97062,7 +97298,7 @@ class RemoteEmbedder {
|
|
|
97062
97298
|
}
|
|
97063
97299
|
throw new Error(message);
|
|
97064
97300
|
}
|
|
97065
|
-
const json = JSON.parse(await readBodyWithByteCap(response, undefined, { bodyTimeoutMs:
|
|
97301
|
+
const json = JSON.parse(await readBodyWithByteCap(response, undefined, { bodyTimeoutMs: timeoutMs, signal }));
|
|
97066
97302
|
if (!json.data || json.data.length !== batch.length) {
|
|
97067
97303
|
throw new Error(`Unexpected embedding batch response: expected ${batch.length} embeddings, got ${json.data?.length ?? 0}.${embeddingEndpointPathHint(this.endpoint)}`);
|
|
97068
97304
|
}
|
|
@@ -97131,6 +97367,15 @@ function resolveOllamaOptions(config) {
|
|
|
97131
97367
|
function hasRemoteEndpoint(config) {
|
|
97132
97368
|
return isHttpUrl(config.endpoint);
|
|
97133
97369
|
}
|
|
97370
|
+
function describeEmbeddingCredential(apiKey) {
|
|
97371
|
+
if (!apiKey)
|
|
97372
|
+
return "none configured";
|
|
97373
|
+
if (SECRET_STORE_REFERENCE_PATTERN.test(apiKey))
|
|
97374
|
+
return `${apiKey} (store)`;
|
|
97375
|
+
if (ENV_REFERENCE_PATTERN.test(apiKey))
|
|
97376
|
+
return `${apiKey} (env)`;
|
|
97377
|
+
return "literal apiKey";
|
|
97378
|
+
}
|
|
97134
97379
|
|
|
97135
97380
|
// src/llm/embedder.ts
|
|
97136
97381
|
var embedderOverrides;
|
|
@@ -97181,9 +97426,12 @@ function deriveSemanticProviderFingerprint(embedding) {
|
|
|
97181
97426
|
}
|
|
97182
97427
|
return `local:${embedding?.localModel ?? DEFAULT_LOCAL_MODEL}`;
|
|
97183
97428
|
}
|
|
97184
|
-
|
|
97429
|
+
function formatEmbeddingHeartbeat(storedCount, total, failedCount) {
|
|
97430
|
+
return `Still generating embeddings: ${storedCount}/${total} stored, ${failedCount} failed; waiting on embedding provider.`;
|
|
97431
|
+
}
|
|
97185
97432
|
var CANARY_SAMPLE_SIZE = 8;
|
|
97186
97433
|
var CANARY_SIMILARITY_THRESHOLD = 0.999;
|
|
97434
|
+
var CIRCUIT_BREAKER_THRESHOLD = 3;
|
|
97187
97435
|
function decideEmbeddingCompatibility(pairs) {
|
|
97188
97436
|
if (pairs.length === 0)
|
|
97189
97437
|
return { outcome: "keep", medianSimilarity: undefined, verifiedSamples: 0 };
|
|
@@ -97215,7 +97463,7 @@ function deriveObservedEmbeddingIdentity(embedding, observedModel, observedVecto
|
|
|
97215
97463
|
}
|
|
97216
97464
|
return `local:${embedding?.localModel ?? DEFAULT_LOCAL_MODEL}|${observedVectorLen}`;
|
|
97217
97465
|
}
|
|
97218
|
-
async function runEmbeddingCanary(db, config, signal) {
|
|
97466
|
+
async function runEmbeddingCanary(db, config, signal, maxInputTokens) {
|
|
97219
97467
|
const samples = sampleEmbeddedEntriesForCanary(db, CANARY_SAMPLE_SIZE);
|
|
97220
97468
|
if (samples.length === 0) {
|
|
97221
97469
|
return { outcome: "keep", verified: false, viaIdentityMatch: false };
|
|
@@ -97224,7 +97472,7 @@ async function runEmbeddingCanary(db, config, signal) {
|
|
|
97224
97472
|
const skips = [];
|
|
97225
97473
|
let canaryVectors;
|
|
97226
97474
|
try {
|
|
97227
|
-
canaryVectors = await embedBatch(samples.map((sample) => sample.searchText), config.embedding, signal, (skip) => skips.push(skip), (_indices, _embeddings, model) => {
|
|
97475
|
+
canaryVectors = await embedBatch(samples.map((sample) => capEmbeddingText(sample.searchText, maxInputTokens).text), config.embedding, signal, (skip) => skips.push(skip), (_indices, _embeddings, model) => {
|
|
97228
97476
|
if (model)
|
|
97229
97477
|
observedModel = model;
|
|
97230
97478
|
});
|
|
@@ -97271,19 +97519,34 @@ function throwIfAborted(signal) {
|
|
|
97271
97519
|
}
|
|
97272
97520
|
}
|
|
97273
97521
|
async function generateEmbeddingsForDb(db, config, onProgress, signal, entryIds, opts) {
|
|
97522
|
+
if (db.inTransaction) {
|
|
97523
|
+
throw new Error("generateEmbeddingsForDb was called with an ambient transaction already open on `db`: per-batch commits " + "would become SAVEPOINTs inside it, losing the crash-durability contract per-batch commit exists for. " + "Run the embedding phase on a connection with no open transaction.");
|
|
97524
|
+
}
|
|
97274
97525
|
throwIfAborted(signal);
|
|
97275
97526
|
if (config.semanticSearchMode === "off") {
|
|
97527
|
+
purgeEmbeddingSalvage(db);
|
|
97276
97528
|
onProgress({ phase: "embeddings", message: "Semantic search disabled; skipping embeddings." });
|
|
97277
97529
|
return { success: false, message: "Semantic search is disabled." };
|
|
97278
97530
|
}
|
|
97531
|
+
if (hasRemoteEndpoint(config.embedding ?? {})) {
|
|
97532
|
+
const endpoint = normalizeEmbeddingEndpoint(config.embedding?.endpoint ?? "");
|
|
97533
|
+
const credential = describeEmbeddingCredential(config.embedding?.apiKey);
|
|
97534
|
+
const configFileSuffix = isVerbose() ? `; config: ${getConfigPath()}` : "";
|
|
97535
|
+
onProgress({
|
|
97536
|
+
phase: "embeddings",
|
|
97537
|
+
message: `[embed] endpoint ${endpoint}, model ${config.embedding?.model ?? "unknown"}; credential: ${credential}${configFileSuffix}`
|
|
97538
|
+
});
|
|
97539
|
+
}
|
|
97279
97540
|
const vecFastPathWasReady = isVecFastPathReady(db);
|
|
97280
97541
|
const currentFingerprint = deriveSemanticProviderFingerprint(config.embedding);
|
|
97281
97542
|
const storedFingerprint = getMeta(db, "embeddingFingerprint");
|
|
97282
97543
|
let targetEntryIds = entryIds;
|
|
97283
97544
|
let rebuildReason;
|
|
97545
|
+
const maxInputTokens = config.embedding?.maxInputTokens ?? DEFAULT_MAX_INPUT_TOKENS;
|
|
97284
97546
|
if (opts?.forceReembed) {
|
|
97285
97547
|
db.transaction(() => {
|
|
97286
97548
|
purgeEmbeddings(db, { dropVecTable: true });
|
|
97549
|
+
purgeEmbeddingSalvage(db);
|
|
97287
97550
|
deleteMeta(db, "embeddingDim");
|
|
97288
97551
|
setMeta(db, "embeddingFingerprint", currentFingerprint);
|
|
97289
97552
|
deleteMeta(db, "embeddingIdentity");
|
|
@@ -97291,7 +97554,7 @@ async function generateEmbeddingsForDb(db, config, onProgress, signal, entryIds,
|
|
|
97291
97554
|
targetEntryIds = undefined;
|
|
97292
97555
|
rebuildReason = "forced by --reembed";
|
|
97293
97556
|
} else if (storedFingerprint && storedFingerprint !== currentFingerprint) {
|
|
97294
|
-
const decision = await runEmbeddingCanary(db, config, signal);
|
|
97557
|
+
const decision = await runEmbeddingCanary(db, config, signal, maxInputTokens);
|
|
97295
97558
|
if (decision.outcome === "unverifiable") {
|
|
97296
97559
|
warn(`[embed] ${decision.message}`);
|
|
97297
97560
|
onProgress({ phase: "embeddings", message: decision.message });
|
|
@@ -97300,6 +97563,7 @@ async function generateEmbeddingsForDb(db, config, onProgress, signal, entryIds,
|
|
|
97300
97563
|
if (decision.outcome === "rebuild") {
|
|
97301
97564
|
db.transaction(() => {
|
|
97302
97565
|
purgeEmbeddings(db, { dropVecTable: true });
|
|
97566
|
+
purgeEmbeddingSalvage(db);
|
|
97303
97567
|
deleteMeta(db, "embeddingDim");
|
|
97304
97568
|
setMeta(db, "embeddingFingerprint", currentFingerprint);
|
|
97305
97569
|
if (decision.identity)
|
|
@@ -97311,6 +97575,7 @@ async function generateEmbeddingsForDb(db, config, onProgress, signal, entryIds,
|
|
|
97311
97575
|
rebuildReason = decision.reason;
|
|
97312
97576
|
} else {
|
|
97313
97577
|
setMeta(db, "embeddingFingerprint", currentFingerprint);
|
|
97578
|
+
relabelEmbeddingSalvageFingerprint(db, storedFingerprint, currentFingerprint);
|
|
97314
97579
|
if (decision.identity)
|
|
97315
97580
|
setMeta(db, "embeddingIdentity", decision.identity);
|
|
97316
97581
|
if (decision.verified) {
|
|
@@ -97321,28 +97586,65 @@ async function generateEmbeddingsForDb(db, config, onProgress, signal, entryIds,
|
|
|
97321
97586
|
onProgress({ phase: "embeddings", message });
|
|
97322
97587
|
}
|
|
97323
97588
|
}
|
|
97589
|
+
} else {
|
|
97590
|
+
setMeta(db, "embeddingFingerprint", currentFingerprint);
|
|
97324
97591
|
}
|
|
97325
97592
|
try {
|
|
97326
97593
|
throwIfAborted(signal);
|
|
97327
97594
|
const allEntries = getAllEntriesForEmbedding(db, targetEntryIds);
|
|
97328
|
-
|
|
97595
|
+
let vecFailedCount = 0;
|
|
97596
|
+
let vecUnavailableCount = 0;
|
|
97597
|
+
const { reusedCount, remaining: candidateEntries } = reuseSalvagedEmbeddings(db, allEntries, currentFingerprint, (entry, embedding) => {
|
|
97598
|
+
const result = upsertEmbedding(db, entry.id, embedding);
|
|
97599
|
+
if (result.vec === "failed")
|
|
97600
|
+
vecFailedCount++;
|
|
97601
|
+
if (result.vec === "unavailable")
|
|
97602
|
+
vecUnavailableCount++;
|
|
97603
|
+
return result.stored;
|
|
97604
|
+
});
|
|
97605
|
+
if (reusedCount > 0) {
|
|
97606
|
+
onProgress({
|
|
97607
|
+
phase: "embeddings",
|
|
97608
|
+
message: `Reused ${reusedCount} embedding${reusedCount === 1 ? "" : "s"} from the previous generation; embedding ${candidateEntries.length} new.`
|
|
97609
|
+
});
|
|
97610
|
+
}
|
|
97611
|
+
if (candidateEntries.length === 0) {
|
|
97329
97612
|
onProgress({ phase: "embeddings", message: "Embeddings already up to date." });
|
|
97330
97613
|
setMeta(db, "embeddingFingerprint", currentFingerprint);
|
|
97331
|
-
|
|
97614
|
+
if (reusedCount > 0) {
|
|
97615
|
+
const vecGenerationComplete = targetEntryIds === undefined ? isVecFastPathComplete(db) : vecFastPathWasReady;
|
|
97616
|
+
setVecFastPathReady(db, vecFailedCount === 0 && vecUnavailableCount === 0 && vecGenerationComplete);
|
|
97617
|
+
}
|
|
97618
|
+
purgeEmbeddingSalvage(db);
|
|
97619
|
+
return reusedCount > 0 ? { success: true, vecInsertFailures: vecFailedCount } : { success: true };
|
|
97620
|
+
}
|
|
97621
|
+
let truncatedCount = 0;
|
|
97622
|
+
const texts = [];
|
|
97623
|
+
const pendingEntries = [];
|
|
97624
|
+
for (const entry of candidateEntries) {
|
|
97625
|
+
const capped = capEmbeddingText(entry.searchText, maxInputTokens);
|
|
97626
|
+
if (capped.text.length === 0)
|
|
97627
|
+
continue;
|
|
97628
|
+
if (capped.truncated)
|
|
97629
|
+
truncatedCount++;
|
|
97630
|
+
pendingEntries.push(entry);
|
|
97631
|
+
texts.push(capped.text);
|
|
97632
|
+
}
|
|
97633
|
+
if (truncatedCount > 0) {
|
|
97634
|
+
const message = `[embed] ${truncatedCount} entr${truncatedCount === 1 ? "y" : "ies"} truncated to the ${maxInputTokens}-token embedding cap (embedding.maxInputTokens); rerun with a higher cap to embed the full text.`;
|
|
97635
|
+
onProgress({ phase: "embeddings", message });
|
|
97332
97636
|
}
|
|
97333
97637
|
if (rebuildReason) {
|
|
97334
|
-
const message = `[embed] Re-embedding ${
|
|
97335
|
-
warn(message);
|
|
97638
|
+
const message = `[embed] Re-embedding ${pendingEntries.length} entr${pendingEntries.length === 1 ? "y" : "ies"} because ${rebuildReason}`;
|
|
97336
97639
|
onProgress({ phase: "embeddings", message });
|
|
97337
97640
|
}
|
|
97338
97641
|
onProgress({
|
|
97339
97642
|
phase: "embeddings",
|
|
97340
|
-
message: `Generating embeddings for ${
|
|
97643
|
+
message: `Generating embeddings for ${pendingEntries.length} entr${pendingEntries.length === 1 ? "y" : "ies"}.`
|
|
97341
97644
|
});
|
|
97342
|
-
const texts = allEntries.map((entry) => entry.searchText);
|
|
97343
97645
|
if (isVerbose()) {
|
|
97344
97646
|
if (hasRemoteEndpoint(config.embedding ?? {})) {
|
|
97345
|
-
const tokenBudget = config.embedding?.maxTokens ??
|
|
97647
|
+
const tokenBudget = config.embedding?.maxTokens ?? DEFAULT_TOKEN_BUDGET;
|
|
97346
97648
|
const maxCount = config.embedding?.batchSize ?? DEFAULT_REMOTE_BATCH_SIZE;
|
|
97347
97649
|
const batches = buildTokenBoundedBatches(texts, tokenBudget, maxCount);
|
|
97348
97650
|
const batchNumberByIndex = new Map;
|
|
@@ -97350,7 +97652,7 @@ async function generateEmbeddingsForDb(db, config, onProgress, signal, entryIds,
|
|
|
97350
97652
|
for (const i of batch.indices)
|
|
97351
97653
|
batchNumberByIndex.set(i, batchIdx + 1);
|
|
97352
97654
|
});
|
|
97353
|
-
for (const [i, entry] of
|
|
97655
|
+
for (const [i, entry] of pendingEntries.entries()) {
|
|
97354
97656
|
const chars = entry.searchText.length;
|
|
97355
97657
|
const tokens = estimateTokenCount(entry.searchText);
|
|
97356
97658
|
const batch = batches[batchNumberByIndex.get(i) - 1];
|
|
@@ -97358,7 +97660,7 @@ async function generateEmbeddingsForDb(db, config, onProgress, signal, entryIds,
|
|
|
97358
97660
|
warnVerbose(`[embed] ${entry.itemRef} (${chars} chars, est. ${tokens} tokens) \u2192 ${label}`);
|
|
97359
97661
|
}
|
|
97360
97662
|
} else {
|
|
97361
|
-
for (const entry of
|
|
97663
|
+
for (const entry of pendingEntries) {
|
|
97362
97664
|
warnVerbose(`[embed] ${entry.itemRef} (${entry.searchText.length} chars, est. ${estimateTokenCount(entry.searchText)} tokens)`);
|
|
97363
97665
|
}
|
|
97364
97666
|
}
|
|
@@ -97367,27 +97669,68 @@ async function generateEmbeddingsForDb(db, config, onProgress, signal, entryIds,
|
|
|
97367
97669
|
let storedCount = 0;
|
|
97368
97670
|
let skippedCount = 0;
|
|
97369
97671
|
let embedFailedCount = 0;
|
|
97370
|
-
let vecFailedCount = 0;
|
|
97371
|
-
let vecUnavailableCount = 0;
|
|
97372
97672
|
let storedTokens = 0;
|
|
97373
|
-
let lastProgressBucket = 0;
|
|
97374
97673
|
try {
|
|
97375
97674
|
heartbeatTimer = setInterval(() => {
|
|
97376
97675
|
onProgress({
|
|
97377
97676
|
phase: "embeddings",
|
|
97378
|
-
message:
|
|
97677
|
+
message: formatEmbeddingHeartbeat(storedCount, pendingEntries.length, embedFailedCount)
|
|
97379
97678
|
});
|
|
97380
97679
|
}, 15000);
|
|
97381
97680
|
const skips = [];
|
|
97382
97681
|
const embedStart = Date.now();
|
|
97682
|
+
let consecutiveSingleDocFailures = 0;
|
|
97683
|
+
let consecutiveNetworkErrorFailures = 0;
|
|
97684
|
+
let circuitBreakerReason;
|
|
97685
|
+
const onSkip = (skip) => {
|
|
97686
|
+
skips.push(skip);
|
|
97687
|
+
if (!skip.batchStart)
|
|
97688
|
+
return;
|
|
97689
|
+
if (skip.reason === "context-window-exceeded") {
|
|
97690
|
+
consecutiveSingleDocFailures = 0;
|
|
97691
|
+
consecutiveNetworkErrorFailures = 0;
|
|
97692
|
+
return;
|
|
97693
|
+
}
|
|
97694
|
+
consecutiveSingleDocFailures = skip.batchSize === 1 ? consecutiveSingleDocFailures + 1 : 0;
|
|
97695
|
+
consecutiveNetworkErrorFailures = skip.failureKind === "network-error" ? consecutiveNetworkErrorFailures + 1 : 0;
|
|
97696
|
+
if (consecutiveSingleDocFailures >= CIRCUIT_BREAKER_THRESHOLD || consecutiveNetworkErrorFailures >= CIRCUIT_BREAKER_THRESHOLD) {
|
|
97697
|
+
circuitBreakerReason = skip.message;
|
|
97698
|
+
return false;
|
|
97699
|
+
}
|
|
97700
|
+
return;
|
|
97701
|
+
};
|
|
97383
97702
|
let observedModel;
|
|
97384
97703
|
let observedVectorLen;
|
|
97385
|
-
const
|
|
97704
|
+
const reportPerBatchLine = hasRemoteEndpoint(config.embedding ?? {});
|
|
97705
|
+
const onBatch = (indices, batchEmbeddings, model, outcome) => {
|
|
97706
|
+
if (outcome?.outcome === "retrying") {
|
|
97707
|
+
if (reportPerBatchLine) {
|
|
97708
|
+
onProgress({
|
|
97709
|
+
phase: "embeddings",
|
|
97710
|
+
message: `[embed] batch ${outcome.batchIndex}/${outcome.batchCount}: ${outcome.docCount} docs, ${outcome.requestTokens.toLocaleString()} tokens \u2192 retrying after ${(outcome.elapsedMs / 1000).toFixed(1)} s`
|
|
97711
|
+
});
|
|
97712
|
+
}
|
|
97713
|
+
return;
|
|
97714
|
+
}
|
|
97715
|
+
if (outcome?.outcome === "budget-lowered") {
|
|
97716
|
+
if (reportPerBatchLine) {
|
|
97717
|
+
onProgress({
|
|
97718
|
+
phase: "embeddings",
|
|
97719
|
+
message: `[embed] batch ${outcome.batchIndex}/${outcome.batchCount}: ${outcome.docCount} docs, ${outcome.requestTokens.toLocaleString()} tokens \u2192 ${outcome.reason}`
|
|
97720
|
+
});
|
|
97721
|
+
}
|
|
97722
|
+
return;
|
|
97723
|
+
}
|
|
97386
97724
|
if (model)
|
|
97387
97725
|
observedModel = model;
|
|
97726
|
+
if (batchEmbeddings.some((embedding) => embedding !== undefined)) {
|
|
97727
|
+
consecutiveSingleDocFailures = 0;
|
|
97728
|
+
consecutiveNetworkErrorFailures = 0;
|
|
97729
|
+
}
|
|
97388
97730
|
db.transaction(() => {
|
|
97389
97731
|
for (let k2 = 0;k2 < indices.length; k2++) {
|
|
97390
|
-
const
|
|
97732
|
+
const index = indices[k2];
|
|
97733
|
+
const entry = pendingEntries[index];
|
|
97391
97734
|
if (!entry)
|
|
97392
97735
|
continue;
|
|
97393
97736
|
const embedding = batchEmbeddings[k2];
|
|
@@ -97400,7 +97743,7 @@ async function generateEmbeddingsForDb(db, config, onProgress, signal, entryIds,
|
|
|
97400
97743
|
const result = upsertEmbedding(db, entry.id, embedding);
|
|
97401
97744
|
if (result.stored) {
|
|
97402
97745
|
storedCount++;
|
|
97403
|
-
storedTokens += estimateTokenCount(
|
|
97746
|
+
storedTokens += estimateTokenCount(texts[index]);
|
|
97404
97747
|
} else {
|
|
97405
97748
|
skippedCount++;
|
|
97406
97749
|
}
|
|
@@ -97410,29 +97753,25 @@ async function generateEmbeddingsForDb(db, config, onProgress, signal, entryIds,
|
|
|
97410
97753
|
vecUnavailableCount++;
|
|
97411
97754
|
}
|
|
97412
97755
|
})();
|
|
97413
|
-
|
|
97414
|
-
|
|
97415
|
-
|
|
97756
|
+
if (outcome && outcome.reason !== "oversized" && reportPerBatchLine) {
|
|
97757
|
+
const elapsedSeconds2 = (outcome.elapsedMs / 1000).toFixed(1);
|
|
97758
|
+
const outcomeLabel = outcome.outcome === "stored" ? `${outcome.docCount} stored (${elapsedSeconds2} s)` : `failed: ${outcome.reason}`;
|
|
97416
97759
|
onProgress({
|
|
97417
97760
|
phase: "embeddings",
|
|
97418
|
-
message: `
|
|
97761
|
+
message: `[embed] batch ${outcome.batchIndex}/${outcome.batchCount}: ${outcome.docCount} docs, ${outcome.requestTokens.toLocaleString()} tokens \u2192 ${outcomeLabel}`
|
|
97419
97762
|
});
|
|
97420
97763
|
}
|
|
97764
|
+
onProgress({
|
|
97765
|
+
phase: "embeddings",
|
|
97766
|
+
message: `Embedded ${storedCount}/${pendingEntries.length} entries.`
|
|
97767
|
+
});
|
|
97421
97768
|
};
|
|
97422
|
-
await embedBatch(texts, config.embedding, signal,
|
|
97769
|
+
await embedBatch(texts, config.embedding, signal, onSkip, onBatch);
|
|
97423
97770
|
throwIfAborted(signal);
|
|
97424
97771
|
const elapsedSeconds = Math.max((Date.now() - embedStart) / 1000, 0.001);
|
|
97425
97772
|
if (skippedCount > 0) {
|
|
97426
97773
|
warn(`[embed] ${skippedCount} embedding${skippedCount === 1 ? "" : "s"} skipped (entry deleted between queue and write)`);
|
|
97427
97774
|
}
|
|
97428
|
-
if (embedFailedCount > 0) {
|
|
97429
|
-
const detail = skips.slice(0, 20).map((skip) => ` - ${allEntries[skip.index]?.itemRef ?? skip.index} (${skip.reason}): ${skip.message}`).join(`
|
|
97430
|
-
`);
|
|
97431
|
-
const more = skips.length > 20 ? `
|
|
97432
|
-
...and ${skips.length - 20} more` : "";
|
|
97433
|
-
warn(`[embed] ${embedFailedCount} embedding${embedFailedCount === 1 ? "" : "s"} could not be generated and ${embedFailedCount === 1 ? "was" : "were"} skipped:
|
|
97434
|
-
${detail}${more}`);
|
|
97435
|
-
}
|
|
97436
97775
|
const vecGenerationComplete = targetEntryIds === undefined ? isVecFastPathComplete(db) : vecFastPathWasReady;
|
|
97437
97776
|
setVecFastPathReady(db, vecFailedCount === 0 && vecUnavailableCount === 0 && vecGenerationComplete);
|
|
97438
97777
|
if (vecFailedCount > 0) {
|
|
@@ -97440,14 +97779,39 @@ ${detail}${more}`);
|
|
|
97440
97779
|
}
|
|
97441
97780
|
const entriesPerSec = storedCount / elapsedSeconds;
|
|
97442
97781
|
const tokensPerSec = storedTokens / elapsedSeconds;
|
|
97782
|
+
const totalStored = storedCount + reusedCount;
|
|
97783
|
+
const oversizedSkips = skips.filter((skip) => skip.reason === "context-window-exceeded");
|
|
97784
|
+
const timedOutSkips = skips.filter((skip) => skip.reason === "batch-request-failed" && skip.failureKind === "timeout");
|
|
97785
|
+
const failedSkips = skips.filter((skip) => skip.reason === "batch-request-failed" && skip.failureKind !== "timeout");
|
|
97786
|
+
const throughputLine = reusedCount > 0 ? `Stored ${totalStored} embedding${totalStored === 1 ? "" : "s"} (${reusedCount} reused, ${storedCount} newly embedded) in ${elapsedSeconds.toFixed(1)}s (${entriesPerSec.toFixed(1)} entries/s, ~${Math.round(tokensPerSec)} tokens/s)` : `Stored ${storedCount} embedding${storedCount === 1 ? "" : "s"} in ${elapsedSeconds.toFixed(1)}s (${entriesPerSec.toFixed(1)} entries/s, ~${Math.round(tokensPerSec)} tokens/s)`;
|
|
97443
97787
|
onProgress({
|
|
97444
97788
|
phase: "embeddings",
|
|
97445
|
-
message:
|
|
97789
|
+
message: `${throughputLine}; ${oversizedSkips.length} oversized skipped, ${timedOutSkips.length} timed out, ${failedSkips.length} failed.`
|
|
97446
97790
|
});
|
|
97791
|
+
const printSkipList = (label, skipList) => {
|
|
97792
|
+
if (skipList.length === 0)
|
|
97793
|
+
return;
|
|
97794
|
+
const limit = isVerbose() ? skipList.length : 20;
|
|
97795
|
+
const listed = skipList.slice(0, limit).map((skip) => ` - ${pendingEntries[skip.index]?.itemRef ?? skip.index}: ${skip.message}`).join(`
|
|
97796
|
+
`);
|
|
97797
|
+
const more = skipList.length > limit ? `
|
|
97798
|
+
...and ${skipList.length - limit} more` : "";
|
|
97799
|
+
onProgress({ phase: "embeddings", message: `[embed] ${label} skipped:
|
|
97800
|
+
${listed}${more}` });
|
|
97801
|
+
};
|
|
97802
|
+
printSkipList("oversized documents", oversizedSkips);
|
|
97803
|
+
printSkipList("timed-out documents", timedOutSkips);
|
|
97804
|
+
printSkipList("failed documents", failedSkips);
|
|
97447
97805
|
setMeta(db, "embeddingFingerprint", currentFingerprint);
|
|
97448
97806
|
const observedIdentity = deriveObservedEmbeddingIdentity(config.embedding, observedModel, observedVectorLen);
|
|
97449
97807
|
if (observedIdentity)
|
|
97450
97808
|
setMeta(db, "embeddingIdentity", observedIdentity);
|
|
97809
|
+
if (circuitBreakerReason !== undefined) {
|
|
97810
|
+
const message = `embedding provider failed ${CIRCUIT_BREAKER_THRESHOLD} consecutive batches ` + `(last: ${circuitBreakerReason}); stopped after ${storedCount} embedding${storedCount === 1 ? "" : "s"} ` + "were stored \u2014 rerun akm index when the endpoint is healthy";
|
|
97811
|
+
warn(`[embed] ${message}`);
|
|
97812
|
+
onProgress({ phase: "embeddings", message });
|
|
97813
|
+
return { success: false, message, vecInsertFailures: vecFailedCount };
|
|
97814
|
+
}
|
|
97451
97815
|
if (storedCount === 0 && embedFailedCount > 0) {
|
|
97452
97816
|
const firstMessage = skips[0]?.message ?? "All embeddings failed.";
|
|
97453
97817
|
return {
|
|
@@ -97455,6 +97819,7 @@ ${detail}${more}`);
|
|
|
97455
97819
|
message: `All ${embedFailedCount} embedding batch(es) failed: ${firstMessage}`
|
|
97456
97820
|
};
|
|
97457
97821
|
}
|
|
97822
|
+
purgeEmbeddingSalvage(db);
|
|
97458
97823
|
return { success: true, vecInsertFailures: vecFailedCount };
|
|
97459
97824
|
} finally {
|
|
97460
97825
|
if (heartbeatTimer)
|
|
@@ -97871,7 +98236,11 @@ async function indexWrittenAssets(stashDir, filePaths, options = {}) {
|
|
|
97871
98236
|
return await (async () => {
|
|
97872
98237
|
const rebuildProbe = probeLock(getIndexRebuildLockPath());
|
|
97873
98238
|
if (rebuildProbe.state === "held") {
|
|
97874
|
-
|
|
98239
|
+
const holderLabel = formatLockHolderPid({
|
|
98240
|
+
pid: rebuildProbe.holderPid,
|
|
98241
|
+
launcherPid: rebuildProbe.launcherPid ?? null
|
|
98242
|
+
});
|
|
98243
|
+
warn(`index rebuild in progress (pid ${holderLabel}); the next index pass will index ${filePaths.join(", ")}`);
|
|
97875
98244
|
return true;
|
|
97876
98245
|
}
|
|
97877
98246
|
const dbPath = getDbPath();
|
|
@@ -98845,7 +99214,7 @@ var PROPOSAL_TXN_PHASES = [
|
|
|
98845
99214
|
"committed"
|
|
98846
99215
|
];
|
|
98847
99216
|
function proposalHash(content) {
|
|
98848
|
-
return
|
|
99217
|
+
return createHash11("sha256").update(content).digest("hex");
|
|
98849
99218
|
}
|
|
98850
99219
|
function proposalFileHash(filePath) {
|
|
98851
99220
|
return proposalHash(fs55.readFileSync(filePath));
|