memorix 1.7.3 → 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 (70) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +1 -1
  3. package/README.zh-CN.md +1 -1
  4. package/dist/cli/index.js +1407 -192
  5. package/dist/dashboard/static/app.js +88 -4
  6. package/dist/dashboard/static/index.html +10 -0
  7. package/dist/dashboard/static/style.css +86 -0
  8. package/dist/index.js +875 -164
  9. package/dist/maintenance-runner.js +607 -68
  10. package/dist/media-audio-runner.js +602 -63
  11. package/dist/media-video-runner.js +607 -68
  12. package/dist/memcode-runtime/CHANGELOG.md +32 -0
  13. package/dist/sdk.js +874 -163
  14. package/dist/types.d.ts +8 -0
  15. package/dist/vector-backfill-runner.js +607 -68
  16. package/docs/1.2.4-PERSISTENT-MEMORY-DELIVERY.md +1 -1
  17. package/docs/1.8.0-RELEASE-CANDIDATE-SPEC.md +75 -0
  18. package/docs/API_REFERENCE.md +37 -1
  19. package/docs/DASHBOARD.md +10 -0
  20. package/docs/DEVELOPMENT.md +3 -2
  21. package/docs/INTEGRATIONS.md +1 -1
  22. package/docs/README.md +11 -1
  23. package/package.json +1 -1
  24. package/plugins/antigravity/memorix/rules/memorix.md +1 -1
  25. package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
  26. package/plugins/claude/memorix/SKILL.md +1 -1
  27. package/plugins/claude/memorix/skills/memorix-memory/SKILL.md +2 -2
  28. package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
  29. package/plugins/codebuddy/memorix/skills/memorix-memory/SKILL.md +2 -2
  30. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  31. package/plugins/codex/memorix/skills/memorix-memory/SKILL.md +2 -2
  32. package/plugins/copilot/memorix/plugin.json +1 -1
  33. package/plugins/copilot/memorix/skills/memorix-memory/SKILL.md +2 -2
  34. package/plugins/cursor/memorix/skills/memorix-memory/SKILL.md +2 -2
  35. package/plugins/gemini/memorix/GEMINI.md +1 -1
  36. package/plugins/gemini/memorix/gemini-extension.json +1 -1
  37. package/plugins/omp/memorix/package.json +1 -1
  38. package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
  39. package/plugins/pi/memorix/package.json +1 -1
  40. package/plugins/pi/memorix/skills/memorix-memory/SKILL.md +2 -2
  41. package/server.json +2 -2
  42. package/src/cli/capability-map.ts +2 -0
  43. package/src/cli/commands/context.ts +11 -0
  44. package/src/cli/commands/evidence.ts +56 -0
  45. package/src/cli/commands/feedback.ts +58 -0
  46. package/src/cli/commands/resume.ts +2 -0
  47. package/src/cli/commands/serve-http.ts +201 -12
  48. package/src/cli/fallback-budget.ts +59 -0
  49. package/src/cli/index.ts +5 -1
  50. package/src/compact/engine.ts +26 -1
  51. package/src/config/dotenv-loader.ts +17 -0
  52. package/src/dashboard/config-provenance.ts +17 -0
  53. package/src/dashboard/evidence.ts +63 -0
  54. package/src/dashboard/server.ts +93 -68
  55. package/src/dashboard/static/index.html +10 -0
  56. package/src/dashboard/static/style.css +86 -0
  57. package/src/hooks/installers/index.ts +1 -1
  58. package/src/hooks/official-skills.ts +3 -3
  59. package/src/hooks/rules/memorix-agent-rules.md +1 -1
  60. package/src/memory/feedback-model.ts +4 -0
  61. package/src/memory/feedback.ts +220 -0
  62. package/src/memory/observations.ts +19 -0
  63. package/src/memory/retention.ts +38 -5
  64. package/src/server/mcp-binding-store.ts +79 -0
  65. package/src/server/mcp-diagnostics.ts +40 -0
  66. package/src/server/tool-profile.ts +4 -0
  67. package/src/server.ts +121 -1
  68. package/src/store/evidence-store.ts +261 -0
  69. package/src/store/sqlite-db.ts +104 -0
  70. package/src/types.ts +8 -0
