memorix 1.2.6 → 1.2.8
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 +20 -0
- package/README.md +6 -3
- package/README.zh-CN.md +5 -2
- package/dist/cli/index.js +1272 -105
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/static/app.js +7 -2
- package/dist/index.js +809 -52
- package/dist/index.js.map +1 -1
- package/dist/maintenance-runner.d.ts +6 -0
- package/dist/maintenance-runner.js +204 -25
- package/dist/maintenance-runner.js.map +1 -1
- package/dist/memcode-runtime/CHANGELOG.md +20 -0
- package/dist/sdk.d.ts +1 -1
- package/dist/sdk.js +809 -52
- package/dist/sdk.js.map +1 -1
- package/dist/types.d.ts +38 -1
- package/dist/types.js.map +1 -1
- package/docs/1.2.7-CONTEXT-CONTINUITY.md +68 -0
- package/docs/API_REFERENCE.md +6 -2
- package/docs/dev-log/progress.txt +48 -0
- package/docs/hooks-architecture.md +2 -1
- package/package.json +3 -3
- package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
- package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/copilot/memorix/plugin.json +1 -1
- package/plugins/gemini/memorix/gemini-extension.json +1 -1
- package/plugins/omp/memorix/extensions/memorix.js +17 -0
- package/plugins/omp/memorix/package.json +1 -1
- package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/pi/memorix/extensions/memorix.js +17 -0
- package/plugins/pi/memorix/package.json +1 -1
- package/src/cli/capability-map.ts +1 -0
- package/src/cli/command-guide.ts +10 -0
- package/src/cli/commands/agent-integrations.ts +102 -5
- package/src/cli/commands/checkpoint.ts +144 -0
- package/src/cli/commands/serve-http.ts +14 -3
- package/src/cli/commands/setup.ts +44 -0
- package/src/cli/index.ts +3 -1
- package/src/codegraph/auto-context.ts +51 -5
- package/src/dashboard/server.ts +11 -0
- package/src/hooks/handler.ts +103 -11
- package/src/hooks/normalizer.ts +85 -1
- package/src/hooks/types.ts +16 -0
- package/src/knowledge/workset.ts +43 -2
- package/src/memory/compaction.ts +165 -0
- package/src/memory/observations.ts +67 -20
- package/src/runtime/maintenance-jobs.ts +84 -4
- package/src/runtime/project-maintenance.ts +63 -6
- package/src/server/tool-profile.ts +1 -0
- package/src/server.ts +94 -0
- package/src/store/compaction-checkpoint-store.ts +397 -0
- package/src/store/sqlite-db.ts +41 -0
- package/src/types.ts +46 -0
|
@@ -102,6 +102,10 @@ function normalizeEmbeddingFailure(error: unknown): { key: string; message: stri
|
|
|
102
102
|
};
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
function vectorBackfillError(error: unknown): string {
|
|
106
|
+
return sanitizeCredentials(normalizeEmbeddingFailure(error).message).slice(0, 1_000);
|
|
107
|
+
}
|
|
108
|
+
|
|
105
109
|
function queueVectorBackfill(projectId: string): void {
|
|
106
110
|
const dataDir = projectDir;
|
|
107
111
|
if (!dataDir) return;
|
|
@@ -1202,6 +1206,7 @@ export async function backfillVectorEmbeddings(options: {
|
|
|
1202
1206
|
attempted: number;
|
|
1203
1207
|
succeeded: number;
|
|
1204
1208
|
failed: number;
|
|
1209
|
+
lastError?: string;
|
|
1205
1210
|
}> {
|
|
1206
1211
|
if (vectorBackfillRunning) {
|
|
1207
1212
|
return { attempted: 0, succeeded: 0, failed: 0 };
|
|
@@ -1218,19 +1223,63 @@ export async function backfillVectorEmbeddings(options: {
|
|
|
1218
1223
|
let succeeded = 0;
|
|
1219
1224
|
let failed = 0;
|
|
1220
1225
|
let lastFailure: string | undefined;
|
|
1226
|
+
const recordFailure = (message: string): void => {
|
|
1227
|
+
// A single batch may have several symptoms after one root cause. Preserve
|
|
1228
|
+
// the first one so an informative provider/index error is not overwritten
|
|
1229
|
+
// by later missing-result fallout.
|
|
1230
|
+
if (!lastFailure) lastFailure = message;
|
|
1231
|
+
};
|
|
1232
|
+
|
|
1233
|
+
const candidates: Array<{ id: number; observation: Observation; text: string }> = [];
|
|
1234
|
+
for (const id of ids) {
|
|
1235
|
+
const observation = observationById.get(id);
|
|
1236
|
+
if (!observation) {
|
|
1237
|
+
vectorMissingIds.delete(id);
|
|
1238
|
+
continue;
|
|
1239
|
+
}
|
|
1240
|
+
candidates.push({
|
|
1241
|
+
id,
|
|
1242
|
+
observation,
|
|
1243
|
+
text: [observation.title, observation.narrative, ...observation.facts].join(' '),
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1221
1246
|
|
|
1222
1247
|
try {
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1248
|
+
if (candidates.length === 0) {
|
|
1249
|
+
return { attempted: ids.length, succeeded, failed };
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
let embeddings: number[][] | null = null;
|
|
1253
|
+
try {
|
|
1254
|
+
const provider = await getEmbeddingProvider();
|
|
1255
|
+
if (!provider) {
|
|
1256
|
+
if (isEmbeddingExplicitlyDisabled()) {
|
|
1257
|
+
for (const candidate of candidates) vectorMissingIds.delete(candidate.id);
|
|
1258
|
+
} else {
|
|
1259
|
+
recordFailure('embedding provider unavailable');
|
|
1260
|
+
failed += candidates.length;
|
|
1261
|
+
}
|
|
1262
|
+
} else {
|
|
1263
|
+
// API providers batch and cache this efficiently; local providers can use
|
|
1264
|
+
// their native batch path. Index writes stay sequential because Orama is
|
|
1265
|
+
// process-local mutable state.
|
|
1266
|
+
embeddings = await provider.embedBatch(candidates.map((candidate) => candidate.text));
|
|
1228
1267
|
}
|
|
1268
|
+
} catch (error) {
|
|
1269
|
+
recordFailure(vectorBackfillError(error));
|
|
1270
|
+
failed += candidates.length;
|
|
1271
|
+
}
|
|
1229
1272
|
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
const
|
|
1233
|
-
|
|
1273
|
+
if (embeddings) {
|
|
1274
|
+
for (let index = 0; index < candidates.length; index++) {
|
|
1275
|
+
const { id, observation: obs } = candidates[index];
|
|
1276
|
+
const embedding = embeddings[index];
|
|
1277
|
+
if (!embedding || embedding.length === 0) {
|
|
1278
|
+
recordFailure('embedding provider returned no vector');
|
|
1279
|
+
failed++;
|
|
1280
|
+
continue;
|
|
1281
|
+
}
|
|
1282
|
+
try {
|
|
1234
1283
|
if (!isVectorCompatibleWithCurrentIndex(embedding)) {
|
|
1235
1284
|
await upgradeVectorSchemaAfterFirstEmbedding(embedding);
|
|
1236
1285
|
}
|
|
@@ -1239,7 +1288,7 @@ export async function backfillVectorEmbeddings(options: {
|
|
|
1239
1288
|
console.error(
|
|
1240
1289
|
`[memorix] Backfill embedding mismatch for obs-${id}: provider returned ${embedding.length}d, index expects ${vectorDimensions ?? 'unknown'}d (kept in queue)`,
|
|
1241
1290
|
);
|
|
1242
|
-
|
|
1291
|
+
recordFailure(`dimension mismatch: provider returned ${embedding.length}d, index expects ${vectorDimensions ?? 'unknown'}d`);
|
|
1243
1292
|
failed++;
|
|
1244
1293
|
continue;
|
|
1245
1294
|
}
|
|
@@ -1277,17 +1326,10 @@ export async function backfillVectorEmbeddings(options: {
|
|
|
1277
1326
|
await insertObservation(doc);
|
|
1278
1327
|
vectorMissingIds.delete(id);
|
|
1279
1328
|
succeeded++;
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1282
|
-
vectorMissingIds.delete(id);
|
|
1283
|
-
} else {
|
|
1284
|
-
// Provider temporarily unavailable — keep in queue for next backfill cycle
|
|
1285
|
-
lastFailure = 'embedding provider unavailable';
|
|
1329
|
+
} catch (error) {
|
|
1330
|
+
recordFailure(vectorBackfillError(error));
|
|
1286
1331
|
failed++;
|
|
1287
1332
|
}
|
|
1288
|
-
} catch (err) {
|
|
1289
|
-
lastFailure = err instanceof Error ? err.message : String(err);
|
|
1290
|
-
failed++;
|
|
1291
1333
|
}
|
|
1292
1334
|
}
|
|
1293
1335
|
} finally {
|
|
@@ -1301,5 +1343,10 @@ export async function backfillVectorEmbeddings(options: {
|
|
|
1301
1343
|
};
|
|
1302
1344
|
}
|
|
1303
1345
|
|
|
1304
|
-
return {
|
|
1346
|
+
return {
|
|
1347
|
+
attempted: ids.length,
|
|
1348
|
+
succeeded,
|
|
1349
|
+
failed,
|
|
1350
|
+
...(lastFailure ? { lastError: lastFailure } : {}),
|
|
1351
|
+
};
|
|
1305
1352
|
}
|
|
@@ -67,7 +67,18 @@ export interface MaintenanceJobSummary {
|
|
|
67
67
|
|
|
68
68
|
export type MaintenanceJobRunResult =
|
|
69
69
|
| { action: 'complete' }
|
|
70
|
-
| {
|
|
70
|
+
| {
|
|
71
|
+
action: 'reschedule';
|
|
72
|
+
delayMs: number;
|
|
73
|
+
resetAttempts?: boolean;
|
|
74
|
+
payload?: Record<string, unknown>;
|
|
75
|
+
/** Keep recoverable work out of the immediate pending queue during a cooldown. */
|
|
76
|
+
status?: 'pending' | 'retry';
|
|
77
|
+
/** Persist a sanitized diagnostic without consuming the job's retry budget. */
|
|
78
|
+
lastError?: string;
|
|
79
|
+
/** Clear a stale transient diagnostic after the job makes progress. */
|
|
80
|
+
clearLastError?: boolean;
|
|
81
|
+
};
|
|
71
82
|
|
|
72
83
|
export type MaintenanceJobHandler = (
|
|
73
84
|
job: MaintenanceJob,
|
|
@@ -160,6 +171,12 @@ export class MaintenanceJobStore {
|
|
|
160
171
|
`).get(input.projectId, input.kind, dedupeKey);
|
|
161
172
|
|
|
162
173
|
if (existing) {
|
|
174
|
+
// A vector backfill in cooldown is a circuit-breaker state. A new MCP
|
|
175
|
+
// process must not pull it forward and recreate the original retry storm.
|
|
176
|
+
if (input.kind === 'vector-backfill' && existing.status === 'retry') {
|
|
177
|
+
this.commit();
|
|
178
|
+
return rowToJob(existing);
|
|
179
|
+
}
|
|
163
180
|
const nextRunAfter = Math.min(Number(existing.run_after), runAfter);
|
|
164
181
|
this.db.prepare(`
|
|
165
182
|
UPDATE maintenance_jobs
|
|
@@ -171,6 +188,30 @@ export class MaintenanceJobStore {
|
|
|
171
188
|
return rowToJob(updated);
|
|
172
189
|
}
|
|
173
190
|
|
|
191
|
+
// Older releases exhausted vector jobs on transient provider/index errors.
|
|
192
|
+
// Reuse one failed record so the next healthy session recovers work instead
|
|
193
|
+
// of adding another permanent failure row for the same project.
|
|
194
|
+
if (input.kind === 'vector-backfill') {
|
|
195
|
+
const failed = this.db.prepare(`
|
|
196
|
+
SELECT * FROM maintenance_jobs
|
|
197
|
+
WHERE project_id = ? AND kind = ? AND dedupe_key = ? AND status = 'failed'
|
|
198
|
+
ORDER BY updated_at DESC
|
|
199
|
+
LIMIT 1
|
|
200
|
+
`).get(input.projectId, input.kind, dedupeKey);
|
|
201
|
+
if (failed) {
|
|
202
|
+
this.db.prepare(`
|
|
203
|
+
UPDATE maintenance_jobs
|
|
204
|
+
SET status = 'pending', attempts = 0, max_attempts = ?, run_after = ?,
|
|
205
|
+
payload_json = ?, lease_owner = NULL, lease_expires_at = NULL,
|
|
206
|
+
last_error = NULL, completed_at = NULL, updated_at = ?
|
|
207
|
+
WHERE id = ?
|
|
208
|
+
`).run(maxAttempts, runAfter, payloadJson, now, failed.id);
|
|
209
|
+
const revived = this.getRow(failed.id)!;
|
|
210
|
+
this.commit();
|
|
211
|
+
return rowToJob(revived);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
174
215
|
const id = randomUUID();
|
|
175
216
|
this.db.prepare(`
|
|
176
217
|
INSERT INTO maintenance_jobs (
|
|
@@ -361,21 +402,57 @@ export class MaintenanceJobStore {
|
|
|
361
402
|
reschedule(
|
|
362
403
|
id: string,
|
|
363
404
|
workerId: string,
|
|
364
|
-
options: {
|
|
405
|
+
options: {
|
|
406
|
+
delayMs: number;
|
|
407
|
+
resetAttempts?: boolean;
|
|
408
|
+
payload?: Record<string, unknown>;
|
|
409
|
+
status?: 'pending' | 'retry';
|
|
410
|
+
lastError?: string;
|
|
411
|
+
clearLastError?: boolean;
|
|
412
|
+
now?: number;
|
|
413
|
+
},
|
|
365
414
|
): MaintenanceJob | undefined {
|
|
366
415
|
const now = options.now ?? Date.now();
|
|
367
416
|
const delayMs = Number.isFinite(options.delayMs) ? Math.max(0, Math.floor(options.delayMs)) : 0;
|
|
368
417
|
const payloadJson = options.payload === undefined ? null : JSON.stringify(options.payload);
|
|
418
|
+
const status = options.status === 'retry' ? 'retry' : 'pending';
|
|
419
|
+
const hasLastError = options.lastError !== undefined;
|
|
420
|
+
const lastError = hasLastError ? errorText(options.lastError) : null;
|
|
369
421
|
this.db.prepare(`
|
|
370
422
|
UPDATE maintenance_jobs
|
|
371
|
-
SET status =
|
|
423
|
+
SET status = ?, run_after = ?, attempts = CASE WHEN ? THEN 0 ELSE attempts END,
|
|
372
424
|
payload_json = COALESCE(?, payload_json),
|
|
425
|
+
last_error = CASE
|
|
426
|
+
WHEN ? THEN NULL
|
|
427
|
+
WHEN ? THEN ?
|
|
428
|
+
ELSE last_error
|
|
429
|
+
END,
|
|
373
430
|
lease_owner = NULL, lease_expires_at = NULL, updated_at = ?
|
|
374
431
|
WHERE id = ? AND status = 'running' AND lease_owner = ?
|
|
375
|
-
`).run(
|
|
432
|
+
`).run(
|
|
433
|
+
status,
|
|
434
|
+
now + delayMs,
|
|
435
|
+
options.resetAttempts ? 1 : 0,
|
|
436
|
+
payloadJson,
|
|
437
|
+
options.clearLastError ? 1 : 0,
|
|
438
|
+
hasLastError ? 1 : 0,
|
|
439
|
+
lastError,
|
|
440
|
+
now,
|
|
441
|
+
id,
|
|
442
|
+
workerId,
|
|
443
|
+
);
|
|
376
444
|
return this.get(id);
|
|
377
445
|
}
|
|
378
446
|
|
|
447
|
+
resolveFailedVectorBackfills(projectId: string, dedupeKey = 'vector-backfill', now = Date.now()): number {
|
|
448
|
+
const result = this.db.prepare(`
|
|
449
|
+
UPDATE maintenance_jobs
|
|
450
|
+
SET status = 'completed', completed_at = ?, updated_at = ?
|
|
451
|
+
WHERE project_id = ? AND kind = 'vector-backfill' AND dedupe_key = ? AND status = 'failed'
|
|
452
|
+
`).run(now, now, projectId, dedupeKey);
|
|
453
|
+
return Number(result.changes ?? 0);
|
|
454
|
+
}
|
|
455
|
+
|
|
379
456
|
fail(id: string, workerId: string, error: unknown, now = Date.now()): MaintenanceJob | undefined {
|
|
380
457
|
this.begin();
|
|
381
458
|
try {
|
|
@@ -486,6 +563,9 @@ export class MaintenanceJobWorker {
|
|
|
486
563
|
delayMs: result.delayMs,
|
|
487
564
|
resetAttempts: result.resetAttempts,
|
|
488
565
|
payload: result.payload,
|
|
566
|
+
status: result.status,
|
|
567
|
+
lastError: result.lastError,
|
|
568
|
+
clearLastError: result.clearLastError,
|
|
489
569
|
now,
|
|
490
570
|
});
|
|
491
571
|
return { state: 'rescheduled', job: updated };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import {
|
|
2
|
+
MaintenanceJobStore,
|
|
3
|
+
type MaintenanceJobHandler,
|
|
4
|
+
type MaintenanceJobRunResult,
|
|
4
5
|
} from './maintenance-jobs.js';
|
|
5
6
|
import { runMaintenanceInChildProcess } from './isolated-maintenance.js';
|
|
6
7
|
import {
|
|
@@ -18,6 +19,8 @@ const DEFAULT_CODEGRAPH_MAX_FILES = 5_000;
|
|
|
18
19
|
const DEFAULT_CLAIM_DERIVATION_BATCH_SIZE = 100;
|
|
19
20
|
const DEFAULT_OBSERVATION_QUALIFICATION_BATCH_SIZE = 100;
|
|
20
21
|
const VECTOR_RETRY_DELAY_MS = 5_000;
|
|
22
|
+
const VECTOR_FAILURE_BASE_RETRY_DELAY_MS = 60_000;
|
|
23
|
+
const VECTOR_FAILURE_MAX_RETRY_DELAY_MS = 15 * 60_000;
|
|
21
24
|
const DEDUP_PER_PAIR_TIMEOUT_MS = 5_000;
|
|
22
25
|
|
|
23
26
|
function vectorBatchSize(payload: Record<string, unknown>): number {
|
|
@@ -26,6 +29,33 @@ function vectorBatchSize(payload: Record<string, unknown>): number {
|
|
|
26
29
|
return Math.min(100, Math.max(1, Math.floor(value)));
|
|
27
30
|
}
|
|
28
31
|
|
|
32
|
+
function vectorBackfillFailureStreak(payload: Record<string, unknown>): number {
|
|
33
|
+
const value = payload.vectorBackfillFailureStreak;
|
|
34
|
+
return typeof value === 'number' && Number.isFinite(value)
|
|
35
|
+
? Math.min(16, Math.max(0, Math.floor(value)))
|
|
36
|
+
: 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function vectorBackfillRetryDelayMs(streak: number): number {
|
|
40
|
+
return Math.min(
|
|
41
|
+
VECTOR_FAILURE_MAX_RETRY_DELAY_MS,
|
|
42
|
+
VECTOR_FAILURE_BASE_RETRY_DELAY_MS * (2 ** Math.max(0, streak - 1)),
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function withoutVectorBackfillFailureStreak(payload: Record<string, unknown>): Record<string, unknown> {
|
|
47
|
+
const { vectorBackfillFailureStreak: _streak, ...rest } = payload;
|
|
48
|
+
return rest;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function resolveSupersededVectorFailures(projectDir: string, projectId: string): void {
|
|
52
|
+
try {
|
|
53
|
+
new MaintenanceJobStore(projectDir).resolveFailedVectorBackfills(projectId);
|
|
54
|
+
} catch {
|
|
55
|
+
// Health cleanup must never block the live memory path.
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
29
59
|
function retentionBatchSize(payload: Record<string, unknown>): number {
|
|
30
60
|
const value = payload.limit;
|
|
31
61
|
if (typeof value !== 'number' || !Number.isFinite(value)) return DEFAULT_RETENTION_BATCH_SIZE;
|
|
@@ -498,23 +528,50 @@ export function createProjectMaintenanceHandler(
|
|
|
498
528
|
|
|
499
529
|
const { backfillVectorEmbeddings, getVectorStatus } = await import('../memory/observations.js');
|
|
500
530
|
const before = getVectorStatus(projectId);
|
|
501
|
-
if (before.missing === 0)
|
|
531
|
+
if (before.missing === 0) {
|
|
532
|
+
resolveSupersededVectorFailures(projectDir, projectId);
|
|
533
|
+
return { action: 'complete' };
|
|
534
|
+
}
|
|
502
535
|
|
|
503
536
|
const result = await backfillVectorEmbeddings({
|
|
504
537
|
projectId,
|
|
505
538
|
limit: vectorBatchSize(job.payload),
|
|
506
539
|
});
|
|
507
540
|
const after = getVectorStatus(projectId);
|
|
508
|
-
if (after.missing === 0)
|
|
541
|
+
if (after.missing === 0) {
|
|
542
|
+
resolveSupersededVectorFailures(projectDir, projectId);
|
|
543
|
+
return { action: 'complete' };
|
|
544
|
+
}
|
|
509
545
|
|
|
510
546
|
if (result.failed > 0 && result.succeeded === 0) {
|
|
511
|
-
|
|
547
|
+
const streak = vectorBackfillFailureStreak(job.payload) + 1;
|
|
548
|
+
return {
|
|
549
|
+
action: 'reschedule',
|
|
550
|
+
status: 'retry',
|
|
551
|
+
delayMs: vectorBackfillRetryDelayMs(streak),
|
|
552
|
+
// This is a recoverable provider/index state, not a terminal job error.
|
|
553
|
+
// Keeping one retry row prevents new MCP processes from creating a storm.
|
|
554
|
+
resetAttempts: true,
|
|
555
|
+
payload: {
|
|
556
|
+
...job.payload,
|
|
557
|
+
vectorBackfillFailureStreak: streak,
|
|
558
|
+
},
|
|
559
|
+
lastError: result.lastError ?? `vector backfill made no progress (${result.failed}/${result.attempted} failed)`,
|
|
560
|
+
};
|
|
512
561
|
}
|
|
513
562
|
|
|
563
|
+
const priorFailureStreak = vectorBackfillFailureStreak(job.payload);
|
|
564
|
+
const payload = withoutVectorBackfillFailureStreak(job.payload);
|
|
514
565
|
return {
|
|
515
566
|
action: 'reschedule',
|
|
516
567
|
delayMs: result.attempted === 0 ? VECTOR_RETRY_DELAY_MS : 0,
|
|
517
568
|
resetAttempts: result.succeeded > 0,
|
|
569
|
+
...(result.succeeded > 0 ? {
|
|
570
|
+
...(priorFailureStreak > 0 ? { payload } : {}),
|
|
571
|
+
...(result.failed > 0 && result.lastError
|
|
572
|
+
? { lastError: result.lastError }
|
|
573
|
+
: priorFailureStreak > 0 ? { clearLastError: true } : {}),
|
|
574
|
+
} : {}),
|
|
518
575
|
};
|
|
519
576
|
};
|
|
520
577
|
}
|
|
@@ -68,6 +68,7 @@ export const TOOL_PROFILES: Record<string, ReadonlyArray<ToolProfile>> = Object.
|
|
|
68
68
|
memorix_rules_sync: ['full'],
|
|
69
69
|
memorix_workspace_sync: ['full'],
|
|
70
70
|
memorix_ingest_image: ['full'],
|
|
71
|
+
memorix_compaction_checkpoint: ['full'],
|
|
71
72
|
|
|
72
73
|
// ── MCP Official Memory Server compatibility (KG tools) ──────────
|
|
73
74
|
// These are only useful to users specifically migrating from the
|
package/src/server.ts
CHANGED
|
@@ -3974,6 +3974,100 @@ export async function createMemorixServer(
|
|
|
3974
3974
|
},
|
|
3975
3975
|
);
|
|
3976
3976
|
|
|
3977
|
+
/**
|
|
3978
|
+
* memorix_compaction_checkpoint — Advanced inspection of native host
|
|
3979
|
+
* compaction checkpoints. Automatic recovery does not need this tool; it is
|
|
3980
|
+
* deliberately full-profile only so normal MCP startup stays compact.
|
|
3981
|
+
*/
|
|
3982
|
+
server.registerTool(
|
|
3983
|
+
'memorix_compaction_checkpoint',
|
|
3984
|
+
{
|
|
3985
|
+
title: 'Compact Continuity Checkpoints',
|
|
3986
|
+
description:
|
|
3987
|
+
'Inspect, preview, or archive bounded checkpoints recorded around a host-native context compaction. ' +
|
|
3988
|
+
'Use only to debug or explicitly review compaction continuity. These are lifecycle records, not durable memories or transcript backups. ' +
|
|
3989
|
+
'CLI equivalent: memorix checkpoint list|show|context|archive.',
|
|
3990
|
+
inputSchema: {
|
|
3991
|
+
action: z.enum(['list', 'show', 'context', 'archive']).default('list'),
|
|
3992
|
+
id: z.string().optional().describe('Checkpoint ID for show, context, or archive'),
|
|
3993
|
+
sessionId: z.string().optional().describe('Optional host session ID filter'),
|
|
3994
|
+
agent: z.string().optional().describe('Optional host agent filter'),
|
|
3995
|
+
task: z.string().optional().describe('Current task for a bounded context preview'),
|
|
3996
|
+
maxTokens: z.number().optional().describe('Maximum tokens for action=context (default: 420)'),
|
|
3997
|
+
limit: z.number().optional().describe('Maximum records for action=list (default: 20)'),
|
|
3998
|
+
includeArchived: z.boolean().optional().default(false).describe('Include archived records in action=list'),
|
|
3999
|
+
},
|
|
4000
|
+
},
|
|
4001
|
+
async ({ action, id, sessionId, agent, task, maxTokens, limit, includeArchived }) => {
|
|
4002
|
+
const unresolved = requireResolvedProject('inspect compact continuity checkpoints');
|
|
4003
|
+
if (unresolved) return unresolved;
|
|
4004
|
+
|
|
4005
|
+
const [{ CompactionCheckpointStore }, { buildCompactionWorkset }] = await Promise.all([
|
|
4006
|
+
import('./store/compaction-checkpoint-store.js'),
|
|
4007
|
+
import('./memory/compaction.js'),
|
|
4008
|
+
]);
|
|
4009
|
+
const store = new CompactionCheckpointStore(projectDir);
|
|
4010
|
+
const assertCheckpoint = (checkpointId: string) => {
|
|
4011
|
+
const checkpoint = store.get(checkpointId);
|
|
4012
|
+
if (!checkpoint || checkpoint.projectId !== project.id) return null;
|
|
4013
|
+
return checkpoint;
|
|
4014
|
+
};
|
|
4015
|
+
|
|
4016
|
+
if (action === 'list') {
|
|
4017
|
+
const checkpoints = store.list({
|
|
4018
|
+
projectId: project.id,
|
|
4019
|
+
sessionId: sessionId?.trim() || undefined,
|
|
4020
|
+
agent: agent?.trim() || undefined,
|
|
4021
|
+
includeArchived: Boolean(includeArchived),
|
|
4022
|
+
limit: limit != null ? Math.max(1, Math.min(100, coerceNumber(limit, 20))) : 20,
|
|
4023
|
+
});
|
|
4024
|
+
return {
|
|
4025
|
+
content: [{ type: 'text' as const, text: JSON.stringify({ projectId: project.id, checkpoints }, null, 2) }],
|
|
4026
|
+
};
|
|
4027
|
+
}
|
|
4028
|
+
|
|
4029
|
+
if (!id?.trim()) {
|
|
4030
|
+
return {
|
|
4031
|
+
content: [{ type: 'text' as const, text: 'id is required for this checkpoint action.' }],
|
|
4032
|
+
isError: true,
|
|
4033
|
+
};
|
|
4034
|
+
}
|
|
4035
|
+
const checkpoint = assertCheckpoint(id.trim());
|
|
4036
|
+
if (!checkpoint) {
|
|
4037
|
+
return {
|
|
4038
|
+
content: [{ type: 'text' as const, text: `Checkpoint "${id.trim()}" was not found for the current project.` }],
|
|
4039
|
+
isError: true,
|
|
4040
|
+
};
|
|
4041
|
+
}
|
|
4042
|
+
|
|
4043
|
+
if (action === 'show') {
|
|
4044
|
+
return {
|
|
4045
|
+
content: [{ type: 'text' as const, text: JSON.stringify({ projectId: project.id, checkpoint }, null, 2) }],
|
|
4046
|
+
};
|
|
4047
|
+
}
|
|
4048
|
+
if (action === 'context') {
|
|
4049
|
+
const workset = buildCompactionWorkset(checkpoint, {
|
|
4050
|
+
task: task?.trim(),
|
|
4051
|
+
maxTokens: maxTokens != null ? coerceNumber(maxTokens, 420) : 420,
|
|
4052
|
+
});
|
|
4053
|
+
return {
|
|
4054
|
+
content: [{ type: 'text' as const, text: workset.text }],
|
|
4055
|
+
};
|
|
4056
|
+
}
|
|
4057
|
+
|
|
4058
|
+
const archived = store.archive(checkpoint.id);
|
|
4059
|
+
if (!archived) {
|
|
4060
|
+
return {
|
|
4061
|
+
content: [{ type: 'text' as const, text: `Checkpoint "${checkpoint.id}" is already archived.` }],
|
|
4062
|
+
isError: true,
|
|
4063
|
+
};
|
|
4064
|
+
}
|
|
4065
|
+
return {
|
|
4066
|
+
content: [{ type: 'text' as const, text: `Archived compact checkpoint ${archived.id}.` }],
|
|
4067
|
+
};
|
|
4068
|
+
},
|
|
4069
|
+
);
|
|
4070
|
+
|
|
3977
4071
|
// ============================================================
|
|
3978
4072
|
// Export / Import
|
|
3979
4073
|
// ============================================================
|