memorix 1.9.4 → 1.9.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.
Files changed (90) hide show
  1. package/CHANGELOG.md +75 -1
  2. package/README.md +12 -6
  3. package/README.zh-CN.md +12 -6
  4. package/dist/cli/index.js +2533 -585
  5. package/dist/index.d.ts +15 -8
  6. package/dist/index.js +1201 -284
  7. package/dist/maintenance-runner.js +387 -216
  8. package/dist/media-audio-runner.js +300 -129
  9. package/dist/media-video-runner.js +300 -129
  10. package/dist/memcode-runtime/CHANGELOG.md +75 -1
  11. package/dist/sdk.d.ts +1 -0
  12. package/dist/sdk.js +1882 -998
  13. package/dist/types.d.ts +1 -1
  14. package/dist/vector-backfill-runner.js +300 -129
  15. package/docs/AGENT_OPERATOR_PLAYBOOK.md +47 -3
  16. package/docs/API_REFERENCE.md +39 -1
  17. package/docs/CONFIGURATION.md +58 -1
  18. package/docs/DEVELOPMENT.md +1 -1
  19. package/docs/DOCKER.md +23 -3
  20. package/docs/INTEGRATIONS.md +1 -1
  21. package/docs/KNOWN_ISSUES_AND_ROADMAP.md +61 -1
  22. package/docs/MULTI-DEVICE-SYNC-RESEARCH.md +4 -0
  23. package/docs/PERFORMANCE.md +46 -2
  24. package/docs/README.md +2 -1
  25. package/docs/SETUP.md +56 -1
  26. package/package.json +3 -2
  27. package/plugins/antigravity/memorix/plugin.json +1 -1
  28. package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
  29. package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
  30. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  31. package/plugins/copilot/memorix/plugin.json +1 -1
  32. package/plugins/gemini/memorix/gemini-extension.json +1 -1
  33. package/plugins/hermes/memorix/plugin.yaml +1 -1
  34. package/plugins/omp/memorix/package.json +1 -1
  35. package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
  36. package/plugins/pi/memorix/package.json +1 -1
  37. package/server.json +2 -2
  38. package/src/cli/capability-map.ts +1 -0
  39. package/src/cli/command-guide.ts +15 -3
  40. package/src/cli/commands/agent-integrations.ts +57 -0
  41. package/src/cli/commands/background.ts +11 -2
  42. package/src/cli/commands/context.ts +2 -0
  43. package/src/cli/commands/continuity.ts +155 -0
  44. package/src/cli/commands/init.ts +5 -2
  45. package/src/cli/commands/serve-http.ts +435 -84
  46. package/src/cli/commands/setup.ts +77 -10
  47. package/src/cli/commands/sync-store.ts +47 -12
  48. package/src/cli/commands/sync.ts +26 -0
  49. package/src/cli/index.ts +3 -1
  50. package/src/cli/tui/ConfigureView.tsx +2 -0
  51. package/src/codegraph/auto-context.ts +17 -0
  52. package/src/config/legacy-loader.ts +2 -2
  53. package/src/config/resolved-config.ts +8 -5
  54. package/src/config/yaml-loader.ts +20 -10
  55. package/src/dashboard/server.ts +10 -12
  56. package/src/embedding/api-provider.ts +4 -2
  57. package/src/embedding/fastembed-provider.ts +47 -14
  58. package/src/embedding/transformers-provider.ts +29 -7
  59. package/src/evaluation/memory-quality-gate.ts +90 -0
  60. package/src/hooks/official-skills.ts +2 -0
  61. package/src/integrations/registry.ts +1 -1
  62. package/src/knowledge/context-assembly.ts +1 -0
  63. package/src/knowledge/context-receipt.ts +15 -0
  64. package/src/knowledge/task-continuity.ts +413 -0
  65. package/src/knowledge/workset.ts +77 -0
  66. package/src/llm/provider.ts +2 -1
  67. package/src/memory/graph.ts +10 -8
  68. package/src/memory/session.ts +1 -1
  69. package/src/orchestrate/coordinator.ts +60 -0
  70. package/src/orchestrate/effect-ledger.ts +160 -0
  71. package/src/orchestrate/pipeline-trace.ts +40 -6
  72. package/src/runtime/maintenance-jobs.ts +12 -6
  73. package/src/sdk.ts +24 -0
  74. package/src/server/mcp-binding-store.ts +14 -3
  75. package/src/server/modern-mcp-bridge.ts +153 -27
  76. package/src/server/tool-profile.ts +3 -2
  77. package/src/server.ts +131 -7
  78. package/src/store/file-lock.ts +28 -3
  79. package/src/store/graph-store.ts +178 -46
  80. package/src/store/persistence.ts +14 -9
  81. package/src/store/sqlite-db.ts +150 -7
  82. package/src/store/sqlite-store.ts +32 -27
  83. package/src/sync/engine.ts +9 -7
  84. package/src/sync/namespace.ts +23 -0
  85. package/src/sync/policy.ts +19 -4
  86. package/src/sync/store-port.ts +43 -13
  87. package/src/sync/types.ts +2 -0
  88. package/src/team/team-store.ts +62 -27
  89. package/src/types.ts +1 -1
  90. package/src/workspace/mcp-adapters/grok.ts +32 -0