@@ -0,0 +1,220 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import type { ObservationStatus } from '../types.js';
3
+ import { getDatabase } from '../store/sqlite-db.js';
4
+ export { feedbackWeightMultiplier } from './feedback-model.js';
5
+
6
+ export type MemoryFeedbackSignal =
7
+ | 'user-correction'
8
+ | 'verification-success'
9
+ | 'verification-failure'
10
+ | 'code-change'
11
+ | 'used'
12
+ | 'not-used'
13
+ | 'code-conflict'
14
+ | 'strengthen'
15
+ | 'weaken'
16
+ | 'revoke';
17
+
18
+ export interface MemoryFeedbackEvent {
19
+ signal: MemoryFeedbackSignal;
20
+ at: string;
21
+ actor?: string;
22
+ note?: string;
23
+ sourceRef?: string;
24
+ targetEventId?: string;
25
+ }
26
+
27
+ export interface FeedbackState {
28
+ weight: number;
29
+ status: ObservationStatus;
30
+ audit: MemoryFeedbackEvent[];
31
+ }
32
+
33
+ export type FeedbackCandidateKind = 'observation' | 'claim' | 'durable-memory' | 'workflow';
34
+
35
+ export interface PersistedFeedbackEvent extends MemoryFeedbackEvent {
36
+ id: string;
37
+ projectId: string;
38
+ candidateKind: FeedbackCandidateKind;
39
+ candidateId: string;
40
+ delta: number;
41
+ }
42
+
43
+ export const FEEDBACK_DELTAS: Record<Exclude<MemoryFeedbackSignal, 'revoke'>, number> = {
44
+ 'user-correction': -0.35,
45
+ 'verification-success': 0.25,
46
+ 'verification-failure': -0.25,
47
+ 'code-change': -0.15,
48
+ used: 0.1,
49
+ 'not-used': -0.05,
50
+ 'code-conflict': -0.4,
51
+ strengthen: 0.2,
52
+ weaken: -0.2,
53
+ };
54
+
55
+ /** Deterministic feedback reducer used by MCP/CLI adapters and tests. */
56
+ export function applyMemoryFeedback(
57
+ current: FeedbackState,
58
+ event: MemoryFeedbackEvent,
59
+ ): FeedbackState {
60
+ const delta = event.signal === 'revoke' ? 0 : FEEDBACK_DELTAS[event.signal];
61
+ const weight = Math.max(0, Math.min(2, current.weight + delta));
62
+ const status: ObservationStatus = (event.signal === 'code-conflict' || event.signal === 'user-correction' || event.signal === 'weaken') && weight < 0.5
63
+ ? 'archived'
64
+ : current.status;
65
+ return { weight, status, audit: [...current.audit, { ...event }] };
66
+ }
67
+
68
+ function rowToEvent(row: any): PersistedFeedbackEvent {
69
+ return {
70
+ id: String(row.id),
71
+ projectId: String(row.project_id),
72
+ candidateKind: row.candidate_kind,
73
+ candidateId: String(row.candidate_id),
74
+ signal: row.signal,
75
+ ...(row.observed_at ? { at: String(row.observed_at) } : { at: new Date().toISOString() }),
76
+ ...(row.actor ? { actor: String(row.actor) } : {}),
77
+ ...(row.note ? { note: String(row.note) } : {}),
78
+ ...(row.source_ref ? { sourceRef: String(row.source_ref) } : {}),
79
+ ...(row.target_event_id ? { targetEventId: String(row.target_event_id) } : {}),
80
+ delta: Number(row.delta ?? 0),
81
+ };
82
+ }
83
+
84
+ function candidateKey(projectId: string, candidateKind: FeedbackCandidateKind, candidateId: string): string {
85
+ return `${projectId}\u0000${candidateKind}\u0000${candidateId}`;
86
+ }
87
+
88
+ /**
89
+ * Durable feedback loop. Events are append-only; the state row is a materialized
90
+ * projection rebuilt from non-revoked events, so undo never destroys audit truth.
91
+ */
92
+ export class MemoryFeedbackStore {
93
+ private db: any = null;
94
+
95
+ async init(dataDir: string): Promise<void> {
96
+ this.db = getDatabase(dataDir);
97
+ }
98
+
99
+ private requireDb(): any {
100
+ if (!this.db) throw new Error('MemoryFeedbackStore is not initialized.');
101
+ return this.db;
102
+ }
103
+
104
+ private loadEvents(projectId: string, candidateKind: FeedbackCandidateKind, candidateId: string): PersistedFeedbackEvent[] {
105
+ return this.requireDb().prepare(`
106
+ SELECT * FROM memory_feedback_events
107
+ WHERE project_id = ? AND candidate_kind = ? AND candidate_id = ?
108
+ ORDER BY observed_at ASC, rowid ASC
109
+ `).all(projectId, candidateKind, candidateId).map(rowToEvent);
110
+ }
111
+
112
+ private rebuild(projectId: string, candidateKind: FeedbackCandidateKind, candidateId: string): FeedbackState {
113
+ const events = this.loadEvents(projectId, candidateKind, candidateId);
114
+ const revoked = new Set(events.filter((event) => event.signal === 'revoke' && event.targetEventId).map((event) => event.targetEventId!));
115
+ let state: FeedbackState = { weight: 1, status: 'active', audit: [] };
116
+ for (const event of events) {
117
+ if (event.signal === 'revoke' || revoked.has(event.id)) {
118
+ state.audit.push(event);
119
+ continue;
120
+ }
121
+ state = applyMemoryFeedback(state, event);
122
+ }
123
+ this.requireDb().prepare(`
124
+ INSERT INTO memory_feedback_states (project_id, candidate_kind, candidate_id, weight, lifecycle_status, updated_at)
125
+ VALUES (?, ?, ?, ?, ?, ?)
126
+ ON CONFLICT(project_id, candidate_kind, candidate_id) DO UPDATE SET
127
+ weight = excluded.weight, lifecycle_status = excluded.lifecycle_status, updated_at = excluded.updated_at
128
+ `).run(projectId, candidateKind, candidateId, state.weight, state.status, new Date().toISOString());
129
+ return state;
130
+ }
131
+
132
+ getState(projectId: string, candidateKind: FeedbackCandidateKind, candidateId: string): FeedbackState {
133
+ const row = this.requireDb().prepare(`
134
+ SELECT weight, lifecycle_status FROM memory_feedback_states
135
+ WHERE project_id = ? AND candidate_kind = ? AND candidate_id = ?
136
+ `).get(projectId, candidateKind, candidateId);
137
+ if (!row) return { weight: 1, status: 'active', audit: [] };
138
+ return {
139
+ weight: Number(row.weight ?? 1),
140
+ status: row.lifecycle_status ?? 'active',
141
+ audit: this.loadEvents(projectId, candidateKind, candidateId),
142
+ };
143
+ }
144
+
145
+ listStates(projectId: string, candidateKind: FeedbackCandidateKind = 'observation'): Array<{ candidateId: string; weight: number; status: ObservationStatus; updatedAt: string }> {
146
+ return this.requireDb().prepare(`
147
+ SELECT candidate_id, weight, lifecycle_status, updated_at
148
+ FROM memory_feedback_states WHERE project_id = ? AND candidate_kind = ?
149
+ ORDER BY updated_at DESC
150
+ `).all(projectId, candidateKind).map((row: any) => ({
151
+ candidateId: String(row.candidate_id),
152
+ weight: Number(row.weight),
153
+ status: row.lifecycle_status,
154
+ updatedAt: String(row.updated_at),
155
+ }));
156
+ }
157
+
158
+ weights(projectId: string, candidateKind: FeedbackCandidateKind = 'observation'): Map<string, FeedbackState> {
159
+ const states = new Map<string, FeedbackState>();
160
+ for (const row of this.listStates(projectId, candidateKind)) {
161
+ states.set(row.candidateId, this.getState(projectId, candidateKind, row.candidateId));
162
+ }
163
+ return states;
164
+ }
165
+
166
+ record(input: {
167
+ projectId: string;
168
+ candidateKind: FeedbackCandidateKind;
169
+ candidateId: string;
170
+ signal: MemoryFeedbackSignal;
171
+ sourceRef: string;
172
+ actor?: string;
173
+ note?: string;
174
+ targetEventId?: string;
175
+ at?: string;
176
+ }): { event: PersistedFeedbackEvent; state: FeedbackState } {
177
+ if (input.signal === 'revoke' && !input.targetEventId) {
178
+ throw new Error('A revoke feedback event requires targetEventId.');
179
+ }
180
+ if (input.signal === 'revoke') {
181
+ const target = this.requireDb().prepare(`
182
+ SELECT 1 FROM memory_feedback_events
183
+ WHERE id = ? AND project_id = ? AND candidate_kind = ? AND candidate_id = ?
184
+ `).get(input.targetEventId, input.projectId, input.candidateKind, input.candidateId);
185
+ if (!target) throw new Error('The feedback event to revoke was not found in the same candidate scope.');
186
+ }
187
+ const event: PersistedFeedbackEvent = {
188
+ id: randomUUID(),
189
+ projectId: input.projectId,
190
+ candidateKind: input.candidateKind,
191
+ candidateId: input.candidateId,
192
+ signal: input.signal,
193
+ sourceRef: input.sourceRef,
194
+ ...(input.actor ? { actor: input.actor } : {}),
195
+ ...(input.note ? { note: input.note.slice(0, 2_000) } : {}),
196
+ ...(input.targetEventId ? { targetEventId: input.targetEventId } : {}),
197
+ at: input.at ?? new Date().toISOString(),
198
+ delta: input.signal === 'revoke' ? 0 : FEEDBACK_DELTAS[input.signal],
199
+ };
200
+ this.requireDb().prepare(`
201
+ INSERT INTO memory_feedback_events (
202
+ id, project_id, candidate_kind, candidate_id, signal, actor, note,
203
+ source_ref, target_event_id, delta, observed_at
204
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
205
+ `).run(
206
+ event.id, event.projectId, event.candidateKind, event.candidateId, event.signal,
207
+ event.actor ?? null, event.note ?? null, event.sourceRef, event.targetEventId ?? null,
208
+ event.delta, event.at,
209
+ );
210
+ return { event, state: this.rebuild(input.projectId, input.candidateKind, input.candidateId) };
211
+ }
212
+
213
+ audit(projectId: string, candidateKind: FeedbackCandidateKind, candidateId: string, limit = 100): PersistedFeedbackEvent[] {
214
+ return this.requireDb().prepare(`
215
+ SELECT * FROM memory_feedback_events
216
+ WHERE project_id = ? AND candidate_kind = ? AND candidate_id = ?
217
+ ORDER BY observed_at DESC, rowid DESC LIMIT ?
218
+ `).all(projectId, candidateKind, candidateId, Math.min(200, Math.max(1, Math.floor(limit)))).map(rowToEvent);
219
+ }
220
+ }
@@ -44,6 +44,7 @@ import { getEmbeddingProvider, isEmbeddingExplicitlyDisabled, validateEmbeddingI
44
44
  import { sanitizeCredentials } from './secret-filter.js';
45
45
  import { enqueueClaimDerivation, enqueueObservationQualification } from '../runtime/lifecycle.js';
46
46
  import { canManageObservation, resolveObservationVisibility } from './visibility.js';
47
+ import { EvidenceCardStore } from '../store/evidence-store.js';
47
48
 
48
49
  /** In-memory observation list (loaded from persistence on init) */
49
50
  let observations: Observation[] = [];
@@ -186,6 +187,20 @@ async function bindObservationCodeRefsBestEffort(observation: Observation): Prom
186
187
  }
