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.
Files changed (79) hide show
  1. package/CHANGELOG.md +144 -0
  2. package/dist/assets/tasks/core/index-refresh.yml +1 -1
  3. package/dist/cli/retired-commands.js +2 -0
  4. package/dist/cli/unknown-flags.js +36 -3
  5. package/dist/commands/improve/collapse-detector.js +2 -2
  6. package/dist/commands/improve/consolidate.js +6 -4
  7. package/dist/commands/improve/improve-cli.js +1 -1
  8. package/dist/commands/proposal/repository.js +12 -3
  9. package/dist/commands/read/curate.js +34 -44
  10. package/dist/commands/read/search.js +50 -2
  11. package/dist/commands/sources/index-status.js +99 -0
  12. package/dist/commands/sources/info.js +8 -8
  13. package/dist/commands/sources/installed-stashes.js +33 -12
  14. package/dist/commands/sources/source-add.js +21 -6
  15. package/dist/commands/sources/stash-cli.js +119 -111
  16. package/dist/core/adapter/adapters/akm-adapter.js +35 -3
  17. package/dist/core/adapter/adapters/akm-metadata.js +11 -1
  18. package/dist/core/asset/asset-placement.js +35 -0
  19. package/dist/core/config/schema/embedding.js +7 -30
  20. package/dist/core/config/schema/search.js +11 -9
  21. package/dist/core/errors.js +5 -2
  22. package/dist/core/hash.js +18 -0
  23. package/dist/core/maintenance-barrier.js +8 -6
  24. package/dist/core/paths.js +0 -11
  25. package/dist/core/run-lock.js +5 -2
  26. package/dist/core/state/migrations.js +26 -1
  27. package/dist/core/state-db.js +63 -27
  28. package/dist/indexer/drain.js +306 -0
  29. package/dist/indexer/embedding-identity.js +20 -0
  30. package/dist/indexer/enrich.js +260 -0
  31. package/dist/indexer/ensure-index.js +5 -0
  32. package/dist/indexer/index-written-assets.js +133 -171
  33. package/dist/indexer/indexer.js +458 -1621
  34. package/dist/indexer/lookup/adapter-concept-owner.js +19 -5
  35. package/dist/indexer/passes/metadata.js +18 -1
  36. package/dist/indexer/reconcile.js +890 -0
  37. package/dist/indexer/scan/drain-dir.js +27 -70
  38. package/dist/indexer/scan/parse-file.js +66 -0
  39. package/dist/indexer/search/db-search.js +373 -89
  40. package/dist/indexer/search/ranking-contributors.js +21 -16
  41. package/dist/indexer/search/ranking.js +135 -57
  42. package/dist/indexer/units/unit.js +159 -0
  43. package/dist/llm/client.js +10 -1
  44. package/dist/llm/embedder.js +10 -3
  45. package/dist/llm/embedders/provider-limits.js +288 -0
  46. package/dist/llm/embedders/remote.js +133 -104
  47. package/dist/llm/feature-gate.js +4 -2
  48. package/dist/llm/rerank-client.js +3 -3
  49. package/dist/output/shapes/passthrough.js +1 -0
  50. package/dist/output/text/command-format.js +19 -13
  51. package/dist/output/text/helpers.js +1 -1
  52. package/dist/output/text/index.js +5 -2
  53. package/dist/scripts/akm-migrate-node.js +1141 -1237
  54. package/dist/scripts/akm-migrate.js +1141 -1237
  55. package/dist/setup/semantic-assets.js +2 -2
  56. package/dist/setup/steps/connection.js +3 -2
  57. package/dist/storage/repositories/files-repository.js +181 -0
  58. package/dist/storage/repositories/index-connection.js +1 -3
  59. package/dist/storage/repositories/index-entries-repository.js +77 -68
  60. package/dist/storage/repositories/index-entry-schema.js +16 -25
  61. package/dist/storage/repositories/index-fts-repository.js +29 -263
  62. package/dist/storage/repositories/index-meta-repository.js +0 -29
  63. package/dist/storage/repositories/index-schema.js +115 -122
  64. package/dist/storage/repositories/index-utility-repository.js +1 -1
  65. package/dist/storage/repositories/index-vec-repository.js +21 -334
  66. package/dist/storage/repositories/units-repository.js +510 -0
  67. package/docs/migration/release-notes/0.9.15.md +34 -36
  68. package/docs/migration/release-notes/0.9.16.md +110 -0
  69. package/docs/migration/release-notes/README.md +5 -0
  70. package/docs/reference/cli.md +93 -87
  71. package/docs/reference/configuration.md +128 -89
  72. package/docs/reference/data-and-telemetry.md +2 -1
  73. package/package.json +1 -1
  74. package/schemas/akm-config.json +2 -58
  75. package/dist/indexer/index-db-contention.js +0 -56
  76. package/dist/indexer/index-rebuild-lock.js +0 -73
  77. package/dist/indexer/materialize-embeddings.js +0 -771
  78. package/dist/indexer/passes/dir-staleness.js +0 -161
  79. package/dist/storage/repositories/embedding-salvage-repository.js +0 -184
