memorix 1.7.2 → 1.8.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 (90) hide show
  1. package/CHANGELOG.md +59 -1
  2. package/README.md +1 -1
  3. package/README.zh-CN.md +1 -1
  4. package/assets/star-history-dark.svg +4 -4
  5. package/assets/star-history-light.svg +4 -4
  6. package/assets/star-history.json +47 -3
  7. package/dist/cli/index.js +1801 -254
  8. package/dist/dashboard/static/app.js +88 -4
  9. package/dist/dashboard/static/index.html +10 -0
  10. package/dist/dashboard/static/style.css +86 -0
  11. package/dist/index.js +1241 -224
  12. package/dist/maintenance-runner.js +972 -126
  13. package/dist/media-audio-runner.js +967 -121
  14. package/dist/media-video-runner.js +972 -126
  15. package/dist/memcode-runtime/CHANGELOG.md +59 -1
  16. package/dist/sdk.js +1240 -223
  17. package/dist/types.d.ts +8 -0
  18. package/dist/vector-backfill-runner.js +972 -126
  19. package/docs/1.2.4-PERSISTENT-MEMORY-DELIVERY.md +1 -1
  20. package/docs/1.8.0-RELEASE-CANDIDATE-SPEC.md +75 -0
  21. package/docs/API_REFERENCE.md +37 -1
  22. package/docs/ARCHITECTURE.md +1 -1
  23. package/docs/CONFIGURATION.md +57 -1
  24. package/docs/DASHBOARD.md +10 -0
  25. package/docs/DEVELOPMENT.md +3 -2
  26. package/docs/INTEGRATIONS.md +1 -1
  27. package/docs/PERFORMANCE.md +3 -1
  28. package/docs/README.md +11 -1
  29. package/docs/SETUP.md +2 -1
  30. package/package.json +1 -1
  31. package/plugins/antigravity/memorix/rules/memorix.md +1 -1
  32. package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
  33. package/plugins/claude/memorix/SKILL.md +1 -1
  34. package/plugins/claude/memorix/skills/memorix-memory/SKILL.md +2 -2
  35. package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
  36. package/plugins/codebuddy/memorix/skills/memorix-memory/SKILL.md +2 -2
  37. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  38. package/plugins/codex/memorix/skills/memorix-memory/SKILL.md +2 -2
  39. package/plugins/copilot/memorix/plugin.json +1 -1
  40. package/plugins/copilot/memorix/skills/memorix-memory/SKILL.md +2 -2
  41. package/plugins/cursor/memorix/skills/memorix-memory/SKILL.md +2 -2
  42. package/plugins/gemini/memorix/GEMINI.md +1 -1
  43. package/plugins/gemini/memorix/gemini-extension.json +1 -1
  44. package/plugins/omp/memorix/package.json +1 -1
  45. package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
  46. package/plugins/pi/memorix/package.json +1 -1
  47. package/plugins/pi/memorix/skills/memorix-memory/SKILL.md +2 -2
  48. package/server.json +2 -2
  49. package/src/cli/capability-map.ts +2 -0
  50. package/src/cli/commands/config-migrate.ts +7 -0
  51. package/src/cli/commands/context.ts +11 -0
  52. package/src/cli/commands/doctor.ts +5 -1
  53. package/src/cli/commands/evidence.ts +56 -0
  54. package/src/cli/commands/feedback.ts +58 -0
  55. package/src/cli/commands/init.ts +17 -0
  56. package/src/cli/commands/resume.ts +2 -0
  57. package/src/cli/commands/serve-http.ts +201 -12
  58. package/src/cli/commands/status.ts +2 -0
  59. package/src/cli/fallback-budget.ts +59 -0
  60. package/src/cli/index.ts +5 -1
  61. package/src/compact/engine.ts +26 -1
  62. package/src/config/dotenv-loader.ts +17 -0
  63. package/src/config/resolved-config.ts +110 -12
  64. package/src/config/toml-loader.ts +8 -0
  65. package/src/config/yaml-loader.ts +9 -0
  66. package/src/dashboard/config-provenance.ts +17 -0
  67. package/src/dashboard/evidence.ts +63 -0
  68. package/src/dashboard/server.ts +93 -68
  69. package/src/dashboard/static/index.html +10 -0
  70. package/src/dashboard/static/style.css +86 -0
  71. package/src/hooks/installers/index.ts +1 -1
  72. package/src/hooks/official-skills.ts +3 -3
  73. package/src/hooks/rules/memorix-agent-rules.md +1 -1
  74. package/src/llm/provider.ts +33 -7
  75. package/src/memory/feedback-model.ts +4 -0
  76. package/src/memory/feedback.ts +220 -0
  77. package/src/memory/observations.ts +19 -0
  78. package/src/memory/retention.ts +38 -5
  79. package/src/multimodal/image-loader.ts +6 -3
  80. package/src/rerank/http-provider.ts +125 -0
  81. package/src/rerank/index.ts +81 -0
  82. package/src/rerank/policy.ts +88 -0
  83. package/src/server/mcp-binding-store.ts +79 -0
  84. package/src/server/mcp-diagnostics.ts +40 -0
  85. package/src/server/tool-profile.ts +4 -0
  86. package/src/server.ts +121 -1
  87. package/src/store/evidence-store.ts +261 -0
  88. package/src/store/orama-store.ts +79 -53
  89. package/src/store/sqlite-db.ts +104 -0
  90. package/src/types.ts +8 -0
