nexusmem 0.9.0 → 0.9.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.
- package/CHANGELOG.md +27 -0
- package/dist/cli/index.js +32 -11
- package/dist/cli/index.js.map +1 -1
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,33 @@ built from, matched by publish timestamp: `v0.1.0` → `67a4776`, `v0.1.1` → `
|
|
|
11
11
|
|
|
12
12
|
No unreleased changes yet.
|
|
13
13
|
|
|
14
|
+
## [0.9.1] — 2026-08-29
|
|
15
|
+
|
|
16
|
+
Three ranking/retrieval correctness fixes, found and validated against a new 28-case retrieval-quality
|
|
17
|
+
eval harness (`npm run eval`, dev-only, not part of the published package).
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- `nodes_vec` (vector search) over-fetched `k` globally, then filtered by `project_id` after the join —
|
|
22
|
+
a heuristic, not a guarantee. A sparse project sharing `memory.db` with a much larger one could have
|
|
23
|
+
every true nearest neighbour fall outside the over-fetch window and get silently dropped (reproduced:
|
|
24
|
+
495 rows in one project + 5 in another, global `k=50` surfaced 0 of the 5). Schema V11 gives `nodes_vec`
|
|
25
|
+
a `PARTITION KEY` on `project_id` (`sqlite-vec` 0.1.9+), pushing the equality filter into the k-NN
|
|
26
|
+
search itself so cross-project exactness is now guaranteed, not probable. Migrates existing databases
|
|
27
|
+
automatically on next `sync`/`query`. `--as-of` date filtering still over-fetches — only the
|
|
28
|
+
`project_id` dimension was ever a correctness guarantee.
|
|
29
|
+
- `MAX_PRIOR_OVERTURN` (the cap on how far a recency/signal prior may overturn relevance) raised
|
|
30
|
+
`2 → 2.4`, the highest value that both improves eval MRR (0.924→0.943) and still passes every legacy
|
|
31
|
+
regression test guarding against the original same-day-fix-commits bug this constant exists to bound.
|
|
32
|
+
- A commit's `code_diff` siblings (one node per changed file, all sharing the same `ts`) could crowd a
|
|
33
|
+
packed result and bury that commit's own `git_commit` node — its answer — several ranks down. Now
|
|
34
|
+
capped per commit, matching the existing `conversation_turn`/`doc_section` family cap.
|
|
35
|
+
- `package.json`/`tsconfig.json` diffs were ranking above more relevant results when a changed file's
|
|
36
|
+
path happened to echo its commit's conventional-commit scope (title is weighted 10x body, so the scope
|
|
37
|
+
word counted twice). Down-weighted as mechanical wiring, same reasoning `TEST_PATHS` already applies to
|
|
38
|
+
tests. Only affects nodes ingested from here on — existing manifest/config diffs need `sync --rebuild`
|
|
39
|
+
to get the corrected signal retroactively.
|
|
40
|
+
|
|
14
41
|
## [0.9.0] — 2026-08-27
|
|
15
42
|
|
|
16
43
|
Closes the three remaining mechanisms from a recurring external review (Simon Strandgaard, Agent
|
package/dist/cli/index.js
CHANGED
|
@@ -995,6 +995,24 @@ var V10 = `
|
|
|
995
995
|
ALTER TABLE nodes ADD COLUMN trust_state TEXT NOT NULL DEFAULT 'candidate';
|
|
996
996
|
CREATE INDEX idx_nodes_trust_state ON nodes (project_id, trust_state) WHERE trust_state != 'candidate';
|
|
997
997
|
`;
|
|
998
|
+
var V11 = `
|
|
999
|
+
CREATE TEMP TABLE nodes_vec_stage AS
|
|
1000
|
+
SELECT v.rowid AS rowid, n.project_id AS project_id, v.embedding AS embedding
|
|
1001
|
+
FROM nodes_vec v
|
|
1002
|
+
JOIN nodes n ON n.rowid = v.rowid;
|
|
1003
|
+
|
|
1004
|
+
DROP TABLE nodes_vec;
|
|
1005
|
+
|
|
1006
|
+
CREATE VIRTUAL TABLE nodes_vec USING vec0 (
|
|
1007
|
+
project_id TEXT PARTITION KEY,
|
|
1008
|
+
embedding float[${EMBEDDING_DIM}]
|
|
1009
|
+
);
|
|
1010
|
+
|
|
1011
|
+
INSERT INTO nodes_vec (rowid, project_id, embedding)
|
|
1012
|
+
SELECT rowid, project_id, embedding FROM nodes_vec_stage;
|
|
1013
|
+
|
|
1014
|
+
DROP TABLE nodes_vec_stage;
|
|
1015
|
+
`;
|
|
998
1016
|
var MIGRATIONS = [
|
|
999
1017
|
{ version: 1, up: (db) => db.exec(V1) },
|
|
1000
1018
|
{ version: 2, up: (db) => db.exec(V2) },
|
|
@@ -1005,7 +1023,8 @@ var MIGRATIONS = [
|
|
|
1005
1023
|
{ version: 7, up: (db) => db.exec(V7) },
|
|
1006
1024
|
{ version: 8, up: (db) => db.exec(V8) },
|
|
1007
1025
|
{ version: 9, up: (db) => db.exec(V9) },
|
|
1008
|
-
{ version: 10, up: (db) => db.exec(V10) }
|
|
1026
|
+
{ version: 10, up: (db) => db.exec(V10) },
|
|
1027
|
+
{ version: 11, up: (db) => db.exec(V11) }
|
|
1009
1028
|
];
|
|
1010
1029
|
var LATEST_SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1]?.version ?? 0;
|
|
1011
1030
|
function currentSchemaVersion(db) {
|
|
@@ -1467,24 +1486,24 @@ function getEmbedding(db, nodeId) {
|
|
|
1467
1486
|
if (!row) return null;
|
|
1468
1487
|
return new Float32Array(row.embedding.buffer, row.embedding.byteOffset, row.embedding.byteLength / 4);
|
|
1469
1488
|
}
|
|
1470
|
-
function upsertEmbedding(db, rowid, embedding) {
|
|
1471
|
-
db.prepare("INSERT OR REPLACE INTO nodes_vec (rowid, embedding) VALUES (?, ?)").run(BigInt(rowid), embedding);
|
|
1489
|
+
function upsertEmbedding(db, rowid, projectId, embedding) {
|
|
1490
|
+
db.prepare("INSERT OR REPLACE INTO nodes_vec (rowid, project_id, embedding) VALUES (?, ?, ?)").run(BigInt(rowid), projectId, embedding);
|
|
1472
1491
|
}
|
|
1473
1492
|
function dropAllEmbeddings(db) {
|
|
1474
1493
|
return db.prepare("DELETE FROM nodes_vec").run().changes;
|
|
1475
1494
|
}
|
|
1476
1495
|
function vectorSearch(db, projectId, embedding, limit = 20, opts = {}) {
|
|
1477
|
-
const overfetch = Math.max(limit * 8, 50);
|
|
1478
1496
|
const asOfEpoch = opts.asOfEpoch ?? null;
|
|
1497
|
+
const k = asOfEpoch === null ? limit : Math.max(limit * 8, 50);
|
|
1479
1498
|
return db.prepare(
|
|
1480
1499
|
`SELECT n.id, n.kind, n.ts, n.title, n.body, n.signal, n.provenance, n.trust_state AS trustState, v.distance AS distance
|
|
1481
1500
|
FROM nodes_vec v
|
|
1482
1501
|
JOIN nodes n ON n.rowid = v.rowid
|
|
1483
|
-
WHERE v.embedding MATCH ? AND k = ? AND
|
|
1502
|
+
WHERE v.embedding MATCH ? AND k = ? AND v.project_id = ?
|
|
1484
1503
|
AND (? IS NULL OR n.created_at <= ?)
|
|
1485
1504
|
ORDER BY v.distance
|
|
1486
1505
|
LIMIT ?`
|
|
1487
|
-
).all(embedding,
|
|
1506
|
+
).all(embedding, k, projectId, asOfEpoch, asOfEpoch, limit);
|
|
1488
1507
|
}
|
|
1489
1508
|
|
|
1490
1509
|
// src/store/fts.ts
|
|
@@ -1755,8 +1774,8 @@ var MemoryStore = class _MemoryStore {
|
|
|
1755
1774
|
countNodesNeedingEmbedding(projectId) {
|
|
1756
1775
|
return countNodesNeedingEmbedding(this.db, projectId);
|
|
1757
1776
|
}
|
|
1758
|
-
upsertEmbedding(rowid, embedding) {
|
|
1759
|
-
upsertEmbedding(this.db, rowid, embedding);
|
|
1777
|
+
upsertEmbedding(rowid, projectId, embedding) {
|
|
1778
|
+
upsertEmbedding(this.db, rowid, projectId, embedding);
|
|
1760
1779
|
}
|
|
1761
1780
|
/** The stored vector for one node, or null if it has not been embedded yet. */
|
|
1762
1781
|
getEmbedding(nodeId) {
|
|
@@ -2336,7 +2355,7 @@ var DEFAULT_SUMMARY_CHARS = 320;
|
|
|
2336
2355
|
var NODE_OVERHEAD_TOKENS = 8;
|
|
2337
2356
|
var CONVERSATION_ANSWER_MARKER = "\n\nA: ";
|
|
2338
2357
|
var MAX_PER_FAMILY = 2;
|
|
2339
|
-
var CHUNKED_KINDS = /* @__PURE__ */ new Set(["conversation_turn", "doc_section"]);
|
|
2358
|
+
var CHUNKED_KINDS = /* @__PURE__ */ new Set(["conversation_turn", "doc_section", "code_diff"]);
|
|
2340
2359
|
var HUNK_BOUNDARY = "\n@@ ";
|
|
2341
2360
|
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
2342
2361
|
"the",
|
|
@@ -2653,7 +2672,7 @@ var HALF_LIFE_RATIO = {
|
|
|
2653
2672
|
};
|
|
2654
2673
|
var SUPERSEDED_PENALTY = 0.5;
|
|
2655
2674
|
var REJECTED_TRUST_PENALTY = 0.3;
|
|
2656
|
-
var MAX_PRIOR_OVERTURN = 2;
|
|
2675
|
+
var MAX_PRIOR_OVERTURN = 2.4;
|
|
2657
2676
|
var PRIOR_COUNT = 2;
|
|
2658
2677
|
var PER_PRIOR_OVERTURN = MAX_PRIOR_OVERTURN ** (1 / PRIOR_COUNT);
|
|
2659
2678
|
var SIGNAL_EXPONENT = Math.log(PER_PRIOR_OVERTURN) / Math.log(1 / SIGNAL_FLOOR);
|
|
@@ -3476,11 +3495,13 @@ function isGeneratedPath(path) {
|
|
|
3476
3495
|
return GENERATED_PATHS.some((re) => re.test(path));
|
|
3477
3496
|
}
|
|
3478
3497
|
var TEST_PATHS = /(^|\/)(tests?|__tests__|spec|e2e)\/|\.(test|spec)\.[cm]?[jt]sx?$/;
|
|
3498
|
+
var MANIFEST_PATHS = /(^|\/)(package\.json|tsconfig(\..+)?\.json)$/;
|
|
3479
3499
|
function scoreFileDiff(subject, file) {
|
|
3480
3500
|
const header = parseConventionalHeader(subject);
|
|
3481
3501
|
let score = header.type ? TYPE_WEIGHTS[header.type] ?? 0.5 : 0.5;
|
|
3482
3502
|
if (header.breaking) score += 0.1;
|
|
3483
3503
|
if (TEST_PATHS.test(file.path)) score -= 0.1;
|
|
3504
|
+
if (MANIFEST_PATHS.test(file.path)) score -= 0.1;
|
|
3484
3505
|
if (file.status === "added") score += 0.05;
|
|
3485
3506
|
if (file.status === "deleted") score -= 0.1;
|
|
3486
3507
|
const churn = file.insertions + file.deletions;
|
|
@@ -4939,7 +4960,7 @@ ${node.body}`)
|
|
|
4939
4960
|
for (const [index, node] of group.entries()) {
|
|
4940
4961
|
const vector = vectors[index];
|
|
4941
4962
|
if (vector && vector.length === provider.dimension) {
|
|
4942
|
-
store.upsertEmbedding(node.rowid, vector);
|
|
4963
|
+
store.upsertEmbedding(node.rowid, projectId, vector);
|
|
4943
4964
|
embedded += 1;
|
|
4944
4965
|
embeddedHere += 1;
|
|
4945
4966
|
} else {
|