@@ -2,90 +2,130 @@
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
4
  /**
5
- * Write-path indexing: targeted single-file index updates for asset writers.
5
+ * Write-path indexing: index exactly what a write just produced, inline, in
6
+ * the same call as the write. `remember` / `writeMarkdownAsset`, extract's
7
+ * session assets, `source clone`, and proposal acceptance all call this
8
+ * right after committing their file write so the asset is searchable
9
+ * immediately, without a background reindex.
6
10
  *
7
- * The index is maintained eagerly by every first-class mutation command
8
- * (`source add`, `wiki`, `workflow`, `setup` all run `akmIndex()` after
9
- * writing). The memory write paths `akm remember` / `writeMarkdownAsset`
10
- * and extract's session assets historically did not, which is why reads
11
- * used to compensate with stale-triggered background reindexes (the
12
- * lock-contention footgun removed alongside this module's introduction, per
13
- * the 2026-07 read-path reindex-contention findings §7).
11
+ * docs/plans/index-redesign-contract.md, module B2: a thin call to B1's
12
+ * `reconcilePaths` (the per-file version of the stat-walk reconcile — parses,
13
+ * upserts `entries`, derives units) followed by B4's `drainEmbeddingQueue`
14
+ * scoped to exactly the units that reconcile just produced. No lock probe, no
15
+ * rebuild detection, no background spawn the redesign has no full-rebuild
16
+ * pipeline for a write path to defer to; two writers reconciling the same
17
+ * path converge on the same idempotent rows instead of contending for a lock.
14
18
  *
15
- * This is NOT a general reindex. It upserts exactly the files the caller just
16
- * wrote: frontmatter/metadata via the shared matcher pipeline, the canonical
17
- * row with its transactionally owned FTS projection, and vectors for changed
18
- * entry IDs when semantic search is enabled. Index-time LLM passes, graph
19
- * extraction, `builtAt`, and the per-dir walk cache remain full-index
20
- * responsibilities.
19
+ * FAIL-OPEN at every step: any error (index.db absent, empty, unreadable,
20
+ * unparseable file) is reduced to a verbose-only warning and the write
21
+ * command succeeds untouched the degraded outcome is the asset appearing
22
+ * after the next reconcile (an explicit `akm index`, or the schedule) rather
23
+ * than immediately.
21
24
  */
22
- import fs from "node:fs";
23
25
  import path from "node:path";
24
- import { akmAdapter } from "../core/adapter/adapters/akm-adapter.js";
25
26
  import { loadConfig } from "../core/config/config.js";
26
27
  import { isDataDirUnreadableError } from "../core/errors.js";
27
- import { probeLock } from "../core/file-lock.js";
28
28
  import { isPathAbsent } from "../core/path-access.js";
29
- import { getDbPath, getIndexRebuildLockPath } from "../core/paths.js";
30
- import { formatLockHolderPid } from "../core/run-lock.js";
29
+ import { getDbPath } from "../core/paths.js";
31
30
  import { warn, warnVerbose } from "../core/warn.js";
32
31
  import { closeDatabase, openExistingDatabase } from "../storage/repositories/index-connection.js";
