memorix 1.1.7 → 1.1.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.
Files changed (174) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/CLAUDE.md +6 -1
  3. package/README.md +21 -0
  4. package/README.zh-CN.md +21 -0
  5. package/TEAM.md +86 -86
  6. package/dist/cli/index.js +730 -150
  7. package/dist/cli/index.js.map +1 -1
  8. package/dist/dashboard/static/index.html +201 -201
  9. package/dist/dashboard/static/style.css +3584 -3584
  10. package/dist/index.js +15 -8
  11. package/dist/index.js.map +1 -1
  12. package/dist/memcode-runtime/CHANGELOG.md +13 -0
  13. package/dist/memcode-runtime/package.json +4 -4
  14. package/dist/sdk.js +15 -8
  15. package/dist/sdk.js.map +1 -1
  16. package/docs/AGENT_OPERATOR_PLAYBOOK.md +18 -0
  17. package/docs/DESIGN_DECISIONS.md +357 -357
  18. package/docs/SETUP.md +10 -0
  19. package/docs/dev-log/progress.txt +35 -26
  20. package/package.json +1 -1
  21. package/src/audit/index.ts +156 -156
  22. package/src/cli/commands/agent-integrations.ts +623 -0
  23. package/src/cli/commands/audit-list.ts +89 -89
  24. package/src/cli/commands/background.ts +659 -659
  25. package/src/cli/commands/cleanup.ts +255 -255
  26. package/src/cli/commands/doctor.ts +23 -0
  27. package/src/cli/commands/formation.ts +48 -48
  28. package/src/cli/commands/git-hook-install.ts +111 -111
  29. package/src/cli/commands/handoff.ts +66 -66
  30. package/src/cli/commands/hooks-status.ts +63 -63
  31. package/src/cli/commands/ingest-commit.ts +153 -153
  32. package/src/cli/commands/ingest-image.ts +73 -73
  33. package/src/cli/commands/ingest-log.ts +180 -180
  34. package/src/cli/commands/ingest.ts +44 -44
  35. package/src/cli/commands/integrate-shared.ts +15 -15
  36. package/src/cli/commands/lock.ts +96 -96
  37. package/src/cli/commands/message.ts +121 -121
  38. package/src/cli/commands/poll.ts +70 -70
  39. package/src/cli/commands/purge-all-memory.ts +85 -85
  40. package/src/cli/commands/purge-project-memory.ts +83 -83
  41. package/src/cli/commands/reasoning.ts +132 -132
  42. package/src/cli/commands/repair.ts +60 -0
  43. package/src/cli/commands/retention.ts +108 -108
  44. package/src/cli/commands/serve-shared.ts +118 -118
  45. package/src/cli/commands/setup.ts +3 -3
  46. package/src/cli/commands/skills.ts +123 -123
  47. package/src/cli/commands/task.ts +192 -192
  48. package/src/cli/commands/transfer.ts +73 -73
  49. package/src/cli/commands/uninstall-project-artifacts.ts +85 -85
  50. package/src/cli/index.ts +3 -1
  51. package/src/cli/tui/ChatView.tsx +234 -234
  52. package/src/cli/tui/CommandBar.tsx +312 -312
  53. package/src/cli/tui/ContextRail.tsx +118 -118
  54. package/src/cli/tui/HeaderBar.tsx +72 -72
  55. package/src/cli/tui/LogoBanner.tsx +51 -51
  56. package/src/cli/tui/Panels.tsx +632 -632
  57. package/src/cli/tui/Sidebar.tsx +179 -179
  58. package/src/cli/tui/chat-service.ts +742 -742
  59. package/src/cli/tui/data.ts +547 -547
  60. package/src/cli/tui/index.ts +41 -41
  61. package/src/cli/tui/markdown-render.tsx +371 -371
  62. package/src/cli/tui/theme.ts +178 -178
  63. package/src/cli/tui/use-mouse.ts +157 -157
  64. package/src/cli/tui/useNavigation.ts +56 -56
  65. package/src/cli/update-checker.ts +211 -211
  66. package/src/cli/version.ts +7 -7
  67. package/src/cli/workbench.ts +1 -1
  68. package/src/codegraph/context-pack.ts +5 -1
  69. package/src/codegraph/lite-provider.ts +5 -1
  70. package/src/codegraph/project-context.ts +5 -1
  71. package/src/compact/token-budget.ts +74 -74
  72. package/src/config/behavior.ts +59 -59
  73. package/src/dashboard/project-classification.ts +64 -64
  74. package/src/dashboard/static/index.html +201 -201
  75. package/src/dashboard/static/style.css +3584 -3584
  76. package/src/embedding/fastembed-provider.ts +142 -142
  77. package/src/embedding/transformers-provider.ts +111 -111
  78. package/src/git/extractor.ts +209 -209
  79. package/src/git/hooks-path.ts +85 -85
  80. package/src/git/noise-filter.ts +210 -210
  81. package/src/hooks/installers/index.ts +4 -4
  82. package/src/hooks/official-skills.ts +1 -1
  83. package/src/hooks/pattern-detector.ts +173 -173
  84. package/src/hooks/rules/memorix-agent-rules.md +2 -2
  85. package/src/hooks/significance-filter.ts +250 -250
  86. package/src/llm/memory-manager.ts +328 -328
  87. package/src/llm/provider.ts +885 -885
  88. package/src/llm/quality.ts +248 -248
  89. package/src/memory/attribution-guard.ts +249 -249
  90. package/src/memory/auto-relations.ts +107 -107
  91. package/src/memory/consolidation.ts +302 -302
  92. package/src/memory/disclosure-policy.ts +141 -141
  93. package/src/memory/entity-extractor.ts +197 -197
  94. package/src/memory/formation/evaluate.ts +217 -217
  95. package/src/memory/formation/extract.ts +361 -361
  96. package/src/memory/formation/index.ts +417 -417
  97. package/src/memory/formation/resolve.ts +344 -344
  98. package/src/memory/formation/types.ts +315 -315
  99. package/src/memory/freshness.ts +122 -122
  100. package/src/memory/graph.ts +197 -197
  101. package/src/memory/refs.ts +94 -94
  102. package/src/memory/retention.ts +433 -433
  103. package/src/memory/secret-filter.ts +79 -79
  104. package/src/memory/session.ts +523 -523
  105. package/src/multimodal/image-loader.ts +143 -143
  106. package/src/orchestrate/adapters/claude-stream.ts +192 -192
  107. package/src/orchestrate/adapters/claude.ts +111 -111
  108. package/src/orchestrate/adapters/codex-stream.ts +134 -134
  109. package/src/orchestrate/adapters/codex.ts +41 -41
  110. package/src/orchestrate/adapters/gemini-stream.ts +166 -166
  111. package/src/orchestrate/adapters/gemini.ts +42 -42
  112. package/src/orchestrate/adapters/index.ts +73 -73
  113. package/src/orchestrate/adapters/opencode-stream.ts +143 -143
  114. package/src/orchestrate/adapters/opencode.ts +47 -47
  115. package/src/orchestrate/adapters/spawn-helper.ts +286 -286
  116. package/src/orchestrate/adapters/types.ts +77 -77
  117. package/src/orchestrate/capability-router.ts +284 -284
  118. package/src/orchestrate/context-compact.ts +188 -188
  119. package/src/orchestrate/cost-tracker.ts +219 -219
  120. package/src/orchestrate/error-recovery.ts +191 -191
  121. package/src/orchestrate/evidence.ts +140 -140
  122. package/src/orchestrate/ledger.ts +110 -110
  123. package/src/orchestrate/memorix-bridge.ts +380 -380
  124. package/src/orchestrate/output-budget.ts +80 -80
  125. package/src/orchestrate/permission.ts +152 -152
  126. package/src/orchestrate/pipeline-trace.ts +131 -131
  127. package/src/orchestrate/prompt-builder.ts +155 -155
  128. package/src/orchestrate/ring-buffer.ts +37 -37
  129. package/src/orchestrate/task-graph.ts +389 -389
  130. package/src/orchestrate/verify-gate.ts +219 -219
  131. package/src/orchestrate/worktree.ts +232 -232
  132. package/src/project/aliases.ts +374 -374
  133. package/src/project/detector.ts +268 -268
  134. package/src/rules/adapters/claude-code.ts +99 -99
  135. package/src/rules/adapters/codex.ts +97 -97
  136. package/src/rules/adapters/copilot.ts +124 -124
  137. package/src/rules/adapters/cursor.ts +114 -114
  138. package/src/rules/adapters/kiro.ts +126 -126
  139. package/src/rules/adapters/trae.ts +56 -56
  140. package/src/rules/adapters/windsurf.ts +83 -83
  141. package/src/rules/syncer.ts +235 -235
  142. package/src/sdk.ts +327 -327
  143. package/src/search/intent-detector.ts +289 -289
  144. package/src/search/query-expansion.ts +52 -52
  145. package/src/server/formation-timeout.ts +27 -27
  146. package/src/skills/mini-skills.ts +386 -386
  147. package/src/store/chat-store.ts +119 -119
  148. package/src/store/file-lock.ts +100 -100
  149. package/src/store/graph-store.ts +249 -249
  150. package/src/store/mini-skill-store.ts +349 -349
  151. package/src/store/obs-store.ts +255 -255
  152. package/src/store/persistence-json.ts +212 -212
  153. package/src/store/persistence.ts +291 -291
  154. package/src/store/project-affinity.ts +195 -195
  155. package/src/store/session-store.ts +259 -259
  156. package/src/store/sqlite-store.ts +339 -339
  157. package/src/team/event-bus.ts +76 -76
  158. package/src/team/file-locks.ts +173 -173
  159. package/src/team/handoff.ts +167 -167
  160. package/src/team/messages.ts +203 -203
  161. package/src/team/poll.ts +132 -132
  162. package/src/team/tasks.ts +211 -211
  163. package/src/wiki/generator.ts +237 -237
  164. package/src/wiki/knowledge-graph.ts +334 -334
  165. package/src/wiki/types.ts +85 -85
  166. package/src/workspace/mcp-adapters/codex.ts +191 -191
  167. package/src/workspace/mcp-adapters/copilot.ts +105 -105
  168. package/src/workspace/mcp-adapters/cursor.ts +53 -53
  169. package/src/workspace/mcp-adapters/kiro.ts +64 -64
  170. package/src/workspace/mcp-adapters/opencode.ts +123 -123
  171. package/src/workspace/mcp-adapters/trae.ts +134 -134
  172. package/src/workspace/mcp-adapters/windsurf.ts +91 -91
  173. package/src/workspace/sanitizer.ts +60 -60
  174. package/src/workspace/workflow-sync.ts +131 -131
