memorix 1.7.2 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/CHANGELOG.md +59 -1
  2. package/README.md +1 -1
  3. package/README.zh-CN.md +1 -1
  4. package/assets/star-history-dark.svg +4 -4
  5. package/assets/star-history-light.svg +4 -4
  6. package/assets/star-history.json +47 -3
  7. package/dist/cli/index.js +1801 -254
  8. package/dist/dashboard/static/app.js +88 -4
  9. package/dist/dashboard/static/index.html +10 -0
  10. package/dist/dashboard/static/style.css +86 -0
  11. package/dist/index.js +1241 -224
  12. package/dist/maintenance-runner.js +972 -126
  13. package/dist/media-audio-runner.js +967 -121
  14. package/dist/media-video-runner.js +972 -126
  15. package/dist/memcode-runtime/CHANGELOG.md +59 -1
  16. package/dist/sdk.js +1240 -223
  17. package/dist/types.d.ts +8 -0
  18. package/dist/vector-backfill-runner.js +972 -126
  19. package/docs/1.2.4-PERSISTENT-MEMORY-DELIVERY.md +1 -1
  20. package/docs/1.8.0-RELEASE-CANDIDATE-SPEC.md +75 -0
  21. package/docs/API_REFERENCE.md +37 -1
  22. package/docs/ARCHITECTURE.md +1 -1
  23. package/docs/CONFIGURATION.md +57 -1
  24. package/docs/DASHBOARD.md +10 -0
  25. package/docs/DEVELOPMENT.md +3 -2
  26. package/docs/INTEGRATIONS.md +1 -1
  27. package/docs/PERFORMANCE.md +3 -1
  28. package/docs/README.md +11 -1
  29. package/docs/SETUP.md +2 -1
  30. package/package.json +1 -1
  31. package/plugins/antigravity/memorix/rules/memorix.md +1 -1
  32. package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
  33. package/plugins/claude/memorix/SKILL.md +1 -1
  34. package/plugins/claude/memorix/skills/memorix-memory/SKILL.md +2 -2
  35. package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
  36. package/plugins/codebuddy/memorix/skills/memorix-memory/SKILL.md +2 -2
  37. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  38. package/plugins/codex/memorix/skills/memorix-memory/SKILL.md +2 -2
  39. package/plugins/copilot/memorix/plugin.json +1 -1
  40. package/plugins/copilot/memorix/skills/memorix-memory/SKILL.md +2 -2
  41. package/plugins/cursor/memorix/skills/memorix-memory/SKILL.md +2 -2
  42. package/plugins/gemini/memorix/GEMINI.md +1 -1
  43. package/plugins/gemini/memorix/gemini-extension.json +1 -1
  44. package/plugins/omp/memorix/package.json +1 -1
  45. package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
  46. package/plugins/pi/memorix/package.json +1 -1
  47. package/plugins/pi/memorix/skills/memorix-memory/SKILL.md +2 -2
  48. package/server.json +2 -2
  49. package/src/cli/capability-map.ts +2 -0
  50. package/src/cli/commands/config-migrate.ts +7 -0
  51. package/src/cli/commands/context.ts +11 -0
  52. package/src/cli/commands/doctor.ts +5 -1
  53. package/src/cli/commands/evidence.ts +56 -0
  54. package/src/cli/commands/feedback.ts +58 -0
  55. package/src/cli/commands/init.ts +17 -0
  56. package/src/cli/commands/resume.ts +2 -0
  57. package/src/cli/commands/serve-http.ts +201 -12
  58. package/src/cli/commands/status.ts +2 -0
  59. package/src/cli/fallback-budget.ts +59 -0
  60. package/src/cli/index.ts +5 -1
  61. package/src/compact/engine.ts +26 -1
  62. package/src/config/dotenv-loader.ts +17 -0
  63. package/src/config/resolved-config.ts +110 -12
  64. package/src/config/toml-loader.ts +8 -0
  65. package/src/config/yaml-loader.ts +9 -0
  66. package/src/dashboard/config-provenance.ts +17 -0
  67. package/src/dashboard/evidence.ts +63 -0
  68. package/src/dashboard/server.ts +93 -68
  69. package/src/dashboard/static/index.html +10 -0
  70. package/src/dashboard/static/style.css +86 -0
  71. package/src/hooks/installers/index.ts +1 -1
  72. package/src/hooks/official-skills.ts +3 -3
  73. package/src/hooks/rules/memorix-agent-rules.md +1 -1
  74. package/src/llm/provider.ts +33 -7
  75. package/src/memory/feedback-model.ts +4 -0
  76. package/src/memory/feedback.ts +220 -0
  77. package/src/memory/observations.ts +19 -0
  78. package/src/memory/retention.ts +38 -5
  79. package/src/multimodal/image-loader.ts +6 -3
  80. package/src/rerank/http-provider.ts +125 -0
  81. package/src/rerank/index.ts +81 -0
  82. package/src/rerank/policy.ts +88 -0
  83. package/src/server/mcp-binding-store.ts +79 -0
  84. package/src/server/mcp-diagnostics.ts +40 -0
  85. package/src/server/tool-profile.ts +4 -0
  86. package/src/server.ts +121 -1
  87. package/src/store/evidence-store.ts +261 -0
  88. package/src/store/orama-store.ts +79 -53
  89. package/src/store/sqlite-db.ts +104 -0
  90. package/src/types.ts +8 -0
@@ -69,6 +69,13 @@ export function serializeResolvedConfigToToml(
69
69
  dimensions: config.embedding.dimensions,
70
70
  });
71
71
 
