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.
@@ -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: createHash10 } = __require("crypto");
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) => createHash10("sha512").update(s).digest("hex");
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
- return JSON.stringify({ ...metadata, pid: process.pid, lockId: randomUUID() });
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 = extractHolderPid(rawContent);
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 extractHolderPid(content) {
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
- return Number.isInteger(pid2) && pid2 > 0 ? pid2 : undefined;
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 createHash10, randomUUID as randomUUID6 } from "crypto";
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 createHash9 } from "crypto";
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 createHash9("sha256").update(text).digest("hex");
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 = 8000;
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
- }, 30000, signal);
97061
+ }, timeoutMs, signal);
96929
97062
  if (!response.ok) {
96930
- const errBody = await readBodyWithByteCap(response, undefined, { bodyTimeoutMs: 30000, signal }).catch((err) => {
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: 30000, signal }));
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
- const tokenBudget = this.config.maxTokens ?? this.config.contextLength ?? DEFAULT_TOKEN_BUDGET;
97082
+ let effectiveTokenBudget = this.config.maxTokens ?? DEFAULT_TOKEN_BUDGET;
96950
97083
  const maxCount = this.config.batchSize ?? DEFAULT_REMOTE_BATCH_SIZE;
96951
- const textBatches = buildTokenBoundedBatches(texts, tokenBudget, maxCount);
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
- warnVerbose(`[embed] batch of ${batch.length} document(s) failed and was skipped: ${message}`);
97002
- for (const idx of indices) {
97003
- onSkip?.({ index: idx, reason, message });
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
- commitBatch(indices, batchEmbeddings, responseModel);
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 ${tokenBudget}-token embedding budget; skipped.`
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, { signal: dispatchAbort.signal });
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
- }, 30000, signal);
97288
+ }, timeoutMs, signal);
97053
97289
  if (!response.ok) {
97054
- const respBody = await readBodyWithByteCap(response, undefined, { bodyTimeoutMs: 30000, signal }).catch((err) => {
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: 30000, signal }));
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
- var PROGRESS_INTERVAL = 500;
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
- if (allEntries.length === 0) {
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
- return { success: true };
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 ${allEntries.length} entr${allEntries.length === 1 ? "y" : "ies"} because ${rebuildReason}`;
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 ${allEntries.length} entr${allEntries.length === 1 ? "y" : "ies"}.`
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 ?? config.embedding?.contextLength ?? DEFAULT_TOKEN_BUDGET;
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 allEntries.entries()) {
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 allEntries) {
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: `Still generating embeddings: ${storedCount}/${allEntries.length} stored; waiting on embedding provider.`
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 onBatch = (indices, batchEmbeddings, model) => {
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 entry = allEntries[indices[k2]];
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(entry.searchText);
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
- const bucket = Math.floor(storedCount / PROGRESS_INTERVAL);
97414
- if (bucket > lastProgressBucket) {
97415
- lastProgressBucket = bucket;
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: `Embedded ${storedCount}/${allEntries.length} entries.`
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, (skip) => skips.push(skip), onBatch);
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: `Stored ${storedCount} embedding${storedCount === 1 ? "" : "s"} in ${elapsedSeconds.toFixed(1)}s (${entriesPerSec.toFixed(1)} entries/s, ~${Math.round(tokensPerSec)} tokens/s).`
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
- warn(`index rebuild in progress (pid ${rebuildProbe.holderPid}); the next index pass will index ${filePaths.join(", ")}`);
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 createHash10("sha256").update(content).digest("hex");
99217
+ return createHash11("sha256").update(content).digest("hex");
98849
99218
  }
98850
99219
  function proposalFileHash(filePath) {
98851
99220
  return proposalHash(fs55.readFileSync(filePath));