@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 +30 -0
- package/README.md +1 -1
- package/dist/cli.js +182 -55
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
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.
|
|
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
|
|
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 (!
|
|
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
|
|
5231
|
+
import { homedir as homedir5 } from "os";
|
|
5135
5232
|
import { rm } from "fs/promises";
|
|
5136
|
-
import { join as
|
|
5233
|
+
import { join as join5, relative, isAbsolute } from "path";
|
|
5137
5234
|
function contextFitKbDir(vaultName) {
|
|
5138
|
-
return
|
|
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
|
|
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
|
-
|
|
5165
|
-
|
|
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
|
|
5464
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
5348
5465
|
import { existsSync } from "fs";
|
|
5349
|
-
import { join as
|
|
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 =
|
|
5428
|
-
const tokenizerPath =
|
|
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
|
-
|
|
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
|
|
5918
|
-
if (
|
|
5919
|
-
const indent =
|
|
5920
|
-
const marker =
|
|
5921
|
-
const lang = (
|
|
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
|
|
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(
|
|
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
|
|
6130
|
-
const mtime = Math.floor(
|
|
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
|
|
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(
|
|
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
|
|
7501
|
-
const
|
|
7502
|
-
const
|
|
7503
|
-
|
|
7504
|
-
r
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
10354
|
-
import { homedir as
|
|
10355
|
-
import { join as
|
|
10356
|
-
function
|
|
10357
|
-
if (rootOverride !== void 0) return
|
|
10358
|
-
return
|
|
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
|
|
10361
|
-
return
|
|
10481
|
+
function lockPath2(vaultName, rootOverride) {
|
|
10482
|
+
return join8(lockDir2(rootOverride), `${vaultName}.lock`);
|
|
10362
10483
|
}
|
|
10363
|
-
function
|
|
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
|
|
10493
|
+
async function readOwnerPid2(path7) {
|
|
10373
10494
|
try {
|
|
10374
|
-
const buf = await
|
|
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 =
|
|
10383
|
-
await
|
|
10384
|
-
const path7 =
|
|
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
|
|
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
|
|
10404
|
-
if (ownerPid === null || !
|
|
10405
|
-
await
|
|
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
|
|
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.
|
|
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
|
|
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(
|
|
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;
|