72
+ writeSection(lines, 'rerank', {
73
+ provider: config.rerank.provider,
74
+ model: config.rerank.model,
75
+ base_url: config.rerank.baseUrl,
76
+ api_key: includeSecrets ? config.rerank.apiKey : undefined,
77
+ });
78
+
72
79
  writeSection(lines, 'memory', {
73
80
  inject: config.memory.inject,
74
81
  formation: config.memory.formation,
@@ -11,6 +11,7 @@ import { buildBoundedContextReceipt } from '../../knowledge/context-receipt.js';
11
11
  import { WORKFLOW_AGENT_TARGETS } from '../../knowledge/workflows.js';
12
12
  import type { AgentTarget } from '../../types.js';
13
13
  import { emitError, emitResult, getCliProjectContext } from './operator-shared.js';
14
+ import { CliFallbackBudget } from '../fallback-budget.js';
14
15
 
15
16
  function coerceRefreshMode(input?: string): AutoContextRefreshMode {
16
17
  const value = (input ?? 'auto').trim().toLowerCase();
@@ -42,6 +43,7 @@ export default defineCommand({
42
43
  agent: { type: 'string', description: 'Optional target agent for compatible workflow selection' },
43
44
  json: { type: 'boolean', description: 'Emit machine-readable JSON output' },
44
45
  briefJson: { type: 'boolean', description: 'Emit only the bounded agent brief and receipt JSON' },
46
+ fallback: { type: 'boolean', description: 'Mark this as the one-shot MCP-unavailable fallback for the task' },
45
47
  },
46
48
  run: async ({ args }) => {
47
49
  const asJson = !!args.json || !!args.briefJson;
@@ -51,6 +53,15 @@ export default defineCommand({
51
53
 
52
54
  try {
53
55
  const { project, dataDir, reader } = await getCliProjectContext();
56
+ if (args.fallback) {
57
+ const budget = new CliFallbackBudget();
58
+ await budget.init(dataDir);
59
+ const claim = budget.claim(project.id, task);
60
+ if (!claim.allowed) {
61
+ emitError(claim.reason ?? 'CLI fallback budget exhausted for this task.', asJson);
62
+ return;
63
+ }
64
+ }
54
65
  const context = await buildAutoProjectContext({
55
66
  project,
56
67
  dataDir,
@@ -517,7 +517,11 @@ export default defineCommand({
517
517
  if (isLLMEnabled()) {
518
518
  const config = getLLMConfig();
519
519
  lines.push(ok(`Provider: ${config?.provider}/${config?.model}`));
520
- lines.push(info('Capabilities: fact extraction, auto-dedup, LLM rerank'));
520
+ const { isNeuralRerankEnabled } = await import('../../rerank/index.js');
521
+ const rerankCap = isNeuralRerankEnabled()
522
+ ? 'HTTP rerank'
523
+ : 'LLM rerank';
524
+ lines.push(info(`Capabilities: fact extraction, auto-dedup, ${rerankCap}`));
521
525
  report.llm = { enabled: true, provider: config?.provider, model: config?.model };
522
526
  } else {
523
527
  lines.push(info('LLM mode: off (heuristic dedup only)'));
@@ -0,0 +1,56 @@
1
+ import { defineCommand } from 'citty';
2
+ import { getObservationStore } from '../../store/obs-store.js';
3
+ import { EvidenceCardStore } from '../../store/evidence-store.js';
4
+ import { emitError, emitResult, getCliProjectContext, getCliReadContext } from './operator-shared.js';
5
+
6
+ export default defineCommand({
7
+ meta: { name: 'evidence', description: 'Inspect project-scoped evidence cards and provenance' },
8
+ args: {
9
+ action: { type: 'positional', description: 'list, get, sync, stale, or events', required: false },
10
+ id: { type: 'string', description: 'Candidate id, usually an observation id' },
11
+ paths: { type: 'string', description: 'Comma-separated changed paths for stale' },
12
+ reason: { type: 'string', description: 'Why the source became stale' },
13
+ query: { type: 'string', description: 'Search title/source fields' },
14
+ limit: { type: 'string', description: 'Maximum cards/events' },
15
+ json: { type: 'boolean', description: 'Emit JSON' },
16
+ },
17
+ async run({ args }) {
18
+ const action = (args.action as string | undefined) || 'list';
19
+ const asJson = !!args.json;
20
+ try {
21
+ const context = action === 'list' || action === 'get' || action === 'events'
22
+ ? await getCliReadContext()
23
+ : await getCliProjectContext({ searchIndex: false });
24
+ const { project, dataDir } = context;
25
+ const store = new EvidenceCardStore();
26
+ await store.init(dataDir);
27
+ const limit = Math.min(100, Math.max(1, Number(args.limit || 20)));
28
+ if (action === 'sync') {
29
+ const synced = store.syncObservations(await getObservationStore().loadByProject(project.id));
30
+ emitResult({ project, synced: synced.length }, `Synced ${synced.length} evidence card(s).`, asJson);
31
+ return;
32
+ }
33
+ if (action === 'stale') {
34
+ const paths = String(args.paths || '').split(',').map((value) => value.trim()).filter(Boolean);
35
+ const changed = store.markStaleForPaths(project.id, paths, String(args.reason || 'source file changed'));
36
+ emitResult({ project, staleMarked: changed, paths }, `Marked ${changed} evidence card(s) stale.`, asJson);
37
+ return;
38
+ }
39
+ const id = String(args.id || '').trim();
40
+ if (action === 'get' || action === 'events') {
41
+ if (!id) { emitError(`--id is required for evidence ${action}.`, asJson); return; }
42
+ const card = store.get(project.id, 'observation', id);
43
+ if (!card) { emitError(`Evidence card for observation ${id} was not found.`, asJson); return; }
44
+ const result = action === 'events' ? store.listEvents(card.id, limit) : { card, events: store.listEvents(card.id) };
45
+ emitResult({ project, result }, JSON.stringify(result, null, 2), asJson);
46
+ return;
47
+ }
48
+ if (action !== 'list') { emitError('action must be list, get, sync, stale, or events.', asJson); return; }
49
+ const query = String(args.query || '').trim().toLowerCase();
50
+ const cards = store.list(project.id, { limit: 100 }).filter((card) => !query || [card.title, card.summary, card.sourceRef, card.locator || ''].join(' ').toLowerCase().includes(query)).slice(0, limit);
51
+ emitResult({ project, cards }, cards.length === 0 ? 'No evidence cards found.' : cards.map((card) => `- ${card.candidateId} ${card.title} [${card.freshness}] ${card.sourceRef}`).join('\n'), asJson);
52
+ } catch (error) {
53
+ emitError(error instanceof Error ? error.message : String(error), asJson);
54
+ }
55
+ },
56
+ });
@@ -0,0 +1,58 @@
1
+ import { defineCommand } from 'citty';
2
+ import { MemoryFeedbackStore, type FeedbackCandidateKind, type MemoryFeedbackSignal } from '../../memory/feedback.js';
3
+ import { emitError, emitResult, getCliProjectContext, getCliReadContext } from './operator-shared.js';
4
+
5
+ const signals: MemoryFeedbackSignal[] = ['user-correction', 'verification-success', 'verification-failure', 'code-change', 'used', 'not-used', 'code-conflict', 'strengthen', 'weaken', 'revoke'];
6
+
7
+ export default defineCommand({
8
+ meta: { name: 'feedback', description: 'Record and audit memory feedback' },
9
+ args: {
10
+ action: { type: 'positional', description: 'record, show, or audit', required: false },
11
+ id: { type: 'string', description: 'Candidate id' },
12
+ kind: { type: 'string', description: 'observation, claim, durable-memory, or workflow' },
13
+ signal: { type: 'string', description: 'Feedback signal' },
14
+ source: { type: 'string', description: 'Source reference for the feedback' },
15
+ actor: { type: 'string', description: 'Actor id or human label' },
16
+ note: { type: 'string', description: 'Short audit note' },
17
+ targetEventId: { type: 'string', description: 'Event id to revoke' },
18
+ limit: { type: 'string', description: 'Maximum audit rows' },
19
+ json: { type: 'boolean', description: 'Emit JSON' },
20
+ },
21
+ async run({ args }) {
22
+ const action = (args.action as string | undefined) || 'show';
23
+ const asJson = !!args.json;
24
+ try {
25
+ const context = action === 'show' || action === 'audit' ? await getCliReadContext() : await getCliProjectContext({ searchIndex: false });
26
+ const { project, dataDir } = context;
27
+ const id = String(args.id || '').trim();
28
+ const kind = (String(args.kind || 'observation').trim() || 'observation') as FeedbackCandidateKind;
29
+ if (!id) { emitError('--id is required.', asJson); return; }
30
+ if (!['observation', 'claim', 'durable-memory', 'workflow'].includes(kind)) { emitError('Invalid --kind.', asJson); return; }
31
+ const store = new MemoryFeedbackStore();
32
+ await store.init(dataDir);
33
+ if (action === 'show') {
34
+ const result = store.getState(project.id, kind, id);
35
+ emitResult({ project, result }, JSON.stringify(result, null, 2), asJson);
36
+ return;
37
+ }
38
+ if (action === 'audit') {
39
+ const result = store.audit(project.id, kind, id, Number(args.limit || 100));
40
+ emitResult({ project, result }, JSON.stringify(result, null, 2), asJson);
41
+ return;
42
+ }
43
+ if (action !== 'record') { emitError('action must be record, show, or audit.', asJson); return; }
44
+ const signal = String(args.signal || '').trim() as MemoryFeedbackSignal;
45
+ const sourceRef = String(args.source || '').trim();
46
+ if (!signals.includes(signal) || !sourceRef) { emitError(`record requires --signal (${signals.join(', ')}) and --source.`, asJson); return; }
47
+ const result = store.record({
48
+ projectId: project.id, candidateKind: kind, candidateId: id, signal, sourceRef,
49
+ ...(args.actor ? { actor: String(args.actor) } : {}),
50
+ ...(args.note ? { note: String(args.note) } : {}),
51
+ ...(args.targetEventId ? { targetEventId: String(args.targetEventId) } : {}),
52
+ });
53
+ emitResult({ project, result }, `Recorded ${signal} for ${kind}:${id}; weight=${result.state.weight.toFixed(2)}.`, asJson);
54
+ } catch (error) {
55
+ emitError(error instanceof Error ? error.message : String(error), asJson);
56
+ }
57
+ },
58
+ });
@@ -183,6 +183,12 @@ export default defineCommand({
183
183
  envLines.push('');
184
184
  }
185
185
 
186
+ envLines.push('# Optional HTTP rerank (Cohere-compatible POST {base_url}/rerank)');
187
+ envLines.push('# MEMORIX_RERANK_PROVIDER=http');
188
+ envLines.push('# MEMORIX_RERANK_MODEL=rerank-model');
189
+ envLines.push('# MEMORIX_RERANK_BASE_URL=https://api.example.com/v1');
190
+ envLines.push('# MEMORIX_RERANK_API_KEY= # required when BASE_URL is not the memory LLM endpoint');
191
+ envLines.push('');
186
192
  envLines.push('# Optional memory LLM simple key (does not apply to embedding or agent lanes)');
187
193
  envLines.push('# MEMORIX_API_KEY=sk-your-key-here');
188
194
  envLines.push('');
@@ -310,6 +316,17 @@ export function buildInitTomlConfig(options: {
310
316
  }
311
317
  lines.push('');
312
318
 
319
+ lines.push('[rerank]');
320
+ lines.push('# Optional HTTP rerank against any Cohere-compatible /rerank API.');
321
+ lines.push('# provider = "http"');
322
+ lines.push('# model = "rerank-model"');
323
+ lines.push('# base_url inherits [memory.llm] when unset.');
324
+ if (isGlobal) {
325
+ lines.push('# Bearer inherits the memory LLM key only when that endpoint matches.');
326
+ lines.push('# A different rerank URL needs MEMORIX_RERANK_API_KEY.');
327
+ }
328
+ lines.push('');
329
+
313
330
  lines.push('[git]');
314
331
  lines.push(`auto_hook = ${options.gitAutoHook}`);
315
332
  lines.push('ingest_on_commit = true');
@@ -16,6 +16,7 @@ export default defineCommand({
16
16
  agent: { type: 'string', description: 'Optional target agent for compatible workflow selection' },
17
17
  json: { type: 'boolean', description: 'Emit machine-readable JSON output' },
18
18
  briefJson: { type: 'boolean', description: 'Emit only the bounded agent brief and receipt JSON' },
19
+ fallback: { type: 'boolean', description: 'Mark this as the one-shot MCP-unavailable fallback for the task' },
19
20
  },
20
21
  async run({ args }) {
21
22
  await contextCommand.run?.({
@@ -26,6 +27,7 @@ export default defineCommand({
26
27
  agent: args.agent,
27
28
  json: args.json,
28
29
  briefJson: args.briefJson,
30
+ fallback: args.fallback,
29
31
  resume: true,
30
32
  },
31
33
  rawArgs: [],
@@ -26,6 +26,7 @@ import { resolveToolProfile } from '../../server/tool-profile.js';
26
26
  import { scopeKnowledgeGraphToProject } from '../../memory/graph-scope.js';
27
27
  import { projectObservationRetention, summarizeRetentionProjections } from '../../memory/retention.js';
28
28
  import { canManageObservation, filterReadableObservations } from '../../memory/visibility.js';
29
+ import { CURRENT_MCP_PROTOCOL_VERSION, MCP_SERVER_NAME, getMcpDiagnostics } from '../../server/mcp-diagnostics.js';
29
30
  import { parseTcpPortOrReport } from '../port.js';
30
31
  import { mcpFileUriToPath } from '../mcp-root-path.js';
31
32
 
@@ -36,6 +37,8 @@ import { mcpFileUriToPath } from '../mcp-root-path.js';
36
37
  */
37
38
  export const DEFAULT_SESSION_TIMEOUT_MS = 12 * 60 * 60 * 1000;
38
39
  export const EXPIRED_SESSION_TTL_MS = 10 * 60 * 1000;
40
+ /** Pinned SDK protocol accepted internally while the HTTP boundary speaks the current stateless contract. */
41
+ export const MCP_SDK_COMPAT_PROTOCOL_VERSION = '2025-11-25';
39
42
 
40
43
  export function parseSessionTimeoutMs(raw: string | undefined): number {
41
44
  const value = raw?.trim();
@@ -116,6 +119,11 @@ export default defineCommand({
116
119
  const { findGitInSubdirs } = await import('../../project/detector.js');
117
120
  const { writeFileSync, mkdirSync } = await import('node:fs');
118
121
  const { homedir } = await import('node:os');
122
+ // Keep SQLite-backed control-plane helpers lazy. Besides reducing the
123
+ // handshake import graph, this prevents agent integration tests that mock
124
+ // node:fs from being evaluated before their mock factories initialize.
125
+ const { McpBindingStore } = await import('../../server/mcp-binding-store.js');
126
+ const { MemoryFeedbackStore } = await import('../../memory/feedback.js');
119
127
 
120
128
  // The [server] config section feeds startup defaults; CLI flags win.
121
129
  let configuredDefaults: ServeHttpConfiguredDefaults = {};
@@ -176,11 +184,12 @@ export default defineCommand({
176
184
  return store;
177
185
  }
178
186
 
179
- // Bootstrap the initial project's TeamStore. Never open SQLite at $HOME
180
- // (that creates a decoy ~/memorix.db). Unresolved launches share ~/.memorix/data.
181
- const detectedProj = serveResolution.detectedProject ?? detectorMod.detectProject(projectRoot);
182
- const teamDataDir = await persistMod.getProjectDataDir(detectedProj?.id ?? '__unresolved__');
183
- const sharedTeamStore = await getTeamStore(teamDataDir);
187
+ // TeamStore is resolved per MCP session/project. Sharing one mutable
188
+ // singleton here made project A's coordination state visible to project B
189
+ // after a roots switch. The cache still reuses the same project's SQLite
190
+ // handle without crossing project data directories.
191
+ const statelessBindingStore = new McpBindingStore();
192
+ await statelessBindingStore.init(persistMod.getBaseDataDir());
184
193
 
185
194
  type SessionState = {
186
195
  transport: InstanceType<typeof StreamableHTTPServerTransport>;
@@ -396,8 +405,95 @@ export default defineCommand({
396
405
  // No Access-Control-Allow-Origin at all for disallowed origins —
397
406
  // browser will block the response (fail-closed).
398
407
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
399
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Accept, Mcp-Session-Id, Last-Event-Id');
400
- res.setHeader('Access-Control-Expose-Headers', 'Mcp-Session-Id');
408
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Accept, Mcp-Session-Id, Mcp-Project-Handle, Mcp-Project-Root, Mcp-Stateless, Last-Event-Id');
409
+ res.setHeader('Access-Control-Expose-Headers', 'Mcp-Session-Id, Mcp-Project-Handle, Mcp-Protocol-Version, Mcp-Method, Mcp-Name');
410
+ res.setHeader('Mcp-Protocol-Version', req.headers['mcp-protocol-version'] || CURRENT_MCP_PROTOCOL_VERSION);
411
+ res.setHeader('Mcp-Method', req.method || 'UNKNOWN');
412
+ res.setHeader('Mcp-Name', MCP_SERVER_NAME);
413
+ }
414
+
415
+ /** Handle one 2026 stateless MCP request with a fresh server/transport. */
416
+ async function handleStatelessPost(req: IncomingMessage, res: ServerResponse, body: unknown): Promise<void> {
417
+ const handleId = typeof req.headers['mcp-project-handle'] === 'string'
418
+ ? req.headers['mcp-project-handle']
419
+ : undefined;
420
+ const persisted = handleId ? statelessBindingStore.touch(handleId) : undefined;
421
+ if (handleId && !persisted) {
422
+ res.writeHead(404, { 'Content-Type': 'application/json' });
423
+ res.end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32001, message: 'MCP project handle not found or expired; initialize again.' }, id: null }));
424
+ return;
425
+ }
426
+ const requestedRoot = typeof req.headers['mcp-project-root'] === 'string'
427
+ ? req.headers['mcp-project-root']
428
+ : undefined;
429
+ const root = persisted?.projectRoot ?? requestedRoot ?? projectRoot;
430
+ if (!persisted && !isInitializeRequest(body)) {
431
+ res.writeHead(400, { 'Content-Type': 'application/json' });
432
+ res.end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32000, message: 'Stateless MCP requests require Mcp-Project-Handle; initialize first.' }, id: null }));
433
+ return;
434
+ }
435
+
436
+ const { createMemorixServer } = await import('../../server.js');
437
+ const { createProjectBindingController } = await import('../../server/request-context.js');
438
+ const binding = createProjectBindingController(root);
439
+ const { server, projectId, handleTransportClose } = await createMemorixServer(
440
+ root,
441
+ undefined,
442
+ undefined,
443
+ {
444
+ allowUntrackedFallback: false,
445
+ deferProjectInitUntilBound: true,
446
+ deferProjectRuntimeInit: true,
447
+ dashboardMode: 'control-plane',
448
+ dashboardPort: port,
449
+ toolProfile,
450
+ projectBinding: binding,
451
+ },
452
+ );
453
+ const transport = new StreamableHTTPServerTransport({
454
+ // No Mcp-Session-Id: the protocol state is carried by our explicit
455
+ // durable project handle and reconstructed on every request.
456
+ sessionIdGenerator: undefined,
457
+ enableJsonResponse: true,
458
+ });
459
+ try {
460
+ await server.connect(transport);
461
+ let effectiveHandle = persisted;
462
+ if (!effectiveHandle && projectId !== '__unresolved__') {
463
+ effectiveHandle = statelessBindingStore.create({
464
+ projectId,
465
+ projectRoot: root,
466
+ dataDir: await persistMod.getProjectDataDir(projectId),
467
+ });
468
+ }
469
+ if (effectiveHandle) res.setHeader('Mcp-Project-Handle', effectiveHandle.handleId);
470
+ // The 1.30 SDK predates the 2026-07-28 version string. Keep the
471
+ // external stateless headers at the current contract, but normalize
472
+ // only the SDK-facing request and initialize params to its newest
473
+ // accepted wire version. This is a real compatibility adapter, not a
474
+ // claim that the pinned SDK implements newer optional extensions.
475
+ const sdkBody = body && typeof body === 'object'
476
+ ? JSON.parse(JSON.stringify(body)) as Record<string, any>
477
+ : body;
478
+ const sdkRequest = sdkBody as Record<string, any> | undefined;
479
+ if (sdkRequest && typeof sdkRequest === 'object' && sdkRequest.method === 'initialize' && sdkRequest.params) {
480
+ sdkRequest.params.protocolVersion = MCP_SDK_COMPAT_PROTOCOL_VERSION;
481
+ }
482
+ if (req.headers['mcp-protocol-version']) {
483
+ req.headers['mcp-protocol-version'] = MCP_SDK_COMPAT_PROTOCOL_VERSION;
484
+ for (let i = 0; i < req.rawHeaders.length; i += 2) {
485
+ if (req.rawHeaders[i].toLowerCase() === 'mcp-protocol-version') {
486
+ req.rawHeaders[i + 1] = MCP_SDK_COMPAT_PROTOCOL_VERSION;
487
+ break;
488
+ }
489
+ }
490
+ }
491
+ patchMcpResponse(res);
492
+ await transport.handleRequest(req, res, sdkBody);
493
+ } finally {
494
+ handleTransportClose();
495
+ await transport.close().catch(() => undefined);
496
+ }
401
497
  }
402
498
 
403
499
  /**
@@ -407,6 +503,14 @@ export default defineCommand({
407
503
  const sessionId = req.headers['mcp-session-id'] as string | undefined;
408
504
  const body = await parseBody(req);
409
505
 
506
+ const requestedProtocol = req.headers['mcp-protocol-version'];
507
+ const statelessRequested = req.headers['mcp-stateless'] === 'true'
508
+ || requestedProtocol === CURRENT_MCP_PROTOCOL_VERSION;
509
+ if (!sessionId && statelessRequested) {
510
+ await handleStatelessPost(req, res, body);
511
+ return;
512
+ }
513
+
410
514
  if (sessionId && sessions.has(sessionId)) {
411
515
  // Existing session — route to its transport
412
516
  sessionLastActivity.set(sessionId, Date.now());
@@ -464,7 +568,7 @@ export default defineCommand({
464
568
  const { server, switchProject, handleTransportClose } = await createMemorixServer(
465
569
  projectRoot,
466
570
  undefined,
467
- { teamStore: sharedTeamStore },
571
+ undefined,
468
572
  {
469
573
  allowUntrackedFallback: false,
470
574
  deferProjectInitUntilBound: true,
@@ -660,6 +764,20 @@ export default defineCommand({
660
764
  );
661
765
  }
662
766
 
767
+ async function loadProjectFeedback(dataDir: string, projectId: string): Promise<Map<string, { weight: number; status: any }>> {
768
+ const result = new Map<string, { weight: number; status: any }>();
769
+ try {
770
+ const feedback = new MemoryFeedbackStore();
771
+ await feedback.init(dataDir);
772
+ for (const state of feedback.listStates(projectId, 'observation')) {
773
+ result.set(state.candidateId, { weight: state.weight, status: state.status });
774
+ }
775
+ } catch {
776
+ // Older stores without the 1.8 tables retain the pre-feedback posture.
777
+ }
778
+ return result;
779
+ }
780
+
663
781
  // Resolve static directory (dist/dashboard/static)
664
782
  const cliDir = pathModule.default.dirname(fileURLToPath(import.meta.url));
665
783
  const dashStaticDir = pathModule.default.join(cliDir, '..', 'dashboard', 'static');
@@ -727,10 +845,53 @@ export default defineCommand({
727
845
  return;
728
846
  }
729
847
 
848
+ const maintenanceMatch = apiPath.match(/^\/maintenance\/(cleanup|deduplicate|consolidate|retention)\/(preview|execute)$/);
849
+ if (maintenanceMatch) {
850
+ if (req.method !== 'POST') {
851
+ sendJson({ error: 'Maintenance actions require POST.' }, 405);
852
+ return;
853
+ }
854
+ const bodyValue = await parseBody(req);
855
+ const body = bodyValue && typeof bodyValue === 'object' && !Array.isArray(bodyValue)
856
+ ? bodyValue as Record<string, unknown>
857
+ : {};
858
+ const { projectId: maintenanceProjectId, dataDir: maintenanceDataDir } = await resolveRequestProject(url);
859
+ const maintenanceStore = await getDashboardObservationStore(maintenanceDataDir);
860
+ const context = {
861
+ dataDir: maintenanceDataDir,
862
+ projectId: maintenanceProjectId,
863
+ projectRoot: maintenanceProjectId === defaultProject.id ? projectRoot : null,
864
+ store: maintenanceStore,
865
+ };
866
+ const maintenance = await import('../../dashboard/maintenance.js');
867
+ const action = maintenanceMatch[1];
868
+ const phase = maintenanceMatch[2];
869
+ let result: unknown;
870
+ if (action === 'cleanup') {
871
+ result = phase === 'preview'
872
+ ? await maintenance.previewCleanup(context, body.includeNoise === true)
873
+ : await maintenance.executeCleanup(context, body.payload, body.token);
874
+ } else if (action === 'deduplicate') {
875
+ result = phase === 'preview'
876
+ ? await maintenance.previewDeduplicate(context)
877
+ : await maintenance.executeDeduplicate(context, body.payload, body.token);
878
+ } else if (action === 'consolidate') {
879
+ result = phase === 'preview'
880
+ ? await maintenance.previewConsolidate(context)
881
+ : await maintenance.executeConsolidate(context, body.payload, body.token);
882
+ } else {
883
+ result = phase === 'preview'
884
+ ? await maintenance.previewRetentionArchive(context)
885
+ : await maintenance.executeRetentionArchive(context, body.payload, body.token);
886
+ }
887
+ sendJson(result);
888
+ return;
889
+ }
890
+
730
891
  if (apiPath === '/team') {
731
892
  // Phase 4a: All team state is in SQLite via TeamStore
732
- const { projectId: teamProjectId } = await resolveRequestProject(url);
733
- const ts = sharedTeamStore;
893
+ const { projectId: teamProjectId, dataDir: teamDataDir } = await resolveRequestProject(url);
894
+ const ts = await getTeamStore(teamDataDir);
734
895
  const scope = url.searchParams.get('scope') || 'project';
735
896
 
736
897
  // Determine effective project ID for query
@@ -847,6 +1008,7 @@ export default defineCommand({
847
1008
  const graph = { entities: getGraphStore().loadEntities(), relations: getGraphStore().loadRelations() };
848
1009
 
849
1010
  const observations = await loadDashboardProjectObservations(statsDataDir, statsProjectId, 'active');
1011
+ const feedback = await loadProjectFeedback(statsDataDir, statsProjectId);
850
1012
  const statsStore = await getDashboardObservationStore(statsDataDir);
851
1013
  const nextId = await statsStore.loadIdCounter();
852
1014
  const typeCounts: Record<string, number> = {};
@@ -887,7 +1049,10 @@ export default defineCommand({
887
1049
  const retention = summarizeRetentionProjections(
888
1050
  observations
889
1051
  .filter((observation) => observation.type !== 'probe')
890
- .map((observation) => projectObservationRetention(observation, { referenceTime: new Date(now) })),
1052
+ .map((observation) => projectObservationRetention(observation, {
1053
+ referenceTime: new Date(now),
1054
+ feedback: feedback.get(String(observation.id)),
1055
+ })),
891
1056
  );
892
1057
  const retentionSummary = {
893
1058
  active: retention.active,
@@ -1006,6 +1171,23 @@ export default defineCommand({
1006
1171
  return;
1007
1172
  }
1008
1173
 
1174
+ if (apiPath === '/evidence') {
1175
+ const { projectId: evidenceProjectId, dataDir: evidenceDataDir } = await resolveRequestProject(url);
1176
+ const { EvidenceCardStore } = await import('../../store/evidence-store.js');
1177
+ const evidenceStore = new EvidenceCardStore();
1178
+ await evidenceStore.init(evidenceDataDir);
1179
+ const observations = await loadDashboardProjectObservations(evidenceDataDir, evidenceProjectId, 'active');
1180
+ evidenceStore.syncObservations(observations);
1181
+ const query = (url.searchParams.get('q') || '').trim().toLowerCase();
1182
+ const limit = Math.min(100, Math.max(1, Number(url.searchParams.get('limit') || 20)));
1183
+ const cards = evidenceStore.list(evidenceProjectId, { limit: 100 })
1184
+ .filter((card) => !query || [card.title, card.summary, card.sourceRef, card.locator ?? ''].join(' ').toLowerCase().includes(query))
1185
+ .slice(0, limit)
1186
+ .map((card) => ({ ...card, events: evidenceStore.listEvents(card.id, 8) }));
1187
+ sendJson({ projectId: evidenceProjectId, cards });
1188
+ return;
1189
+ }
1190
+
1009
1191
  if (apiPath === '/graph') {
1010
1192
  const { projectId: graphProjectId, dataDir: graphDataDir } = await resolveRequestProject(url);
1011
1193
  const { initGraphStore, getGraphStore } = await import('../../store/graph-store.js');
@@ -1030,12 +1212,13 @@ export default defineCommand({
1030
1212
  if (apiPath === '/retention') {
1031
1213
  const { projectId: retProjectId, dataDir: retDataDir } = await resolveRequestProject(url);
1032
1214
  const observations = await loadDashboardProjectObservations(retDataDir, retProjectId, 'active');
1215
+ const feedback = await loadProjectFeedback(retDataDir, retProjectId);
1033
1216
  const referenceTime = new Date();
1034
1217
  const rows = observations
1035
1218
  .filter((observation) => observation.type !== 'probe')
1036
1219
  .map((observation) => ({
1037
1220
  observation,
1038
- retention: projectObservationRetention(observation, { referenceTime }),
1221
+ retention: projectObservationRetention(observation, { referenceTime, feedback: feedback.get(String(observation.id)) }),
1039
1222
  }))
1040
1223
  .sort((a, b) => b.retention.displayScore - a.retention.displayScore);
1041
1224
  const summary = summarizeRetentionProjections(rows.map((row) => row.retention));
@@ -1499,6 +1682,12 @@ export default defineCommand({
1499
1682
  return;
1500
1683
  }
1501
1684
 
1685
+ if (url.pathname === '/protocol-diagnostics') {
1686
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'public, max-age=60' });
1687
+ res.end(JSON.stringify(getMcpDiagnostics()));
1688
+ return;
1689
+ }
1690
+
1502
1691
  // MCP endpoint
1503
1692
  if (url.pathname === '/mcp') {
1504
1693
  // Ensure Accept header includes both application/json and text/event-stream.
@@ -104,6 +104,7 @@ export default defineCommand({
104
104
  diagLines.push(` Agent lane: ${formatLane(agent.provider, agent.model, agent.baseUrl, agent.apiKey)}`);
105
105
  diagLines.push(` Memory LLM lane: ${formatLane(resolved.memory.llm.provider, resolved.memory.llm.model, resolved.memory.llm.baseUrl, resolved.memory.llm.apiKey)}`);
106
106
  diagLines.push(` Embedding lane: ${formatLane(resolved.embedding.provider, resolved.embedding.model, resolved.embedding.baseUrl, resolved.embedding.apiKey)}`);
107
+ diagLines.push(` Rerank lane: ${formatLane(resolved.rerank.provider, resolved.rerank.model, resolved.rerank.baseUrl, resolved.rerank.apiKey)}`);
107
108
  diagLines.push(` Memory behavior: inject=${resolved.memory.inject ?? 'default'}, formation=${resolved.memory.formation ?? 'default'}, autoCleanup=${resolved.memory.autoCleanup ?? 'default'}`);
108
109
  diagLines.push(` Git behavior: autoHook=${resolved.git.autoHook ?? 'default'}, ingestOnCommit=${resolved.git.ingestOnCommit ?? 'default'}, maxDiffSize=${resolved.git.maxDiffSize ?? 'default'}, skipMergeCommits=${resolved.git.skipMergeCommits ?? 'default'}`);
109
110
  diagLines.push(` Server: transport=${resolved.server.transport ?? 'default'}, dashboard=${resolved.server.dashboard ?? 'default'}, dashboardPort=${resolved.server.dashboardPort ?? 'default'}`);
@@ -141,6 +142,7 @@ export default defineCommand({
141
142
  agent: summarizeLane(agent.provider, agent.model, agent.baseUrl, agent.apiKey),
142
143
  memoryLlm: summarizeLane(resolved.memory.llm.provider, resolved.memory.llm.model, resolved.memory.llm.baseUrl, resolved.memory.llm.apiKey),
143
144
  embedding: summarizeLane(resolved.embedding.provider, resolved.embedding.model, resolved.embedding.baseUrl, resolved.embedding.apiKey),
145
+ rerank: summarizeLane(resolved.rerank.provider, resolved.rerank.model, resolved.rerank.baseUrl, resolved.rerank.apiKey),
144
146
  },
145
147
  memory: {
146
148
  inject: resolved.memory.inject ?? 'default',
@@ -0,0 +1,59 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { getDatabase } from '../store/sqlite-db.js';
3
+
4
+ export const DEFAULT_FALLBACK_TTL_MS = 10 * 60 * 1000;
5
+
6
+ export interface FallbackClaim {
7
+ allowed: boolean;
8
+ key: string;
9
+ reason?: string;
10
+ count: number;
11
+ }
12
+
13
+ export function fallbackRequestKey(projectId: string, task: string | undefined): string {
14
+ const normalized = (task ?? '').trim().toLowerCase().replace(/\s+/g, ' ');
15
+ return createHash('sha256').update(`${projectId}\n${normalized}`).digest('hex').slice(0, 32);
16
+ }
17
+
18
+ /**
19
+ * Durable one-shot fallback guard. A failed MCP discovery must not turn into a
20
+ * loop of help/search/status probes; one bounded context request is enough.
21
+ */
22
+ export class CliFallbackBudget {
23
+ private db: any = null;
24
+
25
+ async init(dataDir: string): Promise<void> {
26
+ this.db = getDatabase(dataDir);
27
+ this.db.exec(`
28
+ CREATE TABLE IF NOT EXISTS cli_fallback_attempts (
29
+ project_id TEXT NOT NULL,
30
+ request_key TEXT NOT NULL,
31
+ count INTEGER NOT NULL DEFAULT 0,
32
+ last_attempt_at INTEGER NOT NULL,
33
+ expires_at INTEGER NOT NULL,
34
+ PRIMARY KEY (project_id, request_key)
35
+ )
36
+ `);
37
+ }
38
+
39
+ claim(projectId: string, task: string | undefined, now = Date.now(), ttlMs = DEFAULT_FALLBACK_TTL_MS): FallbackClaim {
40
+ if (!this.db) throw new Error('CliFallbackBudget is not initialized.');
41
+ const key = fallbackRequestKey(projectId, task);
42
+ const current = this.db.prepare(`
43
+ SELECT count, expires_at FROM cli_fallback_attempts WHERE project_id = ? AND request_key = ?
44
+ `).get(projectId, key);
45
+ const count = Number(current?.count ?? 0);
46
+ if (current && Number(current.expires_at) > now && count >= 1) {
47
+ return { allowed: false, key, count, reason: 'Fallback budget exhausted for this task. Stop probing and continue with the current project files.' };
48
+ }
49
+ this.db.prepare(`
50
+ INSERT INTO cli_fallback_attempts (project_id, request_key, count, last_attempt_at, expires_at)
51
+ VALUES (?, ?, 1, ?, ?)
52
+ ON CONFLICT(project_id, request_key) DO UPDATE SET
53
+ count = CASE WHEN excluded.expires_at > cli_fallback_attempts.expires_at THEN 1 ELSE cli_fallback_attempts.count + 1 END,
54
+ last_attempt_at = excluded.last_attempt_at,
55
+ expires_at = CASE WHEN excluded.expires_at > cli_fallback_attempts.expires_at THEN excluded.expires_at ELSE cli_fallback_attempts.expires_at END
56
+ `).run(projectId, key, now, now + Math.max(1_000, ttlMs));
57
+ return { allowed: true, key, count: 1 };
58
+ }
59
+ }
package/src/cli/index.ts CHANGED
@@ -248,6 +248,8 @@ const main = defineCommand({
248
248
  codegraph: () => import('./commands/codegraph.js').then(m => m.default),
249
249
  checkpoint: () => import('./commands/checkpoint.js').then(m => m.default),
250
250
  knowledge: () => import('./commands/knowledge.js').then(m => m.default),
251
+ evidence: () => import('./commands/evidence.js').then(m => m.default),
252
+ feedback: () => import('./commands/feedback.js').then(m => m.default),
251
253
  reasoning: () => import('./commands/reasoning.js').then(m => m.default),
252
254
  retention: () => import('./commands/retention.js').then(m => m.default),
253
255
  formation: () => import('./commands/formation.js').then(m => m.default),
@@ -318,7 +320,7 @@ const main = defineCommand({
318
320
  const firstArg = process.argv[2];
319
321
  const knownSubs = ['ask', 'search', 'remember', 'recent', 'help', 'workbench', 'memcode', 'config',
320
322
  'init', 'setup', 'integrate', 'memory', 'context', 'resume', 'explain', 'codegraph', 'checkpoint', 'knowledge', 'reasoning', 'retention', 'formation', 'audit', 'transfer', 'skills', 'identity',
321
- 'session', 'team', 'task', 'message', 'lock', 'handoff', 'poll',
323
+ 'session', 'team', 'task', 'message', 'lock', 'handoff', 'poll', 'evidence', 'feedback',
322
324
  'receipt',
323
325
  'serve', 'serve-http', 'status', 'sync',
324
326
  'hook', 'hooks', 'ingest', 'media', 'git-hook', 'git-hook-uninstall',
@@ -362,6 +364,8 @@ const main = defineCommand({
362
364
  console.error(' codegraph Refresh/status/context-pack for CodeGraph Memory');
363
365
  console.error(' checkpoint Inspect native compact continuity checkpoints');
364
366
  console.error(' knowledge Review source-backed knowledge pages and project workflows');
367
+ console.error(' evidence Inspect source-backed evidence cards');
368
+ console.error(' feedback Record or audit memory feedback');
365
369
  console.error(' reasoning Store/search decision rationale');
366
370
  console.error(' retention Inspect stale/archive status');
367
371
  console.error(' formation Inspect Memory Formation metrics');