@@ -1,82 +1,82 @@
1
- /**
2
- * Memory Formation — Stage 2: Resolve
3
- *
4
- * Determines what to do with an enriched memory: create new, merge into
5
- * existing, evolve (supersede), or discard as redundant.
6
- *
7
- * This stage absorbs and replaces the previous "Compact on Write" logic
8
- * (src/llm/memory-manager.ts) with a richer resolution model:
9
- *
10
- * - new: Truly new knowledge → proceed to store
11
- * - merge: Same topic as existing → UPDATE with combined content
12
- * - evolve: Existing is outdated → UPDATE with new content as primary
13
- * - discard: Redundant or noise → skip storage entirely
14
- *
15
- * Rules-based mode uses similarity scores, entity overlap, fact comparison,
16
- * and contradiction detection to make decisions without LLM.
17
- */
18
-
19
- import type { ExtractResult, ResolveResult, SearchHit, ExistingMemoryRef } from './types.js';
20
-
21
- // ── Thresholds ───────────────────────────────────────────────────
22
-
23
- /** Above this: very likely same topic */
24
- const SIMILARITY_HIGH = 0.75;
25
- /** Above this: related topic */
26
- const SIMILARITY_MEDIUM = 0.50;
27
- /** Above this: exact duplicate — discard */
28
- const SIMILARITY_DUPLICATE = 0.90;
29
-
30
- // ── Content Comparison Utilities ─────────────────────────────────
31
-
32
- /**
33
- * Compute Jaccard similarity between two sets of normalized words.
34
- */
35
- function wordOverlap(a: string, b: string): number {
36
- const wordsA = new Set(a.toLowerCase().split(/\s+/).filter(w => w.length > 2));
37
- const wordsB = new Set(b.toLowerCase().split(/\s+/).filter(w => w.length > 2));
38
- if (wordsA.size === 0 || wordsB.size === 0) return 0;
39
-
40
- let intersection = 0;
41
- for (const w of wordsA) {
42
- if (wordsB.has(w)) intersection++;
43
- }
44
- return intersection / Math.max(wordsA.size, wordsB.size);
45
- }
46
-
47
- /**
48
- * Check if two entity names refer to the same concept.
49
- */
50
- function entitiesMatch(a: string, b: string): boolean {
51
- const na = a.toLowerCase().replace(/[-_]/g, '');
52
- const nb = b.toLowerCase().replace(/[-_]/g, '');
53
- if (na === nb) return true;
54
- if (na.length >= 3 && nb.length >= 3) {
55
- if (na.includes(nb) || nb.includes(na)) return true;
56
- }
57
- return false;
58
- }
59
-
60
- /**
61
- * Detect potential contradiction between old and new content.
62
- * Looks for negation patterns and opposing statements.
63
- */
1
+ /**
2
+ * Memory Formation — Stage 2: Resolve
3
+ *
4
+ * Determines what to do with an enriched memory: create new, merge into
5
+ * existing, evolve (supersede), or discard as redundant.
6
+ *
7
+ * This stage absorbs and replaces the previous "Compact on Write" logic
8
+ * (src/llm/memory-manager.ts) with a richer resolution model:
9
+ *
10
+ * - new: Truly new knowledge → proceed to store
11
+ * - merge: Same topic as existing → UPDATE with combined content
12
+ * - evolve: Existing is outdated → UPDATE with new content as primary
13
+ * - discard: Redundant or noise → skip storage entirely
14
+ *
15
+ * Rules-based mode uses similarity scores, entity overlap, fact comparison,
16
+ * and contradiction detection to make decisions without LLM.
17
+ */
18
+
19
+ import type { ExtractResult, ResolveResult, SearchHit, ExistingMemoryRef } from './types.js';
20
+
21
+ // ── Thresholds ───────────────────────────────────────────────────
22
+
23
+ /** Above this: very likely same topic */
24
+ const SIMILARITY_HIGH = 0.75;
25
+ /** Above this: related topic */
26
+ const SIMILARITY_MEDIUM = 0.50;
27
+ /** Above this: exact duplicate — discard */
28
+ const SIMILARITY_DUPLICATE = 0.90;
29
+
30
+ // ── Content Comparison Utilities ─────────────────────────────────
31
+
32
+ /**
33
+ * Compute Jaccard similarity between two sets of normalized words.
34
+ */
35
+ function wordOverlap(a: string, b: string): number {
36
+ const wordsA = new Set(a.toLowerCase().split(/\s+/).filter(w => w.length > 2));
37
+ const wordsB = new Set(b.toLowerCase().split(/\s+/).filter(w => w.length > 2));
38
+ if (wordsA.size === 0 || wordsB.size === 0) return 0;
39
+
40
+ let intersection = 0;
41
+ for (const w of wordsA) {
42
+ if (wordsB.has(w)) intersection++;
43
+ }
44
+ return intersection / Math.max(wordsA.size, wordsB.size);
45
+ }
46
+
47
+ /**
48
+ * Check if two entity names refer to the same concept.
49
+ */
50
+ function entitiesMatch(a: string, b: string): boolean {
51
+ const na = a.toLowerCase().replace(/[-_]/g, '');
52
+ const nb = b.toLowerCase().replace(/[-_]/g, '');
53
+ if (na === nb) return true;
54
+ if (na.length >= 3 && nb.length >= 3) {
55
+ if (na.includes(nb) || nb.includes(na)) return true;
56
+ }
57
+ return false;
58
+ }
59
+
60
+ /**
61
+ * Detect potential contradiction between old and new content.
62
+ * Looks for negation patterns and opposing statements.
63
+ */
64
64
  function hasContradiction(oldText: string, newText: string): boolean {
65
- // Simple heuristic: check for "not X" in new when "X" is in old
66
- const negationPatterns = [
67
- /\bnot\s+(\w+)/gi,
68
- /\bno longer\b/i,
69
- /\binstead of\b/i,
70
- /\breplaced\b.*\bwith\b/i,
71
- /\bremoved\b/i,
72
- /\bdeprecated\b/i,
73
- /\bobsolete\b/i,
74
- /不再/,
75
- /已弃用/,
76
- /替换为/,
77
- /改为/,
78
- ];
79
-
65
+ // Simple heuristic: check for "not X" in new when "X" is in old
66
+ const negationPatterns = [
67
+ /\bnot\s+(\w+)/gi,
68
+ /\bno longer\b/i,
69
+ /\binstead of\b/i,
70
+ /\breplaced\b.*\bwith\b/i,
71
+ /\bremoved\b/i,
72
+ /\bdeprecated\b/i,
73
+ /\bobsolete\b/i,
74
+ /不再/,
75
+ /已弃用/,
76
+ /替换为/,
77
+ /改为/,
78
+ ];
79
+
80
80
  return negationPatterns.some(p => p.test(newText));
81
81
  }
