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
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* multi-hop.ts — Iterative Multi-Hop Retrieval
|
|
4
|
+
* MEM-003: Iterative Multi-Hop Retrieval
|
|
5
|
+
*
|
|
6
|
+
* Performs iterative graph traversal for complex queries that require
|
|
7
|
+
* multi-step reasoning through the knowledge graph.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* bun multi-hop.ts retrieve --query "What decisions led to our database choice?" --maxHops 3
|
|
11
|
+
* bun multi-hop.ts benchmark --query "FFB hosting decisions"
|
|
12
|
+
* bun multi-hop.ts explain --factId <id>
|
|
13
|
+
*/
|
|
14
|
+
import { Database } from "bun:sqlite";
|
|
15
|
+
const DB_PATH = process.env.ZO_MEMORY_DB || "/home/workspace/.zo/memory/shared-facts.db";
|
|
16
|
+
const OLLAMA_URL = process.env.OLLAMA_URL || "http://localhost:11434";
|
|
17
|
+
const HYDE_MODEL = process.env.ZO_HYDE_MODEL || "qwen2.5:1.5b";
|
|
18
|
+
function getDb() {
|
|
19
|
+
const db = new Database(DB_PATH);
|
|
20
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
21
|
+
return db;
|
|
22
|
+
}
|
|
23
|
+
async function ollamaGenerate(prompt) {
|
|
24
|
+
try {
|
|
25
|
+
const resp = await fetch(`${OLLAMA_URL}/api/generate`, {
|
|
26
|
+
method: "POST",
|
|
27
|
+
headers: { "Content-Type": "application/json" },
|
|
28
|
+
body: JSON.stringify({ model: HYDE_MODEL, prompt, stream: false, options: { temperature: 0.1, num_predict: 300 } }),
|
|
29
|
+
signal: AbortSignal.timeout(30000),
|
|
30
|
+
});
|
|
31
|
+
if (!resp.ok)
|
|
32
|
+
return "";
|
|
33
|
+
const data = await resp.json();
|
|
34
|
+
return data.response?.trim() || "";
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return "";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async function semanticSearch(query, limit = 5) {
|
|
41
|
+
const db = getDb();
|
|
42
|
+
const safeQuery = query.replace(/['"*]/g, "").trim();
|
|
43
|
+
if (!safeQuery)
|
|
44
|
+
return [];
|
|
45
|
+
try {
|
|
46
|
+
const rows = db.prepare(`
|
|
47
|
+
SELECT f.id, f.entity, f.key, f.value, rank
|
|
48
|
+
FROM facts f
|
|
49
|
+
JOIN facts_fts fts ON f.rowid = fts.rowid
|
|
50
|
+
WHERE facts_fts MATCH ? AND (f.expires_at IS NULL OR f.expires_at > ?)
|
|
51
|
+
ORDER BY rank
|
|
52
|
+
LIMIT ?
|
|
53
|
+
`).all(safeQuery, Math.floor(Date.now() / 1000), limit);
|
|
54
|
+
return rows.map(r => ({
|
|
55
|
+
id: r.id, entity: r.entity,
|
|
56
|
+
key: r.key, value: r.value,
|
|
57
|
+
rank: r.rank,
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function getNeighbors(db, factId) {
|
|
65
|
+
const rows = db.prepare(`
|
|
66
|
+
SELECT fl.target_id as id, f.entity, f.key, f.value, fl.relation, fl.weight
|
|
67
|
+
FROM fact_links fl JOIN facts f ON fl.target_id = f.id
|
|
68
|
+
WHERE fl.source_id = ? AND (f.expires_at IS NULL OR f.expires_at > ?)
|
|
69
|
+
UNION ALL
|
|
70
|
+
SELECT fl.source_id as id, f.entity, f.key, f.value, fl.relation, fl.weight
|
|
71
|
+
FROM fact_links fl JOIN facts f ON fl.source_id = f.id
|
|
72
|
+
WHERE fl.target_id = ? AND (f.expires_at IS NULL OR f.expires_at > ?)
|
|
73
|
+
`).all(factId, Math.floor(Date.now() / 1000), factId, Math.floor(Date.now() / 1000));
|
|
74
|
+
return rows.map(r => ({
|
|
75
|
+
id: r.id, entity: r.entity,
|
|
76
|
+
key: r.key, value: r.value,
|
|
77
|
+
relation: r.relation, weight: r.weight,
|
|
78
|
+
}));
|
|
79
|
+
}
|
|
80
|
+
function assessConfidence(results, query) {
|
|
81
|
+
if (results.length === 0)
|
|
82
|
+
return 0;
|
|
83
|
+
const avgRelevance = results.reduce((s, r) => s + r.relevance, 0) / results.length;
|
|
84
|
+
const diverseEntities = new Set(results.map(r => r.entity)).size;
|
|
85
|
+
const entityBonus = Math.min(diverseEntities / 3, 1) * 0.2;
|
|
86
|
+
return Math.min(avgRelevance * 0.7 + entityBonus, 1);
|
|
87
|
+
}
|
|
88
|
+
async function refineQueryForNextHop(originalQuery, gathered) {
|
|
89
|
+
const factsSummary = gathered.map(r => `[${r.entity}.${r.key || "_"}]: ${r.value.slice(0, 80)}`).join("\n");
|
|
90
|
+
const prompt = `Original question: "${originalQuery}"
|
|
91
|
+
Facts already found:
|
|
92
|
+
${factsSummary}
|
|
93
|
+
|
|
94
|
+
What additional information would help answer the original question? Reply with 1-2 specific search terms.`;
|
|
95
|
+
const refined = await ollamaGenerate(prompt);
|
|
96
|
+
return refined.replace(/"/g, "").trim().slice(0, 100) || originalQuery;
|
|
97
|
+
}
|
|
98
|
+
async function multiHopRetrieve(query, maxHops = 3, limitPerHop = 5) {
|
|
99
|
+
const db = getDb();
|
|
100
|
+
const allResults = [];
|
|
101
|
+
const seenIds = new Set();
|
|
102
|
+
let currentQuery = query;
|
|
103
|
+
let confidence = 0;
|
|
104
|
+
for (let hop = 0; hop < maxHops; hop++) {
|
|
105
|
+
const results = await semanticSearch(currentQuery, limitPerHop);
|
|
106
|
+
for (const r of results) {
|
|
107
|
+
if (seenIds.has(r.id))
|
|
108
|
+
continue;
|
|
109
|
+
seenIds.add(r.id);
|
|
110
|
+
const neighbors = getNeighbors(db, r.id);
|
|
111
|
+
let connection = "direct match";
|
|
112
|
+
let relevance = 1 - (r.rank / (limitPerHop * 2));
|
|
113
|
+
// Check if connected to existing results
|
|
114
|
+
for (const n of neighbors) {
|
|
115
|
+
if (seenIds.has(n.id)) {
|
|
116
|
+
connection = `linked via "${n.relation}"`;
|
|
117
|
+
relevance = Math.min(relevance + 0.15, 1);
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
allResults.push({
|
|
122
|
+
hop: hop + 1,
|
|
123
|
+
factId: r.id,
|
|
124
|
+
entity: r.entity,
|
|
125
|
+
key: r.key,
|
|
126
|
+
value: r.value,
|
|
127
|
+
relevance,
|
|
128
|
+
connection,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
confidence = assessConfidence(allResults, query);
|
|
132
|
+
if (confidence >= 0.75)
|
|
133
|
+
break;
|
|
134
|
+
if (hop < maxHops - 1) {
|
|
135
|
+
currentQuery = await refineQueryForNextHop(query, allResults);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const reasoningPrompt = `Question: "${query}"
|
|
139
|
+
|
|
140
|
+
Found ${allResults.length} relevant facts across ${allResults.length > 0 ? Math.max(...allResults.map(r => r.hop)) : 0} hops.
|
|
141
|
+
Key findings:
|
|
142
|
+
${allResults.slice(0, 6).map(r => `• [${r.entity}.${r.key || "_"}] ${r.value.slice(0, 100)} (hop ${r.hop}, ${r.connection})`).join("\n")}
|
|
143
|
+
|
|
144
|
+
Provide a 2-sentence answer to the question.`;
|
|
145
|
+
const reasoning = await ollamaGenerate(reasoningPrompt);
|
|
146
|
+
const summary = allResults.length > 0
|
|
147
|
+
? `${allResults.length} facts found across ${Math.max(...allResults.map(r => r.hop))} hops. ${reasoning}`
|
|
148
|
+
: "No results found.";
|
|
149
|
+
return {
|
|
150
|
+
query, hopsTaken: Math.max(0, ...allResults.map(r => r.hop)),
|
|
151
|
+
confidence, allResults, summary, reasoning,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
async function main() {
|
|
155
|
+
const args = process.argv.slice(2);
|
|
156
|
+
if (args.length === 0 || args[0] === "help") {
|
|
157
|
+
console.log(`Multi-Hop Retrieval CLI — v1.0
|
|
158
|
+
|
|
159
|
+
Commands:
|
|
160
|
+
retrieve --query "<text>" Perform iterative multi-hop retrieval
|
|
161
|
+
retrieve --query "<text>" --maxHops 3 --limit 5
|
|
162
|
+
benchmark --query "<text>" Compare multi-hop vs single-shot
|
|
163
|
+
explain --factId <id> Show all connections for a fact
|
|
164
|
+
|
|
165
|
+
Flags:
|
|
166
|
+
--query Search query string (required for retrieve/benchmark)
|
|
167
|
+
--maxHops Maximum hops (default: 3)
|
|
168
|
+
--limit Results per hop (default: 5)
|
|
169
|
+
--factId Fact ID for explain command`);
|
|
170
|
+
process.exit(0);
|
|
171
|
+
}
|
|
172
|
+
const flags = {};
|
|
173
|
+
for (let i = 0; i < args.length; i++) {
|
|
174
|
+
if (args[i].startsWith("--"))
|
|
175
|
+
flags[args[i].slice(2)] = args[i + 1] || "";
|
|
176
|
+
}
|
|
177
|
+
const command = args[0];
|
|
178
|
+
if (command === "retrieve" || command === "search") {
|
|
179
|
+
if (!flags.query) {
|
|
180
|
+
console.error("--query is required");
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
const maxHops = parseInt(flags.maxhops || "3");
|
|
184
|
+
const limit = parseInt(flags.limit || "5");
|
|
185
|
+
console.log(`\nMulti-hop retrieval for: "${flags.query}"\n`);
|
|
186
|
+
const result = await multiHopRetrieve(flags.query, maxHops, limit);
|
|
187
|
+
console.log(`Hops taken: ${result.hopsTaken} | Confidence: ${(result.confidence * 100).toFixed(0)}%\n`);
|
|
188
|
+
for (const r of result.allResults) {
|
|
189
|
+
console.log(` [Hop ${r.hop}] ${r.entity}.${r.key || "_"}`);
|
|
190
|
+
console.log(` ${r.value.slice(0, 120)}${r.value.length > 120 ? "..." : ""}`);
|
|
191
|
+
console.log(` ${r.connection} (relevance: ${(r.relevance * 100).toFixed(0)}%)`);
|
|
192
|
+
console.log();
|
|
193
|
+
}
|
|
194
|
+
console.log(`Summary: ${result.reasoning || result.summary}`);
|
|
195
|
+
}
|
|
196
|
+
else if (command === "benchmark") {
|
|
197
|
+
if (!flags.query) {
|
|
198
|
+
console.error("--query is required");
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
const db = getDb();
|
|
202
|
+
const singleStart = Date.now();
|
|
203
|
+
const singleResults = await semanticSearch(flags.query, 10);
|
|
204
|
+
const singleMs = Date.now() - singleStart;
|
|
205
|
+
const multiStart = Date.now();
|
|
206
|
+
const multiResult = await multiHopRetrieve(flags.query, 3, 5);
|
|
207
|
+
const multiMs = Date.now() - multiStart;
|
|
208
|
+
console.log(`\nBenchmark: "${flags.query}"\n`);
|
|
209
|
+
console.log(`Single-shot FTS: ${singleResults.length} results in ${singleMs}ms`);
|
|
210
|
+
console.log(`Multi-hop: ${multiResult.allResults.length} results in ${multiMs}ms (${multiResult.hopsTaken} hops, conf ${(multiResult.confidence * 100).toFixed(0)}%)`);
|
|
211
|
+
console.log(`\nNew facts discovered by multi-hop: ${multiResult.allResults.filter(r => r.hop > 1).length}`);
|
|
212
|
+
}
|
|
213
|
+
else if (command === "explain") {
|
|
214
|
+
if (!flags.factid) {
|
|
215
|
+
console.error("--factId is required");
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
const db = getDb();
|
|
219
|
+
const fact = db.prepare("SELECT id, entity, key, value FROM facts WHERE id = ?").get(flags.factid);
|
|
220
|
+
if (!fact) {
|
|
221
|
+
console.log("Fact not found.");
|
|
222
|
+
process.exit(0);
|
|
223
|
+
}
|
|
224
|
+
console.log(`\n[${fact.entity}.${fact.key || "_"}] ${fact.value}\n`);
|
|
225
|
+
const neighbors = getNeighbors(db, fact.id);
|
|
226
|
+
if (neighbors.length === 0) {
|
|
227
|
+
console.log("No connections.");
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
console.log(`${neighbors.length} connections:\n`);
|
|
231
|
+
for (const n of neighbors) {
|
|
232
|
+
console.log(` --${n.relation}--> [${n.entity}.${n.key || "_"}] ${n.value.slice(0, 80)}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (import.meta.main)
|
|
238
|
+
main();
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cognitive profile tracking for entities (executors, users, subsystems).
|
|
3
|
+
*
|
|
4
|
+
* Tracks interaction patterns, performance traits, and preferences
|
|
5
|
+
* to enable adaptive behavior over time.
|
|
6
|
+
*/
|
|
7
|
+
import type { CognitiveProfile, InteractionRecord } from 'zouroboros-core';
|
|
8
|
+
/**
|
|
9
|
+
* Get or create a cognitive profile for an entity.
|
|
10
|
+
*/
|
|
11
|
+
export declare function getProfile(entity: string): CognitiveProfile;
|
|
12
|
+
/**
|
|
13
|
+
* Update traits for an entity (merges with existing).
|
|
14
|
+
*/
|
|
15
|
+
export declare function updateTraits(entity: string, traits: Record<string, number>): void;
|
|
16
|
+
/**
|
|
17
|
+
* Update preferences for an entity (merges with existing).
|
|
18
|
+
*/
|
|
19
|
+
export declare function updatePreferences(entity: string, preferences: Record<string, string>): void;
|
|
20
|
+
/**
|
|
21
|
+
* Record an interaction for tracking patterns.
|
|
22
|
+
*/
|
|
23
|
+
export declare function recordInteraction(entity: string, type: 'query' | 'store' | 'search', success: boolean, latencyMs: number): void;
|
|
24
|
+
/**
|
|
25
|
+
* Get recent interactions for an entity.
|
|
26
|
+
*/
|
|
27
|
+
export declare function getRecentInteractions(entity: string, limit?: number): InteractionRecord[];
|
|
28
|
+
/**
|
|
29
|
+
* Get performance summary for an entity.
|
|
30
|
+
*/
|
|
31
|
+
export declare function getProfileSummary(entity: string): {
|
|
32
|
+
entity: string;
|
|
33
|
+
totalInteractions: number;
|
|
34
|
+
successRate: number;
|
|
35
|
+
avgLatencyMs: number;
|
|
36
|
+
traitCount: number;
|
|
37
|
+
preferenceCount: number;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* List all known entities with profiles.
|
|
41
|
+
*/
|
|
42
|
+
export declare function listProfiles(): string[];
|
|
43
|
+
/**
|
|
44
|
+
* Delete a cognitive profile.
|
|
45
|
+
*/
|
|
46
|
+
export declare function deleteProfile(entity: string): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Ensure the profile_interactions table exists.
|
|
49
|
+
* Called during memory system init.
|
|
50
|
+
*/
|
|
51
|
+
export declare function ensureProfileSchema(): void;
|
package/dist/profiles.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cognitive profile tracking for entities (executors, users, subsystems).
|
|
3
|
+
*
|
|
4
|
+
* Tracks interaction patterns, performance traits, and preferences
|
|
5
|
+
* to enable adaptive behavior over time.
|
|
6
|
+
*/
|
|
7
|
+
import { getDatabase } from './database.js';
|
|
8
|
+
/**
|
|
9
|
+
* Get or create a cognitive profile for an entity.
|
|
10
|
+
*/
|
|
11
|
+
export function getProfile(entity) {
|
|
12
|
+
const db = getDatabase();
|
|
13
|
+
const row = db.query('SELECT entity, traits, preferences, interaction_count, last_interaction, created_at FROM cognitive_profiles WHERE entity = ?').get(entity);
|
|
14
|
+
if (row) {
|
|
15
|
+
return {
|
|
16
|
+
entity: row.entity,
|
|
17
|
+
traits: row.traits ? JSON.parse(row.traits) : {},
|
|
18
|
+
preferences: row.preferences ? JSON.parse(row.preferences) : {},
|
|
19
|
+
interactionHistory: getRecentInteractions(entity),
|
|
20
|
+
lastUpdated: row.last_interaction
|
|
21
|
+
? new Date(row.last_interaction * 1000).toISOString()
|
|
22
|
+
: new Date(row.created_at * 1000).toISOString(),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
// Auto-create on first access
|
|
26
|
+
const now = Math.floor(Date.now() / 1000);
|
|
27
|
+
db.run('INSERT INTO cognitive_profiles (entity, traits, preferences, interaction_count, last_interaction) VALUES (?, ?, ?, 0, ?)', [entity, '{}', '{}', now]);
|
|
28
|
+
return {
|
|
29
|
+
entity,
|
|
30
|
+
traits: {},
|
|
31
|
+
preferences: {},
|
|
32
|
+
interactionHistory: [],
|
|
33
|
+
lastUpdated: new Date(now * 1000).toISOString(),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Update traits for an entity (merges with existing).
|
|
38
|
+
*/
|
|
39
|
+
export function updateTraits(entity, traits) {
|
|
40
|
+
const db = getDatabase();
|
|
41
|
+
const profile = getProfile(entity);
|
|
42
|
+
const merged = { ...profile.traits, ...traits };
|
|
43
|
+
const now = Math.floor(Date.now() / 1000);
|
|
44
|
+
db.run('UPDATE cognitive_profiles SET traits = ?, last_interaction = ? WHERE entity = ?', [JSON.stringify(merged), now, entity]);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Update preferences for an entity (merges with existing).
|
|
48
|
+
*/
|
|
49
|
+
export function updatePreferences(entity, preferences) {
|
|
50
|
+
const db = getDatabase();
|
|
51
|
+
const profile = getProfile(entity);
|
|
52
|
+
const merged = { ...profile.preferences, ...preferences };
|
|
53
|
+
const now = Math.floor(Date.now() / 1000);
|
|
54
|
+
db.run('UPDATE cognitive_profiles SET preferences = ?, last_interaction = ? WHERE entity = ?', [JSON.stringify(merged), now, entity]);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Record an interaction for tracking patterns.
|
|
58
|
+
*/
|
|
59
|
+
export function recordInteraction(entity, type, success, latencyMs) {
|
|
60
|
+
const db = getDatabase();
|
|
61
|
+
const now = Math.floor(Date.now() / 1000);
|
|
62
|
+
// Ensure profile exists
|
|
63
|
+
getProfile(entity);
|
|
64
|
+
// Store in interactions table
|
|
65
|
+
db.run(`INSERT INTO profile_interactions (entity, type, success, latency_ms, timestamp)
|
|
66
|
+
VALUES (?, ?, ?, ?, ?)`, [entity, type, success ? 1 : 0, latencyMs, now]);
|
|
67
|
+
// Update aggregate counts
|
|
68
|
+
db.run('UPDATE cognitive_profiles SET interaction_count = interaction_count + 1, last_interaction = ? WHERE entity = ?', [now, entity]);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Get recent interactions for an entity.
|
|
72
|
+
*/
|
|
73
|
+
export function getRecentInteractions(entity, limit = 50) {
|
|
74
|
+
const db = getDatabase();
|
|
75
|
+
// Check if table exists (it's created by ensureProfileSchema)
|
|
76
|
+
const tableExists = db.query("SELECT name FROM sqlite_master WHERE type='table' AND name='profile_interactions'").get();
|
|
77
|
+
if (!tableExists)
|
|
78
|
+
return [];
|
|
79
|
+
const rows = db.query('SELECT type, success, latency_ms, timestamp FROM profile_interactions WHERE entity = ? ORDER BY timestamp DESC LIMIT ?').all(entity, limit);
|
|
80
|
+
return rows.map(row => ({
|
|
81
|
+
timestamp: new Date(row.timestamp * 1000).toISOString(),
|
|
82
|
+
type: row.type,
|
|
83
|
+
success: row.success === 1,
|
|
84
|
+
latencyMs: row.latency_ms,
|
|
85
|
+
}));
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Get performance summary for an entity.
|
|
89
|
+
*/
|
|
90
|
+
export function getProfileSummary(entity) {
|
|
91
|
+
const db = getDatabase();
|
|
92
|
+
const profile = getProfile(entity);
|
|
93
|
+
const row = db.query('SELECT interaction_count FROM cognitive_profiles WHERE entity = ?').get(entity);
|
|
94
|
+
const tableExists = db.query("SELECT name FROM sqlite_master WHERE type='table' AND name='profile_interactions'").get();
|
|
95
|
+
let successRate = 0;
|
|
96
|
+
let avgLatencyMs = 0;
|
|
97
|
+
if (tableExists) {
|
|
98
|
+
const stats = db.query('SELECT AVG(CAST(success AS REAL)) as sr, AVG(latency_ms) as avg_lat FROM profile_interactions WHERE entity = ?').get(entity);
|
|
99
|
+
successRate = stats?.sr ?? 0;
|
|
100
|
+
avgLatencyMs = stats?.avg_lat ?? 0;
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
entity,
|
|
104
|
+
totalInteractions: row?.interaction_count ?? 0,
|
|
105
|
+
successRate,
|
|
106
|
+
avgLatencyMs,
|
|
107
|
+
traitCount: Object.keys(profile.traits).length,
|
|
108
|
+
preferenceCount: Object.keys(profile.preferences).length,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* List all known entities with profiles.
|
|
113
|
+
*/
|
|
114
|
+
export function listProfiles() {
|
|
115
|
+
const db = getDatabase();
|
|
116
|
+
const rows = db.query('SELECT entity FROM cognitive_profiles ORDER BY last_interaction DESC').all();
|
|
117
|
+
return rows.map(r => r.entity);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Delete a cognitive profile.
|
|
121
|
+
*/
|
|
122
|
+
export function deleteProfile(entity) {
|
|
123
|
+
const db = getDatabase();
|
|
124
|
+
const result = db.run('DELETE FROM cognitive_profiles WHERE entity = ?', [entity]);
|
|
125
|
+
// Clean up interactions if table exists
|
|
126
|
+
const tableExists = db.query("SELECT name FROM sqlite_master WHERE type='table' AND name='profile_interactions'").get();
|
|
127
|
+
if (tableExists) {
|
|
128
|
+
db.run('DELETE FROM profile_interactions WHERE entity = ?', [entity]);
|
|
129
|
+
}
|
|
130
|
+
return result.changes > 0;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Ensure the profile_interactions table exists.
|
|
134
|
+
* Called during memory system init.
|
|
135
|
+
*/
|
|
136
|
+
export function ensureProfileSchema() {
|
|
137
|
+
const db = getDatabase();
|
|
138
|
+
db.exec(`
|
|
139
|
+
CREATE TABLE IF NOT EXISTS profile_interactions (
|
|
140
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
141
|
+
entity TEXT NOT NULL,
|
|
142
|
+
type TEXT NOT NULL CHECK(type IN ('query', 'store', 'search')),
|
|
143
|
+
success INTEGER NOT NULL DEFAULT 1,
|
|
144
|
+
latency_ms REAL NOT NULL DEFAULT 0,
|
|
145
|
+
timestamp INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
|
|
146
|
+
);
|
|
147
|
+
CREATE INDEX IF NOT EXISTS idx_profile_interactions_entity ON profile_interactions(entity, timestamp);
|
|
148
|
+
`);
|
|
149
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "zouroboros-memory",
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "Hybrid SQLite + Vector memory system for Zouroboros",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist/",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "https://github.com/marlandoj/zouroboros.git",
|
|
21
|
+
"directory": "packages/memory"
|
|
22
|
+
},
|
|
23
|
+
"bin": {
|
|
24
|
+
"zouroboros-memory": "./dist/cli.js",
|
|
25
|
+
"zouroboros-memory-mcp": "./dist/mcp-server.js"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"zouroboros-core": "2.0.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/bun": "latest",
|
|
32
|
+
"typescript": "^5.4.0"
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"zouroboros",
|
|
36
|
+
"memory",
|
|
37
|
+
"sqlite",
|
|
38
|
+
"vector-search",
|
|
39
|
+
"embeddings",
|
|
40
|
+
"episodic-memory"
|
|
41
|
+
],
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsc",
|
|
45
|
+
"dev": "tsc --watch",
|
|
46
|
+
"test": "bun test src/",
|
|
47
|
+
"test:coverage": "bun test --coverage src/",
|
|
48
|
+
"cli": "bun ./src/cli.ts",
|
|
49
|
+
"mcp": "bun ./src/mcp-server.ts",
|
|
50
|
+
"typecheck": "tsc --noEmit"
|
|
51
|
+
}
|
|
52
|
+
}
|