akm-cli 0.9.15 → 0.9.16-alpha.1
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 +144 -0
- package/dist/assets/tasks/core/index-refresh.yml +1 -1
- package/dist/cli/retired-commands.js +2 -0
- package/dist/cli/unknown-flags.js +36 -3
- package/dist/commands/improve/collapse-detector.js +2 -2
- package/dist/commands/improve/consolidate.js +6 -4
- package/dist/commands/improve/improve-cli.js +1 -1
- package/dist/commands/proposal/repository.js +12 -3
- package/dist/commands/read/curate.js +34 -44
- package/dist/commands/read/search.js +50 -2
- package/dist/commands/sources/index-status.js +99 -0
- package/dist/commands/sources/info.js +8 -8
- package/dist/commands/sources/installed-stashes.js +33 -12
- package/dist/commands/sources/source-add.js +21 -6
- package/dist/commands/sources/stash-cli.js +119 -111
- package/dist/core/adapter/adapters/akm-adapter.js +35 -3
- package/dist/core/adapter/adapters/akm-metadata.js +11 -1
- package/dist/core/asset/asset-placement.js +35 -0
- package/dist/core/config/schema/embedding.js +7 -30
- package/dist/core/config/schema/search.js +11 -9
- package/dist/core/errors.js +5 -2
- package/dist/core/hash.js +18 -0
- package/dist/core/maintenance-barrier.js +8 -6
- package/dist/core/paths.js +0 -11
- package/dist/core/run-lock.js +5 -2
- package/dist/core/state/migrations.js +26 -1
- package/dist/core/state-db.js +63 -27
- package/dist/indexer/drain.js +306 -0
- package/dist/indexer/embedding-identity.js +20 -0
- package/dist/indexer/enrich.js +260 -0
- package/dist/indexer/ensure-index.js +5 -0
- package/dist/indexer/index-written-assets.js +133 -171
- package/dist/indexer/indexer.js +458 -1621
- package/dist/indexer/lookup/adapter-concept-owner.js +19 -5
- package/dist/indexer/passes/metadata.js +18 -1
- package/dist/indexer/reconcile.js +890 -0
- package/dist/indexer/scan/drain-dir.js +27 -70
- package/dist/indexer/scan/parse-file.js +66 -0
- package/dist/indexer/search/db-search.js +373 -89
- package/dist/indexer/search/ranking-contributors.js +21 -16
- package/dist/indexer/search/ranking.js +135 -57
- package/dist/indexer/units/unit.js +159 -0
- package/dist/llm/client.js +10 -1
- package/dist/llm/embedder.js +10 -3
- package/dist/llm/embedders/provider-limits.js +288 -0
- package/dist/llm/embedders/remote.js +133 -104
- package/dist/llm/feature-gate.js +4 -2
- package/dist/llm/rerank-client.js +3 -3
- package/dist/output/shapes/passthrough.js +1 -0
- package/dist/output/text/command-format.js +19 -13
- package/dist/output/text/helpers.js +1 -1
- package/dist/output/text/index.js +5 -2
- package/dist/scripts/akm-migrate-node.js +1141 -1237
- package/dist/scripts/akm-migrate.js +1141 -1237
- package/dist/setup/semantic-assets.js +2 -2
- package/dist/setup/steps/connection.js +3 -2
- package/dist/storage/repositories/files-repository.js +181 -0
- package/dist/storage/repositories/index-connection.js +1 -3
- package/dist/storage/repositories/index-entries-repository.js +77 -68
- package/dist/storage/repositories/index-entry-schema.js +16 -25
- package/dist/storage/repositories/index-fts-repository.js +29 -263
- package/dist/storage/repositories/index-meta-repository.js +0 -29
- package/dist/storage/repositories/index-schema.js +115 -122
- package/dist/storage/repositories/index-utility-repository.js +1 -1
- package/dist/storage/repositories/index-vec-repository.js +21 -334
- package/dist/storage/repositories/units-repository.js +510 -0
- package/docs/migration/release-notes/0.9.15.md +34 -36
- package/docs/migration/release-notes/0.9.16.md +110 -0
- package/docs/migration/release-notes/README.md +5 -0
- package/docs/reference/cli.md +93 -87
- package/docs/reference/configuration.md +128 -89
- package/docs/reference/data-and-telemetry.md +2 -1
- package/package.json +1 -1
- package/schemas/akm-config.json +2 -58
- package/dist/indexer/index-db-contention.js +0 -56
- package/dist/indexer/index-rebuild-lock.js +0 -73
- package/dist/indexer/materialize-embeddings.js +0 -771
- package/dist/indexer/passes/dir-staleness.js +0 -161
- package/dist/storage/repositories/embedding-salvage-repository.js +0 -184
|
@@ -17,16 +17,18 @@ import { warnVerbose } from "../../core/warn.js";
|
|
|
17
17
|
import { resolveSecretFromStore } from "../../sources/snapshot-fetchers/secret-seam.js";
|
|
18
18
|
/**
|
|
19
19
|
* Upper bound on the number of documents in one HTTP request, independent of
|
|
20
|
-
* the token budget below. Overridable via `
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
20
|
+
* the token budget below. Overridable via `packing.maxCount` (index
|
|
21
|
+
* redesign, B5 — the retired `embedding.batchSize` config key's replacement,
|
|
22
|
+
* sourced by `drain.ts` from the provider's own probed limits rather than
|
|
23
|
+
* config). Purely a safety cap (very many tiny documents could otherwise
|
|
24
|
+
* pack one request) — the token budget is what actually keeps a request
|
|
25
|
+
* inside the endpoint's context window and inside the timeout (#874).
|
|
24
26
|
*/
|
|
25
27
|
export const DEFAULT_REMOTE_BATCH_SIZE = 100;
|
|
26
28
|
/**
|
|
27
|
-
* Conservative default token budget per HTTP request when
|
|
28
|
-
*
|
|
29
|
-
*
|
|
29
|
+
* Conservative default token budget per HTTP request when no better number
|
|
30
|
+
* is known (`packing.tokenBudget`, sourced from `probeProviderLimits` —
|
|
31
|
+
* see {@link EmbeddingRequestPacking}). #874's measurements:
|
|
30
32
|
* a batch of 100 small docs (~400 KB, ~100K tokens) took 14.8s against a
|
|
31
33
|
* healthy local endpoint — half the 30s request timeout — and a single
|
|
32
34
|
* 128 KB (~24K token) document alone was rejected by the endpoint as
|
|
@@ -45,38 +47,6 @@ export const DEFAULT_TOKEN_BUDGET = 6000;
|
|
|
45
47
|
export function estimateTokenCount(text) {
|
|
46
48
|
return Math.round(text.length / 4);
|
|
47
49
|
}
|
|
48
|
-
/**
|
|
49
|
-
* Default per-document embedding cap (`embedding.maxInputTokens`, #956)
|
|
50
|
-
* — the materializer truncates a document's embedded text to
|
|
51
|
-
* this cap (head only) instead of skipping it outright, so one oversized
|
|
52
|
-
* entry can no longer fail a whole batch. Fragments are not embedded at all
|
|
53
|
-
* (only the entry's own search text is), so this is the only lever on how
|
|
54
|
-
* much of a large document contributes to its vector.
|
|
55
|
-
*/
|
|
56
|
-
export const DEFAULT_MAX_INPUT_TOKENS = 512;
|
|
57
|
-
/**
|
|
58
|
-
* Truncate `text` to at most `maxTokens` (estimated via
|
|
59
|
-
* {@link estimateTokenCount}, the same 4-chars≈1-token rule the batching
|
|
60
|
-
* budget uses), keeping only its head. The cut never splits a UTF-16
|
|
61
|
-
* surrogate pair. Text already at or under the cap is returned unchanged
|
|
62
|
-
* (`truncated: false`) — including empty text, which is never itself
|
|
63
|
-
* "truncated".
|
|
64
|
-
*/
|
|
65
|
-
export function capEmbeddingText(text, maxTokens) {
|
|
66
|
-
if (estimateTokenCount(text) <= maxTokens)
|
|
67
|
-
return { text, truncated: false };
|
|
68
|
-
const charBudget = Math.max(0, maxTokens * 4);
|
|
69
|
-
let cut = Math.min(charBudget, text.length);
|
|
70
|
-
if (cut > 0 && cut < text.length) {
|
|
71
|
-
const code = text.charCodeAt(cut);
|
|
72
|
-
// A low surrogate (0xDC00-0xDFFF) at the cut point means its high
|
|
73
|
-
// surrogate is the character just before it — back off one position so
|
|
74
|
-
// the pair stays together rather than yielding a lone surrogate.
|
|
75
|
-
if (code >= 0xdc00 && code <= 0xdfff)
|
|
76
|
-
cut -= 1;
|
|
77
|
-
}
|
|
78
|
-
return { text: text.slice(0, cut), truncated: true };
|
|
79
|
-
}
|
|
80
50
|
/**
|
|
81
51
|
* Default per-request timeout when `embedding.timeoutMs` is unset (#954).
|
|
82
52
|
* The prior fixed 30s cut off exactly the field-report case: a
|
|
@@ -194,9 +164,10 @@ export function isContextExceededResponse(status, body) {
|
|
|
194
164
|
* either direction, bounded 1-16 at the config schema — added after field
|
|
195
165
|
* evidence that a multi-slot local server (llama.cpp `--parallel N`, vLLM)
|
|
196
166
|
* genuinely serves parallel requests and was left idle by the fixed default.
|
|
197
|
-
* Request SIZE remains the first throughput lever regardless:
|
|
198
|
-
*
|
|
199
|
-
*
|
|
167
|
+
* Request SIZE remains the first throughput lever regardless: the packed
|
|
168
|
+
* request's document count and token budget (`EmbeddingRequestPacking`,
|
|
169
|
+
* sourced from `probeProviderLimits` — the retired `embedding.batchSize`/
|
|
170
|
+
* `maxTokens`/`contextLength` config keys' replacement, index redesign B5)
|
|
200
171
|
* reach a larger batch per request, which is where most of the win is for a
|
|
201
172
|
* single-slot server — a 32-input batch takes about the same wall time as
|
|
202
173
|
* one input against a healthy endpoint.
|
|
@@ -207,15 +178,22 @@ export function resolveEmbeddingConcurrency(config) {
|
|
|
207
178
|
return defaultConcurrencyForEndpoint(config.endpoint);
|
|
208
179
|
}
|
|
209
180
|
/**
|
|
210
|
-
* Group `texts` into request-sized batches bounded by BOTH
|
|
211
|
-
*
|
|
212
|
-
*
|
|
181
|
+
* Group `texts` into request-sized batches bounded by BOTH a token budget and
|
|
182
|
+
* a document-count cap, so one large document does not silently blow the
|
|
183
|
+
* batch past the endpoint's context window (#874).
|
|
184
|
+
*
|
|
185
|
+
* `tokenCounts[i]`, when given, is the count to use for `texts[i]` instead of
|
|
186
|
+
* {@link estimateTokenCount}'s fixed 4-chars≈1-token guess — `embedBatch`
|
|
187
|
+
* passes the calibrated `charsPerToken` estimate for every text
|
|
188
|
+
* (`EmbeddingRequestPacking.charsPerToken`, sourced from the provider's own
|
|
189
|
+
* probed limits). Omitted (or shorter than `texts`, e.g. a caller with no
|
|
190
|
+
* packing at all) falls back to the estimate for the texts it does not cover.
|
|
213
191
|
*
|
|
214
|
-
* A single document whose own
|
|
215
|
-
*
|
|
216
|
-
*
|
|
192
|
+
* A single document whose own count exceeds `tokenBudget` can never fit any
|
|
193
|
+
* batch — it is reported as its own oversized "batch" so the caller can skip
|
|
194
|
+
* it without ever making an HTTP request for it.
|
|
217
195
|
*/
|
|
218
|
-
export function buildTokenBoundedBatches(texts, tokenBudget, maxCount) {
|
|
196
|
+
export function buildTokenBoundedBatches(texts, tokenBudget, maxCount, tokenCounts) {
|
|
219
197
|
const batches = [];
|
|
220
198
|
let current = [];
|
|
221
199
|
let currentTokens = 0;
|
|
@@ -227,7 +205,7 @@ export function buildTokenBoundedBatches(texts, tokenBudget, maxCount) {
|
|
|
227
205
|
}
|
|
228
206
|
};
|
|
229
207
|
for (let i = 0; i < texts.length; i++) {
|
|
230
|
-
const tokens = estimateTokenCount(texts[i]);
|
|
208
|
+
const tokens = tokenCounts?.[i] ?? estimateTokenCount(texts[i]);
|
|
231
209
|
if (tokens > tokenBudget) {
|
|
232
210
|
flush();
|
|
233
211
|
batches.push({ indices: [i], oversized: true });
|
|
@@ -247,16 +225,20 @@ export function buildTokenBoundedBatches(texts, tokenBudget, maxCount) {
|
|
|
247
225
|
* context-size rejection of an `embedBatch` run (#954, field report on
|
|
248
226
|
* beta.1): one 25% cut absorbs the estimator's measured undercount without
|
|
249
227
|
* repeatedly re-shrinking mid-run — see the "shrink at most once" rule on
|
|
250
|
-
* {@link RemoteEmbedder.embedBatch}.
|
|
228
|
+
* {@link RemoteEmbedder.embedBatch}. Only fires at all when the run's window
|
|
229
|
+
* is not already authoritative (`packing.windowIsKnown` false) — see
|
|
230
|
+
* {@link EmbeddingRequestPacking}.
|
|
251
231
|
*/
|
|
252
232
|
const ADAPTIVE_BUDGET_SHRINK_FACTOR = 0.75;
|
|
253
233
|
/**
|
|
254
|
-
* Floor on the adaptive-budget shrink above,
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
234
|
+
* Floor on the adaptive-budget shrink above (#954, revised by the index
|
|
235
|
+
* redesign's B5 — the retired `embedding.maxInputTokens` per-document cap no
|
|
236
|
+
* longer bounds a single document's size, so the floor is a fixed token
|
|
237
|
+
* count rather than a multiple of that cap): a request budget below this
|
|
238
|
+
* could no longer batch more than a couple of average-sized units per
|
|
239
|
+
* request, defeating the point of batching at all.
|
|
258
240
|
*/
|
|
259
|
-
const
|
|
241
|
+
const ADAPTIVE_BUDGET_FLOOR_TOKENS = 1024;
|
|
260
242
|
export class RemoteEmbedder {
|
|
261
243
|
config;
|
|
262
244
|
endpoint;
|
|
@@ -282,6 +264,9 @@ export class RemoteEmbedder {
|
|
|
282
264
|
if (ollamaOpts) {
|
|
283
265
|
body.options = ollamaOpts;
|
|
284
266
|
}
|
|
267
|
+
if (isOllamaNativeEmbedEndpoint(this.endpoint)) {
|
|
268
|
+
body.truncate = false;
|
|
269
|
+
}
|
|
285
270
|
const timeoutMs = resolveEmbeddingTimeoutMs(this.config);
|
|
286
271
|
// `signal` MUST go through fetchWithTimeout's dedicated 4th parameter, not
|
|
287
272
|
// the RequestInit: fetchWithTimeout replaces `opts.signal` with its own
|
|
@@ -362,40 +347,47 @@ export class RemoteEmbedder {
|
|
|
362
347
|
* on a small batch rather than always waiting out the full configured
|
|
363
348
|
* `embedding.timeoutMs`.
|
|
364
349
|
*
|
|
365
|
-
* Run-scoped adaptive budget (#954, field report on beta.1
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
*
|
|
369
|
-
*
|
|
370
|
-
*
|
|
371
|
-
*
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
*
|
|
375
|
-
*
|
|
350
|
+
* Run-scoped adaptive budget (#954, field report on beta.1; gated by
|
|
351
|
+
* `packing.windowIsKnown` per the index redesign's B5): on the FIRST
|
|
352
|
+
* context-size rejection of the run, when the request window is NOT
|
|
353
|
+
* already an observed provider value (`packing.windowIsKnown` false/unset
|
|
354
|
+
* — the generic {@link DEFAULT_TOKEN_BUDGET}/8192-token default, or a
|
|
355
|
+
* caller with no packing at all), this shrinks the effective request
|
|
356
|
+
* budget by {@link ADAPTIVE_BUDGET_SHRINK_FACTOR} (floored at
|
|
357
|
+
* {@link ADAPTIVE_BUDGET_FLOOR_TOKENS}) for every batch not yet
|
|
358
|
+
* dispatched — the still-planned tail of `texts` is re-batched with
|
|
359
|
+
* `buildTokenBoundedBatches` at the smaller budget, and a
|
|
360
|
+
* `budget-lowered` `onBatch` event reports it once. A KNOWN window
|
|
361
|
+
* (llama.cpp/Ollama, probed via `probeProviderLimits`) is already
|
|
362
|
+
* authoritative, so a rejection against it is unexpected — split-and-retry
|
|
363
|
+
* (above) still recovers that one batch, but the run-wide budget is left
|
|
364
|
+
* alone rather than second-guessing a real number. This never touches the
|
|
365
|
+
* split-and-retry of the rejected batch itself, and never fires a second
|
|
366
|
+
* time in the same run even if a later batch is also rejected — a budget
|
|
367
|
+
* that is simply too big for the endpoint should self-correct once, not
|
|
368
|
+
* ratchet down forever.
|
|
376
369
|
*/
|
|
377
|
-
async embedBatch(texts, signal, onSkip, onBatch) {
|
|
370
|
+
async embedBatch(texts, signal, onSkip, onBatch, packing) {
|
|
378
371
|
if (texts.length === 0)
|
|
379
372
|
return [];
|
|
380
373
|
const results = new Array(texts.length).fill(undefined);
|
|
381
374
|
const headers = this.buildHeaders();
|
|
382
|
-
const ollamaOpts = resolveOllamaOptions(this.config);
|
|
383
|
-
//
|
|
384
|
-
//
|
|
385
|
-
//
|
|
386
|
-
//
|
|
387
|
-
|
|
388
|
-
//
|
|
389
|
-
//
|
|
390
|
-
//
|
|
391
|
-
//
|
|
392
|
-
// by that shrink rather than reassigned, so the in-flight
|
|
375
|
+
const ollamaOpts = resolveOllamaOptions(this.config, packing?.ollamaNumCtx);
|
|
376
|
+
// Per-text counts from the calibrated chars-per-token ratio
|
|
377
|
+
// (`packing.charsPerToken`, sourced from the provider's own probed
|
|
378
|
+
// limits) rather than a per-text HTTP tokenize call — synchronous, so
|
|
379
|
+
// planning a corpus of any size costs no requests at all.
|
|
380
|
+
const tokenCounts = texts.map((text) => Math.ceil(text.length / (packing?.charsPerToken ?? 4)));
|
|
381
|
+
// `effectiveTokenBudget` (#954) starts at the probed/configured/default
|
|
382
|
+
// value and MAY shrink once, on the run's first context-size rejection —
|
|
383
|
+
// see `maybeShrinkBudget` below. `textBatches` is mutated in place
|
|
384
|
+
// (spliced) by that shrink rather than reassigned, so the in-flight
|
|
393
385
|
// `concurrentMap` pool below (which reads this same array by reference)
|
|
394
386
|
// picks up the re-planned tail without restarting.
|
|
395
|
-
let effectiveTokenBudget =
|
|
396
|
-
const maxCount =
|
|
397
|
-
const
|
|
398
|
-
const textBatches = buildTokenBoundedBatches(texts, effectiveTokenBudget, maxCount);
|
|
387
|
+
let effectiveTokenBudget = packing?.tokenBudget ?? DEFAULT_TOKEN_BUDGET;
|
|
388
|
+
const maxCount = packing?.maxCount ?? DEFAULT_REMOTE_BATCH_SIZE;
|
|
389
|
+
const windowIsKnown = packing?.windowIsKnown ?? false;
|
|
390
|
+
const textBatches = buildTokenBoundedBatches(texts, effectiveTokenBudget, maxCount, tokenCounts);
|
|
399
391
|
const configuredTimeoutMs = resolveEmbeddingTimeoutMs(this.config);
|
|
400
392
|
// How many of `textBatches` concurrentMap has already claimed (its own
|
|
401
393
|
// `nextIndex`, mirrored here so a budget shrink knows where the
|
|
@@ -404,24 +396,28 @@ export class RemoteEmbedder {
|
|
|
404
396
|
// order, so the highest `batchIndex` seen so far IS the claimed count.
|
|
405
397
|
let dispatchedBatchCount = 0;
|
|
406
398
|
// Set once the run's first context-size rejection has shrunk the budget
|
|
407
|
-
// (#954) — guards `maybeShrinkBudget` so it never fires twice.
|
|
408
|
-
|
|
399
|
+
// (#954) — guards `maybeShrinkBudget` so it never fires twice. Also true
|
|
400
|
+
// (so the shrink never fires at all) when the window is already known —
|
|
401
|
+
// see the method's doc comment.
|
|
402
|
+
let budgetShrunk = windowIsKnown;
|
|
409
403
|
// On the FIRST context-size rejection of this `embedBatch` call, shrink
|
|
410
404
|
// `effectiveTokenBudget` and re-plan every batch `concurrentMap` has not
|
|
411
405
|
// yet claimed from the smaller budget. Never touches `rejectedIndices`
|
|
412
406
|
// itself — the caller's own split-and-retry handles that batch — and is
|
|
413
|
-
// a no-op after the first call (`budgetShrunk`)
|
|
407
|
+
// a no-op after the first call (`budgetShrunk`) or when the window is
|
|
408
|
+
// already known (`windowIsKnown`, folded into `budgetShrunk`'s initial
|
|
409
|
+
// value above).
|
|
414
410
|
const maybeShrinkBudget = (rejectedIndices, rejectedBatchIndex, rejectedRequestTokens) => {
|
|
415
411
|
if (budgetShrunk)
|
|
416
412
|
return;
|
|
417
413
|
budgetShrunk = true;
|
|
418
|
-
|
|
419
|
-
effectiveTokenBudget = Math.max(Math.round(effectiveTokenBudget * ADAPTIVE_BUDGET_SHRINK_FACTOR), floor);
|
|
414
|
+
effectiveTokenBudget = Math.max(Math.round(effectiveTokenBudget * ADAPTIVE_BUDGET_SHRINK_FACTOR), ADAPTIVE_BUDGET_FLOOR_TOKENS);
|
|
420
415
|
const notYetDispatched = textBatches.slice(dispatchedBatchCount);
|
|
421
416
|
const remainingIndices = notYetDispatched.flatMap((batch) => batch.indices);
|
|
422
417
|
if (remainingIndices.length > 0) {
|
|
423
418
|
const remainingTexts = remainingIndices.map((i) => texts[i]);
|
|
424
|
-
const
|
|
419
|
+
const remainingCounts = remainingIndices.map((i) => tokenCounts[i]);
|
|
420
|
+
const replanned = buildTokenBoundedBatches(remainingTexts, effectiveTokenBudget, maxCount, remainingCounts).map((batch) => ({
|
|
425
421
|
indices: batch.indices.map((localIndex) => remainingIndices[localIndex]),
|
|
426
422
|
oversized: batch.oversized,
|
|
427
423
|
}));
|
|
@@ -439,7 +435,7 @@ export class RemoteEmbedder {
|
|
|
439
435
|
});
|
|
440
436
|
};
|
|
441
437
|
// Stops the pool from claiming any FURTHER provider batch once the
|
|
442
|
-
// caller's onBatch has failed once (the
|
|
438
|
+
// caller's onBatch has failed once (the caller's transaction
|
|
443
439
|
// failed, so a subsequent commit would just fail again) — dispatching
|
|
444
440
|
// real HTTP requests whose results can never be persisted is pure waste.
|
|
445
441
|
// Deliberately a SEPARATE controller from the caller's own `signal`,
|
|
@@ -463,7 +459,7 @@ export class RemoteEmbedder {
|
|
|
463
459
|
}
|
|
464
460
|
}
|
|
465
461
|
// First error thrown BY the caller's onBatch callback (e.g. a real
|
|
466
|
-
// competing-process SQLITE_BUSY from the
|
|
462
|
+
// competing-process SQLITE_BUSY from the caller's own db.transaction())
|
|
467
463
|
// rather than by requestBatch itself. Captured here instead of being left
|
|
468
464
|
// to reach requestAndCommit's try/catch below, which exists solely to
|
|
469
465
|
// classify requestBatch's own provider/network failures — a persistence
|
|
@@ -519,7 +515,7 @@ export class RemoteEmbedder {
|
|
|
519
515
|
if (dispatchAbort.signal.aborted)
|
|
520
516
|
return;
|
|
521
517
|
const batch = indices.map((i) => texts[i]);
|
|
522
|
-
const requestTokens =
|
|
518
|
+
const requestTokens = indices.reduce((sum, i) => sum + tokenCounts[i], 0);
|
|
523
519
|
const requestTimeoutMs = scaleEmbeddingTimeoutMs(configuredTimeoutMs, requestTokens, effectiveTokenBudget);
|
|
524
520
|
const requestStart = Date.now();
|
|
525
521
|
let batchEmbeddings;
|
|
@@ -569,8 +565,8 @@ export class RemoteEmbedder {
|
|
|
569
565
|
// verbose line above — a run silently waiting out a multi-minute
|
|
570
566
|
// back-off looked identical to a hang otherwise. Nothing has
|
|
571
567
|
// failed or succeeded yet, so there is nothing to persist:
|
|
572
|
-
// `embeddings` are all `undefined` and the
|
|
573
|
-
//
|
|
568
|
+
// `embeddings` are all `undefined` and the caller's onBatch must
|
|
569
|
+
// not touch storage for this event.
|
|
574
570
|
commitBatch(indices, indices.map(() => undefined), undefined, {
|
|
575
571
|
batchIndex,
|
|
576
572
|
batchCount: textBatches.length,
|
|
@@ -605,13 +601,12 @@ export class RemoteEmbedder {
|
|
|
605
601
|
// Default-level visibility for a failed batch (not verbose-only) is
|
|
606
602
|
// still guaranteed here — just not via warn(). The `commitBatch` call
|
|
607
603
|
// below carries `outcome: "failed"` and this `message` as `reason`
|
|
608
|
-
// through `onBatch`, and
|
|
604
|
+
// through `onBatch`, and the caller's (`drain.ts`) per-batch line
|
|
609
605
|
// (also default-level) prints it from there. A warn() call here used
|
|
610
606
|
// to print the identical event a second time on stderr — the same
|
|
611
|
-
// class of double-print bug
|
|
612
|
-
//
|
|
613
|
-
// Per-entry batch-mapping detail stays verbose-only
|
|
614
|
-
// (materialize-embeddings.ts).
|
|
607
|
+
// class of double-print bug this once guarded against in the retired
|
|
608
|
+
// materialize-embeddings.ts (#954, field-report follow-up).
|
|
609
|
+
// Per-entry batch-mapping detail stays verbose-only (the caller).
|
|
615
610
|
let stopRequested = false;
|
|
616
611
|
for (const [k, idx] of indices.entries()) {
|
|
617
612
|
if (onSkip?.({
|
|
@@ -654,7 +649,7 @@ export class RemoteEmbedder {
|
|
|
654
649
|
dispatchedBatchCount = batchIndex;
|
|
655
650
|
if (textBatch.oversized) {
|
|
656
651
|
const idx = textBatch.indices[0];
|
|
657
|
-
const estTokens =
|
|
652
|
+
const estTokens = tokenCounts[idx];
|
|
658
653
|
onSkip?.({
|
|
659
654
|
index: idx,
|
|
660
655
|
reason: "context-window-exceeded",
|
|
@@ -724,6 +719,9 @@ export class RemoteEmbedder {
|
|
|
724
719
|
if (ollamaOpts) {
|
|
725
720
|
body.options = ollamaOpts;
|
|
726
721
|
}
|
|
722
|
+
if (isOllamaNativeEmbedEndpoint(this.endpoint)) {
|
|
723
|
+
body.truncate = false;
|
|
724
|
+
}
|
|
727
725
|
// See embed(): `signal` goes through the 4th parameter, not the
|
|
728
726
|
// RequestInit, or fetchWithTimeout drops it.
|
|
729
727
|
const response = await fetchWithTimeout(normalizeEmbeddingEndpoint(this.endpoint), {
|
|
@@ -816,6 +814,34 @@ export function normalizeEmbeddingEndpoint(endpoint) {
|
|
|
816
814
|
parsed.pathname = normalizedPath ? `${normalizedPath}/embeddings` : "/embeddings";
|
|
817
815
|
return parsed.toString();
|
|
818
816
|
}
|
|
817
|
+
/**
|
|
818
|
+
* True when `endpoint`'s normalized path is Ollama's native `/api/embed`
|
|
819
|
+
* route (see {@link normalizeEmbeddingEndpoint}) rather than an
|
|
820
|
+
* OpenAI-compatible `/embeddings` route. Gates `truncate: false` on the
|
|
821
|
+
* request body (round-2 field finding): akm never sent `truncate` at all, so
|
|
822
|
+
* Ollama's default — silently truncate an over-budget input and still return
|
|
823
|
+
* 200 — meant a unit denser than the calibrated chars-per-token ratio was
|
|
824
|
+
* embedded from a truncated prefix and stored as a complete, correct-looking
|
|
825
|
+
* vector: never counted `failed` or `skipped`, coverage reporting it done,
|
|
826
|
+
* that content's search quality silently degraded forever. `truncate: false`
|
|
827
|
+
* makes an over-budget request fail loudly instead, so it flows into the
|
|
828
|
+
* existing context-window handling (split-and-retry, ultimately a genuine
|
|
829
|
+
* `skipped` unit) rather than a silent truncation. Scoped to the native
|
|
830
|
+
* route specifically because that is the one shape this field evidence is
|
|
831
|
+
* about — an OpenAI-compatible endpoint ignores the unknown field either
|
|
832
|
+
* way, so this is not a safety boundary, just not sending an option that
|
|
833
|
+
* does nothing elsewhere.
|
|
834
|
+
*/
|
|
835
|
+
function isOllamaNativeEmbedEndpoint(endpoint) {
|
|
836
|
+
let parsed;
|
|
837
|
+
try {
|
|
838
|
+
parsed = new URL(normalizeEmbeddingEndpoint(endpoint));
|
|
839
|
+
}
|
|
840
|
+
catch {
|
|
841
|
+
return false;
|
|
842
|
+
}
|
|
843
|
+
return parsed.pathname.replace(/\/+$/, "").endsWith("/embed");
|
|
844
|
+
}
|
|
819
845
|
function embeddingEndpointPathHint(endpoint) {
|
|
820
846
|
const normalizedEndpoint = normalizeEmbeddingEndpoint(endpoint);
|
|
821
847
|
if (normalizedEndpoint !== endpoint) {
|
|
@@ -828,19 +854,22 @@ function embeddingEndpointPathHint(endpoint) {
|
|
|
828
854
|
*
|
|
829
855
|
* Resolution order:
|
|
830
856
|
* 1. `ollamaOptions` — forwarded verbatim (explicit opt-in, takes precedence).
|
|
831
|
-
* 2. `
|
|
857
|
+
* 2. `ollamaNumCtx` — the caller-supplied, provider-probed native context
|
|
858
|
+
* window (`ProviderLimits.ollamaNumCtx`, sourced from
|
|
859
|
+
* `probeProviderLimits`, NOT the retired `embedding.contextLength`
|
|
860
|
+
* config key), wrapped as `{ num_ctx: ollamaNumCtx }`.
|
|
832
861
|
* 3. Neither set → returns `undefined` (no `options` field in the request body).
|
|
833
862
|
*
|
|
834
863
|
* These options are only meaningful for Ollama's native `/api/embed` endpoint.
|
|
835
864
|
* OpenAI-compatible endpoints ignore unknown request fields, so passing them to
|
|
836
865
|
* other providers is harmless but has no effect.
|
|
837
866
|
*/
|
|
838
|
-
function resolveOllamaOptions(config) {
|
|
867
|
+
function resolveOllamaOptions(config, ollamaNumCtx) {
|
|
839
868
|
if (config.ollamaOptions && Object.keys(config.ollamaOptions).length > 0) {
|
|
840
869
|
return config.ollamaOptions;
|
|
841
870
|
}
|
|
842
|
-
if (
|
|
843
|
-
return { num_ctx:
|
|
871
|
+
if (ollamaNumCtx) {
|
|
872
|
+
return { num_ctx: ollamaNumCtx };
|
|
844
873
|
}
|
|
845
874
|
return undefined;
|
|
846
875
|
}
|
package/dist/llm/feature-gate.js
CHANGED
|
@@ -10,9 +10,11 @@ const FEATURE_LOCATION = {
|
|
|
10
10
|
graph_extraction: (cfg) => cfg.index?.graph?.enabled ?? true,
|
|
11
11
|
metadata_enhance: (cfg) => cfg.index?.metadataEnhance?.enabled ?? false,
|
|
12
12
|
// #951: a real implementation of the dead `curate_rerank` key removed in
|
|
13
|
-
// 0.8.0.
|
|
13
|
+
// 0.8.0, shipped in 0.9.15 wired to curate, then moved to search in 0.9.16
|
|
14
|
+
// (renamed `curate_rerank` → `search_rerank`; the pass was always meant for
|
|
15
|
+
// search). Off by default — it requires a `search.rerank.endpoint` a
|
|
14
16
|
// caller must explicitly configure.
|
|
15
|
-
|
|
17
|
+
search_rerank: (cfg) => Boolean(cfg.search?.rerank?.enabled),
|
|
16
18
|
// Always on at the LLM-wrapper level. Enablement is decided ONCE at the
|
|
17
19
|
// extract entry point (`akmExtract`): the `extract.enabled` process toggle
|
|
18
20
|
// gates extract as a STAGE of `akm improve` (the active improve strategy, per
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* `relevance_score` descending and callers treat a missing index as
|
|
22
22
|
* "unscored" (kept in its original relative position, after every scored
|
|
23
23
|
* document). Deliberately independent of `EngineConfigSchema`'s "llm"/"agent"
|
|
24
|
-
* kinds — see the comment on `
|
|
24
|
+
* kinds — see the comment on `SearchRerankConfigSchema` in
|
|
25
25
|
* `core/config/schema/search.ts` for why.
|
|
26
26
|
*/
|
|
27
27
|
import { fetchWithTimeout, readBodyWithByteCap } from "../core/common.js";
|
|
@@ -45,13 +45,13 @@ export class RerankCallError extends Error {
|
|
|
45
45
|
* after every scored document (never dropped).
|
|
46
46
|
*
|
|
47
47
|
* Throws {@link RerankCallError} on any transport/parse failure — callers
|
|
48
|
-
* that want a graceful fallback should use `tryLlmFeature("
|
|
48
|
+
* that want a graceful fallback should use `tryLlmFeature("search_rerank", ...)`
|
|
49
49
|
* (`llm/feature-gate.ts`), matching every other bounded in-tree LLM/rerank
|
|
50
50
|
* call site.
|
|
51
51
|
*/
|
|
52
52
|
export async function rerankDocuments(config, query, documents) {
|
|
53
53
|
if (!config.endpoint) {
|
|
54
|
-
throw new RerankCallError("search.
|
|
54
|
+
throw new RerankCallError("search.rerank.endpoint is not configured.", "provider_error");
|
|
55
55
|
}
|
|
56
56
|
if (documents.length === 0)
|
|
57
57
|
return [];
|
|
@@ -422,21 +422,13 @@ export function formatInitPlain(r) {
|
|
|
422
422
|
}
|
|
423
423
|
export function formatIndexPlain(r) {
|
|
424
424
|
const indexResult = r;
|
|
425
|
-
let out = `Indexed ${indexResult.totalEntries ?? 0} entries from ${indexResult.
|
|
425
|
+
let out = `Indexed ${indexResult.totalEntries ?? 0} entries from ${indexResult.sourcesScanned ?? 0} source${indexResult.sourcesScanned === 1 ? "" : "s"} (mode: ${indexResult.mode ?? "unknown"})`;
|
|
426
426
|
const warnings = indexResult.warnings;
|
|
427
427
|
if (Array.isArray(warnings) && warnings.length > 0) {
|
|
428
428
|
out += `\nWarnings (${warnings.length}):`;
|
|
429
429
|
for (const message of warnings)
|
|
430
430
|
out += `\n - ${String(message)}`;
|
|
431
431
|
}
|
|
432
|
-
const notices = Array.isArray(indexResult.notices) ? indexResult.notices : [];
|
|
433
|
-
for (const notice of notices) {
|
|
434
|
-
const severity = notice.severity === "info" ? "info" : "warning";
|
|
435
|
-
const field = typeof notice.field === "string" ? ` field=${notice.field}` : "";
|
|
436
|
-
out +=
|
|
437
|
-
`\n notice[${severity}] ${notice.code} adapter=${notice.adapter}${field}` +
|
|
438
|
-
(notice.message ? `: ${notice.message}` : "");
|
|
439
|
-
}
|
|
440
432
|
const verification = indexResult.verification;
|
|
441
433
|
if (verification?.ok === false && verification.message) {
|
|
442
434
|
out += `\nVerification: ${String(verification.message)}`;
|
|
@@ -446,16 +438,30 @@ export function formatIndexPlain(r) {
|
|
|
446
438
|
out +=
|
|
447
439
|
`\nTiming: total ${timing.totalMs}ms` +
|
|
448
440
|
`, preflight ${timing.preflightMs}ms` +
|
|
449
|
-
`,
|
|
450
|
-
`,
|
|
441
|
+
`, source cache ${timing.sourceCacheMs}ms` +
|
|
442
|
+
`, reconcile ${timing.reconcileMs}ms` +
|
|
451
443
|
`, embeddings ${timing.embedMs}ms` +
|
|
452
|
-
`, fts ${timing.ftsMs}ms` +
|
|
453
444
|
`, finalize ${timing.finalizeMs}ms` +
|
|
454
|
-
`, clean ${timing.cleanMs}ms` +
|
|
455
445
|
`, end-to-end ${timing.endToEndMs}ms`;
|
|
456
446
|
}
|
|
457
447
|
return out;
|
|
458
448
|
}
|
|
449
|
+
/** Render `akm index status`'s `IndexStatusResponse` (src/commands/sources/index-status.ts). */
|
|
450
|
+
export function formatIndexStatusPlain(r) {
|
|
451
|
+
const units = (r.units ?? {});
|
|
452
|
+
const lines = [
|
|
453
|
+
`Index: ${String(r.indexPath ?? "unknown")}`,
|
|
454
|
+
`Files: ${Number(r.files ?? 0)}`,
|
|
455
|
+
`Entries: ${Number(r.entries ?? 0)}`,
|
|
456
|
+
`Units: ${Number(units.total ?? 0)} total, ${Number(units.withVector ?? 0)} with a vector, ${Number(units.pending ?? 0)} pending`,
|
|
457
|
+
`Active identity: ${typeof r.activeIdentity === "string" ? r.activeIdentity : "none yet"}`,
|
|
458
|
+
`Last reconcile: ${typeof r.lastReconcileAt === "string" ? r.lastReconcileAt : "never"}`,
|
|
459
|
+
`Built at: ${typeof r.builtAt === "string" ? r.builtAt : "never"}`,
|
|
460
|
+
];
|
|
461
|
+
if (typeof r.unreadable === "string")
|
|
462
|
+
lines.push(`Unreadable: ${r.unreadable}`);
|
|
463
|
+
return lines.join("\n");
|
|
464
|
+
}
|
|
459
465
|
export function formatListPlain(r) {
|
|
460
466
|
const sources = Array.isArray(r.sources) ? r.sources : [];
|
|
461
467
|
if (sources.length === 0)
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*
|
|
16
16
|
* No registry imports — no circular dependencies.
|
|
17
17
|
*/
|
|
18
|
-
export { formatAddPlain, formatBundleShowPlain, formatClonePlain, formatConfigPlain, formatCuratePlain, formatEnvCreatePlain, formatEnvExportPlain, formatEnvListPlain, formatEnvRemovePlain, formatEventLine, formatEventsPlain, formatFeedbackPlain, formatImportPlain, formatIndexPlain, formatInfoPlain, formatInitPlain, formatListPlain, formatModelsListPlain, formatRegistryAddPlain, formatRegistryListPlain, formatRegistryRemovePlain, formatRegistrySearchPlain, formatRememberPlain, formatRemovePlain, formatSearchPlain, formatSyncPlain, formatUpdatePlain, formatUpgradePlain, } from "./command-format.js";
|
|
18
|
+
export { formatAddPlain, formatBundleShowPlain, formatClonePlain, formatConfigPlain, formatCuratePlain, formatEnvCreatePlain, formatEnvExportPlain, formatEnvListPlain, formatEnvRemovePlain, formatEventLine, formatEventsPlain, formatFeedbackPlain, formatImportPlain, formatIndexPlain, formatIndexStatusPlain, formatInfoPlain, formatInitPlain, formatListPlain, formatModelsListPlain, formatRegistryAddPlain, formatRegistryListPlain, formatRegistryRemovePlain, formatRegistrySearchPlain, formatRememberPlain, formatRemovePlain, formatSearchPlain, formatSyncPlain, formatUpdatePlain, formatUpgradePlain, } from "./command-format.js";
|
|
19
19
|
export { formatHealthPlain } from "./health-format.js";
|
|
20
20
|
export { formatLintPlain } from "./lint-format.js";
|
|
21
21
|
export { formatGateDecisionSummary, formatProposalAcceptPlain, formatProposalDiffPlain, formatProposalDrainPlain, formatProposalListPlain, formatProposalProducerPlain, formatProposalRejectPlain, formatProposalShowPlain, } from "./proposal-format.js";
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
2
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
-
import { formatIndexPlain } from "./helpers.js";
|
|
5
|
-
export const indexFormatters = [
|
|
4
|
+
import { formatIndexPlain, formatIndexStatusPlain } from "./helpers.js";
|
|
5
|
+
export const indexFormatters = [
|
|
6
|
+
{ command: "index", handler: (r) => formatIndexPlain(r) },
|
|
7
|
+
{ command: "index-status", handler: (r) => formatIndexStatusPlain(r) },
|
|
8
|
+
];
|