33
- import { deleteEntriesByIds, getEntryCount, upsertEntry } from "../storage/repositories/index-entries-repository.js";
34
- import { deriveEntryProvenance, deriveInstallations } from "./installations.js";
35
- import { generateEmbeddingsForDb, publishTargetedEmbeddingMeta } from "./materialize-embeddings.js";
36
- import { getMarkdownFragmentContent, hasMarkdownFragmentContent, setMarkdownFragmentContent, } from "./passes/metadata.js";
37
- import { drainDirDocuments } from "./scan/drain-dir.js";
38
- import { buildSearchText } from "./search/search-fields.js";
39
- import { buildFileContext } from "./walk/file-context.js";
32
+ import { getEntryCount } from "../storage/repositories/index-entries-repository.js";
33
+ import { SQLITE_CHUNK_SIZE } from "../storage/repositories/index-sql.js";
34
+ import { drainEmbeddingQueue } from "./drain.js";
35
+ import { deriveInstallations } from "./installations.js";
36
+ import { reconcilePaths } from "./reconcile.js";
37
+ import { resolveSourceEntries } from "./search/search-source.js";
40
38
  /**
41
- * Busy-timeout (ms) for write-path index upserts. A real write unlike the
42
- * 250ms telemetry inserts but it must not hang `akm remember` for the full
43
- * default 30s behind a running full reindex. When it times out, the upsert is
44
- * skipped and the asset becomes searchable after that reindex instead.
39
+ * Busy-timeout (ms) for write-path index upserts. Every index write in the
40
+ * redesign is a short immediate transaction relying on SQLite's own busy
41
+ * timeout to serialize concurrent writers (rule 5, docs/plans/index-redesign.md)
42
+ * rather than an application-level lock this bounds how long one interactive
43
+ * write waits behind another process's short reconcile/drain transaction.
45
44
  */
46
45
  export const WRITE_PATH_INDEX_BUSY_TIMEOUT_MS = 5_000;
