@owrede/vault-memory 2.3.0 → 2.3.2

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 CHANGED
@@ -14,6 +14,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
14
14
 
15
15
  _Nothing yet._
16
16
 
17
+ ## [2.3.2] — 2026-07-03
18
+
19
+ ### Changed
20
+
21
+ - **Release tooling: `scripts/release.mjs` now syncs all version declarations.**
22
+ `npm version` bumps `package.json` only; the release cut now also mirrors the
23
+ new version into `plugin/package.json`, `plugin/manifest.json`, and the README
24
+ `Latest: **vX.Y.Z**` badge, which `version-consistency.test.ts` requires. This
25
+ prevents the post-tag publish failure hit on the 2.3.1 cut. No runtime change.
26
+
27
+ ## [2.3.1] — 2026-07-02
28
+
29
+ ### Fixed
30
+
31
+ - **`index --full` no longer crashes with `FOREIGN KEY constraint failed`** (#16).
32
+ The full-mode wipe loop deleted chunks while `sections.chunk_id_first/last`
33
+ (`REFERENCES chunks(id)`, no `ON DELETE`) still pointed at them, so any vault
34
+ with a populated `sections` table failed to full-index. Sections are now
35
+ deleted before chunks in the wipe loop, matching the per-note re-index path.
36
+ - **Concurrent ContextFit ingests no longer corrupt the KB** (#17). A CLI
37
+ `index` running alongside a serve-side debounced re-ingest (or a second stale
38
+ `serve`) had both `contextfit ingest` calls clear and rewrite the same KB
39
+ directory; the loser crashed mid-write and could leave a half-written KB. A
40
+ dedicated per-vault ingest mutex (`locks/<vault>.ingest.lock`, separate from
41
+ the staleness daemon's lifetime-held `<vault>.lock`) now serializes all four
42
+ ingest call sites. A second-comer marks the vault dirty and returns
43
+ immediately (no wait, no wasted double-rebuild); the in-flight holder does one
44
+ trailing re-ingest so the latest change is never lost across processes, and a
45
+ crash-stranded dirty flag is honored on the next server start.
46
+
17
47
  ## [2.3.0] — 2026-07-01
18
48
 
19
49
  ### Added
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  **Local-first, source-agnostic-ready agentic knowledge layer over your Obsidian notes,
4
4
  exposed to any MCP-aware agent.**
5
5
 
6
- > See [CHANGELOG.md](./CHANGELOG.md) for release history. Latest: **v2.3.0** — additive
6
+ > See [CHANGELOG.md](./CHANGELOG.md) for release history. Latest: **v2.3.2** — additive
7
7
  > over v1.x; the 23 v1 tool names + input schemas are preserved byte-identical.
8
8
 
9
9
  ## 30-second example
package/dist/cli.js CHANGED
@@ -203,13 +203,13 @@ async function addVault(opts) {
203
203
  const cfgFile = opts.configFile ?? configPath();
204
204
  const binary = opts.binary ?? "vault-memory";
205
205
  const steps = [];
206
- const stat = await fs.stat(resolvedPath).catch((err) => {
206
+ const stat2 = await fs.stat(resolvedPath).catch((err) => {
207
207
  if (err.code === "ENOENT") {
208
208
  throw new Error(`Vault path does not exist: ${resolvedPath}`);
209
209
  }
210
210
  throw err;
211
211
  });
212
- if (!stat.isDirectory()) {
212
+ if (!stat2.isDirectory()) {
213
213
  throw new Error(`Vault path is not a directory: ${resolvedPath}`);
214
214
  }
215
215
  const proposedName = opts.name ?? slugifyVaultName(basename(resolvedPath));
@@ -5122,6 +5122,103 @@ var init_cli = __esm({
5122
5122
  }
5123
5123
  });
5124
5124
 
5125
+ // src/adapters/retrieval/contextfit/ingest-lock.ts
5126
+ var ingest_lock_exports = {};
5127
+ __export(ingest_lock_exports, {
5128
+ clearIngestDirty: () => clearIngestDirty,
5129
+ isIngestDirty: () => isIngestDirty,
5130
+ markIngestDirty: () => markIngestDirty,
5131
+ releaseIngestLock: () => releaseIngestLock,
5132
+ tryAcquireIngestLock: () => tryAcquireIngestLock
5133
+ });
5134
+ import { open, readFile as readFile3, unlink, mkdir as mkdir2, writeFile as writeFile2, stat } from "fs/promises";
5135
+ import { homedir as homedir4 } from "os";
5136
+ import { join as join4 } from "path";
5137
+ function lockDir(rootOverride) {
5138
+ if (rootOverride !== void 0) return join4(rootOverride, "locks");
5139
+ return join4(homedir4(), ".vault-memory", "locks");
5140
+ }
5141
+ function lockPath(vaultName, rootOverride) {
5142
+ return join4(lockDir(rootOverride), `${vaultName}.ingest.lock`);
5143
+ }
5144
+ function dirtyPath(vaultName, rootOverride) {
5145
+ return join4(lockDir(rootOverride), `${vaultName}.ingest.dirty`);
5146
+ }
5147
+ function isProcessAlive(pid) {
5148
+ try {
5149
+ process.kill(pid, 0);
5150
+ return true;
5151
+ } catch (err) {
5152
+ if (err.code === "ESRCH") return false;
5153
+ return true;
5154
+ }
5155
+ }
5156
+ async function readOwnerPid(path7) {
5157
+ try {
5158
+ const buf = await readFile3(path7, "utf8");
5159
+ const pid = parseInt(buf.trim(), 10);
5160
+ return Number.isFinite(pid) && pid > 0 ? pid : null;
5161
+ } catch {
5162
+ return null;
5163
+ }
5164
+ }
5165
+ async function tryAcquireIngestLock(vaultName, options = {}) {
5166
+ const dir = lockDir(options.rootOverride);
5167
+ await mkdir2(dir, { recursive: true });
5168
+ const path7 = lockPath(vaultName, options.rootOverride);
5169
+ const MAX_ATTEMPTS = 3;
5170
+ const attempt = async (n) => {
5171
+ if (n > MAX_ATTEMPTS) return { acquired: false, ownerPid: -1, path: path7 };
5172
+ try {
5173
+ const handle = await open(path7, "wx");
5174
+ try {
5175
+ await handle.writeFile(`${process.pid}
5176
+ `);
5177
+ } finally {
5178
+ await handle.close();
5179
+ }
5180
+ return { acquired: true, path: path7 };
5181
+ } catch (err) {
5182
+ if (err.code !== "EEXIST") throw err;
5183
+ const ownerPid = await readOwnerPid(path7);
5184
+ if (ownerPid === null || !isProcessAlive(ownerPid)) {
5185
+ await unlink(path7).catch(() => void 0);
5186
+ return attempt(n + 1);
5187
+ }
5188
+ return { acquired: false, ownerPid, path: path7 };
5189
+ }
5190
+ };
5191
+ return attempt(1);
5192
+ }
5193
+ async function releaseIngestLock(vaultName, options = {}) {
5194
+ await unlink(lockPath(vaultName, options.rootOverride)).catch(() => void 0);
5195
+ }
5196
+ async function markIngestDirty(vaultName, options = {}) {
5197
+ const dir = lockDir(options.rootOverride);
5198
+ await mkdir2(dir, { recursive: true });
5199
+ await writeFile2(dirtyPath(vaultName, options.rootOverride), `${process.pid}
5200
+ `).catch(
5201
+ () => void 0
5202
+ );
5203
+ }
5204
+ async function isIngestDirty(vaultName, options = {}) {
5205
+ try {
5206
+ await stat(dirtyPath(vaultName, options.rootOverride));
5207
+ return true;
5208
+ } catch {
5209
+ return false;
5210
+ }
5211
+ }
5212
+ async function clearIngestDirty(vaultName, options = {}) {
5213
+ await unlink(dirtyPath(vaultName, options.rootOverride)).catch(() => void 0);
5214
+ }
5215
+ var init_ingest_lock = __esm({
5216
+ "src/adapters/retrieval/contextfit/ingest-lock.ts"() {
5217
+ "use strict";
5218
+ init_esm_shims();
5219
+ }
5220
+ });
5221
+
5125
5222
  // src/adapters/retrieval/contextfit/index.ts
5126
5223
  var contextfit_exports = {};
5127
5224
  __export(contextfit_exports, {
@@ -5131,11 +5228,11 @@ __export(contextfit_exports, {
5131
5228
  searchVaultWithContextFit: () => searchVaultWithContextFit,
5132
5229
  sourceToNotePath: () => sourceToNotePath
5133
5230
  });
5134
- import { homedir as homedir4 } from "os";
5231
+ import { homedir as homedir5 } from "os";
5135
5232
  import { rm } from "fs/promises";
5136
- import { join as join4, relative, isAbsolute } from "path";
5233
+ import { join as join5, relative, isAbsolute } from "path";
5137
5234
  function contextFitKbDir(vaultName) {
5138
- return join4(homedir4(), ".vault-memory", "contextfit", vaultName);
5235
+ return join5(homedir5(), ".vault-memory", "contextfit", vaultName);
5139
5236
  }
5140
5237
  function cliConfigForVault(vault) {
5141
5238
  const cfg = {
@@ -5150,8 +5247,12 @@ async function indexVaultWithContextFit(vault, opts = {}) {
5150
5247
  });
5151
5248
  const cfg = cliConfigForVault(vault);
5152
5249
  const start = Date.now();
5250
+ const lockOpts = opts.lockRootOverride !== void 0 ? { rootOverride: opts.lockRootOverride } : {};
5251
+ const probe = opts._deps?.probe ?? ((c) => contextFitProbe({ command: c.command }));
5252
+ const ingest = opts._deps?.ingest ?? contextFitIngest;
5253
+ const clearKb = opts._deps?.clearKb ?? ((p) => rm(p, { recursive: true, force: true }));
5153
5254
  log(`ContextFit: ingesting ${vault.path} \u2192 ${cfg.kbPath}`);
5154
- const available = await contextFitProbe({ command: cfg.command });
5255
+ const available = await probe(cfg);
5155
5256
  if (!available) {
5156
5257
  return {
5157
5258
  status: "failed",
@@ -5160,14 +5261,29 @@ async function indexVaultWithContextFit(vault, opts = {}) {
5160
5261
  error: `ContextFit CLI not runnable (tried '${cfg.command}'). Install with \`pipx install contextfit\` or set [[vaults]].contextfit.command.`
5161
5262
  };
5162
5263
  }
5264
+ const lock = await tryAcquireIngestLock(vault.name, lockOpts);
5265
+ if (!lock.acquired) {
5266
+ await markIngestDirty(vault.name, lockOpts);
5267
+ log(`ContextFit: re-ingest already in progress (pid ${lock.ownerPid}); flagged for retry`);
5268
+ return { status: "skipped", stats: "", durationMs: Date.now() - start };
5269
+ }
5163
5270
  try {
5164
- await rm(cfg.kbPath, { recursive: true, force: true });
5165
- const stats = await contextFitIngest(cfg, vault.path);
5271
+ const MAX_PASSES = 8;
5272
+ let stats = "";
5273
+ let passes = 0;
5274
+ do {
5275
+ await clearIngestDirty(vault.name, lockOpts);
5276
+ await clearKb(cfg.kbPath);
5277
+ stats = await ingest(cfg, vault.path);
5278
+ passes += 1;
5279
+ } while (passes < MAX_PASSES && await isIngestDirty(vault.name, lockOpts));
5166
5280
  log(stats.trim().split("\n").slice(-3).join(" \xB7 "));
5167
5281
  return { status: "completed", stats, durationMs: Date.now() - start };
5168
5282
  } catch (err) {
5169
5283
  const message = err instanceof Error ? err.message : String(err);
5170
5284
  return { status: "failed", stats: "", durationMs: Date.now() - start, error: message };
5285
+ } finally {
5286
+ await releaseIngestLock(vault.name, lockOpts);
5171
5287
  }
5172
5288
  }
5173
5289
  function sourceToNotePath(source, vaultPath) {
@@ -5213,6 +5329,7 @@ var init_contextfit = __esm({
5213
5329
  "use strict";
5214
5330
  init_esm_shims();
5215
5331
  init_cli();
5332
+ init_ingest_lock();
5216
5333
  DEFAULT_COMMAND = "contextfit";
5217
5334
  }
5218
5335
  });
@@ -5344,9 +5461,9 @@ var init_reranker = __esm({
5344
5461
  });
5345
5462
 
5346
5463
  // src/rerank/onnx-reranker.ts
5347
- import { readFile as readFile3 } from "fs/promises";
5464
+ import { readFile as readFile4 } from "fs/promises";
5348
5465
  import { existsSync } from "fs";
5349
- import { join as join5 } from "path";
5466
+ import { join as join6 } from "path";
5350
5467
  function sigmoid(x) {
5351
5468
  return 1 / (1 + Math.exp(-x));
5352
5469
  }
@@ -5424,8 +5541,8 @@ var init_onnx_reranker = __esm({
5424
5541
  if (this.loaded) return this.loaded;
5425
5542
  if (this.loading) return this.loading;
5426
5543
  this.loading = (async () => {
5427
- const modelPath = join5(this.modelDir, "model_quantized.onnx");
5428
- const tokenizerPath = join5(this.modelDir, "tokenizer.json");
5544
+ const modelPath = join6(this.modelDir, "model_quantized.onnx");
5545
+ const tokenizerPath = join6(this.modelDir, "tokenizer.json");
5429
5546
  if (!existsSync(modelPath)) {
5430
5547
  throw new Error(
5431
5548
  `OnnxReranker: model file not found at ${modelPath}. Run: curl -L https://huggingface.co/onnx-community/bge-reranker-v2-m3-ONNX/resolve/main/onnx/model_quantized.onnx -o ${modelPath}`
@@ -5439,7 +5556,7 @@ var init_onnx_reranker = __esm({
5439
5556
  const [ort, tokMod, tokJson] = await Promise.all([
5440
5557
  import("onnxruntime-node"),
5441
5558
  import("@huggingface/tokenizers"),
5442
- readFile3(tokenizerPath, "utf-8")
5559
+ readFile4(tokenizerPath, "utf-8")
5443
5560
  ]);
5444
5561
  const tokenizerJson = JSON.parse(tokJson);
5445
5562
  const config = deriveTokenizerConfig(tokenizerJson);
@@ -5914,11 +6031,11 @@ function stripDynamicViewBlocks(body) {
5914
6031
  let i = 0;
5915
6032
  while (i < lines.length) {
5916
6033
  const line = lines[i];
5917
- const open2 = FENCE_OPEN_RE.exec(line);
5918
- if (open2) {
5919
- const indent = open2[1] ?? "";
5920
- const marker = open2[2] ?? "";
5921
- const lang = (open2[3] ?? "").toLowerCase();
6034
+ const open3 = FENCE_OPEN_RE.exec(line);
6035
+ if (open3) {
6036
+ const indent = open3[1] ?? "";
6037
+ const marker = open3[2] ?? "";
6038
+ const lang = (open3[3] ?? "").toLowerCase();
5922
6039
  const markerChar = marker[0];
5923
6040
  const isDynamic = DYNAMIC_VIEW_LANGS.has(lang);
5924
6041
  let j = i + 1;
@@ -6008,7 +6125,7 @@ import * as path3 from "path";
6008
6125
  import matter from "gray-matter";
6009
6126
  async function parseNote(absolutePath, vaultRoot) {
6010
6127
  const raw = await fs3.readFile(absolutePath, "utf-8");
6011
- const stat = await fs3.stat(absolutePath);
6128
+ const stat2 = await fs3.stat(absolutePath);
6012
6129
  const parsed = matter(raw);
6013
6130
  const content = parsed.content;
6014
6131
  const fmData = parsed.data;
@@ -6016,7 +6133,7 @@ async function parseNote(absolutePath, vaultRoot) {
6016
6133
  const title = extractTitle(content) ?? path3.basename(absolutePath, ".md");
6017
6134
  const hash = computeNoteHash(content, frontmatter);
6018
6135
  const bodyHash = computeBodyHash(content);
6019
- const mtime = Math.floor(stat.mtimeMs);
6136
+ const mtime = Math.floor(stat2.mtimeMs);
6020
6137
  const bodyLinks = extractWikilinks(content);
6021
6138
  const frontmatterLinks = extractFrontmatterWikilinks(frontmatter);
6022
6139
  const wikilinks = frontmatterLinks.length === 0 ? bodyLinks : mergeFrontmatterIntoBody(bodyLinks, frontmatterLinks);
@@ -6126,8 +6243,8 @@ var init_obsidian_fs = __esm({
6126
6243
  for (const abs of files) {
6127
6244
  if (limit !== void 0 && yielded >= limit) break;
6128
6245
  const rel = this.toPosix(path4.relative(path4.resolve(this.vault.path), abs));
6129
- const stat = await fs4.stat(abs);
6130
- const mtime = Math.floor(stat.mtimeMs);
6246
+ const stat2 = await fs4.stat(abs);
6247
+ const mtime = Math.floor(stat2.mtimeMs);
6131
6248
  if (since !== void 0 && mtime < since) continue;
6132
6249
  const body = await fs4.readFile(abs, "utf-8");
6133
6250
  const hash = computeBodyHash(body);
@@ -6150,7 +6267,7 @@ var init_obsidian_fs = __esm({
6150
6267
  const abs = this.absPath(rel);
6151
6268
  if (CONTRACT_PATH_RE.test(rel)) {
6152
6269
  const body = await fs4.readFile(abs, "utf-8");
6153
- const stat = await fs4.stat(abs);
6270
+ const stat2 = await fs4.stat(abs);
6154
6271
  const hash = computeBodyHash(body);
6155
6272
  return {
6156
6273
  id,
@@ -6159,7 +6276,7 @@ var init_obsidian_fs = __esm({
6159
6276
  blocks: [{ kind: "paragraph", text: body }],
6160
6277
  properties: {},
6161
6278
  links: [],
6162
- mtime: Math.floor(stat.mtimeMs),
6279
+ mtime: Math.floor(stat2.mtimeMs),
6163
6280
  hash,
6164
6281
  display_url: this.formatDisplayUrl(id)
6165
6282
  };
@@ -6891,6 +7008,7 @@ async function indexVault(vault, options) {
6891
7008
  vault.db.transaction(() => {
6892
7009
  const allNotes = vault.db.notes.listAll();
6893
7010
  for (const n of allNotes) {
7011
+ vault.db.sections.deleteByNote(n.id);
6894
7012
  vault.db.chunks.deleteByNote(n.id);
6895
7013
  vault.db.wikilinks.deleteByNote(n.id);
6896
7014
  vault.db.edges.deleteByNote(n.id);
@@ -7497,12 +7615,15 @@ async function catchupVault(options) {
7497
7615
  }
7498
7616
  }
7499
7617
  }
7500
- if (isContextFit2 && (reindexed > 0 || removed > 0)) {
7501
- const { indexVaultWithContextFit: indexVaultWithContextFit2 } = await Promise.resolve().then(() => (init_contextfit(), contextfit_exports));
7502
- const r = await indexVaultWithContextFit2(vault.config, { onProgress: log });
7503
- log(
7504
- r.status === "completed" ? `catch-up: ContextFit KB rebuilt (${r.durationMs}ms)` : `catch-up: ContextFit KB rebuild failed: ${r.error}`
7505
- );
7618
+ if (isContextFit2) {
7619
+ const cf = await Promise.resolve().then(() => (init_contextfit(), contextfit_exports));
7620
+ const dirty = await (await Promise.resolve().then(() => (init_ingest_lock(), ingest_lock_exports))).isIngestDirty(vault.config.name);
7621
+ if (reindexed > 0 || removed > 0 || dirty) {
7622
+ const r = await cf.indexVaultWithContextFit(vault.config, { onProgress: log });
7623
+ log(
7624
+ r.status === "completed" ? `catch-up: ContextFit KB rebuilt (${r.durationMs}ms)` : r.status === "skipped" ? `catch-up: ContextFit KB re-ingest already in progress; skipping` : `catch-up: ContextFit KB rebuild failed: ${r.error}`
7625
+ );
7626
+ }
7506
7627
  }
7507
7628
  return {
7508
7629
  scanned: files.length,
@@ -7944,7 +8065,7 @@ async function writeNote(input) {
7944
8065
  if (written === null) {
7945
8066
  throw new Error(`Internal error: file disappeared after write: ${relativePath}`);
7946
8067
  }
7947
- const stat = await fs6.stat(absPath);
8068
+ const stat2 = await fs6.stat(absPath);
7948
8069
  const previousNote = vault.db.notes.getByPath(relativePath);
7949
8070
  const previousHash = previousNote?.hash ?? null;
7950
8071
  const title = extractTitle2(written.content, relativePath);
@@ -7958,7 +8079,7 @@ async function writeNote(input) {
7958
8079
  title,
7959
8080
  hash: written.hash,
7960
8081
  bodyHash: computeBodyHash(written.content),
7961
- mtime: Math.floor(stat.mtimeMs),
8082
+ mtime: Math.floor(stat2.mtimeMs),
7962
8083
  wordCount: countWords3(written.content)
7963
8084
  });
7964
8085
  vault.db.aliases.setForNote(up.id, extractAliases(written.frontmatter));
@@ -8330,7 +8451,7 @@ var init_path = __esm({
8330
8451
  });
8331
8452
 
8332
8453
  // src/adapters/delivery/obsidian-fs/contract-yaml-read.ts
8333
- import { readFile as readFile4 } from "fs/promises";
8454
+ import { readFile as readFile5 } from "fs/promises";
8334
8455
  var init_contract_yaml_read = __esm({
8335
8456
  "src/adapters/delivery/obsidian-fs/contract-yaml-read.ts"() {
8336
8457
  "use strict";
@@ -10350,17 +10471,17 @@ var init_get = __esm({
10350
10471
  });
10351
10472
 
10352
10473
  // src/brief/lock.ts
10353
- import { open, readFile as readFile5, unlink, mkdir as mkdir2 } from "fs/promises";
10354
- import { homedir as homedir5 } from "os";
10355
- import { join as join7 } from "path";
10356
- function lockDir(rootOverride) {
10357
- if (rootOverride !== void 0) return join7(rootOverride, "locks");
10358
- return join7(homedir5(), ".vault-memory", "locks");
10474
+ import { open as open2, readFile as readFile6, unlink as unlink2, mkdir as mkdir3 } from "fs/promises";
10475
+ import { homedir as homedir6 } from "os";
10476
+ import { join as join8 } from "path";
10477
+ function lockDir2(rootOverride) {
10478
+ if (rootOverride !== void 0) return join8(rootOverride, "locks");
10479
+ return join8(homedir6(), ".vault-memory", "locks");
10359
10480
  }
10360
- function lockPath(vaultName, rootOverride) {
10361
- return join7(lockDir(rootOverride), `${vaultName}.lock`);
10481
+ function lockPath2(vaultName, rootOverride) {
10482
+ return join8(lockDir2(rootOverride), `${vaultName}.lock`);
10362
10483
  }
10363
- function isProcessAlive(pid) {
10484
+ function isProcessAlive2(pid) {
10364
10485
  try {
10365
10486
  process.kill(pid, 0);
10366
10487
  return true;
@@ -10369,9 +10490,9 @@ function isProcessAlive(pid) {
10369
10490
  return true;
10370
10491
  }
10371
10492
  }
10372
- async function readOwnerPid(path7) {
10493
+ async function readOwnerPid2(path7) {
10373
10494
  try {
10374
- const buf = await readFile5(path7, "utf8");
10495
+ const buf = await readFile6(path7, "utf8");
10375
10496
  const pid = parseInt(buf.trim(), 10);
10376
10497
  return Number.isFinite(pid) && pid > 0 ? pid : null;
10377
10498
  } catch {
@@ -10379,16 +10500,16 @@ async function readOwnerPid(path7) {
10379
10500
  }
10380
10501
  }
10381
10502
  async function tryAcquireLock(vaultName, options = {}) {
10382
- const dir = lockDir(options.rootOverride);
10383
- await mkdir2(dir, { recursive: true });
10384
- const path7 = lockPath(vaultName, options.rootOverride);
10503
+ const dir = lockDir2(options.rootOverride);
10504
+ await mkdir3(dir, { recursive: true });
10505
+ const path7 = lockPath2(vaultName, options.rootOverride);
10385
10506
  const MAX_ATTEMPTS = 3;
10386
10507
  const attempt = async (n, stolenFromPid) => {
10387
10508
  if (n > MAX_ATTEMPTS) {
10388
10509
  return { acquired: false, ownerPid: stolenFromPid ?? -1, path: path7 };
10389
10510
  }
10390
10511
  try {
10391
- const handle = await open(path7, "wx");
10512
+ const handle = await open2(path7, "wx");
10392
10513
  try {
10393
10514
  await handle.writeFile(`${process.pid}
10394
10515
  `);
@@ -10400,9 +10521,9 @@ async function tryAcquireLock(vaultName, options = {}) {
10400
10521
  return result;
10401
10522
  } catch (err) {
10402
10523
  if (err.code !== "EEXIST") throw err;
10403
- const ownerPid = await readOwnerPid(path7);
10404
- if (ownerPid === null || !isProcessAlive(ownerPid)) {
10405
- await unlink(path7).catch(() => void 0);
10524
+ const ownerPid = await readOwnerPid2(path7);
10525
+ if (ownerPid === null || !isProcessAlive2(ownerPid)) {
10526
+ await unlink2(path7).catch(() => void 0);
10406
10527
  return attempt(n + 1, ownerPid ?? -1);
10407
10528
  }
10408
10529
  return { acquired: false, ownerPid, path: path7 };
@@ -10411,7 +10532,7 @@ async function tryAcquireLock(vaultName, options = {}) {
10411
10532
  return attempt(1);
10412
10533
  }
10413
10534
  async function releaseLock(vaultName, options = {}) {
10414
- await unlink(lockPath(vaultName, options.rootOverride)).catch(() => void 0);
10535
+ await unlink2(lockPath2(vaultName, options.rootOverride)).catch(() => void 0);
10415
10536
  }
10416
10537
  var init_lock = __esm({
10417
10538
  "src/brief/lock.ts"() {
@@ -11279,6 +11400,8 @@ var init_watcher = __esm({
11279
11400
  const r = await indexVaultWithContextFit2(this.opts.vault.config, {});
11280
11401
  if (r.status === "completed") {
11281
11402
  this.opts.log(`ContextFit KB refreshed (${r.durationMs}ms)`);
11403
+ } else if (r.status === "skipped") {
11404
+ this.opts.log(`ContextFit KB refresh skipped (another ingest in progress; flagged)`);
11282
11405
  } else {
11283
11406
  this.opts.log(`ContextFit KB refresh failed: ${r.error}`);
11284
11407
  }
@@ -16233,7 +16356,7 @@ var init_package = __esm({
16233
16356
  "package.json"() {
16234
16357
  package_default = {
16235
16358
  name: "@owrede/vault-memory",
16236
- version: "2.3.0",
16359
+ version: "2.3.2",
16237
16360
  description: "Local-first semantic memory MCP server for Obsidian vaults",
16238
16361
  type: "module",
16239
16362
  license: "MIT",
@@ -16328,7 +16451,7 @@ __export(server_exports, {
16328
16451
  });
16329
16452
  import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
16330
16453
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
16331
- import { homedir as homedir6 } from "os";
16454
+ import { homedir as homedir7 } from "os";
16332
16455
  import { join as joinPath } from "path";
16333
16456
  async function discoverMemorySinks(configured, vaults) {
16334
16457
  if (configured.length > 0) {
@@ -16395,7 +16518,7 @@ async function serve(options = {}) {
16395
16518
  const activeVault = process.env.VAULT_MEMORY_ACTIVE_VAULT?.trim() || void 0;
16396
16519
  const rerankerBackend = config.server.reranker_backend ?? (config.server.reranker_model ? "onnx" : void 0);
16397
16520
  const reranker = config.server.reranker_model ? rerankerBackend === "ollama" ? new OllamaReranker({ ollama, model: config.server.reranker_model }) : new OnnxReranker({
16398
- modelDir: config.server.reranker_model_dir ?? joinPath(homedir6(), ".vault-memory", "models", "bge-reranker-v2-m3")
16521
+ modelDir: config.server.reranker_model_dir ?? joinPath(homedir7(), ".vault-memory", "models", "bge-reranker-v2-m3")
16399
16522
  }) : void 0;
16400
16523
  const watchers = /* @__PURE__ */ new Map();
16401
16524
  const briefDaemons = /* @__PURE__ */ new Map();
@@ -17515,6 +17638,10 @@ async function runIndex(rest) {
17515
17638
  console.error(
17516
17639
  `\u2713 ${vault.config.name}: ${sqlite.notesIndexed} notes (SQLite) + ContextFit KB \xB7 ${sqlite.durationMs + cfResult.durationMs}ms`
17517
17640
  );
17641
+ } else if (cfResult.status === "skipped") {
17642
+ console.error(
17643
+ `\u21B7 ${vault.config.name}: ${sqlite.notesIndexed} notes (SQLite); ContextFit KB re-ingest already in progress in another process \u2014 flagged for retry, skipping`
17644
+ );
17518
17645
  } else {
17519
17646
  console.error(`\u2717 ${vault.config.name}: ContextFit KB failed \u2014 ${cfResult.error}`);
17520
17647
  process.exitCode = 1;