akm-cli 0.9.15-beta.2 → 0.9.15-beta.4

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.
@@ -30,10 +30,17 @@ export const DEFAULT_REMOTE_BATCH_SIZE = 100;
30
30
  * a batch of 100 small docs (~400 KB, ~100K tokens) took 14.8s against a
31
31
  * healthy local endpoint — half the 30s request timeout — and a single
32
32
  * 128 KB (~24K token) document alone was rejected by the endpoint as
33
- * exceeding its context size. 8000 tokens keeps a batch's estimated size
34
- * comfortably under both the timeout and common local-model context windows.
33
+ * exceeding its context size.
34
+ *
35
+ * Lowered from 8000 to 6000 (#954, field report on beta.1): the 4-chars-
36
+ * per-token estimator undercounts dense technical text by 7-55%, so 8000
37
+ * against an 8192-token llama.cpp embedder regularly landed real requests
38
+ * over the endpoint's context window. 6000 is the value the field confirmed
39
+ * stops that steady trickle of rejections; `embedBatch`'s run-scoped
40
+ * adaptive budget below still shrinks further, for an endpoint where even
41
+ * this is not enough.
35
42
  */
36
- export const DEFAULT_TOKEN_BUDGET = 8000;
43
+ export const DEFAULT_TOKEN_BUDGET = 6000;
37
44
  /** Cheap token estimator: 4 chars ≈ 1 token. Used in verbose logging and error messages. */
38
45
  export function estimateTokenCount(text) {
39
46
  return Math.round(text.length / 4);
@@ -73,7 +80,7 @@ export function capEmbeddingText(text, maxTokens) {
73
80
  /**
74
81
  * Default per-request timeout when `embedding.timeoutMs` is unset (#954).
75
82
  * The prior fixed 30s cut off exactly the field-report case: a
76
- * local model server on an 8000-token (`DEFAULT_TOKEN_BUDGET`) batch
83
+ * local model server on a full-budget (`DEFAULT_TOKEN_BUDGET`) batch
77
84
  * legitimately takes longer than that, and the timeout fired mid-response
78
85
  * with no retry — every batch it hit was silently dropped for the rest of
79
86
  * an hours-long run. 120s comfortably covers a slow local batch while still
@@ -235,6 +242,21 @@ export function buildTokenBoundedBatches(texts, tokenBudget, maxCount) {
235
242
  flush();
236
243
  return batches;
237
244
  }
245
+ /**
246
+ * Shrink factor applied to the effective request budget on the first
247
+ * context-size rejection of an `embedBatch` run (#954, field report on
248
+ * beta.1): one 25% cut absorbs the estimator's measured undercount without
249
+ * repeatedly re-shrinking mid-run — see the "shrink at most once" rule on
250
+ * {@link RemoteEmbedder.embedBatch}.
251
+ */
252
+ const ADAPTIVE_BUDGET_SHRINK_FACTOR = 0.75;
253
+ /**
254
+ * Floor on the adaptive-budget shrink above, as a multiple of
255
+ * `maxInputTokens` (#954): a request budget below twice the per-document cap
256
+ * could no longer batch more than one document per request, defeating the
257
+ * point of batching at all.
258
+ */
259
+ const ADAPTIVE_BUDGET_FLOOR_MULTIPLIER = 2;
238
260
  export class RemoteEmbedder {
239
261
  config;
240
262
  endpoint;
@@ -339,6 +361,18 @@ export class RemoteEmbedder {
339
361
  * {@link scaleEmbeddingTimeoutMs}, so a dead server is detected in seconds
340
362
  * on a small batch rather than always waiting out the full configured
341
363
  * `embedding.timeoutMs`.
364
+ *
365
+ * Run-scoped adaptive budget (#954, field report on beta.1): the FIRST
366
+ * context-size rejection of the run shrinks the effective request budget
367
+ * by {@link ADAPTIVE_BUDGET_SHRINK_FACTOR} (floored at
368
+ * {@link ADAPTIVE_BUDGET_FLOOR_MULTIPLIER} times `maxInputTokens`) for
369
+ * every batch not yet dispatched — the still-planned tail of `texts` is
370
+ * re-batched with `buildTokenBoundedBatches` at the smaller budget, and a
371
+ * `budget-lowered` `onBatch` event reports it once. This never touches the
372
+ * split-and-retry of the rejected batch itself (above), and never fires a
373
+ * second time in the same run even if a later batch is also rejected — a
374
+ * static configured budget that is simply too big for the endpoint should
375
+ * self-correct once, not ratchet down forever.
342
376
  */
343
377
  async embedBatch(texts, signal, onSkip, onBatch) {
344
378
  if (texts.length === 0)
@@ -351,10 +385,59 @@ export class RemoteEmbedder {
351
385
  // request budget too, so a config author setting it for one purpose
352
386
  // silently changed the other. `maxTokens` is the sole knob for the
353
387
  // request budget now; unset falls back to DEFAULT_TOKEN_BUDGET.
354
- const tokenBudget = this.config.maxTokens ?? DEFAULT_TOKEN_BUDGET;
388
+ //
389
+ // `effectiveTokenBudget` (#954) starts at the configured/default value
390
+ // and MAY shrink once, on the run's first context-size rejection — see
391
+ // `maybeShrinkBudget` below. `textBatches` is mutated in place (spliced)
392
+ // by that shrink rather than reassigned, so the in-flight
393
+ // `concurrentMap` pool below (which reads this same array by reference)
394
+ // picks up the re-planned tail without restarting.
395
+ let effectiveTokenBudget = this.config.maxTokens ?? DEFAULT_TOKEN_BUDGET;
355
396
  const maxCount = this.config.batchSize ?? DEFAULT_REMOTE_BATCH_SIZE;
356
- const textBatches = buildTokenBoundedBatches(texts, tokenBudget, maxCount);
397
+ const maxInputTokens = this.config.maxInputTokens ?? DEFAULT_MAX_INPUT_TOKENS;
398
+ const textBatches = buildTokenBoundedBatches(texts, effectiveTokenBudget, maxCount);
357
399
  const configuredTimeoutMs = resolveEmbeddingTimeoutMs(this.config);
400
+ // How many of `textBatches` concurrentMap has already claimed (its own
401
+ // `nextIndex`, mirrored here so a budget shrink knows where the
402
+ // not-yet-dispatched tail begins). Assigned, not incremented, at the top
403
+ // of `runProviderBatch` — batches are claimed in strictly increasing
404
+ // order, so the highest `batchIndex` seen so far IS the claimed count.
405
+ let dispatchedBatchCount = 0;
406
+ // Set once the run's first context-size rejection has shrunk the budget
407
+ // (#954) — guards `maybeShrinkBudget` so it never fires twice.
408
+ let budgetShrunk = false;
409
+ // On the FIRST context-size rejection of this `embedBatch` call, shrink
410
+ // `effectiveTokenBudget` and re-plan every batch `concurrentMap` has not
411
+ // yet claimed from the smaller budget. Never touches `rejectedIndices`
412
+ // itself — the caller's own split-and-retry handles that batch — and is
413
+ // a no-op after the first call (`budgetShrunk`).
414
+ const maybeShrinkBudget = (rejectedIndices, rejectedBatchIndex, rejectedRequestTokens) => {
415
+ if (budgetShrunk)
416
+ return;
417
+ budgetShrunk = true;
418
+ const floor = ADAPTIVE_BUDGET_FLOOR_MULTIPLIER * maxInputTokens;
419
+ effectiveTokenBudget = Math.max(Math.round(effectiveTokenBudget * ADAPTIVE_BUDGET_SHRINK_FACTOR), floor);
420
+ const notYetDispatched = textBatches.slice(dispatchedBatchCount);
421
+ const remainingIndices = notYetDispatched.flatMap((batch) => batch.indices);
422
+ if (remainingIndices.length > 0) {
423
+ const remainingTexts = remainingIndices.map((i) => texts[i]);
424
+ const replanned = buildTokenBoundedBatches(remainingTexts, effectiveTokenBudget, maxCount).map((batch) => ({
425
+ indices: batch.indices.map((localIndex) => remainingIndices[localIndex]),
426
+ oversized: batch.oversized,
427
+ }));
428
+ textBatches.splice(dispatchedBatchCount, textBatches.length - dispatchedBatchCount, ...replanned);
429
+ }
430
+ warnVerbose(`[embed] provider rejected a ${rejectedRequestTokens}-token request as over its context; request budget lowered to ${effectiveTokenBudget.toLocaleString()} for the rest of this run`);
431
+ commitBatch(rejectedIndices, rejectedIndices.map(() => undefined), undefined, {
432
+ batchIndex: rejectedBatchIndex,
433
+ batchCount: textBatches.length,
434
+ docCount: rejectedIndices.length,
435
+ requestTokens: rejectedRequestTokens,
436
+ elapsedMs: 0,
437
+ outcome: "budget-lowered",
438
+ reason: `provider rejected ${rejectedRequestTokens.toLocaleString()} tokens as over its context; request budget lowered to ${effectiveTokenBudget.toLocaleString()} for the rest of this run`,
439
+ });
440
+ };
358
441
  // Stops the pool from claiming any FURTHER provider batch once the
359
442
  // caller's onBatch has failed once (the materializer's transaction
360
443
  // failed, so a subsequent commit would just fail again) — dispatching
@@ -437,7 +520,7 @@ export class RemoteEmbedder {
437
520
  return;
438
521
  const batch = indices.map((i) => texts[i]);
439
522
  const requestTokens = batch.reduce((sum, text) => sum + estimateTokenCount(text), 0);
440
- const requestTimeoutMs = scaleEmbeddingTimeoutMs(configuredTimeoutMs, requestTokens, tokenBudget);
523
+ const requestTimeoutMs = scaleEmbeddingTimeoutMs(configuredTimeoutMs, requestTokens, effectiveTokenBudget);
441
524
  const requestStart = Date.now();
442
525
  let batchEmbeddings;
443
526
  let responseModel;
@@ -457,6 +540,15 @@ export class RemoteEmbedder {
457
540
  // failed" condition, it means stop entirely.
458
541
  if (signal?.aborted)
459
542
  throw err;
543
+ if (err instanceof ContextExceededError) {
544
+ // #954: the run's FIRST context-size rejection (any size) shrinks
545
+ // the budget for everything not yet dispatched; a no-op after the
546
+ // first call. Deliberately BEFORE the split below — it must fire
547
+ // for a single-document rejection too (which never reaches the
548
+ // `indices.length > 1` split branch), and it never touches this
549
+ // batch's own split-and-retry.
550
+ maybeShrinkBudget(indices, batchIndex, requestTokens);
551
+ }
460
552
  if (err instanceof ContextExceededError && indices.length > 1) {
461
553
  const mid = Math.ceil(indices.length / 2);
462
554
  await requestAndCommit(indices.slice(0, mid), batchIndex, false, timeoutAttempt);
@@ -554,13 +646,19 @@ export class RemoteEmbedder {
554
646
  });
555
647
  };
556
648
  const runProviderBatch = async (textBatch, batchIndex) => {
649
+ // Claimed in strictly increasing order by `concurrentMap` below, so
650
+ // the highest `batchIndex` seen so far is exactly how many batches it
651
+ // has claimed (#954) — see `maybeShrinkBudget`'s doc comment above.
652
+ // Assigned synchronously at entry, before any `await`, so this always
653
+ // matches `concurrentMap`'s own `nextIndex` at the moment of claim.
654
+ dispatchedBatchCount = batchIndex;
557
655
  if (textBatch.oversized) {
558
656
  const idx = textBatch.indices[0];
559
657
  const estTokens = estimateTokenCount(texts[idx]);
560
658
  onSkip?.({
561
659
  index: idx,
562
660
  reason: "context-window-exceeded",
563
- message: `Document estimated at ${estTokens} tokens exceeds the ${tokenBudget}-token embedding budget; skipped.`,
661
+ message: `Document estimated at ${estTokens} tokens exceeds the ${effectiveTokenBudget}-token embedding budget; skipped.`,
564
662
  batchStart: true,
565
663
  batchSize: 1,
566
664
  });
@@ -67,12 +67,18 @@ function toEventMetadata(record) {
67
67
  * opened for its other events. A getter is resolved for every append so a
68
68
  * caller can replace its context binding without replacing this owning sink.
69
69
  * When omitted, `appendEvent` falls back to its default open-insert-close path.
70
+ *
71
+ * `onRecord`, when supplied, runs synchronously for every terminal record
72
+ * BEFORE persistence — improve's first-engine-response heartbeat (#957) uses
73
+ * it to know the run is no longer silent, without this module taking on any
74
+ * dependency of its own on improve's lifecycle.
70
75
  */
71
- export function installLlmUsagePersistence(ctx) {
76
+ export function installLlmUsagePersistence(ctx, onRecord) {
72
77
  let expectedTerminalRecords = 0;
73
78
  let disposed = false;
74
79
  setLlmUsageSink((record) => {
75
80
  expectedTerminalRecords += 1;
81
+ onRecord?.(record);
76
82
  appendEvent({ eventType: LLM_USAGE_EVENT, metadata: toEventMetadata(record) }, typeof ctx === "function" ? ctx() : ctx);
77
83
  });
78
84
  return () => {
@@ -7070,7 +7070,9 @@ 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.",
7075
+ MAINTENANCE_BARRIER_BUSY: "Another akm process is registering a lock or lease right now. Retry shortly, or pass --skip-if-locked on scheduled index/improve/workflow runs."
7074
7076
  };
7075
7077
  NOT_FOUND_HINTS = {
7076
7078
  ASSET_NOT_FOUND: "Run `akm search <query>` or `akm index` to refresh the index.",
@@ -75920,7 +75922,6 @@ var EmbeddingConnectionConfigSchema = exports_external.object({
75920
75922
  maxInputTokens: positiveInt.optional(),
75921
75923
  maxTokens: positiveInt.optional(),
75922
75924
  batchSize: positiveInt.optional(),
75923
- chunkSize: positiveInt.optional(),
75924
75925
  contextLength: positiveInt.optional(),
75925
75926
  ollamaOptions: EmbeddingOllamaOptionsSchema.optional(),
75926
75927
  timeoutMs: positiveInt.optional(),
@@ -77923,10 +77924,13 @@ import { AsyncLocalStorage } from "node:async_hooks";
77923
77924
  import { randomUUID as randomUUID2 } from "node:crypto";
77924
77925
  import fs7 from "node:fs";
77925
77926
  import path12 from "node:path";
77927
+ init_common();
77926
77928
  init_errors();
77927
77929
  init_paths();
77928
77930
  var heldBarrierContext = new AsyncLocalStorage;
77929
77931
  var MAINTENANCE_BARRIER_STALE_AFTER_MS = 5 * 60 * 1000;
77932
+ var MAINTENANCE_BARRIER_BUSY_RETRY_BOUND_MS = 1500;
77933
+ var busyRetryBoundMsForTests;
77930
77934
  function tryAcquireMaintenanceBarrier() {
77931
77935
  const lockPath = getMaintenanceBarrierPath();
77932
77936
  fs7.mkdirSync(path12.dirname(lockPath), { recursive: true });
@@ -77942,10 +77946,18 @@ function tryAcquireMaintenanceBarrier() {
77942
77946
  return;
77943
77947
  }
77944
77948
  function acquireMaintenanceBarrier() {
77945
- const release = tryAcquireMaintenanceBarrier();
77946
- if (release)
77947
- return release;
77948
- throw new ConfigError(`AKM maintenance is in progress (barrier ${getMaintenanceBarrierPath()}); retry after it completes. ` + `A sentinel older than ${MAINTENANCE_BARRIER_STALE_AFTER_MS / 60000} minute(s) is reclaimed automatically on the next attempt.`, "INVALID_CONFIG_FILE");
77949
+ const boundMs = busyRetryBoundMsForTests ?? MAINTENANCE_BARRIER_BUSY_RETRY_BOUND_MS;
77950
+ const deadline = Date.now() + boundMs;
77951
+ for (let attempt = 0;; attempt += 1) {
77952
+ const release = tryAcquireMaintenanceBarrier();
77953
+ if (release)
77954
+ return release;
77955
+ const remainingMs = deadline - Date.now();
77956
+ if (remainingMs <= 0)
77957
+ break;
77958
+ sleepSync(Math.min(backoffDelay(attempt), remainingMs));
77959
+ }
77960
+ throw new TransientError(`AKM maintenance is in progress (barrier ${getMaintenanceBarrierPath()}); retry shortly. ` + `A sentinel older than ${MAINTENANCE_BARRIER_STALE_AFTER_MS / 60000} minute(s) is reclaimed automatically on the next attempt.`, "MAINTENANCE_BARRIER_BUSY");
77949
77961
  }
77950
77962
  function withMaintenanceStartBarrier(run) {
77951
77963
  if (heldBarrierContext.getStore()?.active)
@@ -96987,7 +96999,7 @@ function defaultConcurrencyForEndpoint(endpoint) {
96987
96999
  // src/llm/embedders/remote.ts
96988
97000
  init_warn();
96989
97001
  var DEFAULT_REMOTE_BATCH_SIZE = 100;
96990
- var DEFAULT_TOKEN_BUDGET = 8000;
97002
+ var DEFAULT_TOKEN_BUDGET = 6000;
96991
97003
  function estimateTokenCount(text) {
96992
97004
  return Math.round(text.length / 4);
96993
97005
  }
@@ -97069,6 +97081,8 @@ function buildTokenBoundedBatches(texts, tokenBudget, maxCount) {
97069
97081
  flush();
97070
97082
  return batches;
97071
97083
  }
97084
+ var ADAPTIVE_BUDGET_SHRINK_FACTOR = 0.75;
97085
+ var ADAPTIVE_BUDGET_FLOOR_MULTIPLIER = 2;
97072
97086
 
97073
97087
  class RemoteEmbedder {
97074
97088
  config;
@@ -97121,10 +97135,42 @@ class RemoteEmbedder {
97121
97135
  const results = new Array(texts.length).fill(undefined);
97122
97136
  const headers = this.buildHeaders();
97123
97137
  const ollamaOpts = resolveOllamaOptions(this.config);
97124
- const tokenBudget = this.config.maxTokens ?? DEFAULT_TOKEN_BUDGET;
97138
+ let effectiveTokenBudget = this.config.maxTokens ?? DEFAULT_TOKEN_BUDGET;
97125
97139
  const maxCount = this.config.batchSize ?? DEFAULT_REMOTE_BATCH_SIZE;
97126
- const textBatches = buildTokenBoundedBatches(texts, tokenBudget, maxCount);
97140
+ const maxInputTokens = this.config.maxInputTokens ?? DEFAULT_MAX_INPUT_TOKENS;
97141
+ const textBatches = buildTokenBoundedBatches(texts, effectiveTokenBudget, maxCount);
97127
97142
  const configuredTimeoutMs = resolveEmbeddingTimeoutMs(this.config);
97143
+ let dispatchedBatchCount = 0;
97144
+ let budgetShrunk = false;
97145
+ const maybeShrinkBudget = (rejectedIndices, rejectedBatchIndex, rejectedRequestTokens) => {
97146
+ if (budgetShrunk)
97147
+ return;
97148
+ budgetShrunk = true;
97149
+ const floor2 = ADAPTIVE_BUDGET_FLOOR_MULTIPLIER * maxInputTokens;
97150
+ effectiveTokenBudget = Math.max(Math.round(effectiveTokenBudget * ADAPTIVE_BUDGET_SHRINK_FACTOR), floor2);
97151
+ const notYetDispatched = textBatches.slice(dispatchedBatchCount);
97152
+ const remainingIndices = notYetDispatched.flatMap((batch) => batch.indices);
97153
+ if (remainingIndices.length > 0) {
97154
+ const remainingTexts = remainingIndices.map((i) => texts[i]);
97155
+ const replanned = buildTokenBoundedBatches(remainingTexts, effectiveTokenBudget, maxCount).map((batch) => ({
97156
+ indices: batch.indices.map((localIndex) => remainingIndices[localIndex]),
97157
+ oversized: batch.oversized
97158
+ }));
97159
+ textBatches.splice(dispatchedBatchCount, textBatches.length - dispatchedBatchCount, ...replanned);
97160
+ }
97161
+ warnVerbose(`[embed] provider rejected a ${rejectedRequestTokens}-token request as over its context; request budget lowered to ${effectiveTokenBudget.toLocaleString()} for the rest of this run`);
97162
+ commitBatch(rejectedIndices, rejectedIndices.map(() => {
97163
+ return;
97164
+ }), undefined, {
97165
+ batchIndex: rejectedBatchIndex,
97166
+ batchCount: textBatches.length,
97167
+ docCount: rejectedIndices.length,
97168
+ requestTokens: rejectedRequestTokens,
97169
+ elapsedMs: 0,
97170
+ outcome: "budget-lowered",
97171
+ reason: `provider rejected ${rejectedRequestTokens.toLocaleString()} tokens as over its context; request budget lowered to ${effectiveTokenBudget.toLocaleString()} for the rest of this run`
97172
+ });
97173
+ };
97128
97174
  const dispatchAbort = new AbortController;
97129
97175
  const stopDispatch = (reason) => {
97130
97176
  if (!dispatchAbort.signal.aborted)
@@ -97157,7 +97203,7 @@ class RemoteEmbedder {
97157
97203
  return;
97158
97204
  const batch = indices.map((i) => texts[i]);
97159
97205
  const requestTokens = batch.reduce((sum, text) => sum + estimateTokenCount(text), 0);
97160
- const requestTimeoutMs = scaleEmbeddingTimeoutMs(configuredTimeoutMs, requestTokens, tokenBudget);
97206
+ const requestTimeoutMs = scaleEmbeddingTimeoutMs(configuredTimeoutMs, requestTokens, effectiveTokenBudget);
97161
97207
  const requestStart = Date.now();
97162
97208
  let batchEmbeddings;
97163
97209
  let responseModel;
@@ -97174,6 +97220,9 @@ class RemoteEmbedder {
97174
97220
  } catch (err) {
97175
97221
  if (signal?.aborted)
97176
97222
  throw err;
97223
+ if (err instanceof ContextExceededError) {
97224
+ maybeShrinkBudget(indices, batchIndex, requestTokens);
97225
+ }
97177
97226
  if (err instanceof ContextExceededError && indices.length > 1) {
97178
97227
  const mid = Math.ceil(indices.length / 2);
97179
97228
  await requestAndCommit(indices.slice(0, mid), batchIndex, false, timeoutAttempt);
@@ -97239,13 +97288,14 @@ class RemoteEmbedder {
97239
97288
  });
97240
97289
  };
97241
97290
  const runProviderBatch = async (textBatch, batchIndex) => {
97291
+ dispatchedBatchCount = batchIndex;
97242
97292
  if (textBatch.oversized) {
97243
97293
  const idx = textBatch.indices[0];
97244
97294
  const estTokens = estimateTokenCount(texts[idx]);
97245
97295
  onSkip?.({
97246
97296
  index: idx,
97247
97297
  reason: "context-window-exceeded",
97248
- message: `Document estimated at ${estTokens} tokens exceeds the ${tokenBudget}-token embedding budget; skipped.`,
97298
+ message: `Document estimated at ${estTokens} tokens exceeds the ${effectiveTokenBudget}-token embedding budget; skipped.`,
97249
97299
  batchStart: true,
97250
97300
  batchSize: 1
97251
97301
  });
@@ -97469,7 +97519,7 @@ function deriveObservedEmbeddingIdentity(embedding, observedModel, observedVecto
97469
97519
  }
97470
97520
  return `local:${embedding?.localModel ?? DEFAULT_LOCAL_MODEL}|${observedVectorLen}`;
97471
97521
  }
97472
- async function runEmbeddingCanary(db, config, signal) {
97522
+ async function runEmbeddingCanary(db, config, signal, maxInputTokens) {
97473
97523
  const samples = sampleEmbeddedEntriesForCanary(db, CANARY_SAMPLE_SIZE);
97474
97524
  if (samples.length === 0) {
97475
97525
  return { outcome: "keep", verified: false, viaIdentityMatch: false };
@@ -97478,7 +97528,7 @@ async function runEmbeddingCanary(db, config, signal) {
97478
97528
  const skips = [];
97479
97529
  let canaryVectors;
97480
97530
  try {
97481
- canaryVectors = await embedBatch(samples.map((sample) => sample.searchText), config.embedding, signal, (skip) => skips.push(skip), (_indices, _embeddings, model) => {
97531
+ canaryVectors = await embedBatch(samples.map((sample) => capEmbeddingText(sample.searchText, maxInputTokens).text), config.embedding, signal, (skip) => skips.push(skip), (_indices, _embeddings, model) => {
97482
97532
  if (model)
97483
97533
  observedModel = model;
97484
97534
  });
@@ -97548,6 +97598,7 @@ async function generateEmbeddingsForDb(db, config, onProgress, signal, entryIds,
97548
97598
  const storedFingerprint = getMeta(db, "embeddingFingerprint");
97549
97599
  let targetEntryIds = entryIds;
97550
97600
  let rebuildReason;
97601
+ const maxInputTokens = config.embedding?.maxInputTokens ?? DEFAULT_MAX_INPUT_TOKENS;
97551
97602
  if (opts?.forceReembed) {
97552
97603
  db.transaction(() => {
97553
97604
  purgeEmbeddings(db, { dropVecTable: true });
@@ -97559,7 +97610,7 @@ async function generateEmbeddingsForDb(db, config, onProgress, signal, entryIds,
97559
97610
  targetEntryIds = undefined;
97560
97611
  rebuildReason = "forced by --reembed";
97561
97612
  } else if (storedFingerprint && storedFingerprint !== currentFingerprint) {
97562
- const decision = await runEmbeddingCanary(db, config, signal);
97613
+ const decision = await runEmbeddingCanary(db, config, signal, maxInputTokens);
97563
97614
  if (decision.outcome === "unverifiable") {
97564
97615
  warn(`[embed] ${decision.message}`);
97565
97616
  onProgress({ phase: "embeddings", message: decision.message });
@@ -97623,7 +97674,6 @@ async function generateEmbeddingsForDb(db, config, onProgress, signal, entryIds,
97623
97674
  purgeEmbeddingSalvage(db);
97624
97675
  return reusedCount > 0 ? { success: true, vecInsertFailures: vecFailedCount } : { success: true };
97625
97676
  }
97626
- const maxInputTokens = config.embedding?.maxInputTokens ?? DEFAULT_MAX_INPUT_TOKENS;
97627
97677
  let truncatedCount = 0;
97628
97678
  const texts = [];
97629
97679
  const pendingEntries = [];
@@ -97718,6 +97768,15 @@ async function generateEmbeddingsForDb(db, config, onProgress, signal, entryIds,
97718
97768
  }
97719
97769
  return;
97720
97770
  }
97771
+ if (outcome?.outcome === "budget-lowered") {
97772
+ if (reportPerBatchLine) {
97773
+ onProgress({
97774
+ phase: "embeddings",
97775
+ message: `[embed] batch ${outcome.batchIndex}/${outcome.batchCount}: ${outcome.docCount} docs, ${outcome.requestTokens.toLocaleString()} tokens → ${outcome.reason}`
97776
+ });
97777
+ }
97778
+ return;
97779
+ }
97721
97780
  if (model)
97722
97781
  observedModel = model;
97723
97782
  if (batchEmbeddings.some((embedding) => embedding !== undefined)) {
@@ -97726,7 +97785,8 @@ async function generateEmbeddingsForDb(db, config, onProgress, signal, entryIds,
97726
97785
  }
97727
97786
  db.transaction(() => {
97728
97787
  for (let k2 = 0;k2 < indices.length; k2++) {
97729
- const entry = pendingEntries[indices[k2]];
97788
+ const index = indices[k2];
97789
+ const entry = pendingEntries[index];
97730
97790
  if (!entry)
97731
97791
  continue;
97732
97792
  const embedding = batchEmbeddings[k2];
@@ -97739,7 +97799,7 @@ async function generateEmbeddingsForDb(db, config, onProgress, signal, entryIds,
97739
97799
  const result = upsertEmbedding(db, entry.id, embedding);
97740
97800
  if (result.stored) {
97741
97801
  storedCount++;
97742
- storedTokens += estimateTokenCount(entry.searchText);
97802
+ storedTokens += estimateTokenCount(texts[index]);
97743
97803
  } else {
97744
97804
  skippedCount++;
97745
97805
  }
@@ -7069,7 +7069,9 @@ 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.",
7074
+ MAINTENANCE_BARRIER_BUSY: "Another akm process is registering a lock or lease right now. Retry shortly, or pass --skip-if-locked on scheduled index/improve/workflow runs."
7073
7075
  };
7074
7076
  NOT_FOUND_HINTS = {
7075
7077
  ASSET_NOT_FOUND: "Run `akm search <query>` or `akm index` to refresh the index.",
@@ -75248,7 +75250,6 @@ var EmbeddingConnectionConfigSchema = exports_external.object({
75248
75250
  maxInputTokens: positiveInt.optional(),
75249
75251
  maxTokens: positiveInt.optional(),
75250
75252
  batchSize: positiveInt.optional(),
75251
- chunkSize: positiveInt.optional(),
75252
75253
  contextLength: positiveInt.optional(),
75253
75254
  ollamaOptions: EmbeddingOllamaOptionsSchema.optional(),
75254
75255
  timeoutMs: positiveInt.optional(),
@@ -77251,10 +77252,13 @@ import { AsyncLocalStorage } from "async_hooks";
77251
77252
  import { randomUUID as randomUUID2 } from "crypto";
77252
77253
  import fs7 from "fs";
77253
77254
  import path12 from "path";
77255
+ init_common();
77254
77256
  init_errors();
77255
77257
  init_paths();
77256
77258
  var heldBarrierContext = new AsyncLocalStorage;
77257
77259
  var MAINTENANCE_BARRIER_STALE_AFTER_MS = 5 * 60 * 1000;
77260
+ var MAINTENANCE_BARRIER_BUSY_RETRY_BOUND_MS = 1500;
77261
+ var busyRetryBoundMsForTests;
77258
77262
  function tryAcquireMaintenanceBarrier() {
77259
77263
  const lockPath = getMaintenanceBarrierPath();
77260
77264
  fs7.mkdirSync(path12.dirname(lockPath), { recursive: true });
@@ -77270,10 +77274,18 @@ function tryAcquireMaintenanceBarrier() {
77270
77274
  return;
77271
77275
  }
77272
77276
  function acquireMaintenanceBarrier() {
77273
- const release = tryAcquireMaintenanceBarrier();
77274
- if (release)
77275
- return release;
77276
- throw new ConfigError(`AKM maintenance is in progress (barrier ${getMaintenanceBarrierPath()}); retry after it completes. ` + `A sentinel older than ${MAINTENANCE_BARRIER_STALE_AFTER_MS / 60000} minute(s) is reclaimed automatically on the next attempt.`, "INVALID_CONFIG_FILE");
77277
+ const boundMs = busyRetryBoundMsForTests ?? MAINTENANCE_BARRIER_BUSY_RETRY_BOUND_MS;
77278
+ const deadline = Date.now() + boundMs;
77279
+ for (let attempt = 0;; attempt += 1) {
77280
+ const release = tryAcquireMaintenanceBarrier();
77281
+ if (release)
77282
+ return release;
77283
+ const remainingMs = deadline - Date.now();
77284
+ if (remainingMs <= 0)
77285
+ break;
77286
+ sleepSync(Math.min(backoffDelay(attempt), remainingMs));
77287
+ }
77288
+ throw new TransientError(`AKM maintenance is in progress (barrier ${getMaintenanceBarrierPath()}); retry shortly. ` + `A sentinel older than ${MAINTENANCE_BARRIER_STALE_AFTER_MS / 60000} minute(s) is reclaimed automatically on the next attempt.`, "MAINTENANCE_BARRIER_BUSY");
77277
77289
  }
77278
77290
  function withMaintenanceStartBarrier(run) {
77279
77291
  if (heldBarrierContext.getStore()?.active)
@@ -96943,7 +96955,7 @@ function defaultConcurrencyForEndpoint(endpoint) {
96943
96955
  // src/llm/embedders/remote.ts
96944
96956
  init_warn();
96945
96957
  var DEFAULT_REMOTE_BATCH_SIZE = 100;
96946
- var DEFAULT_TOKEN_BUDGET = 8000;
96958
+ var DEFAULT_TOKEN_BUDGET = 6000;
96947
96959
  function estimateTokenCount(text) {
96948
96960
  return Math.round(text.length / 4);
96949
96961
  }
@@ -97025,6 +97037,8 @@ function buildTokenBoundedBatches(texts, tokenBudget, maxCount) {
97025
97037
  flush();
97026
97038
  return batches;
97027
97039
  }
97040
+ var ADAPTIVE_BUDGET_SHRINK_FACTOR = 0.75;
97041
+ var ADAPTIVE_BUDGET_FLOOR_MULTIPLIER = 2;
97028
97042
 
97029
97043
  class RemoteEmbedder {
97030
97044
  config;
@@ -97077,10 +97091,42 @@ class RemoteEmbedder {
97077
97091
  const results = new Array(texts.length).fill(undefined);
97078
97092
  const headers = this.buildHeaders();
97079
97093
  const ollamaOpts = resolveOllamaOptions(this.config);
97080
- const tokenBudget = this.config.maxTokens ?? DEFAULT_TOKEN_BUDGET;
97094
+ let effectiveTokenBudget = this.config.maxTokens ?? DEFAULT_TOKEN_BUDGET;
97081
97095
  const maxCount = this.config.batchSize ?? DEFAULT_REMOTE_BATCH_SIZE;
97082
- const textBatches = buildTokenBoundedBatches(texts, tokenBudget, maxCount);
97096
+ const maxInputTokens = this.config.maxInputTokens ?? DEFAULT_MAX_INPUT_TOKENS;
97097
+ const textBatches = buildTokenBoundedBatches(texts, effectiveTokenBudget, maxCount);
97083
97098
  const configuredTimeoutMs = resolveEmbeddingTimeoutMs(this.config);
97099
+ let dispatchedBatchCount = 0;
97100
+ let budgetShrunk = false;
97101
+ const maybeShrinkBudget = (rejectedIndices, rejectedBatchIndex, rejectedRequestTokens) => {
97102
+ if (budgetShrunk)
97103
+ return;
97104
+ budgetShrunk = true;
97105
+ const floor2 = ADAPTIVE_BUDGET_FLOOR_MULTIPLIER * maxInputTokens;
97106
+ effectiveTokenBudget = Math.max(Math.round(effectiveTokenBudget * ADAPTIVE_BUDGET_SHRINK_FACTOR), floor2);
97107
+ const notYetDispatched = textBatches.slice(dispatchedBatchCount);
97108
+ const remainingIndices = notYetDispatched.flatMap((batch) => batch.indices);
97109
+ if (remainingIndices.length > 0) {
97110
+ const remainingTexts = remainingIndices.map((i) => texts[i]);
97111
+ const replanned = buildTokenBoundedBatches(remainingTexts, effectiveTokenBudget, maxCount).map((batch) => ({
97112
+ indices: batch.indices.map((localIndex) => remainingIndices[localIndex]),
97113
+ oversized: batch.oversized
97114
+ }));
97115
+ textBatches.splice(dispatchedBatchCount, textBatches.length - dispatchedBatchCount, ...replanned);
97116
+ }
97117
+ warnVerbose(`[embed] provider rejected a ${rejectedRequestTokens}-token request as over its context; request budget lowered to ${effectiveTokenBudget.toLocaleString()} for the rest of this run`);
97118
+ commitBatch(rejectedIndices, rejectedIndices.map(() => {
97119
+ return;
97120
+ }), undefined, {
97121
+ batchIndex: rejectedBatchIndex,
97122
+ batchCount: textBatches.length,
97123
+ docCount: rejectedIndices.length,
97124
+ requestTokens: rejectedRequestTokens,
97125
+ elapsedMs: 0,
97126
+ outcome: "budget-lowered",
97127
+ reason: `provider rejected ${rejectedRequestTokens.toLocaleString()} tokens as over its context; request budget lowered to ${effectiveTokenBudget.toLocaleString()} for the rest of this run`
97128
+ });
97129
+ };
97084
97130
  const dispatchAbort = new AbortController;
97085
97131
  const stopDispatch = (reason) => {
97086
97132
  if (!dispatchAbort.signal.aborted)
@@ -97113,7 +97159,7 @@ class RemoteEmbedder {
97113
97159
  return;
97114
97160
  const batch = indices.map((i) => texts[i]);
97115
97161
  const requestTokens = batch.reduce((sum, text) => sum + estimateTokenCount(text), 0);
97116
- const requestTimeoutMs = scaleEmbeddingTimeoutMs(configuredTimeoutMs, requestTokens, tokenBudget);
97162
+ const requestTimeoutMs = scaleEmbeddingTimeoutMs(configuredTimeoutMs, requestTokens, effectiveTokenBudget);
97117
97163
  const requestStart = Date.now();
97118
97164
  let batchEmbeddings;
97119
97165
  let responseModel;
@@ -97130,6 +97176,9 @@ class RemoteEmbedder {
97130
97176
  } catch (err) {
97131
97177
  if (signal?.aborted)
97132
97178
  throw err;
97179
+ if (err instanceof ContextExceededError) {
97180
+ maybeShrinkBudget(indices, batchIndex, requestTokens);
97181
+ }
97133
97182
  if (err instanceof ContextExceededError && indices.length > 1) {
97134
97183
  const mid = Math.ceil(indices.length / 2);
97135
97184
  await requestAndCommit(indices.slice(0, mid), batchIndex, false, timeoutAttempt);
@@ -97195,13 +97244,14 @@ class RemoteEmbedder {
97195
97244
  });
97196
97245
  };
97197
97246
  const runProviderBatch = async (textBatch, batchIndex) => {
97247
+ dispatchedBatchCount = batchIndex;
97198
97248
  if (textBatch.oversized) {
97199
97249
  const idx = textBatch.indices[0];
97200
97250
  const estTokens = estimateTokenCount(texts[idx]);
97201
97251
  onSkip?.({
97202
97252
  index: idx,
97203
97253
  reason: "context-window-exceeded",
97204
- message: `Document estimated at ${estTokens} tokens exceeds the ${tokenBudget}-token embedding budget; skipped.`,
97254
+ message: `Document estimated at ${estTokens} tokens exceeds the ${effectiveTokenBudget}-token embedding budget; skipped.`,
97205
97255
  batchStart: true,
97206
97256
  batchSize: 1
97207
97257
  });
@@ -97425,7 +97475,7 @@ function deriveObservedEmbeddingIdentity(embedding, observedModel, observedVecto
97425
97475
  }
97426
97476
  return `local:${embedding?.localModel ?? DEFAULT_LOCAL_MODEL}|${observedVectorLen}`;
97427
97477
  }
97428
- async function runEmbeddingCanary(db, config, signal) {
97478
+ async function runEmbeddingCanary(db, config, signal, maxInputTokens) {
97429
97479
  const samples = sampleEmbeddedEntriesForCanary(db, CANARY_SAMPLE_SIZE);
97430
97480
  if (samples.length === 0) {
97431
97481
  return { outcome: "keep", verified: false, viaIdentityMatch: false };
@@ -97434,7 +97484,7 @@ async function runEmbeddingCanary(db, config, signal) {
97434
97484
  const skips = [];
97435
97485
  let canaryVectors;
97436
97486
  try {
97437
- canaryVectors = await embedBatch(samples.map((sample) => sample.searchText), config.embedding, signal, (skip) => skips.push(skip), (_indices, _embeddings, model) => {
97487
+ canaryVectors = await embedBatch(samples.map((sample) => capEmbeddingText(sample.searchText, maxInputTokens).text), config.embedding, signal, (skip) => skips.push(skip), (_indices, _embeddings, model) => {
97438
97488
  if (model)
97439
97489
  observedModel = model;
97440
97490
  });
@@ -97504,6 +97554,7 @@ async function generateEmbeddingsForDb(db, config, onProgress, signal, entryIds,
97504
97554
  const storedFingerprint = getMeta(db, "embeddingFingerprint");
97505
97555
  let targetEntryIds = entryIds;
97506
97556
  let rebuildReason;
97557
+ const maxInputTokens = config.embedding?.maxInputTokens ?? DEFAULT_MAX_INPUT_TOKENS;
97507
97558
  if (opts?.forceReembed) {
97508
97559
  db.transaction(() => {
97509
97560
  purgeEmbeddings(db, { dropVecTable: true });
@@ -97515,7 +97566,7 @@ async function generateEmbeddingsForDb(db, config, onProgress, signal, entryIds,
97515
97566
  targetEntryIds = undefined;
97516
97567
  rebuildReason = "forced by --reembed";
97517
97568
  } else if (storedFingerprint && storedFingerprint !== currentFingerprint) {
97518
- const decision = await runEmbeddingCanary(db, config, signal);
97569
+ const decision = await runEmbeddingCanary(db, config, signal, maxInputTokens);
97519
97570
  if (decision.outcome === "unverifiable") {
97520
97571
  warn(`[embed] ${decision.message}`);
97521
97572
  onProgress({ phase: "embeddings", message: decision.message });
@@ -97579,7 +97630,6 @@ async function generateEmbeddingsForDb(db, config, onProgress, signal, entryIds,
97579
97630
  purgeEmbeddingSalvage(db);
97580
97631
  return reusedCount > 0 ? { success: true, vecInsertFailures: vecFailedCount } : { success: true };
97581
97632
  }
97582
- const maxInputTokens = config.embedding?.maxInputTokens ?? DEFAULT_MAX_INPUT_TOKENS;
97583
97633
  let truncatedCount = 0;
97584
97634
  const texts = [];
97585
97635
  const pendingEntries = [];
@@ -97674,6 +97724,15 @@ async function generateEmbeddingsForDb(db, config, onProgress, signal, entryIds,
97674
97724
  }
97675
97725
  return;
97676
97726
  }
97727
+ if (outcome?.outcome === "budget-lowered") {
97728
+ if (reportPerBatchLine) {
97729
+ onProgress({
97730
+ phase: "embeddings",
97731
+ message: `[embed] batch ${outcome.batchIndex}/${outcome.batchCount}: ${outcome.docCount} docs, ${outcome.requestTokens.toLocaleString()} tokens \u2192 ${outcome.reason}`
97732
+ });
97733
+ }
97734
+ return;
97735
+ }
97677
97736
  if (model)
97678
97737
  observedModel = model;
97679
97738
  if (batchEmbeddings.some((embedding) => embedding !== undefined)) {
@@ -97682,7 +97741,8 @@ async function generateEmbeddingsForDb(db, config, onProgress, signal, entryIds,
97682
97741
  }
97683
97742
  db.transaction(() => {
97684
97743
  for (let k2 = 0;k2 < indices.length; k2++) {
97685
- const entry = pendingEntries[indices[k2]];
97744
+ const index = indices[k2];
97745
+ const entry = pendingEntries[index];
97686
97746
  if (!entry)
97687
97747
  continue;
97688
97748
  const embedding = batchEmbeddings[k2];
@@ -97695,7 +97755,7 @@ async function generateEmbeddingsForDb(db, config, onProgress, signal, entryIds,
97695
97755
  const result = upsertEmbedding(db, entry.id, embedding);
97696
97756
  if (result.stored) {
97697
97757
  storedCount++;
97698
- storedTokens += estimateTokenCount(entry.searchText);
97758
+ storedTokens += estimateTokenCount(texts[index]);
97699
97759
  } else {
97700
97760
  skippedCount++;
97701
97761
  }