46
+ /**
47
+ * The distinct unit hashes reachable from an entry at one of `files`, via the
48
+ * `entry_units` mapping `reconcilePaths` just wrote — i.e. exactly the units
49
+ * this write's reconcile pass added or touched, the scope B4's drain should
50
+ * be asked to embed (module B2, docs/plans/index-redesign-contract.md:
51
+ * "Compute 'the units this write added' from the mapping:
52
+ * entries.file_path IN (paths) → entry_units → unit_hash"). A gone/unindexable
53
+ * path has no `entries` row any more and so contributes nothing here, which
54
+ * is correct — there is nothing left to embed for it.
55
+ */
56
+ function unitHashesForFiles(db, files) {
57
+ const hashes = new Set();
58
+ for (let offset = 0; offset < files.length; offset += SQLITE_CHUNK_SIZE) {
59
+ const chunk = files.slice(offset, offset + SQLITE_CHUNK_SIZE);
60
+ const placeholders = chunk.map(() => "?").join(",");
61
+ const rows = db
62
+ .prepare(`SELECT DISTINCT eu.unit_hash AS unitHash
63
+ FROM entries e
64
+ JOIN entry_units eu ON eu.entry_id = e.id
65
+ WHERE e.file_path IN (${placeholders})`)
66
+ .all(...chunk);
67
+ for (const row of rows)
68
+ hashes.add(row.unitHash);
69
+ }
70
+ return [...hashes];
71
+ }
72
+ /**
73
+ * The bundle id a fresh write into `stashDir` should be recorded under, when
74
+ * the caller did not already know it (`options.bundleId`).
75
+ *
76
+ * `stashDir` is matched against the CONFIGURED sources first, the way every
77
+ * other write-path caller resolves its bundle id (`knowledge.ts`'s demoted-file
78
+ * loop: `resolveSourceEntries` + `deriveInstallations`, matched by resolved
79
+ * root). A stash that is ALSO a configured bundle (`akm bundle add <dir>
80
+ * --name x`) must produce that bundle's id here, not a fresh slug of the raw
81
+ * path — a mismatch leaves an undeletable duplicate row once a full `akm
82
+ * index` reconciles the same path under its real, configured id (issue found
83
+ * integrating write-path W1). Only a stash that is genuinely unconfigured (an
84
+ * ad hoc write target) falls back to deriving an id from the raw path alone,
85
+ * the way a fresh install would.
86
+ */
87
+ function resolveWrittenAssetBundleId(stashDir) {
88
+ const resolvedStashDir = path.resolve(stashDir);
89
+ const sourceEntries = resolveSourceEntries();
90
+ const index = sourceEntries.findIndex((entry) => path.resolve(entry.path) === resolvedStashDir);
91
+ if (index !== -1)
92
+ return deriveInstallations(sourceEntries)[index]?.id;
93
+ return deriveInstallations([{ path: stashDir, writable: true }])[0]?.id;
94
+ }
47
95
  /**
48
96
  * Index the given just-written asset files into the existing local index.
49
97
  *
50
- * FAIL-OPEN at every step: any error (index.db absent, empty, locked past the
51
- * busy timeout, unparseable file) is reduced to a verbose-only warning and the
52
- * write command succeeds untouched. The degraded outcome is exactly the
53
- * pre-write-path-indexing behavior: the asset appears after the next full
54
- * `akm index` / improve-cron run.
98
+ * Returns `true` when the index is left in the state the caller expects —
99
+ * either genuinely updated, or one of the deliberate skips below and
100
+ * `false` only for a real failure, which callers that gate their own success
101
+ * on this boolean (`acceptProposal`, `source clone`) must treat as "the
102
+ * write stands, but tell the operator the index needs a manual `akm index`".
55
103
  *
56
- * An absent or empty index is skipped on purpose — bootstrap belongs to the
57
- * first read (`ensureIndex`) or an explicit `akm index`, which also cover
58
- * embeddings and the other passes this fast path skips.
104
+ * Deliberate `true` skips:
105
+ * - An absent index: bootstrap belongs to the first read (`ensureIndex`) or
106
+ * an explicit `akm index`, not this fast path.
107
+ * - An index with zero entries: same reasoning — nothing has bootstrapped
108
+ * it yet.
109
+ * - Every given path living under a dot-segment directory (e.g. `.meta/`):
110
+ * the full reconcile never descends into those either.
59
111
  *
60
- * A live rebuild holding the rebuild lock is the SAME kind of skip, not a
61
- * failure (#956 fix): a live `akm index` run owns bringing the index to the
62
- * expected state on its own, so this call returns `true` on that skip
63
- * exactly like the absent-index and empty-index cases above. Callers that
64
- * gate their own success on this boolean (`acceptProposal`, `source clone`)
65
- * must never fail or warn just because a concurrent rebuild is in progress.
112
+ * `false` on failure:
113
+ * - An index directory/file that exists but cannot be READ (not merely
114
+ * absent) is the one failure the next reconcile will not heal on its own
115
+ * (#791) this warns audibly, not verbose-only.
116
+ * - Any other error during `reconcilePaths` (unparseable file, a locked
117
+ * database past the busy timeout, …) warns verbose-only; the write itself
118
+ * already succeeded and the asset appears after the next reconcile.
119
+ *
120
+ * The embedding drain that follows a successful reconcile is best-effort: a
121
+ * failure there (a provider outage, a tripped circuit breaker) does NOT turn
122
+ * this call into a `false` — the asset is already lexically searchable, and
123
+ * the embedding queue is durable (module B4): any later drain, including the
124
+ * next write, computes the same "no vector yet" query and picks it up.
66
125
  */
