memorix 1.2.0 → 1.2.2

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 (212) hide show
  1. package/CHANGELOG.md +30 -1
  2. package/README.md +18 -4
  3. package/README.zh-CN.md +18 -4
  4. package/TEAM.md +86 -86
  5. package/dist/cli/index.js +15919 -14055
  6. package/dist/cli/index.js.map +1 -1
  7. package/dist/index.js +1997 -1021
  8. package/dist/index.js.map +1 -1
  9. package/dist/maintenance-runner.d.ts +1 -1
  10. package/dist/maintenance-runner.js +8481 -8005
  11. package/dist/maintenance-runner.js.map +1 -1
  12. package/dist/memcode-runtime/CHANGELOG.md +30 -1
  13. package/dist/sdk.d.ts +7 -2
  14. package/dist/sdk.js +2022 -1024
  15. package/dist/sdk.js.map +1 -1
  16. package/dist/types.d.ts +49 -1
  17. package/dist/types.js.map +1 -1
  18. package/docs/1.2.2-MEMORY-CONTROL-PLANE.md +434 -0
  19. package/docs/AGENT_OPERATOR_PLAYBOOK.md +4 -0
  20. package/docs/API_REFERENCE.md +27 -5
  21. package/docs/DESIGN_DECISIONS.md +357 -357
  22. package/docs/DEVELOPMENT.md +4 -0
  23. package/docs/README.md +1 -1
  24. package/docs/SETUP.md +7 -1
  25. package/docs/dev-log/progress.txt +91 -11
  26. package/docs/knowledge/workflows/memorix-release.md +57 -0
  27. package/package.json +1 -1
  28. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  29. package/src/audit/index.ts +156 -156
  30. package/src/cli/command-guide.ts +192 -0
  31. package/src/cli/commands/audit-list.ts +89 -89
  32. package/src/cli/commands/audit.ts +9 -4
  33. package/src/cli/commands/background.ts +659 -659
  34. package/src/cli/commands/cleanup.ts +5 -1
  35. package/src/cli/commands/codegraph.ts +17 -8
  36. package/src/cli/commands/context.ts +3 -2
  37. package/src/cli/commands/doctor.ts +4 -2
  38. package/src/cli/commands/explain.ts +9 -3
  39. package/src/cli/commands/formation.ts +48 -48
  40. package/src/cli/commands/git-hook-install.ts +111 -111
  41. package/src/cli/commands/handoff.ts +75 -61
  42. package/src/cli/commands/hooks-status.ts +63 -63
  43. package/src/cli/commands/identity.ts +116 -0
  44. package/src/cli/commands/ingest-commit.ts +153 -153
  45. package/src/cli/commands/ingest-image.ts +71 -69
  46. package/src/cli/commands/ingest-log.ts +180 -180
  47. package/src/cli/commands/ingest.ts +44 -44
  48. package/src/cli/commands/integrate-shared.ts +15 -15
  49. package/src/cli/commands/knowledge.ts +40 -0
  50. package/src/cli/commands/lock.ts +93 -92
  51. package/src/cli/commands/memory.ts +58 -21
  52. package/src/cli/commands/message.ts +123 -118
  53. package/src/cli/commands/operator-shared.ts +98 -3
  54. package/src/cli/commands/poll.ts +74 -64
  55. package/src/cli/commands/purge-all-memory.ts +85 -85
  56. package/src/cli/commands/purge-project-memory.ts +83 -83
  57. package/src/cli/commands/reasoning.ts +135 -121
  58. package/src/cli/commands/retention.ts +9 -4
  59. package/src/cli/commands/serve-http.ts +22 -43
  60. package/src/cli/commands/serve-shared.ts +118 -118
  61. package/src/cli/commands/session.ts +29 -3
  62. package/src/cli/commands/setup.ts +9 -3
  63. package/src/cli/commands/skills.ts +124 -119
  64. package/src/cli/commands/status.ts +4 -3
  65. package/src/cli/commands/task.ts +193 -184
  66. package/src/cli/commands/team.ts +14 -10
  67. package/src/cli/commands/transfer.ts +108 -55
  68. package/src/cli/commands/uninstall-project-artifacts.ts +85 -85
  69. package/src/cli/identity.ts +89 -0
  70. package/src/cli/index.ts +96 -19
  71. package/src/cli/invocation.ts +115 -0
  72. package/src/cli/tui/ChatView.tsx +234 -234
  73. package/src/cli/tui/CommandBar.tsx +312 -312
  74. package/src/cli/tui/ContextRail.tsx +118 -118
  75. package/src/cli/tui/HeaderBar.tsx +72 -72
  76. package/src/cli/tui/LogoBanner.tsx +51 -51
  77. package/src/cli/tui/Sidebar.tsx +179 -179
  78. package/src/cli/tui/chat-service.ts +41 -18
  79. package/src/cli/tui/data.ts +23 -44
  80. package/src/cli/tui/index.ts +41 -41
  81. package/src/cli/tui/markdown-render.tsx +371 -371
  82. package/src/cli/tui/operator-context.ts +60 -0
  83. package/src/cli/tui/use-mouse.ts +157 -157
  84. package/src/cli/tui/useNavigation.ts +56 -56
  85. package/src/cli/tui/views/MemoryView.tsx +10 -8
  86. package/src/cli/update-checker.ts +211 -211
  87. package/src/cli/version.ts +7 -7
  88. package/src/cli/workbench.ts +1 -1
  89. package/src/codegraph/auto-context.ts +34 -17
  90. package/src/codegraph/context-pack.ts +1 -0
  91. package/src/codegraph/current-facts.ts +19 -1
  92. package/src/codegraph/project-context.ts +2 -0
  93. package/src/codegraph/task-lens.ts +49 -5
  94. package/src/compact/engine.ts +26 -10
  95. package/src/compact/index-format.ts +25 -2
  96. package/src/compact/token-budget.ts +74 -74
  97. package/src/dashboard/project-classification.ts +64 -64
  98. package/src/dashboard/server.ts +58 -52
  99. package/src/embedding/fastembed-provider.ts +142 -142
  100. package/src/embedding/transformers-provider.ts +111 -111
  101. package/src/git/extractor.ts +209 -209
  102. package/src/git/hooks-path.ts +85 -85
  103. package/src/hooks/admission.ts +117 -0
  104. package/src/hooks/handler.ts +98 -91
  105. package/src/hooks/pattern-detector.ts +173 -173
  106. package/src/hooks/significance-filter.ts +250 -250
  107. package/src/knowledge/claims.ts +51 -1
  108. package/src/knowledge/context-assembly.ts +97 -0
  109. package/src/knowledge/types.ts +1 -0
  110. package/src/knowledge/workflows.ts +34 -3
  111. package/src/knowledge/workset.ts +179 -10
  112. package/src/llm/memory-manager.ts +328 -328
  113. package/src/llm/provider.ts +885 -885
  114. package/src/llm/quality.ts +248 -248
  115. package/src/memory/admission.ts +57 -0
  116. package/src/memory/attribution-guard.ts +249 -249
  117. package/src/memory/auto-relations.ts +21 -0
  118. package/src/memory/consolidation.ts +13 -2
  119. package/src/memory/disclosure-policy.ts +140 -135
  120. package/src/memory/entity-extractor.ts +197 -197
  121. package/src/memory/export-import.ts +11 -3
  122. package/src/memory/formation/evaluate.ts +217 -217
  123. package/src/memory/formation/extract.ts +361 -361
  124. package/src/memory/formation/index.ts +417 -417
  125. package/src/memory/formation/resolve.ts +344 -344
  126. package/src/memory/formation/types.ts +315 -315
  127. package/src/memory/freshness.ts +122 -122
  128. package/src/memory/graph-context.ts +8 -2
  129. package/src/memory/graph-scope.ts +46 -0
  130. package/src/memory/graph.ts +197 -197
  131. package/src/memory/observations.ts +162 -4
  132. package/src/memory/quality-audit.ts +2 -0
  133. package/src/memory/refs.ts +94 -94
  134. package/src/memory/retention.ts +22 -2
  135. package/src/memory/secret-filter.ts +79 -79
  136. package/src/memory/session.ts +5 -2
  137. package/src/memory/visibility.ts +80 -0
  138. package/src/multimodal/image-loader.ts +143 -143
  139. package/src/orchestrate/adapters/claude-stream.ts +192 -192
  140. package/src/orchestrate/adapters/claude.ts +111 -111
  141. package/src/orchestrate/adapters/codex-stream.ts +134 -134
  142. package/src/orchestrate/adapters/codex.ts +41 -41
  143. package/src/orchestrate/adapters/gemini-stream.ts +166 -166
  144. package/src/orchestrate/adapters/gemini.ts +42 -42
  145. package/src/orchestrate/adapters/index.ts +73 -73
  146. package/src/orchestrate/adapters/opencode-stream.ts +143 -143
  147. package/src/orchestrate/adapters/opencode.ts +47 -47
  148. package/src/orchestrate/adapters/spawn-helper.ts +286 -286
  149. package/src/orchestrate/adapters/types.ts +77 -77
  150. package/src/orchestrate/capability-router.ts +284 -284
  151. package/src/orchestrate/context-compact.ts +188 -188
  152. package/src/orchestrate/cost-tracker.ts +219 -219
  153. package/src/orchestrate/error-recovery.ts +191 -191
  154. package/src/orchestrate/evidence.ts +140 -140
  155. package/src/orchestrate/ledger.ts +110 -110
  156. package/src/orchestrate/memorix-bridge.ts +378 -340
  157. package/src/orchestrate/output-budget.ts +80 -80
  158. package/src/orchestrate/permission.ts +152 -152
  159. package/src/orchestrate/pipeline-trace.ts +131 -131
  160. package/src/orchestrate/prompt-builder.ts +155 -155
  161. package/src/orchestrate/ring-buffer.ts +37 -37
  162. package/src/orchestrate/task-graph.ts +389 -389
  163. package/src/orchestrate/verify-gate.ts +33 -10
  164. package/src/orchestrate/worktree.ts +232 -232
  165. package/src/project/aliases.ts +374 -374
  166. package/src/project/detector.ts +268 -268
  167. package/src/rules/adapters/claude-code.ts +99 -99
  168. package/src/rules/adapters/codex.ts +97 -97
  169. package/src/rules/adapters/copilot.ts +124 -124
  170. package/src/rules/adapters/cursor.ts +114 -114
  171. package/src/rules/adapters/kiro.ts +126 -126
  172. package/src/rules/adapters/trae.ts +56 -56
  173. package/src/rules/adapters/windsurf.ts +83 -83
  174. package/src/rules/syncer.ts +235 -235
  175. package/src/runtime/control-plane-maintenance.ts +1 -0
  176. package/src/runtime/isolated-maintenance.ts +1 -0
  177. package/src/runtime/lifecycle.ts +18 -0
  178. package/src/runtime/maintenance-jobs.ts +1 -0
  179. package/src/runtime/maintenance-runner.ts +2 -0
  180. package/src/runtime/project-maintenance.ts +89 -0
  181. package/src/sdk.ts +334 -304
  182. package/src/search/intent-detector.ts +289 -289
  183. package/src/search/query-expansion.ts +52 -52
  184. package/src/server/formation-timeout.ts +27 -27
  185. package/src/server.ts +334 -93
  186. package/src/skills/mini-skills.ts +386 -386
  187. package/src/store/chat-store.ts +119 -119
  188. package/src/store/graph-store.ts +249 -249
  189. package/src/store/mini-skill-store.ts +349 -349
  190. package/src/store/orama-store.ts +61 -6
  191. package/src/store/persistence-json.ts +212 -212
  192. package/src/store/persistence.ts +291 -291
  193. package/src/store/project-affinity.ts +195 -195
  194. package/src/store/sqlite-db.ts +23 -1
  195. package/src/store/sqlite-store.ts +12 -2
  196. package/src/team/event-bus.ts +76 -76
  197. package/src/team/file-locks.ts +173 -173
  198. package/src/team/handoff.ts +168 -161
  199. package/src/team/messages.ts +203 -203
  200. package/src/team/poll.ts +132 -132
  201. package/src/team/tasks.ts +211 -211
  202. package/src/types.ts +51 -0
  203. package/src/wiki/generator.ts +2 -0
  204. package/src/workspace/mcp-adapters/codex.ts +191 -191
  205. package/src/workspace/mcp-adapters/copilot.ts +105 -105
  206. package/src/workspace/mcp-adapters/cursor.ts +53 -53
  207. package/src/workspace/mcp-adapters/kiro.ts +64 -64
  208. package/src/workspace/mcp-adapters/opencode.ts +123 -123
  209. package/src/workspace/mcp-adapters/trae.ts +134 -134
  210. package/src/workspace/mcp-adapters/windsurf.ts +91 -91
  211. package/src/workspace/sanitizer.ts +60 -60
  212. 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
+ }