@@ -99,7 +99,8 @@ function evictOldest(): void {
99
99
  if (old) cachePayloadBytes -= payloadBytes(old);
100
100
  }
101
101
 
102
- function evictToFit(incomingBytes: number, incomingDims: number): void {
102
+ function evictToFit(incomingBytes: number, incomingDims: number): boolean {
103
+ if (incomingBytes > maxCachePayloadBytes()) return false;
103
104
  const limit = maxEntriesForDimensions(incomingDims);
104
105
  while (
105
106
  cache.size > 0 &&
@@ -107,6 +108,7 @@ function evictToFit(incomingBytes: number, incomingDims: number): void {
107
108
  ) {
108
109
  evictOldest();
109
110
  }
111
+ return true;
110
112
  }
111
113
 
112
114
  function isNumberArray(value: unknown): value is number[] {
@@ -139,7 +141,7 @@ function cacheInsert(hash: string, value: number[], dirty: boolean): void {
139
141
  cachePayloadBytes -= payloadBytes(existing);
140
142
  cache.delete(hash);
141
143
  }
142
- evictToFit(incoming, value.length || cacheDimensions || DEFAULT_ASSUMED_DIMENSIONS);
144
+ if (!evictToFit(incoming, value.length || cacheDimensions || DEFAULT_ASSUMED_DIMENSIONS)) return;
143
145
  cache.set(hash, value);
144
146
  cachePayloadBytes += incoming;
145
147
  if (dirty) diskCacheDirty = true;
@@ -30,6 +30,9 @@ const CACHE_FILE = join(CACHE_DIR, '.embedding-cache.json');
30
30
  // In-memory cache keyed by text hash → embedding
31
31
  const cache = new Map<string, number[]>();
32
32
  const MAX_CACHE_SIZE = 5000;
33
+ const DEFAULT_CACHE_PAYLOAD_BYTES = 64 * 1024 * 1024;
34
+ const MAX_CACHE_FILE_BYTES = 128 * 1024 * 1024;
35
+ let cachePayloadBytes = 0;
33
36
  let diskCacheDirty = false;
34
37
 
35
38
  function textHash(text: string): string {
@@ -39,14 +42,53 @@ function textHash(text: string): string {
39
42
  async function loadDiskCache(): Promise<void> {
40
43
  try {
41
44
  const raw = await readFile(CACHE_FILE, 'utf-8');
42
- const entries: [string, number[]][] = JSON.parse(raw);
43
- for (const [k, v] of entries) cache.set(k, v);
44
- console.error(`[memorix] Loaded ${entries.length} cached embeddings from disk`);
45
+ if (Buffer.byteLength(raw, 'utf8') > MAX_CACHE_FILE_BYTES) {
46
+ console.error('[memorix] FastEmbed cache exceeds the disk load limit; starting with an empty cache');
47
+ return;
48
+ }
49
+ const entries: unknown = JSON.parse(raw);
50
+ if (!Array.isArray(entries)) return;
51
+ for (const entry of entries) {
52
+ if (!Array.isArray(entry) || typeof entry[0] !== 'string' || !isVector(entry[1])) continue;
53
+ cacheSet(entry[0], entry[1], false);
54
+ }
55
+ console.error(`[memorix] Loaded ${cache.size} cached embeddings from disk`);
45
56
  } catch {
46
57
  // No cache file or corrupt — start fresh
47
58
  }
48
59
  }
49
60
 
61
+ function isVector(value: unknown): value is number[] {
62
+ return Array.isArray(value) && value.every((entry) => typeof entry === 'number');
63
+ }
64
+
65
+ function maxCachePayloadBytes(): number {
66
+ const raw = Number.parseInt(process.env.MEMORIX_EMBEDDING_CACHE_MAX_BYTES ?? '', 10);
67
+ return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_CACHE_PAYLOAD_BYTES;
68
+ }
69
+
70
+ function cacheSet(hash: string, value: number[], markDirty = true): void {
71
+ const incomingBytes = value.length * 8;
72
+ if (incomingBytes > maxCachePayloadBytes()) return;
73
+ const existing = cache.get(hash);
74
+ if (existing) cachePayloadBytes -= existing.length * 8;
75
+ else {
76
+ while (
77
+ cache.size >= MAX_CACHE_SIZE
78
+ || cachePayloadBytes + incomingBytes > maxCachePayloadBytes()
79
+ ) {
80
+ const firstKey = cache.keys().next().value;
81
+ if (firstKey === undefined) break;
82
+ const old = cache.get(firstKey);
83
+ cache.delete(firstKey);
84
+ if (old) cachePayloadBytes -= old.length * 8;
85
+ }
86
+ }
87
+ cache.set(hash, value);
88
+ cachePayloadBytes += incomingBytes;
89
+ if (markDirty) diskCacheDirty = true;
90
+ }
91
+
50
92
  async function saveDiskCache(): Promise<void> {
51
93
  if (!diskCacheDirty) return;
52
94
  try {
@@ -109,7 +151,7 @@ export class FastEmbedProvider implements EmbeddingProvider {
109
151
  if (result.length !== this.dimensions) {
110
152
  throw new Error(`Expected ${this.dimensions}d embedding, got ${result.length}d`);
111
153
  }
112
- this.cacheSet(hash, result);
154
+ cacheSet(hash, result);
113
155
  return result;
114
156
  }
115
157
 
@@ -139,7 +181,7 @@ export class FastEmbedProvider implements EmbeddingProvider {
139
181
  const originalIdx = uncachedIndices[batchIdx];
140
182
  const plain = Array.from(vec) as number[];
141
183
  results[originalIdx] = plain;
142
- this.cacheSet(textHash(uncachedTexts[batchIdx]), plain);
184
+ cacheSet(textHash(uncachedTexts[batchIdx]), plain);
143
185
  batchIdx++;
144
186
  }
145
187
  }
@@ -150,13 +192,4 @@ export class FastEmbedProvider implements EmbeddingProvider {
150
192
  return results;
151
193
  }
152
194
 
153
- private cacheSet(hash: string, value: number[]): void {
154
- // Evict oldest entries if cache is full
155
- if (cache.size >= MAX_CACHE_SIZE) {
156
- const firstKey = cache.keys().next().value;
157
- if (firstKey !== undefined) cache.delete(firstKey);
158
- }
159
- cache.set(hash, value);
160
- diskCacheDirty = true;
161
- }
162
195
  }
@@ -23,10 +23,22 @@ import {
23
23
  type EmbeddingOptions,
24
24
  type EmbeddingProvider,
25
25
  } from './provider.js';
26
+ import { createHash } from 'node:crypto';
26
27
 
27
28
  // In-memory LRU cache
28
29
  const cache = new Map<string, number[]>();
29
30
  const MAX_CACHE_SIZE = 5000;
31
+ const DEFAULT_CACHE_PAYLOAD_BYTES = 64 * 1024 * 1024;
32
+ let cachePayloadBytes = 0;
33
+
34
+ function cacheKey(text: string): string {
35
+ return createHash('sha256').update(text).digest('hex');
36
+ }
37
+
38
+ function maxCachePayloadBytes(): number {
39
+ const raw = Number.parseInt(process.env.MEMORIX_EMBEDDING_CACHE_MAX_BYTES ?? '', 10);
40
+ return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_CACHE_PAYLOAD_BYTES;
41
+ }
30
42
 
31
43
  export class TransformersProvider implements EmbeddingProvider {
32
44
  readonly name = 'transformers-minilm';
@@ -68,7 +80,7 @@ export class TransformersProvider implements EmbeddingProvider {
68
80
 
69
81
  async embed(text: string): Promise<number[]> {
70
82
  // Check cache first
71
- const cached = cache.get(text);
83
+ const cached = cache.get(cacheKey(text));
72
84
  if (cached) return cached;
73
85
 
74
86
  const output = await this.extractor(text, {
@@ -82,7 +94,7 @@ export class TransformersProvider implements EmbeddingProvider {
82
94
  throw new Error(`Expected ${this.dimensions}d embedding, got ${result.length}d`);
83
95
  }
84
96
 
85
- this.cacheSet(text, result);
97
+ this.cacheSet(cacheKey(text), result);
86
98
  return result;
87
99
  }
88
100
 
@@ -93,7 +105,7 @@ export class TransformersProvider implements EmbeddingProvider {
93
105
 
94
106
  // Check cache for each text
95
107
  for (let i = 0; i < texts.length; i++) {
96
- const cached = cache.get(texts[i]);
108
+ const cached = cache.get(cacheKey(texts[i]));
97
109
  if (cached) {
98
110
  results[i] = cached;
99
111
  } else {
@@ -114,7 +126,7 @@ export class TransformersProvider implements EmbeddingProvider {
114
126
  const vec = Array.from(allVecs[i]) as number[];
115
127
  const originalIdx = uncachedIndices[i];
116
128
  results[originalIdx] = vec;
117
- this.cacheSet(uncachedTexts[i], vec);
129
+ this.cacheSet(cacheKey(uncachedTexts[i]), vec);
118
130
  }
119
131
  }
120
132
 
@@ -122,10 +134,20 @@ export class TransformersProvider implements EmbeddingProvider {
122
134
  }
123
135
 
124
136
  private cacheSet(key: string, value: number[]): void {
125
- if (cache.size >= MAX_CACHE_SIZE) {
126
- const firstKey = cache.keys().next().value;
127
- if (firstKey !== undefined) cache.delete(firstKey);
137
+ const incomingBytes = value.length * 8;
138
+ if (incomingBytes > maxCachePayloadBytes()) return;
139
+ const existing = cache.get(key);
140
+ if (existing) cachePayloadBytes -= existing.length * 8;
141
+ else {
142
+ while (cache.size >= MAX_CACHE_SIZE || cachePayloadBytes + incomingBytes > maxCachePayloadBytes()) {
143
+ const firstKey = cache.keys().next().value;
144
+ if (firstKey === undefined) break;
145
+ const old = cache.get(firstKey);
146
+ cache.delete(firstKey);
147
+ if (old) cachePayloadBytes -= old.length * 8;
148
+ }
128
149
  }
129
150
  cache.set(key, value);
151
+ cachePayloadBytes += incomingBytes;
130
152
  }
131
153
  }
@@ -0,0 +1,90 @@
1
+ /** Deterministic, model-free quality gate for cross-session memory delivery. */
2
+
3
+ export interface MemoryQualitySample {
4
+ id: string;
5
+ expectedIds: string[];
6
+ shownIds: string[];
7
+ leakedProjectRows?: number;
8
+ staleRowsShown?: number;
9
+ shouldAbstain?: boolean;
10
+ tokens: number;
11
+ tokenBudget: number;
12
+ latencyMs: number;
13
+ rssBytes?: number;
14
+ }
15
+
16
+ export interface MemoryQualityGateScore {
17
+ cases: number;
18
+ recallAtK: number;
19
+ precisionAtK: number;
20
+ scopeIsolation: number;
21
+ staleRejection: number;
22
+ abstentionAccuracy: number;
23
+ overBudgetCases: number;
24
+ latencyP50Ms: number;
25
+ latencyP95Ms: number;
26
+ peakRssBytes: number;
27
+ passed: boolean;
28
+ failures: string[];
29
+ }
30
+
31
+ function rate(numerator: number, denominator: number): number {
32
+ return denominator === 0 ? 1 : numerator / denominator;
33
+ }
34
+
35
+ function percentile(values: number[], p: number): number {
36
+ if (values.length === 0) return 0;
37
+ const sorted = [...values].sort((a, b) => a - b);
38
+ const index = Math.min(sorted.length - 1, Math.ceil((p / 100) * sorted.length) - 1);
39
+ return sorted[Math.max(0, index)];
40
+ }
41
+
42
+ export function scoreMemoryQualityGate(samples: MemoryQualitySample[]): MemoryQualityGateScore {
43
+ let recalls = 0;
44
+ let precisionNumerator = 0;
45
+ let precisionDenominator = 0;
46
+ let isolated = 0;
47
+ let staleRejected = 0;
48
+ let abstentionCorrect = 0;
49
+ let overBudgetCases = 0;
50
+ const latencies = samples.map(sample => sample.latencyMs);
51
+ const failures: string[] = [];
52
+
53
+ for (const sample of samples) {
54
+ const expected = new Set(sample.expectedIds);
55
+ const shown = new Set(sample.shownIds);
56
+ if ([...expected].every(id => shown.has(id))) recalls++;
57
+ precisionNumerator += [...shown].filter(id => expected.has(id)).length;
58
+ precisionDenominator += shown.size;
59
+ if (!sample.leakedProjectRows) isolated++;
60
+ if (!sample.staleRowsShown) staleRejected++;
61
+ const abstained = shown.size === 0;
62
+ if (Boolean(sample.shouldAbstain) === abstained) abstentionCorrect++;
63
+ if (sample.tokens > sample.tokenBudget) overBudgetCases++;
64
+ if (sample.shouldAbstain && !abstained) failures.push(`${sample.id}: failed to abstain`);
65
+ if (!sample.shouldAbstain && ![...expected].every(id => shown.has(id))) failures.push(`${sample.id}: expected memory missing`);
66
+ if (sample.leakedProjectRows) failures.push(`${sample.id}: cross-project leakage`);
67
+ if (sample.staleRowsShown) failures.push(`${sample.id}: stale memory surfaced`);
68
+ }
69
+
70
+ const score: MemoryQualityGateScore = {
71
+ cases: samples.length,
72
+ recallAtK: rate(recalls, samples.length),
73
+ precisionAtK: rate(precisionNumerator, precisionDenominator),
74
+ scopeIsolation: rate(isolated, samples.length),
75
+ staleRejection: rate(staleRejected, samples.length),
76
+ abstentionAccuracy: rate(abstentionCorrect, samples.length),
77
+ overBudgetCases,
78
+ latencyP50Ms: percentile(latencies, 50),
79
+ latencyP95Ms: percentile(latencies, 95),
80
+ peakRssBytes: Math.max(0, ...samples.map(sample => sample.rssBytes ?? 0)),
81
+ passed: false,
82
+ failures,
83
+ };
84
+ score.passed = score.recallAtK === 1
85
+ && score.scopeIsolation === 1
86
+ && score.staleRejection === 1
87
+ && score.abstentionAccuracy === 1
88
+ && score.overBudgetCases === 0;
89
+ return score;
90
+ }
@@ -40,6 +40,7 @@ export const OFFICIAL_MEMORIX_SKILLS: OfficialMemorixSkill[] = [
40
40
  '| Need the sequence around one memory | `memorix_timeline` | `memorix memory timeline --id <id>` |',
41
41
  '| Check source and freshness for a memory | `memorix_evidence` | `memorix evidence list|get|events` |',
42
42
  '| Record whether a memory helped, conflicted, or was corrected | `memorix_feedback` | `memorix feedback record|show|audit` |',
43
+ '| Track task requirements, decisions, verification, and outcome | `memorix_continuity` | `memorix continuity start|record|show|close` |',
43
44
  '| Inspect or import controlled media | `memorix_media` | `memorix media import|attach|list|show|status` |',
44
45
  '| Learned reusable project knowledge | `memorix_store` | `memorix memory store --type <type> --entity <name> --title "<title>" "<text>"` |',
45
46
  '| Task or bug is complete/outdated | `memorix_resolve` | `memorix memory resolve --ids <ids>` |',
@@ -63,6 +64,7 @@ export const OFFICIAL_MEMORIX_SKILLS: OfficialMemorixSkill[] = [
63
64
  '3. Use stale or unbound memory only as a warning or search lead.',
64
65
  '4. Store durable decisions, fixes, and gotchas after the project state changes.',
65
66
  '5. Resolve completed or obsolete memories so future briefs stay small.',
67
+ '6. For multi-step work, start a continuity ledger and pass its taskId back to memorix_project_context.',
66
68
  '',
67
69
  '## Store Rules',
68
70
  '',
@@ -72,7 +72,7 @@ export const MEMORIX_INTEGRATIONS: MemorixIntegration[] = [
72
72
  status: 'ready',
73
73
  install: 'memorix setup --agent grok --global',
74
74
  surfaces: ['hooks', 'AGENTS.md'],
75
- note: 'Writes ~/.grok/hooks/memorix.json and leaves Grok MCP host-owned; setup does not write config.toml.',
75
+ note: 'Writes ~/.grok/hooks/memorix.json and leaves Grok MCP host-owned by default. Pass --mcp http to write [mcp_servers.memorix] in Grok config.toml.',
76
76
  },
77
77
  {
78
78
  agent: 'cursor',
@@ -12,6 +12,7 @@ export type ContextDeliveryTarget =
12
12
 
13
13
  export type ContextCandidateKind =
14
14
  | 'task'
15
+ | 'continuity'
15
16
  | 'continuation'
16
17
  | 'current-fact'
17
18
  | 'code-state'
@@ -1,5 +1,6 @@
1
1
  import type { CodeGraphProviderQuality } from '../codegraph/types.js';
2
2
  import type { TaskWorkset } from './workset.js';
3
+ import type { TaskContinuityLedger } from './task-continuity.js';
3
4
 
4
5
  /**
5
6
  * A small machine-readable counterpart to the agent-facing Workset prompt.
@@ -18,6 +19,11 @@ export interface BoundedContextReceipt {
18
19
  snapshotId?: string;
19
20
  };
20
21
  loadout?: TaskWorkset['agentLoadout'];
22
+ continuity?: {
23
+ taskId: string;
24
+ status: TaskContinuityLedger['status'];
25
+ outcome: TaskContinuityLedger['outcome'];
26
+ };
21
27
  }
22
28
 
23
29
  export function buildBoundedContextReceipt(input: {
@@ -37,5 +43,14 @@ export function buildBoundedContextReceipt(input: {
37
43
  ...(input.workset.provenance.snapshotId ? { snapshotId: input.workset.provenance.snapshotId } : {}),
38
44
  },
39
45
  ...(input.workset.agentLoadout ? { loadout: input.workset.agentLoadout } : {}),
46
+ ...(input.workset.continuity
47
+ ? {
48
+ continuity: {
49
+ taskId: input.workset.continuity.taskId,
50
+ status: input.workset.continuity.status,
51
+ outcome: input.workset.continuity.outcome,
52
+ },
53
+ }
54
+ : {}),
40
55
  };
41
56
  }