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.
- package/CHANGELOG.md +13 -0
- package/CLAUDE.md +6 -1
- package/README.md +21 -0
- package/README.zh-CN.md +21 -0
- package/TEAM.md +86 -86
- package/dist/cli/index.js +730 -150
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/static/index.html +201 -201
- package/dist/dashboard/static/style.css +3584 -3584
- package/dist/index.js +15 -8
- package/dist/index.js.map +1 -1
- package/dist/memcode-runtime/CHANGELOG.md +13 -0
- package/dist/memcode-runtime/package.json +4 -4
- package/dist/sdk.js +15 -8
- package/dist/sdk.js.map +1 -1
- package/docs/AGENT_OPERATOR_PLAYBOOK.md +18 -0
- package/docs/DESIGN_DECISIONS.md +357 -357
- package/docs/SETUP.md +10 -0
- package/docs/dev-log/progress.txt +35 -26
- package/package.json +1 -1
- package/src/audit/index.ts +156 -156
- package/src/cli/commands/agent-integrations.ts +623 -0
- package/src/cli/commands/audit-list.ts +89 -89
- package/src/cli/commands/background.ts +659 -659
- package/src/cli/commands/cleanup.ts +255 -255
- package/src/cli/commands/doctor.ts +23 -0
- package/src/cli/commands/formation.ts +48 -48
- package/src/cli/commands/git-hook-install.ts +111 -111
- package/src/cli/commands/handoff.ts +66 -66
- package/src/cli/commands/hooks-status.ts +63 -63
- package/src/cli/commands/ingest-commit.ts +153 -153
- package/src/cli/commands/ingest-image.ts +73 -73
- package/src/cli/commands/ingest-log.ts +180 -180
- package/src/cli/commands/ingest.ts +44 -44
- package/src/cli/commands/integrate-shared.ts +15 -15
- package/src/cli/commands/lock.ts +96 -96
- package/src/cli/commands/message.ts +121 -121
- package/src/cli/commands/poll.ts +70 -70
- package/src/cli/commands/purge-all-memory.ts +85 -85
- package/src/cli/commands/purge-project-memory.ts +83 -83
- package/src/cli/commands/reasoning.ts +132 -132
- package/src/cli/commands/repair.ts +60 -0
- package/src/cli/commands/retention.ts +108 -108
- package/src/cli/commands/serve-shared.ts +118 -118
- package/src/cli/commands/setup.ts +3 -3
- package/src/cli/commands/skills.ts +123 -123
- package/src/cli/commands/task.ts +192 -192
- package/src/cli/commands/transfer.ts +73 -73
- package/src/cli/commands/uninstall-project-artifacts.ts +85 -85
- package/src/cli/index.ts +3 -1
- package/src/cli/tui/ChatView.tsx +234 -234
- package/src/cli/tui/CommandBar.tsx +312 -312
- package/src/cli/tui/ContextRail.tsx +118 -118
- package/src/cli/tui/HeaderBar.tsx +72 -72
- package/src/cli/tui/LogoBanner.tsx +51 -51
- package/src/cli/tui/Panels.tsx +632 -632
- package/src/cli/tui/Sidebar.tsx +179 -179
- package/src/cli/tui/chat-service.ts +742 -742
- package/src/cli/tui/data.ts +547 -547
- package/src/cli/tui/index.ts +41 -41
- package/src/cli/tui/markdown-render.tsx +371 -371
- package/src/cli/tui/theme.ts +178 -178
- package/src/cli/tui/use-mouse.ts +157 -157
- package/src/cli/tui/useNavigation.ts +56 -56
- package/src/cli/update-checker.ts +211 -211
- package/src/cli/version.ts +7 -7
- package/src/cli/workbench.ts +1 -1
- package/src/codegraph/context-pack.ts +5 -1
- package/src/codegraph/lite-provider.ts +5 -1
- package/src/codegraph/project-context.ts +5 -1
- package/src/compact/token-budget.ts +74 -74
- package/src/config/behavior.ts +59 -59
- package/src/dashboard/project-classification.ts +64 -64
- package/src/dashboard/static/index.html +201 -201
- package/src/dashboard/static/style.css +3584 -3584
- package/src/embedding/fastembed-provider.ts +142 -142
- package/src/embedding/transformers-provider.ts +111 -111
- package/src/git/extractor.ts +209 -209
- package/src/git/hooks-path.ts +85 -85
- package/src/git/noise-filter.ts +210 -210
- package/src/hooks/installers/index.ts +4 -4
- package/src/hooks/official-skills.ts +1 -1
- package/src/hooks/pattern-detector.ts +173 -173
- package/src/hooks/rules/memorix-agent-rules.md +2 -2
- package/src/hooks/significance-filter.ts +250 -250
- package/src/llm/memory-manager.ts +328 -328
- package/src/llm/provider.ts +885 -885
- package/src/llm/quality.ts +248 -248
- package/src/memory/attribution-guard.ts +249 -249
- package/src/memory/auto-relations.ts +107 -107
- package/src/memory/consolidation.ts +302 -302
- package/src/memory/disclosure-policy.ts +141 -141
- package/src/memory/entity-extractor.ts +197 -197
- package/src/memory/formation/evaluate.ts +217 -217
- package/src/memory/formation/extract.ts +361 -361
- package/src/memory/formation/index.ts +417 -417
- package/src/memory/formation/resolve.ts +344 -344
- package/src/memory/formation/types.ts +315 -315
- package/src/memory/freshness.ts +122 -122
- package/src/memory/graph.ts +197 -197
- package/src/memory/refs.ts +94 -94
- package/src/memory/retention.ts +433 -433
- package/src/memory/secret-filter.ts +79 -79
- package/src/memory/session.ts +523 -523
- package/src/multimodal/image-loader.ts +143 -143
- package/src/orchestrate/adapters/claude-stream.ts +192 -192
- package/src/orchestrate/adapters/claude.ts +111 -111
- package/src/orchestrate/adapters/codex-stream.ts +134 -134
- package/src/orchestrate/adapters/codex.ts +41 -41
- package/src/orchestrate/adapters/gemini-stream.ts +166 -166
- package/src/orchestrate/adapters/gemini.ts +42 -42
- package/src/orchestrate/adapters/index.ts +73 -73
- package/src/orchestrate/adapters/opencode-stream.ts +143 -143
- package/src/orchestrate/adapters/opencode.ts +47 -47
- package/src/orchestrate/adapters/spawn-helper.ts +286 -286
- package/src/orchestrate/adapters/types.ts +77 -77
- package/src/orchestrate/capability-router.ts +284 -284
- package/src/orchestrate/context-compact.ts +188 -188
- package/src/orchestrate/cost-tracker.ts +219 -219
- package/src/orchestrate/error-recovery.ts +191 -191
- package/src/orchestrate/evidence.ts +140 -140
- package/src/orchestrate/ledger.ts +110 -110
- package/src/orchestrate/memorix-bridge.ts +380 -380
- package/src/orchestrate/output-budget.ts +80 -80
- package/src/orchestrate/permission.ts +152 -152
- package/src/orchestrate/pipeline-trace.ts +131 -131
- package/src/orchestrate/prompt-builder.ts +155 -155
- package/src/orchestrate/ring-buffer.ts +37 -37
- package/src/orchestrate/task-graph.ts +389 -389
- package/src/orchestrate/verify-gate.ts +219 -219
- package/src/orchestrate/worktree.ts +232 -232
- package/src/project/aliases.ts +374 -374
- package/src/project/detector.ts +268 -268
- package/src/rules/adapters/claude-code.ts +99 -99
- package/src/rules/adapters/codex.ts +97 -97
- package/src/rules/adapters/copilot.ts +124 -124
- package/src/rules/adapters/cursor.ts +114 -114
- package/src/rules/adapters/kiro.ts +126 -126
- package/src/rules/adapters/trae.ts +56 -56
- package/src/rules/adapters/windsurf.ts +83 -83
- package/src/rules/syncer.ts +235 -235
- package/src/sdk.ts +327 -327
- package/src/search/intent-detector.ts +289 -289
- package/src/search/query-expansion.ts +52 -52
- package/src/server/formation-timeout.ts +27 -27
- package/src/skills/mini-skills.ts +386 -386
- package/src/store/chat-store.ts +119 -119
- package/src/store/file-lock.ts +100 -100
- package/src/store/graph-store.ts +249 -249
- package/src/store/mini-skill-store.ts +349 -349
- package/src/store/obs-store.ts +255 -255
- package/src/store/persistence-json.ts +212 -212
- package/src/store/persistence.ts +291 -291
- package/src/store/project-affinity.ts +195 -195
- package/src/store/session-store.ts +259 -259
- package/src/store/sqlite-store.ts +339 -339
- package/src/team/event-bus.ts +76 -76
- package/src/team/file-locks.ts +173 -173
- package/src/team/handoff.ts +167 -167
- package/src/team/messages.ts +203 -203
- package/src/team/poll.ts +132 -132
- package/src/team/tasks.ts +211 -211
- package/src/wiki/generator.ts +237 -237
- package/src/wiki/knowledge-graph.ts +334 -334
- package/src/wiki/types.ts +85 -85
- package/src/workspace/mcp-adapters/codex.ts +191 -191
- package/src/workspace/mcp-adapters/copilot.ts +105 -105
- package/src/workspace/mcp-adapters/cursor.ts +53 -53
- package/src/workspace/mcp-adapters/kiro.ts +64 -64
- package/src/workspace/mcp-adapters/opencode.ts +123 -123
- package/src/workspace/mcp-adapters/trae.ts +134 -134
- package/src/workspace/mcp-adapters/windsurf.ts +91 -91
- package/src/workspace/sanitizer.ts +60 -60
- 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
|
+
}
|