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.
- package/CHANGELOG.md +41 -0
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/cli/index.js +1488 -270
- package/dist/dashboard/static/app.js +125 -27
- package/dist/dashboard/static/index.html +11 -1
- package/dist/dashboard/static/style.css +86 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +927 -240
- package/dist/maintenance-runner.js +607 -68
- package/dist/media-audio-runner.js +602 -63
- package/dist/media-video-runner.js +607 -68
- package/dist/memcode-runtime/CHANGELOG.md +41 -0
- package/dist/sdk.js +926 -239
- package/dist/types.d.ts +8 -0
- package/dist/vector-backfill-runner.js +607 -68
- package/docs/1.2.4-PERSISTENT-MEMORY-DELIVERY.md +1 -1
- package/docs/1.8.0-RELEASE-CANDIDATE-SPEC.md +75 -0
- package/docs/API_REFERENCE.md +39 -1
- package/docs/ARCHITECTURE.md +3 -1
- package/docs/DASHBOARD.md +16 -0
- package/docs/DEVELOPMENT.md +3 -2
- package/docs/INTEGRATIONS.md +1 -1
- package/docs/README.md +15 -2
- package/package.json +1 -1
- package/plugins/antigravity/memorix/rules/memorix.md +1 -1
- package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
- package/plugins/claude/memorix/SKILL.md +1 -1
- package/plugins/claude/memorix/skills/memorix-memory/SKILL.md +2 -2
- package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
- package/plugins/codebuddy/memorix/skills/memorix-memory/SKILL.md +2 -2
- package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/codex/memorix/skills/memorix-memory/SKILL.md +2 -2
- package/plugins/copilot/memorix/plugin.json +1 -1
- package/plugins/copilot/memorix/skills/memorix-memory/SKILL.md +2 -2
- package/plugins/cursor/memorix/skills/memorix-memory/SKILL.md +2 -2
- package/plugins/gemini/memorix/GEMINI.md +1 -1
- package/plugins/gemini/memorix/gemini-extension.json +1 -1
- package/plugins/omp/memorix/package.json +1 -1
- package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/pi/memorix/package.json +1 -1
- package/plugins/pi/memorix/skills/memorix-memory/SKILL.md +2 -2
- package/server.json +2 -2
- package/src/cli/capability-map.ts +2 -0
- package/src/cli/commands/context.ts +11 -0
- package/src/cli/commands/dashboard.ts +1 -1
- package/src/cli/commands/evidence.ts +56 -0
- package/src/cli/commands/feedback.ts +58 -0
- package/src/cli/commands/resume.ts +2 -0
- package/src/cli/commands/serve-http.ts +229 -13
- package/src/cli/fallback-budget.ts +59 -0
- package/src/cli/index.ts +5 -1
- package/src/compact/engine.ts +26 -1
- package/src/config/dotenv-loader.ts +17 -0
- package/src/dashboard/config-provenance.ts +17 -0
- package/src/dashboard/evidence.ts +63 -0
- package/src/dashboard/server.ts +178 -154
- package/src/dashboard/static/index.html +11 -1
- package/src/dashboard/static/style.css +86 -0
- package/src/hooks/installers/index.ts +1 -1
- package/src/hooks/official-skills.ts +3 -3
- package/src/hooks/rules/memorix-agent-rules.md +1 -1
- package/src/memory/feedback-model.ts +4 -0
- package/src/memory/feedback.ts +220 -0
- package/src/memory/observations.ts +19 -0
- package/src/memory/retention.ts +38 -5
- package/src/server/mcp-binding-store.ts +79 -0
- package/src/server/mcp-diagnostics.ts +40 -0
- package/src/server/tool-profile.ts +4 -0
- package/src/server.ts +125 -5
- package/src/store/evidence-store.ts +261 -0
- package/src/store/sqlite-db.ts +104 -0
- 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
|
+
}
|
package/src/dashboard/server.ts
CHANGED
|
@@ -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, {
|
|
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, {
|
|
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
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
662
|
-
|
|
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
|
-
|
|
671
|
-
|
|
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 =
|
|
696
|
+
const agentKey = resolved.agent.apiKey;
|
|
680
697
|
if (agentKey) {
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
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({
|
|
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
|
-
|
|
692
|
-
values.push({ key: 'embedding.
|
|
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
|
-
|
|
695
|
-
|
|
696
|
-
|
|
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
|
-
//
|
|
699
|
-
|
|
700
|
-
|
|
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:
|
|
704
|
-
values.push({ key: 'server.dashboard', value: String(
|
|
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'
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
1054
|
-
|
|
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
|
|
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
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
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
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
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 =
|
|
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);
|