@@ -22,6 +22,8 @@ import { getMiniSkillStore } from '../store/mini-skill-store.js';
22
22
  import { miniSkillToDocument, resolveProvenanceStatus, type ProvenanceStatus } from '../skills/mini-skills.js';
23
23
  import { redactCredentials } from '../memory/secret-filter.js';
24
24
  import { canReadObservation, filterReadableObservations } from '../memory/visibility.js';
25
+ import { getProjectDataDir } from '../store/persistence.js';
26
+ import { MemoryFeedbackStore, feedbackWeightMultiplier } from '../memory/feedback.js';
25
27
 
26
28
  export function normalizeMemoryBrowseQuery(query: string): string {
27
29
  const trimmed = query.trim();
@@ -71,9 +73,32 @@ export async function compactSearch(options: SearchOptions, surface: RetrievalSu
71
73
  }> {
72
74
  await ensureFreshIndex();
73
75
  const searchOptions = { ...options, query: normalizeMemoryBrowseQuery(options.query) };
74
- const entries = (await searchObservations(searchOptions)).map((entry) =>
76
+ let entries = (await searchObservations(searchOptions)).map((entry) =>
75
77
  entry.projectId || !options.projectId ? entry : { ...entry, projectId: options.projectId },
76
78
  );
79
+ if (options.projectId) {
80
+ try {
81
+ const feedback = new MemoryFeedbackStore();
82
+ await feedback.init(await getProjectDataDir(options.projectId));
83
+ const states = feedback.weights(options.projectId, 'observation');
84
+ entries = entries
85
+ .map((entry) => {
86
+ const state = states.get(String(entry.id));
87
+ if (!state) return entry;
88
+ return {
89
+ ...entry,
90
+ score: (entry.score ?? 0) * feedbackWeightMultiplier(state.weight),
91
+ feedbackWeight: state.weight,
92
+ feedbackStatus: state.status,
93
+ };
94
+ })
95
+ .filter((entry) => entry.feedbackStatus !== 'archived' || options.status === 'all')
96
+ .sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
97
+ } catch {
98
+ // Feedback is an enrichment lane. Retrieval remains available if an old
99
+ // local database has not yet run the 1.8 schema migration.
100
+ }
101
+ }
77
102
  let formatted = formatIndexTable(entries, searchOptions.query, !options.projectId, surface);
78
103
 
79
104
  if (entries.length === 0 && options.projectId) {
@@ -31,6 +31,8 @@ let dotenvProjectRoot: string | null = null;
31
31
 
32
32
  /** Track which .env files were loaded (for diagnostics) */
33
33
  const loadedEnvFiles: string[] = [];
34
+ const loadedEnvKeys = new Set<string>();
35
+ const loadedEnvValues = new Map<string, string>();
34
36
  /** Track keys injected by .env so project switches can cleanly restore process.env */
35
37
  const injectedKeys = new Set<string>();
36
38
 
@@ -40,6 +42,8 @@ function loadEnvFile(filePath: string): void {
40
42
  const parsed = parse(readFileSync(filePath, 'utf-8'));
41
43
  for (const [key, value] of Object.entries(parsed)) {
42
44
  if (!(key in process.env)) {
45
+ loadedEnvKeys.add(key);
46
+ loadedEnvValues.set(key, value);
43
47
  process.env[key] = value;
44
48
  injectedKeys.add(key);
45
49
  }
@@ -68,6 +72,8 @@ export function loadDotenv(projectRoot?: string, options: DotenvLoadOptions = {}
68
72
  }
69
73
 
70
74
  loadedEnvFiles.length = 0;
75
+ loadedEnvKeys.clear();
76
+ loadedEnvValues.clear();
71
77
 
72
78
  // Loading order = priority order (with override: false, first value wins).
73
79
  // System env vars already exist in process.env, so they always win.
@@ -96,6 +102,7 @@ export function resetDotenv(): void {
96
102
  dotenvLoaded = false;
97
103
  dotenvProjectRoot = null;
98
104
  loadedEnvFiles.length = 0;
105
+ loadedEnvKeys.clear();
99
106
  }
100
107
 
101
108
  /**
@@ -105,6 +112,16 @@ export function getLoadedEnvFiles(): readonly string[] {
105
112
  return loadedEnvFiles;
106
113
  }
107
114
 
115
+ /** Get names found in loaded .env files without exposing their values. */
116
+ export function getLoadedEnvKeys(): ReadonlySet<string> {
117
+ return loadedEnvKeys;
118
+ }
119
+
120
+ /** Get the value injected by .env, if the current process still has that value. */
121
+ export function getLoadedEnvValue(key: string): string | undefined {
122
+ return loadedEnvValues.get(key);
123
+ }
124
+
108
125
  // ─── Supported .env variables ───
109
126
  // These are the ONLY variables Memorix reads from .env.
110
127
  // All are secrets or endpoint URLs — no behavior config.
@@ -9,8 +9,8 @@ import {
9
9
  getProjectYamlPath,
10
10
  } from './config-paths.js';
11
11
  import { loadFileConfig } from './legacy-loader.js';
12
- import { loadTomlConfig } from './toml-loader.js';
13
- import { loadYamlConfig } from './yaml-loader.js';
12
+ import { loadTomlConfig, type MemorixTomlConfig } from './toml-loader.js';
13
+ import { loadYamlConfig, type MemorixYamlConfig } from './yaml-loader.js';
14
14
  import { loadDotenv } from './dotenv-loader.js';
15
15
 
16
16
  export interface ResolvedLaneOptions {
@@ -44,6 +44,14 @@ export interface ResolvedMemorixConfig {
44
44
  apiKey?: string;
45
45
  dimensions?: number;
46
46
  };
47
+ rerank: {
48
+ provider: 'off' | 'http';
49
+ model?: string;
50
+ baseUrl?: string;
51
+ apiKey?: string;
52
+ /** False when a Memory LLM key exists and the rerank URL is a different host without a dedicated key. */
53
+ canSendRequest: boolean;
54
+ };
47
55
  git: {
48
56
  autoHook?: boolean;
49
57
  ingestOnCommit?: boolean;
@@ -109,6 +117,16 @@ export function getResolvedConfig(options: ResolvedLaneOptions = {}): ResolvedMe
109
117
  const openRouterMemoryLlmApiKey = isOpenRouterMemoryLane(memoryLlmProvider, memoryLlmBaseUrl)
110
118
  ? process.env.OPENROUTER_API_KEY
111
119
  : undefined;
120
+ const memoryLlmApiKey = first(
121
+ process.env.MEMORIX_LLM_API_KEY,
122
+ process.env.MEMORIX_API_KEY,
123
+ toml.memory?.llm?.api_key,
124
+ yaml.llm?.apiKey,
125
+ legacy.llm?.apiKey,
126
+ process.env.OPENAI_API_KEY,
127
+ process.env.ANTHROPIC_API_KEY,
128
+ openRouterMemoryLlmApiKey,
129
+ );
112
130
 
113
131
  const resolved: ResolvedMemorixConfig = {
114
132
  agent: {
@@ -150,16 +168,7 @@ export function getResolvedConfig(options: ResolvedLaneOptions = {}): ResolvedMe
150
168
  provider: memoryLlmProvider,
151
169
  model: memoryLlmModel,
152
170
  baseUrl: memoryLlmBaseUrl,
153
- apiKey: first(
154
- process.env.MEMORIX_LLM_API_KEY,
155
- process.env.MEMORIX_API_KEY,
156
- toml.memory?.llm?.api_key,
157
- yaml.llm?.apiKey,
158
- legacy.llm?.apiKey,
159
- process.env.OPENAI_API_KEY,
160
- process.env.ANTHROPIC_API_KEY,
161
- openRouterMemoryLlmApiKey,
162
- ),
171
+ apiKey: memoryLlmApiKey,
163
172
  },
164
173
  },
165
174
  embedding: {
@@ -169,6 +178,12 @@ export function getResolvedConfig(options: ResolvedLaneOptions = {}): ResolvedMe
169
178
  apiKey: first(process.env.MEMORIX_EMBEDDING_API_KEY, toml.embedding?.api_key, yaml.embedding?.apiKey, legacy.embeddingApi?.apiKey, openRouterEmbeddingApiKey),
170
179
  dimensions: firstNumber(parseNumber(process.env.MEMORIX_EMBEDDING_DIMENSIONS), toml.embedding?.dimensions, yaml.embedding?.dimensions, legacy.embeddingApi?.dimensions),
171
180
  },
181
+ rerank: resolveRerankLane({
182
+ toml,
183
+ yaml,
184
+ memoryLlmApiKey,
185
+ memoryLlmBaseUrl,
186
+ }),
172
187
  git: {
173
188
  autoHook: firstBool(toml.git?.auto_hook, yaml.git?.autoHook),
174
189
  ingestOnCommit: firstBool(toml.git?.ingest_on_commit, yaml.git?.ingestOnCommit),
@@ -244,6 +259,10 @@ export function getResolvedEmbeddingLane(options: ResolvedLaneOptions = {}): Res
244
259
  return getResolvedConfig(options).embedding;
245
260
  }
246
261
 
262
+ export function getResolvedRerankLane(options: ResolvedLaneOptions = {}): ResolvedMemorixConfig['rerank'] {
263
+ return getResolvedConfig(options).rerank;
264
+ }
265
+
247
266
  export function resetResolvedConfigCache(): void {
248
267
  // Kept as a public test helper. File-level caches live in individual loaders.
249
268
  }
@@ -294,6 +313,10 @@ function getEnvSourceNames(): string[] {
294
313
  'MEMORIX_EMBEDDING_BASE_URL',
295
314
  'MEMORIX_EMBEDDING_MODEL',
296
315
  'MEMORIX_EMBEDDING_DIMENSIONS',
316
+ 'MEMORIX_RERANK_PROVIDER',
317
+ 'MEMORIX_RERANK_MODEL',
318
+ 'MEMORIX_RERANK_BASE_URL',
319
+ 'MEMORIX_RERANK_API_KEY',
297
320
  'MEMORIX_CODEGRAPH_EXTERNAL_CONTEXT',
298
321
  'MEMORIX_CODEGRAPH_EXTERNAL_COMMAND',
299
322
  'MEMORIX_CODEGRAPH_EXTERNAL_TIMEOUT_MS',
@@ -320,3 +343,78 @@ function normalizeExternalContext(value: string | undefined): 'auto' | 'off' | u
320
343
  if (normalized === 'auto' || normalized === 'off') return normalized;
321
344
  return undefined;
322
345
  }
346
+
347
+ function normalizeRerankProvider(value: string | undefined): 'off' | 'http' {
348
+ const normalized = value?.trim().toLowerCase();
349
+ if (normalized === 'http') return 'http';
350
+ return 'off';
351
+ }
352
+
353
+ function resolveRerankLane(args: {
354
+ toml: MemorixTomlConfig;
355
+ yaml: MemorixYamlConfig;
356
+ memoryLlmApiKey?: string;
357
+ memoryLlmBaseUrl?: string;
358
+ }): ResolvedMemorixConfig['rerank'] {
359
+ const provider = normalizeRerankProvider(first(
360
+ process.env.MEMORIX_RERANK_PROVIDER,
361
+ args.toml.rerank?.provider,
362
+ args.yaml.rerank?.provider,
363
+ 'off',
364
+ ));
365
+ const model = first(
366
+ process.env.MEMORIX_RERANK_MODEL,
367
+ args.toml.rerank?.model,
368
+ args.yaml.rerank?.model,
369
+ );
370
+ const explicitBaseUrl = first(
371
+ process.env.MEMORIX_RERANK_BASE_URL,
372
+ args.toml.rerank?.base_url,
373
+ args.yaml.rerank?.baseUrl,
374
+ );
375
+ const baseUrl = provider === 'http'
376
+ ? first(explicitBaseUrl, args.memoryLlmBaseUrl)
377
+ : explicitBaseUrl;
378
+ const dedicatedApiKey = first(
379
+ process.env.MEMORIX_RERANK_API_KEY,
380
+ args.toml.rerank?.api_key,
381
+ args.yaml.rerank?.apiKey,
382
+ );
383
+ const sameEndpoint = provider === 'http' && sameTrustedEndpoint(baseUrl, args.memoryLlmBaseUrl);
384
+ const inheritedApiKey = sameEndpoint ? args.memoryLlmApiKey : undefined;
385
+ const apiKey = first(dedicatedApiKey, inheritedApiKey);
386
+ const canSendRequest = provider === 'http' && Boolean(baseUrl)
387
+ && (Boolean(apiKey) || !args.memoryLlmApiKey || sameEndpoint);
388
+
389
+ return { provider, model, baseUrl, apiKey, canSendRequest };
390
+ }
391
+
392
+ /**
393
+ * True when two API roots are the same host+path after trailing-slash
394
+ * and optional `/rerank` normalization. Used so a Memory LLM bearer is
395
+ * never sent to a different rerank provider.
396
+ */
397
+ function sameTrustedEndpoint(left?: string, right?: string): boolean {
398
+ const a = normalizeTrustedEndpoint(left);
399
+ const b = normalizeTrustedEndpoint(right);
400
+ return Boolean(a && b && a === b);
401
+ }
402
+
403
+ function normalizeTrustedEndpoint(value?: string): string | undefined {
404
+ const trimmed = value?.trim();
405
+ if (!trimmed) return undefined;
406
+ try {
407
+ const url = new URL(trimmed);
408
+ const protocol = url.protocol.toLowerCase();
409
+ const hostname = url.hostname.toLowerCase();
410
+ const port = url.port;
411
+ let pathname = url.pathname.replace(/\/+$/, '');
412
+ if (pathname.toLowerCase().endsWith('/rerank')) {
413
+ pathname = pathname.slice(0, -'/rerank'.length).replace(/\/+$/, '');
414
+ }
415
+ return `${protocol}//${hostname}${port ? `:${port}` : ''}${pathname}`;
416
+ } catch {
417
+ const fallback = trimmed.replace(/\/+$/, '').replace(/\/rerank$/i, '').replace(/\/+$/, '');
418
+ return fallback.toLowerCase() || undefined;
419
+ }
420
+ }
@@ -28,6 +28,13 @@ export interface MemorixTomlConfig {
28
28
  api_key?: string;
29
29
  dimensions?: number;
30
30
  };
31
+ /** Optional HTTP rerank against a compatible /rerank API. */
32
+ rerank?: {
33
+ provider?: 'off' | 'http' | string;
34
+ model?: string;
35
+ base_url?: string;
36
+ api_key?: string;
37
+ };
31
38
  hooks?: {
32
39
  native_memcode?: boolean;
33
40
  external_agents?: boolean;
@@ -97,6 +104,7 @@ function mergeTomlConfig(base: MemorixTomlConfig, override: MemorixTomlConfig):
97
104
  llm: { ...base.memory?.llm, ...override.memory?.llm },
98
105
  },
99
106
  embedding: { ...base.embedding, ...override.embedding },
107
+ rerank: { ...base.rerank, ...override.rerank },
100
108
  hooks: { ...base.hooks, ...override.hooks },
101
109
  git: { ...base.git, ...override.git },
102
110
  codegraph: { ...base.codegraph, ...override.codegraph },
@@ -48,6 +48,14 @@ export interface MemorixYamlConfig {
48
48
  dimensions?: number;
49
49
  };
50
50
 
51
+ /** Remote neural rerank (HTTP only) */
52
+ rerank?: {
53
+ provider?: 'off' | 'http' | string;
54
+ model?: string;
55
+ apiKey?: string;
56
+ baseUrl?: string;
57
+ };
58
+
51
59
  /** Git-Memory pipeline configuration */
52
60
  git?: {
53
61
  /** Auto-install post-commit hook on first run (default: false) */
@@ -190,6 +198,7 @@ export function loadYamlConfig(projectRoot?: string | null): MemorixYamlConfig {
190
198
  llm: { ...userConfig.llm, ...projectConfig.llm },
191
199
  agent: { ...userConfig.agent, ...projectConfig.agent },
192
200
  embedding: { ...userConfig.embedding, ...projectConfig.embedding },
201
+ rerank: { ...userConfig.rerank, ...projectConfig.rerank },
193
202
  git: { ...userConfig.git, ...projectConfig.git },
194
203
  codegraph: { ...userConfig.codegraph, ...projectConfig.codegraph },
195
204
  behavior: { ...userConfig.behavior, ...projectConfig.behavior },
@@ -0,0 +1,17 @@
1
+ import { getLoadedEnvValue } from '../config/dotenv-loader.js';
2
+
3
+ export interface DashboardKeySourceOptions {
4
+ value: string;
5
+ envKeys: readonly string[];
6
+ configSource: string;
7
+ }
8
+
9
+ /** Resolve a secret source using the ordered environment fallback candidates. */
10
+ export function resolveDashboardKeySource({ value, envKeys, configSource }: DashboardKeySourceOptions): string {
11
+ for (const envKey of envKeys) {
12
+ const envValue = process.env[envKey];
13
+ if (envValue !== value) continue;
14
+ return getLoadedEnvValue(envKey) === envValue ? '.env' : `env:${envKey}`;
15
+ }
16
+ return configSource;
17
+ }
@@ -0,0 +1,63 @@
1
+ import type { Entity, Relation, Observation } from '../types.js';
2
+
3
+ export interface EvidenceCard {
4
+ id: number;
5
+ title: string;
6
+ type: string;
7
+ entityName: string;
8
+ why: string;
9
+ source: string;
10
+ sessionId?: string;
11
+ createdAt: string;
12
+ updatedAt?: string;
13
+ status: string;
14
+ verification: 'unverified' | 'verified' | 'conflicted';
15
+ files: string[];
16
+ relatedEntities: string[];
17
+ correctionHistory: string[];
18
+ }
19
+
20
+ function relationNeighbors(entityName: string, relations: Relation[]): string[] {
21
+ return [...new Set(relations.flatMap((relation) => {
22
+ if (relation.from === entityName) return [relation.to];
23
+ if (relation.to === entityName) return [relation.from];
24
+ return [];
25
+ }))].slice(0, 8);
26
+ }
27
+
28
+ /** Build bounded, operator-readable evidence cards from persisted records. */
29
+ export function buildEvidenceCards(
30
+ observations: Observation[],
31
+ graph: { entities: Entity[]; relations: Relation[] },
32
+ query = '',
33
+ limit = 20,
34
+ ): EvidenceCard[] {
35
+ const needle = query.trim().toLowerCase();
36
+ return observations
37
+ .filter((observation) => !needle || [observation.title, observation.narrative, observation.entityName, ...(observation.concepts ?? [])]
38
+ .join(' ').toLowerCase().includes(needle))
39
+ .sort((a, b) => b.id - a.id)
40
+ .slice(0, Math.max(1, Math.min(100, limit)))
41
+ .map((observation) => {
42
+ const neighbors = relationNeighbors(observation.entityName, graph.relations);
43
+ const entity = graph.entities.find((candidate) => candidate.name === observation.entityName);
44
+ const verification = observation.status === 'resolved' ? 'verified' :
45
+ observation.status === 'archived' ? 'conflicted' : 'unverified';
46
+ return {
47
+ id: observation.id,
48
+ title: observation.title,
49
+ type: observation.type,
50
+ entityName: observation.entityName,
51
+ why: entity ? 'Matched persisted entity and project-scoped evidence.' : 'Matched project-scoped memory record.',
52
+ source: observation.sourceDetail || observation.source || 'unknown',
53
+ ...(observation.sessionId ? { sessionId: observation.sessionId } : {}),
54
+ createdAt: observation.createdAt,
55
+ ...(observation.updatedAt ? { updatedAt: observation.updatedAt } : {}),
56
+ status: observation.status || 'active',
57
+ verification,
58
+ files: observation.filesModified || [],
59
+ relatedEntities: neighbors,
60
+ correctionHistory: [],
61
+ };
62
+ });
63
+ }
@@ -22,10 +22,13 @@ import { loadDotenv } from '../config/dotenv-loader.js';
22
22
  import { resetDotenv } from '../config/dotenv-loader.js';
23
23
  import { initProjectRoot } from '../config/yaml-loader.js';
24
24
  import { clearProjectRoot } from '../config/yaml-loader.js';
25
+ import { getResolvedConfig } from '../config/resolved-config.js';
25
26
  import { scopeKnowledgeGraphToProject } from '../memory/graph-scope.js';
26
27
  import { projectObservationRetention, summarizeRetentionProjections } from '../memory/retention.js';
27
28
  import { canManageObservation, filterReadableObservations } from '../memory/visibility.js';
28
29
  import type { Observation } from '../types.js';
30
+ import { EvidenceCardStore } from '../store/evidence-store.js';
31
+ import { MemoryFeedbackStore } from '../memory/feedback.js';
29
32
  import {
30
33
  DashboardMaintenanceError,
31
34
  executeCleanup,
@@ -37,6 +40,7 @@ import {
37
40
  previewDeduplicate,
38
41
  previewRetentionArchive,
39
42
  } from './maintenance.js';
43
+ import { resolveDashboardKeySource } from './config-provenance.js';
40
44
 
41
45
  // MIME types for static file serving
42
46
  const MIME_TYPES: Record<string, string> = {
@@ -326,6 +330,22 @@ async function handleApi(
326
330
  break;
327
331
  }
328
332
 
333
+ case '/evidence': {
334
+ const observations = filterDashboardObservations(
335
+ await getObservationStore().loadByProject(effectiveProjectId, { status: 'active' }),
336
+ effectiveProjectId,
337
+ );
338
+ const evidenceStore = new EvidenceCardStore();
339
+ await evidenceStore.init(effectiveDataDir);
340
+ evidenceStore.syncObservations(observations);
341
+ const query = url.searchParams.get('q') || '';
342
+ const limit = Number(url.searchParams.get('limit') || 20);
343
+ const cards = evidenceStore.list(effectiveProjectId, { limit: Math.min(100, Math.max(1, limit)) })
344
+ .filter((card) => !query || [card.title, card.summary, card.sourceRef, card.locator ?? ''].join(' ').toLowerCase().includes(query.toLowerCase()));
345
+ sendJson(res, { cards, projectId: effectiveProjectId });
346
+ break;
347
+ }
348
+
329
349
  case '/sessions': {
330
350
  const allSessions = await getSessionStore().loadAll();
331
351
  const sessions = filterByProject(allSessions as Array<{ projectId?: string }>, effectiveProjectId);
@@ -381,10 +401,15 @@ async function handleApi(
381
401
  }));
382
402
 
383
403
  const retentionReferenceTime = new Date(now);
404
+ const feedback = new MemoryFeedbackStore();
405
+ await feedback.init(effectiveDataDir).catch(() => undefined);
384
406
  const retention = summarizeRetentionProjections(
385
407
  observations
386
408
  .filter((observation) => observation.type !== 'probe')
387
- .map((observation) => projectObservationRetention(observation, { referenceTime: retentionReferenceTime })),
409
+ .map((observation) => projectObservationRetention(observation, {
410
+ referenceTime: retentionReferenceTime,
411
+ feedback: feedback.getState(effectiveProjectId, 'observation', String(observation.id)),
412
+ })),
388
413
  );
389
414
  const retentionSummary = {
390
415
  active: retention.active,
@@ -489,11 +514,16 @@ async function handleApi(
489
514
  );
490
515
 
491
516
  const referenceTime = new Date();
517
+ const feedback = new MemoryFeedbackStore();
518
+ await feedback.init(effectiveDataDir).catch(() => undefined);
492
519
  const rows = observations
493
520
  .filter((observation) => observation.type !== 'probe')
494
521
  .map((observation) => ({
495
522
  observation,
496
- retention: projectObservationRetention(observation, { referenceTime }),
523
+ retention: projectObservationRetention(observation, {
524
+ referenceTime,
525
+ feedback: feedback.getState(effectiveProjectId, 'observation', String(observation.id)),
526
+ }),
497
527
  }))
498
528
  .sort((a, b) => b.retention.displayScore - a.retention.displayScore);
499
529
  const summary = summarizeRetentionProjections(rows.map((row) => row.retention));
@@ -580,26 +610,18 @@ async function handleApi(
580
610
  const { existsSync } = await import('node:fs');
581
611
  const { join } = await import('node:path');
582
612
 
583
- let yml: any = {};
584
613
  // Use the real project root from dashboard state, not process.cwd()
585
614
  const configProjectRoot = effectiveProjectRoot;
586
- try {
587
- const { loadYamlConfig } = await import('../config/yaml-loader.js');
588
- yml = configProjectRoot ? loadYamlConfig(configProjectRoot) : loadYamlConfig(null);
589
- } catch { /* best effort */ }
590
-
591
- // Load .env files so process.env reflects actual config (fixes #74, #62)
592
- if (configProjectRoot) {
593
- try {
594
- const { loadDotenv } = await import('../config/dotenv-loader.js');
595
- loadDotenv(configProjectRoot);
596
- } catch { /* best effort */ }
597
- }
615
+ // This is the same TOML-first chain used by status and runtime lanes.
616
+ // Do not fall back to user config if a bound project cannot resolve.
617
+ const resolved = getResolvedConfig({ projectRoot: configProjectRoot });
598
618
 
599
619
  // Check which config files exist
600
620
  const files: Record<string, { exists: boolean; path: string; unavailable?: boolean }> = {
601
621
  'project memorix.yml': { exists: false, path: '', unavailable: !configProjectRoot },
602
622
  'user memorix.yml': { exists: false, path: '' },
623
+ 'project memorix.toml': { exists: false, path: '', unavailable: !configProjectRoot },
624
+ 'user config.toml': { exists: false, path: '' },
603
625
  'project .env': { exists: false, path: '', unavailable: !configProjectRoot },
604
626
  'user .env': { exists: false, path: '' },
605
627
  'legacy config.json': { exists: false, path: '' },
@@ -609,6 +631,8 @@ async function handleApi(
609
631
  const paths: Record<string, string | null> = {
610
632
  'project memorix.yml': configProjectRoot ? join(configProjectRoot, 'memorix.yml') : null,
611
633
  'user memorix.yml': join(home, '.memorix', 'memorix.yml'),
634
+ 'project memorix.toml': configProjectRoot ? join(configProjectRoot, 'memorix.toml') : null,
635
+ 'user config.toml': join(home, '.memorix', 'config.toml'),
612
636
  'project .env': configProjectRoot ? join(configProjectRoot, '.env') : null,
613
637
  'user .env': join(home, '.memorix', '.env'),
614
638
  'legacy config.json': join(home, '.memorix', 'config.json'),
@@ -626,82 +650,83 @@ async function handleApi(
626
650
  const values: Array<{ key: string; value: string; source: string; sensitive?: boolean }> = [];
627
651
 
628
652
  // LLM
629
- // Helper: determine source label (distinguishes .env file vs system env)
630
- const getEnvSource = async (envKey: string, ymlSource?: string): Promise<string> => {
631
- if (process.env[envKey]) {
632
- // Check if this key was injected by dotenv-loader (from .env file)
633
- try {
634
- const { getLoadedEnvFiles } = await import('../config/dotenv-loader.js');
635
- const envFiles = getLoadedEnvFiles();
636
- if (envFiles.length > 0) return `.env (${envKey})`;
637
- } catch { /* ignore */ }
638
- return `env:${envKey}`;
639
- }
640
- return ymlSource ?? 'default';
641
- };
653
+ const tomlSource = configProjectRoot && files['project memorix.toml'].exists
654
+ ? 'memorix.toml'
655
+ : files['user config.toml'].exists ? 'config.toml' : 'default';
656
+ const legacySource = files['project memorix.yml'].exists || files['user memorix.yml'].exists
657
+ ? 'memorix.yml'
658
+ : files['legacy config.json'].exists ? 'config.json' : 'default';
659
+ const configSource = resolved.sources.toml.length > 0 ? tomlSource
660
+ : resolved.sources.legacy.length > 0 ? legacySource : 'default';
661
+ const source = (envKey: string): string => resolveDashboardKeySource({
662
+ value: process.env[envKey] ?? '',
663
+ envKeys: [envKey],
664
+ configSource,
665
+ });
642
666
 
643
- const llmProvider = process.env.MEMORIX_LLM_PROVIDER || yml.llm?.provider;
644
- if (llmProvider) values.push({ key: 'llm.provider', value: llmProvider, source: await getEnvSource('MEMORIX_LLM_PROVIDER', yml.llm?.provider ? 'memorix.yml' : undefined) });
667
+ if (resolved.memory.llm.provider) values.push({ key: 'llm.provider', value: resolved.memory.llm.provider, source: source('MEMORIX_LLM_PROVIDER') });
645
668
 
646
- const llmModel = process.env.MEMORIX_LLM_MODEL || yml.llm?.model;
647
- if (llmModel) values.push({ key: 'llm.model', value: llmModel, source: await getEnvSource('MEMORIX_LLM_MODEL', yml.llm?.model ? 'memorix.yml' : undefined) });
669
+ if (resolved.memory.llm.model) values.push({ key: 'llm.model', value: resolved.memory.llm.model, source: source('MEMORIX_LLM_MODEL') });
648
670
 
649
- const llmKey = process.env.MEMORIX_LLM_API_KEY || process.env.MEMORIX_API_KEY || yml.llm?.apiKey || process.env.OPENAI_API_KEY;
671
+ const llmKey = resolved.memory.llm.apiKey;
672
+ const memoryKeyEnv = [
673
+ 'MEMORIX_LLM_API_KEY',
674
+ 'MEMORIX_API_KEY',
675
+ 'OPENAI_API_KEY',
676
+ 'ANTHROPIC_API_KEY',
677
+ ...(resolved.memory.llm.provider === 'openrouter' || resolved.memory.llm.baseUrl?.includes('openrouter.ai')
678
+ ? ['OPENROUTER_API_KEY']
679
+ : []),
680
+ ];
650
681
  if (llmKey) {
651
- let src = 'unknown';
652
- if (process.env.MEMORIX_LLM_API_KEY) src = await getEnvSource('MEMORIX_LLM_API_KEY');
653
- else if (process.env.MEMORIX_API_KEY) src = await getEnvSource('MEMORIX_API_KEY');
654
- else if (yml.llm?.apiKey) src = 'memorix.yml (move to .env!)';
655
- else if (process.env.OPENAI_API_KEY) src = await getEnvSource('OPENAI_API_KEY');
682
+ const src = resolveDashboardKeySource({ value: llmKey, envKeys: memoryKeyEnv, configSource });
656
683
  values.push({ key: 'llm.apiKey', value: '****' + llmKey.slice(-4), source: src, sensitive: true });
657
684
  } else {
658
685
  values.push({ key: 'llm.apiKey', value: 'not set', source: 'none' });
659
686
  }
660
687
 
661
- const agentProvider = process.env.MEMORIX_AGENT_PROVIDER || process.env.MEMORIX_AGENT_LLM_PROVIDER || yml.agent?.provider;
662
- if (agentProvider) {
663
- let source: string;
664
- if (process.env.MEMORIX_AGENT_PROVIDER) source = await getEnvSource('MEMORIX_AGENT_PROVIDER');
665
- else if (process.env.MEMORIX_AGENT_LLM_PROVIDER) source = `${await getEnvSource('MEMORIX_AGENT_LLM_PROVIDER')} (legacy)`;
666
- else source = yml.agent?.provider ? 'memorix.yml' : 'default';
667
- values.push({ key: 'agent.provider', value: agentProvider, source });
688
+ if (resolved.agent.provider) {
689
+ values.push({ key: 'agent.provider', value: resolved.agent.provider, source: source('MEMORIX_AGENT_PROVIDER') });
668
690
  }
669
691
 
670
- const agentModel = process.env.MEMORIX_AGENT_MODEL || process.env.MEMORIX_AGENT_LLM_MODEL || yml.agent?.model;
671
- if (agentModel) {
672
- let source: string;
673
- if (process.env.MEMORIX_AGENT_MODEL) source = await getEnvSource('MEMORIX_AGENT_MODEL');
674
- else if (process.env.MEMORIX_AGENT_LLM_MODEL) source = `${await getEnvSource('MEMORIX_AGENT_LLM_MODEL')} (legacy)`;
675
- else source = yml.agent?.model ? 'memorix.yml' : 'default';
676
- values.push({ key: 'agent.model', value: agentModel, source });
692
+ if (resolved.agent.model) {
693
+ values.push({ key: 'agent.model', value: resolved.agent.model, source: source('MEMORIX_AGENT_MODEL') });
677
694
  }
678
695
 
679
- const agentKey = process.env.MEMORIX_AGENT_API_KEY || process.env.MEMORIX_AGENT_LLM_API_KEY || yml.agent?.apiKey;
696
+ const agentKey = resolved.agent.apiKey;
680
697
  if (agentKey) {
681
- let src = 'unknown';
682
- if (process.env.MEMORIX_AGENT_API_KEY) src = await getEnvSource('MEMORIX_AGENT_API_KEY');
683
- else if (process.env.MEMORIX_AGENT_LLM_API_KEY) src = `${await getEnvSource('MEMORIX_AGENT_LLM_API_KEY')} (legacy)`;
684
- else if (yml.agent?.apiKey) src = 'memorix.yml (move to .env!)';
698
+ const src = resolveDashboardKeySource({
699
+ value: agentKey,
700
+ envKeys: ['MEMORIX_AGENT_API_KEY', 'MEMORIX_AGENT_LLM_API_KEY'],
701
+ configSource,
702
+ });
685
703
  values.push({ key: 'agent.apiKey', value: '****' + agentKey.slice(-4), source: src, sensitive: true });
686
704
  } else {
687
- values.push({ key: 'agent.apiKey', value: 'fallback to llm.apiKey', source: 'default' });
705
+ values.push({
706
+ key: 'agent.apiKey',
707
+ value: 'fallback to llm.apiKey',
708
+ source: llmKey
709
+ ? resolveDashboardKeySource({ value: llmKey, envKeys: memoryKeyEnv, configSource })
710
+ : 'default',
711
+ });
688
712
  }
689
713
 
690
714
  // Embedding
691
- const embProvider = process.env.MEMORIX_EMBEDDING || yml.embedding?.provider || 'off';
692
- values.push({ key: 'embedding.provider', value: embProvider, source: await getEnvSource('MEMORIX_EMBEDDING', yml.embedding?.provider ? 'memorix.yml' : undefined) });
715
+ values.push({ key: 'embedding.provider', value: resolved.embedding.provider || 'off', source: source('MEMORIX_EMBEDDING') });
716
+ if (resolved.embedding.model) values.push({ key: 'embedding.model', value: resolved.embedding.model, source: source('MEMORIX_EMBEDDING_MODEL') });
693
717
 
694
- // Git
695
- values.push({ key: 'git.autoHook', value: String(yml.git?.autoHook ?? false), source: yml.git?.autoHook !== undefined ? 'memorix.yml' : 'default' });
696
- values.push({ key: 'git.skipMergeCommits', value: String(yml.git?.skipMergeCommits ?? true), source: yml.git?.skipMergeCommits !== undefined ? 'memorix.yml' : 'default' });
718
+ if (resolved.rerank.provider !== 'off') {
719
+ values.push({ key: 'rerank.provider', value: resolved.rerank.provider, source: source('MEMORIX_RERANK_PROVIDER') });
720
+ if (resolved.rerank.model) values.push({ key: 'rerank.model', value: resolved.rerank.model, source: source('MEMORIX_RERANK_MODEL') });
721
+ }
697
722
 
698
- // Behavior
699
- if (yml.behavior?.formationMode) values.push({ key: 'behavior.formationMode', value: yml.behavior.formationMode, source: 'memorix.yml' });
700
- if (yml.behavior?.sessionInject) values.push({ key: 'behavior.sessionInject', value: yml.behavior.sessionInject, source: 'memorix.yml' });
723
+ // Git
724
+ values.push({ key: 'git.autoHook', value: String(resolved.git.autoHook ?? false), source: configSource });
725
+ values.push({ key: 'git.skipMergeCommits', value: String(resolved.git.skipMergeCommits ?? true), source: configSource });
701
726
 
702
727
  // Server
703
- values.push({ key: 'server.transport', value: yml.server?.transport || 'stdio', source: yml.server?.transport ? 'memorix.yml' : 'default' });
704
- values.push({ key: 'server.dashboard', value: String(yml.server?.dashboard ?? true), source: yml.server?.dashboard !== undefined ? 'memorix.yml' : 'default' });
728
+ values.push({ key: 'server.transport', value: resolved.server.transport || 'stdio', source: configSource });
729
+ values.push({ key: 'server.dashboard', value: String(resolved.server.dashboard ?? true), source: configSource });
705
730
 
706
731
  sendJson(res, { files, values });
707
732
  break;