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
|
@@ -7070,7 +7070,8 @@ var init_errors = __esm(() => {
|
|
|
7070
7070
|
};
|
|
7071
7071
|
TRANSIENT_HINTS = {
|
|
7072
7072
|
RUN_LEASE_HELD: "Wait for the named engine invocation to finish or for the lease to expire, then retry. `akm workflow status <id>` shows the current lease.",
|
|
7073
|
-
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
|
+
STATE_DB_CONTENDED: "Another akm process is writing state.db right now. Wait a few seconds and retry; commands that support --skip-if-locked can skip instead of failing.",
|
|
7074
|
+
INDEX_DB_CONTENDED: "Another akm process is writing index.db; retry shortly, or pass --skip-if-locked on scheduled runs."
|
|
7074
7075
|
};
|
|
7075
7076
|
NOT_FOUND_HINTS = {
|
|
7076
7077
|
ASSET_NOT_FOUND: "Run `akm search <query>` or `akm index` to refresh the index.",
|
|
@@ -36375,7 +36376,7 @@ var require_libvips = __commonJS((exports, module) => {
|
|
|
36375
36376
|
SPDX-License-Identifier: Apache-2.0
|
|
36376
36377
|
*/
|
|
36377
36378
|
var { spawnSync: spawnSync6 } = __require("node:child_process");
|
|
36378
|
-
var { createHash:
|
|
36379
|
+
var { createHash: createHash11 } = __require("node:crypto");
|
|
36379
36380
|
var semverCoerce = require_coerce2();
|
|
36380
36381
|
var semverGreaterThanOrEqualTo = require_gte2();
|
|
36381
36382
|
var semverSatisfies = require_satisfies2();
|
|
@@ -36463,7 +36464,7 @@ var require_libvips = __commonJS((exports, module) => {
|
|
|
36463
36464
|
}
|
|
36464
36465
|
return false;
|
|
36465
36466
|
};
|
|
36466
|
-
var sha512 = (s) =>
|
|
36467
|
+
var sha512 = (s) => createHash11("sha512").update(s).digest("hex");
|
|
36467
36468
|
var yarnLocator = () => {
|
|
36468
36469
|
try {
|
|
36469
36470
|
const identHash = sha512(`imgsharp-libvips-${buildPlatformArch()}`);
|
|
@@ -75917,11 +75918,13 @@ var EmbeddingConnectionConfigSchema = exports_external.object({
|
|
|
75917
75918
|
apiKey: symbolicOrWarnApiKey("embedding.apiKey").optional(),
|
|
75918
75919
|
dimension: positiveInt.max(4096).optional(),
|
|
75919
75920
|
localModel: exports_external.string().min(1).optional(),
|
|
75921
|
+
maxInputTokens: positiveInt.optional(),
|
|
75920
75922
|
maxTokens: positiveInt.optional(),
|
|
75921
75923
|
batchSize: positiveInt.optional(),
|
|
75922
|
-
chunkSize: positiveInt.optional(),
|
|
75923
75924
|
contextLength: positiveInt.optional(),
|
|
75924
|
-
ollamaOptions: EmbeddingOllamaOptionsSchema.optional()
|
|
75925
|
+
ollamaOptions: EmbeddingOllamaOptionsSchema.optional(),
|
|
75926
|
+
timeoutMs: positiveInt.optional(),
|
|
75927
|
+
concurrency: positiveInt.max(16).optional()
|
|
75925
75928
|
}).passthrough();
|
|
75926
75929
|
|
|
75927
75930
|
// src/integrations/harnesses/ids.ts
|
|
@@ -76994,8 +76997,21 @@ function releaseLockRaw(lockPath) {
|
|
|
76994
76997
|
function tryAcquireLockSync(lockPath, payload) {
|
|
76995
76998
|
return withLockOperationMutex(lockPath, () => tryAcquireLockRaw(lockPath, payload));
|
|
76996
76999
|
}
|
|
77000
|
+
function launcherPidFromEnv() {
|
|
77001
|
+
const raw = process.env.AKM_LAUNCHER_PID;
|
|
77002
|
+
if (!raw)
|
|
77003
|
+
return;
|
|
77004
|
+
const pid = Number.parseInt(raw, 10);
|
|
77005
|
+
return Number.isInteger(pid) && pid > 0 ? pid : undefined;
|
|
77006
|
+
}
|
|
76997
77007
|
function createLockPayload(metadata = {}) {
|
|
76998
|
-
|
|
77008
|
+
const launcherPid = launcherPidFromEnv();
|
|
77009
|
+
return JSON.stringify({
|
|
77010
|
+
...metadata,
|
|
77011
|
+
pid: process.pid,
|
|
77012
|
+
...launcherPid !== undefined ? { launcherPid } : {},
|
|
77013
|
+
lockId: randomUUID()
|
|
77014
|
+
});
|
|
76999
77015
|
}
|
|
77000
77016
|
function probeLock(lockPath, opts) {
|
|
77001
77017
|
let snapshot;
|
|
@@ -77011,17 +77027,17 @@ function probeLock(lockPath, opts) {
|
|
|
77011
77027
|
return { state: "absent" };
|
|
77012
77028
|
const { rawContent, identity: identity3 } = snapshot;
|
|
77013
77029
|
const ageMs = Date.now() - identity3.mtimeMs;
|
|
77014
|
-
const holderPid =
|
|
77030
|
+
const { holderPid, launcherPid } = extractLockIdentity(rawContent);
|
|
77015
77031
|
if (holderPid === undefined) {
|
|
77016
77032
|
return { state: "stale", reason: "invalid_pid", ageMs, rawContent, identity: identity3 };
|
|
77017
77033
|
}
|
|
77018
77034
|
if (!isProcessAlive(holderPid)) {
|
|
77019
|
-
return { state: "stale", reason: "pid_dead", holderPid, ageMs, rawContent, identity: identity3 };
|
|
77035
|
+
return { state: "stale", reason: "pid_dead", holderPid, launcherPid, ageMs, rawContent, identity: identity3 };
|
|
77020
77036
|
}
|
|
77021
77037
|
if (opts?.staleAfterMs !== undefined && ageMs > opts.staleAfterMs) {
|
|
77022
|
-
return { state: "stale", reason: "age_exceeded", holderPid, ageMs, rawContent, identity: identity3 };
|
|
77038
|
+
return { state: "stale", reason: "age_exceeded", holderPid, launcherPid, ageMs, rawContent, identity: identity3 };
|
|
77023
77039
|
}
|
|
77024
|
-
return { state: "held", holderPid, ageMs, rawContent, identity: identity3 };
|
|
77040
|
+
return { state: "held", holderPid, launcherPid, ageMs, rawContent, identity: identity3 };
|
|
77025
77041
|
}
|
|
77026
77042
|
function reclaimStaleLock(lockPath, probe, options) {
|
|
77027
77043
|
if (probe.rawContent === undefined || probe.identity === undefined)
|
|
@@ -77089,21 +77105,25 @@ function releaseLock(ownership) {
|
|
|
77089
77105
|
}
|
|
77090
77106
|
});
|
|
77091
77107
|
}
|
|
77092
|
-
function
|
|
77108
|
+
function extractLockIdentity(content) {
|
|
77093
77109
|
const trimmed = content.trim();
|
|
77094
77110
|
if (!trimmed)
|
|
77095
|
-
return;
|
|
77111
|
+
return {};
|
|
77096
77112
|
if (trimmed.startsWith("{")) {
|
|
77097
77113
|
try {
|
|
77098
77114
|
const parsed = JSON.parse(trimmed);
|
|
77099
77115
|
const pid2 = typeof parsed.pid === "number" ? parsed.pid : Number.NaN;
|
|
77100
|
-
|
|
77116
|
+
const rawLauncherPid = typeof parsed.launcherPid === "number" ? parsed.launcherPid : Number.NaN;
|
|
77117
|
+
return {
|
|
77118
|
+
holderPid: Number.isInteger(pid2) && pid2 > 0 ? pid2 : undefined,
|
|
77119
|
+
launcherPid: Number.isInteger(rawLauncherPid) && rawLauncherPid > 0 ? rawLauncherPid : undefined
|
|
77120
|
+
};
|
|
77101
77121
|
} catch {
|
|
77102
|
-
return;
|
|
77122
|
+
return {};
|
|
77103
77123
|
}
|
|
77104
77124
|
}
|
|
77105
77125
|
const pid = Number.parseInt(trimmed, 10);
|
|
77106
|
-
return Number.isInteger(pid) && pid > 0 ? pid : undefined;
|
|
77126
|
+
return { holderPid: Number.isInteger(pid) && pid > 0 ? pid : undefined };
|
|
77107
77127
|
}
|
|
77108
77128
|
|
|
77109
77129
|
// src/core/config/config-io.ts
|
|
@@ -95737,7 +95757,7 @@ function listTxnJournalsTolerant(predicate) {
|
|
|
95737
95757
|
}
|
|
95738
95758
|
|
|
95739
95759
|
// src/commands/proposal/repository.ts
|
|
95740
|
-
import { createHash as
|
|
95760
|
+
import { createHash as createHash11, randomUUID as randomUUID6 } from "node:crypto";
|
|
95741
95761
|
import fs55 from "node:fs";
|
|
95742
95762
|
init_dist();
|
|
95743
95763
|
import path66 from "node:path";
|
|
@@ -95803,6 +95823,16 @@ import fs53 from "node:fs";
|
|
|
95803
95823
|
import path64 from "node:path";
|
|
95804
95824
|
init_errors();
|
|
95805
95825
|
init_paths();
|
|
95826
|
+
|
|
95827
|
+
// src/core/run-lock.ts
|
|
95828
|
+
init_errors();
|
|
95829
|
+
function formatLockHolderPid(holder) {
|
|
95830
|
+
if (holder.pid === null)
|
|
95831
|
+
return "unknown";
|
|
95832
|
+
return holder.launcherPid !== null ? `${holder.pid} (launcher ${holder.launcherPid})` : String(holder.pid);
|
|
95833
|
+
}
|
|
95834
|
+
|
|
95835
|
+
// src/indexer/index-written-assets.ts
|
|
95806
95836
|
init_warn();
|
|
95807
95837
|
|
|
95808
95838
|
// src/storage/repositories/index-connection.ts
|
|
@@ -96057,6 +96087,15 @@ function isCanonicalIndexGeneration(db) {
|
|
|
96057
96087
|
init_errors();
|
|
96058
96088
|
init_warn();
|
|
96059
96089
|
|
|
96090
|
+
// src/storage/repositories/embedding-salvage-repository.ts
|
|
96091
|
+
import { createHash as createHash9 } from "node:crypto";
|
|
96092
|
+
|
|
96093
|
+
// src/storage/repositories/embeddings-repository.ts
|
|
96094
|
+
function blobToEmbedding(blob) {
|
|
96095
|
+
const f32 = new Float32Array(blob.buffer, blob.byteOffset, blob.byteLength / 4);
|
|
96096
|
+
return Array.from(f32);
|
|
96097
|
+
}
|
|
96098
|
+
|
|
96060
96099
|
// src/storage/repositories/index-meta-repository.ts
|
|
96061
96100
|
function getMeta(db, key) {
|
|
96062
96101
|
const row = db.prepare("SELECT value FROM index_meta WHERE key = ?").get(key);
|
|
@@ -96069,6 +96108,64 @@ function deleteMeta(db, key) {
|
|
|
96069
96108
|
db.prepare("DELETE FROM index_meta WHERE key = ?").run(key);
|
|
96070
96109
|
}
|
|
96071
96110
|
|
|
96111
|
+
// src/storage/repositories/index-sql.ts
|
|
96112
|
+
var SQLITE_CHUNK_SIZE = 500;
|
|
96113
|
+
|
|
96114
|
+
// src/storage/repositories/embedding-salvage-repository.ts
|
|
96115
|
+
function hashEmbeddableText(searchText) {
|
|
96116
|
+
return createHash9("sha256").update(searchText, "utf8").digest("hex");
|
|
96117
|
+
}
|
|
96118
|
+
function purgeEmbeddingSalvage(db) {
|
|
96119
|
+
db.exec("DELETE FROM embedding_salvage");
|
|
96120
|
+
}
|
|
96121
|
+
function relabelEmbeddingSalvageFingerprint(db, fromFingerprint, toFingerprint) {
|
|
96122
|
+
db.prepare("UPDATE embedding_salvage SET fingerprint = ? WHERE fingerprint = ?").run(toFingerprint, fromFingerprint);
|
|
96123
|
+
}
|
|
96124
|
+
function reuseSalvagedEmbeddings(db, entries, fingerprint, writeReused) {
|
|
96125
|
+
if (entries.length === 0)
|
|
96126
|
+
return { reusedCount: 0, remaining: [] };
|
|
96127
|
+
const anySalvageForFingerprint = db.prepare("SELECT 1 FROM embedding_salvage WHERE fingerprint = ? LIMIT 1").get(fingerprint);
|
|
96128
|
+
if (!anySalvageForFingerprint)
|
|
96129
|
+
return { reusedCount: 0, remaining: [...entries] };
|
|
96130
|
+
const hashes = entries.map((entry) => hashEmbeddableText(entry.searchText));
|
|
96131
|
+
const salvageByHash = new Map;
|
|
96132
|
+
const uniqueHashes = [...new Set(hashes)];
|
|
96133
|
+
for (let offset = 0;offset < uniqueHashes.length; offset += SQLITE_CHUNK_SIZE) {
|
|
96134
|
+
const chunk = uniqueHashes.slice(offset, offset + SQLITE_CHUNK_SIZE);
|
|
96135
|
+
const placeholders = chunk.map(() => "?").join(",");
|
|
96136
|
+
const rows = db.prepare(`SELECT content_hash AS contentHash, embedding FROM embedding_salvage WHERE fingerprint = ? AND content_hash IN (${placeholders})`).all(fingerprint, ...chunk);
|
|
96137
|
+
for (const row of rows)
|
|
96138
|
+
salvageByHash.set(row.contentHash, row.embedding);
|
|
96139
|
+
}
|
|
96140
|
+
if (salvageByHash.size === 0)
|
|
96141
|
+
return { reusedCount: 0, remaining: [...entries] };
|
|
96142
|
+
let reusedCount = 0;
|
|
96143
|
+
const remaining = [];
|
|
96144
|
+
for (let offset = 0;offset < entries.length; offset += SQLITE_CHUNK_SIZE) {
|
|
96145
|
+
const end = Math.min(offset + SQLITE_CHUNK_SIZE, entries.length);
|
|
96146
|
+
const chunkMatches = [];
|
|
96147
|
+
for (let i = offset;i < end; i++) {
|
|
96148
|
+
const entry = entries[i];
|
|
96149
|
+
const blob = salvageByHash.get(hashes[i]);
|
|
96150
|
+
if (blob)
|
|
96151
|
+
chunkMatches.push({ entry, blob });
|
|
96152
|
+
else
|
|
96153
|
+
remaining.push(entry);
|
|
96154
|
+
}
|
|
96155
|
+
if (chunkMatches.length === 0)
|
|
96156
|
+
continue;
|
|
96157
|
+
db.transaction(() => {
|
|
96158
|
+
for (const { entry, blob } of chunkMatches) {
|
|
96159
|
+
if (writeReused(entry, blobToEmbedding(blob)))
|
|
96160
|
+
reusedCount++;
|
|
96161
|
+
else
|
|
96162
|
+
remaining.push(entry);
|
|
96163
|
+
}
|
|
96164
|
+
})();
|
|
96165
|
+
}
|
|
96166
|
+
return { reusedCount, remaining };
|
|
96167
|
+
}
|
|
96168
|
+
|
|
96072
96169
|
// src/storage/repositories/index-vec-repository.ts
|
|
96073
96170
|
import { createRequire as createRequire4 } from "node:module";
|
|
96074
96171
|
|
|
@@ -96110,9 +96207,6 @@ function cosineSimilarity(a, b) {
|
|
|
96110
96207
|
return denom === 0 ? 0 : dot / denom;
|
|
96111
96208
|
}
|
|
96112
96209
|
|
|
96113
|
-
// src/storage/repositories/index-sql.ts
|
|
96114
|
-
var SQLITE_CHUNK_SIZE = 500;
|
|
96115
|
-
|
|
96116
96210
|
// src/storage/repositories/index-vec-repository.ts
|
|
96117
96211
|
var vecStatus = new WeakMap;
|
|
96118
96212
|
function loadVecExtension(db) {
|
|
@@ -96372,11 +96466,11 @@ init_warn();
|
|
|
96372
96466
|
|
|
96373
96467
|
// src/core/asset/markdown-fragments.ts
|
|
96374
96468
|
init_markdown();
|
|
96375
|
-
import { createHash as
|
|
96469
|
+
import { createHash as createHash10 } from "node:crypto";
|
|
96376
96470
|
var MARKDOWN_FRAGMENT_MAX_CHARS = 1600;
|
|
96377
96471
|
var MARKDOWN_FRAGMENT_PREFIX = "akm-fragment-";
|
|
96378
96472
|
function hash3(text) {
|
|
96379
|
-
return
|
|
96473
|
+
return createHash10("sha256").update(text).digest("hex");
|
|
96380
96474
|
}
|
|
96381
96475
|
function uniqueSlugs(body) {
|
|
96382
96476
|
const out = new Map;
|
|
@@ -96657,6 +96751,7 @@ function getEmbeddableEntryCount(db) {
|
|
|
96657
96751
|
}
|
|
96658
96752
|
|
|
96659
96753
|
// src/indexer/materialize-embeddings.ts
|
|
96754
|
+
init_paths();
|
|
96660
96755
|
init_warn();
|
|
96661
96756
|
|
|
96662
96757
|
// src/llm/embedders/cache.ts
|
|
@@ -96892,10 +96987,43 @@ function defaultConcurrencyForEndpoint(endpoint) {
|
|
|
96892
96987
|
// src/llm/embedders/remote.ts
|
|
96893
96988
|
init_warn();
|
|
96894
96989
|
var DEFAULT_REMOTE_BATCH_SIZE = 100;
|
|
96895
|
-
var DEFAULT_TOKEN_BUDGET =
|
|
96990
|
+
var DEFAULT_TOKEN_BUDGET = 6000;
|
|
96896
96991
|
function estimateTokenCount(text) {
|
|
96897
96992
|
return Math.round(text.length / 4);
|
|
96898
96993
|
}
|
|
96994
|
+
var DEFAULT_MAX_INPUT_TOKENS = 512;
|
|
96995
|
+
function capEmbeddingText(text, maxTokens) {
|
|
96996
|
+
if (estimateTokenCount(text) <= maxTokens)
|
|
96997
|
+
return { text, truncated: false };
|
|
96998
|
+
const charBudget = Math.max(0, maxTokens * 4);
|
|
96999
|
+
let cut = Math.min(charBudget, text.length);
|
|
97000
|
+
if (cut > 0 && cut < text.length) {
|
|
97001
|
+
const code = text.charCodeAt(cut);
|
|
97002
|
+
if (code >= 56320 && code <= 57343)
|
|
97003
|
+
cut -= 1;
|
|
97004
|
+
}
|
|
97005
|
+
return { text: text.slice(0, cut), truncated: true };
|
|
97006
|
+
}
|
|
97007
|
+
var DEFAULT_EMBEDDING_TIMEOUT_MS = 120000;
|
|
97008
|
+
function resolveEmbeddingTimeoutMs(config) {
|
|
97009
|
+
return config.timeoutMs ?? DEFAULT_EMBEDDING_TIMEOUT_MS;
|
|
97010
|
+
}
|
|
97011
|
+
function scaleEmbeddingTimeoutMs(timeoutMs, requestTokens, tokenBudget) {
|
|
97012
|
+
const scaled = tokenBudget > 0 ? timeoutMs * (requestTokens / tokenBudget) : timeoutMs;
|
|
97013
|
+
return Math.min(Math.max(scaled, 30000), timeoutMs);
|
|
97014
|
+
}
|
|
97015
|
+
function isEmbeddingTimeoutError(err) {
|
|
97016
|
+
if (!(err instanceof Error))
|
|
97017
|
+
return false;
|
|
97018
|
+
if (err.name === "BodyReadTimeoutError")
|
|
97019
|
+
return true;
|
|
97020
|
+
return err.message.startsWith("Request timed out after ");
|
|
97021
|
+
}
|
|
97022
|
+
var embeddingTimeoutBackoffOverrideForTests;
|
|
97023
|
+
function embeddingTimeoutRetryBackoffMs(timeoutAttempt = 0) {
|
|
97024
|
+
const { baseMs, maxMs } = embeddingTimeoutBackoffOverrideForTests ?? { baseMs: 5000, maxMs: 60000 };
|
|
97025
|
+
return backoffDelay(timeoutAttempt, baseMs, maxMs);
|
|
97026
|
+
}
|
|
96899
97027
|
|
|
96900
97028
|
class ContextExceededError extends Error {
|
|
96901
97029
|
constructor(message) {
|
|
@@ -96903,13 +97031,15 @@ class ContextExceededError extends Error {
|
|
|
96903
97031
|
this.name = "ContextExceededError";
|
|
96904
97032
|
}
|
|
96905
97033
|
}
|
|
96906
|
-
var CONTEXT_EXCEEDED_PATTERN = /exceed_context_size_error|context size|context length|too many tokens/i;
|
|
97034
|
+
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;
|
|
96907
97035
|
function isContextExceededResponse(status, body) {
|
|
96908
97036
|
if (status === 413)
|
|
96909
97037
|
return true;
|
|
96910
97038
|
return CONTEXT_EXCEEDED_PATTERN.test(body);
|
|
96911
97039
|
}
|
|
96912
97040
|
function resolveEmbeddingConcurrency(config) {
|
|
97041
|
+
if (typeof config.concurrency === "number")
|
|
97042
|
+
return config.concurrency;
|
|
96913
97043
|
return defaultConcurrencyForEndpoint(config.endpoint);
|
|
96914
97044
|
}
|
|
96915
97045
|
function buildTokenBoundedBatches(texts, tokenBudget, maxCount) {
|
|
@@ -96939,6 +97069,8 @@ function buildTokenBoundedBatches(texts, tokenBudget, maxCount) {
|
|
|
96939
97069
|
flush();
|
|
96940
97070
|
return batches;
|
|
96941
97071
|
}
|
|
97072
|
+
var ADAPTIVE_BUDGET_SHRINK_FACTOR = 0.75;
|
|
97073
|
+
var ADAPTIVE_BUDGET_FLOOR_MULTIPLIER = 2;
|
|
96942
97074
|
|
|
96943
97075
|
class RemoteEmbedder {
|
|
96944
97076
|
config;
|
|
@@ -96965,20 +97097,21 @@ class RemoteEmbedder {
|
|
|
96965
97097
|
if (ollamaOpts) {
|
|
96966
97098
|
body.options = ollamaOpts;
|
|
96967
97099
|
}
|
|
97100
|
+
const timeoutMs = resolveEmbeddingTimeoutMs(this.config);
|
|
96968
97101
|
const response = await fetchWithTimeout(normalizeEmbeddingEndpoint(this.endpoint), {
|
|
96969
97102
|
method: "POST",
|
|
96970
97103
|
headers,
|
|
96971
97104
|
body: JSON.stringify(body)
|
|
96972
|
-
},
|
|
97105
|
+
}, timeoutMs, signal);
|
|
96973
97106
|
if (!response.ok) {
|
|
96974
|
-
const errBody = await readBodyWithByteCap(response, undefined, { bodyTimeoutMs:
|
|
97107
|
+
const errBody = await readBodyWithByteCap(response, undefined, { bodyTimeoutMs: timeoutMs, signal }).catch((err) => {
|
|
96975
97108
|
if (signal?.aborted)
|
|
96976
97109
|
throw err;
|
|
96977
97110
|
return "";
|
|
96978
97111
|
});
|
|
96979
97112
|
throw new Error(`Embedding request failed (${response.status}): ${this.safeErrorBody(errBody)}`);
|
|
96980
97113
|
}
|
|
96981
|
-
const json = JSON.parse(await readBodyWithByteCap(response, undefined, { bodyTimeoutMs:
|
|
97114
|
+
const json = JSON.parse(await readBodyWithByteCap(response, undefined, { bodyTimeoutMs: timeoutMs, signal }));
|
|
96982
97115
|
if (!json.data?.[0]?.embedding) {
|
|
96983
97116
|
throw new Error(`Unexpected embedding response format: missing data[0].embedding.${embeddingEndpointPathHint(this.endpoint)}`);
|
|
96984
97117
|
}
|
|
@@ -96990,9 +97123,42 @@ class RemoteEmbedder {
|
|
|
96990
97123
|
const results = new Array(texts.length).fill(undefined);
|
|
96991
97124
|
const headers = this.buildHeaders();
|
|
96992
97125
|
const ollamaOpts = resolveOllamaOptions(this.config);
|
|
96993
|
-
|
|
97126
|
+
let effectiveTokenBudget = this.config.maxTokens ?? DEFAULT_TOKEN_BUDGET;
|
|
96994
97127
|
const maxCount = this.config.batchSize ?? DEFAULT_REMOTE_BATCH_SIZE;
|
|
96995
|
-
const
|
|
97128
|
+
const maxInputTokens = this.config.maxInputTokens ?? DEFAULT_MAX_INPUT_TOKENS;
|
|
97129
|
+
const textBatches = buildTokenBoundedBatches(texts, effectiveTokenBudget, maxCount);
|
|
97130
|
+
const configuredTimeoutMs = resolveEmbeddingTimeoutMs(this.config);
|
|
97131
|
+
let dispatchedBatchCount = 0;
|
|
97132
|
+
let budgetShrunk = false;
|
|
97133
|
+
const maybeShrinkBudget = (rejectedIndices, rejectedBatchIndex, rejectedRequestTokens) => {
|
|
97134
|
+
if (budgetShrunk)
|
|
97135
|
+
return;
|
|
97136
|
+
budgetShrunk = true;
|
|
97137
|
+
const floor2 = ADAPTIVE_BUDGET_FLOOR_MULTIPLIER * maxInputTokens;
|
|
97138
|
+
effectiveTokenBudget = Math.max(Math.round(effectiveTokenBudget * ADAPTIVE_BUDGET_SHRINK_FACTOR), floor2);
|
|
97139
|
+
const notYetDispatched = textBatches.slice(dispatchedBatchCount);
|
|
97140
|
+
const remainingIndices = notYetDispatched.flatMap((batch) => batch.indices);
|
|
97141
|
+
if (remainingIndices.length > 0) {
|
|
97142
|
+
const remainingTexts = remainingIndices.map((i) => texts[i]);
|
|
97143
|
+
const replanned = buildTokenBoundedBatches(remainingTexts, effectiveTokenBudget, maxCount).map((batch) => ({
|
|
97144
|
+
indices: batch.indices.map((localIndex) => remainingIndices[localIndex]),
|
|
97145
|
+
oversized: batch.oversized
|
|
97146
|
+
}));
|
|
97147
|
+
textBatches.splice(dispatchedBatchCount, textBatches.length - dispatchedBatchCount, ...replanned);
|
|
97148
|
+
}
|
|
97149
|
+
warnVerbose(`[embed] provider rejected a ${rejectedRequestTokens}-token request as over its context; request budget lowered to ${effectiveTokenBudget.toLocaleString()} for the rest of this run`);
|
|
97150
|
+
commitBatch(rejectedIndices, rejectedIndices.map(() => {
|
|
97151
|
+
return;
|
|
97152
|
+
}), undefined, {
|
|
97153
|
+
batchIndex: rejectedBatchIndex,
|
|
97154
|
+
batchCount: textBatches.length,
|
|
97155
|
+
docCount: rejectedIndices.length,
|
|
97156
|
+
requestTokens: rejectedRequestTokens,
|
|
97157
|
+
elapsedMs: 0,
|
|
97158
|
+
outcome: "budget-lowered",
|
|
97159
|
+
reason: `provider rejected ${rejectedRequestTokens.toLocaleString()} tokens as over its context; request budget lowered to ${effectiveTokenBudget.toLocaleString()} for the rest of this run`
|
|
97160
|
+
});
|
|
97161
|
+
};
|
|
96996
97162
|
const dispatchAbort = new AbortController;
|
|
96997
97163
|
const stopDispatch = (reason) => {
|
|
96998
97164
|
if (!dispatchAbort.signal.aborted)
|
|
@@ -97008,66 +97174,136 @@ class RemoteEmbedder {
|
|
|
97008
97174
|
}
|
|
97009
97175
|
}
|
|
97010
97176
|
let firstOnBatchError;
|
|
97011
|
-
const commitBatch = (indices, embeddings, model) => {
|
|
97177
|
+
const commitBatch = (indices, embeddings, model, outcome) => {
|
|
97012
97178
|
if (!onBatch)
|
|
97013
97179
|
return;
|
|
97014
97180
|
if (firstOnBatchError !== undefined)
|
|
97015
97181
|
return;
|
|
97016
97182
|
try {
|
|
97017
|
-
onBatch(indices, embeddings, model);
|
|
97183
|
+
onBatch(indices, embeddings, model, outcome);
|
|
97018
97184
|
} catch (err) {
|
|
97019
97185
|
firstOnBatchError = err;
|
|
97020
97186
|
stopDispatch(err);
|
|
97021
97187
|
}
|
|
97022
97188
|
};
|
|
97023
|
-
const requestAndCommit = async (indices) => {
|
|
97189
|
+
const requestAndCommit = async (indices, batchIndex, isTimeoutRetry = false, timeoutAttempt = 0) => {
|
|
97190
|
+
if (dispatchAbort.signal.aborted)
|
|
97191
|
+
return;
|
|
97024
97192
|
const batch = indices.map((i) => texts[i]);
|
|
97193
|
+
const requestTokens = batch.reduce((sum, text) => sum + estimateTokenCount(text), 0);
|
|
97194
|
+
const requestTimeoutMs = scaleEmbeddingTimeoutMs(configuredTimeoutMs, requestTokens, effectiveTokenBudget);
|
|
97195
|
+
const requestStart = Date.now();
|
|
97025
97196
|
let batchEmbeddings;
|
|
97026
97197
|
let responseModel;
|
|
97198
|
+
let outcome;
|
|
97199
|
+
let failureReason;
|
|
97027
97200
|
try {
|
|
97028
|
-
const { vectors, model } = await this.requestBatch(batch, headers, ollamaOpts, signal);
|
|
97201
|
+
const { vectors, model } = await this.requestBatch(batch, headers, ollamaOpts, requestTimeoutMs, signal);
|
|
97029
97202
|
for (let k2 = 0;k2 < indices.length; k2++) {
|
|
97030
97203
|
results[indices[k2]] = vectors[k2];
|
|
97031
97204
|
}
|
|
97032
97205
|
batchEmbeddings = indices.map((i) => results[i]);
|
|
97033
97206
|
responseModel = model;
|
|
97207
|
+
outcome = "stored";
|
|
97034
97208
|
} catch (err) {
|
|
97035
97209
|
if (signal?.aborted)
|
|
97036
97210
|
throw err;
|
|
97211
|
+
if (err instanceof ContextExceededError) {
|
|
97212
|
+
maybeShrinkBudget(indices, batchIndex, requestTokens);
|
|
97213
|
+
}
|
|
97037
97214
|
if (err instanceof ContextExceededError && indices.length > 1) {
|
|
97038
97215
|
const mid = Math.ceil(indices.length / 2);
|
|
97039
|
-
await requestAndCommit(indices.slice(0, mid));
|
|
97040
|
-
await requestAndCommit(indices.slice(mid));
|
|
97216
|
+
await requestAndCommit(indices.slice(0, mid), batchIndex, false, timeoutAttempt);
|
|
97217
|
+
await requestAndCommit(indices.slice(mid), batchIndex, false, timeoutAttempt);
|
|
97218
|
+
return;
|
|
97219
|
+
}
|
|
97220
|
+
const timedOut = isEmbeddingTimeoutError(err);
|
|
97221
|
+
if (timedOut && !isTimeoutRetry) {
|
|
97222
|
+
const backoffMs = embeddingTimeoutRetryBackoffMs(timeoutAttempt);
|
|
97223
|
+
warnVerbose(`[embed] batch of ${batch.length} document(s) timed out after ${requestTimeoutMs}ms; retrying once after a ${Math.round(backoffMs)}ms backoff`);
|
|
97224
|
+
commitBatch(indices, indices.map(() => {
|
|
97225
|
+
return;
|
|
97226
|
+
}), undefined, {
|
|
97227
|
+
batchIndex,
|
|
97228
|
+
batchCount: textBatches.length,
|
|
97229
|
+
docCount: indices.length,
|
|
97230
|
+
requestTokens,
|
|
97231
|
+
elapsedMs: backoffMs,
|
|
97232
|
+
outcome: "retrying",
|
|
97233
|
+
reason: "timed out"
|
|
97234
|
+
});
|
|
97235
|
+
await abortableDelay(backoffMs, signal, "embedding interrupted during retry backoff");
|
|
97236
|
+
if (!dispatchAbort.signal.aborted) {
|
|
97237
|
+
return requestAndCommit(indices, batchIndex, true, timeoutAttempt);
|
|
97238
|
+
}
|
|
97239
|
+
} else if (timedOut && indices.length > 1) {
|
|
97240
|
+
const mid = Math.ceil(indices.length / 2);
|
|
97241
|
+
await requestAndCommit(indices.slice(0, mid), batchIndex, false, timeoutAttempt + 1);
|
|
97242
|
+
await requestAndCommit(indices.slice(mid), batchIndex, false, timeoutAttempt + 1);
|
|
97041
97243
|
return;
|
|
97042
97244
|
}
|
|
97043
97245
|
const message = err instanceof Error ? err.message : String(err);
|
|
97044
97246
|
const reason = err instanceof ContextExceededError ? "context-window-exceeded" : "batch-request-failed";
|
|
97045
|
-
|
|
97046
|
-
|
|
97047
|
-
|
|
97048
|
-
|
|
97247
|
+
const failureKind = reason === "batch-request-failed" ? timedOut ? "timeout" : "network-error" : undefined;
|
|
97248
|
+
let stopRequested = false;
|
|
97249
|
+
for (const [k2, idx] of indices.entries()) {
|
|
97250
|
+
if (onSkip?.({
|
|
97251
|
+
index: idx,
|
|
97252
|
+
reason,
|
|
97253
|
+
message,
|
|
97254
|
+
batchStart: k2 === 0,
|
|
97255
|
+
batchSize: indices.length,
|
|
97256
|
+
failureKind
|
|
97257
|
+
}) === false)
|
|
97258
|
+
stopRequested = true;
|
|
97259
|
+
}
|
|
97260
|
+
if (stopRequested)
|
|
97261
|
+
stopDispatch();
|
|
97049
97262
|
batchEmbeddings = indices.map(() => {
|
|
97050
97263
|
return;
|
|
97051
97264
|
});
|
|
97052
|
-
|
|
97053
|
-
|
|
97265
|
+
outcome = "failed";
|
|
97266
|
+
failureReason = message;
|
|
97267
|
+
}
|
|
97268
|
+
commitBatch(indices, batchEmbeddings, responseModel, {
|
|
97269
|
+
batchIndex,
|
|
97270
|
+
batchCount: textBatches.length,
|
|
97271
|
+
docCount: indices.length,
|
|
97272
|
+
requestTokens,
|
|
97273
|
+
elapsedMs: Date.now() - requestStart,
|
|
97274
|
+
outcome,
|
|
97275
|
+
reason: failureReason
|
|
97276
|
+
});
|
|
97054
97277
|
};
|
|
97055
|
-
const runProviderBatch = async (textBatch) => {
|
|
97278
|
+
const runProviderBatch = async (textBatch, batchIndex) => {
|
|
97279
|
+
dispatchedBatchCount = batchIndex;
|
|
97056
97280
|
if (textBatch.oversized) {
|
|
97057
97281
|
const idx = textBatch.indices[0];
|
|
97058
97282
|
const estTokens = estimateTokenCount(texts[idx]);
|
|
97059
97283
|
onSkip?.({
|
|
97060
97284
|
index: idx,
|
|
97061
97285
|
reason: "context-window-exceeded",
|
|
97062
|
-
message: `Document estimated at ${estTokens} tokens exceeds the ${
|
|
97286
|
+
message: `Document estimated at ${estTokens} tokens exceeds the ${effectiveTokenBudget}-token embedding budget; skipped.`,
|
|
97287
|
+
batchStart: true,
|
|
97288
|
+
batchSize: 1
|
|
97289
|
+
});
|
|
97290
|
+
commitBatch([idx], [undefined], undefined, {
|
|
97291
|
+
batchIndex,
|
|
97292
|
+
batchCount: textBatches.length,
|
|
97293
|
+
docCount: 1,
|
|
97294
|
+
requestTokens: estTokens,
|
|
97295
|
+
elapsedMs: 0,
|
|
97296
|
+
outcome: "failed",
|
|
97297
|
+
reason: "oversized"
|
|
97063
97298
|
});
|
|
97064
|
-
commitBatch([idx], [undefined]);
|
|
97065
97299
|
return;
|
|
97066
97300
|
}
|
|
97067
|
-
await requestAndCommit(textBatch.indices);
|
|
97301
|
+
await requestAndCommit(textBatch.indices, batchIndex);
|
|
97068
97302
|
};
|
|
97069
97303
|
const concurrency = resolveEmbeddingConcurrency(this.config);
|
|
97070
|
-
await concurrentMap(textBatches, runProviderBatch, concurrency, {
|
|
97304
|
+
await concurrentMap(textBatches, (textBatch, i) => runProviderBatch(textBatch, i + 1), concurrency, {
|
|
97305
|
+
signal: dispatchAbort.signal
|
|
97306
|
+
});
|
|
97071
97307
|
if (callerAbortListener)
|
|
97072
97308
|
signal?.removeEventListener("abort", callerAbortListener);
|
|
97073
97309
|
if (signal?.aborted) {
|
|
@@ -97078,7 +97314,7 @@ class RemoteEmbedder {
|
|
|
97078
97314
|
}
|
|
97079
97315
|
return results;
|
|
97080
97316
|
}
|
|
97081
|
-
async requestBatch(batch, headers, ollamaOpts, signal) {
|
|
97317
|
+
async requestBatch(batch, headers, ollamaOpts, timeoutMs, signal) {
|
|
97082
97318
|
const body = {
|
|
97083
97319
|
input: batch,
|
|
97084
97320
|
model: this.model
|
|
@@ -97093,9 +97329,9 @@ class RemoteEmbedder {
|
|
|
97093
97329
|
method: "POST",
|
|
97094
97330
|
headers,
|
|
97095
97331
|
body: JSON.stringify(body)
|
|
97096
|
-
},
|
|
97332
|
+
}, timeoutMs, signal);
|
|
97097
97333
|
if (!response.ok) {
|
|
97098
|
-
const respBody = await readBodyWithByteCap(response, undefined, { bodyTimeoutMs:
|
|
97334
|
+
const respBody = await readBodyWithByteCap(response, undefined, { bodyTimeoutMs: timeoutMs, signal }).catch((err) => {
|
|
97099
97335
|
if (signal?.aborted)
|
|
97100
97336
|
throw err;
|
|
97101
97337
|
return "";
|
|
@@ -97106,7 +97342,7 @@ class RemoteEmbedder {
|
|
|
97106
97342
|
}
|
|
97107
97343
|
throw new Error(message);
|
|
97108
97344
|
}
|
|
97109
|
-
const json = JSON.parse(await readBodyWithByteCap(response, undefined, { bodyTimeoutMs:
|
|
97345
|
+
const json = JSON.parse(await readBodyWithByteCap(response, undefined, { bodyTimeoutMs: timeoutMs, signal }));
|
|
97110
97346
|
if (!json.data || json.data.length !== batch.length) {
|
|
97111
97347
|
throw new Error(`Unexpected embedding batch response: expected ${batch.length} embeddings, got ${json.data?.length ?? 0}.${embeddingEndpointPathHint(this.endpoint)}`);
|
|
97112
97348
|
}
|
|
@@ -97175,6 +97411,15 @@ function resolveOllamaOptions(config) {
|
|
|
97175
97411
|
function hasRemoteEndpoint(config) {
|
|
97176
97412
|
return isHttpUrl(config.endpoint);
|
|
97177
97413
|
}
|
|
97414
|
+
function describeEmbeddingCredential(apiKey) {
|
|
97415
|
+
if (!apiKey)
|
|
97416
|
+
return "none configured";
|
|
97417
|
+
if (SECRET_STORE_REFERENCE_PATTERN.test(apiKey))
|
|
97418
|
+
return `${apiKey} (store)`;
|
|
97419
|
+
if (ENV_REFERENCE_PATTERN.test(apiKey))
|
|
97420
|
+
return `${apiKey} (env)`;
|
|
97421
|
+
return "literal apiKey";
|
|
97422
|
+
}
|
|
97178
97423
|
|
|
97179
97424
|
// src/llm/embedder.ts
|
|
97180
97425
|
var embedderOverrides;
|
|
@@ -97225,9 +97470,12 @@ function deriveSemanticProviderFingerprint(embedding) {
|
|
|
97225
97470
|
}
|
|
97226
97471
|
return `local:${embedding?.localModel ?? DEFAULT_LOCAL_MODEL}`;
|
|
97227
97472
|
}
|
|
97228
|
-
|
|
97473
|
+
function formatEmbeddingHeartbeat(storedCount, total, failedCount) {
|
|
97474
|
+
return `Still generating embeddings: ${storedCount}/${total} stored, ${failedCount} failed; waiting on embedding provider.`;
|
|
97475
|
+
}
|
|
97229
97476
|
var CANARY_SAMPLE_SIZE = 8;
|
|
97230
97477
|
var CANARY_SIMILARITY_THRESHOLD = 0.999;
|
|
97478
|
+
var CIRCUIT_BREAKER_THRESHOLD = 3;
|
|
97231
97479
|
function decideEmbeddingCompatibility(pairs) {
|
|
97232
97480
|
if (pairs.length === 0)
|
|
97233
97481
|
return { outcome: "keep", medianSimilarity: undefined, verifiedSamples: 0 };
|
|
@@ -97259,7 +97507,7 @@ function deriveObservedEmbeddingIdentity(embedding, observedModel, observedVecto
|
|
|
97259
97507
|
}
|
|
97260
97508
|
return `local:${embedding?.localModel ?? DEFAULT_LOCAL_MODEL}|${observedVectorLen}`;
|
|
97261
97509
|
}
|
|
97262
|
-
async function runEmbeddingCanary(db, config, signal) {
|
|
97510
|
+
async function runEmbeddingCanary(db, config, signal, maxInputTokens) {
|
|
97263
97511
|
const samples = sampleEmbeddedEntriesForCanary(db, CANARY_SAMPLE_SIZE);
|
|
97264
97512
|
if (samples.length === 0) {
|
|
97265
97513
|
return { outcome: "keep", verified: false, viaIdentityMatch: false };
|
|
@@ -97268,7 +97516,7 @@ async function runEmbeddingCanary(db, config, signal) {
|
|
|
97268
97516
|
const skips = [];
|
|
97269
97517
|
let canaryVectors;
|
|
97270
97518
|
try {
|
|
97271
|
-
canaryVectors = await embedBatch(samples.map((sample) => sample.searchText), config.embedding, signal, (skip) => skips.push(skip), (_indices, _embeddings, model) => {
|
|
97519
|
+
canaryVectors = await embedBatch(samples.map((sample) => capEmbeddingText(sample.searchText, maxInputTokens).text), config.embedding, signal, (skip) => skips.push(skip), (_indices, _embeddings, model) => {
|
|
97272
97520
|
if (model)
|
|
97273
97521
|
observedModel = model;
|
|
97274
97522
|
});
|
|
@@ -97315,19 +97563,34 @@ function throwIfAborted(signal) {
|
|
|
97315
97563
|
}
|
|
97316
97564
|
}
|
|
97317
97565
|
async function generateEmbeddingsForDb(db, config, onProgress, signal, entryIds, opts) {
|
|
97566
|
+
if (db.inTransaction) {
|
|
97567
|
+
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.");
|
|
97568
|
+
}
|
|
97318
97569
|
throwIfAborted(signal);
|
|
97319
97570
|
if (config.semanticSearchMode === "off") {
|
|
97571
|
+
purgeEmbeddingSalvage(db);
|
|
97320
97572
|
onProgress({ phase: "embeddings", message: "Semantic search disabled; skipping embeddings." });
|
|
97321
97573
|
return { success: false, message: "Semantic search is disabled." };
|
|
97322
97574
|
}
|
|
97575
|
+
if (hasRemoteEndpoint(config.embedding ?? {})) {
|
|
97576
|
+
const endpoint = normalizeEmbeddingEndpoint(config.embedding?.endpoint ?? "");
|
|
97577
|
+
const credential = describeEmbeddingCredential(config.embedding?.apiKey);
|
|
97578
|
+
const configFileSuffix = isVerbose() ? `; config: ${getConfigPath()}` : "";
|
|
97579
|
+
onProgress({
|
|
97580
|
+
phase: "embeddings",
|
|
97581
|
+
message: `[embed] endpoint ${endpoint}, model ${config.embedding?.model ?? "unknown"}; credential: ${credential}${configFileSuffix}`
|
|
97582
|
+
});
|
|
97583
|
+
}
|
|
97323
97584
|
const vecFastPathWasReady = isVecFastPathReady(db);
|
|
97324
97585
|
const currentFingerprint = deriveSemanticProviderFingerprint(config.embedding);
|
|
97325
97586
|
const storedFingerprint = getMeta(db, "embeddingFingerprint");
|
|
97326
97587
|
let targetEntryIds = entryIds;
|
|
97327
97588
|
let rebuildReason;
|
|
97589
|
+
const maxInputTokens = config.embedding?.maxInputTokens ?? DEFAULT_MAX_INPUT_TOKENS;
|
|
97328
97590
|
if (opts?.forceReembed) {
|
|
97329
97591
|
db.transaction(() => {
|
|
97330
97592
|
purgeEmbeddings(db, { dropVecTable: true });
|
|
97593
|
+
purgeEmbeddingSalvage(db);
|
|
97331
97594
|
deleteMeta(db, "embeddingDim");
|
|
97332
97595
|
setMeta(db, "embeddingFingerprint", currentFingerprint);
|
|
97333
97596
|
deleteMeta(db, "embeddingIdentity");
|
|
@@ -97335,7 +97598,7 @@ async function generateEmbeddingsForDb(db, config, onProgress, signal, entryIds,
|
|
|
97335
97598
|
targetEntryIds = undefined;
|
|
97336
97599
|
rebuildReason = "forced by --reembed";
|
|
97337
97600
|
} else if (storedFingerprint && storedFingerprint !== currentFingerprint) {
|
|
97338
|
-
const decision = await runEmbeddingCanary(db, config, signal);
|
|
97601
|
+
const decision = await runEmbeddingCanary(db, config, signal, maxInputTokens);
|
|
97339
97602
|
if (decision.outcome === "unverifiable") {
|
|
97340
97603
|
warn(`[embed] ${decision.message}`);
|
|
97341
97604
|
onProgress({ phase: "embeddings", message: decision.message });
|
|
@@ -97344,6 +97607,7 @@ async function generateEmbeddingsForDb(db, config, onProgress, signal, entryIds,
|
|
|
97344
97607
|
if (decision.outcome === "rebuild") {
|
|
97345
97608
|
db.transaction(() => {
|
|
97346
97609
|
purgeEmbeddings(db, { dropVecTable: true });
|
|
97610
|
+
purgeEmbeddingSalvage(db);
|
|
97347
97611
|
deleteMeta(db, "embeddingDim");
|
|
97348
97612
|
setMeta(db, "embeddingFingerprint", currentFingerprint);
|
|
97349
97613
|
if (decision.identity)
|
|
@@ -97355,6 +97619,7 @@ async function generateEmbeddingsForDb(db, config, onProgress, signal, entryIds,
|
|
|
97355
97619
|
rebuildReason = decision.reason;
|
|
97356
97620
|
} else {
|
|
97357
97621
|
setMeta(db, "embeddingFingerprint", currentFingerprint);
|
|
97622
|
+
relabelEmbeddingSalvageFingerprint(db, storedFingerprint, currentFingerprint);
|
|
97358
97623
|
if (decision.identity)
|
|
97359
97624
|
setMeta(db, "embeddingIdentity", decision.identity);
|
|
97360
97625
|
if (decision.verified) {
|
|
@@ -97365,28 +97630,65 @@ async function generateEmbeddingsForDb(db, config, onProgress, signal, entryIds,
|
|
|
97365
97630
|
onProgress({ phase: "embeddings", message });
|
|
97366
97631
|
}
|
|
97367
97632
|
}
|
|
97633
|
+
} else {
|
|
97634
|
+
setMeta(db, "embeddingFingerprint", currentFingerprint);
|
|
97368
97635
|
}
|
|
97369
97636
|
try {
|
|
97370
97637
|
throwIfAborted(signal);
|
|
97371
97638
|
const allEntries = getAllEntriesForEmbedding(db, targetEntryIds);
|
|
97372
|
-
|
|
97639
|
+
let vecFailedCount = 0;
|
|
97640
|
+
let vecUnavailableCount = 0;
|
|
97641
|
+
const { reusedCount, remaining: candidateEntries } = reuseSalvagedEmbeddings(db, allEntries, currentFingerprint, (entry, embedding) => {
|
|
97642
|
+
const result = upsertEmbedding(db, entry.id, embedding);
|
|
97643
|
+
if (result.vec === "failed")
|
|
97644
|
+
vecFailedCount++;
|
|
97645
|
+
if (result.vec === "unavailable")
|
|
97646
|
+
vecUnavailableCount++;
|
|
97647
|
+
return result.stored;
|
|
97648
|
+
});
|
|
97649
|
+
if (reusedCount > 0) {
|
|
97650
|
+
onProgress({
|
|
97651
|
+
phase: "embeddings",
|
|
97652
|
+
message: `Reused ${reusedCount} embedding${reusedCount === 1 ? "" : "s"} from the previous generation; embedding ${candidateEntries.length} new.`
|
|
97653
|
+
});
|
|
97654
|
+
}
|
|
97655
|
+
if (candidateEntries.length === 0) {
|
|
97373
97656
|
onProgress({ phase: "embeddings", message: "Embeddings already up to date." });
|
|
97374
97657
|
setMeta(db, "embeddingFingerprint", currentFingerprint);
|
|
97375
|
-
|
|
97658
|
+
if (reusedCount > 0) {
|
|
97659
|
+
const vecGenerationComplete = targetEntryIds === undefined ? isVecFastPathComplete(db) : vecFastPathWasReady;
|
|
97660
|
+
setVecFastPathReady(db, vecFailedCount === 0 && vecUnavailableCount === 0 && vecGenerationComplete);
|
|
97661
|
+
}
|
|
97662
|
+
purgeEmbeddingSalvage(db);
|
|
97663
|
+
return reusedCount > 0 ? { success: true, vecInsertFailures: vecFailedCount } : { success: true };
|
|
97664
|
+
}
|
|
97665
|
+
let truncatedCount = 0;
|
|
97666
|
+
const texts = [];
|
|
97667
|
+
const pendingEntries = [];
|
|
97668
|
+
for (const entry of candidateEntries) {
|
|
97669
|
+
const capped = capEmbeddingText(entry.searchText, maxInputTokens);
|
|
97670
|
+
if (capped.text.length === 0)
|
|
97671
|
+
continue;
|
|
97672
|
+
if (capped.truncated)
|
|
97673
|
+
truncatedCount++;
|
|
97674
|
+
pendingEntries.push(entry);
|
|
97675
|
+
texts.push(capped.text);
|
|
97676
|
+
}
|
|
97677
|
+
if (truncatedCount > 0) {
|
|
97678
|
+
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.`;
|
|
97679
|
+
onProgress({ phase: "embeddings", message });
|
|
97376
97680
|
}
|
|
97377
97681
|
if (rebuildReason) {
|
|
97378
|
-
const message = `[embed] Re-embedding ${
|
|
97379
|
-
warn(message);
|
|
97682
|
+
const message = `[embed] Re-embedding ${pendingEntries.length} entr${pendingEntries.length === 1 ? "y" : "ies"} because ${rebuildReason}`;
|
|
97380
97683
|
onProgress({ phase: "embeddings", message });
|
|
97381
97684
|
}
|
|
97382
97685
|
onProgress({
|
|
97383
97686
|
phase: "embeddings",
|
|
97384
|
-
message: `Generating embeddings for ${
|
|
97687
|
+
message: `Generating embeddings for ${pendingEntries.length} entr${pendingEntries.length === 1 ? "y" : "ies"}.`
|
|
97385
97688
|
});
|
|
97386
|
-
const texts = allEntries.map((entry) => entry.searchText);
|
|
97387
97689
|
if (isVerbose()) {
|
|
97388
97690
|
if (hasRemoteEndpoint(config.embedding ?? {})) {
|
|
97389
|
-
const tokenBudget = config.embedding?.maxTokens ??
|
|
97691
|
+
const tokenBudget = config.embedding?.maxTokens ?? DEFAULT_TOKEN_BUDGET;
|
|
97390
97692
|
const maxCount = config.embedding?.batchSize ?? DEFAULT_REMOTE_BATCH_SIZE;
|
|
97391
97693
|
const batches = buildTokenBoundedBatches(texts, tokenBudget, maxCount);
|
|
97392
97694
|
const batchNumberByIndex = new Map;
|
|
@@ -97394,7 +97696,7 @@ async function generateEmbeddingsForDb(db, config, onProgress, signal, entryIds,
|
|
|
97394
97696
|
for (const i of batch.indices)
|
|
97395
97697
|
batchNumberByIndex.set(i, batchIdx + 1);
|
|
97396
97698
|
});
|
|
97397
|
-
for (const [i, entry] of
|
|
97699
|
+
for (const [i, entry] of pendingEntries.entries()) {
|
|
97398
97700
|
const chars = entry.searchText.length;
|
|
97399
97701
|
const tokens = estimateTokenCount(entry.searchText);
|
|
97400
97702
|
const batch = batches[batchNumberByIndex.get(i) - 1];
|
|
@@ -97402,7 +97704,7 @@ async function generateEmbeddingsForDb(db, config, onProgress, signal, entryIds,
|
|
|
97402
97704
|
warnVerbose(`[embed] ${entry.itemRef} (${chars} chars, est. ${tokens} tokens) → ${label}`);
|
|
97403
97705
|
}
|
|
97404
97706
|
} else {
|
|
97405
|
-
for (const entry of
|
|
97707
|
+
for (const entry of pendingEntries) {
|
|
97406
97708
|
warnVerbose(`[embed] ${entry.itemRef} (${entry.searchText.length} chars, est. ${estimateTokenCount(entry.searchText)} tokens)`);
|
|
97407
97709
|
}
|
|
97408
97710
|
}
|
|
@@ -97411,27 +97713,68 @@ async function generateEmbeddingsForDb(db, config, onProgress, signal, entryIds,
|
|
|
97411
97713
|
let storedCount = 0;
|
|
97412
97714
|
let skippedCount = 0;
|
|
97413
97715
|
let embedFailedCount = 0;
|
|
97414
|
-
let vecFailedCount = 0;
|
|
97415
|
-
let vecUnavailableCount = 0;
|
|
97416
97716
|
let storedTokens = 0;
|
|
97417
|
-
let lastProgressBucket = 0;
|
|
97418
97717
|
try {
|
|
97419
97718
|
heartbeatTimer = setInterval(() => {
|
|
97420
97719
|
onProgress({
|
|
97421
97720
|
phase: "embeddings",
|
|
97422
|
-
message:
|
|
97721
|
+
message: formatEmbeddingHeartbeat(storedCount, pendingEntries.length, embedFailedCount)
|
|
97423
97722
|
});
|
|
97424
97723
|
}, 15000);
|
|
97425
97724
|
const skips = [];
|
|
97426
97725
|
const embedStart = Date.now();
|
|
97726
|
+
let consecutiveSingleDocFailures = 0;
|
|
97727
|
+
let consecutiveNetworkErrorFailures = 0;
|
|
97728
|
+
let circuitBreakerReason;
|
|
97729
|
+
const onSkip = (skip) => {
|
|
97730
|
+
skips.push(skip);
|
|
97731
|
+
if (!skip.batchStart)
|
|
97732
|
+
return;
|
|
97733
|
+
if (skip.reason === "context-window-exceeded") {
|
|
97734
|
+
consecutiveSingleDocFailures = 0;
|
|
97735
|
+
consecutiveNetworkErrorFailures = 0;
|
|
97736
|
+
return;
|
|
97737
|
+
}
|
|
97738
|
+
consecutiveSingleDocFailures = skip.batchSize === 1 ? consecutiveSingleDocFailures + 1 : 0;
|
|
97739
|
+
consecutiveNetworkErrorFailures = skip.failureKind === "network-error" ? consecutiveNetworkErrorFailures + 1 : 0;
|
|
97740
|
+
if (consecutiveSingleDocFailures >= CIRCUIT_BREAKER_THRESHOLD || consecutiveNetworkErrorFailures >= CIRCUIT_BREAKER_THRESHOLD) {
|
|
97741
|
+
circuitBreakerReason = skip.message;
|
|
97742
|
+
return false;
|
|
97743
|
+
}
|
|
97744
|
+
return;
|
|
97745
|
+
};
|
|
97427
97746
|
let observedModel;
|
|
97428
97747
|
let observedVectorLen;
|
|
97429
|
-
const
|
|
97748
|
+
const reportPerBatchLine = hasRemoteEndpoint(config.embedding ?? {});
|
|
97749
|
+
const onBatch = (indices, batchEmbeddings, model, outcome) => {
|
|
97750
|
+
if (outcome?.outcome === "retrying") {
|
|
97751
|
+
if (reportPerBatchLine) {
|
|
97752
|
+
onProgress({
|
|
97753
|
+
phase: "embeddings",
|
|
97754
|
+
message: `[embed] batch ${outcome.batchIndex}/${outcome.batchCount}: ${outcome.docCount} docs, ${outcome.requestTokens.toLocaleString()} tokens → retrying after ${(outcome.elapsedMs / 1000).toFixed(1)} s`
|
|
97755
|
+
});
|
|
97756
|
+
}
|
|
97757
|
+
return;
|
|
97758
|
+
}
|
|
97759
|
+
if (outcome?.outcome === "budget-lowered") {
|
|
97760
|
+
if (reportPerBatchLine) {
|
|
97761
|
+
onProgress({
|
|
97762
|
+
phase: "embeddings",
|
|
97763
|
+
message: `[embed] batch ${outcome.batchIndex}/${outcome.batchCount}: ${outcome.docCount} docs, ${outcome.requestTokens.toLocaleString()} tokens → ${outcome.reason}`
|
|
97764
|
+
});
|
|
97765
|
+
}
|
|
97766
|
+
return;
|
|
97767
|
+
}
|
|
97430
97768
|
if (model)
|
|
97431
97769
|
observedModel = model;
|
|
97770
|
+
if (batchEmbeddings.some((embedding) => embedding !== undefined)) {
|
|
97771
|
+
consecutiveSingleDocFailures = 0;
|
|
97772
|
+
consecutiveNetworkErrorFailures = 0;
|
|
97773
|
+
}
|
|
97432
97774
|
db.transaction(() => {
|
|
97433
97775
|
for (let k2 = 0;k2 < indices.length; k2++) {
|
|
97434
|
-
const
|
|
97776
|
+
const index = indices[k2];
|
|
97777
|
+
const entry = pendingEntries[index];
|
|
97435
97778
|
if (!entry)
|
|
97436
97779
|
continue;
|
|
97437
97780
|
const embedding = batchEmbeddings[k2];
|
|
@@ -97444,7 +97787,7 @@ async function generateEmbeddingsForDb(db, config, onProgress, signal, entryIds,
|
|
|
97444
97787
|
const result = upsertEmbedding(db, entry.id, embedding);
|
|
97445
97788
|
if (result.stored) {
|
|
97446
97789
|
storedCount++;
|
|
97447
|
-
storedTokens += estimateTokenCount(
|
|
97790
|
+
storedTokens += estimateTokenCount(texts[index]);
|
|
97448
97791
|
} else {
|
|
97449
97792
|
skippedCount++;
|
|
97450
97793
|
}
|
|
@@ -97454,29 +97797,25 @@ async function generateEmbeddingsForDb(db, config, onProgress, signal, entryIds,
|
|
|
97454
97797
|
vecUnavailableCount++;
|
|
97455
97798
|
}
|
|
97456
97799
|
})();
|
|
97457
|
-
|
|
97458
|
-
|
|
97459
|
-
|
|
97800
|
+
if (outcome && outcome.reason !== "oversized" && reportPerBatchLine) {
|
|
97801
|
+
const elapsedSeconds2 = (outcome.elapsedMs / 1000).toFixed(1);
|
|
97802
|
+
const outcomeLabel = outcome.outcome === "stored" ? `${outcome.docCount} stored (${elapsedSeconds2} s)` : `failed: ${outcome.reason}`;
|
|
97460
97803
|
onProgress({
|
|
97461
97804
|
phase: "embeddings",
|
|
97462
|
-
message: `
|
|
97805
|
+
message: `[embed] batch ${outcome.batchIndex}/${outcome.batchCount}: ${outcome.docCount} docs, ${outcome.requestTokens.toLocaleString()} tokens → ${outcomeLabel}`
|
|
97463
97806
|
});
|
|
97464
97807
|
}
|
|
97808
|
+
onProgress({
|
|
97809
|
+
phase: "embeddings",
|
|
97810
|
+
message: `Embedded ${storedCount}/${pendingEntries.length} entries.`
|
|
97811
|
+
});
|
|
97465
97812
|
};
|
|
97466
|
-
await embedBatch(texts, config.embedding, signal,
|
|
97813
|
+
await embedBatch(texts, config.embedding, signal, onSkip, onBatch);
|
|
97467
97814
|
throwIfAborted(signal);
|
|
97468
97815
|
const elapsedSeconds = Math.max((Date.now() - embedStart) / 1000, 0.001);
|
|
97469
97816
|
if (skippedCount > 0) {
|
|
97470
97817
|
warn(`[embed] ${skippedCount} embedding${skippedCount === 1 ? "" : "s"} skipped (entry deleted between queue and write)`);
|
|
97471
97818
|
}
|
|
97472
|
-
if (embedFailedCount > 0) {
|
|
97473
|
-
const detail = skips.slice(0, 20).map((skip) => ` - ${allEntries[skip.index]?.itemRef ?? skip.index} (${skip.reason}): ${skip.message}`).join(`
|
|
97474
|
-
`);
|
|
97475
|
-
const more = skips.length > 20 ? `
|
|
97476
|
-
...and ${skips.length - 20} more` : "";
|
|
97477
|
-
warn(`[embed] ${embedFailedCount} embedding${embedFailedCount === 1 ? "" : "s"} could not be generated and ${embedFailedCount === 1 ? "was" : "were"} skipped:
|
|
97478
|
-
${detail}${more}`);
|
|
97479
|
-
}
|
|
97480
97819
|
const vecGenerationComplete = targetEntryIds === undefined ? isVecFastPathComplete(db) : vecFastPathWasReady;
|
|
97481
97820
|
setVecFastPathReady(db, vecFailedCount === 0 && vecUnavailableCount === 0 && vecGenerationComplete);
|
|
97482
97821
|
if (vecFailedCount > 0) {
|
|
@@ -97484,14 +97823,39 @@ ${detail}${more}`);
|
|
|
97484
97823
|
}
|
|
97485
97824
|
const entriesPerSec = storedCount / elapsedSeconds;
|
|
97486
97825
|
const tokensPerSec = storedTokens / elapsedSeconds;
|
|
97826
|
+
const totalStored = storedCount + reusedCount;
|
|
97827
|
+
const oversizedSkips = skips.filter((skip) => skip.reason === "context-window-exceeded");
|
|
97828
|
+
const timedOutSkips = skips.filter((skip) => skip.reason === "batch-request-failed" && skip.failureKind === "timeout");
|
|
97829
|
+
const failedSkips = skips.filter((skip) => skip.reason === "batch-request-failed" && skip.failureKind !== "timeout");
|
|
97830
|
+
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)`;
|
|
97487
97831
|
onProgress({
|
|
97488
97832
|
phase: "embeddings",
|
|
97489
|
-
message:
|
|
97833
|
+
message: `${throughputLine}; ${oversizedSkips.length} oversized skipped, ${timedOutSkips.length} timed out, ${failedSkips.length} failed.`
|
|
97490
97834
|
});
|
|
97835
|
+
const printSkipList = (label, skipList) => {
|
|
97836
|
+
if (skipList.length === 0)
|
|
97837
|
+
return;
|
|
97838
|
+
const limit = isVerbose() ? skipList.length : 20;
|
|
97839
|
+
const listed = skipList.slice(0, limit).map((skip) => ` - ${pendingEntries[skip.index]?.itemRef ?? skip.index}: ${skip.message}`).join(`
|
|
97840
|
+
`);
|
|
97841
|
+
const more = skipList.length > limit ? `
|
|
97842
|
+
...and ${skipList.length - limit} more` : "";
|
|
97843
|
+
onProgress({ phase: "embeddings", message: `[embed] ${label} skipped:
|
|
97844
|
+
${listed}${more}` });
|
|
97845
|
+
};
|
|
97846
|
+
printSkipList("oversized documents", oversizedSkips);
|
|
97847
|
+
printSkipList("timed-out documents", timedOutSkips);
|
|
97848
|
+
printSkipList("failed documents", failedSkips);
|
|
97491
97849
|
setMeta(db, "embeddingFingerprint", currentFingerprint);
|
|
97492
97850
|
const observedIdentity = deriveObservedEmbeddingIdentity(config.embedding, observedModel, observedVectorLen);
|
|
97493
97851
|
if (observedIdentity)
|
|
97494
97852
|
setMeta(db, "embeddingIdentity", observedIdentity);
|
|
97853
|
+
if (circuitBreakerReason !== undefined) {
|
|
97854
|
+
const message = `embedding provider failed ${CIRCUIT_BREAKER_THRESHOLD} consecutive batches ` + `(last: ${circuitBreakerReason}); stopped after ${storedCount} embedding${storedCount === 1 ? "" : "s"} ` + "were stored — rerun akm index when the endpoint is healthy";
|
|
97855
|
+
warn(`[embed] ${message}`);
|
|
97856
|
+
onProgress({ phase: "embeddings", message });
|
|
97857
|
+
return { success: false, message, vecInsertFailures: vecFailedCount };
|
|
97858
|
+
}
|
|
97495
97859
|
if (storedCount === 0 && embedFailedCount > 0) {
|
|
97496
97860
|
const firstMessage = skips[0]?.message ?? "All embeddings failed.";
|
|
97497
97861
|
return {
|
|
@@ -97499,6 +97863,7 @@ ${detail}${more}`);
|
|
|
97499
97863
|
message: `All ${embedFailedCount} embedding batch(es) failed: ${firstMessage}`
|
|
97500
97864
|
};
|
|
97501
97865
|
}
|
|
97866
|
+
purgeEmbeddingSalvage(db);
|
|
97502
97867
|
return { success: true, vecInsertFailures: vecFailedCount };
|
|
97503
97868
|
} finally {
|
|
97504
97869
|
if (heartbeatTimer)
|
|
@@ -97915,7 +98280,11 @@ async function indexWrittenAssets(stashDir, filePaths, options = {}) {
|
|
|
97915
98280
|
return await (async () => {
|
|
97916
98281
|
const rebuildProbe = probeLock(getIndexRebuildLockPath());
|
|
97917
98282
|
if (rebuildProbe.state === "held") {
|
|
97918
|
-
|
|
98283
|
+
const holderLabel = formatLockHolderPid({
|
|
98284
|
+
pid: rebuildProbe.holderPid,
|
|
98285
|
+
launcherPid: rebuildProbe.launcherPid ?? null
|
|
98286
|
+
});
|
|
98287
|
+
warn(`index rebuild in progress (pid ${holderLabel}); the next index pass will index ${filePaths.join(", ")}`);
|
|
97919
98288
|
return true;
|
|
97920
98289
|
}
|
|
97921
98290
|
const dbPath = getDbPath();
|
|
@@ -98889,7 +99258,7 @@ var PROPOSAL_TXN_PHASES = [
|
|
|
98889
99258
|
"committed"
|
|
98890
99259
|
];
|
|
98891
99260
|
function proposalHash(content) {
|
|
98892
|
-
return
|
|
99261
|
+
return createHash11("sha256").update(content).digest("hex");
|
|
98893
99262
|
}
|
|
98894
99263
|
function proposalFileHash(filePath) {
|
|
98895
99264
|
return proposalHash(fs55.readFileSync(filePath));
|