@199-bio/engram 0.4.3 → 0.5.0

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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consolidator.d.ts","sourceRoot":"","sources":["../../src/consolidation/consolidator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,EAAE,cAAc,EAAU,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAoCxE,UAAU,kBAAkB;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2BAA2B,CAAC,EAAE,MAAM,CAAC;CACtC;AAED,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,EAAE,CAAiB;gBAEf,EAAE,EAAE,cAAc;IAS9B,YAAY,IAAI,OAAO;IAIvB;;;OAGG;IACG,WAAW,CAAC,OAAO,GAAE,kBAAuB,GAAG,OAAO,CAAC;QAC3D,cAAc,EAAE,MAAM,CAAC;QACvB,mBAAmB,EAAE,MAAM,CAAC;QAC5B,iBAAiB,EAAE,MAAM,CAAC;KAC3B,CAAC;IAwEF;;OAEG;YACW,gBAAgB;IA4D9B;;OAEG;IACG,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IA8GjE;;OAEG;IACH,SAAS,IAAI;QACX,UAAU,EAAE,OAAO,CAAC;QACpB,sBAAsB,EAAE,MAAM,CAAC;QAC/B,YAAY,EAAE,MAAM,CAAC;QACrB,wBAAwB,EAAE,MAAM,CAAC;KAClC;CAYF"}
package/dist/index.js CHANGED
@@ -42,7 +42,7 @@ async function initialize() {
42
42
  // ============ MCP Server ============
43
43
  const server = new Server({
44
44
  name: "engram",
45
- version: "0.4.3",
45
+ version: "0.5.0",
46
46
  }, {
47
47
  capabilities: {
48
48
  tools: {},
@@ -1 +1 @@
1
- {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/storage/database.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/storage/database.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/web/server.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAMH,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAiBtD,UAAU,gBAAgB;IACxB,EAAE,EAAE,cAAc,CAAC;IACnB,KAAK,EAAE,cAAc,CAAC;IACtB,MAAM,EAAE,YAAY,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,EAAE,CAAiB;IAC3B,OAAO,CAAC,KAAK,CAAiB;IAC9B,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,IAAI,CAAc;IAC1B,OAAO,CAAC,IAAI,CAAS;gBAET,OAAO,EAAE,gBAAgB;IAY/B,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;IAkB9B,IAAI,IAAI,IAAI;YAOE,aAAa;YA+Bb,SAAS;YAwMT,WAAW;IAgCzB,OAAO,CAAC,SAAS;CAclB"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/web/server.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAMH,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAkBtD,UAAU,gBAAgB;IACxB,EAAE,EAAE,cAAc,CAAC;IACnB,KAAK,EAAE,cAAc,CAAC;IACtB,MAAM,EAAE,YAAY,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,EAAE,CAAiB;IAC3B,OAAO,CAAC,KAAK,CAAiB;IAC9B,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,IAAI,CAAc;IAC1B,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,IAAI,CAAS;gBAET,OAAO,EAAE,gBAAgB;IAa/B,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;IAkB9B,IAAI,IAAI,IAAI;YAOE,aAAa;YA+Bb,SAAS;YA8ST,WAAW;IAgCzB,OAAO,CAAC,SAAS;CAclB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@199-bio/engram",
3
- "version": "0.4.3",
3
+ "version": "0.5.0",
4
4
  "description": "Give Claude a perfect memory. Local-first MCP server with hybrid search.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,349 @@
1
+ /**
2
+ * Memory Consolidator
3
+ *
4
+ * Uses Opus 4.5 with extended thinking to consolidate memories into digests
5
+ * and detect contradictions. Inspired by how the brain consolidates
6
+ * short-term memories into long-term storage during sleep.
7
+ *
8
+ * Levels:
9
+ * - L1: Session digests (consolidate recent memories)
10
+ * - L2: Topic clusters (group related digests)
11
+ * - L3: Entity profiles (comprehensive view of each entity)
12
+ */
13
+
14
+ import Anthropic from "@anthropic-ai/sdk";
15
+ import { EngramDatabase, Memory, Digest } from "../storage/database.js";
16
+
17
+ const CONSOLIDATION_SYSTEM = `You are a memory consolidation system. Your job is to:
18
+
19
+ 1. CONSOLIDATE: Take a batch of related memories and produce a concise summary that preserves all important facts, dates, names, and relationships. Be factual and precise.
20
+
21
+ 2. DETECT CONTRADICTIONS: If any memories contain conflicting information (e.g., different ages, dates, locations, or facts about the same topic), identify them clearly.
22
+
23
+ Output JSON with this structure:
24
+ {
25
+ "digest": "Your consolidated summary here. Include all key facts, dates, names. Be concise but complete.",
26
+ "topic": "A short topic label (2-5 words)",
27
+ "contradictions": [
28
+ {
29
+ "description": "Clear description of the contradiction",
30
+ "memory_ids": ["id1", "id2"]
31
+ }
32
+ ]
33
+ }
34
+
35
+ Rules:
36
+ - Preserve specific details: names, numbers, dates, locations
37
+ - Use present tense for current facts, past tense for past events
38
+ - If memories are about a person, structure the digest around that person
39
+ - Only flag true contradictions (not just incomplete information)
40
+ - Be concise - consolidate 10 memories into 2-3 sentences typically`;
41
+
42
+ interface ConsolidationResult {
43
+ digest: string;
44
+ topic: string;
45
+ contradictions: Array<{
46
+ description: string;
47
+ memory_ids: string[];
48
+ }>;
49
+ }
50
+
51
+ interface ConsolidateOptions {
52
+ batchSize?: number;
53
+ minMemoriesForConsolidation?: number;
54
+ }
55
+
56
+ export class Consolidator {
57
+ private client: Anthropic | null = null;
58
+ private db: EngramDatabase;
59
+
60
+ constructor(db: EngramDatabase) {
61
+ this.db = db;
62
+
63
+ const apiKey = process.env.ANTHROPIC_API_KEY;
64
+ if (apiKey) {
65
+ this.client = new Anthropic({ apiKey });
66
+ }
67
+ }
68
+
69
+ isConfigured(): boolean {
70
+ return this.client !== null;
71
+ }
72
+
73
+ /**
74
+ * Run consolidation on unconsolidated memories
75
+ * Returns number of digests created and contradictions found
76
+ */
77
+ async consolidate(options: ConsolidateOptions = {}): Promise<{
78
+ digestsCreated: number;
79
+ contradictionsFound: number;
80
+ memoriesProcessed: number;
81
+ }> {
82
+ if (!this.client) {
83
+ throw new Error("Consolidator not configured - set ANTHROPIC_API_KEY");
84
+ }
85
+
86
+ const { batchSize = 15, minMemoriesForConsolidation = 5 } = options;
87
+
88
+ // Get unconsolidated memories
89
+ const memories = this.db.getUnconsolidatedMemories(undefined, 100);
90
+
91
+ if (memories.length < minMemoriesForConsolidation) {
92
+ return {
93
+ digestsCreated: 0,
94
+ contradictionsFound: 0,
95
+ memoriesProcessed: 0,
96
+ };
97
+ }
98
+
99
+ let digestsCreated = 0;
100
+ let contradictionsFound = 0;
101
+ let memoriesProcessed = 0;
102
+
103
+ // Process in batches
104
+ for (let i = 0; i < memories.length; i += batchSize) {
105
+ const batch = memories.slice(i, i + batchSize);
106
+ if (batch.length < 3) break; // Skip tiny batches
107
+
108
+ try {
109
+ const result = await this.consolidateBatch(batch);
110
+
111
+ if (result) {
112
+ // Create digest
113
+ const memoryIds = batch.map((m) => m.id);
114
+ const periodStart = new Date(
115
+ Math.min(...batch.map((m) => m.timestamp.getTime()))
116
+ );
117
+ const periodEnd = new Date(
118
+ Math.max(...batch.map((m) => m.timestamp.getTime()))
119
+ );
120
+
121
+ this.db.createDigest(result.digest, 1, memoryIds, {
122
+ topic: result.topic,
123
+ periodStart,
124
+ periodEnd,
125
+ });
126
+ digestsCreated++;
127
+ memoriesProcessed += batch.length;
128
+
129
+ // Create contradictions
130
+ for (const c of result.contradictions) {
131
+ if (c.memory_ids.length >= 2) {
132
+ // Find the actual memory IDs from our batch
133
+ const [idA, idB] = c.memory_ids.slice(0, 2);
134
+ const memA = batch.find((m) => m.id === idA);
135
+ const memB = batch.find((m) => m.id === idB);
136
+
137
+ if (memA && memB) {
138
+ this.db.createContradiction(memA.id, memB.id, c.description);
139
+ contradictionsFound++;
140
+ }
141
+ }
142
+ }
143
+ }
144
+ } catch (error) {
145
+ console.error("[Consolidator] Batch consolidation failed:", error);
146
+ // Continue with next batch
147
+ }
148
+ }
149
+
150
+ return { digestsCreated, contradictionsFound, memoriesProcessed };
151
+ }
152
+
153
+ /**
154
+ * Consolidate a batch of memories using Opus 4.5 with extended thinking
155
+ */
156
+ private async consolidateBatch(
157
+ memories: Memory[]
158
+ ): Promise<ConsolidationResult | null> {
159
+ if (!this.client) return null;
160
+
161
+ // Format memories for the prompt
162
+ const memoriesText = memories
163
+ .map(
164
+ (m) =>
165
+ `[${m.id}] (${m.timestamp.toISOString().split("T")[0]}) ${m.content}`
166
+ )
167
+ .join("\n\n");
168
+
169
+ const userPrompt = `Consolidate these ${memories.length} memories into a single digest. Identify any contradictions.
170
+
171
+ MEMORIES:
172
+ ${memoriesText}
173
+
174
+ Respond with JSON only.`;
175
+
176
+ try {
177
+ const response = await this.client.messages.create({
178
+ model: "claude-opus-4-5-20251101",
179
+ max_tokens: 16000,
180
+ thinking: {
181
+ type: "enabled",
182
+ budget_tokens: 4000,
183
+ },
184
+ messages: [
185
+ {
186
+ role: "user",
187
+ content: userPrompt,
188
+ },
189
+ ],
190
+ system: CONSOLIDATION_SYSTEM,
191
+ });
192
+
193
+ // Extract text response (skip thinking blocks)
194
+ let text = "";
195
+ for (const block of response.content) {
196
+ if (block.type === "text") {
197
+ text = block.text;
198
+ break;
199
+ }
200
+ }
201
+
202
+ if (!text) return null;
203
+
204
+ // Parse JSON response
205
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
206
+ if (!jsonMatch) return null;
207
+
208
+ const result = JSON.parse(jsonMatch[0]) as ConsolidationResult;
209
+ return result;
210
+ } catch (error) {
211
+ console.error("[Consolidator] API call failed:", error);
212
+ return null;
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Create an entity profile by consolidating all observations about an entity
218
+ */
219
+ async consolidateEntity(entityId: string): Promise<Digest | null> {
220
+ if (!this.client) {
221
+ throw new Error("Consolidator not configured - set ANTHROPIC_API_KEY");
222
+ }
223
+
224
+ const entity = this.db.getEntity(entityId);
225
+ if (!entity) return null;
226
+
227
+ const observations = this.db.getEntityObservations(entityId);
228
+ if (observations.length < 2) return null;
229
+
230
+ // Get source memories for each observation
231
+ const memories: Memory[] = [];
232
+ for (const obs of observations) {
233
+ if (obs.source_memory_id) {
234
+ const mem = this.db.getMemory(obs.source_memory_id);
235
+ if (mem) memories.push(mem);
236
+ }
237
+ }
238
+
239
+ if (memories.length < 2) return null;
240
+
241
+ // Consolidate with entity context
242
+ const memoriesText = memories
243
+ .map(
244
+ (m) =>
245
+ `[${m.id}] (${m.timestamp.toISOString().split("T")[0]}) ${m.content}`
246
+ )
247
+ .join("\n\n");
248
+
249
+ const userPrompt = `Create a comprehensive profile for the entity "${entity.name}" (${entity.type}) based on these memories. Include all known facts, relationships, preferences, and history.
250
+
251
+ MEMORIES ABOUT ${entity.name}:
252
+ ${memoriesText}
253
+
254
+ Respond with JSON only.`;
255
+
256
+ try {
257
+ const response = await this.client.messages.create({
258
+ model: "claude-opus-4-5-20251101",
259
+ max_tokens: 16000,
260
+ thinking: {
261
+ type: "enabled",
262
+ budget_tokens: 6000,
263
+ },
264
+ messages: [
265
+ {
266
+ role: "user",
267
+ content: userPrompt,
268
+ },
269
+ ],
270
+ system: CONSOLIDATION_SYSTEM,
271
+ });
272
+
273
+ let text = "";
274
+ for (const block of response.content) {
275
+ if (block.type === "text") {
276
+ text = block.text;
277
+ break;
278
+ }
279
+ }
280
+
281
+ if (!text) return null;
282
+
283
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
284
+ if (!jsonMatch) return null;
285
+
286
+ const result = JSON.parse(jsonMatch[0]) as ConsolidationResult;
287
+
288
+ // Create level 3 entity profile digest
289
+ const memoryIds = memories.map((m) => m.id);
290
+ const periodStart = new Date(
291
+ Math.min(...memories.map((m) => m.timestamp.getTime()))
292
+ );
293
+ const periodEnd = new Date(
294
+ Math.max(...memories.map((m) => m.timestamp.getTime()))
295
+ );
296
+
297
+ const digest = this.db.createDigest(result.digest, 3, memoryIds, {
298
+ topic: `Profile: ${entity.name}`,
299
+ entityId: entity.id,
300
+ periodStart,
301
+ periodEnd,
302
+ });
303
+
304
+ // Record any contradictions
305
+ for (const c of result.contradictions) {
306
+ if (c.memory_ids.length >= 2) {
307
+ const [idA, idB] = c.memory_ids.slice(0, 2);
308
+ const memA = memories.find((m) => m.id === idA);
309
+ const memB = memories.find((m) => m.id === idB);
310
+
311
+ if (memA && memB) {
312
+ this.db.createContradiction(
313
+ memA.id,
314
+ memB.id,
315
+ c.description,
316
+ entity.id
317
+ );
318
+ }
319
+ }
320
+ }
321
+
322
+ return digest;
323
+ } catch (error) {
324
+ console.error("[Consolidator] Entity profile failed:", error);
325
+ return null;
326
+ }
327
+ }
328
+
329
+ /**
330
+ * Get consolidation status
331
+ */
332
+ getStatus(): {
333
+ configured: boolean;
334
+ unconsolidatedMemories: number;
335
+ totalDigests: number;
336
+ unresolvedContradictions: number;
337
+ } {
338
+ const unconsolidated = this.db.getUnconsolidatedMemories(undefined, 1000);
339
+ const digests = this.db.getDigests(undefined, 1000);
340
+ const contradictions = this.db.getContradictions(false, 1000);
341
+
342
+ return {
343
+ configured: this.isConfigured(),
344
+ unconsolidatedMemories: unconsolidated.length,
345
+ totalDigests: digests.length,
346
+ unresolvedContradictions: contradictions.length,
347
+ };
348
+ }
349
+ }
package/src/index.ts CHANGED
@@ -60,7 +60,7 @@ async function initialize(): Promise<void> {
60
60
  const server = new Server(
61
61
  {
62
62
  name: "engram",
63
- version: "0.4.3",
63
+ version: "0.5.0",
64
64
  },
65
65
  {
66
66
  capabilities: {
@@ -45,6 +45,30 @@ export interface Relation {
45
45
  created_at: Date;
46
46
  }
47
47
 
48
+ export interface Digest {
49
+ id: string;
50
+ content: string;
51
+ level: number; // 1 = session, 2 = topic, 3 = entity profile
52
+ topic: string | null;
53
+ entity_id: string | null;
54
+ source_count: number;
55
+ created_at: Date;
56
+ period_start: Date;
57
+ period_end: Date;
58
+ }
59
+
60
+ export interface Contradiction {
61
+ id: string;
62
+ entity_id: string | null;
63
+ memory_id_a: string;
64
+ memory_id_b: string;
65
+ description: string;
66
+ resolved: boolean;
67
+ resolution: string | null;
68
+ created_at: Date;
69
+ resolved_at: Date | null;
70
+ }
71
+
48
72
  export class EngramDatabase {
49
73
  private db: Database.Database;
50
74
  private stmtCache: Map<string, Database.Statement> = new Map();
@@ -154,6 +178,54 @@ export class EngramDatabase {
154
178
  CREATE INDEX IF NOT EXISTS idx_relations_to ON relations(to_entity);
155
179
  CREATE INDEX IF NOT EXISTS idx_relations_type ON relations(type);
156
180
  `);
181
+
182
+ // Digests table (consolidated memories)
183
+ this.db.exec(`
184
+ CREATE TABLE IF NOT EXISTS digests (
185
+ id TEXT PRIMARY KEY,
186
+ content TEXT NOT NULL,
187
+ level INTEGER DEFAULT 1,
188
+ topic TEXT,
189
+ entity_id TEXT REFERENCES entities(id) ON DELETE SET NULL,
190
+ source_count INTEGER DEFAULT 0,
191
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
192
+ period_start DATETIME,
193
+ period_end DATETIME
194
+ );
195
+
196
+ CREATE INDEX IF NOT EXISTS idx_digests_level ON digests(level);
197
+ CREATE INDEX IF NOT EXISTS idx_digests_entity ON digests(entity_id);
198
+ CREATE INDEX IF NOT EXISTS idx_digests_period ON digests(period_start, period_end);
199
+ `);
200
+
201
+ // Digest sources (links digests to their source memories)
202
+ this.db.exec(`
203
+ CREATE TABLE IF NOT EXISTS digest_sources (
204
+ digest_id TEXT NOT NULL REFERENCES digests(id) ON DELETE CASCADE,
205
+ memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
206
+ PRIMARY KEY (digest_id, memory_id)
207
+ );
208
+
209
+ CREATE INDEX IF NOT EXISTS idx_digest_sources_memory ON digest_sources(memory_id);
210
+ `);
211
+
212
+ // Contradictions table (detected conflicts)
213
+ this.db.exec(`
214
+ CREATE TABLE IF NOT EXISTS contradictions (
215
+ id TEXT PRIMARY KEY,
216
+ entity_id TEXT REFERENCES entities(id) ON DELETE SET NULL,
217
+ memory_id_a TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
218
+ memory_id_b TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
219
+ description TEXT NOT NULL,
220
+ resolved INTEGER DEFAULT 0,
221
+ resolution TEXT,
222
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
223
+ resolved_at DATETIME
224
+ );
225
+
226
+ CREATE INDEX IF NOT EXISTS idx_contradictions_entity ON contradictions(entity_id);
227
+ CREATE INDEX IF NOT EXISTS idx_contradictions_resolved ON contradictions(resolved);
228
+ `);
157
229
  }
158
230
 
159
231
  // ============ Memory Operations ============
@@ -628,6 +700,157 @@ export class EngramDatabase {
628
700
  return { entities, relations: allRelations, observations };
629
701
  }
630
702
 
703
+ // ============ Digest Operations ============
704
+
705
+ createDigest(
706
+ content: string,
707
+ level: number,
708
+ sourceMemoryIds: string[],
709
+ options: {
710
+ topic?: string;
711
+ entityId?: string;
712
+ periodStart?: Date;
713
+ periodEnd?: Date;
714
+ } = {}
715
+ ): Digest {
716
+ const id = randomUUID();
717
+ const stmt = this.db.prepare(`
718
+ INSERT INTO digests (id, content, level, topic, entity_id, source_count, period_start, period_end)
719
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
720
+ `);
721
+ stmt.run(
722
+ id,
723
+ content,
724
+ level,
725
+ options.topic || null,
726
+ options.entityId || null,
727
+ sourceMemoryIds.length,
728
+ options.periodStart?.toISOString() || null,
729
+ options.periodEnd?.toISOString() || null
730
+ );
731
+
732
+ // Link source memories
733
+ const linkStmt = this.db.prepare(
734
+ "INSERT OR IGNORE INTO digest_sources (digest_id, memory_id) VALUES (?, ?)"
735
+ );
736
+ for (const memoryId of sourceMemoryIds) {
737
+ linkStmt.run(id, memoryId);
738
+ }
739
+
740
+ return this.getDigest(id)!;
741
+ }
742
+
743
+ getDigest(id: string): Digest | null {
744
+ const row = this.stmt("SELECT * FROM digests WHERE id = ?").get(id) as Record<string, unknown> | undefined;
745
+ return row ? this.rowToDigest(row) : null;
746
+ }
747
+
748
+ getDigests(level?: number, limit: number = 100): Digest[] {
749
+ let sql = "SELECT * FROM digests";
750
+ const params: unknown[] = [];
751
+
752
+ if (level !== undefined) {
753
+ sql += " WHERE level = ?";
754
+ params.push(level);
755
+ }
756
+
757
+ sql += " ORDER BY created_at DESC LIMIT ?";
758
+ params.push(limit);
759
+
760
+ const rows = this.db.prepare(sql).all(...params) as Record<string, unknown>[];
761
+ return rows.map((row) => this.rowToDigest(row));
762
+ }
763
+
764
+ getDigestSources(digestId: string): Memory[] {
765
+ const rows = this.stmt(`
766
+ SELECT m.* FROM memories m
767
+ JOIN digest_sources ds ON ds.memory_id = m.id
768
+ WHERE ds.digest_id = ?
769
+ ORDER BY m.timestamp DESC
770
+ `).all(digestId) as Record<string, unknown>[];
771
+ return rows.map((row) => this.rowToMemory(row));
772
+ }
773
+
774
+ getUnconsolidatedMemories(since?: Date, limit: number = 100): Memory[] {
775
+ let sql = `
776
+ SELECT m.* FROM memories m
777
+ LEFT JOIN digest_sources ds ON ds.memory_id = m.id
778
+ WHERE ds.digest_id IS NULL
779
+ `;
780
+ const params: unknown[] = [];
781
+
782
+ if (since) {
783
+ sql += " AND m.timestamp >= ?";
784
+ params.push(since.toISOString());
785
+ }
786
+
787
+ sql += " ORDER BY m.timestamp DESC LIMIT ?";
788
+ params.push(limit);
789
+
790
+ const rows = this.db.prepare(sql).all(...params) as Record<string, unknown>[];
791
+ return rows.map((row) => this.rowToMemory(row));
792
+ }
793
+
794
+ deleteDigest(id: string): boolean {
795
+ const stmt = this.db.prepare("DELETE FROM digests WHERE id = ?");
796
+ const result = stmt.run(id);
797
+ return result.changes > 0;
798
+ }
799
+
800
+ // ============ Contradiction Operations ============
801
+
802
+ createContradiction(
803
+ memoryIdA: string,
804
+ memoryIdB: string,
805
+ description: string,
806
+ entityId?: string
807
+ ): Contradiction {
808
+ const id = randomUUID();
809
+ const stmt = this.db.prepare(`
810
+ INSERT INTO contradictions (id, entity_id, memory_id_a, memory_id_b, description)
811
+ VALUES (?, ?, ?, ?, ?)
812
+ `);
813
+ stmt.run(id, entityId || null, memoryIdA, memoryIdB, description);
814
+ return this.getContradiction(id)!;
815
+ }
816
+
817
+ getContradiction(id: string): Contradiction | null {
818
+ const row = this.stmt("SELECT * FROM contradictions WHERE id = ?").get(id) as Record<string, unknown> | undefined;
819
+ return row ? this.rowToContradiction(row) : null;
820
+ }
821
+
822
+ getContradictions(resolved?: boolean, limit: number = 100): Contradiction[] {
823
+ let sql = "SELECT * FROM contradictions";
824
+ const params: unknown[] = [];
825
+
826
+ if (resolved !== undefined) {
827
+ sql += " WHERE resolved = ?";
828
+ params.push(resolved ? 1 : 0);
829
+ }
830
+
831
+ sql += " ORDER BY created_at DESC LIMIT ?";
832
+ params.push(limit);
833
+
834
+ const rows = this.db.prepare(sql).all(...params) as Record<string, unknown>[];
835
+ return rows.map((row) => this.rowToContradiction(row));
836
+ }
837
+
838
+ resolveContradiction(id: string, resolution: string): boolean {
839
+ const stmt = this.db.prepare(`
840
+ UPDATE contradictions
841
+ SET resolved = 1, resolution = ?, resolved_at = CURRENT_TIMESTAMP
842
+ WHERE id = ?
843
+ `);
844
+ const result = stmt.run(resolution, id);
845
+ return result.changes > 0;
846
+ }
847
+
848
+ deleteContradiction(id: string): boolean {
849
+ const stmt = this.db.prepare("DELETE FROM contradictions WHERE id = ?");
850
+ const result = stmt.run(id);
851
+ return result.changes > 0;
852
+ }
853
+
631
854
  // ============ Statistics ============
632
855
 
633
856
  getStats(): {
@@ -635,15 +858,26 @@ export class EngramDatabase {
635
858
  entities: number;
636
859
  relations: number;
637
860
  observations: number;
861
+ digests: number;
862
+ contradictions: number;
638
863
  } {
639
- // Single query for all stats - much faster than 4 separate queries
864
+ // Single query for all stats - much faster than separate queries
640
865
  const row = this.stmt(`
641
866
  SELECT
642
867
  (SELECT COUNT(*) FROM memories) as memories,
643
868
  (SELECT COUNT(*) FROM entities) as entities,
644
869
  (SELECT COUNT(*) FROM relations) as relations,
645
- (SELECT COUNT(*) FROM observations) as observations
646
- `).get() as { memories: number; entities: number; relations: number; observations: number };
870
+ (SELECT COUNT(*) FROM observations) as observations,
871
+ (SELECT COUNT(*) FROM digests) as digests,
872
+ (SELECT COUNT(*) FROM contradictions WHERE resolved = 0) as contradictions
873
+ `).get() as {
874
+ memories: number;
875
+ entities: number;
876
+ relations: number;
877
+ observations: number;
878
+ digests: number;
879
+ contradictions: number;
880
+ };
647
881
 
648
882
  return row;
649
883
  }
@@ -710,4 +944,32 @@ export class EngramDatabase {
710
944
  created_at: new Date(row.created_at as string),
711
945
  };
712
946
  }
947
+
948
+ private rowToDigest(row: Record<string, unknown>): Digest {
949
+ return {
950
+ id: row.id as string,
951
+ content: row.content as string,
952
+ level: row.level as number,
953
+ topic: row.topic as string | null,
954
+ entity_id: row.entity_id as string | null,
955
+ source_count: row.source_count as number,
956
+ created_at: new Date(row.created_at as string),
957
+ period_start: row.period_start ? new Date(row.period_start as string) : new Date(),
958
+ period_end: row.period_end ? new Date(row.period_end as string) : new Date(),
959
+ };
960
+ }
961
+
962
+ private rowToContradiction(row: Record<string, unknown>): Contradiction {
963
+ return {
964
+ id: row.id as string,
965
+ entity_id: row.entity_id as string | null,
966
+ memory_id_a: row.memory_id_a as string,
967
+ memory_id_b: row.memory_id_b as string,
968
+ description: row.description as string,
969
+ resolved: Boolean(row.resolved),
970
+ resolution: row.resolution as string | null,
971
+ created_at: new Date(row.created_at as string),
972
+ resolved_at: row.resolved_at ? new Date(row.resolved_at as string) : null,
973
+ };
974
+ }
713
975
  }
package/src/web/server.ts CHANGED
@@ -11,6 +11,7 @@ import { EngramDatabase } from "../storage/database.js";
11
11
  import { KnowledgeGraph } from "../graph/knowledge-graph.js";
12
12
  import { HybridSearch } from "../retrieval/hybrid.js";
13
13
  import { ChatHandler } from "./chat-handler.js";
14
+ import { Consolidator } from "../consolidation/consolidator.js";
14
15
 
15
16
  const __filename = fileURLToPath(import.meta.url);
16
17
  const __dirname = path.dirname(__filename);
@@ -39,6 +40,7 @@ export class EngramWebServer {
39
40
  private graph: KnowledgeGraph;
40
41
  private search: HybridSearch;
41
42
  private chat: ChatHandler;
43
+ private consolidator: Consolidator;
42
44
  private port: number;
43
45
 
44
46
  constructor(options: WebServerOptions) {
@@ -50,6 +52,7 @@ export class EngramWebServer {
50
52
  graph: options.graph,
51
53
  search: options.search,
52
54
  });
55
+ this.consolidator = new Consolidator(options.db);
53
56
  this.port = options.port || 3847;
54
57
  }
55
58
 
@@ -304,6 +307,108 @@ export class EngramWebServer {
304
307
  return;
305
308
  }
306
309
 
310
+ // ============ Consolidation Endpoints ============
311
+
312
+ // GET /api/consolidation/status - get consolidation status
313
+ if (pathname === "/api/consolidation/status" && method === "GET") {
314
+ const status = this.consolidator.getStatus();
315
+ res.end(JSON.stringify(status));
316
+ return;
317
+ }
318
+
319
+ // POST /api/consolidation/run - run consolidation
320
+ if (pathname === "/api/consolidation/run" && method === "POST") {
321
+ if (!this.consolidator.isConfigured()) {
322
+ res.writeHead(503);
323
+ res.end(JSON.stringify({
324
+ error: "Consolidation not available - set ANTHROPIC_API_KEY",
325
+ }));
326
+ return;
327
+ }
328
+
329
+ try {
330
+ const result = await this.consolidator.consolidate();
331
+ res.end(JSON.stringify(result));
332
+ } catch (error) {
333
+ res.writeHead(500);
334
+ res.end(JSON.stringify({
335
+ error: error instanceof Error ? error.message : "Consolidation failed",
336
+ }));
337
+ }
338
+ return;
339
+ }
340
+
341
+ // GET /api/digests - list all digests
342
+ if (pathname === "/api/digests" && method === "GET") {
343
+ const level = url.searchParams.get("level");
344
+ const limit = parseInt(url.searchParams.get("limit") || "100");
345
+ const digests = this.db.getDigests(
346
+ level ? parseInt(level) : undefined,
347
+ limit
348
+ );
349
+ res.end(JSON.stringify({ digests }));
350
+ return;
351
+ }
352
+
353
+ // GET /api/digests/:id/sources - get source memories for a digest
354
+ const digestSourcesMatch = pathname.match(/^\/api\/digests\/([a-f0-9-]+)\/sources$/);
355
+ if (digestSourcesMatch && method === "GET") {
356
+ const id = digestSourcesMatch[1];
357
+ const sources = this.db.getDigestSources(id);
358
+ res.end(JSON.stringify({ sources }));
359
+ return;
360
+ }
361
+
362
+ // GET /api/contradictions - list contradictions
363
+ if (pathname === "/api/contradictions" && method === "GET") {
364
+ const resolved = url.searchParams.get("resolved");
365
+ const limit = parseInt(url.searchParams.get("limit") || "100");
366
+ const contradictions = this.db.getContradictions(
367
+ resolved !== null ? resolved === "true" : undefined,
368
+ limit
369
+ );
370
+
371
+ // Enrich with memory content
372
+ const enriched = contradictions.map((c) => {
373
+ const memA = this.db.getMemory(c.memory_id_a);
374
+ const memB = this.db.getMemory(c.memory_id_b);
375
+ const entity = c.entity_id ? this.db.getEntity(c.entity_id) : null;
376
+ return {
377
+ ...c,
378
+ memory_a: memA,
379
+ memory_b: memB,
380
+ entity: entity,
381
+ };
382
+ });
383
+
384
+ res.end(JSON.stringify({ contradictions: enriched }));
385
+ return;
386
+ }
387
+
388
+ // POST /api/contradictions/:id/resolve - resolve a contradiction
389
+ const resolveMatch = pathname.match(/^\/api\/contradictions\/([a-f0-9-]+)\/resolve$/);
390
+ if (resolveMatch && method === "POST") {
391
+ const id = resolveMatch[1];
392
+ const { resolution } = body as { resolution: string };
393
+ if (!resolution) {
394
+ res.writeHead(400);
395
+ res.end(JSON.stringify({ error: "Resolution is required" }));
396
+ return;
397
+ }
398
+ const success = this.db.resolveContradiction(id, resolution);
399
+ res.end(JSON.stringify({ success }));
400
+ return;
401
+ }
402
+
403
+ // DELETE /api/contradictions/:id - dismiss a contradiction
404
+ const contradictionMatch = pathname.match(/^\/api\/contradictions\/([a-f0-9-]+)$/);
405
+ if (contradictionMatch && method === "DELETE") {
406
+ const id = contradictionMatch[1];
407
+ const success = this.db.deleteContradiction(id);
408
+ res.end(JSON.stringify({ success }));
409
+ return;
410
+ }
411
+
307
412
  // 404 for unknown API routes
308
413
  res.writeHead(404);
309
414
  res.end(JSON.stringify({ error: "Not found" }));
@@ -14,6 +14,7 @@ const views = {
14
14
  memories: document.getElementById('memories-view'),
15
15
  entities: document.getElementById('entities-view'),
16
16
  graph: document.getElementById('graph-view'),
17
+ consolidation: document.getElementById('consolidation-view'),
17
18
  };
18
19
 
19
20
  const statsEl = document.getElementById('stats');
@@ -61,7 +62,14 @@ function formatDate(dateStr) {
61
62
  // Load stats
62
63
  async function loadStats() {
63
64
  const stats = await api('/api/stats');
64
- statsEl.textContent = `${stats.memories} memories \u00b7 ${stats.entities} entities \u00b7 ${stats.relations} relations`;
65
+ let text = `${stats.memories} memories \u00b7 ${stats.entities} entities \u00b7 ${stats.relations} relations`;
66
+ if (stats.digests > 0) {
67
+ text += ` \u00b7 ${stats.digests} digests`;
68
+ }
69
+ if (stats.contradictions > 0) {
70
+ text += ` \u00b7 ${stats.contradictions} contradictions`;
71
+ }
72
+ statsEl.textContent = text;
65
73
  }
66
74
 
67
75
  // Load memories
@@ -308,8 +316,213 @@ function switchView(view) {
308
316
  if (view === 'memories') loadMemories(searchInput.value);
309
317
  if (view === 'entities') loadEntities(entityTypeFilter.value);
310
318
  if (view === 'graph') loadGraph();
319
+ if (view === 'consolidation') loadConsolidation();
320
+ }
321
+
322
+ // ============ Consolidation ============
323
+
324
+ const contradictionsList = document.getElementById('contradictions-list');
325
+ const digestsList = document.getElementById('digests-list');
326
+ const unconsolidatedCount = document.getElementById('unconsolidated-count');
327
+ const digestsCount = document.getElementById('digests-count');
328
+ const contradictionsCount = document.getElementById('contradictions-count');
329
+ const runConsolidationBtn = document.getElementById('run-consolidation-btn');
330
+ const contradictionModal = document.getElementById('contradiction-modal');
331
+ const contradictionModalBody = document.getElementById('contradiction-modal-body');
332
+ const contradictionForm = document.getElementById('contradiction-form');
333
+ const contradictionResolution = document.getElementById('contradiction-resolution');
334
+
335
+ let currentContradictionId = null;
336
+
337
+ // Load consolidation view
338
+ async function loadConsolidation() {
339
+ await Promise.all([
340
+ loadConsolidationStatus(),
341
+ loadContradictions(),
342
+ loadDigests(),
343
+ ]);
344
+ }
345
+
346
+ // Load consolidation status
347
+ async function loadConsolidationStatus() {
348
+ try {
349
+ const status = await api('/api/consolidation/status');
350
+ unconsolidatedCount.textContent = status.unconsolidatedMemories;
351
+ digestsCount.textContent = status.totalDigests;
352
+ contradictionsCount.textContent = status.unresolvedContradictions;
353
+ runConsolidationBtn.disabled = !status.configured;
354
+ if (!status.configured) {
355
+ runConsolidationBtn.title = 'Set ANTHROPIC_API_KEY to enable';
356
+ }
357
+ } catch (e) {
358
+ console.error('Failed to load consolidation status', e);
359
+ }
360
+ }
361
+
362
+ // Load contradictions
363
+ async function loadContradictions() {
364
+ try {
365
+ const data = await api('/api/contradictions?resolved=false');
366
+
367
+ if (data.contradictions.length === 0) {
368
+ contradictionsList.innerHTML = '<div class="empty-state">No contradictions found</div>';
369
+ return;
370
+ }
371
+
372
+ contradictionsList.innerHTML = data.contradictions.map(c => `
373
+ <div class="list-item contradiction-item" data-id="${c.id}">
374
+ ${c.entity ? `<span class="entity-tag">${escapeHtml(c.entity.name)}</span>` : ''}
375
+ <div class="description">${escapeHtml(c.description)}</div>
376
+ <div class="memories">
377
+ <div class="memory-quote">
378
+ ${escapeHtml(c.memory_a?.content || 'Memory deleted')}
379
+ <span class="date">${c.memory_a ? formatDate(c.memory_a.timestamp) : ''}</span>
380
+ </div>
381
+ <div class="memory-quote">
382
+ ${escapeHtml(c.memory_b?.content || 'Memory deleted')}
383
+ <span class="date">${c.memory_b ? formatDate(c.memory_b.timestamp) : ''}</span>
384
+ </div>
385
+ </div>
386
+ <div class="actions">
387
+ <button class="resolve-btn" data-id="${c.id}">Resolve</button>
388
+ </div>
389
+ </div>
390
+ `).join('');
391
+
392
+ // Attach event listeners
393
+ contradictionsList.querySelectorAll('.resolve-btn').forEach(btn => {
394
+ btn.addEventListener('click', (e) => {
395
+ e.stopPropagation();
396
+ openContradictionModal(btn.dataset.id);
397
+ });
398
+ });
399
+ } catch (e) {
400
+ console.error('Failed to load contradictions', e);
401
+ contradictionsList.innerHTML = '<div class="empty-state">Failed to load contradictions</div>';
402
+ }
403
+ }
404
+
405
+ // Load digests
406
+ async function loadDigests() {
407
+ try {
408
+ const data = await api('/api/digests');
409
+
410
+ if (data.digests.length === 0) {
411
+ digestsList.innerHTML = '<div class="empty-state">No digests yet. Run consolidation to create summaries.</div>';
412
+ return;
413
+ }
414
+
415
+ digestsList.innerHTML = data.digests.map(d => `
416
+ <div class="list-item digest-item">
417
+ <div class="content">${escapeHtml(d.content)}</div>
418
+ <div class="meta">
419
+ ${d.topic ? `<span class="topic">${escapeHtml(d.topic)}</span>` : ''}
420
+ <span>Level ${d.level}</span>
421
+ <span>${d.source_count} memories</span>
422
+ <span>${formatDate(d.created_at)}</span>
423
+ </div>
424
+ </div>
425
+ `).join('');
426
+ } catch (e) {
427
+ console.error('Failed to load digests', e);
428
+ digestsList.innerHTML = '<div class="empty-state">Failed to load digests</div>';
429
+ }
430
+ }
431
+
432
+ // Run consolidation
433
+ async function runConsolidation() {
434
+ runConsolidationBtn.disabled = true;
435
+ runConsolidationBtn.textContent = 'Consolidating...';
436
+
437
+ try {
438
+ const result = await api('/api/consolidation/run', { method: 'POST' });
439
+ alert(`Consolidation complete!\n\nDigests created: ${result.digestsCreated}\nContradictions found: ${result.contradictionsFound}\nMemories processed: ${result.memoriesProcessed}`);
440
+ await loadConsolidation();
441
+ } catch (e) {
442
+ console.error('Consolidation failed', e);
443
+ alert('Consolidation failed. Check console for details.');
444
+ } finally {
445
+ runConsolidationBtn.disabled = false;
446
+ runConsolidationBtn.textContent = 'Run Consolidation';
447
+ }
448
+ }
449
+
450
+ // Open contradiction modal
451
+ function openContradictionModal(id) {
452
+ const item = contradictionsList.querySelector(`[data-id="${id}"]`);
453
+ if (!item) return;
454
+
455
+ currentContradictionId = id;
456
+
457
+ // Copy the memories to the modal
458
+ const description = item.querySelector('.description').textContent;
459
+ const memories = item.querySelectorAll('.memory-quote');
460
+
461
+ contradictionModalBody.innerHTML = `
462
+ <p><strong>${escapeHtml(description)}</strong></p>
463
+ <div class="memory-quote">${memories[0].innerHTML}</div>
464
+ <div class="memory-quote">${memories[1].innerHTML}</div>
465
+ `;
466
+
467
+ contradictionResolution.value = '';
468
+ contradictionModal.classList.remove('hidden');
469
+ }
470
+
471
+ // Close contradiction modal
472
+ function closeContradictionModal() {
473
+ contradictionModal.classList.add('hidden');
474
+ currentContradictionId = null;
475
+ }
476
+
477
+ // Resolve contradiction
478
+ async function resolveContradiction(resolution) {
479
+ if (!currentContradictionId || !resolution.trim()) return;
480
+
481
+ try {
482
+ await api(`/api/contradictions/${currentContradictionId}/resolve`, {
483
+ method: 'POST',
484
+ body: { resolution: resolution.trim() },
485
+ });
486
+ closeContradictionModal();
487
+ await loadConsolidation();
488
+ await loadStats();
489
+ } catch (e) {
490
+ console.error('Failed to resolve contradiction', e);
491
+ alert('Failed to resolve contradiction');
492
+ }
493
+ }
494
+
495
+ // Dismiss contradiction
496
+ async function dismissContradiction() {
497
+ if (!currentContradictionId) return;
498
+ if (!confirm('Dismiss this contradiction without resolution?')) return;
499
+
500
+ try {
501
+ await api(`/api/contradictions/${currentContradictionId}`, { method: 'DELETE' });
502
+ closeContradictionModal();
503
+ await loadConsolidation();
504
+ await loadStats();
505
+ } catch (e) {
506
+ console.error('Failed to dismiss contradiction', e);
507
+ alert('Failed to dismiss contradiction');
508
+ }
311
509
  }
312
510
 
511
+ // Event listeners for consolidation
512
+ runConsolidationBtn.addEventListener('click', runConsolidation);
513
+
514
+ contradictionForm.addEventListener('submit', (e) => {
515
+ e.preventDefault();
516
+ resolveContradiction(contradictionResolution.value);
517
+ });
518
+
519
+ document.getElementById('contradiction-cancel').addEventListener('click', closeContradictionModal);
520
+ document.getElementById('contradiction-dismiss').addEventListener('click', dismissContradiction);
521
+
522
+ contradictionModal.addEventListener('click', (e) => {
523
+ if (e.target === contradictionModal) closeContradictionModal();
524
+ });
525
+
313
526
  // Event listeners
314
527
  document.querySelectorAll('.nav-btn').forEach(btn => {
315
528
  btn.addEventListener('click', () => switchView(btn.dataset.view));
@@ -13,6 +13,7 @@
13
13
  <button class="nav-btn active" data-view="memories">Memories</button>
14
14
  <button class="nav-btn" data-view="entities">Entities</button>
15
15
  <button class="nav-btn" data-view="graph">Graph</button>
16
+ <button class="nav-btn" data-view="consolidation">Consolidation</button>
16
17
  </nav>
17
18
  <div class="stats" id="stats"></div>
18
19
  <button id="chat-toggle" class="chat-toggle" title="Open Chat Assistant">Chat</button>
@@ -53,6 +54,32 @@
53
54
  <section id="graph-view" class="view">
54
55
  <div id="graph-container"></div>
55
56
  </section>
57
+
58
+ <!-- Consolidation View -->
59
+ <section id="consolidation-view" class="view">
60
+ <div class="consolidation-header">
61
+ <div class="consolidation-status" id="consolidation-status">
62
+ <span class="status-item"><strong>Unconsolidated:</strong> <span id="unconsolidated-count">-</span></span>
63
+ <span class="status-item"><strong>Digests:</strong> <span id="digests-count">-</span></span>
64
+ <span class="status-item"><strong>Contradictions:</strong> <span id="contradictions-count">-</span></span>
65
+ </div>
66
+ <button id="run-consolidation-btn">Run Consolidation</button>
67
+ </div>
68
+
69
+ <div class="consolidation-sections">
70
+ <div class="section">
71
+ <h2>Contradictions</h2>
72
+ <p class="section-desc">Conflicting information detected during consolidation. Review and resolve.</p>
73
+ <div id="contradictions-list" class="list"></div>
74
+ </div>
75
+
76
+ <div class="section">
77
+ <h2>Digests</h2>
78
+ <p class="section-desc">Consolidated summaries of your memories.</p>
79
+ <div id="digests-list" class="list"></div>
80
+ </div>
81
+ </div>
82
+ </section>
56
83
  </main>
57
84
 
58
85
  <!-- Modal for adding/editing -->
@@ -91,6 +118,25 @@
91
118
  </div>
92
119
  </div>
93
120
 
121
+ <!-- Contradiction Resolution Modal -->
122
+ <div id="contradiction-modal" class="modal hidden">
123
+ <div class="modal-content modal-wide">
124
+ <h2>Resolve Contradiction</h2>
125
+ <div id="contradiction-modal-body"></div>
126
+ <form id="contradiction-form">
127
+ <label>
128
+ Resolution:
129
+ <textarea id="contradiction-resolution" placeholder="Explain how you resolved this contradiction..." rows="3"></textarea>
130
+ </label>
131
+ <div class="modal-actions">
132
+ <button type="button" id="contradiction-dismiss">Dismiss</button>
133
+ <button type="button" id="contradiction-cancel">Cancel</button>
134
+ <button type="submit">Resolve</button>
135
+ </div>
136
+ </form>
137
+ </div>
138
+ </div>
139
+
94
140
  <!-- Chat Panel -->
95
141
  <aside id="chat-panel" class="chat-panel hidden">
96
142
  <div class="chat-header">
@@ -638,6 +638,199 @@ body.chat-open main {
638
638
  margin-right: 400px;
639
639
  }
640
640
 
641
+ /* Consolidation View */
642
+ .consolidation-header {
643
+ display: flex;
644
+ justify-content: space-between;
645
+ align-items: center;
646
+ margin-bottom: 2rem;
647
+ padding: 1rem 1.5rem;
648
+ background: var(--bg-secondary);
649
+ border: 1px solid var(--border);
650
+ }
651
+
652
+ .consolidation-status {
653
+ display: flex;
654
+ gap: 2rem;
655
+ font-size: 0.9375rem;
656
+ }
657
+
658
+ .status-item {
659
+ color: var(--text-secondary);
660
+ }
661
+
662
+ .status-item strong {
663
+ color: var(--text-primary);
664
+ font-weight: 500;
665
+ }
666
+
667
+ #run-consolidation-btn {
668
+ background: var(--accent);
669
+ }
670
+
671
+ #run-consolidation-btn:hover {
672
+ background: var(--accent-hover);
673
+ }
674
+
675
+ #run-consolidation-btn:disabled {
676
+ background: var(--text-muted);
677
+ }
678
+
679
+ .consolidation-sections {
680
+ display: flex;
681
+ flex-direction: column;
682
+ gap: 2.5rem;
683
+ }
684
+
685
+ .consolidation-sections .section h2 {
686
+ margin-bottom: 0.5rem;
687
+ }
688
+
689
+ .section-desc {
690
+ font-size: 0.875rem;
691
+ color: var(--text-muted);
692
+ margin-bottom: 1rem;
693
+ }
694
+
695
+ /* Contradiction item */
696
+ .contradiction-item {
697
+ background: var(--bg-secondary);
698
+ border: 1px solid var(--border);
699
+ border-left: 3px solid var(--danger);
700
+ padding: 1.25rem 1.5rem;
701
+ }
702
+
703
+ .contradiction-item .description {
704
+ font-size: 1rem;
705
+ line-height: 1.6;
706
+ margin-bottom: 1rem;
707
+ font-weight: 500;
708
+ }
709
+
710
+ .contradiction-item .memories {
711
+ display: flex;
712
+ flex-direction: column;
713
+ gap: 0.75rem;
714
+ margin-bottom: 1rem;
715
+ }
716
+
717
+ .contradiction-item .memory-quote {
718
+ padding: 0.75rem 1rem;
719
+ background: var(--bg-tertiary);
720
+ font-size: 0.875rem;
721
+ line-height: 1.5;
722
+ color: var(--text-secondary);
723
+ }
724
+
725
+ .contradiction-item .memory-quote .date {
726
+ display: block;
727
+ font-size: 0.75rem;
728
+ color: var(--text-muted);
729
+ margin-top: 0.375rem;
730
+ }
731
+
732
+ .contradiction-item .entity-tag {
733
+ display: inline-block;
734
+ font-size: 0.75rem;
735
+ padding: 0.125rem 0.5rem;
736
+ background: var(--bg-tertiary);
737
+ color: var(--text-muted);
738
+ margin-bottom: 0.75rem;
739
+ }
740
+
741
+ .contradiction-item .actions {
742
+ padding-top: 1rem;
743
+ border-top: 1px solid var(--border);
744
+ }
745
+
746
+ .contradiction-item .actions button {
747
+ font-size: 0.8125rem;
748
+ padding: 0.5rem 1rem;
749
+ background: var(--text-primary);
750
+ color: var(--bg-primary);
751
+ }
752
+
753
+ /* Digest item */
754
+ .digest-item {
755
+ background: var(--bg-secondary);
756
+ border: 1px solid var(--border);
757
+ padding: 1.25rem 1.5rem;
758
+ }
759
+
760
+ .digest-item .content {
761
+ font-size: 1rem;
762
+ line-height: 1.7;
763
+ margin-bottom: 1rem;
764
+ }
765
+
766
+ .digest-item .meta {
767
+ display: flex;
768
+ gap: 1.5rem;
769
+ font-size: 0.8125rem;
770
+ color: var(--text-muted);
771
+ }
772
+
773
+ .digest-item .topic {
774
+ font-weight: 500;
775
+ color: var(--accent);
776
+ }
777
+
778
+ /* Modal wide */
779
+ .modal-wide {
780
+ max-width: 800px;
781
+ }
782
+
783
+ .modal-wide .memory-quote {
784
+ padding: 0.75rem 1rem;
785
+ background: var(--bg-secondary);
786
+ font-size: 0.875rem;
787
+ line-height: 1.5;
788
+ margin-bottom: 0.75rem;
789
+ border: 1px solid var(--border);
790
+ }
791
+
792
+ .modal-wide .memory-quote .date {
793
+ display: block;
794
+ font-size: 0.75rem;
795
+ color: var(--text-muted);
796
+ margin-top: 0.375rem;
797
+ }
798
+
799
+ .modal-wide label {
800
+ display: block;
801
+ margin-top: 1.5rem;
802
+ font-size: 0.875rem;
803
+ color: var(--text-secondary);
804
+ }
805
+
806
+ .modal-wide textarea {
807
+ width: 100%;
808
+ margin-top: 0.5rem;
809
+ font-family: inherit;
810
+ font-size: 0.9375rem;
811
+ padding: 0.75rem 1rem;
812
+ border: 1px solid var(--border);
813
+ background: var(--bg-secondary);
814
+ color: var(--text-primary);
815
+ resize: vertical;
816
+ }
817
+
818
+ .modal-wide textarea:focus {
819
+ outline: none;
820
+ border-color: var(--accent);
821
+ }
822
+
823
+ #contradiction-dismiss {
824
+ background: var(--bg-tertiary);
825
+ color: var(--text-secondary);
826
+ margin-right: auto;
827
+ }
828
+
829
+ #contradiction-dismiss:hover {
830
+ background: var(--border);
831
+ color: var(--text-primary);
832
+ }
833
+
641
834
  /* Responsive */
642
835
  @media (max-width: 640px) {
643
836
  header {