memorix 1.1.9 → 1.1.10

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.
@@ -4,32 +4,35 @@
4
4
  > exist under subdirectories; prefer this file for repository-wide state.
5
5
 
6
6
  ## Current State
7
- - Phase: 1.1.9 release finalization
8
- - Branch: main
9
- - PR: #120, #121, and #123 merged
10
- - Last updated: 2026-07-12
11
- - Goal: publish 1.1.9 after accepting the latest contributor fixes and closing local release hygiene gaps.
7
+ - Phase: 1.1.10 emergency patch release
8
+ - Branch: codex/issue-124-project-context-timeout
9
+ - Issue: #124
10
+ - Last updated: 2026-07-13
11
+ - Goal: release the large-project `memorix_project_context` timeout fix and follow up with the reporter.
12
12
 
13
13
  ## Completed Recently
14
- - Merged PR #120: configurable CodeGraph exclude patterns for indexing, Project Context, context packs, diagnostics, and docs.
15
- - Merged PR #121: `memorix config get` accepts TOML-style snake_case dotted keys against resolved camelCase config.
16
- - Merged PR #123: Orama search access tracking preserves vector-backed documents, public/detail cache results strip embeddings, and hydration reconciles exact observation IDs even when mini-skills already populate the shared index.
17
- - Cleaned the unrelated local model catalog refresh and deleted memcode image from the main working tree before release prep.
18
- - Bumped root and workspace package versions to `1.1.9` and updated lockfile/internal workspace dependencies.
14
+ - Reproduced #124 with 3308 active memories and confirmed the old refresh path exceeded the MCP 60-second timeout.
15
+ - Replaced broad per-observation symbol scans with indexed exact-name lookup and bulk existing-ref reads.
16
+ - Changed first-use backfill to load one project graph snapshot, match all missing observations in memory, and persist refs in one transaction.
17
+ - Reused one graph snapshot per project-context request and batched project refs/referenced symbols.
18
+ - Prevented prose-only and ambiguous symbol mentions from creating large volumes of noisy code refs.
19
+ - Preserved Ruby namespace and punctuated method names through Lite indexing and both binding paths.
20
+ - Bumped root and workspace metadata to `1.1.10`.
19
21
 
20
22
  ## Current Follow-Up
21
- - Publish `1.1.9`, create the GitHub release, and confirm npm resolves the new root package version.
23
+ - Commit, publish `memorix@1.1.10`, create the GitHub release, and reply to #124 with the upgrade path.
22
24
 
23
25
  ## Verification Snapshot
24
- - Latest main CI for `cd3f024` passed on Ubuntu and Windows before release prep.
25
- - Targeted Orama/config/CodeGraph checks passed during PR review for #120, #121, and #123.
26
- - Final 1.1.9 release verification is run from the release commit before publishing: `git diff --check`, `npm run lint`, `npm run build`, `npm test`, and package dry-runs.
26
+ - TDD regression coverage passes for CodeGraph store, binder, project context, and auto context.
27
+ - A copied real database with all refs removed improved from 57.9 seconds for 173 active memories to 12.4 seconds.
28
+ - A scaled 3308-active first-use refresh completed in 13.3 seconds, including a forced code scan and full code-ref backfill.
29
+ - Built stdio MCP smoke returned `memorix_project_context` successfully in 5.79 seconds.
30
+ - Final release verification: `git diff --check`, `npm run lint`, `npm run build`, `npm test`, package dry-run, and clean-install npm smoke.
27
31
 
28
32
  ## Known UX Notes
29
- - Claude Code print-mode can be slow or fail to finish under budget during full user-style smoke. Use tool traces plus deterministic CLI/MCP smoke to distinguish product failures from model/runtime drift.
30
- - `memorix context --refresh never --task "<task>"` is fast and stable for first-contact context. `--refresh auto` can take longer on this repository.
33
+ - Before 1.1.10, `refresh: "never"` is a temporary workaround for very large active-memory corpora.
31
34
  - Current code and release facts outrank stale handoff notes or older dev logs when they conflict.
