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.
@@ -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: createHash10 } = __require("node:crypto");
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) => createHash10("sha512").update(s).digest("hex");
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
- return JSON.stringify({ ...metadata, pid: process.pid, lockId: randomUUID() });
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 = extractHolderPid(rawContent);
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 extractHolderPid(content) {
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
- return Number.isInteger(pid2) && pid2 > 0 ? pid2 : undefined;
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 createHash10, randomUUID as randomUUID6 } from "node:crypto";
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 createHash9 } from "node:crypto";
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 createHash9("sha256").update(text).digest("hex");
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 = 8000;
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
- }, 30000, signal);
97105
+ }, timeoutMs, signal);
96973
97106
  if (!response.ok) {
96974
- const errBody = await readBodyWithByteCap(response, undefined, { bodyTimeoutMs: 30000, signal }).catch((err) => {
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: 30000, signal }));
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
- const tokenBudget = this.config.maxTokens ?? this.config.contextLength ?? DEFAULT_TOKEN_BUDGET;
97126
+ let effectiveTokenBudget = this.config.maxTokens ?? DEFAULT_TOKEN_BUDGET;
96994
97127
  const maxCount = this.config.batchSize ?? DEFAULT_REMOTE_BATCH_SIZE;
96995
- const textBatches = buildTokenBoundedBatches(texts, tokenBudget, maxCount);
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
- warnVerbose(`[embed] batch of ${batch.length} document(s) failed and was skipped: ${message}`);
97046
- for (const idx of indices) {
97047
- onSkip?.({ index: idx, reason, message });
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
- commitBatch(indices, batchEmbeddings, responseModel);
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 ${tokenBudget}-token embedding budget; skipped.`
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, { signal: dispatchAbort.signal });
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
- }, 30000, signal);
97332
+ }, timeoutMs, signal);
97097
97333
  if (!response.ok) {
97098
- const respBody = await readBodyWithByteCap(response, undefined, { bodyTimeoutMs: 30000, signal }).catch((err) => {
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: 30000, signal }));
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
- var PROGRESS_INTERVAL = 500;
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
- if (allEntries.length === 0) {
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
- return { success: true };
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 ${allEntries.length} entr${allEntries.length === 1 ? "y" : "ies"} because ${rebuildReason}`;
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 ${allEntries.length} entr${allEntries.length === 1 ? "y" : "ies"}.`
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 ?? config.embedding?.contextLength ?? DEFAULT_TOKEN_BUDGET;
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 allEntries.entries()) {
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 allEntries) {
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: `Still generating embeddings: ${storedCount}/${allEntries.length} stored; waiting on embedding provider.`
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 onBatch = (indices, batchEmbeddings, model) => {
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 entry = allEntries[indices[k2]];
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(entry.searchText);
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
- const bucket = Math.floor(storedCount / PROGRESS_INTERVAL);
97458
- if (bucket > lastProgressBucket) {
97459
- lastProgressBucket = bucket;
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: `Embedded ${storedCount}/${allEntries.length} entries.`
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, (skip) => skips.push(skip), onBatch);
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: `Stored ${storedCount} embedding${storedCount === 1 ? "" : "s"} in ${elapsedSeconds.toFixed(1)}s (${entriesPerSec.toFixed(1)} entries/s, ~${Math.round(tokensPerSec)} tokens/s).`
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
- warn(`index rebuild in progress (pid ${rebuildProbe.holderPid}); the next index pass will index ${filePaths.join(", ")}`);
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 createHash10("sha256").update(content).digest("hex");
99261
+ return createHash11("sha256").update(content).digest("hex");
98893
99262
  }
98894
99263
  function proposalFileHash(filePath) {
98895
99264
  return proposalHash(fs55.readFileSync(filePath));