@reposkein/mcp 0.7.0 → 0.9.0

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 (67) hide show
  1. package/README.md +1 -0
  2. package/binary-digests.json +4 -4
  3. package/dist/SKILL.md +22 -0
  4. package/dist/cli/doctor.d.ts +2 -2
  5. package/dist/cli/doctor.js +8 -4
  6. package/dist/cli/doctor.js.map +1 -1
  7. package/dist/cli/doctorFreshness.d.ts +6 -0
  8. package/dist/cli/doctorFreshness.js +23 -0
  9. package/dist/cli/doctorFreshness.js.map +1 -1
  10. package/dist/cli/migrate.d.ts +26 -0
  11. package/dist/cli/migrate.js +208 -0
  12. package/dist/cli/migrate.js.map +1 -0
  13. package/dist/embed/batch.d.ts +47 -0
  14. package/dist/embed/batch.js +92 -0
  15. package/dist/embed/batch.js.map +1 -0
  16. package/dist/embed/cache.d.ts +22 -48
  17. package/dist/embed/cache.js +44 -164
  18. package/dist/embed/cache.js.map +1 -1
  19. package/dist/embed/corpusVectors.d.ts +20 -0
  20. package/dist/embed/corpusVectors.js +60 -0
  21. package/dist/embed/corpusVectors.js.map +1 -0
  22. package/dist/embed/provider.d.ts +21 -2
  23. package/dist/embed/provider.js.map +1 -1
  24. package/dist/embed/providers/http.d.ts +3 -2
  25. package/dist/embed/providers/http.js +14 -1
  26. package/dist/embed/providers/http.js.map +1 -1
  27. package/dist/embed/providers/voyage.d.ts +3 -2
  28. package/dist/embed/providers/voyage.js +50 -54
  29. package/dist/embed/providers/voyage.js.map +1 -1
  30. package/dist/embed/vectorStore.d.ts +89 -0
  31. package/dist/embed/vectorStore.js +267 -0
  32. package/dist/embed/vectorStore.js.map +1 -0
  33. package/dist/guard/caps.d.ts +3 -0
  34. package/dist/guard/caps.js +4 -1
  35. package/dist/guard/caps.js.map +1 -1
  36. package/dist/index.js +16 -1
  37. package/dist/index.js.map +1 -1
  38. package/dist/search/bm25f.d.ts +17 -1
  39. package/dist/search/bm25f.js +89 -53
  40. package/dist/search/bm25f.js.map +1 -1
  41. package/dist/store/GraphStore.d.ts +6 -1
  42. package/dist/store/JsonlGraphStore.d.ts +47 -10
  43. package/dist/store/JsonlGraphStore.js +129 -50
  44. package/dist/store/JsonlGraphStore.js.map +1 -1
  45. package/dist/store/Neo4jGraphStore.d.ts +11 -0
  46. package/dist/store/Neo4jGraphStore.js +29 -2
  47. package/dist/store/Neo4jGraphStore.js.map +1 -1
  48. package/dist/store/instrumentTool.d.ts +9 -1
  49. package/dist/store/instrumentTool.js +33 -2
  50. package/dist/store/instrumentTool.js.map +1 -1
  51. package/dist/store/jsonlGraph.d.ts +22 -0
  52. package/dist/store/jsonlGraph.js +75 -18
  53. package/dist/store/jsonlGraph.js.map +1 -1
  54. package/dist/store/jsonlLines.d.ts +21 -0
  55. package/dist/store/jsonlLines.js +64 -0
  56. package/dist/store/jsonlLines.js.map +1 -0
  57. package/dist/store/repoSession.d.ts +19 -0
  58. package/dist/store/repoSession.js +25 -0
  59. package/dist/store/repoSession.js.map +1 -1
  60. package/dist/store/trackedGraph.d.ts +14 -0
  61. package/dist/store/trackedGraph.js +39 -0
  62. package/dist/store/trackedGraph.js.map +1 -0
  63. package/dist/tools/readCypher.js +9 -2
  64. package/dist/tools/readCypher.js.map +1 -1
  65. package/dist/tools/semanticFind.js +9 -6
  66. package/dist/tools/semanticFind.js.map +1 -1
  67. package/package.json +1 -1
