zouroboros-memory 3.0.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/LICENSE +21 -0
- package/dist/capture.d.ts +57 -0
- package/dist/capture.js +181 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +91 -0
- package/dist/conflict-resolver.d.ts +55 -0
- package/dist/conflict-resolver.js +221 -0
- package/dist/context-budget.d.ts +94 -0
- package/dist/context-budget.js +272 -0
- package/dist/cross-persona.d.ts +31 -0
- package/dist/cross-persona.js +188 -0
- package/dist/database.d.ts +35 -0
- package/dist/database.js +189 -0
- package/dist/embedding-benchmark.d.ts +12 -0
- package/dist/embedding-benchmark.js +224 -0
- package/dist/embeddings.d.ts +79 -0
- package/dist/embeddings.js +233 -0
- package/dist/episode-summarizer.d.ts +51 -0
- package/dist/episode-summarizer.js +285 -0
- package/dist/episodes.d.ts +41 -0
- package/dist/episodes.js +141 -0
- package/dist/facts.d.ts +60 -0
- package/dist/facts.js +263 -0
- package/dist/graph-traversal.d.ts +38 -0
- package/dist/graph-traversal.js +297 -0
- package/dist/graph.d.ts +51 -0
- package/dist/graph.js +221 -0
- package/dist/import-pipeline.d.ts +17 -0
- package/dist/import-pipeline.js +324 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.js +62 -0
- package/dist/mcp-server.d.ts +31 -0
- package/dist/mcp-server.js +285 -0
- package/dist/metrics.d.ts +63 -0
- package/dist/metrics.js +243 -0
- package/dist/multi-hop.d.ts +30 -0
- package/dist/multi-hop.js +238 -0
- package/dist/profiles.d.ts +51 -0
- package/dist/profiles.js +149 -0
- package/package.json +52 -0
package/dist/episodes.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Episodic memory for event-based storage
|
|
3
|
+
*/
|
|
4
|
+
import { randomUUID } from 'crypto';
|
|
5
|
+
import { getDatabase } from './database.js';
|
|
6
|
+
/**
|
|
7
|
+
* Create a new episode
|
|
8
|
+
*/
|
|
9
|
+
export function createEpisode(input) {
|
|
10
|
+
const db = getDatabase();
|
|
11
|
+
const id = randomUUID();
|
|
12
|
+
const happenedAt = input.happenedAt || new Date();
|
|
13
|
+
db.run(`INSERT INTO episodes (id, summary, outcome, happened_at, duration_ms, procedure_id, metadata)
|
|
14
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`, [
|
|
15
|
+
id,
|
|
16
|
+
input.summary,
|
|
17
|
+
input.outcome,
|
|
18
|
+
Math.floor(happenedAt.getTime() / 1000),
|
|
19
|
+
input.durationMs || null,
|
|
20
|
+
input.procedureId || null,
|
|
21
|
+
input.metadata ? JSON.stringify(input.metadata) : null,
|
|
22
|
+
]);
|
|
23
|
+
// Link entities
|
|
24
|
+
for (const entity of input.entities) {
|
|
25
|
+
db.run('INSERT INTO episode_entities (episode_id, entity) VALUES (?, ?)', [id, entity]);
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
id,
|
|
29
|
+
conversationId: id, // Using episode ID as conversation ID
|
|
30
|
+
summary: input.summary,
|
|
31
|
+
outcome: input.outcome,
|
|
32
|
+
entities: input.entities,
|
|
33
|
+
tags: [],
|
|
34
|
+
createdAt: happenedAt.toISOString(),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Search episodes with temporal filters
|
|
39
|
+
*/
|
|
40
|
+
export function searchEpisodes(query) {
|
|
41
|
+
const db = getDatabase();
|
|
42
|
+
let sql = `
|
|
43
|
+
SELECT e.id, e.summary, e.outcome, e.happened_at as happenedAt,
|
|
44
|
+
e.duration_ms as durationMs, e.procedure_id as procedureId,
|
|
45
|
+
e.metadata, e.created_at as createdAt
|
|
46
|
+
FROM episodes e
|
|
47
|
+
WHERE 1=1
|
|
48
|
+
`;
|
|
49
|
+
const params = [];
|
|
50
|
+
if (query.since) {
|
|
51
|
+
sql += ' AND e.happened_at >= ?';
|
|
52
|
+
params.push(Math.floor(new Date(query.since).getTime() / 1000));
|
|
53
|
+
}
|
|
54
|
+
if (query.until) {
|
|
55
|
+
sql += ' AND e.happened_at <= ?';
|
|
56
|
+
params.push(Math.floor(new Date(query.until).getTime() / 1000));
|
|
57
|
+
}
|
|
58
|
+
if (query.outcome) {
|
|
59
|
+
sql += ' AND e.outcome = ?';
|
|
60
|
+
params.push(query.outcome);
|
|
61
|
+
}
|
|
62
|
+
sql += ' ORDER BY e.happened_at DESC';
|
|
63
|
+
if (query.limit) {
|
|
64
|
+
sql += ' LIMIT ?';
|
|
65
|
+
params.push(query.limit);
|
|
66
|
+
}
|
|
67
|
+
const rows = db.query(sql).all(...params);
|
|
68
|
+
// Get entities for each episode
|
|
69
|
+
return rows.map(row => {
|
|
70
|
+
const entityRows = db.query('SELECT entity FROM episode_entities WHERE episode_id = ?').all(row.id);
|
|
71
|
+
return {
|
|
72
|
+
id: row.id,
|
|
73
|
+
conversationId: row.id,
|
|
74
|
+
summary: row.summary,
|
|
75
|
+
outcome: row.outcome,
|
|
76
|
+
entities: entityRows.map(e => e.entity),
|
|
77
|
+
tags: [],
|
|
78
|
+
createdAt: new Date(row.createdAt * 1000).toISOString(),
|
|
79
|
+
tokenCount: row.durationMs || undefined,
|
|
80
|
+
};
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Get episodes for a specific entity
|
|
85
|
+
*/
|
|
86
|
+
export function getEntityEpisodes(entity, options = {}) {
|
|
87
|
+
const db = getDatabase();
|
|
88
|
+
const { limit = 10, outcome } = options;
|
|
89
|
+
let sql = `
|
|
90
|
+
SELECT e.id, e.summary, e.outcome, e.happened_at as happenedAt,
|
|
91
|
+
e.duration_ms as durationMs, e.procedure_id as procedureId,
|
|
92
|
+
e.metadata, e.created_at as createdAt
|
|
93
|
+
FROM episodes e
|
|
94
|
+
JOIN episode_entities ee ON e.id = ee.episode_id
|
|
95
|
+
WHERE ee.entity = ?
|
|
96
|
+
`;
|
|
97
|
+
const params = [entity];
|
|
98
|
+
if (outcome) {
|
|
99
|
+
sql += ' AND e.outcome = ?';
|
|
100
|
+
params.push(outcome);
|
|
101
|
+
}
|
|
102
|
+
sql += ' ORDER BY e.happened_at DESC LIMIT ?';
|
|
103
|
+
params.push(limit);
|
|
104
|
+
const rows = db.query(sql).all(...params);
|
|
105
|
+
return rows.map(row => ({
|
|
106
|
+
id: row.id,
|
|
107
|
+
conversationId: row.id,
|
|
108
|
+
summary: row.summary,
|
|
109
|
+
outcome: row.outcome,
|
|
110
|
+
entities: [entity],
|
|
111
|
+
tags: [],
|
|
112
|
+
createdAt: new Date(row.createdAt * 1000).toISOString(),
|
|
113
|
+
tokenCount: row.durationMs || undefined,
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Update episode outcome
|
|
118
|
+
*/
|
|
119
|
+
export function updateEpisodeOutcome(id, outcome) {
|
|
120
|
+
const db = getDatabase();
|
|
121
|
+
const result = db.run('UPDATE episodes SET outcome = ? WHERE id = ?', [outcome, id]);
|
|
122
|
+
return result.changes > 0;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Get episode statistics
|
|
126
|
+
*/
|
|
127
|
+
export function getEpisodeStats() {
|
|
128
|
+
const db = getDatabase();
|
|
129
|
+
const total = db.query('SELECT COUNT(*) as count FROM episodes').get().count;
|
|
130
|
+
const byOutcome = {
|
|
131
|
+
success: 0,
|
|
132
|
+
failure: 0,
|
|
133
|
+
resolved: 0,
|
|
134
|
+
ongoing: 0,
|
|
135
|
+
};
|
|
136
|
+
const rows = db.query('SELECT outcome, COUNT(*) as count FROM episodes GROUP BY outcome').all();
|
|
137
|
+
for (const row of rows) {
|
|
138
|
+
byOutcome[row.outcome] = row.count;
|
|
139
|
+
}
|
|
140
|
+
return { total, byOutcome };
|
|
141
|
+
}
|
package/dist/facts.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fact storage and retrieval operations
|
|
3
|
+
*/
|
|
4
|
+
import type { MemoryConfig, MemoryEntry, MemorySearchResult, DecayClass } from 'zouroboros-core';
|
|
5
|
+
type Category = 'preference' | 'fact' | 'decision' | 'convention' | 'other' | 'reference' | 'project';
|
|
6
|
+
interface StoreFactInput {
|
|
7
|
+
entity: string;
|
|
8
|
+
key?: string;
|
|
9
|
+
value: string;
|
|
10
|
+
category?: Category;
|
|
11
|
+
decay?: DecayClass;
|
|
12
|
+
importance?: number;
|
|
13
|
+
source?: string;
|
|
14
|
+
confidence?: number;
|
|
15
|
+
metadata?: Record<string, unknown>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Store a fact in memory
|
|
19
|
+
*/
|
|
20
|
+
export declare function storeFact(input: StoreFactInput, config: MemoryConfig): Promise<MemoryEntry>;
|
|
21
|
+
/**
|
|
22
|
+
* Search facts by exact match or keyword
|
|
23
|
+
*/
|
|
24
|
+
export declare function searchFacts(query: string, options?: {
|
|
25
|
+
entity?: string;
|
|
26
|
+
category?: string;
|
|
27
|
+
limit?: number;
|
|
28
|
+
}): MemoryEntry[];
|
|
29
|
+
/**
|
|
30
|
+
* Search facts using vector similarity
|
|
31
|
+
*/
|
|
32
|
+
export declare function searchFactsVector(query: string, config: MemoryConfig, options?: {
|
|
33
|
+
limit?: number;
|
|
34
|
+
threshold?: number;
|
|
35
|
+
useHyDE?: boolean;
|
|
36
|
+
}): Promise<MemorySearchResult[]>;
|
|
37
|
+
/**
|
|
38
|
+
* Hybrid search combining exact and vector search
|
|
39
|
+
*/
|
|
40
|
+
export declare function searchFactsHybrid(query: string, config: MemoryConfig, options?: {
|
|
41
|
+
limit?: number;
|
|
42
|
+
vectorWeight?: number;
|
|
43
|
+
}): Promise<MemorySearchResult[]>;
|
|
44
|
+
/**
|
|
45
|
+
* Get a fact by ID
|
|
46
|
+
*/
|
|
47
|
+
export declare function getFact(id: string): MemoryEntry | null;
|
|
48
|
+
/**
|
|
49
|
+
* Delete a fact by ID
|
|
50
|
+
*/
|
|
51
|
+
export declare function deleteFact(id: string): boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Update fact access time
|
|
54
|
+
*/
|
|
55
|
+
export declare function touchFact(id: string): void;
|
|
56
|
+
/**
|
|
57
|
+
* Clean up expired facts
|
|
58
|
+
*/
|
|
59
|
+
export declare function cleanupExpiredFacts(): number;
|
|
60
|
+
export {};
|
package/dist/facts.js
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fact storage and retrieval operations
|
|
3
|
+
*/
|
|
4
|
+
import { randomUUID } from 'crypto';
|
|
5
|
+
import { getDatabase } from './database.js';
|
|
6
|
+
import { generateEmbedding, serializeEmbedding, generateHyDEExpansion, blendEmbeddings } from './embeddings.js';
|
|
7
|
+
import { invalidateGraphCache } from './graph.js';
|
|
8
|
+
// Decay class TTLs in seconds
|
|
9
|
+
const TTL_DEFAULTS = {
|
|
10
|
+
permanent: null,
|
|
11
|
+
long: 365 * 24 * 3600,
|
|
12
|
+
medium: 90 * 24 * 3600,
|
|
13
|
+
short: 30 * 24 * 3600,
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Store a fact in memory
|
|
17
|
+
*/
|
|
18
|
+
export async function storeFact(input, config) {
|
|
19
|
+
const db = getDatabase();
|
|
20
|
+
const id = randomUUID();
|
|
21
|
+
const now = Math.floor(Date.now() / 1000);
|
|
22
|
+
const decay = input.decay || 'medium';
|
|
23
|
+
const ttl = TTL_DEFAULTS[decay];
|
|
24
|
+
const expiresAt = ttl ? now + ttl : null;
|
|
25
|
+
const text = input.key
|
|
26
|
+
? `${input.entity} ${input.key} ${input.value}`
|
|
27
|
+
: `${input.entity} ${input.value}`;
|
|
28
|
+
const entry = {
|
|
29
|
+
id,
|
|
30
|
+
entity: input.entity,
|
|
31
|
+
key: input.key || null,
|
|
32
|
+
value: input.value,
|
|
33
|
+
decay,
|
|
34
|
+
createdAt: new Date().toISOString(),
|
|
35
|
+
updatedAt: new Date().toISOString(),
|
|
36
|
+
};
|
|
37
|
+
// Insert fact
|
|
38
|
+
db.run(`INSERT INTO facts (id, entity, key, value, text, category, decay_class, importance, source,
|
|
39
|
+
created_at, expires_at, confidence, metadata)
|
|
40
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
41
|
+
id,
|
|
42
|
+
input.entity,
|
|
43
|
+
input.key || null,
|
|
44
|
+
input.value,
|
|
45
|
+
text,
|
|
46
|
+
input.category || 'fact',
|
|
47
|
+
decay,
|
|
48
|
+
input.importance || 1.0,
|
|
49
|
+
input.source || 'manual',
|
|
50
|
+
now,
|
|
51
|
+
expiresAt,
|
|
52
|
+
input.confidence || 1.0,
|
|
53
|
+
input.metadata ? JSON.stringify(input.metadata) : null,
|
|
54
|
+
]);
|
|
55
|
+
// Generate and store embedding if vector search is enabled
|
|
56
|
+
if (config.vectorEnabled) {
|
|
57
|
+
try {
|
|
58
|
+
const embedding = await generateEmbedding(text, config);
|
|
59
|
+
const serialized = serializeEmbedding(embedding);
|
|
60
|
+
db.run('INSERT INTO fact_embeddings (fact_id, embedding, model) VALUES (?, ?, ?)', [id, serialized, config.ollamaModel]);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
console.warn('Failed to generate embedding:', error);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
invalidateGraphCache();
|
|
67
|
+
return entry;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Search facts by exact match or keyword
|
|
71
|
+
*/
|
|
72
|
+
export function searchFacts(query, options = {}) {
|
|
73
|
+
const db = getDatabase();
|
|
74
|
+
const { entity, category, limit = 10 } = options;
|
|
75
|
+
let sql = `
|
|
76
|
+
SELECT id, entity, key, value, category, decay_class as decayClass,
|
|
77
|
+
importance, source, created_at as createdAt, expires_at as expiresAt,
|
|
78
|
+
confidence, metadata
|
|
79
|
+
FROM facts
|
|
80
|
+
WHERE (text LIKE ? OR entity LIKE ? OR value LIKE ?)
|
|
81
|
+
AND (expires_at IS NULL OR expires_at > strftime('%s', 'now'))
|
|
82
|
+
`;
|
|
83
|
+
const params = [`%${query}%`, `%${query}%`, `%${query}%`];
|
|
84
|
+
if (entity) {
|
|
85
|
+
sql += ' AND entity = ?';
|
|
86
|
+
params.push(entity);
|
|
87
|
+
}
|
|
88
|
+
if (category) {
|
|
89
|
+
sql += ' AND category = ?';
|
|
90
|
+
params.push(category);
|
|
91
|
+
}
|
|
92
|
+
sql += ' ORDER BY importance DESC, created_at DESC LIMIT ?';
|
|
93
|
+
params.push(limit);
|
|
94
|
+
const rows = db.query(sql).all(...params);
|
|
95
|
+
return rows.map(row => ({
|
|
96
|
+
id: row.id,
|
|
97
|
+
entity: row.entity,
|
|
98
|
+
key: row.key,
|
|
99
|
+
value: row.value,
|
|
100
|
+
decay: row.decayClass,
|
|
101
|
+
createdAt: new Date(row.createdAt * 1000).toISOString(),
|
|
102
|
+
updatedAt: new Date(row.createdAt * 1000).toISOString(),
|
|
103
|
+
tags: row.category ? [row.category] : undefined,
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Search facts using vector similarity
|
|
108
|
+
*/
|
|
109
|
+
export async function searchFactsVector(query, config, options = {}) {
|
|
110
|
+
if (!config.vectorEnabled) {
|
|
111
|
+
throw new Error('Vector search is disabled. Enable it in configuration.');
|
|
112
|
+
}
|
|
113
|
+
const db = getDatabase();
|
|
114
|
+
const { limit = 10, threshold = 0.7, useHyDE } = options;
|
|
115
|
+
// Generate query embedding, optionally with HyDE expansion
|
|
116
|
+
let queryEmbedding;
|
|
117
|
+
const shouldUseHyDE = useHyDE ?? config.hydeExpansion;
|
|
118
|
+
if (shouldUseHyDE) {
|
|
119
|
+
const hyde = await generateHyDEExpansion(query, config);
|
|
120
|
+
queryEmbedding = blendEmbeddings(hyde.original, hyde.expanded);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
queryEmbedding = await generateEmbedding(query, config);
|
|
124
|
+
}
|
|
125
|
+
// Get all facts with embeddings
|
|
126
|
+
const rows = db.query(`
|
|
127
|
+
SELECT f.id, f.entity, f.key, f.value, f.category, f.decay_class as decayClass,
|
|
128
|
+
f.importance, f.created_at as createdAt, fe.embedding
|
|
129
|
+
FROM facts f
|
|
130
|
+
JOIN fact_embeddings fe ON f.id = fe.fact_id
|
|
131
|
+
WHERE f.expires_at IS NULL OR f.expires_at > strftime('%s', 'now')
|
|
132
|
+
`).all();
|
|
133
|
+
// Calculate similarity and filter
|
|
134
|
+
const { cosineSimilarity, deserializeEmbedding } = await import('./embeddings.js');
|
|
135
|
+
const results = rows
|
|
136
|
+
.map(row => {
|
|
137
|
+
const embedding = deserializeEmbedding(row.embedding);
|
|
138
|
+
const similarity = cosineSimilarity(queryEmbedding, embedding);
|
|
139
|
+
return {
|
|
140
|
+
entry: {
|
|
141
|
+
id: row.id,
|
|
142
|
+
entity: row.entity,
|
|
143
|
+
key: row.key,
|
|
144
|
+
value: row.value,
|
|
145
|
+
decay: row.decayClass,
|
|
146
|
+
createdAt: new Date(row.createdAt * 1000).toISOString(),
|
|
147
|
+
updatedAt: new Date(row.createdAt * 1000).toISOString(),
|
|
148
|
+
tags: row.category ? [row.category] : undefined,
|
|
149
|
+
},
|
|
150
|
+
score: similarity,
|
|
151
|
+
matchType: 'semantic',
|
|
152
|
+
};
|
|
153
|
+
})
|
|
154
|
+
.filter(r => r.score >= threshold)
|
|
155
|
+
.sort((a, b) => b.score - a.score)
|
|
156
|
+
.slice(0, limit);
|
|
157
|
+
return results;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Hybrid search combining exact and vector search
|
|
161
|
+
*/
|
|
162
|
+
export async function searchFactsHybrid(query, config, options = {}) {
|
|
163
|
+
const { limit = 10, vectorWeight = 0.7 } = options;
|
|
164
|
+
// Get exact matches
|
|
165
|
+
const exactMatches = searchFacts(query, { limit: limit * 2 });
|
|
166
|
+
// Get vector matches
|
|
167
|
+
let vectorMatches = [];
|
|
168
|
+
if (config.vectorEnabled) {
|
|
169
|
+
try {
|
|
170
|
+
vectorMatches = await searchFactsVector(query, config, { limit: limit * 2 });
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
console.warn('Vector search failed:', error);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// RRF fusion
|
|
177
|
+
const k = 60;
|
|
178
|
+
const scores = new Map();
|
|
179
|
+
// Score exact matches
|
|
180
|
+
exactMatches.forEach((entry, rank) => {
|
|
181
|
+
const id = entry.id;
|
|
182
|
+
const rrfScore = 1 / (k + rank + 1);
|
|
183
|
+
scores.set(id, { entry, score: rrfScore * (1 - vectorWeight) });
|
|
184
|
+
});
|
|
185
|
+
// Score vector matches
|
|
186
|
+
vectorMatches.forEach((result, rank) => {
|
|
187
|
+
const id = result.entry.id;
|
|
188
|
+
const rrfScore = 1 / (k + rank + 1);
|
|
189
|
+
const existing = scores.get(id);
|
|
190
|
+
if (existing) {
|
|
191
|
+
existing.score += rrfScore * vectorWeight;
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
scores.set(id, { entry: result.entry, score: rrfScore * vectorWeight });
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
// Sort by combined score
|
|
198
|
+
return Array.from(scores.values())
|
|
199
|
+
.sort((a, b) => b.score - a.score)
|
|
200
|
+
.slice(0, limit)
|
|
201
|
+
.map(r => ({
|
|
202
|
+
entry: r.entry,
|
|
203
|
+
score: r.score,
|
|
204
|
+
matchType: 'hybrid',
|
|
205
|
+
}));
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Get a fact by ID
|
|
209
|
+
*/
|
|
210
|
+
export function getFact(id) {
|
|
211
|
+
const db = getDatabase();
|
|
212
|
+
const row = db.query(`
|
|
213
|
+
SELECT id, entity, key, value, category, decay_class as decayClass,
|
|
214
|
+
importance, source, created_at as createdAt, expires_at as expiresAt,
|
|
215
|
+
confidence, metadata
|
|
216
|
+
FROM facts
|
|
217
|
+
WHERE id = ?
|
|
218
|
+
`).get(id);
|
|
219
|
+
if (!row)
|
|
220
|
+
return null;
|
|
221
|
+
return {
|
|
222
|
+
id: row.id,
|
|
223
|
+
entity: row.entity,
|
|
224
|
+
key: row.key,
|
|
225
|
+
value: row.value,
|
|
226
|
+
decay: row.decayClass,
|
|
227
|
+
createdAt: new Date(row.createdAt * 1000).toISOString(),
|
|
228
|
+
updatedAt: new Date(row.createdAt * 1000).toISOString(),
|
|
229
|
+
tags: row.category ? [row.category] : undefined,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Delete a fact by ID
|
|
234
|
+
*/
|
|
235
|
+
export function deleteFact(id) {
|
|
236
|
+
const db = getDatabase();
|
|
237
|
+
const result = db.run('DELETE FROM facts WHERE id = ?', [id]);
|
|
238
|
+
if (result.changes > 0)
|
|
239
|
+
invalidateGraphCache();
|
|
240
|
+
return result.changes > 0;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Update fact access time
|
|
244
|
+
*/
|
|
245
|
+
export function touchFact(id) {
|
|
246
|
+
const db = getDatabase();
|
|
247
|
+
const now = Math.floor(Date.now() / 1000);
|
|
248
|
+
db.run('UPDATE facts SET last_accessed = ? WHERE id = ?', [now, id]);
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Clean up expired facts
|
|
252
|
+
*/
|
|
253
|
+
export function cleanupExpiredFacts() {
|
|
254
|
+
const db = getDatabase();
|
|
255
|
+
const result = db.run(`
|
|
256
|
+
DELETE FROM facts
|
|
257
|
+
WHERE expires_at IS NOT NULL
|
|
258
|
+
AND expires_at < strftime('%s', 'now')
|
|
259
|
+
`);
|
|
260
|
+
if (result.changes > 0)
|
|
261
|
+
invalidateGraphCache();
|
|
262
|
+
return result.changes;
|
|
263
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* graph-traversal.ts — Enhanced Knowledge Graph Traversal
|
|
4
|
+
* MEM-105: Enhanced Knowledge Graph
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* bun graph-traversal.ts ancestors --entity <name>
|
|
8
|
+
* bun graph-traversal.ts descendants --entity <name>
|
|
9
|
+
* bun graph-traversal.ts cycles
|
|
10
|
+
* bun graph-traversal.ts infer --entity <name>
|
|
11
|
+
* bun graph-traversal.ts export-dot --entity <name> --output /tmp/graph.dot
|
|
12
|
+
*/
|
|
13
|
+
import { Database } from "bun:sqlite";
|
|
14
|
+
export declare const KNOWN_RELATIONS: Set<string>;
|
|
15
|
+
export declare function getAncestors(db: Database, entity: string, maxDepth?: number): Array<{
|
|
16
|
+
entity: string;
|
|
17
|
+
relation: string;
|
|
18
|
+
depth: number;
|
|
19
|
+
path: string[];
|
|
20
|
+
}>;
|
|
21
|
+
export declare function getDescendants(db: Database, entity: string, maxDepth?: number): Array<{
|
|
22
|
+
entity: string;
|
|
23
|
+
relation: string;
|
|
24
|
+
depth: number;
|
|
25
|
+
path: string[];
|
|
26
|
+
}>;
|
|
27
|
+
export declare function detectCycles(db: Database): Array<{
|
|
28
|
+
cycle: string[];
|
|
29
|
+
length: number;
|
|
30
|
+
}>;
|
|
31
|
+
export declare function inferRelations(db: Database, entity: string): Array<{
|
|
32
|
+
from: string;
|
|
33
|
+
to: string;
|
|
34
|
+
inferred_relation: string;
|
|
35
|
+
confidence: number;
|
|
36
|
+
reason: string;
|
|
37
|
+
}>;
|
|
38
|
+
export declare function exportDot(db: Database, entity?: string, outputPath?: string): string;
|