@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.
- package/dist/consolidation/consolidator.d.ts.map +1 -0
- package/dist/index.js +1 -1
- package/dist/storage/database.d.ts.map +1 -1
- package/dist/web/server.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/consolidation/consolidator.ts +349 -0
- package/src/index.ts +1 -1
- package/src/storage/database.ts +265 -3
- package/src/web/server.ts +105 -0
- package/src/web/static/app.js +214 -1
- package/src/web/static/index.html +46 -0
- package/src/web/static/style.css +193 -0
|
@@ -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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/storage/database.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAOH,MAAM,WAAW,MAAM;IACrB,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,IAAI,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,IAAI,GAAG,IAAI,CAAC;CAC5B;AAED,MAAM,WAAW,MAAM;IACrB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,QAAQ,GAAG,OAAO,GAAG,SAAS,GAAG,OAAO,GAAG,cAAc,CAAC;IAChE,UAAU,EAAE,IAAI,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CAC1C;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,IAAI,CAAC;IACjB,WAAW,EAAE,IAAI,GAAG,IAAI,CAAC;CAC1B;AAED,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC3C,UAAU,EAAE,IAAI,CAAC;CAClB;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,EAAE,CAAoB;IAC9B,OAAO,CAAC,SAAS,CAA8C;gBAEnD,MAAM,EAAE,MAAM;IAoB1B,OAAO,CAAC,UAAU;
|
|
1
|
+
{"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/storage/database.ts"],"names":[],"mappings":"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"}
|
package/dist/web/server.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
@@ -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
package/src/storage/database.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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" }));
|
package/src/web/static/app.js
CHANGED
|
@@ -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
|
-
|
|
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">
|
package/src/web/static/style.css
CHANGED
|
@@ -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 {
|