@@ -27,5 +27,21 @@ export declare function tokenize(s: string): string[];
27
27
  /**
28
28
  * Rank corpus nodes by BM25F for the given query, returning up to limit results.
29
29
  * Deterministic: identical corpus + query → identical ranking.
30
+ *
31
+ * Takes an ITERABLE, and iterates it twice — BM25F needs corpus-wide statistics
32
+ * (average field lengths, document frequencies) before any document can be
33
+ * scored, so the corpus cannot be consumed in a single pass. What it does NOT
34
+ * do any more is RETAIN the corpus: the old form built a tokenized copy of all
35
+ * five fields of every node and held it through scoring, then collected every
36
+ * positive-scoring document before sorting. Retention is now bounded by
37
+ * `limit`; tokens are discarded as soon as a document has been scored.
38
+ *
39
+ * The two passes cost tokenization twice. That is the trade: CPU proportional
40
+ * to the corpus, memory proportional to the limit — which is the right way
41
+ * round for a query path that runs inside an agent's tool call.
42
+ *
43
+ * The iterable must be re-iterable (an array, or a generator function's result
44
+ * re-invoked). Results are identical to the single-pass form: same maths, and
45
+ * the same total ordering (score desc, id asc) applied to a bounded heap.
30
46
  */
31
- export declare function rankCorpus(corpus: CorpusNode[], query: string, limit: number): Scored[];
47
+ export declare function rankCorpus(corpus: Iterable<CorpusNode>, query: string, limit: number): Scored[];
@@ -57,58 +57,93 @@ export function tokenize(s) {
57
57
  function fieldTokens(value) {
58
58
  return tokenize(value);
59
59
  }
60
+ /** Count occurrences without allocating. The old form was
61
+ * `toks.filter((t) => t === qt).length`, which built a throwaway array for
62
+ * every (document x field x query token) — millions of them on a large
63
+ * corpus, for a number. */
64
+ function countOccurrences(toks, needle) {
65
+ let n = 0;
66
+ for (const t of toks)
67
+ if (t === needle)
68
+ n++;
69
+ return n;
70
+ }
71
+ /** Tokenize one node's fields. Callers must NOT retain the result. */
72
+ function tokenizeFields(node) {
73
+ const fields = {};
74
+ const lengths = {};
75
+ for (const f of FIELDS) {
76
+ const toks = fieldTokens(node[f] ?? "");
77
+ fields[f] = toks;
78
+ lengths[f] = toks.length;
79
+ }
80
+ return { fields, lengths };
81
+ }
60
82
  /**
61
83
  * Rank corpus nodes by BM25F for the given query, returning up to limit results.
62
84
  * Deterministic: identical corpus + query → identical ranking.
85
+ *
86
+ * Takes an ITERABLE, and iterates it twice — BM25F needs corpus-wide statistics
87
+ * (average field lengths, document frequencies) before any document can be
88
+ * scored, so the corpus cannot be consumed in a single pass. What it does NOT
89
+ * do any more is RETAIN the corpus: the old form built a tokenized copy of all
90
+ * five fields of every node and held it through scoring, then collected every
91
+ * positive-scoring document before sorting. Retention is now bounded by
92
+ * `limit`; tokens are discarded as soon as a document has been scored.
93
+ *
94
+ * The two passes cost tokenization twice. That is the trade: CPU proportional
95
+ * to the corpus, memory proportional to the limit — which is the right way
96
+ * round for a query path that runs inside an agent's tool call.
97
+ *
98
+ * The iterable must be re-iterable (an array, or a generator function's result
99
+ * re-invoked). Results are identical to the single-pass form: same maths, and
100
+ * the same total ordering (score desc, id asc) applied to a bounded heap.
63
101
  */
64
102
  export function rankCorpus(corpus, query, limit) {
65
- if (corpus.length === 0 || !query.trim())
103
+ if (limit <= 0 || !query.trim())
66
104
  return [];
67
- // 1. Tokenize query
68
105
  const queryTokens = tokenize(query);
69
106
  if (queryTokens.length === 0)
70
107
  return [];
71
108
  const uniqueQueryTokens = [...new Set(queryTokens)];
72
- // 2. Build per-node documents in id-sorted order (corpus is already sorted by store)
73
- const docs = corpus.map((node) => {
74
- const fields = {};
75
- const fieldLengths = {};
76
- for (const f of FIELDS) {
77
- const value = node[f] ?? "";
78
- const toks = fieldTokens(value);
79
- fields[f] = toks;
80
- fieldLengths[f] = toks.length;
81
- }
82
- return { node, fields, fieldLengths };
83
- });
84
- // 3. Compute average field lengths across corpus
85
- const avgFieldLen = {};
86
- for (const f of FIELDS) {
87
- const total = docs.reduce((sum, d) => sum + d.fieldLengths[f], 0);
88
- avgFieldLen[f] = docs.length > 0 ? total / docs.length : 1;
89
- }
90
- // 4. Build inverted index: token → set of doc indices (for IDF)
109
+ // Pass 1 corpus statistics only. Field-length totals and document
110
+ // frequencies; every token is dropped as soon as it has been counted.
111
+ const totalFieldLen = {};
112
+ for (const f of FIELDS)
113
+ totalFieldLen[f] = 0;
91
114
  const dfMap = new Map();
92
- for (const qt of uniqueQueryTokens) {
93
- let df = 0;
94
- for (const doc of docs) {
95
- // Check if token appears in any field of this doc
96
- let found = false;
115
+ for (const qt of uniqueQueryTokens)
116
+ dfMap.set(qt, 0);
117
+ let N = 0;
118
+ for (const node of corpus) {
119
+ N++;
120
+ const { fields, lengths } = tokenizeFields(node);
121
+ for (const f of FIELDS)
122
+ totalFieldLen[f] += lengths[f];
123
+ for (const qt of uniqueQueryTokens) {
97
124
  for (const f of FIELDS) {
98
- if (doc.fields[f].includes(qt)) {
99
- found = true;
125
+ if (fields[f].includes(qt)) {
126
+ dfMap.set(qt, dfMap.get(qt) + 1);
100
127
  break;
101
128
  }
102
129
  }
103
- if (found)
104
- df++;
105
130
  }
106
- dfMap.set(qt, df);
107
131
  }
108
- const N = docs.length;
109
- // 5. Score each document
110
- const scored = [];
111
- for (const doc of docs) {
132
+ if (N === 0)
133
+ return [];
134
+ const avgFieldLen = {};
135
+ for (const f of FIELDS)
136
+ avgFieldLen[f] = totalFieldLen[f] / N;
137
+ // Pass 2 — score, keeping only the best `limit`.
138
+ const best = [];
139
+ const worseThanWorst = (score, id) => {
140
+ const w = best[best.length - 1];
141
+ if (score !== w.score)
142
+ return score < w.score;
143
+ return id > w.node.id; // ties: lower id wins, matching the sort below
144
+ };
145
+ for (const node of corpus) {
146
+ const { fields, lengths } = tokenizeFields(node);
112
147
  let totalScore = 0;
113
148
  const matchedTokens = new Set();
114
149
  for (const qt of uniqueQueryTokens) {
@@ -117,35 +152,36 @@ export function rankCorpus(corpus, query, limit) {
117
152
  continue;
118
153
  // IDF: ln(1 + (N - df + 0.5) / (df + 0.5)) — always positive for df < N
119
154
  const idf = Math.log(1 + (N - df + 0.5) / (df + 0.5));
120
- // Weighted TF across fields (BM25F-style)
121
155
  let weightedTf = 0;
122
156
  for (const f of FIELDS) {
123
- const toks = doc.fields[f];
124
- const tf = toks.filter((t) => t === qt).length;
157
+ const tf = countOccurrences(fields[f], qt);
125
158
  if (tf === 0)
126
159
  continue;
127
160
  matchedTokens.add(qt);
128
- const fieldLen = doc.fieldLengths[f];
161
+ const fieldLen = lengths[f];
129
162
  const avgLen = avgFieldLen[f] > 0 ? avgFieldLen[f] : 1;
130
- // BM25 saturation with length normalization
131
163
  const saturated = (tf * (K1 + 1)) / (tf + K1 * (1 - B + B * (fieldLen / avgLen)));
132
164
  weightedTf += WEIGHTS[f] * saturated;
133
165
  }
134
166
  totalScore += idf * weightedTf;
135
167
  }
136
- if (totalScore > 0) {
137
- // Round to fixed precision to prevent cross-platform FP jitter from reordering ties
138
- const roundedScore = Math.round(totalScore * SCORE_PRECISION) / SCORE_PRECISION;
139
- scored.push({ node: doc.node, score: roundedScore, matched: [...matchedTokens].sort() });
140
- }
168
+ if (totalScore <= 0)
169
+ continue;
170
+ // Round to fixed precision to prevent cross-platform FP jitter from
171
+ // reordering ties.
172
+ const score = Math.round(totalScore * SCORE_PRECISION) / SCORE_PRECISION;
173
+ if (best.length >= limit && worseThanWorst(score, node.id))
174
+ continue;
175
+ best.push({ node, score, matched: [...matchedTokens].sort() });
176
+ best.sort((a, b) => {
177
+ const diff = b.score - a.score;
178
+ if (diff !== 0)
179
+ return diff;
180
+ return a.node.id < b.node.id ? -1 : a.node.id > b.node.id ? 1 : 0;
181
+ });
182
+ if (best.length > limit)
183
+ best.pop();
141
184
  }
142
- // 6. Sort: descending score, tie-break ascending node_id
143
- scored.sort((a, b) => {
144
- const diff = b.score - a.score;
145
- if (diff !== 0)
146
- return diff;
147
- return a.node.id < b.node.id ? -1 : a.node.id > b.node.id ? 1 : 0;
148
- });
149
- return scored.slice(0, limit);
185
+ return best;
150
186
  }
151
187
  //# sourceMappingURL=bm25f.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"bm25f.js","sourceRoot":"","sources":["../../src/search/bm25f.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"bm25f.js","sourceRoot":"","sources":["../../src/search/bm25f.ts"],"names":[],"mappings":"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"}
@@ -74,8 +74,13 @@ export interface GraphStore {
74
74
  findByContentHash?(repoIds: string[], hash: string): Promise<TargetRow[]>;
75
75
  /** Raw read-only Cypher (read_cypher tool only). Stores without a Cypher
76
76
  * engine throw CypherUnsupportedError. */
77
- runRead(query: string, params?: Record<string, unknown>, opts?: {
77
+ runRead(query: string, params?: Record<string, unknown>,
78
+ /** `maxRows` is a request to STOP STREAMING there, not a post-hoc filter.
79
+ * An implementation that cannot stream may return everything, but one
80
+ * that can must not materialise past the cap. */
81
+ opts?: {
78
82
  timeoutMs?: number;
83
+ maxRows?: number;
79
84
  }): Promise<Record<string, unknown>[]>;
80
85
  close(): Promise<void>;
81
86
  }
@@ -13,7 +13,12 @@ import type { TargetRow } from "../profile/types.js";
13
13
  export declare class JsonlGraphStore implements GraphStore {
14
14
  private readonly repoId;
15
15
  private graph;
16
- private stamp;
16
+ private graphStamp;
17
+ private overlayStamp;
18
+ /** Per node, the prop values the overlay overwrote — its own undo log.
19
+ * A key absent before the overlay is recorded as `undefined`, so removing a
20
+ * summary removes the props it added instead of leaving them behind. */
21
+ private overlayUndo;
17
22
  private readonly repoPath;
18
23
  private readonly nodesPath;
19
24
  private readonly edgesPath;
@@ -27,17 +32,49 @@ export declare class JsonlGraphStore implements GraphStore {
27
32
  * children (guarded, cycle-safe). Each child's repo_id comes from the proxy
28
33
  * Repository node's federated_repo_id in the parent's nodes.jsonl. */
29
34
  private collectRepos;
30
- /** Reloads the graph when anything it reads has changed on disk.
35
+ /** Two inputs, two keys.
31
36
  *
32
- * The key covers the derived JSONL, the committed summary shards, AND every
33
- * agent's sidecar. Keying on nodes/edges alone made a `git pull` that
34
- * brought in only a teammate's shards invisible for the life of the process
35
- * the shards are committed and the derived graph is not, so that is the
36
- * ordinary case. Leaving the sidecars out did the same to a second agent
37
- * working the same checkout, which the overlay explicitly promises to
38
- * surface. Stat calls only, O(shards + sidecars). */
39
- private freshnessKey;
37
+ * The derived JSONL and the authored overlay change independently and cost
38
+ * wildly different amounts to re-read. Keying both together meant every
39
+ * `write_semantic_summary` which rewrites this machine's sidecar threw
40
+ * the parsed graph away and re-read nodes.jsonl, edges.jsonl and every
41
+ * shard on the next call. The node had already been updated in place, so
42
+ * that rebuild bought nothing; a summarisation loop of K writes cost K full
43
+ * reloads. Stat calls only, O(shards + sidecars). */
44
+ private graphKey;
45
+ /** The committed shards AND every agent's sidecar.
46
+ *
47
+ * Keying on nodes/edges alone made a `git pull` that brought in only a
48
+ * teammate's shards invisible for the life of the process — the shards are
49
+ * committed and the derived graph is not, so that is the ordinary case.
50
+ * Leaving the sidecars out did the same to a second agent working the same
51
+ * checkout, which the overlay explicitly promises to surface. */
52
+ private overlayKey;
40
53
  private ensureFresh;
54
+ /** Write overlay props onto a node, remembering what they displaced.
55
+ *
56
+ * Every in-place summary mutation goes through here — the overlay fold AND
57
+ * writeSummary. A mutation that skipped it would be invisible to the undo
58
+ * pass, and worse, the NEXT fold would record the value it wrote as the
59
+ * pre-overlay original: removing that summary would then restore it. */
60
+ private overlayProps;
61
+ /** Fold the authored summaries onto the parsed graph, in place.
62
+ *
63
+ * The committed shards first, then this machine's sidecars as a strictly
64
+ * newer source, through the same rules the indexer uses. So what the server
65
+ * serves is exactly what the next `index` will write — including which side
66
+ * wins a divergence.
67
+ *
68
+ * The shard overlay only matters between indexes (`index` already grafts
69
+ * them into nodes.jsonl), but that is the ordinary case: a teammate's
70
+ * summaries arrive with a `git pull` long before anyone re-indexes.
71
+ * Sidecars are every agent's, not just this process's, so two agents on one
72
+ * checkout see each other's work.
73
+ *
74
+ * Re-applying is now routine rather than a side effect of a rebuild, so it
75
+ * undoes the previous overlay first: a summary that has gone away must take
76
+ * its props with it. */
77
+ private applyOverlay;
41
78
  getNode(repoIds: string[], id: string): Promise<TargetRow | null>;
42
79
  findByContentHash(repoIds: string[], hash: string): Promise<TargetRow[]>;
43
80
  resolveByPathAndName(repoIds: string[], filePath: string, name: string): Promise<TargetRow[]>;
@@ -1,7 +1,8 @@
1
- import { statSync, readFileSync, existsSync } from "node:fs";
1
+ import { statSync, existsSync } from "node:fs";
2
+ import { readLines } from "./jsonlLines.js";
2
3
  import { join, resolve, isAbsolute, sep } from "node:path";
3
4
  import { CypherUnsupportedError, } from "./GraphStore.js";
4
- import { buildFederatedGraph, emptyGraph, } from "./jsonlGraph.js";
5
+ import { buildFederatedGraphLines, emptyGraph, } from "./jsonlGraph.js";
5
6
  import { loadAllSidecars, sidecarPaths, sidecarPath, upsertSidecar } from "./sidecar.js";
6
7
  import { absorbSummarySource, emptyAccumulator, loadSummaryShards, recordSummaryConflicts, summaryShardsStamp, } from "./summaryShards.js";
7
8
  function str(v) {
@@ -73,7 +74,12 @@ const hasLabel = (n, l) => n.labels.includes(l);
73
74
  export class JsonlGraphStore {
74
75
  repoId;
75
76
  graph = emptyGraph();
76
- stamp = "";
77
+ graphStamp = "";
78
+ overlayStamp = "";
79
+ /** Per node, the prop values the overlay overwrote — its own undo log.
80
+ * A key absent before the overlay is recorded as `undefined`, so removing a
81
+ * summary removes the props it added instead of leaving them behind. */
82
+ overlayUndo = new Map();
77
83
  repoPath;
78
84
  nodesPath;
79
85
  edgesPath;
@@ -103,22 +109,21 @@ export class JsonlGraphStore {
103
109
  seen.add(repoId);
104
110
  const nodesPath = join(dir, ".reposkein", "nodes.jsonl");
105
111
  const edgesPath = join(dir, ".reposkein", "edges.jsonl");
106
- let nodesText = "";
107
- let edgesText = "";
108
- try {
109
- if (existsSync(nodesPath))
110
- nodesText = readFileSync(nodesPath, "utf8");
111
- if (existsSync(edgesPath))
112
- edgesText = readFileSync(edgesPath, "utf8");
113
- }
114
- catch {
112
+ if (!existsSync(nodesPath) && !existsSync(edgesPath))
115
113
  return;
116
- }
117
- repos.push({ repoId, nodesText, edgesText });
118
- // Discover children from this repo's Repository proxy nodes.
119
- for (const line of nodesText.split("\n")) {
120
- if (line.trim() === "")
121
- continue;
114
+ // Lazy: nothing is read until the build iterates them, and only one line
115
+ // is live at a time. This used to hold the COMPLETE text of every
116
+ // federated repo simultaneously a root plus ten children meant the
117
+ // whole federation resident at once, before a single line was parsed.
118
+ repos.push({
119
+ repoId,
120
+ nodes: { [Symbol.iterator]: () => readLines(nodesPath) },
121
+ edges: { [Symbol.iterator]: () => readLines(edgesPath) },
122
+ });
123
+ // Discover children from this repo's Repository proxy nodes. A second
124
+ // streaming pass over the same file, which costs I/O rather than the
125
+ // second full line array the old code built.
126
+ for (const line of readLines(nodesPath)) {
122
127
  let obj;
123
128
  try {
124
129
  obj = JSON.parse(line);
@@ -141,16 +146,16 @@ export class JsonlGraphStore {
141
146
  visit(this.repoPath, this.repoId);
142
147
  return repos;
143
148
  }
144
- /** Reloads the graph when anything it reads has changed on disk.
149
+ /** Two inputs, two keys.
145
150
  *
146
- * The key covers the derived JSONL, the committed summary shards, AND every
147
- * agent's sidecar. Keying on nodes/edges alone made a `git pull` that
148
- * brought in only a teammate's shards invisible for the life of the process
149
- * the shards are committed and the derived graph is not, so that is the
150
- * ordinary case. Leaving the sidecars out did the same to a second agent
151
- * working the same checkout, which the overlay explicitly promises to
152
- * surface. Stat calls only, O(shards + sidecars). */
153
- freshnessKey() {
151
+ * The derived JSONL and the authored overlay change independently and cost
152
+ * wildly different amounts to re-read. Keying both together meant every
153
+ * `write_semantic_summary` which rewrites this machine's sidecar threw
154
+ * the parsed graph away and re-read nodes.jsonl, edges.jsonl and every
155
+ * shard on the next call. The node had already been updated in place, so
156
+ * that rebuild bought nothing; a summarisation loop of K writes cost K full
157
+ * reloads. Stat calls only, O(shards + sidecars). */
158
+ graphKey() {
154
159
  let m = 0;
155
160
  try {
156
161
  if (existsSync(this.nodesPath))
@@ -161,25 +166,93 @@ export class JsonlGraphStore {
161
166
  catch {
162
167
  m = 0;
163
168
  }
164
- return `${m}|${summaryShardsStamp(this.repoPath, sidecarPaths)}`;
169
+ return String(m);
170
+ }
171
+ /** The committed shards AND every agent's sidecar.
172
+ *
173
+ * Keying on nodes/edges alone made a `git pull` that brought in only a
174
+ * teammate's shards invisible for the life of the process — the shards are
175
+ * committed and the derived graph is not, so that is the ordinary case.
176
+ * Leaving the sidecars out did the same to a second agent working the same
177
+ * checkout, which the overlay explicitly promises to surface. */
178
+ overlayKey() {
179
+ return summaryShardsStamp(this.repoPath, sidecarPaths);
165
180
  }
166
181
  ensureFresh() {
167
- const key = this.freshnessKey();
168
- if (key === this.stamp)
182
+ const graphKey = this.graphKey();
183
+ const overlayKey = this.overlayKey();
184
+ if (graphKey === this.graphStamp && overlayKey === this.overlayStamp)
169
185
  return;
170
- this.stamp = key;
186
+ if (graphKey !== this.graphStamp) {
187
+ this.graphStamp = graphKey;
188
+ // A fresh graph carries no overlay, and the undo log refers to the node
189
+ // objects that were just discarded.
190
+ this.overlayUndo.clear();
191
+ this.overlayStamp = "";
192
+ try {
193
+ this.graph = buildFederatedGraphLines(this.collectRepos());
194
+ }
195
+ catch {
196
+ this.graph = emptyGraph();
197
+ return;
198
+ }
199
+ }
200
+ if (overlayKey !== this.overlayStamp) {
201
+ // Only bank the stamp when the overlay actually landed. Banking it first
202
+ // and then throwing would leave the graph with its previous overlay
203
+ // already undone and no new one applied — every summary silently gone
204
+ // until some unrelated change moved the key again.
205
+ this.overlayStamp = this.applyOverlay() ? overlayKey : "";
206
+ }
207
+ }
208
+ /** Write overlay props onto a node, remembering what they displaced.
209
+ *
210
+ * Every in-place summary mutation goes through here — the overlay fold AND
211
+ * writeSummary. A mutation that skipped it would be invisible to the undo
212
+ * pass, and worse, the NEXT fold would record the value it wrote as the
213
+ * pre-overlay original: removing that summary would then restore it. */
214
+ overlayProps(id, n, props) {
215
+ const saved = this.overlayUndo.get(id) ?? {};
216
+ for (const [k, v] of Object.entries(props)) {
217
+ // First writer wins the undo slot: it holds the pristine value, and a
218
+ // later overwrite would replace it with an already-overlaid one.
219
+ if (!Object.prototype.hasOwnProperty.call(saved, k)) {
220
+ saved[k] = Object.prototype.hasOwnProperty.call(n.props, k) ? n.props[k] : undefined;
221
+ }
222
+ n.props[k] = v;
223
+ }
224
+ this.overlayUndo.set(id, saved);
225
+ }
226
+ /** Fold the authored summaries onto the parsed graph, in place.
227
+ *
228
+ * The committed shards first, then this machine's sidecars as a strictly
229
+ * newer source, through the same rules the indexer uses. So what the server
230
+ * serves is exactly what the next `index` will write — including which side
231
+ * wins a divergence.
232
+ *
233
+ * The shard overlay only matters between indexes (`index` already grafts
234
+ * them into nodes.jsonl), but that is the ordinary case: a teammate's
235
+ * summaries arrive with a `git pull` long before anyone re-indexes.
236
+ * Sidecars are every agent's, not just this process's, so two agents on one
237
+ * checkout see each other's work.
238
+ *
239
+ * Re-applying is now routine rather than a side effect of a rebuild, so it
240
+ * undoes the previous overlay first: a summary that has gone away must take
241
+ * its props with it. */
242
+ applyOverlay() {
171
243
  try {
172
- this.graph = buildFederatedGraph(this.collectRepos());
173
- // Overlay authored summaries: the committed shards first, then this
174
- // machine's sidecars as a strictly newer source, folded through the same
175
- // rules the indexer uses. So what the server serves is exactly what the
176
- // next `index` will write including which side wins a divergence.
177
- //
178
- // The shard overlay only matters between indexes (`index` already grafts
179
- // them into nodes.jsonl), but that is the ordinary case: a teammate's
180
- // summaries arrive with a `git pull` long before anyone re-indexes.
181
- // Sidecars are every agent's, not just this process's, so two agents on
182
- // one checkout see each other's work.
244
+ for (const [id, saved] of this.overlayUndo) {
245
+ const n = this.graph.byId.get(id);
246
+ if (!n)
247
+ continue;
248
+ for (const [k, v] of Object.entries(saved)) {
249
+ if (v === undefined)
250
+ delete n.props[k];
251
+ else
252
+ n.props[k] = v;
253
+ }
254
+ }
255
+ this.overlayUndo.clear();
183
256
  const shards = loadSummaryShards(this.repoPath);
184
257
  const overlay = emptyAccumulator();
185
258
  absorbSummarySource(overlay, {
@@ -199,16 +272,17 @@ export class JsonlGraphStore {
199
272
  // node passes this the same way a shard record does.
200
273
  if (rec.props.summary_of_hash !== n.props.content_hash)
201
274
  continue;
202
- for (const [k, v] of Object.entries(rec.props))
203
- n.props[k] = v;
275
+ this.overlayProps(id, n, rec.props);
204
276
  }
205
277
  // Divergence losers — from a merged shard, or from two agents writing the
206
278
  // same node — are authored prose. Preserve them for a human rather than
207
279
  // discarding them on the read path.
208
280
  recordSummaryConflicts(this.repoPath, [...shards.conflicts, ...overlay.conflicts]);
281
+ return true;
209
282
  }
210
283
  catch {
211
- this.graph = emptyGraph();
284
+ // An unreadable overlay must not cost the caller the graph.
285
+ return false;
212
286
  }
213
287
  }
214
288
  async getNode(repoIds, id) {
@@ -286,11 +360,16 @@ export class JsonlGraphStore {
286
360
  const oldSummary = str(n.props.semantic_summary);
287
361
  const oldHash = str(n.props.summary_of_hash);
288
362
  const stale_replaced = oldSummary !== null && oldHash !== chash;
289
- n.props.semantic_summary = fields.summary;
290
- n.props.summary_of_hash = chash;
291
- n.props.summary_model = fields.model;
292
- n.props.summary_at = fields.at;
293
- n.props.summary_by = fields.by;
363
+ // Through overlayProps, not raw assignment: this mutation has to be
364
+ // undoable like any other overlay write, or the next fold would mistake
365
+ // what it wrote for the node's pre-overlay state.
366
+ this.overlayProps(id, n, {
367
+ semantic_summary: fields.summary,
368
+ summary_of_hash: chash,
369
+ summary_model: fields.model,
370
+ summary_at: fields.at,
371
+ summary_by: fields.by,
372
+ });
294
373
  upsertSidecar(this.sidecarFile, {
295
374
  id,
296
375
  semantic_summary: fields.summary,