memorix 1.6.3 → 1.7.1

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.
@@ -497,8 +497,11 @@ It provides an operational view for:
497
497
  - identity health
498
498
  - sessions
499
499
  - retention state
500
+ - preview-first cleanup, consolidation, intelligent deduplication, and retention archival
500
501
  - read-only orchestration coordination state in the standalone dashboard and live state in HTTP service mode
501
502
 
503
+ Dashboard maintenance is not a separate business-logic implementation. CLI, MCP, background jobs, and Dashboard routes share the same cleanup, consolidation, deduplication, and retention functions. Mutation routes issue a project-bound signed preview token and reject execution when the candidate set changed after preview.
504
+
502
505
  This is part of Memorix's shift from a single MCP server to a broader local memory platform.
503
506
 
504
507
  ---
@@ -0,0 +1,54 @@
1
+ # Dashboard
2
+
3
+ The Memorix Dashboard is the local control surface for inspecting project memory and running explicit maintenance. It is available through the shared background service or as a standalone local process.
4
+
5
+ ## Start
6
+
7
+ ```bash
8
+ memorix background start
9
+ # dashboard: http://127.0.0.1:3211
10
+
11
+ memorix dashboard
12
+ # standalone dashboard: http://127.0.0.1:3210
13
+ ```
14
+
15
+ Both modes read the same project-scoped stores. The project switcher changes the active project filter; it does not merge projects or grant access to personal memories.
16
+
17
+ ## Maintenance Workbench
18
+
19
+ The Observations page exposes three explicit maintenance actions:
20
+
21
+ | Action | Effect | Shared implementation |
22
+ | --- | --- | --- |
23
+ | Cleanup | Deletes low-quality activity records and exact duplicates; optionally archives test/demo noise | `analyzeCleanupObservations` and `applyCleanupMutations` |
24
+ | Consolidate | Merges strongly similar project-visible observations while preserving facts and references | `findConsolidationCandidates` and `executeConsolidation` |
25
+ | Deduplicate | Uses the configured memory LLM to identify redundant or superseded records and marks them resolved | `planMemoryDeduplication` and `applyDeduplicationPlan` |
26
+
27
+ The Retention page exposes Archive for records already classified as archive candidates by the canonical retention projection.
28
+
29
+ Every action follows the same contract:
30
+
31
+ 1. The Dashboard requests a project-scoped preview.
32
+ 2. The server returns the candidate records, counts, and a signed preview token.
33
+ 3. The user confirms the displayed plan.
34
+ 4. Execution verifies the token and current candidate set before mutating data.
35
+ 5. If memory changed after preview, execution fails with `409` and requires a new preview.
36
+
37
+ Cleanup noise archival is opt-in. Consolidation never touches personal or team-scoped observations from an unbound Dashboard. Deduplication marks records resolved rather than deleting them. Retention changes status to archived. Manual selection and hard deletion remain separate from the maintenance workbench.
38
+
39
+ ## HTTP Contract
40
+
41
+ Maintenance endpoints are local Dashboard APIs and are not a replacement for MCP or CLI integrations:
42
+
43
+ ```text
44
+ POST /api/maintenance/cleanup/preview
45
+ POST /api/maintenance/cleanup/execute
46
+ POST /api/maintenance/consolidate/preview
47
+ POST /api/maintenance/consolidate/execute
48
+ POST /api/maintenance/deduplicate/preview
49
+ POST /api/maintenance/deduplicate/execute
50
+ POST /api/maintenance/retention/preview
51
+ POST /api/maintenance/retention/execute
52
+ ```
53
+
54
+ Use the CLI for scripts and remote terminals. Use MCP for agent-driven maintenance. The Dashboard is the reviewable local operator surface; all three call the same maintenance logic.
package/docs/README.md CHANGED
@@ -33,6 +33,7 @@ The public docs are organized by user intent:
33
33
  | CLI commands and MCP tools | [API_REFERENCE.md](API_REFERENCE.md) |
