nexusmem 0.3.2 → 0.3.3

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
@@ -9,6 +9,32 @@ built from, matched by publish timestamp: `v0.1.0` → `67a4776`, `v0.1.1` → `
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [0.3.3] — 2026-08-16
13
+
14
+ ### Added
15
+
16
+ - **`nexusmem status` now shows failure→fix chain counts** — `N/M failure(s) resolved (X retry, Y
17
+ discussion)`, plus a hint to run `sync --link-failures` when failures remain unresolved. The
18
+ chain feature is this project's most distinctive capability, but was previously invisible to
19
+ anyone who didn't already know to query `node_links` directly. Backed by the new
20
+ `getChainStats` in `src/correlate/failure-fix.ts`, which dedupes failures resolved by both
21
+ heuristics rather than double-counting them.
22
+
23
+ ### Fixed
24
+
25
+ - **Discussion-bridge heuristic (`sync --link-failures`): corpus-relative boilerplate tokens no
26
+ longer produce false-positive failure→fix links.** Re-dogfooded at larger scale against a second
27
+ real project's history: a command made entirely of words that saturate a project's own corpus
28
+ (e.g. this repo's own name/verbs) could AND-match an unrelated turn that just happened to mention
29
+ the same words, and bm25 score alone could not separate that from a true positive (measured: the
30
+ false positive scored *stronger* than two real true positives). `filterBoilerplateTokens` in
31
+ `src/correlate/failure-fix.ts` drops any token that appears in over 20% of a project's own
32
+ `conversation_turn`/`session_summary` history before building the match query — measured against
33
+ real data, not guessed — and skips the discussion-match attempt entirely (rather than falling back
34
+ unfiltered) when every token turns out to be boilerplate, since a missed link is preferred over a
35
+ false one for this heuristic. Below 10 discussable nodes the filter is skipped, since frequency
36
+ isn't a meaningful signal yet on a young project.
37
+
12
38
  ## [0.3.2] — 2026-08-16
13
39
 
14
40
  ### Added
@@ -250,7 +276,8 @@ First public release.
250
276
  there is no local-model summarization pass, and the conversation collector has never been audited
251
277
  for the stale-node bug that was found and fixed in the docs collector.
252
278
 
253
- [Unreleased]: https://github.com/yaminbkk/NexusMem/compare/v0.3.2...HEAD
279
+ [Unreleased]: https://github.com/yaminbkk/NexusMem/compare/v0.3.3...HEAD
280
+ [0.3.3]: https://github.com/yaminbkk/NexusMem/compare/v0.3.2...v0.3.3
254
281
  [0.3.2]: https://github.com/yaminbkk/NexusMem/compare/v0.3.1...v0.3.2
255
282
  [0.3.1]: https://github.com/yaminbkk/NexusMem/compare/v0.3.0...v0.3.1
256
283
  [0.3.0]: https://github.com/yaminbkk/NexusMem/compare/v0.2.0...v0.3.0
package/dist/cli/index.js CHANGED
@@ -710,11 +710,6 @@ function toMatchQuery(input) {
710
710
  if (kept.length === 0) return null;
711
711
  return kept.map((t) => `"${t}"*`).join(" OR ");
712
712
  }
713
- function toStrictMatchQuery(input) {
714
- const kept = significantTokens(input);
715
- if (kept.length === 0) return null;
716
- return kept.map((t) => `"${t}"*`).join(" AND ");
717
- }
718
713
 
719
714
  // src/store/schema.ts
720
715
  var V1 = `
@@ -1521,6 +1516,21 @@ var RESOLVED_BY_DISCUSSION = "resolved_by:discussion";
1521
1516
  function normalizeCommand(command) {
1522
1517
  return command.trim().replace(/\s+/g, " ").toLowerCase();
1523
1518
  }
