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.
Files changed (123) hide show
  1. package/CHANGELOG.md +265 -0
  2. package/dist/commands/env/env-cli.js +1 -2
  3. package/dist/commands/env/secret-cli.js +1 -5
  4. package/dist/commands/feedback-cli.js +0 -4
  5. package/dist/commands/health/checks.js +0 -32
  6. package/dist/commands/health/surfaces.js +2 -2
  7. package/dist/commands/health.js +6 -15
  8. package/dist/commands/improve/anti-collapse.js +4 -91
  9. package/dist/commands/improve/autonomy-gate.js +1 -1
  10. package/dist/commands/improve/consolidate.js +25 -26
  11. package/dist/commands/improve/distill.js +2 -2
  12. package/dist/commands/improve/extract.js +8 -1
  13. package/dist/commands/improve/improve.js +3 -5
  14. package/dist/commands/improve/locks.js +13 -5
  15. package/dist/commands/improve/loop-stages.js +8 -9
  16. package/dist/commands/improve/memory/memory-contradiction-detect.js +1 -19
  17. package/dist/commands/improve/outcome-loop.js +0 -6
  18. package/dist/commands/improve/reflect.js +20 -9
  19. package/dist/commands/lint/index.js +4 -6
  20. package/dist/commands/proposal/validators/proposal-quality-validators.js +0 -7
  21. package/dist/commands/proposal/validators/proposal-validators.js +12 -0
  22. package/dist/commands/read/search.js +14 -24
  23. package/dist/commands/sources/bundle-config-ops.js +9 -4
  24. package/dist/commands/sources/info.js +14 -10
  25. package/dist/commands/tasks/tasks-cli.js +56 -3
  26. package/dist/commands/tasks/tasks.js +101 -18
  27. package/dist/commands/workflow-cli.js +2 -2
  28. package/dist/core/adapter/adapters/akm-adapter.js +2 -0
  29. package/dist/core/common.js +61 -24
  30. package/dist/core/config/config-io.js +2 -2
  31. package/dist/core/config/config-sources.js +32 -2
  32. package/dist/core/config/config-version-shim.js +101 -0
  33. package/dist/core/config/config.js +7 -7
  34. package/dist/core/config/schema/engines.js +9 -7
  35. package/dist/core/config/schema/primitives.js +0 -5
  36. package/dist/core/config/schema/search.js +2 -1
  37. package/dist/core/file-lock.js +2 -1
  38. package/dist/core/improve-result.js +35 -14
  39. package/dist/core/maintenance-barrier.js +2 -14
  40. package/dist/core/paths.js +0 -3
  41. package/dist/core/redaction.js +2 -2
  42. package/dist/core/spawn-env.js +8 -12
  43. package/dist/core/state/migrations.js +1 -12
  44. package/dist/core/state-db.js +9 -27
  45. package/dist/core/write-source.js +12 -19
  46. package/dist/execution/directory-identity.js +36 -10
  47. package/dist/execution/guarded-source.js +0 -10
  48. package/dist/indexer/graph/graph-boost.js +0 -4
  49. package/dist/indexer/index-writer-lock.js +43 -24
  50. package/dist/indexer/index-written-assets.js +5 -6
  51. package/dist/indexer/indexer.js +2 -39
  52. package/dist/indexer/materialize-embeddings.js +85 -41
  53. package/dist/indexer/passes/metadata.js +12 -4
  54. package/dist/indexer/scan/doc-to-entry.js +2 -0
  55. package/dist/indexer/search/db-search.js +15 -48
  56. package/dist/indexer/search/ranking-contributors.js +0 -25
  57. package/dist/indexer/search/ranking.js +3 -13
  58. package/dist/indexer/search/search-fields.js +16 -1
  59. package/dist/integrations/agent/builder-shared.js +0 -25
  60. package/dist/integrations/agent/model-map.js +2 -60
  61. package/dist/integrations/harnesses/aider/agent-builder.js +1 -3
  62. package/dist/integrations/harnesses/amazonq/agent-builder.js +2 -7
  63. package/dist/integrations/harnesses/claude/agent-builder.js +1 -4
  64. package/dist/integrations/harnesses/codex/agent-builder.js +1 -4
  65. package/dist/integrations/harnesses/copilot/agent-builder.js +2 -6
  66. package/dist/integrations/harnesses/gemini/agent-builder.js +2 -7
  67. package/dist/integrations/harnesses/opencode/agent-builder.js +1 -4
  68. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +2 -16
  69. package/dist/integrations/harnesses/openhands/agent-builder.js +1 -3
  70. package/dist/integrations/harnesses/pi/agent-builder.js +2 -4
  71. package/dist/integrations/session-logs/index.js +0 -9
  72. package/dist/llm/client.js +75 -42
  73. package/dist/llm/embedder.js +7 -3
  74. package/dist/llm/embedders/remote.js +141 -42
  75. package/dist/output/shapes/passthrough.js +17 -5
  76. package/dist/registry/network.js +5 -37
  77. package/dist/runtime.js +2 -10
  78. package/dist/scripts/akm-migrate-node.js +180 -238
  79. package/dist/scripts/akm-migrate.js +180 -238
  80. package/dist/setup/engine-config.js +2 -5
  81. package/dist/setup/registry-stash-loader.js +0 -8
  82. package/dist/setup/setup.js +9 -46
  83. package/dist/setup/steps/connection-shared.js +10 -13
  84. package/dist/sources/providers/git-install.js +1 -1
  85. package/dist/storage/engines/sqlite-migrations.js +20 -1
  86. package/dist/storage/repositories/index-entries-repository.js +0 -15
  87. package/dist/storage/repositories/proposals-repository.js +32 -6
  88. package/dist/tasks/backends/launchd.js +15 -20
  89. package/dist/tasks/backends/schtasks.js +18 -8
  90. package/dist/tasks/run/run-native-task.js +8 -6
  91. package/dist/tasks/scheduler-binding.js +15 -5
  92. package/dist/tasks/scheduler-sync-preview.js +4 -2
  93. package/dist/tasks/scheduler-sync.js +77 -42
  94. package/dist/tasks/source/bounded-document.js +2 -4
  95. package/dist/tasks/source/task-source-v3-frozen.js +5 -7
  96. package/dist/tasks/source/task-source-v4.js +5 -10
  97. package/dist/tasks/source/task-to-v3.js +29 -22
  98. package/dist/tasks/source/task-to-v4.js +1 -14
  99. package/dist/tasks/source-v3.js +6 -6
  100. package/dist/workflows/exec/native-executor.js +21 -31
  101. package/dist/workflows/exec/run-workflow.js +5 -6
  102. package/dist/workflows/exec/scheduler.js +3 -19
  103. package/dist/workflows/exec/step-work.js +1 -4
  104. package/dist/workflows/exec/unit-dispatch.js +2 -2
  105. package/dist/workflows/exec/worktree.js +1 -13
  106. package/dist/workflows/freeze/targets/child-workflow.js +2 -10
  107. package/dist/workflows/ir/plan-hash.js +4 -6
  108. package/dist/workflows/ir/schema-v4.js +0 -12
  109. package/dist/workflows/ir/schema.js +20 -31
  110. package/dist/workflows/parser.js +11 -52
  111. package/dist/workflows/renderer.js +2 -3
  112. package/dist/workflows/resource-limits.js +11 -41
  113. package/dist/workflows/runtime/runs.js +3 -4
  114. package/dist/workflows/source-ir/schema.js +14 -30
  115. package/dist/workflows/validator.js +1 -7
  116. package/docs/reference/cli.md +25 -3
  117. package/docs/reference/configuration.md +28 -7
  118. package/docs/reference/tasks.md +10 -0
  119. package/package.json +1 -1
  120. package/schemas/akm-config.json +4 -23
  121. package/schemas/akm-task.json +1 -2
  122. package/schemas/akm-workflow.json +1 -13
  123. 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
