memorix 1.1.8 → 1.1.9

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.
@@ -103,6 +103,8 @@ MCP:
103
103
 
104
104
  `memorix context` defaults to `--refresh auto`, so first use can seed CodeGraph Memory without a separate manual `memorix codegraph refresh`. Its brief puts live package/changelog/Git facts before memory hints and flags old `progress.txt` / dev-log notes as historical when they predate the latest changelog, so agents should treat current facts as the source of truth when files disagree. Task lenses keep the packet shaped to the work: bugfix briefs prefer failing tests and repros, release briefs prefer metadata/changelog/package checks, and onboarding briefs prefer docs and entry points while hiding unrelated suspect details. Use `--refresh never` for read-only inspection and `--refresh always` when you want to force a fresh scan.
105
105
 
106
+ Project-specific generated, vendored, or cache paths can be excluded from CodeGraph Memory with `[codegraph].exclude_patterns` in `memorix.toml` or `~/.memorix/config.toml` (`codegraph.excludePatterns` in legacy YAML). User patterns extend the built-in excludes and are applied to indexing, Project Context suggested reads, and Context Pack suggested reads.
107
+
106
108
  SessionStart hooks keep the default minimal hint lightweight. When memory behavior is configured with `sessionInject=full`, Memorix injects the compact Memory Autopilot brief at session start instead of only listing recent text memories.
107
109
 
108
110
  The intended loop for agents is: get the project brief when it helps, inspect the suggested current files, use stale or unbound memory only as a lead, store durable outcomes after the work changes the project, and resolve obsolete memories.
@@ -60,6 +60,9 @@ skip_merge_commits = true
60
60
  exclude_patterns = ["*.lock", "dist/**"]
61
61
  noise_keywords = ["format", "typo"]
62
62
 
63
+ [codegraph]
64
+ exclude_patterns = ["vendor/**", "third_party/**", "generated/**"]
65
+
63
66
  [server]
64
67
  transport = "stdio"
65
68
  dashboard = true
@@ -197,6 +200,21 @@ Project identity is still resolved from the real `.git` root. A project
197
200
  `memorix.toml` is an override file under that root; it does not create or rename
198
201
  the Memorix project ID.
199
202
 
203
+ ### `[codegraph]`
204
+
205
+ CodeGraph Memory and Project Context path filtering.
206
+
207
+ Common keys:
208
+
209
+ - `exclude_patterns = ["vendor/**", "third_party/**", "generated/**"]`
210
+
211
+ Legacy YAML uses `codegraph.excludePatterns` for the same setting.
212
+
213
+ These patterns extend Memorix's built-in CodeGraph excludes (`node_modules`,
214
+ build outputs, worktrees, and similar generated directories). Matching paths are
215
+ skipped during CodeGraph indexing and hidden from Project Context / Context Pack
216
+ suggested reads.
217
+
200
218
  ### `[server]`
201
219
 
202
220
  Server and dashboard behavior.
@@ -4,41 +4,26 @@
4
4
  > exist under subdirectories; prefer this file for repository-wide state.
5
5
 
6
6
  ## Current State
7
- - Phase: 1.1.8 release finalization
7
+ - Phase: 1.1.9 release finalization
8
8
  - Branch: main
9
- - PR: #118 merged
10
- - Last updated: 2026-07-08
11
- - Goal: publish 1.1.8 after committing the final Claude local MCP repair and CodeGraph noise-control patch.
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.
12
12
 
13
13
  ## Completed Recently
14
- - Merged PR #118: `fix(setup): make Memory Autopilot self-checking`.
15
- - Added `memorix doctor agents` and `memorix repair agents` so users can detect and repair stale agent MCP/rules setup.
16
- - Strengthened generated agent guidance so `memorix_project_context` is the default first step for non-trivial coding work.
17
- - Verified main at `8567f5e` after merge; GitHub CI passed before merge.
18
- - Confirmed local OpenRouter embedding lane resolves to `qwen/qwen3-embedding-8b` without printing secrets.
19
- - Fixed Claude Code 2.x local MCP repair: doctor/repair now inspect `~/.claude.json` project-private entries and rewrite stale worktree commands to `memorix serve` with `alwaysLoad: true`.
20
- - Fixed all-scope doctor noise: one healthy local, project, or global scope is enough in all-scope mode unless a stale actionable config is present.
21
- - Fixed CodeGraph Memory noise: `.tmp`, `.worktrees`, and `.claude/worktrees` are ignored by the lite scan and Autopilot suggested reads.
22
- - Bumped root and workspace package versions to `1.1.8` and updated lockfile/internal workspace dependencies.
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.
23
19
 
