memorix 1.7.3 → 1.8.1

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 (73) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +1 -1
  3. package/README.zh-CN.md +1 -1
  4. package/dist/cli/index.js +1488 -270
  5. package/dist/dashboard/static/app.js +125 -27
  6. package/dist/dashboard/static/index.html +11 -1
  7. package/dist/dashboard/static/style.css +86 -0
  8. package/dist/index.d.ts +2 -2
  9. package/dist/index.js +927 -240
  10. package/dist/maintenance-runner.js +607 -68
  11. package/dist/media-audio-runner.js +602 -63
  12. package/dist/media-video-runner.js +607 -68
  13. package/dist/memcode-runtime/CHANGELOG.md +41 -0
  14. package/dist/sdk.js +926 -239
  15. package/dist/types.d.ts +8 -0
  16. package/dist/vector-backfill-runner.js +607 -68
  17. package/docs/1.2.4-PERSISTENT-MEMORY-DELIVERY.md +1 -1
  18. package/docs/1.8.0-RELEASE-CANDIDATE-SPEC.md +75 -0
  19. package/docs/API_REFERENCE.md +39 -1
  20. package/docs/ARCHITECTURE.md +3 -1
  21. package/docs/DASHBOARD.md +16 -0
  22. package/docs/DEVELOPMENT.md +3 -2
  23. package/docs/INTEGRATIONS.md +1 -1
  24. package/docs/README.md +15 -2
  25. package/package.json +1 -1
  26. package/plugins/antigravity/memorix/rules/memorix.md +1 -1
  27. package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
  28. package/plugins/claude/memorix/SKILL.md +1 -1
  29. package/plugins/claude/memorix/skills/memorix-memory/SKILL.md +2 -2
  30. package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
  31. package/plugins/codebuddy/memorix/skills/memorix-memory/SKILL.md +2 -2
  32. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  33. package/plugins/codex/memorix/skills/memorix-memory/SKILL.md +2 -2
  34. package/plugins/copilot/memorix/plugin.json +1 -1
  35. package/plugins/copilot/memorix/skills/memorix-memory/SKILL.md +2 -2
  36. package/plugins/cursor/memorix/skills/memorix-memory/SKILL.md +2 -2
  37. package/plugins/gemini/memorix/GEMINI.md +1 -1
  38. package/plugins/gemini/memorix/gemini-extension.json +1 -1
  39. package/plugins/omp/memorix/package.json +1 -1
  40. package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
  41. package/plugins/pi/memorix/package.json +1 -1
  42. package/plugins/pi/memorix/skills/memorix-memory/SKILL.md +2 -2
  43. package/server.json +2 -2
  44. package/src/cli/capability-map.ts +2 -0
  45. package/src/cli/commands/context.ts +11 -0
  46. package/src/cli/commands/dashboard.ts +1 -1
  47. package/src/cli/commands/evidence.ts +56 -0
  48. package/src/cli/commands/feedback.ts +58 -0
  49. package/src/cli/commands/resume.ts +2 -0
  50. package/src/cli/commands/serve-http.ts +229 -13
  51. package/src/cli/fallback-budget.ts +59 -0
  52. package/src/cli/index.ts +5 -1
  53. package/src/compact/engine.ts +26 -1
  54. package/src/config/dotenv-loader.ts +17 -0
  55. package/src/dashboard/config-provenance.ts +17 -0
  56. package/src/dashboard/evidence.ts +63 -0
  57. package/src/dashboard/server.ts +178 -154
  58. package/src/dashboard/static/index.html +11 -1
  59. package/src/dashboard/static/style.css +86 -0
  60. package/src/hooks/installers/index.ts +1 -1
  61. package/src/hooks/official-skills.ts +3 -3
  62. package/src/hooks/rules/memorix-agent-rules.md +1 -1
  63. package/src/memory/feedback-model.ts +4 -0
  64. package/src/memory/feedback.ts +220 -0
  65. package/src/memory/observations.ts +19 -0
  66. package/src/memory/retention.ts +38 -5
  67. package/src/server/mcp-binding-store.ts +79 -0
  68. package/src/server/mcp-diagnostics.ts +40 -0
  69. package/src/server/tool-profile.ts +4 -0
  70. package/src/server.ts +125 -5
  71. package/src/store/evidence-store.ts +261 -0
  72. package/src/store/sqlite-db.ts +104 -0
  73. package/src/types.ts +8 -0
