memorix 1.1.7 → 1.1.9

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 (185) hide show
  1. package/CHANGELOG.md +22 -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 +852 -214
  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 +129 -62
  11. package/dist/index.js.map +1 -1
  12. package/dist/memcode-runtime/CHANGELOG.md +22 -0
  13. package/dist/memcode-runtime/package.json +4 -4
  14. package/dist/sdk.js +129 -62
  15. package/dist/sdk.js.map +1 -1
  16. package/docs/AGENT_OPERATOR_PLAYBOOK.md +18 -0
  17. package/docs/API_REFERENCE.md +2 -0
  18. package/docs/CONFIGURATION.md +18 -0
  19. package/docs/DESIGN_DECISIONS.md +357 -357
  20. package/docs/SETUP.md +10 -0
  21. package/docs/dev-log/progress.txt +23 -30
  22. package/package.json +1 -1
  23. package/src/audit/index.ts +156 -156
  24. package/src/cli/commands/agent-integrations.ts +623 -0
  25. package/src/cli/commands/audit-list.ts +89 -89
  26. package/src/cli/commands/background.ts +659 -659
  27. package/src/cli/commands/cleanup.ts +255 -255
  28. package/src/cli/commands/codegraph.ts +4 -0
  29. package/src/cli/commands/config-get.ts +9 -2
  30. package/src/cli/commands/doctor.ts +26 -0
  31. package/src/cli/commands/formation.ts +48 -48
  32. package/src/cli/commands/git-hook-install.ts +111 -111
  33. package/src/cli/commands/handoff.ts +66 -66
  34. package/src/cli/commands/hooks-status.ts +63 -63
  35. package/src/cli/commands/ingest-commit.ts +153 -153
  36. package/src/cli/commands/ingest-image.ts +73 -73
  37. package/src/cli/commands/ingest-log.ts +180 -180
  38. package/src/cli/commands/ingest.ts +44 -44
  39. package/src/cli/commands/integrate-shared.ts +15 -15
  40. package/src/cli/commands/lock.ts +96 -96
  41. package/src/cli/commands/message.ts +121 -121
  42. package/src/cli/commands/poll.ts +70 -70
  43. package/src/cli/commands/purge-all-memory.ts +85 -85
  44. package/src/cli/commands/purge-project-memory.ts +83 -83
  45. package/src/cli/commands/reasoning.ts +132 -132
  46. package/src/cli/commands/repair.ts +60 -0
  47. package/src/cli/commands/retention.ts +108 -108
  48. package/src/cli/commands/serve-shared.ts +118 -118
  49. package/src/cli/commands/setup.ts +3 -3
  50. package/src/cli/commands/skills.ts +123 -123
  51. package/src/cli/commands/task.ts +192 -192
  52. package/src/cli/commands/transfer.ts +73 -73
  53. package/src/cli/commands/uninstall-project-artifacts.ts +85 -85
  54. package/src/cli/index.ts +3 -1
  55. package/src/cli/tui/ChatView.tsx +234 -234
  56. package/src/cli/tui/CommandBar.tsx +312 -312
  57. package/src/cli/tui/ContextRail.tsx +118 -118
  58. package/src/cli/tui/HeaderBar.tsx +72 -72
  59. package/src/cli/tui/LogoBanner.tsx +51 -51
  60. package/src/cli/tui/Panels.tsx +632 -632
  61. package/src/cli/tui/Sidebar.tsx +179 -179
  62. package/src/cli/tui/chat-service.ts +742 -742
  63. package/src/cli/tui/data.ts +547 -547
  64. package/src/cli/tui/index.ts +41 -41
  65. package/src/cli/tui/markdown-render.tsx +371 -371
  66. package/src/cli/tui/theme.ts +178 -178
  67. package/src/cli/tui/use-mouse.ts +157 -157
  68. package/src/cli/tui/useNavigation.ts +56 -56
  69. package/src/cli/update-checker.ts +211 -211
  70. package/src/cli/version.ts +7 -7
  71. package/src/cli/workbench.ts +1 -1
  72. package/src/codegraph/auto-context.ts +6 -0
  73. package/src/codegraph/context-pack.ts +7 -6
  74. package/src/codegraph/exclude.ts +47 -0
  75. package/src/codegraph/lite-provider.ts +5 -24
  76. package/src/codegraph/project-context.ts +13 -15
  77. package/src/compact/token-budget.ts +74 -74
  78. package/src/config/behavior.ts +59 -59
  79. package/src/config/resolved-config.ts +6 -0
  80. package/src/config/toml-loader.ts +4 -0
  81. package/src/config/yaml-loader.ts +7 -0
  82. package/src/dashboard/project-classification.ts +64 -64
  83. package/src/dashboard/static/index.html +201 -201
  84. package/src/dashboard/static/style.css +3584 -3584
  85. package/src/embedding/fastembed-provider.ts +142 -142
  86. package/src/embedding/transformers-provider.ts +111 -111
  87. package/src/git/extractor.ts +209 -209
  88. package/src/git/hooks-path.ts +85 -85
  89. package/src/git/noise-filter.ts +210 -210
  90. package/src/hooks/installers/index.ts +4 -4
  91. package/src/hooks/official-skills.ts +1 -1
  92. package/src/hooks/pattern-detector.ts +173 -173
  93. package/src/hooks/rules/memorix-agent-rules.md +2 -2
  94. package/src/hooks/significance-filter.ts +250 -250
  95. package/src/llm/memory-manager.ts +328 -328
  96. package/src/llm/provider.ts +885 -885
  97. package/src/llm/quality.ts +248 -248
  98. package/src/memory/attribution-guard.ts +249 -249
  99. package/src/memory/auto-relations.ts +107 -107
  100. package/src/memory/consolidation.ts +302 -302
  101. package/src/memory/disclosure-policy.ts +141 -141
  102. package/src/memory/entity-extractor.ts +197 -197
  103. package/src/memory/formation/evaluate.ts +217 -217
  104. package/src/memory/formation/extract.ts +361 -361
  105. package/src/memory/formation/index.ts +417 -417
  106. package/src/memory/formation/resolve.ts +344 -344
  107. package/src/memory/formation/types.ts +315 -315
  108. package/src/memory/freshness.ts +122 -122
  109. package/src/memory/graph.ts +197 -197
  110. package/src/memory/refs.ts +94 -94
  111. package/src/memory/retention.ts +433 -433
  112. package/src/memory/secret-filter.ts +79 -79
  113. package/src/memory/session.ts +523 -523
  114. package/src/multimodal/image-loader.ts +143 -143
  115. package/src/orchestrate/adapters/claude-stream.ts +192 -192
  116. package/src/orchestrate/adapters/claude.ts +111 -111
  117. package/src/orchestrate/adapters/codex-stream.ts +134 -134
  118. package/src/orchestrate/adapters/codex.ts +41 -41
  119. package/src/orchestrate/adapters/gemini-stream.ts +166 -166
  120. package/src/orchestrate/adapters/gemini.ts +42 -42
  121. package/src/orchestrate/adapters/index.ts +73 -73
  122. package/src/orchestrate/adapters/opencode-stream.ts +143 -143
  123. package/src/orchestrate/adapters/opencode.ts +47 -47
  124. package/src/orchestrate/adapters/spawn-helper.ts +286 -286
  125. package/src/orchestrate/adapters/types.ts +77 -77
  126. package/src/orchestrate/capability-router.ts +284 -284
  127. package/src/orchestrate/context-compact.ts +188 -188
  128. package/src/orchestrate/cost-tracker.ts +219 -219
  129. package/src/orchestrate/error-recovery.ts +191 -191
  130. package/src/orchestrate/evidence.ts +140 -140
  131. package/src/orchestrate/ledger.ts +110 -110
  132. package/src/orchestrate/memorix-bridge.ts +380 -380
  133. package/src/orchestrate/output-budget.ts +80 -80
  134. package/src/orchestrate/permission.ts +152 -152
  135. package/src/orchestrate/pipeline-trace.ts +131 -131
  136. package/src/orchestrate/prompt-builder.ts +155 -155
  137. package/src/orchestrate/ring-buffer.ts +37 -37
  138. package/src/orchestrate/task-graph.ts +389 -389
  139. package/src/orchestrate/verify-gate.ts +219 -219
  140. package/src/orchestrate/worktree.ts +232 -232
  141. package/src/project/aliases.ts +374 -374
  142. package/src/project/detector.ts +268 -268
  143. package/src/rules/adapters/claude-code.ts +99 -99
  144. package/src/rules/adapters/codex.ts +97 -97
  145. package/src/rules/adapters/copilot.ts +124 -124
  146. package/src/rules/adapters/cursor.ts +114 -114
  147. package/src/rules/adapters/kiro.ts +126 -126
  148. package/src/rules/adapters/trae.ts +56 -56
  149. package/src/rules/adapters/windsurf.ts +83 -83
  150. package/src/rules/syncer.ts +235 -235
  151. package/src/sdk.ts +327 -327
  152. package/src/search/intent-detector.ts +289 -289
  153. package/src/search/query-expansion.ts +52 -52
  154. package/src/server/formation-timeout.ts +27 -27
  155. package/src/server.ts +3 -0
  156. package/src/skills/mini-skills.ts +386 -386
  157. package/src/store/chat-store.ts +119 -119
  158. package/src/store/file-lock.ts +100 -100
  159. package/src/store/graph-store.ts +249 -249
  160. package/src/store/mini-skill-store.ts +349 -349
  161. package/src/store/obs-store.ts +255 -255
  162. package/src/store/orama-store.ts +15 -8
  163. package/src/store/persistence-json.ts +212 -212
  164. package/src/store/persistence.ts +291 -291
  165. package/src/store/project-affinity.ts +195 -195
  166. package/src/store/session-store.ts +259 -259
  167. package/src/store/sqlite-store.ts +339 -339
  168. package/src/team/event-bus.ts +76 -76
  169. package/src/team/file-locks.ts +173 -173
  170. package/src/team/handoff.ts +167 -167
  171. package/src/team/messages.ts +203 -203
  172. package/src/team/poll.ts +132 -132
  173. package/src/team/tasks.ts +211 -211
  174. package/src/wiki/generator.ts +237 -237
  175. package/src/wiki/knowledge-graph.ts +334 -334
  176. package/src/wiki/types.ts +85 -85
  177. package/src/workspace/mcp-adapters/codex.ts +191 -191
  178. package/src/workspace/mcp-adapters/copilot.ts +105 -105
  179. package/src/workspace/mcp-adapters/cursor.ts +53 -53
  180. package/src/workspace/mcp-adapters/kiro.ts +64 -64
  181. package/src/workspace/mcp-adapters/opencode.ts +123 -123
  182. package/src/workspace/mcp-adapters/trae.ts +134 -134
  183. package/src/workspace/mcp-adapters/windsurf.ts +91 -91
  184. package/src/workspace/sanitizer.ts +60 -60
  185. 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
+ }