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.
- package/CHANGELOG.md +32 -0
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/cli/index.js +1407 -192
- 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 +875 -164
- package/dist/maintenance-runner.js +607 -68
- package/dist/media-audio-runner.js +602 -63
- package/dist/media-video-runner.js +607 -68
- package/dist/memcode-runtime/CHANGELOG.md +32 -0
- package/dist/sdk.js +874 -163
- package/dist/types.d.ts +8 -0
- package/dist/vector-backfill-runner.js +607 -68
- package/docs/1.2.4-PERSISTENT-MEMORY-DELIVERY.md +1 -1
- package/docs/1.8.0-RELEASE-CANDIDATE-SPEC.md +75 -0
- package/docs/API_REFERENCE.md +37 -1
- package/docs/DASHBOARD.md +10 -0
- package/docs/DEVELOPMENT.md +3 -2
- package/docs/INTEGRATIONS.md +1 -1
- package/docs/README.md +11 -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/context.ts +11 -0
- package/src/cli/commands/evidence.ts +56 -0
- package/src/cli/commands/feedback.ts +58 -0
- package/src/cli/commands/resume.ts +2 -0
- package/src/cli/commands/serve-http.ts +201 -12
- package/src/cli/fallback-budget.ts +59 -0
- package/src/cli/index.ts +5 -1
- package/src/compact/engine.ts +26 -1
- package/src/config/dotenv-loader.ts +17 -0
- package/src/dashboard/config-provenance.ts +17 -0
- package/src/dashboard/evidence.ts +63 -0
- package/src/dashboard/server.ts +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/memory/feedback-model.ts +4 -0
- package/src/memory/feedback.ts +220 -0
- package/src/memory/observations.ts +19 -0
- package/src/memory/retention.ts +38 -5
- package/src/server/mcp-binding-store.ts +79 -0
- package/src/server/mcp-diagnostics.ts +40 -0
- package/src/server/tool-profile.ts +4 -0
- package/src/server.ts +121 -1
- package/src/store/evidence-store.ts +261 -0
- package/src/store/sqlite-db.ts +104 -0
- 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
|
|
package/src/memory/retention.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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, {
|
|
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'],
|