67
126
  export async function indexWrittenAssets(stashDir, filePaths, options = {}) {
68
127
  try {
69
128
  return await (async () => {
70
- // #956: a live `akm index` rebuild holds index.db under one long
71
- // transaction (persistDirRecords), which this fast path's own 5s
72
- // busy_timeout would just contend with pointlessly. Interactive
73
- // commands (remember, import, extract session assets) must never wait
74
- // on it — skip the inline upsert/embedding entirely; the write itself
75
- // (file + commit) has already succeeded by the time this runs, and the
76
- // rebuild in progress will pick up the change on its own.
77
- const rebuildProbe = probeLock(getIndexRebuildLockPath());
78
- if (rebuildProbe.state === "held") {
79
- // #956: name the launcher pid alongside the holder pid when known —
80
- // every process listing and task log shows the launcher's pid, not
81
- // the bun/node child's.
82
- const holderLabel = formatLockHolderPid({
83
- pid: rebuildProbe.holderPid,
84
- launcherPid: rebuildProbe.launcherPid ?? null,
85
- });
86
- warn(`index rebuild in progress (pid ${holderLabel}); the next index pass will index ${filePaths.join(", ")}`);
87
- return true;
88
- }
89
129
  const dbPath = getDbPath();
90
130
  // `true` here means "the index is in the state the caller expects" — and
91
131
  // `acceptProposal` advances its journal to `index-finalized` on the
@@ -94,132 +134,54 @@ export async function indexWrittenAssets(stashDir, filePaths, options = {}) {
94
134
  // `openExistingDatabase` and surfaces as the honest `false` (#791).
95
135
  if (isPathAbsent(dbPath))
96
136
  return true;
97
- // The full walk never descends into dot-directories (for example `.meta/`)
98
- // — mirror that dot-segment skip here so this fast path indexes exactly
99
- // what a full run would. Sensitive/
100
- // infra abstention is the adapter's job now (see the `akmAdapter` note
101
- // below), not a path pre-filter.
137
+ // The full reconcile never descends into dot-directories (for example
138
+ // `.meta/`) — mirror that dot-segment skip here so this fast path
139
+ // indexes exactly what a full reconcile would.
102
140
  const files = filePaths.filter((f) => {
103
141
  const rel = path.relative(stashDir, f);
104
142
  return !rel.split(/[\\/]+/).some((segment) => segment.startsWith("."));
105
143
  });
106
144
  if (files.length === 0)
107
145
  return true;
108
- // Generate metadata BEFORE opening the DB so the write window stays
109
- // short. One drain call per file keeps the entry↔path pairing exact and
110
- // reuses the full-index recognize engine (F4a M-core-2 item 5): broken
111
- // workflows drop, valid workflow docs are cached for the side-table upsert.
112
- const component = deriveInstallations([
113
- { path: stashDir, writable: true, ...(options.bundleId ? { registryId: options.bundleId } : {}) },
114
- ])[0]?.components[0];
115
- if (!component)
146
+ // An explicit bundleId wins outright (it IS the resulting installation
147
+ // id); otherwise resolve it config-aware, matching a configured source
148
+ // before ever falling back to deriving one from the raw path.
149
+ const bundleId = options.bundleId ?? resolveWrittenAssetBundleId(stashDir);
150
+ if (!bundleId)
116
151
  throw new Error(`Could not derive bundle provenance for ${stashDir}`);
117
- const pairs = [];
118
- const unindexable = new Set();
119
- const rejectedConceptIds = new Set();
120
- for (const file of files) {
121
- if (!fs.existsSync(file)) {
122
- let authoredDanglingSymlink = false;
123
- try {
124
- authoredDanglingSymlink = fs.lstatSync(file).isSymbolicLink();
125
- }
126
- catch {
127
- // A genuinely absent path has no source identity to preflight.
128
- }
129
- if (!authoredDanglingSymlink) {
130
- unindexable.add(file);
131
- continue;
132
- }
133
- }
134
- const ctx = buildFileContext(stashDir, file);
135
- // Hardcoded `akmAdapter` on purpose (owner ruling 2026-07-21): this
136
- // write-path fast path only ever runs for assets a first-class akm
137
- // mutation command just wrote into a managed akm source, so the akm
138
- // adapter is always the right recognizer here — no per-component
139
- // dispatch needed.
140
- const drained = drainDirDocuments(akmAdapter, component, [ctx]);
141
- for (const rejectedPath of drained.rejectedPaths)
142
- unindexable.add(rejectedPath);
143
- for (const conceptId of drained.rejectedConceptIds)
144
- rejectedConceptIds.add(conceptId);
145
- const entry = drained.entries[0];
146
- // A broken workflow drains to zero entries and is treated as
147
- // unindexable; valid peer sources have already compiled to source IR.
148
- const conceptId = drained.conceptIdByFile.get(ctx.absPath);
149
- if (entry && conceptId)
150
- pairs.push({ file, entry, conceptId, contentHash: drained.hashByFile.get(ctx.absPath) });
151
- else
152
- unindexable.add(file);
153
- }
154
152
  const db = openExistingDatabase(dbPath);
155
153
  try {
156
154
  db.exec(`PRAGMA busy_timeout = ${WRITE_PATH_INDEX_BUSY_TIMEOUT_MS}`);
155
+ // An absent or empty index is skipped on purpose — bootstrap belongs
156
+ // to the first read (`ensureIndex`) or an explicit `akm index`.
157
157
  if (getEntryCount(db) === 0)
158
158
  return true;
159
- const targetEntryIds = new Set();
160
- let mutated = false;
161
- db.transaction(() => {
162
- const unindexableEntryIds = new Set();
163
- for (const file of unindexable) {
164
- const rows = db
165
- .prepare(`SELECT id FROM entries
166
- WHERE file_path = ?
167
- AND bundle_id = ?
168
- AND adapter_id = ?`)
169
- .all(file, component.id, component.adapter);
170
- for (const row of rows)
171
- unindexableEntryIds.add(row.id);
172
- }
173
- for (const conceptId of rejectedConceptIds) {
174
- const itemRef = `${component.id}//${conceptId}`;
175
- const rows = db
176
- .prepare(`SELECT id FROM entries
177
- WHERE bundle_id = ? AND adapter_id = ?
178
- AND type = 'workflow'
179
- AND (concept_id = ? OR item_ref = ?)`)
180
- .all(component.id, component.adapter, conceptId, itemRef);
181
- for (const row of rows)
182
- unindexableEntryIds.add(row.id);
183
- }
184
- deleteEntriesByIds(db, [...unindexableEntryIds]);
185
- mutated ||= unindexableEntryIds.size > 0;
186
- for (const { file, entry, conceptId, contentHash } of pairs) {
187
- let entryWithSize = entry;
188
- try {
189
- entryWithSize = { ...entry, fileSize: fs.statSync(file).size };
190
- if (hasMarkdownFragmentContent(entry)) {
191
- setMarkdownFragmentContent(entryWithSize, getMarkdownFragmentContent(entry));
192
- }
193
- }
194
- catch {
195
- // stat raced a delete — index without the size, like the full walk does.
196
- }
197
- // Real provenance (F4a M-core-2 item 5): populate item_ref/content_hash
198
- // via the SAME derivation the full-index writer uses, so a write-path
199
- // row is never a NULL-item_ref straggler.
200
- const provenance = deriveEntryProvenance({ bundleId: component.id, componentId: component.id, adapterId: component.adapter }, entry.type, entry.name, conceptId);
201
- // A materialized file has one current owner. If a targeted write is
202
- // the ownership handoff (for example an explicitly named bundle
203
- // replacing an earlier path-derived bootstrap identity), remove the
204
- // superseded physical-path row before publishing the canonical ref.
205
- const supersededIds = db
206
- .prepare("SELECT id FROM entries WHERE file_path = ? AND item_ref <> ?")
207
- .all(file, provenance.itemRef);
208
- deleteEntriesByIds(db, supersededIds.map((row) => row.id));
209
- targetEntryIds.add(upsertEntry(db, file, entryWithSize, buildSearchText(entry), provenance, contentHash));
210
- mutated = true;
211
- }
212
- })();
213
- if (mutated) {
159
+ // Pass `stashDir` through as `reconcilePaths`'s `opts.root` rather
160
+ // than relying on its config-based bundleId->root lookup: this
161
+ // caller already knows the exact root its write happened under, and
162
+ // a bundleId that is not (yet, or ever) a `bundles.<key>` config
163
+ // entry an ad hoc write target, e.g. a proposal's named target —
164
+ // can resolve to the WRONG root through that lookup, producing a
165
+ // corrupt conceptId and a duplicate `entries` row instead of a clean
166
+ // no-op (caught integrating this against tests/integration/proposal/proposals.test.ts).
167
+ await reconcilePaths(db, files, bundleId, { root: stashDir });
168
+ try {
214
169
  const config = loadConfig();
215
- await generateEmbeddingsForDb(db, config, () => { }, undefined, [...targetEntryIds]);
216
- publishTargetedEmbeddingMeta(db, config);
170
+ const onlyHashes = unitHashesForFiles(db, files);
171
+ await drainEmbeddingQueue(db, config, { onlyHashes });
172
+ }
173
+ catch (drainError) {
174
+ // Best-effort: the write is already lexically searchable via
175
+ // reconcilePaths above. The embedding queue is durable, so a
176
+ // failed drain here is not a failure of this call — see the
177
+ // return-contract note above.
178
+ warnVerbose("Write-path embedding drain skipped (vectors appear after the next drain):", drainError instanceof Error ? drainError.message : String(drainError));
217
179
  }
180
+ return true;
218
181
  }
219
182
  finally {
220
183
  closeDatabase(db);
221
184
  }
222
- return true;
223
185
  })();
224
186
  }
225
187
  catch (error) {