24
20
  ## Current Follow-Up
25
- - The release patch is implemented and verified locally, but not committed or pushed yet.
26
- - Two unrelated local changes were temporarily stashed before final verification so they do not enter the release package: `packages/ai/src/models.generated.ts` and `packages/memcode/docs/images/exy.png`.
21
+ - Publish `1.1.9`, create the GitHub release, and confirm npm resolves the new root package version.
27
22
 
28
23
  ## Verification Snapshot
29
- - `git diff --check`
30
- - `npm test -- tests/cli/agent-doctor.test.ts tests/codegraph/lite-provider.test.ts tests/codegraph/project-context.test.ts tests/codegraph/context-pack.test.ts`
31
- - `npm run lint`
32
- - `npm run build`
33
- - `npm test`
34
- - `npm pack --dry-run --json` -> `memorix@1.1.8`, about 14.1 MB packed / 60.3 MB unpacked
35
- - `node dist/cli/index.js doctor agents --agent claude --scope local`
36
- - `node dist/cli/index.js doctor agents --agent claude --scope all`
37
- - `node dist/cli/index.js codegraph refresh --json`
38
- - `node dist/cli/index.js context --refresh never --task "准备发布 1.1.8:判断当前进度、风险和下一步"`
39
- - `claude mcp get memorix`
40
- - `claude mcp list`
41
- - Claude Code user-style smoke: a short print-mode handoff correctly blocked release because local changes were not committed yet; longer print-mode smoke can still time out.
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.
42
27
 
43
28
  ## Known UX Notes
44
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.
@@ -46,6 +31,5 @@
46
31
  - Current code and release facts outrank stale handoff notes or older dev logs when they conflict.
47
32
 
48
33
  ## Next Steps
49
- - Commit and push the 1.1.8 release patch.
50
- - Publish `memorix@1.1.8` to npm, then tag and push `v1.1.8`.
51
- - Restore the temporary local stash after publishing if those local model/image changes are still needed.
34
+ - Commit and push the 1.1.9 release patch.
35
+ - Tag and publish `memorix@1.1.9` plus workspaces in dependency order.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix",
3
- "version": "1.1.8",
3
+ "version": "1.1.9",
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,
@@ -3,6 +3,7 @@ import { CodeGraphStore } from '../../codegraph/store.js';
3
3
  import { indexProjectLite } from '../../codegraph/lite-provider.js';
4
4
  import { assembleContextPackForTask, buildContextPackPrompt } from '../../codegraph/context-pack.js';
5
5
  import { backfillMissingObservationCodeRefs } from '../../codegraph/binder.js';
6
+ import { getResolvedConfig } from '../../config/resolved-config.js';
6
7
  import { getAllObservations } from '../../memory/observations.js';
7
8
  import { emitError, emitResult, getCliProjectContext, parsePositiveInt } from './operator-shared.js';
8
9
 