1519
+ var MAX_TOKEN_DOC_FREQUENCY = 0.2;
1520
+ var MIN_CORPUS_FOR_FREQUENCY_FILTER = 10;
1521
+ function filterBoilerplateTokens(db, projectId, tokens) {
1522
+ if (tokens.length === 0) return tokens;
1523
+ const total = db.prepare(`SELECT COUNT(*) AS c FROM nodes WHERE project_id = ? AND kind IN ('conversation_turn', 'session_summary')`).get(projectId).c;
1524
+ if (total < MIN_CORPUS_FOR_FREQUENCY_FILTER) return tokens;
1525
+ const countMatching = db.prepare(
1526
+ `SELECT COUNT(*) AS c FROM nodes_fts JOIN nodes n ON n.rowid = nodes_fts.rowid
1527
+ WHERE nodes_fts MATCH ? AND n.project_id = ? AND n.kind IN ('conversation_turn', 'session_summary')`
1528
+ );
1529
+ return tokens.filter((t) => {
1530
+ const matching = countMatching.get(`"${t}"*`, projectId).c;
1531
+ return matching / total <= MAX_TOKEN_DOC_FREQUENCY;
1532
+ });
1533
+ }
1524
1534
  function correlateFailures(store, projectId, opts = {}) {
1525
1535
  const retryWindowMs = opts.retryWindowMs ?? DEFAULT_RETRY_WINDOW_MS;
1526
1536
  const discussionWindowMs = opts.discussionWindowMs ?? DEFAULT_DISCUSSION_WINDOW_MS;
@@ -1565,7 +1575,8 @@ function correlateFailures(store, projectId, opts = {}) {
1565
1575
  store.linkNodes(failure.id, retry.id, RESOLVED_BY_RETRY);
1566
1576
  linkedByRetry += 1;
1567
1577
  }
1568
- const match = toStrictMatchQuery(failure.command);
1578
+ const tokens = filterBoilerplateTokens(db, projectId, significantTokens(failure.command));
1579
+ const match = tokens.length > 0 ? tokens.map((t) => `"${t}"*`).join(" AND ") : null;
1569
1580
  if (match) {
1570
1581
  const discussion = findDiscussion.get(match, projectId, failure.ts_epoch, failure.ts_epoch + discussionWindowMs);
1571
1582
  if (discussion) {
@@ -1576,6 +1587,25 @@ function correlateFailures(store, projectId, opts = {}) {
1576
1587
  }
1577
1588
  return { failuresExamined: failures.length, linkedByRetry, linkedByDiscussion };
1578
1589
  }
1590
+ function getChainStats(store, projectId) {
1591
+ const db = store.raw;
1592
+ const failuresTotal = db.prepare(
1593
+ `SELECT COUNT(*) AS c FROM nodes
1594
+ WHERE project_id = ? AND kind = 'shell_command'
1595
+ AND json_extract(meta, '$.exitCode') IS NOT NULL AND json_extract(meta, '$.exitCode') != 0`
1596
+ ).get(projectId).c;
1597
+ const countDistinctLinked = (relations) => db.prepare(
1598
+ `SELECT COUNT(DISTINCT nl.from_node_id) AS c
1599
+ FROM node_links nl JOIN nodes n ON n.id = nl.from_node_id
1600
+ WHERE n.project_id = ? AND nl.relation IN (${relations.map(() => "?").join(", ")})`
1601
+ ).get(projectId, ...relations).c;
1602
+ return {
1603
+ failuresTotal,
1604
+ resolvedByRetry: countDistinctLinked([RESOLVED_BY_RETRY]),
1605
+ resolvedByDiscussion: countDistinctLinked([RESOLVED_BY_DISCUSSION]),
1606
+ resolvedTotal: countDistinctLinked([RESOLVED_BY_RETRY, RESOLVED_BY_DISCUSSION])
1607
+ };
1608
+ }
1579
1609
 
1580
1610
  // src/retrieval/fuse.ts
1581
1611
  var RRF_K = 60;
@@ -4293,6 +4323,7 @@ async function runStatus(opts) {
4293
4323
  const sources = store.listSyncState(projectId);
4294
4324
  const gitCursor = sources.find((s) => s.source === "git")?.cursor ?? null;
4295
4325
  const schema = currentSchemaVersion(store.raw);
4326
+ const chains = getChainStats(store, projectId);
4296
4327
  const dbBytes = fileSize(ws.dbPath) + fileSize(`${ws.dbPath}-wal`);
4297
4328
  const kinds = Object.entries(stats.byKind).sort((a, b) => b[1] - a[1]).map(([kind, n]) => ` ${String(n).padStart(6)} ${kind}`);
4298
4329
  process.stdout.write(
@@ -4314,7 +4345,8 @@ async function runStatus(opts) {
4314
4345
  return ` ${s.source.padEnd(14)} ${pc13.dim(`last run ${when}`)} ${pc13.dim(`cursor ${cursorLabel}`)}`;
4315
4346
  }),
4316
4347
  gitCursor && gitCursor !== repo.head ? `${pc13.yellow("git behind HEAD")} \u2014 run ${pc13.bold("nexusmem sync")}` : "",
4317
- ""
4348
+ "",
4349
+ chains.failuresTotal ? `${pc13.dim("chains ")} ${pc13.bold(String(chains.resolvedTotal))}/${chains.failuresTotal} failure(s) resolved ${pc13.dim(`(${chains.resolvedByRetry} retry, ${chains.resolvedByDiscussion} discussion)`)}${chains.resolvedTotal < chains.failuresTotal ? ` \u2014 run ${pc13.bold("nexusmem sync --link-failures")} to link more` : ""}` : ""
4318
4350
  ].filter((line) => line !== "").join("\n").concat("\n")
4319
4351
  );
4320
4352
  return 0;