memorix 1.4.4 → 1.4.6
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 +69 -0
- package/CLAUDE.md +7 -3
- package/README.md +14 -4
- package/README.zh-CN.md +9 -2
- package/dist/cli/index.js +1802 -1138
- package/dist/index.js +1298 -991
- package/dist/{maintenance-jobs-B1-W0ssz.d.ts → maintenance-jobs-DVRF-46r.d.ts} +1 -1
- package/dist/maintenance-runner.d.ts +1 -1
- package/dist/maintenance-runner.js +1755 -829
- package/dist/media-audio-runner.js +1760 -835
- package/dist/media-video-runner.js +1755 -830
- package/dist/memcode-runtime/CHANGELOG.md +69 -0
- package/dist/sdk.js +1290 -990
- package/dist/types.d.ts +7 -1
- package/dist/vector-backfill-runner.d.ts +1 -1
- package/dist/vector-backfill-runner.js +1751 -826
- package/docs/1.4.5-AGENT-AUTO-MEMORY-RESEARCH.md +95 -0
- package/docs/AGENT_OPERATOR_PLAYBOOK.md +3 -2
- package/docs/API_REFERENCE.md +23 -4
- package/docs/CONFIGURATION.md +17 -3
- package/docs/INTEGRATIONS.md +9 -2
- package/docs/KNOWN_ISSUES_AND_ROADMAP.md +1 -0
- package/docs/MEMCODE.md +1 -1
- package/docs/SETUP.md +14 -4
- package/llms-full.txt +5 -4
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/plugins/antigravity/memorix/mcp_config.json +3 -1
- package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
- package/plugins/claude/memorix/.mcp.json +1 -1
- package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
- package/plugins/codebuddy/memorix/.mcp.json +1 -1
- package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/codex/memorix/.mcp.json +1 -1
- package/plugins/copilot/memorix/.mcp.json +1 -1
- package/plugins/copilot/memorix/plugin.json +1 -1
- package/plugins/gemini/memorix/gemini-extension.json +4 -2
- package/plugins/omp/memorix/package.json +1 -1
- package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/openclaw/memorix/.mcp.json +3 -1
- package/plugins/pi/memorix/package.json +1 -1
- package/server.json +2 -2
- package/src/cli/commands/agent-integrations.ts +7 -1
- package/src/cli/commands/dashboard.ts +14 -3
- package/src/cli/commands/hooks-install.ts +2 -0
- package/src/cli/commands/hooks-preview.ts +5 -0
- package/src/cli/commands/ingest-commit.ts +47 -3
- package/src/cli/commands/integrate.ts +2 -0
- package/src/cli/commands/memory.ts +17 -4
- package/src/cli/commands/purge.ts +120 -0
- package/src/cli/commands/serve-http.ts +39 -5
- package/src/cli/commands/setup.ts +60 -3
- package/src/cli/commands/uninstall.ts +20 -0
- package/src/cli/index.ts +3 -804
- package/src/codegraph/auto-context.ts +82 -1
- package/src/config/yaml-loader.ts +3 -8
- package/src/config.ts +0 -5
- package/src/evaluation/session-replay.ts +98 -0
- package/src/git/extractor.ts +5 -3
- package/src/hooks/installers/index.ts +58 -2
- package/src/hooks/types.ts +2 -0
- package/src/integrations/registry.ts +9 -0
- package/src/knowledge/workset.ts +58 -0
- package/src/memory/long-term.ts +97 -2
- package/src/runtime/control-plane-maintenance.ts +1 -0
- package/src/runtime/isolated-maintenance.ts +1 -0
- package/src/runtime/lifecycle.ts +19 -0
- package/src/runtime/maintenance-jobs.ts +1 -0
- package/src/runtime/project-maintenance.ts +8 -0
- package/src/search/surfaced-registry.ts +62 -0
- package/src/server.ts +74 -14
- package/src/store/orama-store.ts +17 -1
- package/src/types.ts +7 -1
- package/src/workspace/mcp-adapters/dsh.ts +147 -0
- package/src/cli/commands/audit-list.ts +0 -89
- package/src/cli/commands/purge-all-memory.ts +0 -85
- package/src/cli/commands/purge-project-memory.ts +0 -83
- package/src/cli/commands/uninstall-project-artifacts.ts +0 -86
- package/src/cli/tui/Sidebar.tsx +0 -179
- package/src/cli/tui/interactive-commands.ts +0 -261
- package/src/cli/tui/markdown-render.tsx +0 -371
- package/src/cli/tui/use-mouse.ts +0 -157
- package/src/cli/tui/useNavigation.ts +0 -56
- package/src/orchestrate/context-compact.ts +0 -188
- package/src/team/file-locks.ts +0 -173
- package/src/team/index.ts +0 -23
- package/src/team/messages.ts +0 -203
- package/src/team/persistence.ts +0 -96
- package/src/team/registry.ts +0 -158
- package/src/team/tasks.ts +0 -211
package/src/runtime/lifecycle.ts
CHANGED
|
@@ -83,6 +83,25 @@ export function enqueueObservationQualification(input: {
|
|
|
83
83
|
});
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Long-term memory maintains itself on a daily rule leg: stale active records
|
|
88
|
+
* archive themselves and newer records supersede same-title older ones. No
|
|
89
|
+
* LLM required; enqueueing is a durable no-op when nothing is due.
|
|
90
|
+
*/
|
|
91
|
+
export function enqueueLongTermMaintenance(input: {
|
|
92
|
+
dataDir: string;
|
|
93
|
+
projectId: string;
|
|
94
|
+
source: string;
|
|
95
|
+
queue?: MaintenanceQueue;
|
|
96
|
+
}): void {
|
|
97
|
+
queueFor(input).enqueue({
|
|
98
|
+
projectId: input.projectId,
|
|
99
|
+
kind: 'long-term-maintenance',
|
|
100
|
+
dedupeKey: 'long-term-maintenance',
|
|
101
|
+
payload: { source: input.source },
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
86
105
|
/**
|
|
87
106
|
* The follow-up jobs operate on the same workspace the Workset reads: a
|
|
88
107
|
* versioned workspace when present, otherwise the local workspace.
|
|
@@ -263,6 +263,14 @@ export function createProjectMaintenanceHandler(
|
|
|
263
263
|
return { action: 'complete' };
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
+
if (job.kind === 'long-term-maintenance') {
|
|
267
|
+
const { maintainLongTermMemories } = await import('../memory/long-term.js');
|
|
268
|
+
await maintainLongTermMemories({ dataDir: projectDir, projectId });
|
|
269
|
+
// Decay and supersede thresholds are measured in days: one daily pass is
|
|
270
|
+
// plenty. Rescheduling the same row keeps a quiet, durable heartbeat.
|
|
271
|
+
return { action: 'reschedule', delayMs: 24 * 60 * 60 * 1000, resetAttempts: true };
|
|
272
|
+
}
|
|
273
|
+
|
|
266
274
|
if (job.kind === 'consolidation') {
|
|
267
275
|
const limit = consolidationBatchSize(job.payload);
|
|
268
276
|
const result = await runAutomaticConsolidation(projectId, projectDir, {
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Surfaced-entry registry — session-level "already shown this session" bookkeeping.
|
|
3
|
+
*
|
|
4
|
+
* The retrieval layer can demote entries whose ids are marked as surfaced, so
|
|
5
|
+
* a coding session does not get the same memory rows re-shown for every
|
|
6
|
+
* related question. This registry is the state that remembers which ids have
|
|
7
|
+
* been shown, keyed by session scope.
|
|
8
|
+
*
|
|
9
|
+
* Design boundaries:
|
|
10
|
+
* - In-memory and bounded: at most MAX_KEYS scopes, at most MAX_IDS ids per
|
|
11
|
+
* scope, oldest evicted first. Restarting the server resets it, which is
|
|
12
|
+
* correct — "surfaced" only means "shown in this live session".
|
|
13
|
+
* - Best-effort: losing an entry only costs one redundant row, never data.
|
|
14
|
+
* - Scope keys are caller-owned strings (e.g. "session:<id>" or
|
|
15
|
+
* "project:<id>"); the registry does not interpret them.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const MAX_KEYS = 64;
|
|
19
|
+
const MAX_IDS = 200;
|
|
20
|
+
|
|
21
|
+
const registry = new Map<string, number[]>();
|
|
22
|
+
|
|
23
|
+
function touch(key: string): number[] {
|
|
24
|
+
const ids = registry.get(key);
|
|
25
|
+
if (!ids) return [];
|
|
26
|
+
// Re-insert so Map iteration order approximates LRU for eviction.
|
|
27
|
+
registry.delete(key);
|
|
28
|
+
registry.set(key, ids);
|
|
29
|
+
return ids;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Ids already surfaced for this scope, oldest first. Never mutates state. */
|
|
33
|
+
export function getSurfacedIds(key: string): number[] {
|
|
34
|
+
return [...touch(key)];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Mark ids as surfaced for this scope. New ids append; old ids overflow out. */
|
|
38
|
+
export function recordSurfacedIds(key: string, ids: number[]): void {
|
|
39
|
+
if (ids.length === 0) return;
|
|
40
|
+
|
|
41
|
+
let list = registry.get(key) ?? [];
|
|
42
|
+
const known = new Set(list);
|
|
43
|
+
for (const id of ids) {
|
|
44
|
+
if (known.has(id)) continue;
|
|
45
|
+
known.add(id);
|
|
46
|
+
list.push(id);
|
|
47
|
+
}
|
|
48
|
+
if (list.length > MAX_IDS) {
|
|
49
|
+
list = list.slice(list.length - MAX_IDS);
|
|
50
|
+
}
|
|
51
|
+
registry.set(key, list);
|
|
52
|
+
|
|
53
|
+
if (registry.size > MAX_KEYS) {
|
|
54
|
+
const oldestKey = registry.keys().next().value;
|
|
55
|
+
if (oldestKey !== undefined && oldestKey !== key) registry.delete(oldestKey);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Test/teardown helper — resets all in-memory state. */
|
|
60
|
+
export function clearSurfacedRegistry(): void {
|
|
61
|
+
registry.clear();
|
|
62
|
+
}
|
package/src/server.ts
CHANGED
|
@@ -52,6 +52,7 @@ import type { FormationConfig, SearchHit, FormedMemory, FormationStage, Formatio
|
|
|
52
52
|
import { parseFormationTimeoutMs } from './server/formation-timeout.js';
|
|
53
53
|
import { withTimeout } from './timeout.js';
|
|
54
54
|
import { sanitizeCredentials } from './memory/secret-filter.js';
|
|
55
|
+
import { getSurfacedIds, recordSurfacedIds } from './search/surfaced-registry.js';
|
|
55
56
|
import {
|
|
56
57
|
createProjectBindingController,
|
|
57
58
|
type ProjectBindingController,
|
|
@@ -508,6 +509,12 @@ export async function createMemorixServer(
|
|
|
508
509
|
kind: 'consolidation',
|
|
509
510
|
dedupeKey: 'startup-consolidation',
|
|
510
511
|
});
|
|
512
|
+
lifecycle.enqueueLongTermMaintenance({
|
|
513
|
+
dataDir: projectDir,
|
|
514
|
+
projectId: project.id,
|
|
515
|
+
source: 'startup',
|
|
516
|
+
queue,
|
|
517
|
+
});
|
|
511
518
|
}
|
|
512
519
|
|
|
513
520
|
// This check is only a small SQLite metadata query. The actual directory
|
|
@@ -1152,7 +1159,7 @@ export async function createMemorixServer(
|
|
|
1152
1159
|
let longTermNote = '';
|
|
1153
1160
|
if (longTerm) {
|
|
1154
1161
|
try {
|
|
1155
|
-
const { promoteObservationToLongTermMemory } = await import('./memory/long-term.js');
|
|
1162
|
+
const { promoteObservationToLongTermMemory, maybeAutoQualifyLongTermMemory } = await import('./memory/long-term.js');
|
|
1156
1163
|
const promoted = await promoteObservationToLongTermMemory({
|
|
1157
1164
|
dataDir: projectDir,
|
|
1158
1165
|
observation: obs,
|
|
@@ -1166,7 +1173,28 @@ export async function createMemorixServer(
|
|
|
1166
1173
|
...(reader.isTeamMember ? { isTeamMember: true } : {}),
|
|
1167
1174
|
},
|
|
1168
1175
|
});
|
|
1169
|
-
|
|
1176
|
+
// An explicit store call carries its own source evidence: qualify
|
|
1177
|
+
// on the spot instead of waiting for a manual CLI review.
|
|
1178
|
+
const autoQualified = await maybeAutoQualifyLongTermMemory({
|
|
1179
|
+
dataDir: projectDir,
|
|
1180
|
+
id: promoted.memory.id,
|
|
1181
|
+
sourceDetail: 'explicit',
|
|
1182
|
+
});
|
|
1183
|
+
if (autoQualified) {
|
|
1184
|
+
// A fresh explicit record retires stale same-title records on the
|
|
1185
|
+
// durable rule leg instead of waiting for the next session start.
|
|
1186
|
+
try {
|
|
1187
|
+
const { enqueueLongTermMaintenance } = await import('./runtime/lifecycle.js');
|
|
1188
|
+
enqueueLongTermMaintenance({
|
|
1189
|
+
dataDir: projectDir,
|
|
1190
|
+
projectId: project.id,
|
|
1191
|
+
source: 'long-term:' + promoted.memory.id,
|
|
1192
|
+
});
|
|
1193
|
+
} catch { /* maintenance is optional; the record already qualified */ }
|
|
1194
|
+
}
|
|
1195
|
+
longTermNote = autoQualified
|
|
1196
|
+
? `\nLong-term memory: ${promoted.memory.id} (${promoted.memory.kind}/${promoted.memory.scope}, auto-qualified) and now enters briefs as durable context. Audit with \`memorix memory long-term list\`.`
|
|
1197
|
+
: `\nLong-term candidate: ${promoted.memory.id} (${promoted.memory.kind}/${promoted.memory.scope}). Review it with the CLI before it is delivered automatically.`;
|
|
1170
1198
|
} catch (longTermError) {
|
|
1171
1199
|
longTermNote = `\n[WARN] Observation stored, but long-term candidate was not created: ${longTermError instanceof Error ? longTermError.message : 'unknown error'}`;
|
|
1172
1200
|
}
|
|
@@ -1394,6 +1422,17 @@ export async function createMemorixServer(
|
|
|
1394
1422
|
const boundary = requireExplicitAutopilotExpansion('memorix_search', purpose);
|
|
1395
1423
|
if (boundary) return boundary;
|
|
1396
1424
|
}
|
|
1425
|
+
|
|
1426
|
+
// Session dedup scope: prefer the active session id, fall back to the
|
|
1427
|
+
// bound project. Best-effort bookkeeping — losing it only costs one
|
|
1428
|
+
// redundant result row, never data.
|
|
1429
|
+
let surfacedKey = `project:${project.id}`;
|
|
1430
|
+
try {
|
|
1431
|
+
const { getActiveSession } = await import('./memory/session.js');
|
|
1432
|
+
const active = await getActiveSession(projectDir, project.id);
|
|
1433
|
+
if (active) surfacedKey = `session:${active.id}`;
|
|
1434
|
+
} catch { /* fall back to the project key */ }
|
|
1435
|
+
|
|
1397
1436
|
return withFreshIndex(async () => {
|
|
1398
1437
|
|
|
1399
1438
|
const safeLimit = limit != null ? coerceNumber(limit, 20) : undefined;
|
|
@@ -1415,6 +1454,7 @@ export async function createMemorixServer(
|
|
|
1415
1454
|
source: source as 'agent' | 'git' | 'manual' | undefined,
|
|
1416
1455
|
quality: quality as 'fast' | 'balanced' | 'thorough',
|
|
1417
1456
|
reader: getObservationReader(scope === 'global' ? 'global' : 'project'),
|
|
1457
|
+
surfacedIds: getSurfacedIds(surfacedKey),
|
|
1418
1458
|
});
|
|
1419
1459
|
|
|
1420
1460
|
let result;
|
|
@@ -1436,6 +1476,10 @@ export async function createMemorixServer(
|
|
|
1436
1476
|
throw error;
|
|
1437
1477
|
}
|
|
1438
1478
|
|
|
1479
|
+
// These rows were just shown to the model — remember them so later
|
|
1480
|
+
// searches in the same session demote instead of re-showing.
|
|
1481
|
+
recordSurfacedIds(surfacedKey, result.entries.map(entry => entry.id));
|
|
1482
|
+
|
|
1439
1483
|
const activeBoundary = getActiveAutopilotRetrievalBoundary();
|
|
1440
1484
|
if (
|
|
1441
1485
|
scope !== 'global'
|
|
@@ -4998,6 +5042,12 @@ export async function createMemorixServer(
|
|
|
4998
5042
|
return undefined;
|
|
4999
5043
|
}
|
|
5000
5044
|
|
|
5045
|
+
// The advertised schema matches what the operator actually enabled: a
|
|
5046
|
+
// closed gate means the action does not exist for the agent, not that it
|
|
5047
|
+
// exists and throws. The handler keeps its own runtime checks as well.
|
|
5048
|
+
const mediaGenerationEnabled = process.env.MEMORIX_MCP_MEDIA_GENERATION === '1';
|
|
5049
|
+
const mediaTranscriptionEnabled = process.env.MEMORIX_MCP_MEDIA_TRANSCRIPTION === '1';
|
|
5050
|
+
const mediaGenerationOrTranscription = mediaGenerationEnabled || mediaTranscriptionEnabled;
|
|
5001
5051
|
server.registerTool(
|
|
5002
5052
|
'memorix_media',
|
|
5003
5053
|
{
|
|
@@ -5008,7 +5058,11 @@ export async function createMemorixServer(
|
|
|
5008
5058
|
'Use the CLI for destructive removal, quota cleanup, and direct generation. ' +
|
|
5009
5059
|
'MCP generation is disabled by default and requires MEMORIX_MCP_MEDIA_GENERATION=1 after the operator reviews provider billing.',
|
|
5010
5060
|
inputSchema: {
|
|
5011
|
-
action: z.enum([
|
|
5061
|
+
action: z.enum([
|
|
5062
|
+
'import', 'attach', 'list', 'show', 'derive-pdf', 'status', 'cancel',
|
|
5063
|
+
...(mediaTranscriptionEnabled ? ['derive-audio'] : []),
|
|
5064
|
+
...(mediaGenerationEnabled ? ['generate-image', 'generate-video'] : []),
|
|
5065
|
+
] as [string, ...string[]]),
|
|
5012
5066
|
path: z.string().optional().describe('Explicit local image/audio/video/PDF path for import.'),
|
|
5013
5067
|
assetId: z.string().optional().describe('Controlled MediaAsset ID for attach/show.'),
|
|
5014
5068
|
jobId: z.string().optional().describe('Durable media job ID for status.'),
|
|
@@ -5016,20 +5070,26 @@ export async function createMemorixServer(
|
|
|
5016
5070
|
limit: z.number().int().min(1).max(100).optional().describe('Maximum assets to list.'),
|
|
5017
5071
|
title: z.string().optional().describe('Observation title when attaching generated/imported output.'),
|
|
5018
5072
|
narrative: z.string().optional().describe('Short retrieval text when attaching an asset.'),
|
|
5019
|
-
prompt: z.string().optional().describe('Explicit MiniMax generation prompt.'),
|
|
5020
|
-
image: z.string().optional().describe('Base64-encoded reference image for image-to-image generation.'),
|
|
5021
|
-
model: z.string().max(160).optional().describe('Provider model for the selected media action.'),
|
|
5022
|
-
provider: z.enum(['openai', 'groq']).optional().describe('Explicit audio transcription provider.'),
|
|
5023
|
-
language: z.string().max(32).optional().describe('Optional ISO-639-1 hint for audio transcription.'),
|
|
5024
|
-
region: z.enum(['global', 'cn']).optional().describe('MiniMax deployment region.'),
|
|
5025
|
-
n: z.number().int().min(1).max(4).optional().describe('Image output count.'),
|
|
5026
|
-
ratio: z.enum(['adaptive', '1:1', '16:9', '4:3', '3:2', '2:3', '3:4', '9:16', '21:9']).optional().describe('Image or video aspect ratio.'),
|
|
5027
|
-
width: z.number().int().min(1).max(8192).optional().describe('Requested image width.'),
|
|
5028
|
-
height: z.number().int().min(1).max(8192).optional().describe('Requested image height.'),
|
|
5029
|
-
duration: z.union([z.literal(5), z.literal(10)]).optional().describe('MiniMax video duration in seconds.'),
|
|
5030
5073
|
maxPages: z.number().int().min(1).max(100).optional().describe('Bounded PDF extraction page limit.'),
|
|
5031
5074
|
maxChars: z.number().int().min(1).max(60_000).optional().describe('Bounded PDF extraction character limit.'),
|
|
5032
5075
|
attach: z.boolean().optional().describe('Attach generated/imported output to normal project memory explicitly.'),
|
|
5076
|
+
...(mediaGenerationOrTranscription ? {
|
|
5077
|
+
model: z.string().max(160).optional().describe('Provider model for the selected media action.'),
|
|
5078
|
+
prompt: z.string().optional().describe('Explicit prompt for generation or transcription.'),
|
|
5079
|
+
} : {}),
|
|
5080
|
+
...(mediaTranscriptionEnabled ? {
|
|
5081
|
+
provider: z.enum(['openai', 'groq']).optional().describe('Explicit audio transcription provider.'),
|
|
5082
|
+
language: z.string().max(32).optional().describe('Optional ISO-639-1 hint for audio transcription.'),
|
|
5083
|
+
} : {}),
|
|
5084
|
+
...(mediaGenerationEnabled ? {
|
|
5085
|
+
image: z.string().optional().describe('Base64-encoded reference image for image-to-image generation.'),
|
|
5086
|
+
region: z.enum(['global', 'cn']).optional().describe('MiniMax deployment region.'),
|
|
5087
|
+
n: z.number().int().min(1).max(4).optional().describe('Image output count.'),
|
|
5088
|
+
ratio: z.enum(['adaptive', '1:1', '16:9', '4:3', '3:2', '2:3', '3:4', '9:16', '21:9']).optional().describe('Image or video aspect ratio.'),
|
|
5089
|
+
width: z.number().int().min(1).max(8192).optional().describe('Requested image width.'),
|
|
5090
|
+
height: z.number().int().min(1).max(8192).optional().describe('Requested image height.'),
|
|
5091
|
+
duration: z.union([z.literal(5), z.literal(10)]).optional().describe('MiniMax video duration in seconds.'),
|
|
5092
|
+
} : {}),
|
|
5033
5093
|
},
|
|
5034
5094
|
},
|
|
5035
5095
|
async ({ action, path: assetPath, assetId, jobId, kind, limit, title, narrative, prompt, image, model, provider, language, region, n, ratio, width, height, duration, maxPages, maxChars, attach }) => {
|
package/src/store/orama-store.ts
CHANGED
|
@@ -587,7 +587,9 @@ export async function searchObservations(options: SearchOptions): Promise<IndexE
|
|
|
587
587
|
|
|
588
588
|
// Orama's vector/hybrid search can leak cross-project hits even when `where`
|
|
589
589
|
// is present, so always keep enough headroom for a deterministic post-filter.
|
|
590
|
-
|
|
590
|
+
// Session dedup demotes already-surfaced entries after ranking, so it needs
|
|
591
|
+
// the same headroom to let unseen candidates rise into the final limit.
|
|
592
|
+
const requestLimit = projectIds || (options.surfacedIds && options.surfacedIds.length > 0)
|
|
591
593
|
? (options.limit ?? 20) * 3
|
|
592
594
|
: (options.limit ?? 20);
|
|
593
595
|
|
|
@@ -902,6 +904,20 @@ export async function searchObservations(options: SearchOptions): Promise<IndexE
|
|
|
902
904
|
intermediate.sort((a, b) => b.score - a.score);
|
|
903
905
|
}
|
|
904
906
|
|
|
907
|
+
// ── Session dedup (demote, never remove) ────────────────────────
|
|
908
|
+
// Entries already surfaced earlier in the same session are demoted so
|
|
909
|
+
// unseen evidence can rise into the final limit. A demoted entry is never
|
|
910
|
+
// removed: when it is the strong match for a new question, it still wins
|
|
911
|
+
// on score and answers that turn.
|
|
912
|
+
if (options.surfacedIds && options.surfacedIds.length > 0) {
|
|
913
|
+
const SURFACED_PENALTY = 0.5;
|
|
914
|
+
const surfaced = new Set(options.surfacedIds);
|
|
915
|
+
intermediate = intermediate.map(entry =>
|
|
916
|
+
surfaced.has(entry.id) ? { ...entry, score: (entry.score ?? 0) * SURFACED_PENALTY } : entry,
|
|
917
|
+
);
|
|
918
|
+
intermediate.sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
|
|
919
|
+
}
|
|
920
|
+
|
|
905
921
|
// Temporal filtering: since/until date range
|
|
906
922
|
if (options.since) {
|
|
907
923
|
const sinceDate = new Date(options.since).getTime();
|
package/src/types.ts
CHANGED
|
@@ -317,6 +317,12 @@ export interface SearchOptions {
|
|
|
317
317
|
trackAccess?: boolean;
|
|
318
318
|
/** Internal reader context for agent-facing visibility filtering. */
|
|
319
319
|
reader?: ObservationReader;
|
|
320
|
+
/**
|
|
321
|
+
* Observation ids already surfaced earlier in the same session. Surfaced
|
|
322
|
+
* entries are demoted (never removed) so unseen evidence can rise into the
|
|
323
|
+
* result limit; a strong match still answers a new question.
|
|
324
|
+
*/
|
|
325
|
+
surfacedIds?: number[];
|
|
320
326
|
/**
|
|
321
327
|
* `fast` stays fully local, `balanced` (default) permits embeddings, and
|
|
322
328
|
* `thorough` explicitly permits optional LLM query rewrite and reranking.
|
|
@@ -654,7 +660,7 @@ export interface MemoryRef {
|
|
|
654
660
|
/** MCP config format adapter interface */
|
|
655
661
|
export interface MCPConfigAdapter {
|
|
656
662
|
/** Setup-only adapters may support a host that does not participate in workspace sync. */
|
|
657
|
-
readonly source: AgentTarget | 'codebuddy';
|
|
663
|
+
readonly source: AgentTarget | 'codebuddy' | 'dsh';
|
|
658
664
|
/** Parse MCP server entries from a config file */
|
|
659
665
|
parse(content: string): MCPServerEntry[];
|
|
660
666
|
/** Generate config file content from MCP server entries */
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
|
4
|
+
import type { MCPConfigAdapter, MCPServerEntry } from '../../types.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* DeepSeek Harness (DSH) MCP Configuration Adapter.
|
|
8
|
+
*
|
|
9
|
+
* DSH composes its runtime from Cordis patch layers. External MCP servers are
|
|
10
|
+
* registered as rows of the `@deepseek-ai/dsh-mcp-client` plugin inside a
|
|
11
|
+
* patch list. The user-level layer that applies to every profile lives at:
|
|
12
|
+
*
|
|
13
|
+
* $DSH_HOME/cordis.patch.yml (default: ~/.dsh/cordis.patch.yml)
|
|
14
|
+
*
|
|
15
|
+
* Row shape follows DSH's own shipped Memorix reference
|
|
16
|
+
* (examples/mcp-memory/memorix.cordis.yml in deepseek-ai/deepseek-harness):
|
|
17
|
+
*
|
|
18
|
+
* - insert:
|
|
19
|
+
* - id: memory-memorix
|
|
20
|
+
* name: '@deepseek-ai/dsh-mcp-client'
|
|
21
|
+
* config:
|
|
22
|
+
* serverName: memorix
|
|
23
|
+
* transport: stdio
|
|
24
|
+
* command: memorix
|
|
25
|
+
* args: [serve]
|
|
26
|
+
*
|
|
27
|
+
* The `cwd` key is deliberately omitted so the MCP child inherits the DSH
|
|
28
|
+
* process working directory — the workspace DSH was launched from — which is
|
|
29
|
+
* what keeps Memorix bound to the current Git project.
|
|
30
|
+
*
|
|
31
|
+
* Source: https://github.com/deepseek-ai/deepseek-harness/tree/master/packages/mcp/mcp-client
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
const MCP_CLIENT_PLUGIN = '@deepseek-ai/dsh-mcp-client';
|
|
35
|
+
const MEMORIX_ROW_ID = 'memory-memorix';
|
|
36
|
+
|
|
37
|
+
function resolveDshHome(): string {
|
|
38
|
+
const configured = process.env.DSH_HOME?.trim();
|
|
39
|
+
if (configured) return configured;
|
|
40
|
+
return join(homedir(), '.dsh');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isMemorixRow(row: unknown): row is Record<string, unknown> {
|
|
44
|
+
if (!row || typeof row !== 'object' || Array.isArray(row)) return false;
|
|
45
|
+
const record = row as Record<string, unknown>;
|
|
46
|
+
if (record.name !== MCP_CLIENT_PLUGIN) return false;
|
|
47
|
+
const config = record.config;
|
|
48
|
+
return Boolean(
|
|
49
|
+
config && typeof config === 'object' && !Array.isArray(config)
|
|
50
|
+
&& (config as Record<string, unknown>).serverName === 'memorix',
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function rowToEntry(row: Record<string, unknown>): MCPServerEntry | null {
|
|
55
|
+
const config = row.config as Record<string, unknown>;
|
|
56
|
+
if (!config || typeof config !== 'object') return null;
|
|
57
|
+
const entry: MCPServerEntry = {
|
|
58
|
+
name: 'memorix',
|
|
59
|
+
command: typeof config.command === 'string' ? config.command : '',
|
|
60
|
+
args: Array.isArray(config.args) ? config.args.filter((arg): arg is string => typeof arg === 'string') : [],
|
|
61
|
+
};
|
|
62
|
+
if (typeof config.url === 'string' && config.url) entry.url = config.url;
|
|
63
|
+
if (config.env && typeof config.env === 'object' && !Array.isArray(config.env)) {
|
|
64
|
+
const env: Record<string, string> = {};
|
|
65
|
+
for (const [key, value] of Object.entries(config.env as Record<string, unknown>)) {
|
|
66
|
+
if (typeof value === 'string') env[key] = value;
|
|
67
|
+
}
|
|
68
|
+
if (Object.keys(env).length > 0) entry.env = env;
|
|
69
|
+
}
|
|
70
|
+
if (config.disabled === true) entry.disabled = true;
|
|
71
|
+
return entry;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Flatten one parsed patch document into its row list, tolerating both
|
|
75
|
+
* `- insert: [...]` operations and bare row lists. */
|
|
76
|
+
function patchRows(document: unknown): Record<string, unknown>[] {
|
|
77
|
+
const rows: Record<string, unknown>[] = [];
|
|
78
|
+
if (!Array.isArray(document)) return rows;
|
|
79
|
+
for (const item of document) {
|
|
80
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) continue;
|
|
81
|
+
const record = item as Record<string, unknown>;
|
|
82
|
+
if (Array.isArray(record.insert)) {
|
|
83
|
+
for (const row of record.insert) {
|
|
84
|
+
if (row && typeof row === 'object' && !Array.isArray(row)) {
|
|
85
|
+
rows.push(row as Record<string, unknown>);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
rows.push(record);
|
|
91
|
+
}
|
|
92
|
+
return rows;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export class DshMCPAdapter implements MCPConfigAdapter {
|
|
96
|
+
readonly source = 'dsh' as const;
|
|
97
|
+
|
|
98
|
+
parse(content: string): MCPServerEntry[] {
|
|
99
|
+
if (!content.trim()) return [];
|
|
100
|
+
let document: unknown;
|
|
101
|
+
try {
|
|
102
|
+
document = parseYaml(content);
|
|
103
|
+
} catch {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
return patchRows(document)
|
|
107
|
+
.filter(isMemorixRow)
|
|
108
|
+
.map(rowToEntry)
|
|
109
|
+
.filter((entry): entry is MCPServerEntry => entry !== null);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
generate(servers: MCPServerEntry[]): string {
|
|
113
|
+
const rows = servers
|
|
114
|
+
.filter((server) => server.name === 'memorix')
|
|
115
|
+
.map((server) => {
|
|
116
|
+
const config: Record<string, unknown> = {
|
|
117
|
+
serverName: server.name,
|
|
118
|
+
};
|
|
119
|
+
if (server.url) {
|
|
120
|
+
config.transport = 'streamable-http';
|
|
121
|
+
config.url = server.url;
|
|
122
|
+
if (server.headers && Object.keys(server.headers).length > 0) config.headers = server.headers;
|
|
123
|
+
} else {
|
|
124
|
+
config.transport = 'stdio';
|
|
125
|
+
config.command = server.command;
|
|
126
|
+
if (server.args && server.args.length > 0) config.args = server.args;
|
|
127
|
+
if (server.env && Object.keys(server.env).length > 0) config.env = server.env;
|
|
128
|
+
}
|
|
129
|
+
const row: Record<string, unknown> = {
|
|
130
|
+
id: MEMORIX_ROW_ID,
|
|
131
|
+
name: MCP_CLIENT_PLUGIN,
|
|
132
|
+
config,
|
|
133
|
+
};
|
|
134
|
+
if (server.disabled === true) row.disabled = true;
|
|
135
|
+
return row;
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
if (rows.length === 0) return '';
|
|
139
|
+
return stringifyYaml([{ insert: rows }], { lineWidth: 0 }) + '\n';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
getConfigPath(_projectRoot?: string): string {
|
|
143
|
+
return join(resolveDshHome(), 'cordis.patch.yml');
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export { MEMORIX_ROW_ID };
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CLI Command: memorix audit list
|
|
3
|
-
*
|
|
4
|
-
* List all files written by Memorix (audit trail).
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { defineCommand } from 'citty';
|
|
8
|
-
import * as p from '@clack/prompts';
|
|
9
|
-
|
|
10
|
-
export default defineCommand({
|
|
11
|
-
meta: {
|
|
12
|
-
name: 'list',
|
|
13
|
-
description: 'List Memorix-written files (audit trail)',
|
|
14
|
-
},
|
|
15
|
-
args: {
|
|
16
|
-
project: {
|
|
17
|
-
type: 'string',
|
|
18
|
-
description: 'Filter by project (optional)',
|
|
19
|
-
required: false,
|
|
20
|
-
},
|
|
21
|
-
},
|
|
22
|
-
run: async ({ args }) => {
|
|
23
|
-
const { getAllAuditEntries, getProjectId } = await import('../../audit/index.js');
|
|
24
|
-
const os = await import('node:os');
|
|
25
|
-
let cwd: string;
|
|
26
|
-
try {
|
|
27
|
-
cwd = process.cwd();
|
|
28
|
-
} catch {
|
|
29
|
-
cwd = os.homedir();
|
|
30
|
-
console.log(`[WARN] Could not access current directory, using home: ${cwd}`);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
p.intro('Memorix Audit Trail');
|
|
34
|
-
|
|
35
|
-
const allEntries = await getAllAuditEntries();
|
|
36
|
-
|
|
37
|
-
if (allEntries.length === 0) {
|
|
38
|
-
console.log('No Memorix-written files found.');
|
|
39
|
-
p.outro('Audit trail is empty.');
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Filter by project if specified
|
|
44
|
-
let entries = allEntries;
|
|
45
|
-
if (args.project) {
|
|
46
|
-
const targetProjectId = getProjectId(args.project);
|
|
47
|
-
entries = allEntries.filter((e) => e.projectId === targetProjectId);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (entries.length === 0) {
|
|
51
|
-
console.log('No Memorix-written files found for this project.');
|
|
52
|
-
p.outro('Audit trail is empty for this project.');
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Group by project
|
|
57
|
-
const byProject: Record<string, Array<{ entry: import('../../audit/index.js').AuditEntry }>> = {};
|
|
58
|
-
for (const { projectId, entry } of entries) {
|
|
59
|
-
if (!byProject[projectId]) {
|
|
60
|
-
byProject[projectId] = [];
|
|
61
|
-
}
|
|
62
|
-
byProject[projectId].push({ entry });
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Display by project
|
|
66
|
-
console.log('');
|
|
67
|
-
for (const [projectId, items] of Object.entries(byProject)) {
|
|
68
|
-
console.log(`Project: ${projectId}`);
|
|
69
|
-
console.log('');
|
|
70
|
-
|
|
71
|
-
for (const { entry } of items) {
|
|
72
|
-
const icon = entry.type === 'hook' ? '[HOOK]' : '[TASK]';
|
|
73
|
-
const agent = entry.agent ? ` (${entry.agent})` : '';
|
|
74
|
-
console.log(`${icon} ${entry.path}${agent}`);
|
|
75
|
-
console.log(` Created: ${entry.createdAt}`);
|
|
76
|
-
console.log('');
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Summary
|
|
81
|
-
const hookCount = entries.filter((e) => e.entry.type === 'hook').length;
|
|
82
|
-
const ruleCount = entries.filter((e) => e.entry.type === 'rule').length;
|
|
83
|
-
const projectCount = new Set(entries.map((e) => e.projectId)).size;
|
|
84
|
-
|
|
85
|
-
p.note(`${projectCount} projects, ${hookCount} hooks, ${ruleCount} rules`);
|
|
86
|
-
|
|
87
|
-
p.outro('Audit trail complete.');
|
|
88
|
-
},
|
|
89
|
-
});
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CLI Command: memorix purge all-memory
|
|
3
|
-
*
|
|
4
|
-
* Delete ALL memories (dangerous operation).
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { defineCommand } from 'citty';
|
|
8
|
-
import * as p from '@clack/prompts';
|
|
9
|
-
|
|
10
|
-
export default defineCommand({
|
|
11
|
-
meta: {
|
|
12
|
-
name: 'all-memory',
|
|
13
|
-
description: 'Delete ALL memories (dangerous)',
|
|
14
|
-
},
|
|
15
|
-
args: {},
|
|
16
|
-
run: async ({}) => {
|
|
17
|
-
p.intro('[WARN] Purge All Memory');
|
|
18
|
-
|
|
19
|
-
// Show warning
|
|
20
|
-
console.log('');
|
|
21
|
-
console.log('[WARN] WARNING: This will delete ALL memories!');
|
|
22
|
-
console.log('');
|
|
23
|
-
console.log('This will remove:');
|
|
24
|
-
console.log(' - All observations from all projects');
|
|
25
|
-
console.log(' - All entities and relations');
|
|
26
|
-
console.log(' - All embeddings');
|
|
27
|
-
console.log('');
|
|
28
|
-
console.log('This action CANNOT be undone.');
|
|
29
|
-
console.log('');
|
|
30
|
-
console.log('Project artifacts (hook files) will be preserved.');
|
|
31
|
-
console.log('');
|
|
32
|
-
|
|
33
|
-
// First confirmation
|
|
34
|
-
const confirmed1 = await p.confirm({
|
|
35
|
-
message: 'Are you sure you want to delete ALL memories?',
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
if (p.isCancel(confirmed1) || !confirmed1) {
|
|
39
|
-
p.outro('Purge cancelled.');
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Second confirmation (type "DELETE" to confirm)
|
|
44
|
-
const confirmed2 = await p.text({
|
|
45
|
-
message: 'Type "DELETE" to confirm:',
|
|
46
|
-
validate: (value) => {
|
|
47
|
-
if (value !== 'DELETE') {
|
|
48
|
-
return 'Please type "DELETE" to confirm';
|
|
49
|
-
}
|
|
50
|
-
return undefined;
|
|
51
|
-
},
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
if (p.isCancel(confirmed2) || confirmed2 !== 'DELETE') {
|
|
55
|
-
p.outro('Purge cancelled.');
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Delete all memories
|
|
60
|
-
try {
|
|
61
|
-
const {
|
|
62
|
-
getAllObservations,
|
|
63
|
-
resolveObservations,
|
|
64
|
-
} = await import('../../memory/observations.js');
|
|
65
|
-
const { withFreshIndex } = await import('../../memory/freshness.js');
|
|
66
|
-
const observations = await withFreshIndex(() => getAllObservations());
|
|
67
|
-
const ids = observations.map((o) => o.id);
|
|
68
|
-
|
|
69
|
-
if (ids.length === 0) {
|
|
70
|
-
console.log('No observations found.');
|
|
71
|
-
p.outro('No memories to purge.');
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const result = await resolveObservations(ids, 'archived');
|
|
76
|
-
|
|
77
|
-
console.log(`[OK] Archived ${result.resolved.length} observations`);
|
|
78
|
-
|
|
79
|
-
p.outro(`All memory purged. ${result.resolved.length} observations archived.`);
|
|
80
|
-
} catch (err) {
|
|
81
|
-
console.error(`[ERROR] Failed to purge all memory: ${err}`);
|
|
82
|
-
p.outro('Purge failed.');
|
|
83
|
-
}
|
|
84
|
-
},
|
|
85
|
-
});
|