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.
- package/CHANGELOG.md +59 -1
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/assets/star-history-dark.svg +4 -4
- package/assets/star-history-light.svg +4 -4
- package/assets/star-history.json +47 -3
- package/dist/cli/index.js +1801 -254
- package/dist/dashboard/static/app.js +88 -4
- package/dist/dashboard/static/index.html +10 -0
- package/dist/dashboard/static/style.css +86 -0
- package/dist/index.js +1241 -224
- package/dist/maintenance-runner.js +972 -126
- package/dist/media-audio-runner.js +967 -121
- package/dist/media-video-runner.js +972 -126
- package/dist/memcode-runtime/CHANGELOG.md +59 -1
- package/dist/sdk.js +1240 -223
- package/dist/types.d.ts +8 -0
- package/dist/vector-backfill-runner.js +972 -126
- 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 +37 -1
- package/docs/ARCHITECTURE.md +1 -1
- package/docs/CONFIGURATION.md +57 -1
- package/docs/DASHBOARD.md +10 -0
- package/docs/DEVELOPMENT.md +3 -2
- package/docs/INTEGRATIONS.md +1 -1
- package/docs/PERFORMANCE.md +3 -1
- package/docs/README.md +11 -1
- package/docs/SETUP.md +2 -1
- 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/config-migrate.ts +7 -0
- package/src/cli/commands/context.ts +11 -0
- package/src/cli/commands/doctor.ts +5 -1
- package/src/cli/commands/evidence.ts +56 -0
- package/src/cli/commands/feedback.ts +58 -0
- package/src/cli/commands/init.ts +17 -0
- package/src/cli/commands/resume.ts +2 -0
- package/src/cli/commands/serve-http.ts +201 -12
- package/src/cli/commands/status.ts +2 -0
- 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/config/resolved-config.ts +110 -12
- package/src/config/toml-loader.ts +8 -0
- package/src/config/yaml-loader.ts +9 -0
- package/src/dashboard/config-provenance.ts +17 -0
- package/src/dashboard/evidence.ts +63 -0
- package/src/dashboard/server.ts +93 -68
- package/src/dashboard/static/index.html +10 -0
- 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/llm/provider.ts +33 -7
- 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/multimodal/image-loader.ts +6 -3
- package/src/rerank/http-provider.ts +125 -0
- package/src/rerank/index.ts +81 -0
- package/src/rerank/policy.ts +88 -0
- 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 +121 -1
- package/src/store/evidence-store.ts +261 -0
- package/src/store/orama-store.ts +79 -53
- package/src/store/sqlite-db.ts +104 -0
- package/src/types.ts +8 -0
package/src/compact/engine.ts
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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
|
+
}
|
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;
|