34
34
  | Memory Autopilot, Code State, and context packs | [API_REFERENCE.md § Memory Autopilot, Code State, and Context Packs](API_REFERENCE.md#memory-autopilot-code-state-and-context-packs), [1.2 Code State](1.2.0-CODE-STATE.md), and [1.2 Workset Retrieval](1.2.0-WORKSET-RETRIEVAL.md) |
35
35
  | Reviewed episodic, semantic, and procedural long-term memory | [1.3 Memory Architecture](1.3-MEMORY-ARCHITECTURE.md) |
36
+ | Browse memory and run preview-first local maintenance | [Dashboard](DASHBOARD.md) |
36
37
  | Reviewable knowledge pages and workflow inheritance | [API_REFERENCE.md § Knowledge Workspace and Workflows](API_REFERENCE.md#knowledge-workspace-and-workflows), [1.2 Knowledge Workspace](1.2.0-KNOWLEDGE-WORKSPACE.md), and [1.2 Workflow Inheritance](1.2.0-WORKFLOW-INHERITANCE.md) |
37
38
  | Git-derived engineering memory | [GIT_MEMORY.md](GIT_MEMORY.md) |
38
39
  | Memory formation and quality pipeline | [MEMORY_FORMATION_PIPELINE.md](MEMORY_FORMATION_PIPELINE.md) |
@@ -51,6 +52,7 @@ The public docs are organized by user intent:
51
52
  | TOML-first configuration | [CONFIGURATION.md](CONFIGURATION.md) |
52
53
  | Docker/compose deployment | [DOCKER.md](DOCKER.md) |
53
54
  | Resource and timeout tuning | [PERFORMANCE.md](PERFORMANCE.md) |
55
+ | Dashboard modes, maintenance actions, and safety contract | [DASHBOARD.md](DASHBOARD.md) |
54
56
  | AI-facing install and troubleshooting playbook | [Agent Playbook](AGENT_OPERATOR_PLAYBOOK.md) |
55
57
 
56
58
  ---
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "memorix",
3
3
  "mcpName": "io.github.AVIDS2/memorix",
4
- "version": "1.6.3",
4
+ "version": "1.7.1",
5
5
  "description": "Local-first shared memory layer for AI coding agents across MCP clients, Git history, reasoning context, and project sessions.",
6
6
  "type": "module",
7
7
  "sideEffects": false,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://anthropic.com/claude-code/plugin.schema.json",
3
3
  "name": "memorix",
4
- "version": "1.6.3",
4
+ "version": "1.7.1",
5
5
  "description": "Shared workspace memory for Claude Code and other AI coding agents.",
6
6
  "author": {
7
7
  "name": "AVIDS2",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix",
3
- "version": "1.6.3",
3
+ "version": "1.7.1",
4
4
  "description": "Local-first project memory for CodeBuddy Code and other coding agents.",
5
5
  "author": {
6
6
  "name": "AVIDS2",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix",
3
- "version": "1.6.3",
3
+ "version": "1.7.1",
4
4
  "description": "Shared workspace memory for Codex and other AI coding agents.",
5
5
  "author": {
6
6
  "name": "AVIDS2",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix",
3
- "version": "1.6.3",
3
+ "version": "1.7.1",
4
4
  "description": "Shared local workspace memory for GitHub Copilot CLI and other AI coding agents.",
5
5
  "author": {
6
6
  "name": "AVIDS2",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix",
3
- "version": "1.6.3",
3
+ "version": "1.7.1",
4
4
  "mcpServers": {
5
5
  "memorix": {
6
6
  "command": "memorix",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix-omp-package",
3
- "version": "1.6.3",
3
+ "version": "1.7.1",
4
4
  "description": "Memorix shared workspace memory package for Oh-my-Pi.",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix",
3
- "version": "1.6.3",
3
+ "version": "1.7.1",
4
4
  "description": "Shared workspace memory for OpenClaw and other AI coding agents.",
5
5
  "author": {
6
6
  "name": "AVIDS2",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix-pi-package",
3
- "version": "1.6.3",
3
+ "version": "1.7.1",
4
4
  "description": "Memorix shared workspace memory package for Pi coding agent.",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
package/server.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "url": "https://github.com/AVIDS2/memorix",
8
8
  "source": "github"
9
9
  },
10
- "version": "1.6.3",
10
+ "version": "1.7.1",
11
11
  "websiteUrl": "https://github.com/AVIDS2/memorix#readme",
12
12
  "icons": [
13
13
  {
@@ -22,7 +22,7 @@
22
22
  {
23
23
  "registryType": "npm",
24
24
  "identifier": "memorix",
25
- "version": "1.6.3",
25
+ "version": "1.7.1",
26
26
  "runtimeHint": "npx",
27
27
  "packageArguments": [
28
28
  {
@@ -17,88 +17,15 @@ import { defineCommand } from 'citty';
17
17
  import type { Observation } from '../../types.js';
18
18
  import { detectProject } from '../../project/detector.js';
19
19
  import { getProjectDataDir } from '../../store/persistence.js';
20
- import type { ObservationStore } from '../../store/obs-store.js';
21
20
  import { getObservationStore, initObservationStore } from '../../store/obs-store.js';
22
21
  import { filterReadableObservations } from '../../memory/visibility.js';
22
+ import {
23
+ analyzeCleanupObservations,
24
+ applyCleanupMutations,
25
+ isLowQualityObservation,
26
+ } from '../../memory/cleanup.js';
23
27
 
24
- /** Patterns that indicate auto-generated, low-value observations */
25
- const LOW_QUALITY_PATTERNS = [
26
- /^Session activity/i,
27
- /^Updated \S+\.\w+$/i,
28
- /^Created \S+\.\w+$/i,
29
- /^Deleted \S+\.\w+$/i,
30
- /^Modified \S+\.\w+$/i,
31
- /^Ran command:/i,
32
- /^Read file:/i,
33
- ];
34
-
35
- /** Patterns for demo/test noise — matches session.ts NOISE_PATTERNS */
36
- const NOISE_PATTERNS = [
37
- /\[测试\]/i, /\[test\]/i, /验证/i, /兼容/i, /\bcompat(?:ibility)?\b/i,
38
- /\bdemo\b/i, /展示/i, /全能力/i, /handoff/i, /交接/i,
39
- /for_memmcp_test/i, /\bbenchmark\b/i, /\bsandbox\b/i, /\bplayground\b/i,
40
- ];
41
-
42
- /** Patterns for Memorix system self-reference — should not pollute unrelated projects */
43
- const SYSTEM_SELF_PATTERNS = [
44
- /memorix.demo/i, /memorix.*全能力/i, /memorix.*工具.*能力/i,
45
- /memorix.*runtime.*mode/i, /memorix.*运行模式/i, /memorix.*control.plane/i,
46
- /session.*inject(?:ion)?/i, /注入.*逻辑/i,
47
- /\b22\s*(?:个|tools?).*(?:工具|能力|capabilit)/i,
48
- /memorix.*(?:v\d|版本|version)/i, /memorix.*(?:兼容|compat)/i,
49
- /memorix.*(?:测试|test)/i, /memmcp/i,
50
- ];
51
-
52
- /** Check if an observation title matches low-quality patterns */
53
- function isLowQuality(title: string): boolean {
54
- return LOW_QUALITY_PATTERNS.some(p => p.test(title.trim()));
55
- }
56
-
57
- /** Check if observation text matches noise/demo/test/system-self patterns */
58
- function isNoisePollution(obs: { title?: string; narrative?: string; entityName?: string; facts?: string[]; concepts?: string[] }): { isNoise: boolean; reason: string } {
59
- const text = [obs.title, obs.narrative, obs.entityName, ...(obs.facts ?? []), ...(obs.concepts ?? [])]
60
- .filter(Boolean).join('\n');
61
- for (const p of SYSTEM_SELF_PATTERNS) {
62
- if (p.test(text)) return { isNoise: true, reason: 'system-self' };
63
- }
64
- for (const p of NOISE_PATTERNS) {
65
- if (p.test(text)) return { isNoise: true, reason: 'demo/test/noise' };
66
- }
67
- return { isNoise: false, reason: '' };
68
- }
69
-
70
- function requireObservationIds(observations: Observation[], action: string): number[] {
71
- const ids = observations.map((observation) => observation.id);
72
- if (ids.some((id) => typeof id !== 'number')) {
73
- throw new Error(`Cannot ${action}: an observation has no persisted ID.`);
74
- }
75
- return ids as number[];
76
- }
77
-
78
- /**
79
- * Apply cleanup mutations without replacing the shared observation table.
80
- * Keeping lifecycle updates targeted prevents a cleanup in one project from
81
- * overwriting observations written concurrently by another project.
82
- */
83
- export async function applyCleanupMutations(
84
- store: ObservationStore,
85
- toArchive: Observation[],
86
- toRemove: Observation[],
87
- ): Promise<{ archived: number; removed: number }> {
88
- const archiveIds = requireObservationIds(toArchive, 'archive');
89
- const removeIds = requireObservationIds(toRemove, 'delete');
90
- const removals = new Set(removeIds);
91
- if (archiveIds.some((id) => removals.has(id))) {
92
- throw new Error('Cleanup cannot archive and delete the same observation.');
93
- }
94
-
95
- await store.atomic(async (tx) => {
96
- await Promise.all(archiveIds.map((id) => tx.setStatus(id, 'archived')));
97
- await Promise.all(removeIds.map((id) => tx.remove(id)));
98
- });
99
-
100
- return { archived: archiveIds.length, removed: removeIds.length };
101
- }
28
+ export { applyCleanupMutations } from '../../memory/cleanup.js';
102
29
 
103
30
  export default defineCommand({
104
31
  meta: {
@@ -170,40 +97,12 @@ export default defineCommand({
170
97
  return;
171
98
  }
172
99
 
173
- // Categorize: low-quality
174
- const lowQuality = projectObs.filter(o => isLowQuality(o.title ?? ''));
175
- const highQuality = projectObs.filter(o => !isLowQuality(o.title ?? ''));
176
-
177
- // Find duplicates (same title + type + entity)
178
- const seen = new Set<string>();
179
- const duplicates: typeof projectObs = [];
180
- const unique: typeof projectObs = [];
181
- for (const obs of highQuality) {
182
- const key = `${obs.type}|${obs.title}|${obs.entityName}`;
183
- if (seen.has(key)) {
184
- duplicates.push(obs);
185
- } else {
186
- seen.add(key);
187
- unique.push(obs);
188
- }
189
- }
190
-
191
- // Find noise pollution (demo/test/system-self)
192
- const noiseHits: Array<{ obs: typeof projectObs[0]; reason: string }> = [];
193
- if (args.noise) {
194
- for (const obs of projectObs) {
195
- if (lowQuality.includes(obs) || duplicates.includes(obs)) continue;
196
- const { isNoise, reason } = isNoisePollution(obs);
197
- if (isNoise) noiseHits.push({ obs, reason });
198
- }
199
- }
200
-
201
- const toRemove = [...lowQuality, ...duplicates];
202
- const toArchive = noiseHits.map(h => h.obs);
100
+ const analysis = analyzeCleanupObservations(projectObs as Observation[], { includeNoise: args.noise });
101
+ const { lowQuality, duplicates, noise: noiseHits, toRemove, toArchive } = analysis;
203
102
 
204
103
  console.log(`Analysis (active observations for ${projectId}):`);
205
- console.log(` Total active: ${projectObs.length}`);
206
- console.log(` High quality: ${unique.length - toArchive.length}`);
104
+ console.log(` Total active: ${analysis.totalActive}`);
105
+ console.log(` High quality: ${analysis.highQuality}`);
207
106
  console.log(` Low quality: ${lowQuality.length}`);
208
107
  console.log(` Duplicates: ${duplicates.length}`);
209
108
  if (args.noise) {
@@ -224,7 +123,7 @@ export default defineCommand({
224
123
  if (toRemove.length > 0) {
225
124
  console.log('Items to DELETE:');
226
125
  toRemove.slice(0, 10).forEach(o => {
227
- const tag = isLowQuality(o.title ?? '') ? '(low-quality)' : '(duplicate)';
126
+ const tag = isLowQualityObservation(o.title ?? '') ? '(low-quality)' : '(duplicate)';
228
127
  console.log(` ${tag} #${o.id ?? '?'} "${o.title}" [${o.type}]`);
229
128
  });
230
129
  if (toRemove.length > 10) {
@@ -236,8 +135,8 @@ export default defineCommand({
236
135
  // Preview noise archival
237
136
  if (toArchive.length > 0) {
238
137
  console.log('Items to ARCHIVE (noise pollution):');
239
- noiseHits.slice(0, 15).forEach(({ obs, reason }) => {
240
- console.log(` (${reason}) #${obs.id ?? '?'} "${obs.title}" [${obs.type}] entity=${obs.entityName}`);
138
+ noiseHits.slice(0, 15).forEach(({ observation, reason }) => {
139
+ console.log(` (${reason}) #${observation.id ?? '?'} "${observation.title}" [${observation.type}] entity=${observation.entityName}`);
241
140
  });
242
141
  if (toArchive.length > 15) {
243
142
  console.log(` ... and ${toArchive.length - 15} more`);
@@ -3,7 +3,9 @@ import { compactDetail, compactSearch, compactTimeline } from '../../compact/eng
3
3
  import { withFreshIndex } from '../../memory/freshness.js';
4
4
  import { getAllObservations, getObservation, getProjectObservations, resolveObservations, storeObservation, suggestTopicKey } from '../../memory/observations.js';
5
5
  import { buildGraphContextPacket, formatGraphContextPrompt } from '../../memory/graph-context.js';
6
+ import { applyDeduplicationPlan, planMemoryDeduplication } from '../../memory/deduplication.js';
6
7
  import { canManageObservation, filterReadableObservations, resolveObservationVisibility } from '../../memory/visibility.js';
8
+ import { getObservationStore } from '../../store/obs-store.js';
7
9
  import {
8
10
  asStringArg,
9
11
  coerceObservationStatus,
@@ -288,12 +290,11 @@ export default defineCommand({
288
290
  return;
289
291
  }
290
292
 
291
- const { deduplicateMemory } = await import('../../llm/memory-manager.js');
292
293
  const allObs = await withFreshIndex(() =>
293
294
  filterReadableObservations(
294
295
  getAllObservations().filter((obs) => (obs.status ?? 'active') === 'active' && obs.projectId === project.id),
295
296
  reader,
296
- ),
297
+ ).filter((observation) => canManageObservation(observation, reader)),
297
298
  );
298
299
 
299
300
  if (allObs.length < 2) {
@@ -310,55 +311,23 @@ export default defineCommand({
310
311
  candidates = allObs.slice(-20);
311
312
  }
312
313
 
313
- const byEntity = new Map<string, typeof candidates>();
314
- for (const obs of candidates) {
315
- const bucket = byEntity.get(obs.entityName) ?? [];
316
- bucket.push(obs);
317
- byEntity.set(obs.entityName, bucket);
318
- }
319
-
320
- const actions: string[] = [];
321
- const toResolve: number[] = [];
322
- for (const [, group] of byEntity) {
323
- if (group.length < 2) continue;
324
- for (let index = 0; index < group.length; index += 1) {
325
- for (let compareIndex = index + 1; compareIndex < group.length; compareIndex += 1) {
326
- const newer = group[compareIndex];
327
- const older = group[index];
328
- try {
329
- const decision = await deduplicateMemory(
330
- { title: newer.title, narrative: newer.narrative, facts: newer.facts },
331
- [{ id: older.id, title: older.title, narrative: older.narrative, facts: older.facts.join('\n') }],
332
- );
333
- if (decision && (decision.action === 'DELETE' || decision.action === 'UPDATE' || decision.action === 'NONE')) {
334
- actions.push(`Resolve #${older.id} because it duplicates newer #${newer.id}`);
335
- toResolve.push(older.id);
336
- }
337
- } catch {
338
- // Ignore failed pair analysis so one bad comparison doesn't abort the batch.
339
- }
340
- }
341
- }
342
- }
314
+ const plan = await planMemoryDeduplication(candidates);
315
+ const actionLines = plan.actions.map((item) =>
316
+ `Resolve #${item.resolveId} "${item.resolveTitle}"; keep #${item.keepId} "${item.keepTitle}" (${item.reason})`,
317
+ );
343
318
 
344
- if (dryRun || toResolve.length === 0) {
319
+ if (dryRun || plan.resolveIds.length === 0) {
345
320
  emitResult(
346
- { project, actions, resolved: [], dryRun: true },
347
- actions.length === 0 ? 'No duplicate candidates found.' : actions.join('\n'),
321
+ { project, plan, resolved: [], dryRun: true },
322
+ actionLines.length === 0 ? 'No duplicate candidates found.' : actionLines.join('\n'),
348
323
  asJson,
349
324
  );
350
325
  return;
351
326
  }
352
327
 
353
- const result = await resolveObservations(
354
- [...new Set(toResolve)].filter((id) => {
355
- const observation = getObservation(id, project.id);
356
- return observation ? canManageObservation(observation, reader) : false;
357
- }),
358
- 'resolved',
359
- );
328
+ const result = await applyDeduplicationPlan(getObservationStore(), project.id, plan.resolveIds, reader);
360
329
  emitResult(
361
- { project, actions, resolved: result.resolved, notFound: result.notFound, dryRun: false },
330
+ { project, plan, resolved: result.resolved, skipped: result.skipped, dryRun: false },
362
331
  `Resolved ${result.resolved.length} duplicate observation(s).`,
363
332
  asJson,
364
333
  );