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
|
@@ -6,7 +6,11 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { getLLMApiKey, getLLMBaseUrl, getLLMModel } from '../config.js';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
isLLMEnabled,
|
|
11
|
+
getLLMConfig,
|
|
12
|
+
normalizeOpenAICompatibleBaseUrl,
|
|
13
|
+
} from '../llm/provider.js';
|
|
10
14
|
import { sanitizeCredentials } from '../memory/secret-filter.js';
|
|
11
15
|
import {
|
|
12
16
|
decodeBase64ImagePayload,
|
|
@@ -55,8 +59,7 @@ async function callVisionLLM(
|
|
|
55
59
|
throw new Error('No LLM API key configured for image analysis.');
|
|
56
60
|
}
|
|
57
61
|
|
|
58
|
-
|
|
59
|
-
if (!baseUrl.endsWith('/v1')) baseUrl += '/v1';
|
|
62
|
+
const baseUrl = normalizeOpenAICompatibleBaseUrl(getLLMBaseUrl('https://api.openai.com/v1'));
|
|
60
63
|
const model = getLLMModel('gpt-4o');
|
|
61
64
|
|
|
62
65
|
const response = await fetch(`${baseUrl}/chat/completions`, {
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional HTTP rerank client.
|
|
3
|
+
*
|
|
4
|
+
* Posts a Cohere-compatible `{model, query, documents}` body to
|
|
5
|
+
* `{base_url}/rerank` (or an explicit `/rerank` path). Compatible with
|
|
6
|
+
* any `/rerank` endpoint that accepts that body. The model id is
|
|
7
|
+
* whatever the user configured — Memorix does not rewrite it.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { resolveRerankTimeoutMs } from './policy.js';
|
|
11
|
+
|
|
12
|
+
export type NeuralRerankProviderName = 'http';
|
|
13
|
+
|
|
14
|
+
export interface NeuralRerankRequestConfig {
|
|
15
|
+
provider: NeuralRerankProviderName;
|
|
16
|
+
model?: string;
|
|
17
|
+
baseUrl: string;
|
|
18
|
+
apiKey?: string;
|
|
19
|
+
timeoutMs?: number;
|
|
20
|
+
fetchImpl?: typeof fetch;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface RerankedDocument {
|
|
24
|
+
index: number;
|
|
25
|
+
score: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Join a provider base URL with `/rerank`, leaving an explicit path intact.
|
|
30
|
+
*/
|
|
31
|
+
export function buildRerankUrl(baseUrl: string): string {
|
|
32
|
+
const trimmed = baseUrl.trim().replace(/\/+$/, '');
|
|
33
|
+
if (trimmed.endsWith('/rerank')) return trimmed;
|
|
34
|
+
return `${trimmed}/rerank`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Accept Cohere-compatible `{results:[{index,relevance_score}]}` and TEI `[{index,score}]`.
|
|
39
|
+
*/
|
|
40
|
+
export function parseRerankResponse(payload: unknown): RerankedDocument[] {
|
|
41
|
+
const rows = extractRows(payload);
|
|
42
|
+
if (rows.length === 0) {
|
|
43
|
+
throw new Error('Neural rerank response contained no ranked results');
|
|
44
|
+
}
|
|
45
|
+
return rows.sort((a, b) => b.score - a.score);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function extractRows(payload: unknown): RerankedDocument[] {
|
|
49
|
+
if (Array.isArray(payload)) {
|
|
50
|
+
return payload.map(rowFromUnknown).filter((row): row is RerankedDocument => row !== null);
|
|
51
|
+
}
|
|
52
|
+
if (payload && typeof payload === 'object' && 'results' in payload) {
|
|
53
|
+
const results = (payload as { results: unknown }).results;
|
|
54
|
+
if (Array.isArray(results)) {
|
|
55
|
+
return results.map(rowFromUnknown).filter((row): row is RerankedDocument => row !== null);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
throw new Error('Neural rerank response was not Cohere or TEI shaped');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function rowFromUnknown(value: unknown): RerankedDocument | null {
|
|
62
|
+
if (!value || typeof value !== 'object') return null;
|
|
63
|
+
const row = value as { index?: unknown; relevance_score?: unknown; score?: unknown };
|
|
64
|
+
if (typeof row.index !== 'number' || !Number.isFinite(row.index)) return null;
|
|
65
|
+
const score = typeof row.relevance_score === 'number'
|
|
66
|
+
? row.relevance_score
|
|
67
|
+
: typeof row.score === 'number'
|
|
68
|
+
? row.score
|
|
69
|
+
: null;
|
|
70
|
+
if (score === null || !Number.isFinite(score)) return null;
|
|
71
|
+
return { index: row.index, score };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function buildRerankBody(query: string, documents: string[], model?: string): Record<string, unknown> {
|
|
75
|
+
const body: Record<string, unknown> = {
|
|
76
|
+
query,
|
|
77
|
+
documents,
|
|
78
|
+
top_n: documents.length,
|
|
79
|
+
return_documents: false,
|
|
80
|
+
};
|
|
81
|
+
const trimmed = model?.trim();
|
|
82
|
+
if (trimmed) body.model = trimmed;
|
|
83
|
+
return body;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* POST query + documents to a compatible `/rerank` endpoint and return ranked indexes.
|
|
88
|
+
*/
|
|
89
|
+
export async function rerankViaHttp(
|
|
90
|
+
query: string,
|
|
91
|
+
documents: string[],
|
|
92
|
+
config: NeuralRerankRequestConfig,
|
|
93
|
+
): Promise<RerankedDocument[]> {
|
|
94
|
+
if (documents.length === 0) return [];
|
|
95
|
+
|
|
96
|
+
const url = buildRerankUrl(config.baseUrl);
|
|
97
|
+
const headers: Record<string, string> = {
|
|
98
|
+
'content-type': 'application/json',
|
|
99
|
+
};
|
|
100
|
+
if (config.apiKey) {
|
|
101
|
+
headers.authorization = `Bearer ${config.apiKey}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const controller = new AbortController();
|
|
105
|
+
const timeoutMs = resolveRerankTimeoutMs(config.timeoutMs);
|
|
106
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const fetchImpl = config.fetchImpl ?? fetch;
|
|
110
|
+
const response = await fetchImpl(url, {
|
|
111
|
+
method: 'POST',
|
|
112
|
+
headers,
|
|
113
|
+
signal: controller.signal,
|
|
114
|
+
body: JSON.stringify(buildRerankBody(query, documents, config.model)),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
if (!response.ok) {
|
|
118
|
+
throw new Error(`Neural rerank HTTP ${response.status} from ${url}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return parseRerankResponse(await response.json());
|
|
122
|
+
} finally {
|
|
123
|
+
clearTimeout(timer);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional HTTP rerank lane. LLM rerank stays in quality.ts when HTTP is off.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getResolvedRerankLane, type ResolvedLaneOptions } from '../config/resolved-config.js';
|
|
6
|
+
import { rerankViaHttp } from './http-provider.js';
|
|
7
|
+
import { resolveRerankTimeoutMs } from './policy.js';
|
|
8
|
+
|
|
9
|
+
export {
|
|
10
|
+
DEFAULT_RERANK_TIMEOUT_MS,
|
|
11
|
+
MIN_RERANK_CANDIDATES,
|
|
12
|
+
parseRerankTimeoutMs,
|
|
13
|
+
resolveRerankTimeoutMs,
|
|
14
|
+
shouldAttemptHttpRerank,
|
|
15
|
+
shouldAttemptLlmRerank,
|
|
16
|
+
} from './policy.js';
|
|
17
|
+
|
|
18
|
+
/** Minimal search hit the HTTP reranker can reorder. */
|
|
19
|
+
export interface NeuralRerankCandidate {
|
|
20
|
+
id: string;
|
|
21
|
+
title: string;
|
|
22
|
+
type: string;
|
|
23
|
+
score: number;
|
|
24
|
+
narrative?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const MAX_RERANK = 10;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* True when an HTTP `/rerank` endpoint is configured.
|
|
31
|
+
*/
|
|
32
|
+
export function isNeuralRerankEnabled(options: ResolvedLaneOptions = {}): boolean {
|
|
33
|
+
const lane = getResolvedRerankLane(options);
|
|
34
|
+
return lane.provider === 'http' && Boolean(lane.baseUrl);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Reorder search candidates via the configured remote reranker.
|
|
39
|
+
* Returns null when the provider is off or the HTTP call cannot be used.
|
|
40
|
+
*/
|
|
41
|
+
export async function neuralRerankCandidates(
|
|
42
|
+
query: string,
|
|
43
|
+
candidates: NeuralRerankCandidate[],
|
|
44
|
+
options: ResolvedLaneOptions = {},
|
|
45
|
+
): Promise<NeuralRerankCandidate[] | null> {
|
|
46
|
+
if (!isNeuralRerankEnabled(options) || candidates.length === 0) return null;
|
|
47
|
+
|
|
48
|
+
const lane = getResolvedRerankLane(options);
|
|
49
|
+
if (!lane.canSendRequest || !lane.baseUrl) return null;
|
|
50
|
+
const toRerank = candidates.slice(0, MAX_RERANK);
|
|
51
|
+
const rest = candidates.slice(MAX_RERANK);
|
|
52
|
+
const documents = toRerank.map((candidate) => candidateText(candidate));
|
|
53
|
+
|
|
54
|
+
const ranked = await rerankViaHttp(query, documents, {
|
|
55
|
+
provider: 'http',
|
|
56
|
+
model: lane.model,
|
|
57
|
+
baseUrl: lane.baseUrl!,
|
|
58
|
+
apiKey: lane.apiKey,
|
|
59
|
+
timeoutMs: resolveRerankTimeoutMs(),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const seen = new Set<number>();
|
|
63
|
+
const reranked: NeuralRerankCandidate[] = [];
|
|
64
|
+
for (const row of ranked) {
|
|
65
|
+
const candidate = toRerank[row.index];
|
|
66
|
+
if (!candidate || seen.has(row.index)) continue;
|
|
67
|
+
reranked.push(candidate);
|
|
68
|
+
seen.add(row.index);
|
|
69
|
+
}
|
|
70
|
+
for (const [index, candidate] of toRerank.entries()) {
|
|
71
|
+
if (!seen.has(index)) reranked.push(candidate);
|
|
72
|
+
}
|
|
73
|
+
reranked.push(...rest);
|
|
74
|
+
return reranked;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function candidateText(candidate: NeuralRerankCandidate): string {
|
|
78
|
+
const narrative = candidate.narrative?.trim();
|
|
79
|
+
if (narrative) return `${candidate.title} — ${narrative.slice(0, 200)}`;
|
|
80
|
+
return candidate.title;
|
|
81
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Product rules for optional HTTP rerank and the existing LLM rerank path.
|
|
3
|
+
*
|
|
4
|
+
* HTTP rerank is opt-in. When it is configured, thorough search with enough
|
|
5
|
+
* candidates uses the HTTP lane only — a miss or timeout keeps the original
|
|
6
|
+
* order instead of falling through to LLM rerank.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const DEFAULT_RERANK_TIMEOUT_MS = 30_000;
|
|
10
|
+
export const RERANK_TIMEOUT_MIN_MS = 1_000;
|
|
11
|
+
export const RERANK_TIMEOUT_MAX_MS = 300_000;
|
|
12
|
+
export const MIN_RERANK_CANDIDATES = 3;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Parse and validate MEMORIX_RERANK_TIMEOUT_MS.
|
|
16
|
+
*
|
|
17
|
+
* @param raw - Environment value, or undefined when unset
|
|
18
|
+
* @returns Timeout in milliseconds (default 30000)
|
|
19
|
+
*/
|
|
20
|
+
export function parseRerankTimeoutMs(raw: string | undefined): number {
|
|
21
|
+
const value = raw?.trim();
|
|
22
|
+
if (!value) return DEFAULT_RERANK_TIMEOUT_MS;
|
|
23
|
+
|
|
24
|
+
const parsed = Number(value);
|
|
25
|
+
if (!Number.isInteger(parsed) || Number.isNaN(parsed)) {
|
|
26
|
+
console.warn(
|
|
27
|
+
`[memorix] MEMORIX_RERANK_TIMEOUT_MS="${raw}" is invalid (must be a positive integer between ${RERANK_TIMEOUT_MIN_MS}-${RERANK_TIMEOUT_MAX_MS}ms). Using default ${DEFAULT_RERANK_TIMEOUT_MS}ms.`,
|
|
28
|
+
);
|
|
29
|
+
return DEFAULT_RERANK_TIMEOUT_MS;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (parsed < RERANK_TIMEOUT_MIN_MS) return RERANK_TIMEOUT_MIN_MS;
|
|
33
|
+
if (parsed > RERANK_TIMEOUT_MAX_MS) return RERANK_TIMEOUT_MAX_MS;
|
|
34
|
+
return parsed;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Resolve the rerank timeout from an explicit override or the environment.
|
|
39
|
+
*
|
|
40
|
+
* @param explicitTimeoutMs - Caller-supplied timeout, when present
|
|
41
|
+
* @returns Timeout in milliseconds
|
|
42
|
+
*/
|
|
43
|
+
export function resolveRerankTimeoutMs(explicitTimeoutMs?: number): number {
|
|
44
|
+
if (typeof explicitTimeoutMs === 'number' && Number.isFinite(explicitTimeoutMs) && explicitTimeoutMs > 0) {
|
|
45
|
+
return explicitTimeoutMs;
|
|
46
|
+
}
|
|
47
|
+
return parseRerankTimeoutMs(process.env.MEMORIX_RERANK_TIMEOUT_MS);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* True when thorough search has enough candidates for HTTP rerank.
|
|
52
|
+
*
|
|
53
|
+
* @param input - Retrieval quality, query presence, and hit count
|
|
54
|
+
* @returns Whether the HTTP rerank lane should run
|
|
55
|
+
*/
|
|
56
|
+
export function shouldAttemptHttpRerank(input: {
|
|
57
|
+
quality?: string;
|
|
58
|
+
hasQuery: boolean;
|
|
59
|
+
candidateCount: number;
|
|
60
|
+
}): boolean {
|
|
61
|
+
return input.quality === 'thorough'
|
|
62
|
+
&& input.hasQuery
|
|
63
|
+
&& input.candidateCount >= MIN_RERANK_CANDIDATES;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* True when the existing LLM rerank path should run.
|
|
68
|
+
*
|
|
69
|
+
* LLM rerank stays on the original thorough + heavy + close top-2 gate.
|
|
70
|
+
* It never runs when an HTTP rerank lane is configured.
|
|
71
|
+
*
|
|
72
|
+
* @param input - Search gate inputs plus whether HTTP rerank is configured
|
|
73
|
+
* @returns Whether LLM rerank should run
|
|
74
|
+
*/
|
|
75
|
+
export function shouldAttemptLlmRerank(input: {
|
|
76
|
+
quality?: string;
|
|
77
|
+
tier: string;
|
|
78
|
+
hasQuery: boolean;
|
|
79
|
+
candidateCount: number;
|
|
80
|
+
topScore: number;
|
|
81
|
+
secondScore: number;
|
|
82
|
+
httpRerankConfigured: boolean;
|
|
83
|
+
}): boolean {
|
|
84
|
+
if (input.httpRerankConfigured) return false;
|
|
85
|
+
if (input.quality !== 'thorough' || input.tier !== 'heavy' || !input.hasQuery) return false;
|
|
86
|
+
if (input.candidateCount < MIN_RERANK_CANDIDATES) return false;
|
|
87
|
+
return input.topScore > 0 && input.secondScore / input.topScore > 0.7;
|
|
88
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { getDatabase } from '../store/sqlite-db.js';
|
|
3
|
+
|
|
4
|
+
export interface McpProjectBinding {
|
|
5
|
+
handleId: string;
|
|
6
|
+
projectId: string;
|
|
7
|
+
projectRoot: string;
|
|
8
|
+
dataDir: string;
|
|
9
|
+
createdAt: string;
|
|
10
|
+
lastUsedAt: string;
|
|
11
|
+
expiresAt?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function rowToBinding(row: any): McpProjectBinding {
|
|
15
|
+
return {
|
|
16
|
+
handleId: String(row.handle_id),
|
|
17
|
+
projectId: String(row.project_id),
|
|
18
|
+
projectRoot: String(row.project_root),
|
|
19
|
+
dataDir: String(row.data_dir),
|
|
20
|
+
createdAt: String(row.created_at),
|
|
21
|
+
lastUsedAt: String(row.last_used_at),
|
|
22
|
+
...(row.expires_at ? { expiresAt: String(row.expires_at) } : {}),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Durable project binding handles for stateless MCP requests. */
|
|
27
|
+
export class McpBindingStore {
|
|
28
|
+
private db: any = null;
|
|
29
|
+
|
|
30
|
+
async init(dataDir: string): Promise<void> {
|
|
31
|
+
this.db = getDatabase(dataDir);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private requireDb(): any {
|
|
35
|
+
if (!this.db) throw new Error('McpBindingStore is not initialized.');
|
|
36
|
+
return this.db;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
create(input: { projectId: string; projectRoot: string; dataDir: string; ttlMs?: number }): McpProjectBinding {
|
|
40
|
+
const now = new Date();
|
|
41
|
+
const binding: McpProjectBinding = {
|
|
42
|
+
handleId: `mxh_${randomUUID().replace(/-/g, '')}`,
|
|
43
|
+
projectId: input.projectId,
|
|
44
|
+
projectRoot: input.projectRoot,
|
|
45
|
+
dataDir: input.dataDir,
|
|
46
|
+
createdAt: now.toISOString(),
|
|
47
|
+
lastUsedAt: now.toISOString(),
|
|
48
|
+
...(input.ttlMs && input.ttlMs > 0 ? { expiresAt: new Date(now.getTime() + input.ttlMs).toISOString() } : {}),
|
|
49
|
+
};
|
|
50
|
+
this.requireDb().prepare(`
|
|
51
|
+
INSERT INTO mcp_bindings (handle_id, project_id, project_root, data_dir, created_at, last_used_at, expires_at)
|
|
52
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
53
|
+
`).run(binding.handleId, binding.projectId, binding.projectRoot, binding.dataDir, binding.createdAt, binding.lastUsedAt, binding.expiresAt ?? null);
|
|
54
|
+
return binding;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
get(handleId: string): McpProjectBinding | undefined {
|
|
58
|
+
const row = this.requireDb().prepare(`SELECT * FROM mcp_bindings WHERE handle_id = ?`).get(handleId);
|
|
59
|
+
if (!row) return undefined;
|
|
60
|
+
const binding = rowToBinding(row);
|
|
61
|
+
if (binding.expiresAt && Date.parse(binding.expiresAt) <= Date.now()) {
|
|
62
|
+
this.delete(handleId);
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
return binding;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
touch(handleId: string): McpProjectBinding | undefined {
|
|
69
|
+
const binding = this.get(handleId);
|
|
70
|
+
if (!binding) return undefined;
|
|
71
|
+
const lastUsedAt = new Date().toISOString();
|
|
72
|
+
this.requireDb().prepare(`UPDATE mcp_bindings SET last_used_at = ? WHERE handle_id = ?`).run(lastUsedAt, handleId);
|
|
73
|
+
return { ...binding, lastUsedAt };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
delete(handleId: string): void {
|
|
77
|
+
this.requireDb().prepare(`DELETE FROM mcp_bindings WHERE handle_id = ?`).run(handleId);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** MCP compatibility diagnostics shared by HTTP and CLI-facing checks. */
|
|
2
|
+
|
|
3
|
+
export const CURRENT_MCP_PROTOCOL_VERSION = '2026-07-28';
|
|
4
|
+
export const LEGACY_MCP_PROTOCOL_VERSION = '2024-11-05';
|
|
5
|
+
export const MCP_SERVER_NAME = 'io.github.AVIDS2/memorix';
|
|
6
|
+
|
|
7
|
+
export interface McpDiagnostics {
|
|
8
|
+
serverName: string;
|
|
9
|
+
protocol: {
|
|
10
|
+
current: string;
|
|
11
|
+
legacy: string;
|
|
12
|
+
statelessRequests: 'supported' | 'compatibility-only' | 'unsupported';
|
|
13
|
+
statefulStreamableHttp: 'supported';
|
|
14
|
+
};
|
|
15
|
+
capabilities: {
|
|
16
|
+
tasks: 'supported' | 'unsupported';
|
|
17
|
+
polling: 'supported' | 'unsupported';
|
|
18
|
+
listCache: 'supported';
|
|
19
|
+
explicitProjectHandle: 'supported';
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The pinned SDK is stateful; do not present a fake durable task loop. */
|
|
24
|
+
export function getMcpDiagnostics(): McpDiagnostics {
|
|
25
|
+
return {
|
|
26
|
+
serverName: MCP_SERVER_NAME,
|
|
27
|
+
protocol: {
|
|
28
|
+
current: CURRENT_MCP_PROTOCOL_VERSION,
|
|
29
|
+
legacy: LEGACY_MCP_PROTOCOL_VERSION,
|
|
30
|
+
statelessRequests: 'supported',
|
|
31
|
+
statefulStreamableHttp: 'supported',
|
|
32
|
+
},
|
|
33
|
+
capabilities: {
|
|
34
|
+
tasks: 'unsupported',
|
|
35
|
+
polling: 'unsupported',
|
|
36
|
+
listCache: 'supported',
|
|
37
|
+
explicitProjectHandle: 'supported',
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -40,6 +40,10 @@ export const TOOL_PROFILES: Record<string, ReadonlyArray<ToolProfile>> = Object.
|
|
|
40
40
|
// multimodal support to one additional schema rather than enabling lite.
|
|
41
41
|
memorix_session_start: ['micro', 'lite', 'team', 'full'],
|
|
42
42
|
memorix_media: ['micro', 'lite', 'team', 'full'],
|
|
43
|
+
// Evidence and feedback are deliberate operator/context-control surfaces;
|
|
44
|
+
// keep the default micro profile at its nine-tool token budget.
|
|
45
|
+
memorix_evidence: ['lite', 'team', 'full'],
|
|
46
|
+
memorix_feedback: ['lite', 'team', 'full'],
|
|
43
47
|
|
|
44
48
|
// ── lite: extended solo memory surface ────────────────────────────
|
|
45
49
|
memorix_graph_context: ['lite', 'team', 'full'],
|
package/src/server.ts
CHANGED
|
@@ -36,6 +36,8 @@ import { detectProject } from './project/detector.js';
|
|
|
36
36
|
import { homeProjectRootError, isHomeDirectory, writeLastProjectRoot } from './project/launch-root.js';
|
|
37
37
|
import { registerAlias, initAliasRegistry, resolveAliases, autoMergeByBaseName } from './project/aliases.js';
|
|
38
38
|
import { getProjectDataDir } from './store/persistence.js';
|
|
39
|
+
import { EvidenceCardStore } from './store/evidence-store.js';
|
|
40
|
+
import { MemoryFeedbackStore } from './memory/feedback.js';
|
|
39
41
|
import type { ObservationType, RuleSource, AgentTarget, MCPServerEntry, ObservationReader } from './types.js';
|
|
40
42
|
import { RulesSyncer } from './rules/syncer.js';
|
|
41
43
|
import { WorkspaceSyncEngine } from './workspace/engine.js';
|
|
@@ -4445,7 +4447,7 @@ export async function createMemorixServer(
|
|
|
4445
4447
|
process.platform === 'win32' ? `start "" "${url}"` :
|
|
4446
4448
|
process.platform === 'darwin' ? `open "${url}"` :
|
|
4447
4449
|
`xdg-open "${url}"`;
|
|
4448
|
-
execCmd(openCmd, () => { });
|
|
4450
|
+
execCmd(openCmd, { windowsHide: true }, () => { });
|
|
4449
4451
|
|
|
4450
4452
|
return {
|
|
4451
4453
|
content: [{
|
|
@@ -4474,6 +4476,124 @@ export async function createMemorixServer(
|
|
|
4474
4476
|
// Orchestration Coordination Tools (Multi-Agent) - SQLite-backed
|
|
4475
4477
|
// ================================================================
|
|
4476
4478
|
|
|
4479
|
+
server.registerTool(
|
|
4480
|
+
'memorix_evidence',
|
|
4481
|
+
{
|
|
4482
|
+
title: 'Evidence Cards',
|
|
4483
|
+
description:
|
|
4484
|
+
'Inspect the bounded, project-scoped provenance cards behind Memorix context. ' +
|
|
4485
|
+
'Use list for a compact index, get/events for one source, sync to repair the projection, ' +
|
|
4486
|
+
'and stale when known source files changed. Cards expose sourceRef, locator, capturedHash, and freshness.',
|
|
4487
|
+
inputSchema: {
|
|
4488
|
+
action: z.enum(['list', 'get', 'sync', 'stale', 'events']).describe('Evidence operation'),
|
|
4489
|
+
candidateKind: z.enum(['observation', 'code', 'git', 'test', 'document', 'workflow', 'graph']).optional().default('observation'),
|
|
4490
|
+
candidateId: z.string().optional().describe('Candidate id, usually an observation id'),
|
|
4491
|
+
query: z.string().optional().describe('Optional title/source query for list'),
|
|
4492
|
+
paths: z.array(z.string()).max(200).optional().describe('Changed source paths for stale'),
|
|
4493
|
+
reason: z.string().max(500).optional().describe('Why evidence became stale'),
|
|
4494
|
+
limit: z.number().int().positive().max(100).optional().default(20),
|
|
4495
|
+
},
|
|
4496
|
+
},
|
|
4497
|
+
async ({ action, candidateKind, candidateId, query, paths, reason, limit }) => {
|
|
4498
|
+
const unresolved = requireResolvedProject('inspect evidence cards for the current project');
|
|
4499
|
+
if (unresolved) return unresolved;
|
|
4500
|
+
const cards = new EvidenceCardStore();
|
|
4501
|
+
await cards.init(projectDir);
|
|
4502
|
+
if (action === 'sync') {
|
|
4503
|
+
const observations = await getObservationStore().loadByProject(project.id);
|
|
4504
|
+
const synced = cards.syncObservations(observations);
|
|
4505
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ projectId: project.id, synced: synced.length, cards: synced.slice(0, limit ?? 20) }, null, 2) }] };
|
|
4506
|
+
}
|
|
4507
|
+
if (action === 'stale') {
|
|
4508
|
+
const changed = cards.markStaleForPaths(project.id, paths ?? [], reason ?? 'source file changed');
|
|
4509
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ projectId: project.id, staleMarked: changed, paths: paths ?? [] }, null, 2) }] };
|
|
4510
|
+
}
|
|
4511
|
+
if (action === 'get') {
|
|
4512
|
+
if (!candidateId?.trim()) return { content: [{ type: 'text' as const, text: 'candidateId is required for evidence get.' }], isError: true };
|
|
4513
|
+
const card = cards.get(project.id, candidateKind ?? 'observation', candidateId.trim());
|
|
4514
|
+
if (!card) return { content: [{ type: 'text' as const, text: 'Evidence card not found.' }], isError: true };
|
|
4515
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ card, events: cards.listEvents(card.id), projectId: project.id }, null, 2) }] };
|
|
4516
|
+
}
|
|
4517
|
+
if (action === 'events') {
|
|
4518
|
+
if (!candidateId?.trim()) return { content: [{ type: 'text' as const, text: 'candidateId is required for evidence events.' }], isError: true };
|
|
4519
|
+
const card = cards.get(project.id, candidateKind ?? 'observation', candidateId.trim());
|
|
4520
|
+
if (!card) return { content: [{ type: 'text' as const, text: 'Evidence card not found.' }], isError: true };
|
|
4521
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(cards.listEvents(card.id, limit), null, 2) }] };
|
|
4522
|
+
}
|
|
4523
|
+
const needle = query?.trim().toLowerCase();
|
|
4524
|
+
const listed = cards.list(project.id, { limit: Math.max(limit ?? 20, 100) })
|
|
4525
|
+
.filter((card) => !needle || [card.title, card.summary, card.sourceRef, card.locator ?? ''].join(' ').toLowerCase().includes(needle))
|
|
4526
|
+
.slice(0, limit ?? 20);
|
|
4527
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ projectId: project.id, cards: listed }, null, 2) }] };
|
|
4528
|
+
},
|
|
4529
|
+
);
|
|
4530
|
+
|
|
4531
|
+
server.registerTool(
|
|
4532
|
+
'memorix_feedback',
|
|
4533
|
+
{
|
|
4534
|
+
title: 'Memory Feedback',
|
|
4535
|
+
description:
|
|
4536
|
+
'Record or inspect auditable memory outcomes. Positive verification/use strengthens recall; correction, conflict, source change, or weaken lowers it. ' +
|
|
4537
|
+
'Use revoke with targetEventId to undo one prior signal without deleting audit history.',
|
|
4538
|
+
inputSchema: {
|
|
4539
|
+
action: z.enum(['record', 'show', 'audit']).describe('Feedback operation'),
|
|
4540
|
+
candidateKind: z.enum(['observation', 'claim', 'durable-memory', 'workflow']).optional().default('observation'),
|
|
4541
|
+
candidateId: z.string().describe('Memory or knowledge candidate id'),
|
|
4542
|
+
signal: z.enum(['user-correction', 'verification-success', 'verification-failure', 'code-change', 'used', 'not-used', 'code-conflict', 'strengthen', 'weaken', 'revoke']).optional(),
|
|
4543
|
+
sourceRef: z.string().max(500).optional().describe('Source of this feedback, such as test:run-42 or user:chat'),
|
|
4544
|
+
actor: z.string().max(200).optional(),
|
|
4545
|
+
note: z.string().max(2_000).optional(),
|
|
4546
|
+
targetEventId: z.string().optional().describe('Existing event id when action=record and signal=revoke'),
|
|
4547
|
+
limit: z.number().int().positive().max(200).optional().default(100),
|
|
4548
|
+
},
|
|
4549
|
+
},
|
|
4550
|
+
async ({ action, candidateKind, candidateId, signal, sourceRef, actor, note, targetEventId, limit }) => {
|
|
4551
|
+
const unresolved = requireResolvedProject('record memory feedback for the current project');
|
|
4552
|
+
if (unresolved) return unresolved;
|
|
4553
|
+
const feedback = new MemoryFeedbackStore();
|
|
4554
|
+
await feedback.init(projectDir);
|
|
4555
|
+
const kind = candidateKind ?? 'observation';
|
|
4556
|
+
if (action === 'show') {
|
|
4557
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(feedback.getState(project.id, kind, candidateId), null, 2) }] };
|
|
4558
|
+
}
|
|
4559
|
+
if (action === 'audit') {
|
|
4560
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(feedback.audit(project.id, kind, candidateId, limit), null, 2) }] };
|
|
4561
|
+
}
|
|
4562
|
+
if (!signal || !sourceRef?.trim()) {
|
|
4563
|
+
return { content: [{ type: 'text' as const, text: 'record requires signal and sourceRef.' }], isError: true };
|
|
4564
|
+
}
|
|
4565
|
+
try {
|
|
4566
|
+
const result = feedback.record({
|
|
4567
|
+
projectId: project.id,
|
|
4568
|
+
candidateKind: kind,
|
|
4569
|
+
candidateId,
|
|
4570
|
+
signal,
|
|
4571
|
+
sourceRef,
|
|
4572
|
+
...(actor || currentAgentId ? { actor: actor ?? currentAgentId } : {}),
|
|
4573
|
+
...(note ? { note } : {}),
|
|
4574
|
+
...(targetEventId ? { targetEventId } : {}),
|
|
4575
|
+
});
|
|
4576
|
+
if (kind === 'observation') {
|
|
4577
|
+
const cards = new EvidenceCardStore();
|
|
4578
|
+
await cards.init(projectDir);
|
|
4579
|
+
const card = cards.get(project.id, 'observation', candidateId);
|
|
4580
|
+
if (card) {
|
|
4581
|
+
const verification = signal === 'verification-success' || signal === 'strengthen' ? 'verified' : signal === 'user-correction' || signal === 'code-conflict' || signal === 'weaken' ? 'conflicted' : card.verification;
|
|
4582
|
+
cards.recordEvent(project.id, card.id, `feedback:${signal}`, note);
|
|
4583
|
+
if (verification !== card.verification) {
|
|
4584
|
+
// Keep the card projection in sync with the feedback state while
|
|
4585
|
+
// retaining the append-only feedback audit as the source of truth.
|
|
4586
|
+
cards.setVerification(project.id, card.id, verification);
|
|
4587
|
+
}
|
|
4588
|
+
}
|
|
4589
|
+
}
|
|
4590
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] };
|
|
4591
|
+
} catch (error) {
|
|
4592
|
+
return { content: [{ type: 'text' as const, text: error instanceof Error ? error.message : String(error) }], isError: true };
|
|
4593
|
+
}
|
|
4594
|
+
},
|
|
4595
|
+
);
|
|
4596
|
+
|
|
4477
4597
|
// Use shared TeamStore (from HTTP server) or create new one (stdio mode).
|
|
4478
4598
|
// All team state is canonical in SQLite — no JSON persistence, no sync/flush.
|
|
4479
4599
|
if (teamFeaturesEnabled) {
|