akm-cli 0.9.4 → 0.9.6
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 +265 -0
- package/dist/commands/env/env-cli.js +1 -2
- package/dist/commands/env/secret-cli.js +1 -5
- package/dist/commands/feedback-cli.js +0 -4
- package/dist/commands/health/checks.js +0 -32
- package/dist/commands/health/surfaces.js +2 -2
- package/dist/commands/health.js +6 -15
- package/dist/commands/improve/anti-collapse.js +4 -91
- package/dist/commands/improve/autonomy-gate.js +1 -1
- package/dist/commands/improve/consolidate.js +25 -26
- package/dist/commands/improve/distill.js +2 -2
- package/dist/commands/improve/extract.js +8 -1
- package/dist/commands/improve/improve.js +3 -5
- package/dist/commands/improve/locks.js +13 -5
- package/dist/commands/improve/loop-stages.js +8 -9
- package/dist/commands/improve/memory/memory-contradiction-detect.js +1 -19
- package/dist/commands/improve/outcome-loop.js +0 -6
- package/dist/commands/improve/reflect.js +20 -9
- package/dist/commands/lint/index.js +4 -6
- package/dist/commands/proposal/validators/proposal-quality-validators.js +0 -7
- package/dist/commands/proposal/validators/proposal-validators.js +12 -0
- package/dist/commands/read/search.js +14 -24
- package/dist/commands/sources/bundle-config-ops.js +9 -4
- package/dist/commands/sources/info.js +14 -10
- package/dist/commands/tasks/tasks-cli.js +56 -3
- package/dist/commands/tasks/tasks.js +101 -18
- package/dist/commands/workflow-cli.js +2 -2
- package/dist/core/adapter/adapters/akm-adapter.js +2 -0
- package/dist/core/common.js +61 -24
- package/dist/core/config/config-io.js +2 -2
- package/dist/core/config/config-sources.js +32 -2
- package/dist/core/config/config-version-shim.js +101 -0
- package/dist/core/config/config.js +7 -7
- package/dist/core/config/schema/engines.js +9 -7
- package/dist/core/config/schema/primitives.js +0 -5
- package/dist/core/config/schema/search.js +2 -1
- package/dist/core/file-lock.js +2 -1
- package/dist/core/improve-result.js +35 -14
- package/dist/core/maintenance-barrier.js +2 -14
- package/dist/core/paths.js +0 -3
- package/dist/core/redaction.js +2 -2
- package/dist/core/spawn-env.js +8 -12
- package/dist/core/state/migrations.js +1 -12
- package/dist/core/state-db.js +9 -27
- package/dist/core/write-source.js +12 -19
- package/dist/execution/directory-identity.js +36 -10
- package/dist/execution/guarded-source.js +0 -10
- package/dist/indexer/graph/graph-boost.js +0 -4
- package/dist/indexer/index-writer-lock.js +43 -24
- package/dist/indexer/index-written-assets.js +5 -6
- package/dist/indexer/indexer.js +2 -39
- package/dist/indexer/materialize-embeddings.js +85 -41
- package/dist/indexer/passes/metadata.js +12 -4
- package/dist/indexer/scan/doc-to-entry.js +2 -0
- package/dist/indexer/search/db-search.js +15 -48
- package/dist/indexer/search/ranking-contributors.js +0 -25
- package/dist/indexer/search/ranking.js +3 -13
- package/dist/indexer/search/search-fields.js +16 -1
- package/dist/integrations/agent/builder-shared.js +0 -25
- package/dist/integrations/agent/model-map.js +2 -60
- package/dist/integrations/harnesses/aider/agent-builder.js +1 -3
- package/dist/integrations/harnesses/amazonq/agent-builder.js +2 -7
- package/dist/integrations/harnesses/claude/agent-builder.js +1 -4
- package/dist/integrations/harnesses/codex/agent-builder.js +1 -4
- package/dist/integrations/harnesses/copilot/agent-builder.js +2 -6
- package/dist/integrations/harnesses/gemini/agent-builder.js +2 -7
- package/dist/integrations/harnesses/opencode/agent-builder.js +1 -4
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +2 -16
- package/dist/integrations/harnesses/openhands/agent-builder.js +1 -3
- package/dist/integrations/harnesses/pi/agent-builder.js +2 -4
- package/dist/integrations/session-logs/index.js +0 -9
- package/dist/llm/client.js +75 -42
- package/dist/llm/embedder.js +7 -3
- package/dist/llm/embedders/remote.js +141 -42
- package/dist/output/shapes/passthrough.js +17 -5
- package/dist/registry/network.js +5 -37
- package/dist/runtime.js +2 -10
- package/dist/scripts/akm-migrate-node.js +180 -238
- package/dist/scripts/akm-migrate.js +180 -238
- package/dist/setup/engine-config.js +2 -5
- package/dist/setup/registry-stash-loader.js +0 -8
- package/dist/setup/setup.js +9 -46
- package/dist/setup/steps/connection-shared.js +10 -13
- package/dist/sources/providers/git-install.js +1 -1
- package/dist/storage/engines/sqlite-migrations.js +20 -1
- package/dist/storage/repositories/index-entries-repository.js +0 -15
- package/dist/storage/repositories/proposals-repository.js +32 -6
- package/dist/tasks/backends/launchd.js +15 -20
- package/dist/tasks/backends/schtasks.js +18 -8
- package/dist/tasks/run/run-native-task.js +8 -6
- package/dist/tasks/scheduler-binding.js +15 -5
- package/dist/tasks/scheduler-sync-preview.js +4 -2
- package/dist/tasks/scheduler-sync.js +77 -42
- package/dist/tasks/source/bounded-document.js +2 -4
- package/dist/tasks/source/task-source-v3-frozen.js +5 -7
- package/dist/tasks/source/task-source-v4.js +5 -10
- package/dist/tasks/source/task-to-v3.js +29 -22
- package/dist/tasks/source/task-to-v4.js +1 -14
- package/dist/tasks/source-v3.js +6 -6
- package/dist/workflows/exec/native-executor.js +21 -31
- package/dist/workflows/exec/run-workflow.js +5 -6
- package/dist/workflows/exec/scheduler.js +3 -19
- package/dist/workflows/exec/step-work.js +1 -4
- package/dist/workflows/exec/unit-dispatch.js +2 -2
- package/dist/workflows/exec/worktree.js +1 -13
- package/dist/workflows/freeze/targets/child-workflow.js +2 -10
- package/dist/workflows/ir/plan-hash.js +4 -6
- package/dist/workflows/ir/schema-v4.js +0 -12
- package/dist/workflows/ir/schema.js +20 -31
- package/dist/workflows/parser.js +11 -52
- package/dist/workflows/renderer.js +2 -3
- package/dist/workflows/resource-limits.js +11 -41
- package/dist/workflows/runtime/runs.js +3 -4
- package/dist/workflows/source-ir/schema.js +14 -30
- package/dist/workflows/validator.js +1 -7
- package/docs/reference/cli.md +25 -3
- package/docs/reference/configuration.md +28 -7
- package/docs/reference/tasks.md +10 -0
- package/package.json +1 -1
- package/schemas/akm-config.json +4 -23
- package/schemas/akm-task.json +1 -2
- package/schemas/akm-workflow.json +1 -13
- package/dist/indexer/search/semantic-status.js +0 -142
|
@@ -1,15 +1,36 @@
|
|
|
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
|
+
/**
|
|
5
|
+
* Asset-mutation lease — serializes writes to real, authored user content
|
|
6
|
+
* (source updates, `akm remember`, proposal apply, ...) so two concurrent
|
|
7
|
+
* writers cannot both pass a git exact-path preflight check before either
|
|
8
|
+
* commits.
|
|
9
|
+
*
|
|
10
|
+
* This module previously also gated full index REBUILDS behind the same
|
|
11
|
+
* lease with a 12-hour age-based stale-reclaim window (#872). That guard was
|
|
12
|
+
* removed: the index is a regenerable cache, concurrent rebuilds only waste
|
|
13
|
+
* work rather than corrupt anything, and a live-but-wedged holder passed the
|
|
14
|
+
* PID-liveness check forever, so only the 12h clock could ever free it —
|
|
15
|
+
* which cost one real install a half-day indexing outage. Index rebuilds no
|
|
16
|
+
* longer take any lease.
|
|
17
|
+
*
|
|
18
|
+
* What remains here guards actual data loss (a lost or conflicting git
|
|
19
|
+
* commit), so per AGENTS.md `## Defensive Code` it stays — but with the same
|
|
20
|
+
* fix applied: no age-based stale-reclaim. A holder is only ever reclaimed
|
|
21
|
+
* once its PID is verifiably dead; a live-but-wedged holder makes an
|
|
22
|
+
* acquisition attempt wait (bounded by `maxWaitMs`) or fail, never silently
|
|
23
|
+
* override a lease someone might still be using.
|
|
24
|
+
*/
|
|
4
25
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
5
26
|
import fs from "node:fs";
|
|
6
27
|
import path from "node:path";
|
|
7
28
|
import { createLockPayload, probeLock, reclaimStaleLock, releaseLock, tryAcquireLockSync, } from "../core/file-lock.js";
|
|
8
29
|
import { tryAcquireMaintenanceBarrier } from "../core/maintenance-barrier.js";
|
|
9
30
|
import { getDbPath, getIndexWriterLockPath } from "../core/paths.js";
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
const
|
|
31
|
+
import { sleepSync } from "../runtime.js";
|
|
32
|
+
const ASSET_MUTATION_WAIT_MS = 100;
|
|
33
|
+
const DEFAULT_ASSET_MUTATION_MAX_WAIT_MS = 10 * 60 * 1000;
|
|
13
34
|
const leaseContext = new AsyncLocalStorage();
|
|
14
35
|
function buildPayload(purpose) {
|
|
15
36
|
return createLockPayload({
|
|
@@ -24,7 +45,7 @@ function delay(ms) {
|
|
|
24
45
|
function throwIfAborted(signal) {
|
|
25
46
|
if (!signal?.aborted)
|
|
26
47
|
return;
|
|
27
|
-
throw signal.reason instanceof Error ? signal.reason : new Error("
|
|
48
|
+
throw signal.reason instanceof Error ? signal.reason : new Error("asset mutation lease wait aborted");
|
|
28
49
|
}
|
|
29
50
|
function createLease(lockPath, ownership) {
|
|
30
51
|
const exitHandler = () => releaseLock(ownership);
|
|
@@ -41,7 +62,7 @@ function createLease(lockPath, ownership) {
|
|
|
41
62
|
},
|
|
42
63
|
};
|
|
43
64
|
}
|
|
44
|
-
function
|
|
65
|
+
function tryAcquireAssetMutationLease(lockPath, purpose) {
|
|
45
66
|
while (true) {
|
|
46
67
|
const releaseBarrier = tryAcquireMaintenanceBarrier();
|
|
47
68
|
if (!releaseBarrier)
|
|
@@ -50,7 +71,8 @@ function tryAcquireIndexWriterLease(lockPath, purpose) {
|
|
|
50
71
|
const ownership = tryAcquireLockSync(lockPath, buildPayload(purpose));
|
|
51
72
|
if (ownership)
|
|
52
73
|
return createLease(lockPath, ownership);
|
|
53
|
-
|
|
74
|
+
// No `staleAfterMs`: only a verifiably dead holder is ever reclaimed.
|
|
75
|
+
const probe = probeLock(lockPath);
|
|
54
76
|
if (probe.state !== "stale" || !reclaimStaleLock(lockPath, probe))
|
|
55
77
|
return undefined;
|
|
56
78
|
}
|
|
@@ -59,16 +81,16 @@ function tryAcquireIndexWriterLease(lockPath, purpose) {
|
|
|
59
81
|
}
|
|
60
82
|
}
|
|
61
83
|
}
|
|
62
|
-
export async function
|
|
84
|
+
export async function acquireAssetMutationLease(options) {
|
|
63
85
|
const mode = options.mode ?? "wait";
|
|
64
86
|
const lockPath = getIndexWriterLockPath();
|
|
65
87
|
const startedAt = Date.now();
|
|
66
|
-
const maxWaitMs = options.maxWaitMs ??
|
|
88
|
+
const maxWaitMs = options.maxWaitMs ?? DEFAULT_ASSET_MUTATION_MAX_WAIT_MS;
|
|
67
89
|
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
68
90
|
let lastWaitNoticeMs = 0;
|
|
69
91
|
while (true) {
|
|
70
92
|
throwIfAborted(options.signal);
|
|
71
|
-
const lease =
|
|
93
|
+
const lease = tryAcquireAssetMutationLease(lockPath, options.purpose);
|
|
72
94
|
if (lease) {
|
|
73
95
|
options.onAcquired?.({ waitedMs: Date.now() - startedAt });
|
|
74
96
|
return lease;
|
|
@@ -79,26 +101,27 @@ export async function acquireIndexWriterLease(options) {
|
|
|
79
101
|
// attempt, so a caller with maxWaitMs:0 still gets one chance at a free lock
|
|
80
102
|
// instead of throwing before it ever tries.
|
|
81
103
|
if (maxWaitMs >= 0 && Date.now() - startedAt >= maxWaitMs) {
|
|
82
|
-
throw new Error(`timed out waiting for
|
|
104
|
+
throw new Error(`timed out waiting for asset mutation lease for ${options.purpose}`);
|
|
83
105
|
}
|
|
84
106
|
const waitedMs = Date.now() - startedAt;
|
|
85
107
|
if (waitedMs - lastWaitNoticeMs >= 15000) {
|
|
86
108
|
options.onWait?.({ waitedMs });
|
|
87
109
|
lastWaitNoticeMs = waitedMs;
|
|
88
110
|
}
|
|
89
|
-
await delay(
|
|
111
|
+
await delay(ASSET_MUTATION_WAIT_MS);
|
|
90
112
|
}
|
|
91
113
|
}
|
|
92
|
-
|
|
114
|
+
/** Asset writes share one lease so two concurrent writers cannot both pass preflight before either commits. */
|
|
115
|
+
export async function withAssetMutationLease(purpose, run) {
|
|
93
116
|
const lockPath = getIndexWriterLockPath();
|
|
94
117
|
const inherited = leaseContext.getStore();
|
|
95
118
|
if (inherited?.has(lockPath))
|
|
96
119
|
return run();
|
|
97
120
|
const context = inherited ?? new Set();
|
|
98
121
|
const execute = async () => {
|
|
99
|
-
const lease = await
|
|
122
|
+
const lease = await acquireAssetMutationLease({ purpose });
|
|
100
123
|
if (!lease)
|
|
101
|
-
throw new Error(`
|
|
124
|
+
throw new Error(`asset mutation lease unavailable for ${purpose}`);
|
|
102
125
|
context.add(lockPath);
|
|
103
126
|
try {
|
|
104
127
|
return await run();
|
|
@@ -110,10 +133,6 @@ export async function withIndexWriterLease(options, run) {
|
|
|
110
133
|
};
|
|
111
134
|
return inherited ? execute() : leaseContext.run(context, execute);
|
|
112
135
|
}
|
|
113
|
-
/** Asset writes and index rebuilds share one lease so scans cannot publish stale snapshots. */
|
|
114
|
-
export function withAssetMutationLease(purpose, run) {
|
|
115
|
-
return withIndexWriterLease({ purpose }, run);
|
|
116
|
-
}
|
|
117
136
|
/** Synchronous asset-write boundary over the same interprocess lease. */
|
|
118
137
|
export function withAssetMutationLeaseSync(purpose, run) {
|
|
119
138
|
const lockPath = getIndexWriterLockPath();
|
|
@@ -126,12 +145,12 @@ export function withAssetMutationLeaseSync(purpose, run) {
|
|
|
126
145
|
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
127
146
|
let lease;
|
|
128
147
|
while (!lease) {
|
|
129
|
-
lease =
|
|
148
|
+
lease = tryAcquireAssetMutationLease(lockPath, purpose);
|
|
130
149
|
if (!lease) {
|
|
131
|
-
if (Date.now() - startedAt >=
|
|
132
|
-
throw new Error(`timed out waiting for
|
|
150
|
+
if (Date.now() - startedAt >= DEFAULT_ASSET_MUTATION_MAX_WAIT_MS) {
|
|
151
|
+
throw new Error(`timed out waiting for asset mutation lease for ${purpose}`);
|
|
133
152
|
}
|
|
134
|
-
|
|
153
|
+
sleepSync(ASSET_MUTATION_WAIT_MS);
|
|
135
154
|
}
|
|
136
155
|
}
|
|
137
156
|
context.add(lockPath);
|
|
@@ -145,6 +164,6 @@ export function withAssetMutationLeaseSync(purpose, run) {
|
|
|
145
164
|
};
|
|
146
165
|
return inherited ? execute() : leaseContext.run(context, execute);
|
|
147
166
|
}
|
|
148
|
-
export function
|
|
149
|
-
return probeLock(getIndexWriterLockPath()
|
|
167
|
+
export function probeAssetMutationLease() {
|
|
168
|
+
return probeLock(getIndexWriterLockPath());
|
|
150
169
|
}
|
|
@@ -29,9 +29,8 @@ import { getDbPath } from "../core/paths.js";
|
|
|
29
29
|
import { warn, warnVerbose } from "../core/warn.js";
|
|
30
30
|
import { closeDatabase, openExistingDatabase } from "../storage/repositories/index-connection.js";
|
|
31
31
|
import { deleteEntriesByIds, getEntryCount, upsertEntry } from "../storage/repositories/index-entries-repository.js";
|
|
32
|
-
import { withIndexWriterLease } from "./index-writer-lock.js";
|
|
33
32
|
import { deriveEntryProvenance, deriveInstallations } from "./installations.js";
|
|
34
|
-
import { generateEmbeddingsForDb,
|
|
33
|
+
import { generateEmbeddingsForDb, publishTargetedEmbeddingMeta } from "./materialize-embeddings.js";
|
|
35
34
|
import { drainDirDocuments } from "./scan/drain-dir.js";
|
|
36
35
|
import { buildSearchText } from "./search/search-fields.js";
|
|
37
36
|
import { buildFileContext } from "./walk/file-context.js";
|
|
@@ -57,7 +56,7 @@ export const WRITE_PATH_INDEX_BUSY_TIMEOUT_MS = 5_000;
|
|
|
57
56
|
*/
|
|
58
57
|
export async function indexWrittenAssets(stashDir, filePaths, options = {}) {
|
|
59
58
|
try {
|
|
60
|
-
return await
|
|
59
|
+
return await (async () => {
|
|
61
60
|
const dbPath = getDbPath();
|
|
62
61
|
// `true` here means "the index is in the state the caller expects" — and
|
|
63
62
|
// `acceptProposal` advances its journal to `index-finalized` on the
|
|
@@ -181,15 +180,15 @@ export async function indexWrittenAssets(stashDir, filePaths, options = {}) {
|
|
|
181
180
|
})();
|
|
182
181
|
if (mutated) {
|
|
183
182
|
const config = loadConfig();
|
|
184
|
-
|
|
185
|
-
|
|
183
|
+
await generateEmbeddingsForDb(db, config, () => { }, undefined, [...targetEntryIds]);
|
|
184
|
+
publishTargetedEmbeddingMeta(db, config);
|
|
186
185
|
}
|
|
187
186
|
}
|
|
188
187
|
finally {
|
|
189
188
|
closeDatabase(db);
|
|
190
189
|
}
|
|
191
190
|
return true;
|
|
192
|
-
});
|
|
191
|
+
})();
|
|
193
192
|
}
|
|
194
193
|
catch (error) {
|
|
195
194
|
// A permission fault is the one failure the next full index will NOT heal,
|
package/dist/indexer/indexer.js
CHANGED
|
@@ -27,7 +27,6 @@ import { upsertUtilityScore } from "../storage/repositories/index-utility-reposi
|
|
|
27
27
|
import { getEmbeddingCount, isVecAvailable, isVecFastPathReady, warnIfVecMissing, } from "../storage/repositories/index-vec-repository.js";
|
|
28
28
|
import { assertIndexedWorkflowSourceIdentity, WorkflowSourceIdentityError } from "../workflows/source-files.js";
|
|
29
29
|
import { deleteStoredGraph } from "./db/graph-db.js";
|
|
30
|
-
import { withIndexWriterLease } from "./index-writer-lock.js";
|
|
31
30
|
import { deriveEntryProvenance, deriveInstallations } from "./installations.js";
|
|
32
31
|
import { indexedPathMatchesOwner, resolveAdapterConceptOwner, } from "./lookup/adapter-concept-owner.js";
|
|
33
32
|
import { generateEmbeddingsForDb } from "./materialize-embeddings.js";
|
|
@@ -35,7 +34,6 @@ import { canUseIncrementalSkip, computeDirFingerprint, getCachedZeroRowDirState,
|
|
|
35
34
|
import { isEnrichmentComplete, isWorkflowSkipWarning } from "./passes/metadata.js";
|
|
36
35
|
import { drainDirDocuments } from "./scan/drain-dir.js";
|
|
37
36
|
import { buildSearchText } from "./search/search-fields.js";
|
|
38
|
-
import { clearSemanticStatus, deriveSemanticProviderFingerprint, writeSemanticStatus } from "./search/semantic-status.js";
|
|
39
37
|
import { purgeOldUsageEvents } from "./usage/usage-events.js";
|
|
40
38
|
import { walkStashFlatWithStatus } from "./walk/walker.js";
|
|
41
39
|
function collectLoweringNotices(target, notices) {
|
|
@@ -265,27 +263,6 @@ async function runFinalizePhase(ctx, deferredUpdateTransaction) {
|
|
|
265
263
|
const semanticEntryCount = getEmbeddableEntryCount(db);
|
|
266
264
|
onProgress({ phase: "finalize", message: "Verifying semantic search state." });
|
|
267
265
|
const verification = verifyIndexState(db, config, semanticEntryCount, embeddingResult);
|
|
268
|
-
const persistSemanticStatus = () => {
|
|
269
|
-
if (config.semanticSearchMode === "off") {
|
|
270
|
-
clearSemanticStatus();
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
|
-
writeSemanticStatus({
|
|
274
|
-
status: verification.semanticStatus === "disabled" ? "pending" : verification.semanticStatus,
|
|
275
|
-
...(embeddingResult.reason ? { reason: embeddingResult.reason } : {}),
|
|
276
|
-
...(embeddingResult.message ? { message: embeddingResult.message } : {}),
|
|
277
|
-
providerFingerprint: deriveSemanticProviderFingerprint(config.embedding),
|
|
278
|
-
lastCheckedAt: new Date().toISOString(),
|
|
279
|
-
entryCount: verification.entryCount,
|
|
280
|
-
embeddingCount: verification.embeddingCount,
|
|
281
|
-
});
|
|
282
|
-
};
|
|
283
|
-
if (deferredUpdateTransaction) {
|
|
284
|
-
deferredUpdateTransaction.afterCommit = persistSemanticStatus;
|
|
285
|
-
}
|
|
286
|
-
else {
|
|
287
|
-
persistSemanticStatus();
|
|
288
|
-
}
|
|
289
266
|
onProgress({ phase: "verify", message: verification.message });
|
|
290
267
|
// Store verification result and totalEntries on ctx for the caller to use
|
|
291
268
|
ctx.verification = verification;
|
|
@@ -492,20 +469,7 @@ async function akmIndexReal(options) {
|
|
|
492
469
|
"Pass `akm index --clean --dry-run`, or drop `--dry-run` to run a real index.", "INVALID_FLAG_VALUE", "Run `akm index --clean --dry-run` to preview, or `akm index --clean` to apply.");
|
|
493
470
|
}
|
|
494
471
|
const requestedAt = Date.now();
|
|
495
|
-
|
|
496
|
-
return withIndexWriterLease({
|
|
497
|
-
purpose: "akm-index",
|
|
498
|
-
signal: options?.signal,
|
|
499
|
-
onWait: ({ waitedMs }) => {
|
|
500
|
-
options?.onProgress?.({
|
|
501
|
-
phase: "preflight",
|
|
502
|
-
message: `Waiting for index writer lease (${Math.round(waitedMs / 1000)}s elapsed).`,
|
|
503
|
-
});
|
|
504
|
-
},
|
|
505
|
-
onAcquired: ({ waitedMs }) => {
|
|
506
|
-
acquiredAt = requestedAt + waitedMs;
|
|
507
|
-
},
|
|
508
|
-
}, async () => {
|
|
472
|
+
return (async () => {
|
|
509
473
|
const stashDir = options.stashDir;
|
|
510
474
|
const onProgress = options?.onProgress ?? (() => { });
|
|
511
475
|
const signal = options?.signal;
|
|
@@ -644,7 +608,6 @@ async function akmIndexReal(options) {
|
|
|
644
608
|
finalizeMs: timing.tFinalizeEnd - timing.tFinalizeStart,
|
|
645
609
|
cleanMs: clean ? cleanEnd - cleanStart : 0,
|
|
646
610
|
preflightMs: timing.t0 - requestedAt,
|
|
647
|
-
leaseWaitMs: acquiredAt - requestedAt,
|
|
648
611
|
sourceCacheMs: sourceCacheEnd - sourceCacheStart,
|
|
649
612
|
endToEndMs: Date.now() - requestedAt,
|
|
650
613
|
},
|
|
@@ -658,7 +621,7 @@ async function akmIndexReal(options) {
|
|
|
658
621
|
if (!borrowedUpdateDb)
|
|
659
622
|
closeDatabase(db);
|
|
660
623
|
}
|
|
661
|
-
});
|
|
624
|
+
})();
|
|
662
625
|
}
|
|
663
626
|
function buildIndexedSourceOwners(sources) {
|
|
664
627
|
const installations = deriveInstallations([...sources]);
|
|
@@ -3,11 +3,25 @@
|
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
4
|
import { isVerbose, warn, warnVerbose } from "../core/warn.js";
|
|
5
5
|
import { embedBatch } from "../llm/embedder.js";
|
|
6
|
-
import {
|
|
6
|
+
import { DETERMINISTIC_EMBED_MODEL_ID, isDeterministicEmbedEnabled } from "../llm/embedders/deterministic.js";
|
|
7
|
+
import { DEFAULT_LOCAL_MODEL } from "../llm/embedders/local.js";
|
|
8
|
+
import { buildTokenBoundedBatches, DEFAULT_REMOTE_BATCH_SIZE, DEFAULT_TOKEN_BUDGET, estimateTokenCount, hasRemoteEndpoint, } from "../llm/embedders/remote.js";
|
|
7
9
|
import { getEmbeddableEntryCount } from "../storage/repositories/index-entries-repository.js";
|
|
8
10
|
import { deleteMeta, getMeta, setMeta } from "../storage/repositories/index-meta-repository.js";
|
|
9
|
-
import { getAllEntriesForEmbedding, getEmbeddingCount,
|
|
10
|
-
|
|
11
|
+
import { getAllEntriesForEmbedding, getEmbeddingCount, isVecFastPathComplete, isVecFastPathReady, purgeEmbeddings, setVecFastPathReady, upsertEmbedding, } from "../storage/repositories/index-vec-repository.js";
|
|
12
|
+
/** Identifies the embedding provider+model+dimension a stored vector was generated with. */
|
|
13
|
+
export function deriveSemanticProviderFingerprint(embedding) {
|
|
14
|
+
if (isDeterministicEmbedEnabled()) {
|
|
15
|
+
return `deterministic:${DETERMINISTIC_EMBED_MODEL_ID}`;
|
|
16
|
+
}
|
|
17
|
+
if (embedding?.endpoint) {
|
|
18
|
+
// Fingerprint keys on vector identity only (model + dimension). The endpoint
|
|
19
|
+
// is transport/routing and has no bearing on vector compatibility, so moving
|
|
20
|
+
// the same model+dimension to a different host must not force a full re-embed.
|
|
21
|
+
return `remote:${embedding.model}|${embedding.dimension ?? "default"}`;
|
|
22
|
+
}
|
|
23
|
+
return `local:${embedding?.localModel ?? DEFAULT_LOCAL_MODEL}`;
|
|
24
|
+
}
|
|
11
25
|
function throwIfAborted(signal) {
|
|
12
26
|
if (signal?.aborted) {
|
|
13
27
|
throw signal.reason instanceof Error ? signal.reason : new Error("index interrupted");
|
|
@@ -17,7 +31,7 @@ export async function generateEmbeddingsForDb(db, config, onProgress, signal, en
|
|
|
17
31
|
throwIfAborted(signal);
|
|
18
32
|
if (config.semanticSearchMode === "off") {
|
|
19
33
|
onProgress({ phase: "embeddings", message: "Semantic search disabled; skipping embeddings." });
|
|
20
|
-
return { success: false,
|
|
34
|
+
return { success: false, message: "Semantic search is disabled." };
|
|
21
35
|
}
|
|
22
36
|
// A targeted call starts from an already-published generation. Preserve its
|
|
23
37
|
// trust decision in O(1): successful writes for the changed IDs keep a
|
|
@@ -48,14 +62,35 @@ export async function generateEmbeddingsForDb(db, config, onProgress, signal, en
|
|
|
48
62
|
});
|
|
49
63
|
const texts = allEntries.map((entry) => entry.searchText);
|
|
50
64
|
if (isVerbose()) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
65
|
+
// Mirror RemoteEmbedder's actual token-bounded batching (#874) so this
|
|
66
|
+
// log reflects the real request grouping rather than a fixed count of
|
|
67
|
+
// 100 that no longer matches what gets sent over the wire. Local runs
|
|
68
|
+
// don't batch by size at all (LocalEmbedder chunks by a fixed count
|
|
69
|
+
// for inference throughput only, never fails/skips), so there's
|
|
70
|
+
// nothing meaningful to report per-batch for them.
|
|
71
|
+
if (hasRemoteEndpoint(config.embedding ?? {})) {
|
|
72
|
+
const tokenBudget = config.embedding?.maxTokens ?? config.embedding?.contextLength ?? DEFAULT_TOKEN_BUDGET;
|
|
73
|
+
const maxCount = config.embedding?.batchSize ?? DEFAULT_REMOTE_BATCH_SIZE;
|
|
74
|
+
const batches = buildTokenBoundedBatches(texts, tokenBudget, maxCount);
|
|
75
|
+
const batchNumberByIndex = new Map();
|
|
76
|
+
batches.forEach((batch, batchIdx) => {
|
|
77
|
+
for (const i of batch.indices)
|
|
78
|
+
batchNumberByIndex.set(i, batchIdx + 1);
|
|
79
|
+
});
|
|
80
|
+
for (const [i, entry] of allEntries.entries()) {
|
|
81
|
+
const chars = entry.searchText.length;
|
|
82
|
+
const tokens = estimateTokenCount(entry.searchText);
|
|
83
|
+
const batch = batches[batchNumberByIndex.get(i) - 1];
|
|
84
|
+
const label = batch?.oversized
|
|
85
|
+
? "oversized (skipped)"
|
|
86
|
+
: `batch ${batchNumberByIndex.get(i)}/${batches.length}`;
|
|
87
|
+
warnVerbose(`[embed] ${entry.itemRef} (${chars} chars, est. ${tokens} tokens) → ${label}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
for (const entry of allEntries) {
|
|
92
|
+
warnVerbose(`[embed] ${entry.itemRef} (${entry.searchText.length} chars, est. ${estimateTokenCount(entry.searchText)} tokens)`);
|
|
93
|
+
}
|
|
59
94
|
}
|
|
60
95
|
}
|
|
61
96
|
let heartbeatTimer;
|
|
@@ -66,17 +101,24 @@ export async function generateEmbeddingsForDb(db, config, onProgress, signal, en
|
|
|
66
101
|
message: `Still generating embeddings for ${allEntries.length} entr${allEntries.length === 1 ? "y" : "ies"}; waiting on embedding provider.`,
|
|
67
102
|
});
|
|
68
103
|
}, 15000);
|
|
69
|
-
|
|
104
|
+
// A failing sub-batch or an oversized document is SKIPPED by embedBatch,
|
|
105
|
+
// not thrown (#874) — collect what couldn't be embedded and why, so a
|
|
106
|
+
// few bad documents don't discard every other entry's embedding.
|
|
107
|
+
const skips = [];
|
|
108
|
+
const embeddings = await embedBatch(texts, config.embedding, signal, (skip) => skips.push(skip));
|
|
70
109
|
throwIfAborted(signal);
|
|
71
110
|
let storedCount = 0;
|
|
72
111
|
let skippedCount = 0;
|
|
112
|
+
let embedFailedCount = 0;
|
|
73
113
|
let vecFailedCount = 0;
|
|
74
114
|
let vecUnavailableCount = 0;
|
|
75
115
|
db.transaction(() => {
|
|
76
116
|
for (const [i, entry] of allEntries.entries()) {
|
|
77
117
|
const embedding = embeddings[i];
|
|
78
|
-
if (!embedding)
|
|
79
|
-
|
|
118
|
+
if (!embedding) {
|
|
119
|
+
embedFailedCount++;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
80
122
|
const result = upsertEmbedding(db, entry.id, embedding);
|
|
81
123
|
if (result.stored)
|
|
82
124
|
storedCount++;
|
|
@@ -91,6 +133,14 @@ export async function generateEmbeddingsForDb(db, config, onProgress, signal, en
|
|
|
91
133
|
if (skippedCount > 0) {
|
|
92
134
|
warn(`[embed] ${skippedCount} embedding${skippedCount === 1 ? "" : "s"} skipped (entry deleted between queue and write)`);
|
|
93
135
|
}
|
|
136
|
+
if (embedFailedCount > 0) {
|
|
137
|
+
const detail = skips
|
|
138
|
+
.slice(0, 20)
|
|
139
|
+
.map((skip) => ` - ${allEntries[skip.index]?.itemRef ?? skip.index} (${skip.reason}): ${skip.message}`)
|
|
140
|
+
.join("\n");
|
|
141
|
+
const more = skips.length > 20 ? `\n ...and ${skips.length - 20} more` : "";
|
|
142
|
+
warn(`[embed] ${embedFailedCount} embedding${embedFailedCount === 1 ? "" : "s"} could not be generated and ${embedFailedCount === 1 ? "was" : "were"} skipped:\n${detail}${more}`);
|
|
143
|
+
}
|
|
94
144
|
const vecGenerationComplete = targetEntryIds === undefined ? isVecFastPathComplete(db) : vecFastPathWasReady;
|
|
95
145
|
setVecFastPathReady(db, vecFailedCount === 0 && vecUnavailableCount === 0 && vecGenerationComplete);
|
|
96
146
|
if (vecFailedCount > 0) {
|
|
@@ -103,6 +153,19 @@ export async function generateEmbeddingsForDb(db, config, onProgress, signal, en
|
|
|
103
153
|
message: `Stored ${storedCount} embedding${storedCount === 1 ? "" : "s"}.`,
|
|
104
154
|
});
|
|
105
155
|
setMeta(db, "embeddingFingerprint", currentFingerprint);
|
|
156
|
+
// Only a total failure (nothing at all embedded, despite having entries
|
|
157
|
+
// to embed) turns into a phase failure. Any partial success — the vast
|
|
158
|
+
// majority of a large bundle embedding fine around a handful of skips —
|
|
159
|
+
// must not discard what DID get stored (#874).
|
|
160
|
+
if (storedCount === 0 && embedFailedCount > 0) {
|
|
161
|
+
const firstMessage = skips[0]?.message ?? "All embeddings failed.";
|
|
162
|
+
// #873 removed the persisted semantic verdict, so there is no failure
|
|
163
|
+
// class to record — just report what happened on this run.
|
|
164
|
+
return {
|
|
165
|
+
success: false,
|
|
166
|
+
message: `All ${embedFailedCount} embedding batch(es) failed: ${firstMessage}`,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
106
169
|
return { success: true, vecInsertFailures: vecFailedCount };
|
|
107
170
|
}
|
|
108
171
|
finally {
|
|
@@ -116,40 +179,21 @@ export async function generateEmbeddingsForDb(db, config, onProgress, signal, en
|
|
|
116
179
|
onProgress({ phase: "embeddings", message: `Embedding generation failed: ${message}` });
|
|
117
180
|
return {
|
|
118
181
|
success: false,
|
|
119
|
-
reason: classifySemanticFailure(message),
|
|
120
182
|
message: `Semantic search verification failed: ${message}`,
|
|
121
183
|
};
|
|
122
184
|
}
|
|
123
185
|
}
|
|
124
|
-
/**
|
|
125
|
-
|
|
186
|
+
/**
|
|
187
|
+
* Update the `hasEmbeddings` DB fact after a targeted mutation, from the
|
|
188
|
+
* index's actual current embedding coverage — read fresh, not cached.
|
|
189
|
+
*/
|
|
190
|
+
export function publishTargetedEmbeddingMeta(db, config) {
|
|
126
191
|
if (config.semanticSearchMode === "off") {
|
|
127
|
-
clearSemanticStatus();
|
|
128
192
|
setMeta(db, "hasEmbeddings", "0");
|
|
129
193
|
return;
|
|
130
194
|
}
|
|
131
195
|
const entryCount = getEmbeddableEntryCount(db);
|
|
132
196
|
const embeddingCount = getEmbeddingCount(db);
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
status = "pending";
|
|
136
|
-
else if (embeddingCount >= entryCount)
|
|
137
|
-
status = isVecAvailable(db) && isVecFastPathReady(db) ? "ready-vec" : "ready-js";
|
|
138
|
-
else
|
|
139
|
-
status = "blocked";
|
|
140
|
-
setMeta(db, "hasEmbeddings", status === "ready-js" || status === "ready-vec" ? "1" : "0");
|
|
141
|
-
writeSemanticStatus({
|
|
142
|
-
status,
|
|
143
|
-
...(status === "blocked" ? { reason: result.reason ?? "index-failed" } : {}),
|
|
144
|
-
...(status === "blocked"
|
|
145
|
-
? {
|
|
146
|
-
message: result.message ??
|
|
147
|
-
`Semantic search verification failed (${embeddingCount}/${entryCount} embeddings available).`,
|
|
148
|
-
}
|
|
149
|
-
: {}),
|
|
150
|
-
providerFingerprint: deriveSemanticProviderFingerprint(config.embedding),
|
|
151
|
-
lastCheckedAt: new Date().toISOString(),
|
|
152
|
-
entryCount,
|
|
153
|
-
embeddingCount,
|
|
154
|
-
});
|
|
197
|
+
const ready = entryCount > 0 && embeddingCount >= entryCount;
|
|
198
|
+
setMeta(db, "hasEmbeddings", ready ? "1" : "0");
|
|
155
199
|
}
|
|
@@ -6,7 +6,7 @@ import path from "node:path";
|
|
|
6
6
|
import { parseBundleRef } from "../../core/asset/asset-ref.js";
|
|
7
7
|
import { parseFrontmatter } from "../../core/asset/frontmatter.js";
|
|
8
8
|
import { asNonEmptyString } from "../../core/common.js";
|
|
9
|
-
import { isVerbose, warn } from "../../core/warn.js";
|
|
9
|
+
import { isVerbose, warn, warnVerbose } from "../../core/warn.js";
|
|
10
10
|
export const SCOPE_KEYS = ["user", "agent", "run", "channel"];
|
|
11
11
|
// ── Quality semantics (v1 spec §4.2) ────────────────────────────────────────
|
|
12
12
|
/**
|
|
@@ -1119,7 +1119,7 @@ function stripMarkdownLinkDestinations(text, nesting = 0) {
|
|
|
1119
1119
|
* secret/env/session bytes; that policy is enforced at the adapter metadata
|
|
1120
1120
|
* boundary below.
|
|
1121
1121
|
*/
|
|
1122
|
-
export function projectMarkdownContent(body) {
|
|
1122
|
+
export function projectMarkdownContent(body, truncationInfo) {
|
|
1123
1123
|
const lines = body.split(/\r?\n/);
|
|
1124
1124
|
const innerBlock = findInnerFrontmatterBlock(lines);
|
|
1125
1125
|
const start = innerBlock && isFrontmatterShaped(lines, innerBlock) ? innerBlock.close + 1 : 0;
|
|
@@ -1168,6 +1168,8 @@ export function projectMarkdownContent(body) {
|
|
|
1168
1168
|
const text = projected.join(" ").replace(/\s+/g, " ").trim();
|
|
1169
1169
|
if (!text)
|
|
1170
1170
|
return undefined;
|
|
1171
|
+
if (truncationInfo)
|
|
1172
|
+
truncationInfo.truncated = text.length > MARKDOWN_CONTENT_MAX_CHARS;
|
|
1171
1173
|
return truncateUnicodeSafe(text, MARKDOWN_CONTENT_MAX_CHARS);
|
|
1172
1174
|
}
|
|
1173
1175
|
// ── Metadata Generation ─────────────────────────────────────────────────────
|
|
@@ -1212,9 +1214,15 @@ export function applyPreContributorFields(entry, file, ctx, pkgMeta) {
|
|
|
1212
1214
|
// Native Markdown has one bounded low-weight body projection. Sensitive
|
|
1213
1215
|
// types and raw session/checkpoint material never cross this boundary.
|
|
1214
1216
|
if (entry.type !== "env" && entry.type !== "session" && !hasSessionMemoryMarker(parsed.data, parsed.content)) {
|
|
1215
|
-
const
|
|
1216
|
-
|
|
1217
|
+
const truncationInfo = { truncated: false };
|
|
1218
|
+
const contentProjection = projectMarkdownContent(parsed.content, truncationInfo);
|
|
1219
|
+
if (contentProjection) {
|
|
1217
1220
|
entry.content = contentProjection;
|
|
1221
|
+
if (truncationInfo.truncated) {
|
|
1222
|
+
entry.contentTruncated = true;
|
|
1223
|
+
warnVerbose(`${file}: indexed content truncated to ${MARKDOWN_CONTENT_MAX_CHARS} chars`);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1218
1226
|
}
|
|
1219
1227
|
// Extract parameters from template placeholders ($1, $ARGUMENTS, {{named}})
|
|
1220
1228
|
if (entry.type === "command") {
|
|
@@ -58,6 +58,8 @@ export function indexDocumentToStashEntry(doc) {
|
|
|
58
58
|
entry.tags = doc.tags;
|
|
59
59
|
if (doc.content !== undefined)
|
|
60
60
|
entry.content = doc.content;
|
|
61
|
+
if (doc.contentTruncated !== undefined)
|
|
62
|
+
entry.contentTruncated = doc.contentTruncated;
|
|
61
63
|
if (doc.ownsPresentation !== undefined)
|
|
62
64
|
entry.ownsPresentation = doc.ownsPresentation;
|
|
63
65
|
if (doc.updated !== undefined)
|
|
@@ -26,7 +26,7 @@ import { assertIndexPathReadable, closeDatabase, openExistingDatabase, } from ".
|
|
|
26
26
|
import { getAllEntries, getBaseBeliefStatesForDerivedTwins, getEntryById, getEntryCount, getPositiveFeedbackCountsByIds, } from "../../storage/repositories/index-entries-repository.js";
|
|
27
27
|
import { searchFts } from "../../storage/repositories/index-fts-repository.js";
|
|
28
28
|
import { getMeta } from "../../storage/repositories/index-meta-repository.js";
|
|
29
|
-
import { searchVec } from "../../storage/repositories/index-vec-repository.js";
|
|
29
|
+
import { getEmbeddingCount, searchVec } from "../../storage/repositories/index-vec-repository.js";
|
|
30
30
|
import { getCurrentWorkflowScopeKey } from "../../workflows/authoring/scope-key.js";
|
|
31
31
|
import { ensureIndex } from "../ensure-index.js";
|
|
32
32
|
import { collectGraphRelatedHit, loadGraphBoostContext } from "../graph/graph-boost.js";
|
|
@@ -38,7 +38,6 @@ import { typeBoostFor } from "./ranking-contributors.js";
|
|
|
38
38
|
import { attachSearchHitAttribution, copySearchHitAttribution, getSearchHitAttribution } from "./search-attribution.js";
|
|
39
39
|
import { enrichSearchHit } from "./search-hit-enrichers.js";
|
|
40
40
|
import { buildEditHint, findSourceForPath, isEditable } from "./search-source.js";
|
|
41
|
-
import { deriveSemanticProviderFingerprint, getEffectiveSemanticStatus, isSemanticRuntimeReady, readSemanticStatus, } from "./semantic-status.js";
|
|
42
41
|
/**
|
|
43
42
|
* Age past which search surfaces a "run akm index" hint. Reads serve the
|
|
44
43
|
* existing index as-is (freshness is the writers' job — `indexWrittenAssets`
|
|
@@ -126,49 +125,14 @@ export async function searchLocal(input) {
|
|
|
126
125
|
const disableScopedUtility = input.disableScopedUtility === true;
|
|
127
126
|
const rendererRegistry = input.rendererRegistry ?? defaultRendererRegistry;
|
|
128
127
|
const allSourceDirs = sources.map((s) => s.path);
|
|
129
|
-
const rawStatus = readSemanticStatus();
|
|
130
|
-
const semanticStatus = getEffectiveSemanticStatus(config, rawStatus);
|
|
131
128
|
const warnings = [];
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
// configured (e.g. `akm setup --yes` ran without picking one), telling
|
|
140
|
-
// the user to "run akm setup" is misleading — they just did. Surface
|
|
141
|
-
// the actual remediation: configure an embedding endpoint OR switch
|
|
142
|
-
// semanticSearchMode to `off` to silence the warning.
|
|
143
|
-
warnings.push("Semantic search is enabled (semanticSearchMode='auto') but no embedding provider is configured. " +
|
|
144
|
-
'Either: (a) `akm config set embedding \'{"endpoint":"...","model":"..."}\'`, or ' +
|
|
145
|
-
"(b) `akm config set semanticSearchMode off` to use keyword-only search.");
|
|
146
|
-
}
|
|
147
|
-
else {
|
|
148
|
-
warnings.push("Semantic search is pending verification. Run 'akm index --full' to build the semantic index now, or wait for the next background index pass.");
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
if (config.semanticSearchMode === "auto" && semanticStatus === "blocked") {
|
|
152
|
-
if (!hasConfiguredEmbeddingProvider(config)) {
|
|
153
|
-
// F7/A2: same predicate as the `pending` branch above (#480) — a
|
|
154
|
-
// `blocked` status can outlive the provider config that produced it
|
|
155
|
-
// (e.g. the embedding config was later unset). This is not a fault;
|
|
156
|
-
// there is simply nothing configured to use.
|
|
157
|
-
warnings.push("Semantic search is enabled (semanticSearchMode='auto') but no embedding provider is configured. " +
|
|
158
|
-
'Either: (a) `akm config set embedding \'{"endpoint":"...","model":"..."}\'`, or ' +
|
|
159
|
-
"(b) `akm config set semanticSearchMode off` to use keyword-only search.");
|
|
160
|
-
}
|
|
161
|
-
else {
|
|
162
|
-
// F7/A2: surface the ACTUAL diagnostic instead of one fixed generic
|
|
163
|
-
// string. `rawStatus.reason`/`rawStatus.message` record why the
|
|
164
|
-
// semantic backend failed (auth, network, a stuck local-model
|
|
165
|
-
// download, …) — read at `readSemanticStatus()` above and, before
|
|
166
|
-
// this fix, discarded here in favor of a message that never varied.
|
|
167
|
-
const detail = rawStatus?.message ?? (rawStatus?.reason ? `reason: ${rawStatus.reason}` : undefined);
|
|
168
|
-
warnings.push(`Semantic search is blocked${detail ? ` (${detail})` : ""}. Using keyword search until the semantic ` +
|
|
169
|
-
"backend is healthy again. Run 'akm index --full' to retry, or " +
|
|
170
|
-
"`akm config set semanticSearchMode off` to silence this warning.");
|
|
171
|
-
}
|
|
129
|
+
// Semantic search is attempted fresh on every query (see `tryVecScores`);
|
|
130
|
+
// there is no cached readiness verdict to consult here. The only thing
|
|
131
|
+
// worth flagging ahead of the attempt is a config that can never succeed.
|
|
132
|
+
if (config.semanticSearchMode === "auto" && !hasConfiguredEmbeddingProvider(config)) {
|
|
133
|
+
warnings.push("Semantic search is enabled (semanticSearchMode='auto') but no embedding provider is configured. " +
|
|
134
|
+
'Either: (a) `akm config set embedding \'{"endpoint":"...","model":"..."}\'`, or ' +
|
|
135
|
+
"(b) `akm config set semanticSearchMode off` to use keyword-only search.");
|
|
172
136
|
}
|
|
173
137
|
// Bootstrap-only: builds the index inline when it cannot serve this stash.
|
|
174
138
|
// Content freshness is the writers' job (indexWrittenAssets + full runs);
|
|
@@ -662,11 +626,14 @@ function matchBeliefFilter(beliefState, filter) {
|
|
|
662
626
|
}
|
|
663
627
|
// ── Vector scorer ───────────────────────────────────────────────────────────
|
|
664
628
|
async function tryVecScores(db, query, k, config) {
|
|
665
|
-
|
|
666
|
-
if (!isSemanticRuntimeReady(semanticStatus))
|
|
629
|
+
if (config.semanticSearchMode === "off")
|
|
667
630
|
return { scores: null };
|
|
668
|
-
|
|
669
|
-
|
|
631
|
+
// A real-time completeness fact, not a cached verdict: skip the network
|
|
632
|
+
// round trip only when the index has never embedded anything. A PARTIAL
|
|
633
|
+
// failure (some entries embedded, one write degraded) still attempts —
|
|
634
|
+
// and if the endpoint is genuinely down, the failure surfaces as a live
|
|
635
|
+
// `semanticWarning` below instead of silently skipping with no signal.
|
|
636
|
+
if (getEmbeddingCount(db) === 0)
|
|
670
637
|
return { scores: null };
|
|
671
638
|
try {
|
|
672
639
|
const { embed } = await import("../../llm/embedder.js");
|