akm-cli 0.9.15-beta.2 → 0.9.15-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +60 -1
- package/dist/cli.js +3 -3
- package/dist/commands/health/checks.js +11 -2
- package/dist/commands/health/scheduler-binary.js +120 -0
- package/dist/commands/health.js +9 -0
- package/dist/core/config/schema/embedding.js +2 -3
- package/dist/core/errors.js +1 -0
- package/dist/indexer/indexer.js +44 -3
- package/dist/indexer/materialize-embeddings.js +51 -9
- package/dist/llm/embedders/remote.js +106 -8
- package/dist/scripts/akm-migrate-node.js +61 -13
- package/dist/scripts/akm-migrate.js +61 -13
- package/docs/migration/release-notes/0.9.15.md +18 -0
- package/docs/reference/cli.md +27 -5
- package/docs/reference/configuration.md +21 -5
- package/package.json +1 -1
- package/schemas/akm-config.json +0 -12
|
@@ -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.
|
|
34
|
-
*
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
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,
|
|
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 ${
|
|
661
|
+
message: `Document estimated at ${estTokens} tokens exceeds the ${effectiveTokenBudget}-token embedding budget; skipped.`,
|
|
564
662
|
batchStart: true,
|
|
565
663
|
batchSize: 1,
|
|
566
664
|
});
|
|
@@ -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.",
|
|
@@ -75920,7 +75921,6 @@ var EmbeddingConnectionConfigSchema = exports_external.object({
|
|
|
75920
75921
|
maxInputTokens: positiveInt.optional(),
|
|
75921
75922
|
maxTokens: positiveInt.optional(),
|
|
75922
75923
|
batchSize: positiveInt.optional(),
|
|
75923
|
-
chunkSize: positiveInt.optional(),
|
|
75924
75924
|
contextLength: positiveInt.optional(),
|
|
75925
75925
|
ollamaOptions: EmbeddingOllamaOptionsSchema.optional(),
|
|
75926
75926
|
timeoutMs: positiveInt.optional(),
|
|
@@ -96987,7 +96987,7 @@ function defaultConcurrencyForEndpoint(endpoint) {
|
|
|
96987
96987
|
// src/llm/embedders/remote.ts
|
|
96988
96988
|
init_warn();
|
|
96989
96989
|
var DEFAULT_REMOTE_BATCH_SIZE = 100;
|
|
96990
|
-
var DEFAULT_TOKEN_BUDGET =
|
|
96990
|
+
var DEFAULT_TOKEN_BUDGET = 6000;
|
|
96991
96991
|
function estimateTokenCount(text) {
|
|
96992
96992
|
return Math.round(text.length / 4);
|
|
96993
96993
|
}
|
|
@@ -97069,6 +97069,8 @@ function buildTokenBoundedBatches(texts, tokenBudget, maxCount) {
|
|
|
97069
97069
|
flush();
|
|
97070
97070
|
return batches;
|
|
97071
97071
|
}
|
|
97072
|
+
var ADAPTIVE_BUDGET_SHRINK_FACTOR = 0.75;
|
|
97073
|
+
var ADAPTIVE_BUDGET_FLOOR_MULTIPLIER = 2;
|
|
97072
97074
|
|
|
97073
97075
|
class RemoteEmbedder {
|
|
97074
97076
|
config;
|
|
@@ -97121,10 +97123,42 @@ class RemoteEmbedder {
|
|
|
97121
97123
|
const results = new Array(texts.length).fill(undefined);
|
|
97122
97124
|
const headers = this.buildHeaders();
|
|
97123
97125
|
const ollamaOpts = resolveOllamaOptions(this.config);
|
|
97124
|
-
|
|
97126
|
+
let effectiveTokenBudget = this.config.maxTokens ?? DEFAULT_TOKEN_BUDGET;
|
|
97125
97127
|
const maxCount = this.config.batchSize ?? DEFAULT_REMOTE_BATCH_SIZE;
|
|
97126
|
-
const
|
|
97128
|
+
const maxInputTokens = this.config.maxInputTokens ?? DEFAULT_MAX_INPUT_TOKENS;
|
|
97129
|
+
const textBatches = buildTokenBoundedBatches(texts, effectiveTokenBudget, maxCount);
|
|
97127
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
|
+
};
|
|
97128
97162
|
const dispatchAbort = new AbortController;
|
|
97129
97163
|
const stopDispatch = (reason) => {
|
|
97130
97164
|
if (!dispatchAbort.signal.aborted)
|
|
@@ -97157,7 +97191,7 @@ class RemoteEmbedder {
|
|
|
97157
97191
|
return;
|
|
97158
97192
|
const batch = indices.map((i) => texts[i]);
|
|
97159
97193
|
const requestTokens = batch.reduce((sum, text) => sum + estimateTokenCount(text), 0);
|
|
97160
|
-
const requestTimeoutMs = scaleEmbeddingTimeoutMs(configuredTimeoutMs, requestTokens,
|
|
97194
|
+
const requestTimeoutMs = scaleEmbeddingTimeoutMs(configuredTimeoutMs, requestTokens, effectiveTokenBudget);
|
|
97161
97195
|
const requestStart = Date.now();
|
|
97162
97196
|
let batchEmbeddings;
|
|
97163
97197
|
let responseModel;
|
|
@@ -97174,6 +97208,9 @@ class RemoteEmbedder {
|
|
|
97174
97208
|
} catch (err) {
|
|
97175
97209
|
if (signal?.aborted)
|
|
97176
97210
|
throw err;
|
|
97211
|
+
if (err instanceof ContextExceededError) {
|
|
97212
|
+
maybeShrinkBudget(indices, batchIndex, requestTokens);
|
|
97213
|
+
}
|
|
97177
97214
|
if (err instanceof ContextExceededError && indices.length > 1) {
|
|
97178
97215
|
const mid = Math.ceil(indices.length / 2);
|
|
97179
97216
|
await requestAndCommit(indices.slice(0, mid), batchIndex, false, timeoutAttempt);
|
|
@@ -97239,13 +97276,14 @@ class RemoteEmbedder {
|
|
|
97239
97276
|
});
|
|
97240
97277
|
};
|
|
97241
97278
|
const runProviderBatch = async (textBatch, batchIndex) => {
|
|
97279
|
+
dispatchedBatchCount = batchIndex;
|
|
97242
97280
|
if (textBatch.oversized) {
|
|
97243
97281
|
const idx = textBatch.indices[0];
|
|
97244
97282
|
const estTokens = estimateTokenCount(texts[idx]);
|
|
97245
97283
|
onSkip?.({
|
|
97246
97284
|
index: idx,
|
|
97247
97285
|
reason: "context-window-exceeded",
|
|
97248
|
-
message: `Document estimated at ${estTokens} tokens exceeds the ${
|
|
97286
|
+
message: `Document estimated at ${estTokens} tokens exceeds the ${effectiveTokenBudget}-token embedding budget; skipped.`,
|
|
97249
97287
|
batchStart: true,
|
|
97250
97288
|
batchSize: 1
|
|
97251
97289
|
});
|
|
@@ -97469,7 +97507,7 @@ function deriveObservedEmbeddingIdentity(embedding, observedModel, observedVecto
|
|
|
97469
97507
|
}
|
|
97470
97508
|
return `local:${embedding?.localModel ?? DEFAULT_LOCAL_MODEL}|${observedVectorLen}`;
|
|
97471
97509
|
}
|
|
97472
|
-
async function runEmbeddingCanary(db, config, signal) {
|
|
97510
|
+
async function runEmbeddingCanary(db, config, signal, maxInputTokens) {
|
|
97473
97511
|
const samples = sampleEmbeddedEntriesForCanary(db, CANARY_SAMPLE_SIZE);
|
|
97474
97512
|
if (samples.length === 0) {
|
|
97475
97513
|
return { outcome: "keep", verified: false, viaIdentityMatch: false };
|
|
@@ -97478,7 +97516,7 @@ async function runEmbeddingCanary(db, config, signal) {
|
|
|
97478
97516
|
const skips = [];
|
|
97479
97517
|
let canaryVectors;
|
|
97480
97518
|
try {
|
|
97481
|
-
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) => {
|
|
97482
97520
|
if (model)
|
|
97483
97521
|
observedModel = model;
|
|
97484
97522
|
});
|
|
@@ -97548,6 +97586,7 @@ async function generateEmbeddingsForDb(db, config, onProgress, signal, entryIds,
|
|
|
97548
97586
|
const storedFingerprint = getMeta(db, "embeddingFingerprint");
|
|
97549
97587
|
let targetEntryIds = entryIds;
|
|
97550
97588
|
let rebuildReason;
|
|
97589
|
+
const maxInputTokens = config.embedding?.maxInputTokens ?? DEFAULT_MAX_INPUT_TOKENS;
|
|
97551
97590
|
if (opts?.forceReembed) {
|
|
97552
97591
|
db.transaction(() => {
|
|
97553
97592
|
purgeEmbeddings(db, { dropVecTable: true });
|
|
@@ -97559,7 +97598,7 @@ async function generateEmbeddingsForDb(db, config, onProgress, signal, entryIds,
|
|
|
97559
97598
|
targetEntryIds = undefined;
|
|
97560
97599
|
rebuildReason = "forced by --reembed";
|
|
97561
97600
|
} else if (storedFingerprint && storedFingerprint !== currentFingerprint) {
|
|
97562
|
-
const decision = await runEmbeddingCanary(db, config, signal);
|
|
97601
|
+
const decision = await runEmbeddingCanary(db, config, signal, maxInputTokens);
|
|
97563
97602
|
if (decision.outcome === "unverifiable") {
|
|
97564
97603
|
warn(`[embed] ${decision.message}`);
|
|
97565
97604
|
onProgress({ phase: "embeddings", message: decision.message });
|
|
@@ -97623,7 +97662,6 @@ async function generateEmbeddingsForDb(db, config, onProgress, signal, entryIds,
|
|
|
97623
97662
|
purgeEmbeddingSalvage(db);
|
|
97624
97663
|
return reusedCount > 0 ? { success: true, vecInsertFailures: vecFailedCount } : { success: true };
|
|
97625
97664
|
}
|
|
97626
|
-
const maxInputTokens = config.embedding?.maxInputTokens ?? DEFAULT_MAX_INPUT_TOKENS;
|
|
97627
97665
|
let truncatedCount = 0;
|
|
97628
97666
|
const texts = [];
|
|
97629
97667
|
const pendingEntries = [];
|
|
@@ -97718,6 +97756,15 @@ async function generateEmbeddingsForDb(db, config, onProgress, signal, entryIds,
|
|
|
97718
97756
|
}
|
|
97719
97757
|
return;
|
|
97720
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
|
+
}
|
|
97721
97768
|
if (model)
|
|
97722
97769
|
observedModel = model;
|
|
97723
97770
|
if (batchEmbeddings.some((embedding) => embedding !== undefined)) {
|
|
@@ -97726,7 +97773,8 @@ async function generateEmbeddingsForDb(db, config, onProgress, signal, entryIds,
|
|
|
97726
97773
|
}
|
|
97727
97774
|
db.transaction(() => {
|
|
97728
97775
|
for (let k2 = 0;k2 < indices.length; k2++) {
|
|
97729
|
-
const
|
|
97776
|
+
const index = indices[k2];
|
|
97777
|
+
const entry = pendingEntries[index];
|
|
97730
97778
|
if (!entry)
|
|
97731
97779
|
continue;
|
|
97732
97780
|
const embedding = batchEmbeddings[k2];
|
|
@@ -97739,7 +97787,7 @@ async function generateEmbeddingsForDb(db, config, onProgress, signal, entryIds,
|
|
|
97739
97787
|
const result = upsertEmbedding(db, entry.id, embedding);
|
|
97740
97788
|
if (result.stored) {
|
|
97741
97789
|
storedCount++;
|
|
97742
|
-
storedTokens += estimateTokenCount(
|
|
97790
|
+
storedTokens += estimateTokenCount(texts[index]);
|
|
97743
97791
|
} else {
|
|
97744
97792
|
skippedCount++;
|
|
97745
97793
|
}
|
|
@@ -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.",
|
|
@@ -75248,7 +75249,6 @@ var EmbeddingConnectionConfigSchema = exports_external.object({
|
|
|
75248
75249
|
maxInputTokens: positiveInt.optional(),
|
|
75249
75250
|
maxTokens: positiveInt.optional(),
|
|
75250
75251
|
batchSize: positiveInt.optional(),
|
|
75251
|
-
chunkSize: positiveInt.optional(),
|
|
75252
75252
|
contextLength: positiveInt.optional(),
|
|
75253
75253
|
ollamaOptions: EmbeddingOllamaOptionsSchema.optional(),
|
|
75254
75254
|
timeoutMs: positiveInt.optional(),
|
|
@@ -96943,7 +96943,7 @@ function defaultConcurrencyForEndpoint(endpoint) {
|
|
|
96943
96943
|
// src/llm/embedders/remote.ts
|
|
96944
96944
|
init_warn();
|
|
96945
96945
|
var DEFAULT_REMOTE_BATCH_SIZE = 100;
|
|
96946
|
-
var DEFAULT_TOKEN_BUDGET =
|
|
96946
|
+
var DEFAULT_TOKEN_BUDGET = 6000;
|
|
96947
96947
|
function estimateTokenCount(text) {
|
|
96948
96948
|
return Math.round(text.length / 4);
|
|
96949
96949
|
}
|
|
@@ -97025,6 +97025,8 @@ function buildTokenBoundedBatches(texts, tokenBudget, maxCount) {
|
|
|
97025
97025
|
flush();
|
|
97026
97026
|
return batches;
|
|
97027
97027
|
}
|
|
97028
|
+
var ADAPTIVE_BUDGET_SHRINK_FACTOR = 0.75;
|
|
97029
|
+
var ADAPTIVE_BUDGET_FLOOR_MULTIPLIER = 2;
|
|
97028
97030
|
|
|
97029
97031
|
class RemoteEmbedder {
|
|
97030
97032
|
config;
|
|
@@ -97077,10 +97079,42 @@ class RemoteEmbedder {
|
|
|
97077
97079
|
const results = new Array(texts.length).fill(undefined);
|
|
97078
97080
|
const headers = this.buildHeaders();
|
|
97079
97081
|
const ollamaOpts = resolveOllamaOptions(this.config);
|
|
97080
|
-
|
|
97082
|
+
let effectiveTokenBudget = this.config.maxTokens ?? DEFAULT_TOKEN_BUDGET;
|
|
97081
97083
|
const maxCount = this.config.batchSize ?? DEFAULT_REMOTE_BATCH_SIZE;
|
|
97082
|
-
const
|
|
97084
|
+
const maxInputTokens = this.config.maxInputTokens ?? DEFAULT_MAX_INPUT_TOKENS;
|
|
97085
|
+
const textBatches = buildTokenBoundedBatches(texts, effectiveTokenBudget, maxCount);
|
|
97083
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
|
+
};
|
|
97084
97118
|
const dispatchAbort = new AbortController;
|
|
97085
97119
|
const stopDispatch = (reason) => {
|
|
97086
97120
|
if (!dispatchAbort.signal.aborted)
|
|
@@ -97113,7 +97147,7 @@ class RemoteEmbedder {
|
|
|
97113
97147
|
return;
|
|
97114
97148
|
const batch = indices.map((i) => texts[i]);
|
|
97115
97149
|
const requestTokens = batch.reduce((sum, text) => sum + estimateTokenCount(text), 0);
|
|
97116
|
-
const requestTimeoutMs = scaleEmbeddingTimeoutMs(configuredTimeoutMs, requestTokens,
|
|
97150
|
+
const requestTimeoutMs = scaleEmbeddingTimeoutMs(configuredTimeoutMs, requestTokens, effectiveTokenBudget);
|
|
97117
97151
|
const requestStart = Date.now();
|
|
97118
97152
|
let batchEmbeddings;
|
|
97119
97153
|
let responseModel;
|
|
@@ -97130,6 +97164,9 @@ class RemoteEmbedder {
|
|
|
97130
97164
|
} catch (err) {
|
|
97131
97165
|
if (signal?.aborted)
|
|
97132
97166
|
throw err;
|
|
97167
|
+
if (err instanceof ContextExceededError) {
|
|
97168
|
+
maybeShrinkBudget(indices, batchIndex, requestTokens);
|
|
97169
|
+
}
|
|
97133
97170
|
if (err instanceof ContextExceededError && indices.length > 1) {
|
|
97134
97171
|
const mid = Math.ceil(indices.length / 2);
|
|
97135
97172
|
await requestAndCommit(indices.slice(0, mid), batchIndex, false, timeoutAttempt);
|
|
@@ -97195,13 +97232,14 @@ class RemoteEmbedder {
|
|
|
97195
97232
|
});
|
|
97196
97233
|
};
|
|
97197
97234
|
const runProviderBatch = async (textBatch, batchIndex) => {
|
|
97235
|
+
dispatchedBatchCount = batchIndex;
|
|
97198
97236
|
if (textBatch.oversized) {
|
|
97199
97237
|
const idx = textBatch.indices[0];
|
|
97200
97238
|
const estTokens = estimateTokenCount(texts[idx]);
|
|
97201
97239
|
onSkip?.({
|
|
97202
97240
|
index: idx,
|
|
97203
97241
|
reason: "context-window-exceeded",
|
|
97204
|
-
message: `Document estimated at ${estTokens} tokens exceeds the ${
|
|
97242
|
+
message: `Document estimated at ${estTokens} tokens exceeds the ${effectiveTokenBudget}-token embedding budget; skipped.`,
|
|
97205
97243
|
batchStart: true,
|
|
97206
97244
|
batchSize: 1
|
|
97207
97245
|
});
|
|
@@ -97425,7 +97463,7 @@ function deriveObservedEmbeddingIdentity(embedding, observedModel, observedVecto
|
|
|
97425
97463
|
}
|
|
97426
97464
|
return `local:${embedding?.localModel ?? DEFAULT_LOCAL_MODEL}|${observedVectorLen}`;
|
|
97427
97465
|
}
|
|
97428
|
-
async function runEmbeddingCanary(db, config, signal) {
|
|
97466
|
+
async function runEmbeddingCanary(db, config, signal, maxInputTokens) {
|
|
97429
97467
|
const samples = sampleEmbeddedEntriesForCanary(db, CANARY_SAMPLE_SIZE);
|
|
97430
97468
|
if (samples.length === 0) {
|
|
97431
97469
|
return { outcome: "keep", verified: false, viaIdentityMatch: false };
|
|
@@ -97434,7 +97472,7 @@ async function runEmbeddingCanary(db, config, signal) {
|
|
|
97434
97472
|
const skips = [];
|
|
97435
97473
|
let canaryVectors;
|
|
97436
97474
|
try {
|
|
97437
|
-
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) => {
|
|
97438
97476
|
if (model)
|
|
97439
97477
|
observedModel = model;
|
|
97440
97478
|
});
|
|
@@ -97504,6 +97542,7 @@ async function generateEmbeddingsForDb(db, config, onProgress, signal, entryIds,
|
|
|
97504
97542
|
const storedFingerprint = getMeta(db, "embeddingFingerprint");
|
|
97505
97543
|
let targetEntryIds = entryIds;
|
|
97506
97544
|
let rebuildReason;
|
|
97545
|
+
const maxInputTokens = config.embedding?.maxInputTokens ?? DEFAULT_MAX_INPUT_TOKENS;
|
|
97507
97546
|
if (opts?.forceReembed) {
|
|
97508
97547
|
db.transaction(() => {
|
|
97509
97548
|
purgeEmbeddings(db, { dropVecTable: true });
|
|
@@ -97515,7 +97554,7 @@ async function generateEmbeddingsForDb(db, config, onProgress, signal, entryIds,
|
|
|
97515
97554
|
targetEntryIds = undefined;
|
|
97516
97555
|
rebuildReason = "forced by --reembed";
|
|
97517
97556
|
} else if (storedFingerprint && storedFingerprint !== currentFingerprint) {
|
|
97518
|
-
const decision = await runEmbeddingCanary(db, config, signal);
|
|
97557
|
+
const decision = await runEmbeddingCanary(db, config, signal, maxInputTokens);
|
|
97519
97558
|
if (decision.outcome === "unverifiable") {
|
|
97520
97559
|
warn(`[embed] ${decision.message}`);
|
|
97521
97560
|
onProgress({ phase: "embeddings", message: decision.message });
|
|
@@ -97579,7 +97618,6 @@ async function generateEmbeddingsForDb(db, config, onProgress, signal, entryIds,
|
|
|
97579
97618
|
purgeEmbeddingSalvage(db);
|
|
97580
97619
|
return reusedCount > 0 ? { success: true, vecInsertFailures: vecFailedCount } : { success: true };
|
|
97581
97620
|
}
|
|
97582
|
-
const maxInputTokens = config.embedding?.maxInputTokens ?? DEFAULT_MAX_INPUT_TOKENS;
|
|
97583
97621
|
let truncatedCount = 0;
|
|
97584
97622
|
const texts = [];
|
|
97585
97623
|
const pendingEntries = [];
|
|
@@ -97674,6 +97712,15 @@ async function generateEmbeddingsForDb(db, config, onProgress, signal, entryIds,
|
|
|
97674
97712
|
}
|
|
97675
97713
|
return;
|
|
97676
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
|
+
}
|
|
97677
97724
|
if (model)
|
|
97678
97725
|
observedModel = model;
|
|
97679
97726
|
if (batchEmbeddings.some((embedding) => embedding !== undefined)) {
|
|
@@ -97682,7 +97729,8 @@ async function generateEmbeddingsForDb(db, config, onProgress, signal, entryIds,
|
|
|
97682
97729
|
}
|
|
97683
97730
|
db.transaction(() => {
|
|
97684
97731
|
for (let k2 = 0;k2 < indices.length; k2++) {
|
|
97685
|
-
const
|
|
97732
|
+
const index = indices[k2];
|
|
97733
|
+
const entry = pendingEntries[index];
|
|
97686
97734
|
if (!entry)
|
|
97687
97735
|
continue;
|
|
97688
97736
|
const embedding = batchEmbeddings[k2];
|
|
@@ -97695,7 +97743,7 @@ async function generateEmbeddingsForDb(db, config, onProgress, signal, entryIds,
|
|
|
97695
97743
|
const result = upsertEmbedding(db, entry.id, embedding);
|
|
97696
97744
|
if (result.stored) {
|
|
97697
97745
|
storedCount++;
|
|
97698
|
-
storedTokens += estimateTokenCount(
|
|
97746
|
+
storedTokens += estimateTokenCount(texts[index]);
|
|
97699
97747
|
} else {
|
|
97700
97748
|
skippedCount++;
|
|
97701
97749
|
}
|
|
@@ -38,6 +38,17 @@ scheduled or opportunistic run step aside instead of contending with a rebuild
|
|
|
38
38
|
already in progress; the shipped `index-refresh` scheduled task already passes
|
|
39
39
|
it.
|
|
40
40
|
|
|
41
|
+
`embedding.maxTokens`'s default (the per-request token budget) is now 6000,
|
|
42
|
+
down from 8000: a field report on an 8192-token llama.cpp embedder showed the
|
|
43
|
+
4-chars-per-token estimator undercounts dense technical text by 7-55%, so the
|
|
44
|
+
old default regularly overshot the endpoint's real context window. If you
|
|
45
|
+
already set `embedding.maxTokens` explicitly, this default change does not
|
|
46
|
+
affect you — your configured value is unchanged. `akm index` also now
|
|
47
|
+
recovers automatically within a run: on the first request rejected for
|
|
48
|
+
exceeding the endpoint's context window, it lowers its effective budget for
|
|
49
|
+
the rest of that run (reported with one line) rather than continuing to hit
|
|
50
|
+
the same wall on every following batch.
|
|
51
|
+
|
|
41
52
|
`embedding.concurrency` (positive integer, 1-16) overrides the number of
|
|
42
53
|
embedding requests kept in flight at once, which otherwise defaults to 1 for
|
|
43
54
|
a loopback endpoint and 2 for a remote one. Set it only for an endpoint that
|
|
@@ -131,3 +142,10 @@ effective config and another config file or bundle-relative file. `akm config
|
|
|
131
142
|
unset` now refuses to unset a key whose value comes only from an
|
|
132
143
|
`extends`-inherited base, naming the source, since there would be nothing local
|
|
133
144
|
to remove.
|
|
145
|
+
|
|
146
|
+
The scheduler runs the binary path `akm task sync` recorded at sync time, not
|
|
147
|
+
whichever akm your shell now resolves to. After upgrading akm through a
|
|
148
|
+
different installer than the one active at your last `task sync` (e.g.
|
|
149
|
+
npm-global to a standalone download), run `akm task sync` again so the
|
|
150
|
+
schedule points at the new binary; `akm health --probe` now warns via a new
|
|
151
|
+
`scheduler-binary` advisory when the two diverge.
|
package/docs/reference/cli.md
CHANGED
|
@@ -117,7 +117,7 @@ Every command exits with one of the following codes:
|
|
|
117
117
|
| 2 | Usage / bad input | `UsageError` |
|
|
118
118
|
| 4 | Health warning (`akm health` only) | — |
|
|
119
119
|
| 70 | Internal / unclassified error | unexpected throw |
|
|
120
|
-
| 75 | Transient — retry shortly (sysexits `EX_TEMPFAIL`); another akm process holds a lock or is writing `state.db` right now, not a bad command line | `TransientError` |
|
|
120
|
+
| 75 | Transient — retry shortly (sysexits `EX_TEMPFAIL`); another akm process holds a lock or is writing `state.db` or `index.db` right now, not a bad command line | `TransientError` |
|
|
121
121
|
| 78 | Configuration error | `ConfigError` |
|
|
122
122
|
|
|
123
123
|
Failures classified by akm emit a JSON error envelope on **stderr** before
|
|
@@ -279,7 +279,12 @@ opt-in, PID-liveness-only rebuild lock and releases it on exit — this is
|
|
|
279
279
|
advisory, never the blocking lock #872 removed (see
|
|
280
280
|
[Locks](https://github.com/itlackey/akm/blob/main/docs/architecture/internals/indexing.md#locks)). A human-typed
|
|
281
281
|
`akm index` with no flag is never gated by it: if another run already holds
|
|
282
|
-
the lock, it warns and proceeds anyway, contending with the existing run.
|
|
282
|
+
the lock, it warns and proceeds anyway, contending with the existing run. If
|
|
283
|
+
that contention makes index.db genuinely busy (SQLite `database is locked`)
|
|
284
|
+
long enough to exhaust the driver's retry window, the run now fails with
|
|
285
|
+
exit 75 (`TransientError`, code `INDEX_DB_CONTENDED`) instead of the raw
|
|
286
|
+
driver error at exit 70 — the same retry-shortly contract as
|
|
287
|
+
`STATE_DB_CONTENDED`, so a scheduler can branch on it instead of alerting.
|
|
283
288
|
`--skip-if-locked` changes that only for the invocation that passes it: if
|
|
284
289
|
the lock is already held by a live process, it skips gracefully (exit 0,
|
|
285
290
|
`{ ok: true, skipped: { reason: "lock-held", pid, launcherPid, startedAt } }`
|
|
@@ -358,15 +363,17 @@ akm health --report --window-compare 7d --format html
|
|
|
358
363
|
| `--window-compare` | Compare the current window against the prior window of the same duration (e.g. `24h`, `7d`). With `--report`, overrides the default trend window. |
|
|
359
364
|
| `--group-by` | Group rows by `run` (one row per `improve_runs` entry). Omit for the default summary. |
|
|
360
365
|
| `--windows` | Explicit comparison window(s) as `name=...,since=ISO,until=ISO` (repeatable, up to 4). Mutually exclusive with `--window-compare`. |
|
|
361
|
-
| `--no-probe` | Skip the `default-llm-engine` / `configured-engines` reachability probes
|
|
366
|
+
| `--no-probe` | Skip the `default-llm-engine` / `configured-engines` reachability probes, the `cli-version` update check, and the `scheduler-binary` version check (for an offline or air-gapped host). |
|
|
362
367
|
|
|
363
368
|
The command reads `state.db`, verifies that the required tables exist, performs a
|
|
364
369
|
write-read probe against the events stream, inspects `task_history`, checks the
|
|
365
370
|
default agent engine, and summarizes recent `improve_*` events. Unless
|
|
366
371
|
`--no-probe` is given, it also sends a bounded (3s timeout) reachability probe
|
|
367
372
|
to the `default-llm-engine` and every `configured-engines` LLM connection (and
|
|
368
|
-
an SDK engine's LLM fallback), one probe per distinct endpoint,
|
|
369
|
-
installed akm-cli version against the latest GitHub release (`cli-version`)
|
|
373
|
+
an SDK engine's LLM fallback), one probe per distinct endpoint, checks the
|
|
374
|
+
installed akm-cli version against the latest GitHub release (`cli-version`),
|
|
375
|
+
and runs the scheduler's recorded akm binary with `--version` to check it
|
|
376
|
+
against the running CLI (`scheduler-binary`).
|
|
370
377
|
|
|
371
378
|
Primary result fields:
|
|
372
379
|
|
|
@@ -1239,6 +1246,14 @@ Shipping akm inside your own product (a Docker image, a plugin's own
|
|
|
1239
1246
|
`node_modules`)? See [Bundling akm](../integration/bundling-akm.md) for the
|
|
1240
1247
|
full boot contract, JSON shapes, and exit codes.
|
|
1241
1248
|
|
|
1249
|
+
`akm upgrade` replaces the binary in place for its own install method, but a
|
|
1250
|
+
scheduler binding recorded by an earlier `akm task sync` under a *different*
|
|
1251
|
+
install method is not repointed automatically — the scheduler runs the
|
|
1252
|
+
binary path recorded at sync time, not whichever akm `upgrade` just
|
|
1253
|
+
installed. Run `akm task sync` after switching installers so scheduled runs
|
|
1254
|
+
pick up the new binary; see [`task sync`](#task) and `akm health`'s
|
|
1255
|
+
`scheduler-binary` advisory.
|
|
1256
|
+
|
|
1242
1257
|
### clone
|
|
1243
1258
|
|
|
1244
1259
|
Copy an asset from any source into a managed writable bundle or an unmanaged
|
|
@@ -2872,6 +2887,13 @@ task template (both the core set and the improve-schedule set) and asks once
|
|
|
2872
2887
|
before changing task files or scheduler state; non-interactive setup changes
|
|
2873
2888
|
neither.
|
|
2874
2889
|
|
|
2890
|
+
Because the scheduler runs the exact binary path recorded at the last `task
|
|
2891
|
+
sync`, upgrading akm through a different installer than the one active at
|
|
2892
|
+
that sync (npm-global to a standalone download, or vice versa) leaves
|
|
2893
|
+
scheduled runs invoking the old, now-stale binary — `task sync` re-resolves
|
|
2894
|
+
the current path and repoints them. `akm health --probe`'s `scheduler-binary`
|
|
2895
|
+
advisory warns when the two diverge, naming both versions.
|
|
2896
|
+
|
|
2875
2897
|
Setup reconfiguration preserves existing scheduler runtime bindings. Changing
|
|
2876
2898
|
the AKM storage path or installed runtime path therefore requires an explicit
|
|
2877
2899
|
`akm task sync --rebind`; setup does not silently migrate those entries.
|