187
188
  }
188
189
 
190
+ async function persistEvidenceCard(observation: Observation): Promise<void> {
191
+ if (!projectDir) return;
192
+ try {
193
+ const evidence = new EvidenceCardStore();
194
+ await evidence.init(projectDir);
195
+ evidence.upsertObservation(observation);
196
+ } catch (error) {
197
+ // Evidence is a derived control-plane projection. A failed projection must
198
+ // never roll back the canonical observation, but it must be visible for a
199
+ // later repair/sync pass.
200
+ console.error(`[memorix] Evidence card projection failed for obs-${observation.id}: ${error instanceof Error ? error.message : String(error)}`);
201
+ }
202
+ }
203
+
189
204
  function queueClaimDerivation(observation: Observation): void {
190
205
  const dataDir = projectDir;
191
206
  const isExplicit = observation.sourceDetail === 'explicit';
@@ -692,6 +707,7 @@ export async function storeObservation(input: {
692
707
  if (!persistedCallbackInvoked) await input.onPersisted?.(observation);
693
708
 
694
709
  await bindObservationCodeRefsBestEffort(observation);
710
+ await persistEvidenceCard(observation);
695
711
  queueObservationQualification(observation);
696
712
  if (resolveObservationVisibility(observation) === 'project') {
697
713
  queueClaimDerivation(observation);
@@ -828,6 +844,7 @@ async function upsertObservation(
828
844
  }
829
845
 
830
846
  await bindObservationCodeRefsBestEffort(existing);
847
+ await persistEvidenceCard(existing);
831
848
  queueObservationQualification(existing);
832
849
  if (resolveObservationVisibility(existing) === 'project') {
833
850
  queueClaimDerivation(existing);
@@ -1002,6 +1019,8 @@ export async function resolveObservations(
1002
1019
  });
1003
1020
  }
1004
1021
 
1022
+ for (const observation of changed) await persistEvidenceCard(observation);
1023
+
1005
1024
  return { resolved, notFound };
1006
1025
  }
1007
1026
 
@@ -15,9 +15,10 @@
15
15
  * retention period but are no longer permanently immune — they decay normally.
16
16
  */
17
17
 
18
- import type { MemorixDocument, Observation, ObservationReader } from '../types.js';
18
+ import type { MemorixDocument, Observation, ObservationReader, ObservationStatus } from '../types.js';
19
19
  import { getObservationStore } from '../store/obs-store.js';
20
20
  import { canManageObservation } from './visibility.js';
21
+ import { feedbackWeightMultiplier } from './feedback-model.js';
21
22
 
22
23
  // ── Importance → Retention Period mapping ────────────────────────────
23
24
 
@@ -113,6 +114,7 @@ export function getEffectiveRetentionDays(doc: MemorixDocument): number {
113
114
  export function isImmune(doc: MemorixDocument): boolean {
114
115
  // Probe observations are operational heartbeats -- never immune, regardless of valueCategory or access.
115
116
  if (doc.type === 'probe') return false;
117
+ if (doc.feedbackStatus === 'archived') return false;
116
118
 
117
119
  // A candidate has not earned durable status. Never let an automatic core
118
120
  // classification turn unqualified capture into permanently retained memory.
@@ -164,6 +166,7 @@ export interface RelevanceScore {
164
166
  accessBoost: number;
165
167
  ageDays: number;
166
168
  isImmune: boolean;
169
+ feedbackMultiplier: number;
167
170
  }
168
171
 
169
172
  /**
@@ -202,7 +205,12 @@ export function calculateRelevance(
202
205
  const accessCount = doc.accessCount ?? 0;
203
206
  const accessBoost = Math.min(2.0, 1 + 0.1 * accessCount);
204
207
 
205
- let totalScore = base * decayFactor * accessBoost;
208
+ // Legacy memories have no feedback row. Preserve their historical score
209
+ // exactly; only an explicit feedback projection changes relevance.
210
+ const feedbackMultiplier = doc.feedbackWeight === undefined
211
+ ? 1
212
+ : feedbackWeightMultiplier(doc.feedbackWeight);
213
+ let totalScore = base * decayFactor * accessBoost * feedbackMultiplier;
206
214
 
207
215
  // Immune observations get minimum 0.5 relevance
208
216
  const immune = isImmune(doc);
@@ -218,6 +226,7 @@ export function calculateRelevance(
218
226
  accessBoost,
219
227
  ageDays,
220
228
  isImmune: immune,
229
+ feedbackMultiplier,
221
230
  };
222
231
  }
223
232
 
@@ -248,6 +257,7 @@ export type RetentionZone = 'active' | 'stale' | 'archive-candidate';
248
257
  */
249
258
  export function getRetentionZone(doc: MemorixDocument, referenceTime?: Date): RetentionZone {
250
259
  const now = referenceTime ?? new Date();
260
+ if (doc.feedbackStatus === 'archived') return 'archive-candidate';
251
261
  const importance = getImportanceLevel(doc);
252
262
  const retention = getEffectiveRetentionDays(doc);
253
263
 
@@ -344,6 +354,7 @@ export interface RetentionProjection extends RetentionExplanation {
344
354
  export interface ObservationRetentionProjectionOptions {
345
355
  referenceTime?: Date;
346
356
  accessMap?: ObservationAccessMap;
357
+ feedback?: { weight: number; status: ObservationStatus };
347
358
  }
348
359
 
349
360
  export interface RetentionProjectionSummary {
@@ -415,6 +426,7 @@ export function explainRetention(
415
426
  export function toRetentionDocument(
416
427
  obs: Observation,
417
428
  accessMap?: ObservationAccessMap,
429
+ feedback?: { weight: number; status: ObservationStatus },
418
430
  ): MemorixDocument {
419
431
  const access = accessMap?.get(obs.id);
420
432
  return {
@@ -438,6 +450,7 @@ export function toRetentionDocument(
438
450
  valueCategory: obs.valueCategory ?? '',
439
451
  admissionState: obs.admissionState ?? '',
440
452
  admissionReason: obs.admissionReason ?? '',
453
+ ...(feedback ? { feedbackWeight: feedback.weight, feedbackStatus: feedback.status } : {}),
441
454
  };
442
455
  }
443
456
 
@@ -446,7 +459,7 @@ export function projectObservationRetention(
446
459
  observation: Observation,
447
460
  options: ObservationRetentionProjectionOptions = {},
448
461
  ): RetentionProjection {
449
- const document = toRetentionDocument(observation, options.accessMap);
462
+ const document = toRetentionDocument(observation, options.accessMap, options.feedback);
450
463
  const relevance = calculateRelevance(document, options.referenceTime);
451
464
  const explanation = explainRetention(document, options.referenceTime);
452
465
 
@@ -497,12 +510,25 @@ export interface ArchiveExpiredBatchResult {
497
510
  nextCursor?: number;
498
511
  }
499
512
 
513
+ async function loadFeedbackStore(dataDir: string): Promise<import('./feedback.js').MemoryFeedbackStore | undefined> {
514
+ try {
515
+ const { MemoryFeedbackStore } = await import('./feedback.js');
516
+ const store = new MemoryFeedbackStore();
517
+ await store.init(dataDir);
518
+ return store;
519
+ } catch {
520
+ // Pre-1.8 databases or degraded SQLite installations keep the legacy
521
+ // retention behavior until the feedback projection is available.
522
+ return undefined;
523
+ }
524
+ }
525
+
500
526
  /**
501
527
  * Archive one bounded page of a project's active memories. The caller owns the
502
528
  * cursor, which makes the operation safe to run from a durable maintenance job.
503
529
  */
504
530
  export async function archiveExpiredBatch(
505
- _projectDir: string,
531
+ projectDir: string,
506
532
  options: ArchiveExpiredBatchOptions,
507
533
  ): Promise<ArchiveExpiredBatchResult> {
508
534
  const store = getObservationStore();
@@ -513,6 +539,7 @@ export async function archiveExpiredBatch(
513
539
  afterId,
514
540
  limit: limit + 1,
515
541
  });
542
+ const feedback = await loadFeedbackStore(projectDir);
516
543
  const hasMore = page.length > limit;
517
544
  const scanned = hasMore ? page.slice(0, limit) : page;
518
545
  const candidateIds = scanned
@@ -520,6 +547,7 @@ export async function archiveExpiredBatch(
520
547
  .filter((observation) => projectObservationRetention(observation, {
521
548
  accessMap: options.accessMap,
522
549
  referenceTime: options.referenceTime,
550
+ ...(feedback ? { feedback: feedback.getState(options.projectId, 'observation', String(observation.id)) } : {}),
523
551
  }).zone === 'archive-candidate')
524
552
  .map((observation) => observation.id);
525
553
 
@@ -580,6 +608,7 @@ export async function archiveExpired(
580
608
  return { archived, remaining };
581
609
  }
582
610
 
611
+ const feedback = await loadFeedbackStore(projectDir);
583
612
  return await store.atomic(async (tx) => {
584
613
  const allObs = await tx.loadAll();
585
614
 
@@ -591,7 +620,11 @@ export async function archiveExpired(
591
620
  const archivedIds: number[] = [];
592
621
 
593
622
  for (const obs of activeObs) {
594
- const retention = projectObservationRetention(obs, { accessMap, referenceTime });
623
+ const retention = projectObservationRetention(obs, {
624
+ accessMap,
625
+ referenceTime,
626
+ ...(feedback ? { feedback: feedback.getState(obs.projectId, 'observation', String(obs.id)) } : {}),
627
+ });
595
628
  if (retention.zone === 'archive-candidate') {
596
629
  archivedIds.push(obs.id);
597
630
  }
@@ -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'],