- const INDEX_WRITER_LOCK_STALE_AFTER_MS = 12 * 60 * 60 * 1000;
11
- const INDEX_WRITER_WAIT_MS = 100;
12
- const DEFAULT_INDEX_WRITER_MAX_WAIT_MS = 10 * 60 * 1000;
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("index writer wait aborted");
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 tryAcquireIndexWriterLease(lockPath, purpose) {
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
- const probe = probeLock(lockPath, { staleAfterMs: INDEX_WRITER_LOCK_STALE_AFTER_MS });
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 acquireIndexWriterLease(options) {
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 ?? DEFAULT_INDEX_WRITER_MAX_WAIT_MS;
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 = tryAcquireIndexWriterLease(lockPath, options.purpose);
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 index writer lease for ${options.purpose}`);
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(INDEX_WRITER_WAIT_MS);
111
+ await delay(ASSET_MUTATION_WAIT_MS);
90
112
  }
91
113
  }
92
- export async function withIndexWriterLease(options, run) {
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 acquireIndexWriterLease(options);
122
+ const lease = await acquireAssetMutationLease({ purpose });
100
123
  if (!lease)
101
- throw new Error(`index writer lease unavailable for ${options.purpose}`);
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 = tryAcquireIndexWriterLease(lockPath, purpose);
148
+ lease = tryAcquireAssetMutationLease(lockPath, purpose);
130
149
  if (!lease) {
131
- if (Date.now() - startedAt >= DEFAULT_INDEX_WRITER_MAX_WAIT_MS) {
132
- throw new Error(`timed out waiting for index writer lease for ${purpose}`);
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
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, INDEX_WRITER_WAIT_MS);
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 probeIndexWriterLease() {
149
- return probeLock(getIndexWriterLockPath(), { staleAfterMs: INDEX_WRITER_LOCK_STALE_AFTER_MS });
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, publishTargetedSemanticStatus } from "./materialize-embeddings.js";
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 withIndexWriterLease({ purpose: "index-written-assets" }, async () => {
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
- const embeddingResult = await generateEmbeddingsForDb(db, config, () => { }, undefined, [...targetEntryIds]);
185
- publishTargetedSemanticStatus(db, config, embeddingResult);
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,
@@ -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
- let acquiredAt = requestedAt;
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 { estimateTokenCount } from "../llm/embedders/remote.js";
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, isVecAvailable, isVecFastPathComplete, isVecFastPathReady, purgeEmbeddings, setVecFastPathReady, upsertEmbedding, } from "../storage/repositories/index-vec-repository.js";
10
- import { classifySemanticFailure, clearSemanticStatus, deriveSemanticProviderFingerprint, writeSemanticStatus, } from "./search/semantic-status.js";
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, reason: "index-missing", message: "Semantic search is disabled." };
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
- const EMBED_BATCH_SIZE = 100;
52
- const totalBatches = Math.ceil(texts.length / EMBED_BATCH_SIZE);
53
- for (const [i, entry] of allEntries.entries()) {
54
- const batchNum = Math.floor(i / EMBED_BATCH_SIZE) + 1;
55
- const chars = entry.searchText.length;
56
- const tokens = estimateTokenCount(entry.searchText);
57
- const ref = entry.itemRef;
58
- warnVerbose(`[embed] ${ref} (${chars} chars, est. ${tokens} tokens) batch ${batchNum}/${totalBatches}`);
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
- const embeddings = await embedBatch(texts, config.embedding, signal);
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
- throw new Error(`Embedding provider returned no vector for ${entry.itemRef}.`);
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
- /** Publish the canonical semantic health snapshot after a targeted mutation. */
125
- export function publishTargetedSemanticStatus(db, config, result) {
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
- let status;
134
- if (entryCount === 0)
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 contentProjection = projectMarkdownContent(parsed.content);
1216
- if (contentProjection)
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
- if (config.semanticSearchMode === "auto" && semanticStatus === "pending") {
133
- const currentFingerprint = deriveSemanticProviderFingerprint(config.embedding);
134
- if (rawStatus && rawStatus.providerFingerprint !== currentFingerprint) {
135
- warnings.push("Embedding config changed. Run 'akm index --full' to rebuild the semantic index with the new provider.");
136
- }
137
- else if (!hasConfiguredEmbeddingProvider(config)) {
138
- // #480: when semantic mode is `auto` but no embedding provider is
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
- const semanticStatus = getEffectiveSemanticStatus(config, readSemanticStatus());
666
- if (!isSemanticRuntimeReady(semanticStatus))
629
+ if (config.semanticSearchMode === "off")
667
630
  return { scores: null };
668
- const hasEmbeddings = getMeta(db, "hasEmbeddings");
669
- if (hasEmbeddings !== "1")
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");