32
35
 
33
36
  ## Next Steps
34
- - Commit and push the 1.1.9 release patch.
35
- - Tag and publish `memorix@1.1.9` plus workspaces in dependency order.
37
+ - Commit and push the 1.1.10 release patch.
38
+ - Tag, publish the root package, create the GitHub release, and reply to #124.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix",
3
- "version": "1.1.9",
3
+ "version": "1.1.10",
4
4
  "description": "Local-first shared memory layer for AI coding agents across MCP clients, Git history, reasoning context, and project sessions.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -7,7 +7,6 @@ import { collectCurrentProjectFacts, type CurrentProjectFacts } from './current-
7
7
  import { indexProjectLite } from './lite-provider.js';
8
8
  import {
9
9
  buildProjectContextExplain,
10
- buildProjectContextOverview,
11
10
  type ProjectContextExplain,
12
11
  type ProjectContextObservation,
13
12
  type ProjectContextOverview,
@@ -147,18 +146,13 @@ export async function buildAutoProjectContext(input: {
147
146
  }
148
147
  }
149
148
 
150
- const overview = buildProjectContextOverview({
151
- project: input.project,
152
- store,
153
- observations: input.observations,
154
- exclude,
155
- });
156
149
  const explain = buildProjectContextExplain({
157
150
  project: input.project,
158
151
  store,
159
152
  observations: input.observations,
160
153
  exclude,
161
154
  });
155
+ const overview = explain.overview;
162
156
 
163
157
  return {
164
158
  project: input.project,
@@ -27,6 +27,24 @@ function mentionsSymbol(text: string, symbol: CodeSymbol): boolean {
27
27
  return new RegExp(`(^|[^\\w$])${name}([^\\w$]|$)`).test(text);
28
28
  }
29
29
 
30
+ function identifierCandidates(text: string): string[] {
31
+ return [...new Set(text.match(/[A-Za-z_$][\w$]*(?:::[A-Za-z_$][\w$]*)*[!?=]?/g) ?? [])];
32
+ }
33
+
34
+ function codeIdentifierCandidates(text: string): string[] {
35
+ const explicit = new Set<string>();
36
+ for (const match of text.matchAll(/`([A-Za-z_$][\w$]*(?:::[A-Za-z_$][\w$]*)*[!?=]?)`/g)) explicit.add(match[1]);
37
+ for (const match of text.matchAll(/\b([A-Za-z_$][\w$]*(?:::[A-Za-z_$][\w$]*)*[!?=]?)\s*\(/g)) explicit.add(match[1]);
38
+ return identifierCandidates(text).filter(candidate =>
39
+ explicit.has(candidate) ||
40
+ candidate.includes('::') ||
41
+ /[!?=]$/.test(candidate) ||
42
+ /^[A-Z]/.test(candidate) ||
43
+ /[_$]/.test(candidate) ||
44
+ /[a-z0-9][A-Z]/.test(candidate) ||
45
+ /[A-Z]{2}/.test(candidate));
46
+ }
47
+
30
48
  function fileRef(projectId: string, obs: BindableObservation, file: CodeFile): ObservationCodeRef {
31
49
  return {
32
50
  id: makeObservationCodeRefId(projectId, obs.id, file.id),
@@ -55,35 +73,89 @@ function symbolRef(projectId: string, obs: BindableObservation, file: CodeFile,
55
73
  };
56
74
  }
57
75
 
58
- export async function bindObservationToCode(
59
- store: CodeGraphStore,
76
+ interface CodeBindingLookup {
77
+ findFile(path: string): CodeFile | undefined;
78
+ findSymbols(names: string[], hintedFileIds: string[]): CodeSymbol[];
79
+ }
80
+
81
+ function resolveObservationCodeRefs(
60
82
  obs: BindableObservation,
61
- ): Promise<ObservationCodeRef[]> {
83
+ lookup: CodeBindingLookup,
84
+ ): ObservationCodeRef[] {
62
85
  const refs = new Map<string, ObservationCodeRef>();
63
86
  const text = observationText(obs);
64
87
  const candidateFiles = new Map<string, CodeFile>();
65
88
 
66
89
  for (const rawPath of obs.filesModified ?? []) {
67
- const file = store.getFile(obs.projectId, normalizeCodePath(rawPath));
90
+ const file = lookup.findFile(normalizeCodePath(rawPath));
68
91
  if (!file) continue;
69
92
  candidateFiles.set(file.id, file);
70
93
  const ref = fileRef(obs.projectId, obs, file);
71
94
  refs.set(ref.id, ref);
72
95
  }
73
96
 
74
- const symbols = store.findSymbols(obs.projectId, '', 500);
97
+ const hintedFileIds = new Set(candidateFiles.keys());
98
+ const symbolNames = hintedFileIds.size > 0 ? identifierCandidates(text) : codeIdentifierCandidates(text);
99
+ const symbols = lookup.findSymbols(symbolNames, [...hintedFileIds]);
100
+ const symbolsByName = new Map<string, CodeSymbol[]>();
75
101
  for (const symbol of symbols) {
76
- if (!mentionsSymbol(text, symbol)) continue;
77
- const file = candidateFiles.get(symbol.fileId) ?? store.getFile(obs.projectId, symbol.path);
78
- if (!file) continue;
79
- candidateFiles.set(file.id, file);
80
- const ref = symbolRef(obs.projectId, obs, file, symbol);
81
- refs.set(ref.id, ref);
102
+ const group = symbolsByName.get(symbol.name) ?? [];
103
+ group.push(symbol);
104
+ symbolsByName.set(symbol.name, group);
105
+ }
106
+
107
+ for (const group of symbolsByName.values()) {
108
+ const candidates = group.length === 1 ? group : [];
109
+ for (const symbol of candidates) {
110
+ if (!mentionsSymbol(text, symbol)) continue;
111
+ const file = candidateFiles.get(symbol.fileId) ?? lookup.findFile(symbol.path);
112
+ if (!file) continue;
113
+ candidateFiles.set(file.id, file);
114
+ const ref = symbolRef(obs.projectId, obs, file, symbol);
115
+ refs.set(ref.id, ref);
116
+ }
82
117
  }
83
118
 
84
- const result = [...refs.values()];
85
- store.replaceObservationRefs(obs.projectId, obs.id, result);
86
- return result;
119
+ return [...refs.values()];
120
+ }
121
+
122
+ function createStoreLookup(store: CodeGraphStore, projectId: string): CodeBindingLookup {
123
+ return {
124
+ findFile: path => store.getFile(projectId, path) ?? undefined,
125
+ findSymbols: (names, hintedFileIds) => store.findSymbolsByNames(projectId, names, hintedFileIds),
126
+ };
127
+ }
128
+
129
+ function createSnapshotLookup(files: CodeFile[], symbols: CodeSymbol[]): CodeBindingLookup {
130
+ const filesByPath = new Map(files.map(file => [normalizeCodePath(file.path), file]));
131
+ const symbolsByName = new Map<string, CodeSymbol[]>();
132
+ for (const symbol of symbols) {
133
+ const group = symbolsByName.get(symbol.name) ?? [];
134
+ group.push(symbol);
135
+ symbolsByName.set(symbol.name, group);
136
+ }
137
+
138
+ return {
139
+ findFile: path => filesByPath.get(normalizeCodePath(path)),
140
+ findSymbols: (names, hintedFileIds) => {
141
+ const hinted = new Set(hintedFileIds);
142
+ const found: CodeSymbol[] = [];
143
+ for (const name of names) {
144
+ const candidates = symbolsByName.get(name) ?? [];
145
+ found.push(...(hinted.size > 0 ? candidates.filter(symbol => hinted.has(symbol.fileId)) : candidates));
146
+ }
147
+ return found;
148
+ },
149
+ };
150
+ }
151
+
152
+ export async function bindObservationToCode(
153
+ store: CodeGraphStore,
154
+ obs: BindableObservation,
155
+ ): Promise<ObservationCodeRef[]> {
156
+ const refs = resolveObservationCodeRefs(obs, createStoreLookup(store, obs.projectId));
157
+ store.replaceObservationRefs(obs.projectId, obs.id, refs);
158
+ return refs;
87
159
  }
88
160
 
89
161
  export async function backfillMissingObservationCodeRefs(
@@ -92,14 +164,35 @@ export async function backfillMissingObservationCodeRefs(
92
164
  ): Promise<CodeRefBackfillResult> {
93
165
  let observationsBackfilled = 0;
94
166
  let refsBackfilled = 0;
167
+ const boundObservationIdsByProject = new Map<string, Set<number>>();
168
+ const lookupByProject = new Map<string, CodeBindingLookup>();
169
+
170
+ for (const projectId of new Set(observations.map(observation => observation.projectId))) {
171
+ boundObservationIdsByProject.set(
172
+ projectId,
173
+ new Set(store.listProjectObservationRefs(projectId).map(ref => ref.observationId)),
174
+ );
175
+ lookupByProject.set(
176
+ projectId,
177
+ createSnapshotLookup(store.listFiles(projectId), store.listSymbols(projectId)),
178
+ );
179
+ }
95
180
 
181
+ const refsToInsert: ObservationCodeRef[] = [];
96
182
  for (const observation of observations) {
97
- if (store.listObservationRefs(observation.projectId, observation.id).length > 0) continue;
98
- const refs = await bindObservationToCode(store, observation);
183
+ if (boundObservationIdsByProject.get(observation.projectId)?.has(observation.id)) continue;
184
+ if ((observation.filesModified?.length ?? 0) === 0 && codeIdentifierCandidates(observationText(observation)).length === 0) {
185
+ continue;
186
+ }
187
+ const lookup = lookupByProject.get(observation.projectId);
188
+ if (!lookup) continue;
189
+ const refs = resolveObservationCodeRefs(observation, lookup);
99
190
  if (refs.length === 0) continue;
100
191
  observationsBackfilled += 1;
101
192
  refsBackfilled += refs.length;
193
+ refsToInsert.push(...refs);
102
194
  }
195
+ store.upsertObservationRefs(refsToInsert);
103
196
 
104
197
  return {
105
198
  observationsScanned: observations.length,
@@ -141,8 +141,8 @@ const languageProfiles: Record<string, LanguageProfile> = {
141
141
  },
142
142
  ruby: {
143
143
  symbols: [
144
- { kind: 'class', re: /^class\s+([A-Za-z_][\w:]*?)\b/gm },
145
- { kind: 'function', re: /^def\s+([A-Za-z_][\w!?=]*)\b/gm },
144
+ { kind: 'class', re: /^class\s+([A-Za-z_][\w]*(?:::[A-Za-z_][\w]*)*)/gm },
145
+ { kind: 'function', re: /^def\s+([A-Za-z_][\w]*[!?=]?)/gm },
146
146
  ],
147
147
  imports: [/require\s+['"]([^'"]+)['"]/g],
148
148
  },
@@ -107,11 +107,13 @@ function collectGraph(
107
107
  suggestedReads: string[];
108
108
  } {
109
109
  const files = store.listFiles(projectId);
110
- const symbols = files.flatMap(file => store.listSymbolsForFile(file.id));
110
+ const symbols = store.listReferencedSymbols(projectId);
111
111
  const filesById = new Map(files.map(file => [file.id, file]));
112
112
  const symbolsById = new Map(symbols.map(symbol => [symbol.id, symbol]));
113
113
  const observationsById = new Map(observations.map(obs => [obs.id, obs]));
114
- const refs = observations.flatMap(obs => store.listObservationRefs(projectId, obs.id));
114
+ const activeObservationIds = new Set(observations.map(obs => obs.id));
115
+ const refs = store.listProjectObservationRefs(projectId)
116
+ .filter(ref => activeObservationIds.has(ref.observationId));
115
117
  const freshness: ProjectContextOverview['freshness'] = {
116
118
  current: 0,
117
119
  suspect: 0,
@@ -153,16 +155,14 @@ function collectGraph(
153
155
  };
154
156
  }
155
157
 
156
- export function buildProjectContextOverview(input: {
158
+ function overviewFromGraph(input: {
157
159
  project: Pick<ProjectInfo, 'id' | 'name' | 'rootPath'>;
158
160
  store: CodeGraphStore;
159
161
  observations: ProjectContextObservation[];
160
- exclude?: string[];
162
+ active: ProjectContextObservation[];
163
+ graph: ReturnType<typeof collectGraph>;
161
164
  }): ProjectContextOverview {
162
- const active = activeObservations(input.observations, input.project.id);
163
- const graph = collectGraph(input.store, input.project.id, active, input.exclude);
164
165
  const status = input.store.status(input.project.id);
165
-
166
166
  return {
167
167
  project: input.project,
168
168
  code: {
@@ -172,26 +172,49 @@ export function buildProjectContextOverview(input: {
172
172
  edges: status.edges,
173
173
  refs: status.refs,
174
174
  ...(status.indexedAt ? { indexedAt: status.indexedAt } : {}),
175
- languages: countLanguages(graph.files),
175
+ languages: countLanguages(input.graph.files),
176
176
  },
177
177
  memory: {
178
178
  total: input.observations.filter(obs => obs.projectId === input.project.id).length,
179
- active: active.length,
179
+ active: input.active.length,
180
180
  },
181
- freshness: graph.freshness,
182
- suggestedReads: graph.suggestedReads,
181
+ freshness: input.graph.freshness,
182
+ suggestedReads: input.graph.suggestedReads,
183
183
  };
184
184
  }
185
185
 
186
+ export function buildProjectContextOverview(input: {
187
+ project: Pick<ProjectInfo, 'id' | 'name' | 'rootPath'>;
188
+ store: CodeGraphStore;
189
+ observations: ProjectContextObservation[];
190
+ exclude?: string[];
191
+ }): ProjectContextOverview {
192
+ const active = activeObservations(input.observations, input.project.id);
193
+ const graph = collectGraph(input.store, input.project.id, active, input.exclude);
194
+ return overviewFromGraph({
195
+ project: input.project,
196
+ store: input.store,
197
+ observations: input.observations,
198
+ active,
199
+ graph,
200
+ });
201
+ }
202
+
186
203
  export function buildProjectContextExplain(input: {
187
204
  project: Pick<ProjectInfo, 'id' | 'name' | 'rootPath'>;
188
205
  store: CodeGraphStore;
189
206
  observations: ProjectContextObservation[];
190
207
  exclude?: string[];
191
208
  }): ProjectContextExplain {
192
- const overview = buildProjectContextOverview(input);
193
209
  const active = activeObservations(input.observations, input.project.id);
194
210
  const graph = collectGraph(input.store, input.project.id, active, input.exclude);
211
+ const overview = overviewFromGraph({
212
+ project: input.project,
213
+ store: input.store,
214
+ observations: input.observations,
215
+ active,
216
+ graph,
217
+ });
195
218
  return {
196
219
  project: input.project,
197
220
  sources: graph.sources.sort((a, b) => a.observationId - b.observationId || (a.path ?? '').localeCompare(b.path ?? '')),
@@ -316,6 +316,47 @@ export class CodeGraphStore {
316
316
  `).all(projectId, like, like, like, limit).map(rowToSymbol);
317
317
  }
318
318
 
319
+ listSymbols(projectId: string): CodeSymbol[] {
320
+ return this.db.prepare(`
321
+ SELECT * FROM code_symbols
322
+ WHERE projectId = ? AND stale = 0
323
+ ORDER BY path, startLine
324
+ `).all(projectId).map(rowToSymbol);
325
+ }
326
+
327
+ findSymbolsByNames(projectId: string, names: string[], fileIds: string[] = []): CodeSymbol[] {
328
+ const candidates = [...new Set(names.map(name => name.trim()).filter(Boolean))];
329
+ if (candidates.length === 0) return [];
330
+ const candidateJson = JSON.stringify(candidates);
331
+ const hintedFiles = [...new Set(fileIds.map(fileId => fileId.trim()).filter(Boolean))];
332
+ if (hintedFiles.length > 0) {
333
+ return this.db.prepare(`
334
+ SELECT * FROM code_symbols
335
+ WHERE projectId = ?
336
+ AND stale = 0
337
+ AND name IN (SELECT value FROM json_each(?))
338
+ AND fileId IN (SELECT value FROM json_each(?))
339
+ ORDER BY path, startLine
340
+ `).all(projectId, candidateJson, JSON.stringify(hintedFiles)).map(rowToSymbol);
341
+ }
342
+
343
+ return this.db.prepare(`
344
+ SELECT symbols.*
345
+ FROM code_symbols AS symbols
346
+ INNER JOIN (
347
+ SELECT name
348
+ FROM code_symbols
349
+ WHERE projectId = ?
350
+ AND stale = 0
351
+ AND name IN (SELECT value FROM json_each(?))
352
+ GROUP BY name
353
+ HAVING COUNT(*) = 1
354
+ ) AS unambiguous ON unambiguous.name = symbols.name
355
+ WHERE symbols.projectId = ? AND symbols.stale = 0
356
+ ORDER BY symbols.path, symbols.startLine
357
+ `).all(projectId, candidateJson, projectId).map(rowToSymbol);
358
+ }
359
+
319
360
  listSymbolsForFile(fileId: string): CodeSymbol[] {
320
361
  return this.db.prepare(`SELECT * FROM code_symbols WHERE fileId = ? AND stale = 0 ORDER BY startLine`).all(fileId).map(rowToSymbol);
321
362
  }
@@ -332,6 +373,24 @@ export class CodeGraphStore {
332
373
  `).all(projectId, observationId).map(rowToRef);
333
374
  }
334
375
 
376
+ listProjectObservationRefs(projectId: string): ObservationCodeRef[] {
377
+ return this.db.prepare(`
378
+ SELECT * FROM observation_code_refs
379
+ WHERE projectId = ?
380
+ ORDER BY observationId, status, id
381
+ `).all(projectId).map(rowToRef);
382
+ }
383
+
384
+ listReferencedSymbols(projectId: string): CodeSymbol[] {
385
+ return this.db.prepare(`
386
+ SELECT DISTINCT symbols.*
387
+ FROM code_symbols AS symbols
388
+ INNER JOIN observation_code_refs AS refs ON refs.symbolId = symbols.id
389
+ WHERE refs.projectId = ? AND symbols.projectId = ? AND symbols.stale = 0
390
+ ORDER BY symbols.path, symbols.startLine
391
+ `).all(projectId, projectId).map(rowToSymbol);
392
+ }
393
+
335
394
  status(projectId: string): CodeGraphStatus {
336
395
  const files = this.db.prepare(`SELECT COUNT(*) AS count FROM code_files WHERE projectId = ?`).get(projectId).count;
337
396
  const symbols = this.db.prepare(`SELECT COUNT(*) AS count FROM code_symbols WHERE projectId = ? AND stale = 0`).get(projectId).count;