@@ -49,6 +50,7 @@ export default defineCommand({
49
50
  const store = new CodeGraphStore();
50
51
  await store.init(dataDir);
51
52
  const explicitAction = Boolean(positional[0] || (args.action as string | undefined));
53
+ const exclude = getResolvedConfig({ projectRoot: project.rootPath }).codegraph.excludePatterns;
52
54
 
53
55
  switch (action) {
54
56
  case 'status': {
@@ -64,6 +66,7 @@ export default defineCommand({
64
66
  const indexed = await indexProjectLite({
65
67
  projectId: project.id,
66
68
  projectRoot: project.rootPath,
69
+ exclude,
67
70
  });
68
71
  store.replaceProjectIndex(project.id, indexed);
69
72
  const activeObservations = getAllObservations()
@@ -99,6 +102,7 @@ export default defineCommand({
99
102
  task,
100
103
  observations,
101
104
  limit,
105
+ exclude,
102
106
  });
103
107
  emitResult({ project, pack }, buildContextPackPrompt(pack), asJson);
104
108
  return;
@@ -29,12 +29,19 @@ export default defineCommand({
29
29
  function readDotted(source: Record<string, unknown>, key: string): unknown {
30
30
  let cursor: unknown = source;
31
31
  for (const part of key.split('.')) {
32
- if (!cursor || typeof cursor !== 'object' || !(part in cursor)) return undefined;
33
- cursor = (cursor as Record<string, unknown>)[part];
32
+ if (!cursor || typeof cursor !== 'object') return undefined;
33
+ const record = cursor as Record<string, unknown>;
34
+ const normalizedPart = part in record ? part : snakeToCamel(part);
35
+ if (!(normalizedPart in record)) return undefined;
36
+ cursor = record[normalizedPart];
34
37
  }
35
38
  return cursor;
36
39
  }
37
40
 
41
+ function snakeToCamel(value: string): string {
42
+ return value.replace(/_([a-z])/g, (_, char: string) => char.toUpperCase());
43
+ }
44
+
38
45
  function formatValue(value: unknown, key: string): string {
39
46
  if (typeof value === 'string') return shouldRedactKey(key) ? '<redacted>' : value;
40
47
  if (typeof value === 'number' || typeof value === 'boolean') return String(value);
@@ -318,8 +318,10 @@ export default defineCommand({
318
318
  try {
319
319
  const { CodeGraphStore } = await import('../../codegraph/store.js');
320
320
  const { buildProjectContextOverview } = await import('../../codegraph/project-context.js');
321
+ const { getResolvedConfig } = await import('../../config/resolved-config.js');
321
322
  const codeStore = new CodeGraphStore();
322
323
  await codeStore.init(dataDir);
324
+ const exclude = getResolvedConfig({ projectRoot: projectRoot || process.cwd() }).codegraph.excludePatterns;
323
325
  const overview = buildProjectContextOverview({
324
326
  project: {
325
327
  id: projectId,
@@ -328,6 +330,7 @@ export default defineCommand({
328
330
  },
329
331
  store: codeStore,
330
332
  observations: projectObservations,
333
+ exclude,
331
334
  });
332
335
  const languageText = overview.code.languages.length > 0
333
336
  ? overview.code.languages.map((item: any) => `${item.language} ${item.files}`).join(', ')
@@ -1,5 +1,6 @@
1
1
  import { existsSync, statSync } from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { getResolvedConfig } from '../config/resolved-config.js';
3
4
  import type { ProjectInfo } from '../types.js';
4
5
  import { backfillMissingObservationCodeRefs, type CodeRefBackfillResult } from './binder.js';
5
6
  import { collectCurrentProjectFacts, type CurrentProjectFacts } from './current-facts.js';
@@ -100,11 +101,13 @@ export async function buildAutoProjectContext(input: {
100
101
  refresh?: AutoContextRefreshMode;
101
102
  maxAgeMs?: number;
102
103
  now?: Date;
104
+ exclude?: string[];
103
105
  }): Promise<AutoProjectContext> {
104
106
  const refreshMode = input.refresh ?? 'auto';
105
107
  const now = input.now ?? new Date();
106
108
  const task = input.task?.trim();
107
109
  const lens = resolveTaskLens(task);
110
+ const exclude = input.exclude ?? getResolvedConfig({ projectRoot: input.project.rootPath }).codegraph.excludePatterns;
108
111
  const store = new CodeGraphStore();
109
112
  await store.init(input.dataDir);
110
113
 
@@ -126,6 +129,7 @@ export async function buildAutoProjectContext(input: {
126
129
  const indexed = await indexProjectLite({
127
130
  projectId: input.project.id,
128
131
  projectRoot: input.project.rootPath,
132
+ exclude,
129
133
  });
130
134
  store.replaceProjectIndex(input.project.id, indexed);
131
135
  const backfill = await backfillMissingObservationCodeRefs(
@@ -147,11 +151,13 @@ export async function buildAutoProjectContext(input: {
147
151
  project: input.project,
148
152
  store,
149
153
  observations: input.observations,
154
+ exclude,
150
155
  });
151
156
  const explain = buildProjectContextExplain({
152
157
  project: input.project,
153
158
  store,
154
159
  observations: input.observations,
160
+ exclude,
155
161
  });
156
162
 
157
163
  return {
@@ -1,6 +1,7 @@
1
1
  import { evaluateCodeRefFreshness } from './freshness.js';
2
2
  import type { CodeFile, CodeRefStatus, CodeSymbol, ObservationCodeRef } from './types.js';
3
3
  import type { CodeGraphStore } from './store.js';
4
+ import { isCodeGraphExcludedPath } from './exclude.js';
4
5
 
5
6
  export interface ContextPackMemory {
6
7
  id: number;
@@ -52,6 +53,7 @@ export interface AssembleContextPackInput {
52
53
  files: CodeFile[];
53
54
  symbols: CodeSymbol[];
54
55
  suggestedVerification?: string[];
56
+ exclude?: string[];
55
57
  }
56
58
 
57
59
  function uniq<T>(items: T[]): T[] {
@@ -68,14 +70,6 @@ function timestampOf(observation: ContextPackObservation): number {
68
70
  return Date.parse(observation.updatedAt ?? observation.createdAt ?? '') || 0;
69
71
  }
70
72
 
71
- function isGeneratedPath(path: string): boolean {
72
- const normalized = path.replace(/\\/g, '/');
73
- return (
74
- /(^|\/)(dist|build|coverage|\.next|\.turbo|node_modules|\.git|\.tmp|\.worktrees)(\/|$)/i.test(normalized) ||
75
- /(^|\/)\.claude\/worktrees(\/|$)/i.test(normalized)
76
- );
77
- }
78
-
79
73
  function relevanceScore(observation: ContextPackObservation, taskTokens: string[]): number {
80
74
  if (taskTokens.length === 0) return 0;
81
75
  const title = observation.title.toLowerCase();
@@ -137,6 +131,7 @@ export function assembleContextPackForTask(input: {
137
131
  observations: ContextPackObservation[];
138
132
  limit?: number;
139
133
  suggestedVerification?: string[];
134
+ exclude?: string[];
140
135
  }): ContextPack {
141
136
  const selected = selectRelevantObservations(input.observations, input.task, input.limit ?? 20);
142
137
  const refs = selected.flatMap(obs => input.store.listObservationRefs(input.projectId, obs.id));
@@ -151,6 +146,7 @@ export function assembleContextPackForTask(input: {
151
146
  files,
152
147
  symbols,
153
148
  suggestedVerification: input.suggestedVerification,
149
+ exclude: input.exclude,
154
150
  });
155
151
  }
156
152
 
@@ -172,6 +168,7 @@ export function assembleContextPack(input: AssembleContextPackInput): ContextPac
172
168
 
173
169
  const file = ref.fileId ? files.get(ref.fileId) : undefined;
174
170
  const symbol = ref.symbolId ? symbols.get(ref.symbolId) : undefined;
171
+ if (file && isCodeGraphExcludedPath(file.path, input.exclude)) continue;
175
172
  const freshness = evaluateCodeRefFreshness(ref, file, symbol);
176
173
 
177
174
  if (freshness.status === 'current') {
@@ -247,8 +244,8 @@ export function buildContextPackPrompt(pack: ContextPack): string {
247
244
  const reliableMemories = pack.memories.filter(memory => memory.status === 'current');
248
245
  const unboundMemories = pack.memories.filter(memory => memory.status === 'unbound');
249
246
  const lines: string[] = ['## Task', pack.task, '', '## Reliable Memories'];
250
- const visibleCodeFacts = pack.codeFacts.filter(fact => !isGeneratedPath(fact.path)).slice(0, 5);
251
- const visibleSuggestedReads = pack.suggestedReads.filter(path => !isGeneratedPath(path)).slice(0, 5);
247
+ const visibleCodeFacts = pack.codeFacts.filter(fact => !isCodeGraphExcludedPath(fact.path)).slice(0, 5);
248
+ const visibleSuggestedReads = pack.suggestedReads.filter(path => !isCodeGraphExcludedPath(path)).slice(0, 5);
252
249
 
253
250
  if (reliableMemories.length === 0) lines.push('- none');
254
251
  for (const memory of reliableMemories) {
@@ -0,0 +1,47 @@
1
+ export const DEFAULT_CODEGRAPH_EXCLUDES = [
2
+ 'node_modules/**',
3
+ 'dist/**',
4
+ 'build/**',
5
+ 'coverage/**',
6
+ '.next/**',
7
+ '.turbo/**',
8
+ '.git/**',
9
+ '.tmp/**',
10
+ '.worktrees/**',
11
+ '.claude/worktrees/**',
12
+ ];
13
+
14
+ export function normalizeCodeGraphExcludePatterns(exclude?: string[]): string[] {
15
+ return [...new Set([
16
+ ...DEFAULT_CODEGRAPH_EXCLUDES,
17
+ ...(exclude ?? []).map(pattern => pattern.trim()).filter(Boolean),
18
+ ])];
19
+ }
20
+
21
+ export function isCodeGraphExcludedPath(path: string, exclude?: string[]): boolean {
22
+ const normalized = normalizeCodePath(path);
23
+ return normalizeCodeGraphExcludePatterns(exclude).some((pattern) => matchesPattern(normalized, normalizeCodePath(pattern)));
24
+ }
25
+
26
+ function normalizeCodePath(path: string): string {
27
+ return path.replace(/\\/g, '/').replace(/^\.\/+/, '');
28
+ }
29
+
30
+ function matchesPattern(path: string, pattern: string): boolean {
31
+ if (pattern.endsWith('/**')) {
32
+ const base = pattern.slice(0, -3);
33
+ if (base.startsWith('**/')) {
34
+ const suffix = base.slice(3);
35
+ return path === suffix || path.endsWith(`/${suffix}`) || path.includes(`/${suffix}/`);
36
+ }
37
+ if (!base.includes('/')) {
38
+ return path === base || path.startsWith(`${base}/`) || path.includes(`/${base}/`);
39
+ }
40
+ return path === base || path.startsWith(`${base}/`);
41
+ }
42
+ if (pattern.startsWith('**/')) {
43
+ const suffix = pattern.slice(3);
44
+ return path === suffix || path.endsWith(`/${suffix}`);
45
+ }
46
+ return path === pattern || path.startsWith(`${pattern}/`);
47
+ }
@@ -3,6 +3,7 @@ import { readdirSync, readFileSync, statSync, type Dirent } from 'node:fs';
3
3
  import { join, relative } from 'node:path';
4
4
  import type { CodeEdge, CodeFile, CodeSymbol } from './types.js';
5
5
  import { makeCodeEdgeId, makeCodeFileId, makeCodeSymbolId, normalizeCodePath } from './ids.js';
6
+ import { isCodeGraphExcludedPath, normalizeCodeGraphExcludePatterns } from './exclude.js';
6
7
 
7
8
  export interface LiteIndexOptions {
8
9
  projectId: string;
@@ -169,19 +170,6 @@ function languageForPath(path: string): string {
169
170
  return LANGUAGE_BY_EXTENSION.get(ext) ?? 'unknown';
170
171
  }
171
172
 
172
- function isExcluded(path: string, exclude: string[]): boolean {
173
- const normalized = normalizeCodePath(path);
174
- return exclude.some((pattern) => {
175
- const p = normalizeCodePath(pattern);
176
- if (p.endsWith('/**')) {
177
- const base = p.slice(0, -3);
178
- return normalized === base || normalized.startsWith(`${base}/`);
179
- }
180
- if (p.startsWith('**/')) return normalized.endsWith(p.slice(3));
181
- return normalized === p || normalized.startsWith(`${p}/`);
182
- });
183
- }
184
-
185
173
  function walk(root: string, exclude: string[], maxFiles: number): string[] {
186
174
  const out: string[] = [];
187
175
  const visit = (dir: string) => {
@@ -196,7 +184,7 @@ function walk(root: string, exclude: string[], maxFiles: number): string[] {
196
184
  for (const entry of entries) {
197
185
  const abs = join(dir, entry.name);
198
186
  const rel = normalizeCodePath(relative(root, abs));
199
- if (isExcluded(rel, exclude)) continue;
187
+ if (isCodeGraphExcludedPath(rel, exclude)) continue;
200
188
  if (entry.isDirectory()) {
201
189
  if (entry.name === '.git' || entry.name === '.worktrees' || entry.name === '.tmp' || entry.name === 'node_modules') continue;
202
190
  if (rel === '.claude/worktrees' || rel.startsWith('.claude/worktrees/')) continue;
@@ -290,18 +278,7 @@ function extractImportEdges(projectId: string, file: CodeFile, text: string, ind
290
278
  }
291
279
 
292
280
  export async function indexProjectLite(options: LiteIndexOptions): Promise<LiteIndexResult> {
293
- const exclude = options.exclude ?? [
294
- 'node_modules/**',
295
- 'dist/**',
296
- 'build/**',
297
- 'coverage/**',
298
- '.next/**',
299
- '.turbo/**',
300
- '.git/**',
301
- '.tmp/**',
302
- '.worktrees/**',
303
- '.claude/worktrees/**',
304
- ];
281
+ const exclude = normalizeCodeGraphExcludePatterns(options.exclude);
305
282
  const maxFiles = options.maxFiles ?? 5000;
306
283
  const indexedAt = new Date().toISOString();
307
284
  const paths = walk(options.projectRoot, exclude, maxFiles);
@@ -2,6 +2,7 @@ import type { ProjectInfo } from '../types.js';
2
2
  import { evaluateCodeRefFreshness } from './freshness.js';
3
3
  import type { CodeFile, CodeRefStatus, CodeSymbol, ObservationCodeRef } from './types.js';
4
4
  import type { CodeGraphStore } from './store.js';
5
+ import { isCodeGraphExcludedPath } from './exclude.js';
5
6
 
6
7
  export interface ProjectContextObservation {
7
8
  id: number;
@@ -77,29 +78,17 @@ function countLanguages(files: CodeFile[]): LanguageSummary[] {
77
78
  .sort((a, b) => a.language.localeCompare(b.language));
78
79
  }
79
80
 
80
- function normalizePath(path: string): string {
81
- return path.replace(/\\/g, '/');
82
- }
83
-
84
- function isGeneratedPath(path: string): boolean {
85
- const normalized = normalizePath(path);
86
- return (
87
- /(^|\/)(dist|build|coverage|\.next|\.turbo|node_modules|\.git|\.tmp|\.worktrees)(\/|$)/i.test(normalized) ||
88
- /(^|\/)\.claude\/worktrees(\/|$)/i.test(normalized)
89
- );
90
- }
91
-
92
81
  function suggestedReadRank(path: string): number {
93
- const normalized = normalizePath(path);
82
+ const normalized = path.replace(/\\/g, '/');
94
83
  if (normalized.startsWith('src/')) return 0;
95
84
  if (normalized.startsWith('tests/') || normalized.startsWith('test/')) return 0;
96
85
  if (normalized.startsWith('packages/') && normalized.includes('/src/')) return 2;
97
86
  return 3;
98
87
  }
99
88
 
100
- function compactSuggestedReads(paths: string[], limit = 8): string[] {
89
+ function compactSuggestedReads(paths: string[], limit = 8, exclude?: string[]): string[] {
101
90
  return uniq(paths)
102
- .filter(path => !isGeneratedPath(path))
91
+ .filter(path => !isCodeGraphExcludedPath(path, exclude))
103
92
  .sort((a, b) => suggestedReadRank(a) - suggestedReadRank(b))
104
93
  .slice(0, limit);
105
94
  }
@@ -108,6 +97,7 @@ function collectGraph(
108
97
  store: CodeGraphStore,
109
98
  projectId: string,
110
99
  observations: ProjectContextObservation[],
100
+ exclude?: string[],
111
101
  ): {
112
102
  files: CodeFile[];
113
103
  symbols: CodeSymbol[];
@@ -139,7 +129,9 @@ function collectGraph(
139
129
 
140
130
  const observation = observationsById.get(ref.observationId);
141
131
  if (!observation) continue;
142
- if (result.status === 'current' && file) suggestedReads.push(file.path);
132
+ const excluded = file ? isCodeGraphExcludedPath(file.path, exclude) : false;
133
+ if (result.status === 'current' && file && !excluded) suggestedReads.push(file.path);
134
+ if (excluded) continue;
143
135
  sources.push({
144
136
  observationId: observation.id,
145
137
  title: observation.title,
@@ -157,7 +149,7 @@ function collectGraph(
157
149
  refs,
158
150
  freshness,
159
151
  sources,
160
- suggestedReads: compactSuggestedReads(suggestedReads),
152
+ suggestedReads: compactSuggestedReads(suggestedReads, 8, exclude),
161
153
  };
162
154
  }
163
155
 
@@ -165,9 +157,10 @@ export function buildProjectContextOverview(input: {
165
157
  project: Pick<ProjectInfo, 'id' | 'name' | 'rootPath'>;
166
158
  store: CodeGraphStore;
167
159
  observations: ProjectContextObservation[];
160
+ exclude?: string[];
168
161
  }): ProjectContextOverview {
169
162
  const active = activeObservations(input.observations, input.project.id);
170
- const graph = collectGraph(input.store, input.project.id, active);
163
+ const graph = collectGraph(input.store, input.project.id, active, input.exclude);
171
164
  const status = input.store.status(input.project.id);
172
165
 
173
166
  return {
@@ -194,10 +187,11 @@ export function buildProjectContextExplain(input: {
194
187
  project: Pick<ProjectInfo, 'id' | 'name' | 'rootPath'>;
195
188
  store: CodeGraphStore;
196
189
  observations: ProjectContextObservation[];
190
+ exclude?: string[];
197
191
  }): ProjectContextExplain {
198
192
  const overview = buildProjectContextOverview(input);
199
193
  const active = activeObservations(input.observations, input.project.id);
200
- const graph = collectGraph(input.store, input.project.id, active);
194
+ const graph = collectGraph(input.store, input.project.id, active, input.exclude);
201
195
  return {
202
196
  project: input.project,
203
197
  sources: graph.sources.sort((a, b) => a.observationId - b.observationId || (a.path ?? '').localeCompare(b.path ?? '')),
@@ -51,6 +51,9 @@ export interface ResolvedMemorixConfig {
51
51
  excludePatterns?: string[];
52
52
  noiseKeywords?: string[];
53
53
  };
54
+ codegraph: {
55
+ excludePatterns?: string[];
56
+ };
54
57
  server: {
55
58
  transport?: 'stdio' | 'http';
56
59
  dashboard?: boolean;
@@ -147,6 +150,9 @@ export function getResolvedConfig(options: ResolvedLaneOptions = {}): ResolvedMe
147
150
  excludePatterns: firstArray(toml.git?.exclude_patterns, yaml.git?.excludePatterns),
148
151
  noiseKeywords: firstArray(toml.git?.noise_keywords, yaml.git?.noiseKeywords),
149
152
  },
153
+ codegraph: {
154
+ excludePatterns: firstArray(toml.codegraph?.exclude_patterns, yaml.codegraph?.excludePatterns),
155
+ },
150
156
  server: {
151
157
  transport: first(toml.server?.transport, yaml.server?.transport),
152
158
  dashboard: firstBool(toml.server?.dashboard, yaml.server?.dashboard),
@@ -39,6 +39,9 @@ export interface MemorixTomlConfig {
39
39
  exclude_patterns?: string[];
40
40
  noise_keywords?: string[];
41
41
  };
42
+ codegraph?: {
43
+ exclude_patterns?: string[];
44
+ };
42
45
  server?: {
43
46
  transport?: 'stdio' | 'http';
44
47
  dashboard?: boolean;
@@ -91,6 +94,7 @@ function mergeTomlConfig(base: MemorixTomlConfig, override: MemorixTomlConfig):
91
94
  embedding: { ...base.embedding, ...override.embedding },
92
95
  hooks: { ...base.hooks, ...override.hooks },
93
96
  git: { ...base.git, ...override.git },
97
+ codegraph: { ...base.codegraph, ...override.codegraph },
94
98
  server: { ...base.server, ...override.server },
95
99
  };
96
100
  }
@@ -64,6 +64,12 @@ export interface MemorixYamlConfig {
64
64
  noiseKeywords?: string[];
65
65
  };
66
66
 
67
+ /** CodeGraph / Project Context configuration */
68
+ codegraph?: {
69
+ /** File patterns to exclude from CodeGraph indexing and context suggestions */
70
+ excludePatterns?: string[];
71
+ };
72
+
67
73
  /** Behavior settings */
68
74
  behavior?: {
69
75
  /** Session start injection mode */
@@ -184,6 +190,7 @@ export function loadYamlConfig(projectRoot?: string | null): MemorixYamlConfig {
184
190
  agent: { ...userConfig.agent, ...projectConfig.agent },
185
191
  embedding: { ...userConfig.embedding, ...projectConfig.embedding },
186
192
  git: { ...userConfig.git, ...projectConfig.git },
193
+ codegraph: { ...userConfig.codegraph, ...projectConfig.codegraph },
187
194
  behavior: { ...userConfig.behavior, ...projectConfig.behavior },
188
195
  server: { ...userConfig.server, ...projectConfig.server },
189
196
  team: { ...userConfig.team, ...projectConfig.team },
package/src/server.ts CHANGED
@@ -1295,8 +1295,10 @@ export async function createMemorixServer(
1295
1295
  return withFreshIndex(async () => {
1296
1296
  const { CodeGraphStore } = await import('./codegraph/store.js');
1297
1297
  const { assembleContextPackForTask, buildContextPackPrompt } = await import('./codegraph/context-pack.js');
1298
+ const { getResolvedConfig } = await import('./config/resolved-config.js');
1298
1299
  const store = new CodeGraphStore();
1299
1300
  await store.init(projectDir);
1301
+ const exclude = getResolvedConfig({ projectRoot: project.rootPath }).codegraph.excludePatterns;
1300
1302
 
1301
1303
  const observations = getAllObservations()
1302
1304
  .filter(obs => obs.projectId === project.id && (obs.status ?? 'active') === 'active')
@@ -1307,6 +1309,7 @@ export async function createMemorixServer(
1307
1309
  task,
1308
1310
  observations,
1309
1311
  limit: typeof limit === 'number' ? limit : 20,
1312
+ exclude,
1310
1313
  });
1311
1314
  const text = buildContextPackPrompt(pack);
1312
1315
 
@@ -8,7 +8,7 @@
8
8
  * Vector search (embeddings) will be added in P1 phase.
9
9
  */
10
10
 
11
- import { create, insert, search, remove, update, count, type AnyOrama } from '@orama/orama';
11
+ import { create, insert, search, remove, update, count, getByID, type AnyOrama } from '@orama/orama';
12
12
  import type { MemorixDocument, SearchOptions, IndexEntry, KnowledgeLayer } from '../types.js';
13
13
  import { OBSERVATION_ICONS, type ObservationType } from '../types.js';
14
14
  import { resolveKnowledgeLayer } from '../skills/mini-skills.js';
@@ -52,9 +52,13 @@ function makeEntryKey(projectId: string | undefined, observationId: number): str
52
52
  return `${projectId ?? ''}::${observationId}`;
53
53
  }
54
54
 
55
- function rememberObservationDoc(doc: MemorixDocument): void {
56
- if (!doc.projectId || typeof doc.observationId !== 'number') return;
57
- docByObservationKey.set(makeEntryKey(doc.projectId, doc.observationId), doc);
55
+ function rememberObservationDoc(doc: MemorixDocument): MemorixDocument {
56
+ const publicDoc = { ...doc };
57
+ delete publicDoc.embedding;
58
+ if (doc.projectId && typeof doc.observationId === 'number') {
59
+ docByObservationKey.set(makeEntryKey(doc.projectId, doc.observationId), publicDoc);
60
+ }
61
+ return publicDoc;
58
62
  }
59
63
 
60
64
  function isCommandLikeQuery(query: string): boolean {
@@ -243,15 +247,15 @@ export async function batchGenerateEmbeddings(texts: string[]): Promise<(number[
243
247
  */
244
248
  export async function hydrateIndex(observations: any[]): Promise<number> {
245
249
  const database = await getDb();
246
- const currentCount = await count(database);
247
- if (currentCount > 0) return 0; // already hydrated
248
250
 
249
251
  let inserted = 0;
250
252
  for (const obs of observations) {
251
253
  if (!obs || !obs.id || !obs.projectId) continue;
252
254
  try {
255
+ const id = makeOramaObservationId(obs.projectId, obs.id);
256
+ if (getByID(database, id)) continue;
253
257
  const doc: MemorixDocument = {
254
- id: makeOramaObservationId(obs.projectId, obs.id),
258
+ id,
255
259
  observationId: obs.id,
256
260
  entityName: obs.entityName || '',
257
261
  type: obs.type || 'discovery',
@@ -376,6 +380,7 @@ export async function searchObservations(options: SearchOptions): Promise<IndexE
376
380
  let searchParams: Record<string, unknown> = {
377
381
  term: originalQuery,
378
382
  limit: requestLimit,
383
+ includeVectors: true,
379
384
  ...(Object.keys(filters).length > 0 ? { where: filters } : {}),
380
385
  // Search specific fields (not tokens, accessCount, etc.)
381
386
  properties: ['title', 'entityName', 'narrative', 'facts', 'concepts', 'filesModified'],
@@ -458,6 +463,7 @@ export async function searchObservations(options: SearchOptions): Promise<IndexE
458
463
  const vectorOnlyParams: Record<string, unknown> = {
459
464
  term: '',
460
465
  limit: requestLimit,
466
+ includeVectors: true,
461
467
  ...(Object.keys(filters).length > 0 ? { where: filters } : {}),
462
468
  mode: 'vector',
463
469
  vector: {
@@ -887,6 +893,7 @@ export async function getObservationsByIds(
887
893
 
888
894
  const searchResult = await search(database, {
889
895
  term: '',
896
+ includeVectors: true,
890
897
  where: {
891
898
  observationId: { eq: id },
892
899
  ...(projectId ? { projectId } : {}),
@@ -895,7 +902,7 @@ export async function getObservationsByIds(
895
902
  });
896
903
 
897
904
  if (searchResult.hits.length > 0) {
898
- results.push(searchResult.hits[0].document as unknown as MemorixDocument);
905
+ results.push(rememberObservationDoc(searchResult.hits[0].document as unknown as MemorixDocument));
899
906
  }
900
907
  }
901
908