82
82
 
@@ -90,141 +90,141 @@ function normalizedSearchSimilarity(score: number): number {
90
90
  if (score <= 1) return score;
91
91
  return 0;
92
92
  }
93
-
94
- /**
95
- * Merge two narratives, keeping the most comprehensive version.
96
- */
97
- function mergeNarratives(oldNarrative: string, newNarrative: string): string {
98
- if (newNarrative.length > oldNarrative.length * 1.5) return newNarrative;
99
- if (oldNarrative.length > newNarrative.length * 1.5) return oldNarrative;
100
- return `${newNarrative}\n\n[Previous context]: ${oldNarrative}`;
101
- }
102
-
103
- /**
104
- * Merge two fact lists, deduplicating by normalized text.
105
- */
106
- function mergeFacts(oldFacts: string[], newFacts: string[]): string[] {
107
- const seen = new Set<string>();
108
- const merged: string[] = [];
109
-
110
- // New facts first (more recent)
111
- for (const f of newFacts) {
112
- const norm = f.toLowerCase().trim();
113
- if (!seen.has(norm) && f.trim().length > 0) {
114
- seen.add(norm);
115
- merged.push(f);
116
- }
117
- }
118
- for (const f of oldFacts) {
119
- const norm = f.toLowerCase().trim();
120
- if (!seen.has(norm) && f.trim().length > 0) {
121
- seen.add(norm);
122
- merged.push(f);
123
- }
124
- }
125
-
126
- return merged;
127
- }
128
-
129
- // ── LLM Resolution (Mem0-style) ───────────────────────────────
130
-
131
- const LLM_RESOLVE_PROMPT = `You are a Memory Consolidation Manager for a software engineering knowledge base.
132
-
133
- You must decide what to do with a NEW memory given EXISTING memories that are similar.
134
-
135
- Operations:
136
- - ADD: The new memory contains genuinely new information not present in existing memories.
137
- - UPDATE: The new memory adds to or refines an existing memory. Specify which existing memory ID to update.
138
- - DELETE: The new memory contradicts an existing memory. Specify which existing memory ID to delete.
139
- - NOOP: The new memory is redundant (already covered by existing memories). Skip storage.
140
-
141
- Rules:
142
- - Return ONLY a JSON object
143
- - If UPDATE: merge the best information from both old and new
144
- - If DELETE: the new memory supersedes the old (contradiction detected)
145
- - Prefer UPDATE over ADD when the topic is the same but information differs
146
- - Prefer NOOP over ADD when the information is essentially the same
147
-
148
- Response format:
149
- {"action": "ADD|UPDATE|DELETE|NOOP", "targetId": <number or null>, "reason": "<brief explanation>", "mergedText": "<merged content for UPDATE, or null>"}`;
150
-
151
- async function resolveWithLLM(
152
- extracted: ExtractResult,
153
- hits: SearchHit[],
154
- getObservation: (id: number) => ExistingMemoryRef | null,
155
- ): Promise<ResolveResult | null> {
156
- try {
157
- const { callLLM } = await import('../../llm/provider.js');
158
-
159
- // Build context for LLM
160
- const existingMemories = hits.slice(0, 5).map((h, i) => ({
161
- id: h.observationId,
162
- index: i,
163
- title: h.title,
164
- content: h.narrative.substring(0, 300),
165
- facts: h.facts.substring(0, 200),
166
- }));
167
-
168
- const input = `NEW MEMORY:
169
- Title: ${extracted.title}
170
- Content: ${extracted.narrative.substring(0, 500)}
171
- Facts: ${extracted.facts.join('; ')}
172
-
173
- EXISTING MEMORIES:
174
- ${existingMemories.map(m => `[ID:${m.id}] ${m.title} | ${m.content} | Facts: ${m.facts}`).join('\n')}`;
175
-
176
- const response = await callLLM(LLM_RESOLVE_PROMPT, input);
177
- const text = response.content.trim();
178
-
179
- const jsonMatch = text.match(/\{[\s\S]*\}/);
180
- if (!jsonMatch) return null;
181
- const parsed = JSON.parse(jsonMatch[0]);
182
-
183
- const action = parsed.action?.toUpperCase();
184
- const targetId = parsed.targetId ? Number(parsed.targetId) : undefined;
185
- const reason = parsed.reason || 'LLM decision';
186
-
187
- if (action === 'NOOP') {
188
- return { action: 'discard', targetId, reason: `LLM: ${reason}` };
189
- }
190
- if (action === 'ADD') {
191
- return { action: 'new', reason: `LLM: ${reason}` };
192
- }
193
- if (action === 'UPDATE' && targetId) {
194
- const existing = getObservation(targetId);
195
- const oldFacts = existing?.facts ?? [];
196
- return {
197
- action: 'merge',
198
- targetId,
199
- reason: `LLM: ${reason}`,
200
- mergedNarrative: parsed.mergedText || mergeNarratives(existing?.narrative ?? '', extracted.narrative),
201
- mergedFacts: mergeFacts(oldFacts, extracted.facts),
202
- };
203
- }
204
- if (action === 'DELETE' && targetId) {
205
- const existing = getObservation(targetId);
206
- const oldFacts = existing?.facts ?? [];
207
- return {
208
- action: 'evolve',
209
- targetId,
210
- reason: `LLM: ${reason}`,
211
- mergedNarrative: extracted.narrative,
212
- mergedFacts: mergeFacts(oldFacts, extracted.facts),
213
- };
214
- }
215
-
216
- return null; // Unrecognized action
217
- } catch {
218
- return null; // LLM failure → fall back to rules
219
- }
220
- }
221
-
222
- // ── Resolve Implementation ───────────────────────────────────────
223
-
224
- /**
225
- * Score a candidate match for resolution.
226
- * Returns a composite score considering similarity, entity overlap, and content richness.
227
- */
93
+
94
+ /**
95
+ * Merge two narratives, keeping the most comprehensive version.
96
+ */
97
+ function mergeNarratives(oldNarrative: string, newNarrative: string): string {
98
+ if (newNarrative.length > oldNarrative.length * 1.5) return newNarrative;
99
+ if (oldNarrative.length > newNarrative.length * 1.5) return oldNarrative;
100
+ return `${newNarrative}\n\n[Previous context]: ${oldNarrative}`;
101
+ }
102
+
103
+ /**
104
+ * Merge two fact lists, deduplicating by normalized text.
105
+ */
106
+ function mergeFacts(oldFacts: string[], newFacts: string[]): string[] {
107
+ const seen = new Set<string>();
108
+ const merged: string[] = [];
109
+
110
+ // New facts first (more recent)
111
+ for (const f of newFacts) {
112
+ const norm = f.toLowerCase().trim();
113
+ if (!seen.has(norm) && f.trim().length > 0) {
114
+ seen.add(norm);
115
+ merged.push(f);
116
+ }
117
+ }
118
+ for (const f of oldFacts) {
119
+ const norm = f.toLowerCase().trim();
120
+ if (!seen.has(norm) && f.trim().length > 0) {
121
+ seen.add(norm);
122
+ merged.push(f);
123
+ }
124
+ }
125
+
126
+ return merged;
127
+ }
128
+
129
+ // ── LLM Resolution (Mem0-style) ───────────────────────────────
130
+
131
+ const LLM_RESOLVE_PROMPT = `You are a Memory Consolidation Manager for a software engineering knowledge base.
132
+
133
+ You must decide what to do with a NEW memory given EXISTING memories that are similar.
134
+
135
+ Operations:
136
+ - ADD: The new memory contains genuinely new information not present in existing memories.
137
+ - UPDATE: The new memory adds to or refines an existing memory. Specify which existing memory ID to update.
138
+ - DELETE: The new memory contradicts an existing memory. Specify which existing memory ID to delete.
139
+ - NOOP: The new memory is redundant (already covered by existing memories). Skip storage.
140
+
141
+ Rules:
142
+ - Return ONLY a JSON object
143
+ - If UPDATE: merge the best information from both old and new
144
+ - If DELETE: the new memory supersedes the old (contradiction detected)
145
+ - Prefer UPDATE over ADD when the topic is the same but information differs
146
+ - Prefer NOOP over ADD when the information is essentially the same
147
+
148
+ Response format:
149
+ {"action": "ADD|UPDATE|DELETE|NOOP", "targetId": <number or null>, "reason": "<brief explanation>", "mergedText": "<merged content for UPDATE, or null>"}`;
150
+
151
+ async function resolveWithLLM(
152
+ extracted: ExtractResult,
153
+ hits: SearchHit[],
154
+ getObservation: (id: number) => ExistingMemoryRef | null,
155
+ ): Promise<ResolveResult | null> {
156
+ try {
157
+ const { callLLM } = await import('../../llm/provider.js');
158
+
159
+ // Build context for LLM
160
+ const existingMemories = hits.slice(0, 5).map((h, i) => ({
161
+ id: h.observationId,
162
+ index: i,
163
+ title: h.title,
164
+ content: h.narrative.substring(0, 300),
165
+ facts: h.facts.substring(0, 200),
166
+ }));
167
+
168
+ const input = `NEW MEMORY:
169
+ Title: ${extracted.title}
170
+ Content: ${extracted.narrative.substring(0, 500)}
171
+ Facts: ${extracted.facts.join('; ')}
172
+
173
+ EXISTING MEMORIES:
174
+ ${existingMemories.map(m => `[ID:${m.id}] ${m.title} | ${m.content} | Facts: ${m.facts}`).join('\n')}`;
175
+
176
+ const response = await callLLM(LLM_RESOLVE_PROMPT, input);
177
+ const text = response.content.trim();
178
+
179
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
180
+ if (!jsonMatch) return null;
181
+ const parsed = JSON.parse(jsonMatch[0]);
182
+
183
+ const action = parsed.action?.toUpperCase();
184
+ const targetId = parsed.targetId ? Number(parsed.targetId) : undefined;
185
+ const reason = parsed.reason || 'LLM decision';
186
+
187
+ if (action === 'NOOP') {
188
+ return { action: 'discard', targetId, reason: `LLM: ${reason}` };
189
+ }
190
+ if (action === 'ADD') {
191
+ return { action: 'new', reason: `LLM: ${reason}` };
192
+ }
193
+ if (action === 'UPDATE' && targetId) {
194
+ const existing = getObservation(targetId);
195
+ const oldFacts = existing?.facts ?? [];
196
+ return {
197
+ action: 'merge',
198
+ targetId,
199
+ reason: `LLM: ${reason}`,
200
+ mergedNarrative: parsed.mergedText || mergeNarratives(existing?.narrative ?? '', extracted.narrative),
201
+ mergedFacts: mergeFacts(oldFacts, extracted.facts),
202
+ };
203
+ }
204
+ if (action === 'DELETE' && targetId) {
205
+ const existing = getObservation(targetId);
206
+ const oldFacts = existing?.facts ?? [];
207
+ return {
208
+ action: 'evolve',
209
+ targetId,
210
+ reason: `LLM: ${reason}`,
211
+ mergedNarrative: extracted.narrative,
212
+ mergedFacts: mergeFacts(oldFacts, extracted.facts),
213
+ };
214
+ }
215
+
216
+ return null; // Unrecognized action
217
+ } catch {
218
+ return null; // LLM failure → fall back to rules
219
+ }
220
+ }
221
+
222
+ // ── Resolve Implementation ───────────────────────────────────────
223
+
224
+ /**
225
+ * Score a candidate match for resolution.
226
+ * Returns a composite score considering similarity, entity overlap, and content richness.
227
+ */
228
228
  function scoreCandidate(
229
229
  extracted: ExtractResult,
230
230
  candidate: SearchHit,
@@ -240,139 +240,139 @@ function scoreCandidate(
240
240
  const score = searchSimilarity * 0.6
241
241
  + (entityMatch ? 0.2 : 0)
242
242
  + contentOverlap * 0.2;
243
-
244
- // Is new memory richer?
245
- const newLength = extracted.narrative.length + extracted.facts.join(' ').length;
246
- const oldLength = candidate.narrative.length + candidate.facts.length;
247
- const richer = newLength > oldLength * 1.15;
248
-
249
- const contradiction = hasContradiction(candidate.narrative, extracted.narrative);
250
-
243
+
244
+ // Is new memory richer?
245
+ const newLength = extracted.narrative.length + extracted.facts.join(' ').length;
246
+ const oldLength = candidate.narrative.length + candidate.facts.length;
247
+ const richer = newLength > oldLength * 1.15;
248
+
249
+ const contradiction = hasContradiction(candidate.narrative, extracted.narrative);
250
+
251
251
  return { score, searchSimilarity, entityMatch, richer, contradiction };
252
252
  }
253
-
254
- /**
255
- * Run Stage 2: Resolve.
256
- *
257
- * Determines the resolution action for an enriched memory by comparing
258
- * it against existing memories found via search.
259
- */
260
- export async function runResolve(
261
- extracted: ExtractResult,
262
- projectId: string,
263
- searchMemories: (query: string, limit: number, projectId: string) => Promise<SearchHit[]>,
264
- getObservation: (id: number) => ExistingMemoryRef | null,
265
- useLLM = false,
266
- ): Promise<ResolveResult> {
267
- // Search for similar existing memories
268
- const query = `${extracted.title} ${extracted.narrative.substring(0, 200)}`;
269
- let hits: SearchHit[];
270
- try {
271
- hits = await searchMemories(query, 5, projectId);
272
- } catch {
273
- // Search failed — default to ADD
274
- return { action: 'new', reason: 'Search unavailable, defaulting to new' };
275
- }
276
-
277
- if (hits.length === 0) {
278
- return { action: 'new', reason: 'No similar existing memories found' };
279
- }
280
-
281
- // LLM-powered resolution (Mem0-style, quality-first)
282
- if (useLLM) {
283
- const llmResult = await resolveWithLLM(extracted, hits, getObservation);
284
- if (llmResult) return llmResult;
285
- // LLM failed → fall through to rules-based resolution
286
- }
287
-
288
- // Rules-based resolution (free mode fallback)
289
- const scored = hits.map(hit => ({
290
- hit,
291
- ...scoreCandidate(extracted, hit),
292
- }));
293
-
294
- // Sort by composite score descending
295
- scored.sort((a, b) => b.score - a.score);
296
- const best = scored[0];
297
-
298
- // ── Decision logic ──
299
-
253
+
254
+ /**
255
+ * Run Stage 2: Resolve.
256
+ *
257
+ * Determines the resolution action for an enriched memory by comparing
258
+ * it against existing memories found via search.
259
+ */
260
+ export async function runResolve(
261
+ extracted: ExtractResult,
262
+ projectId: string,
263
+ searchMemories: (query: string, limit: number, projectId: string) => Promise<SearchHit[]>,
264
+ getObservation: (id: number) => ExistingMemoryRef | null,
265
+ useLLM = false,
266
+ ): Promise<ResolveResult> {
267
+ // Search for similar existing memories
268
+ const query = `${extracted.title} ${extracted.narrative.substring(0, 200)}`;
269
+ let hits: SearchHit[];
270
+ try {
271
+ hits = await searchMemories(query, 5, projectId);
272
+ } catch {
273
+ // Search failed — default to ADD
274
+ return { action: 'new', reason: 'Search unavailable, defaulting to new' };
275
+ }
276
+
277
+ if (hits.length === 0) {
278
+ return { action: 'new', reason: 'No similar existing memories found' };
279
+ }
280
+
281
+ // LLM-powered resolution (Mem0-style, quality-first)
282
+ if (useLLM) {
283
+ const llmResult = await resolveWithLLM(extracted, hits, getObservation);
284
+ if (llmResult) return llmResult;
285
+ // LLM failed → fall through to rules-based resolution
286
+ }
287
+
288
+ // Rules-based resolution (free mode fallback)
289
+ const scored = hits.map(hit => ({
290
+ hit,
291
+ ...scoreCandidate(extracted, hit),
292
+ }));
293
+
294
+ // Sort by composite score descending
295
+ scored.sort((a, b) => b.score - a.score);
296
+ const best = scored[0];
297
+
298
+ // ── Decision logic ──
299
+
300
300
  // Very high normalized search similarity → likely duplicate.
301
301
  // Raw backend ranking scores must not be compared to 0..1 thresholds.
302
302
  if (best.searchSimilarity >= SIMILARITY_DUPLICATE) {
303
- if (best.richer) {
304
- // New is richer → evolve (supersede)
305
- const existing = getObservation(best.hit.observationId);
306
- const oldFacts = existing?.facts ?? best.hit.facts.split('\n').filter(Boolean);
307
- return {
308
- action: 'evolve',
309
- targetId: best.hit.observationId,
310
- reason: `Near-duplicate of #${best.hit.observationId} but richer content (score: ${best.score.toFixed(2)})`,
311
- mergedNarrative: mergeNarratives(best.hit.narrative, extracted.narrative),
312
- mergedFacts: mergeFacts(oldFacts, extracted.facts),
313
- };
314
- }
315
- return {
316
- action: 'discard',
317
- targetId: best.hit.observationId,
318
- reason: `Duplicate of #${best.hit.observationId} (score: ${best.score.toFixed(2)})`,
319
- };
320
- }
321
-
322
- // High similarity → same topic
323
- if (best.score >= SIMILARITY_HIGH) {
324
- if (best.contradiction) {
325
- // Content contradicts existing → evolve
326
- const existing = getObservation(best.hit.observationId);
327
- const oldFacts = existing?.facts ?? best.hit.facts.split('\n').filter(Boolean);
328
- return {
329
- action: 'evolve',
330
- targetId: best.hit.observationId,
331
- reason: `Supersedes #${best.hit.observationId}: contradiction detected (score: ${best.score.toFixed(2)})`,
332
- mergedNarrative: extracted.narrative,
333
- mergedFacts: mergeFacts(oldFacts, extracted.facts),
334
- };
335
- }
336
-
337
- if (best.richer) {
338
- // New is richer → merge
339
- const existing = getObservation(best.hit.observationId);
340
- const oldFacts = existing?.facts ?? best.hit.facts.split('\n').filter(Boolean);
341
- return {
342
- action: 'merge',
343
- targetId: best.hit.observationId,
344
- reason: `Merging with #${best.hit.observationId}: same topic, new content is richer (score: ${best.score.toFixed(2)})`,
345
- mergedNarrative: mergeNarratives(best.hit.narrative, extracted.narrative),
346
- mergedFacts: mergeFacts(oldFacts, extracted.facts),
347
- };
348
- }
349
-
350
- // Not richer → discard
351
- return {
352
- action: 'discard',
353
- targetId: best.hit.observationId,
354
- reason: `Already covered by #${best.hit.observationId} (score: ${best.score.toFixed(2)})`,
355
- };
356
- }
357
-
358
- // Medium similarity + entity match → merge
359
- if (best.score >= SIMILARITY_MEDIUM && best.entityMatch) {
360
- const existing = getObservation(best.hit.observationId);
361
- const oldFacts = existing?.facts ?? best.hit.facts.split('\n').filter(Boolean);
362
- const newFactCount = extracted.facts.length;
363
- const oldFactCount = oldFacts.length;
364
-
365
- if (newFactCount > oldFactCount) {
366
- return {
367
- action: 'merge',
368
- targetId: best.hit.observationId,
369
- reason: `Same entity "${extracted.entityName}", new memory has more facts (${newFactCount} > ${oldFactCount})`,
370
- mergedNarrative: mergeNarratives(best.hit.narrative, extracted.narrative),
371
- mergedFacts: mergeFacts(oldFacts, extracted.facts),
372
- };
373
- }
374
- }
375
-
376
- // Low similarity or different entity → new memory
377
- return { action: 'new', reason: `Different from existing memories (best score: ${best.score.toFixed(2)})` };
378
- }
303
+ if (best.richer) {
304
+ // New is richer → evolve (supersede)
305
+ const existing = getObservation(best.hit.observationId);
306
+ const oldFacts = existing?.facts ?? best.hit.facts.split('\n').filter(Boolean);
307
+ return {
308
+ action: 'evolve',
309
+ targetId: best.hit.observationId,
310
+ reason: `Near-duplicate of #${best.hit.observationId} but richer content (score: ${best.score.toFixed(2)})`,
311
+ mergedNarrative: mergeNarratives(best.hit.narrative, extracted.narrative),
312
+ mergedFacts: mergeFacts(oldFacts, extracted.facts),
313
+ };
314
+ }
315
+ return {
316
+ action: 'discard',
317
+ targetId: best.hit.observationId,
318
+ reason: `Duplicate of #${best.hit.observationId} (score: ${best.score.toFixed(2)})`,
319
+ };
320
+ }
321
+
322
+ // High similarity → same topic
323
+ if (best.score >= SIMILARITY_HIGH) {
324
+ if (best.contradiction) {
325
+ // Content contradicts existing → evolve
326
+ const existing = getObservation(best.hit.observationId);
327
+ const oldFacts = existing?.facts ?? best.hit.facts.split('\n').filter(Boolean);
328
+ return {
329
+ action: 'evolve',
330
+ targetId: best.hit.observationId,
331
+ reason: `Supersedes #${best.hit.observationId}: contradiction detected (score: ${best.score.toFixed(2)})`,
332
+ mergedNarrative: extracted.narrative,
333
+ mergedFacts: mergeFacts(oldFacts, extracted.facts),
334
+ };
335
+ }
336
+
337
+ if (best.richer) {
338
+ // New is richer → merge
339
+ const existing = getObservation(best.hit.observationId);
340
+ const oldFacts = existing?.facts ?? best.hit.facts.split('\n').filter(Boolean);
341
+ return {
342
+ action: 'merge',
343
+ targetId: best.hit.observationId,
344
+ reason: `Merging with #${best.hit.observationId}: same topic, new content is richer (score: ${best.score.toFixed(2)})`,
345
+ mergedNarrative: mergeNarratives(best.hit.narrative, extracted.narrative),
346
+ mergedFacts: mergeFacts(oldFacts, extracted.facts),
347
+ };
348
+ }
349
+
350
+ // Not richer → discard
351
+ return {
352
+ action: 'discard',
353
+ targetId: best.hit.observationId,
354
+ reason: `Already covered by #${best.hit.observationId} (score: ${best.score.toFixed(2)})`,
355
+ };
356
+ }
357
+
358
+ // Medium similarity + entity match → merge
359
+ if (best.score >= SIMILARITY_MEDIUM && best.entityMatch) {
360
+ const existing = getObservation(best.hit.observationId);
361
+ const oldFacts = existing?.facts ?? best.hit.facts.split('\n').filter(Boolean);
362
+ const newFactCount = extracted.facts.length;
363
+ const oldFactCount = oldFacts.length;
364
+
365
+ if (newFactCount > oldFactCount) {
366
+ return {
367
+ action: 'merge',
368
+ targetId: best.hit.observationId,
369
+ reason: `Same entity "${extracted.entityName}", new memory has more facts (${newFactCount} > ${oldFactCount})`,
370
+ mergedNarrative: mergeNarratives(best.hit.narrative, extracted.narrative),
371
+ mergedFacts: mergeFacts(oldFacts, extracted.facts),
372
+ };
373
+ }
374
+ }
375
+
376
+ // Low similarity or different entity → new memory
377
+ return { action: 'new', reason: `Different from existing memories (best score: ${best.score.toFixed(2)})` };
378
+ }