@@ -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.
@@ -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;
@@ -933,18 +958,10 @@ interface DashboardState {
933
958
  dataDir: string;
934
959
  projectRoot: string | null;
935
960
  projectResolved: boolean;
936
- mode: 'standalone' | 'control-plane';
961
+ mode: 'standalone';
937
962
  port: number;
938
963
  }
939
964
 
940
- /** Optional coordination instances passed from MCP server */
941
- export interface TeamInstances {
942
- registry: { listAgents: (filter?: any) => any[]; getActiveCount: () => number; getAgent: (id: string) => any };
943
- fileLocks: { listLocks: (agentId?: string) => any[]; cleanExpired: () => void };
944
- taskManager: { list: (filter?: any) => any[]; getAvailable: () => any[] };
945
- messageBus: { getUnreadCount: (agentId: string) => number };
946
- }
947
-
948
965
  function parseJsonField(value: unknown, fallback: unknown): unknown {
949
966
  if (typeof value !== 'string') return value ?? fallback;
950
967
  try {
@@ -1002,7 +1019,72 @@ function normalizeDashboardTask(task: any) {
1002
1019
  };
1003
1020
  }
1004
1021
 
1005
- async function buildTeamSnapshot(dataDir: string, projectId: string, scope: string, mode: DashboardState['mode']) {
1022
+ type TeamSnapshotStatus = 'ok' | 'degraded' | 'error';
1023
+
1024
+ interface TeamSnapshot {
1025
+ status: TeamSnapshotStatus;
1026
+ mode: DashboardState['mode'];
1027
+ readOnly: true;
1028
+ scope: string;
1029
+ agents: any[];
1030
+ activeCount: number;
1031
+ recentCount: number;
1032
+ historicalCount: number;
1033
+ totalAgents: number;
1034
+ recentWindowDays: number;
1035
+ locks: any[];
1036
+ tasks: any[];
1037
+ availableTasks: number;
1038
+ sessions: number;
1039
+ roles: any[];
1040
+ roleOccupancy: any[];
1041
+ handoffs: any[];
1042
+ openTasks: number;
1043
+ openHandoffs: number;
1044
+ totalUnread: number;
1045
+ activeSessions: number;
1046
+ error?: string;
1047
+ errorCode?: string;
1048
+ }
1049
+
1050
+ function emptyTeamSnapshot(
1051
+ mode: DashboardState['mode'],
1052
+ scope: string,
1053
+ status: Exclude<TeamSnapshotStatus, 'ok'>,
1054
+ ): TeamSnapshot {
1055
+ return {
1056
+ status,
1057
+ mode,
1058
+ readOnly: true,
1059
+ scope,
1060
+ agents: [],
1061
+ activeCount: 0,
1062
+ recentCount: 0,
1063
+ historicalCount: 0,
1064
+ totalAgents: 0,
1065
+ recentWindowDays: 7,
1066
+ locks: [],
1067
+ tasks: [],
1068
+ availableTasks: 0,
1069
+ sessions: 0,
1070
+ roles: [],
1071
+ roleOccupancy: [],
1072
+ handoffs: [],
1073
+ openTasks: 0,
1074
+ openHandoffs: 0,
1075
+ totalUnread: 0,
1076
+ activeSessions: 0,
1077
+ error: status === 'error' ? 'Coordination state unavailable' : 'Coordination state is incomplete',
1078
+ errorCode: status === 'error' ? 'COORDINATION_STATE_UNAVAILABLE' : 'COORDINATION_STATE_DEGRADED',
1079
+ };
1080
+ }
1081
+
1082
+ function logTeamSnapshotFailure(scope: string, status: Exclude<TeamSnapshotStatus, 'ok'>): void {
1083
+ const safeScope = scope === 'global' ? 'global' : 'project';
1084
+ console.error('[dashboard] coordination snapshot unavailable', { status, scope: safeScope });
1085
+ }
1086
+
1087
+ async function buildTeamSnapshot(dataDir: string, projectId: string, scope: string, mode: DashboardState['mode']): Promise<TeamSnapshot> {
1006
1088
  try {
1007
1089
  const { initTeamStore } = await import('../team/team-store.js');
1008
1090
  const teamStore = await initTeamStore(dataDir);
@@ -1028,8 +1110,9 @@ async function buildTeamSnapshot(dataDir: string, projectId: string, scope: stri
1028
1110
  const roleOccupancy = effectiveProjectId ? teamStore.getRoleOccupancy(effectiveProjectId) : [];
1029
1111
  const handoffs = effectiveProjectId ? teamStore.listHandoffs(effectiveProjectId) : [];
1030
1112
  return {
1113
+ status: 'ok',
1031
1114
  mode,
1032
- readOnly: mode === 'standalone',
1115
+ readOnly: true,
1033
1116
  scope,
1034
1117
  agents: withTier,
1035
1118
  activeCount,
@@ -1050,28 +1133,8 @@ async function buildTeamSnapshot(dataDir: string, projectId: string, scope: stri
1050
1133
  activeSessions: activeCount,
1051
1134
  };
1052
1135
  } catch {
1053
- return {
1054
- mode,
1055
- readOnly: mode === 'standalone',
1056
- scope,
1057
- agents: [],
1058
- activeCount: 0,
1059
- recentCount: 0,
1060
- historicalCount: 0,
1061
- totalAgents: 0,
1062
- recentWindowDays: 7,
1063
- locks: [],
1064
- tasks: [],
1065
- availableTasks: 0,
1066
- sessions: 0,
1067
- roles: [],
1068
- roleOccupancy: [],
1069
- handoffs: [],
1070
- openTasks: 0,
1071
- openHandoffs: 0,
1072
- totalUnread: 0,
1073
- activeSessions: 0,
1074
- };
1136
+ logTeamSnapshotFailure(scope, 'error');
1137
+ return emptyTeamSnapshot(mode, scope, 'error');
1075
1138
  }
1076
1139
  }
1077
1140
 
@@ -1100,7 +1163,6 @@ export async function startDashboard(
1100
1163
  projectId: string,
1101
1164
  projectName: string,
1102
1165
  autoOpen = true,
1103
- teamInstances?: TeamInstances,
1104
1166
  projectRoot: string | null = null,
1105
1167
  projectResolved = true,
1106
1168
  ): Promise<void> {
@@ -1111,8 +1173,7 @@ export async function startDashboard(
1111
1173
  const baseDir = getBaseDataDir();
1112
1174
 
1113
1175
  // Mutable state — can be updated via /api/set-current-project
1114
- const isControlPlane = !!teamInstances;
1115
- const state: DashboardState = { projectId, projectName, dataDir, projectRoot, projectResolved, mode: isControlPlane ? 'control-plane' : 'standalone', port };
1176
+ const state: DashboardState = { projectId, projectName, dataDir, projectRoot, projectResolved, mode: 'standalone', port };
1116
1177
 
1117
1178
  const server = createServer(async (req, res) => {
1118
1179
  const url = req.url || '/';
@@ -1139,56 +1200,20 @@ export async function startDashboard(
1139
1200
  return;
1140
1201
  }
1141
1202
 
1142
- if (url.startsWith('/api/team')) {
1143
- if (!teamInstances) {
1144
- const parsedUrl = new URL(url, `http://127.0.0.1:${port}`);
1145
- const scope = parsedUrl.searchParams.get('scope') || 'project';
1146
- sendJson(res, await buildTeamSnapshot(state.dataDir, state.projectId, scope, state.mode));
1147
- return;
1148
- }
1149
- try {
1150
- teamInstances.fileLocks.cleanExpired();
1151
- const agents = teamInstances.registry.listAgents();
1152
- const locks = teamInstances.fileLocks.listLocks();
1153
- const tasks = teamInstances.taskManager.list();
1154
- const available = teamInstances.taskManager.getAvailable();
1155
-
1156
- // Role occupancy and handoffs from TeamStore (if available)
1157
- let roles: any[] = [];
1158
- let roleOccupancy: any[] = [];
1159
- let handoffs: any[] = [];
1160
- try {
1161
- const { getTeamStore, isTeamStoreInitialized } = await import('../team/team-store.js');
1162
- if (isTeamStoreInitialized()) {
1163
- const teamStore = getTeamStore();
1164
- const projectId = state.projectId;
1165
- roles = teamStore.listRoles(projectId);
1166
- roleOccupancy = teamStore.getRoleOccupancy(projectId);
1167
- handoffs = teamStore.listHandoffs(projectId);
1168
- }
1169
- } catch { /* team store not available in standalone mode */ }
1170
-
1203
+ const parsedUrl = new URL(url, `http://127.0.0.1:${port}`);
1204
+ if (parsedUrl.pathname === '/api/team') {
1205
+ if (req.method !== 'GET') {
1171
1206
  sendJson(res, {
1172
- agents: agents.map((a: any) => ({
1173
- ...a,
1174
- unread: teamInstances!.messageBus.getUnreadCount(a.id),
1175
- })),
1176
- activeCount: teamInstances.registry.getActiveCount(),
1177
- locks,
1178
- tasks,
1179
- availableTasks: available.length,
1180
- roles,
1181
- roleOccupancy,
1182
- handoffs,
1183
- // Resume data for "Continue this project" area
1184
- openTasks: tasks.filter((t: any) => t.status === 'pending' || t.status === 'in_progress').length,
1185
- openHandoffs: handoffs.filter((h: any) => h.handoff_status === 'open' || h.handoffStatus === 'open').length,
1186
- totalUnread: agents.reduce((sum: number, a: any) => sum + teamInstances!.messageBus.getUnreadCount(a.id), 0),
1187
- activeSessions: agents.filter((a: any) => a.status === 'active').length,
1188
- });
1189
- } catch {
1190
- sendJson(res, { agents: [], activeCount: 0, locks: [], tasks: [], availableTasks: 0, roles: [], roleOccupancy: [], handoffs: [] });
1207
+ status: 'error',
1208
+ readOnly: true,
1209
+ error: 'Coordination status is read-only',
1210
+ errorCode: 'COORDINATION_STATUS_READ_ONLY',
1211
+ }, 405);
1212
+ return;
1191
1213
  }
1214
+ const scope = parsedUrl.searchParams.get('scope') || 'project';
1215
+ const snapshot = await buildTeamSnapshot(state.dataDir, state.projectId, scope, state.mode);
1216
+ sendJson(res, snapshot, snapshot.status === 'error' ? 503 : 200);
1192
1217
  return;
1193
1218
  }
1194
1219
 
@@ -1212,13 +1237,12 @@ export async function startDashboard(
1212
1237
  server.listen(port, '127.0.0.1', () => {
1213
1238
  const url = `http://127.0.0.1:${port}`;
1214
1239
  const resolvedLabel = projectResolved ? 'resolved' : 'unresolved';
1215
- const modeLabel = isControlPlane ? 'Control Plane' : 'Standalone';
1240
+ const modeLabel = 'Standalone';
1216
1241
  console.error(` Memorix Dashboard [${modeLabel}]`);
1217
1242
  console.error(` ───────────────────────`);
1218
1243
  console.error(` Mode: ${modeLabel}`);
1219
1244
  console.error(` Project: ${projectName} (${projectId}) [${resolvedLabel}]`);
1220
1245
  console.error(` Local: ${url}`);
1221
- if (isControlPlane) console.error(` MCP: ${url}/mcp`);
1222
1246
  console.error(` Data dir: ${dataDir}`);
1223
1247
  console.error(`\n Press Ctrl+C to stop\n`);
1224
1248
  if (autoOpen) openBrowser(url);