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,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP (Model Context Protocol) server for Zouroboros Memory
|
|
3
|
+
*
|
|
4
|
+
* Exposes memory operations as MCP tools accessible by external
|
|
5
|
+
* AI agents and clients via stdio transport.
|
|
6
|
+
*
|
|
7
|
+
* Usage: bun run packages/memory/src/mcp-server.ts [--db-path <path>]
|
|
8
|
+
*/
|
|
9
|
+
import { initDatabase, closeDatabase, getDbStats } from './database.js';
|
|
10
|
+
import { storeFact, searchFacts, searchFactsHybrid } from './facts.js';
|
|
11
|
+
import { createEpisode, searchEpisodes, getEntityEpisodes, getEpisodeStats } from './episodes.js';
|
|
12
|
+
import { getProfile, updateTraits, updatePreferences, getProfileSummary, listProfiles } from './profiles.js';
|
|
13
|
+
import { ensureProfileSchema } from './profiles.js';
|
|
14
|
+
import { buildEntityGraph, getRelatedEntities } from './graph.js';
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// Tool Definitions
|
|
17
|
+
// ============================================================================
|
|
18
|
+
const TOOLS = [
|
|
19
|
+
{
|
|
20
|
+
name: 'memory_store',
|
|
21
|
+
description: 'Store a fact in memory with optional embedding generation',
|
|
22
|
+
inputSchema: {
|
|
23
|
+
type: 'object',
|
|
24
|
+
properties: {
|
|
25
|
+
entity: { type: 'string', description: 'The entity this fact is about' },
|
|
26
|
+
value: { type: 'string', description: 'The fact content' },
|
|
27
|
+
key: { type: 'string', description: 'Optional key for the fact' },
|
|
28
|
+
category: { type: 'string', enum: ['preference', 'fact', 'decision', 'convention', 'other', 'reference', 'project'] },
|
|
29
|
+
decay: { type: 'string', enum: ['permanent', 'long', 'medium', 'short'] },
|
|
30
|
+
importance: { type: 'number', description: 'Importance score (default 1.0)' },
|
|
31
|
+
},
|
|
32
|
+
required: ['entity', 'value'],
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'memory_search',
|
|
37
|
+
description: 'Search facts by keyword or semantic similarity',
|
|
38
|
+
inputSchema: {
|
|
39
|
+
type: 'object',
|
|
40
|
+
properties: {
|
|
41
|
+
query: { type: 'string', description: 'Search query' },
|
|
42
|
+
entity: { type: 'string', description: 'Filter by entity' },
|
|
43
|
+
category: { type: 'string', description: 'Filter by category' },
|
|
44
|
+
limit: { type: 'number', description: 'Max results (default 10)' },
|
|
45
|
+
mode: { type: 'string', enum: ['keyword', 'hybrid'], description: 'Search mode (default keyword)' },
|
|
46
|
+
},
|
|
47
|
+
required: ['query'],
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'memory_episodes',
|
|
52
|
+
description: 'Search or create episodic memories',
|
|
53
|
+
inputSchema: {
|
|
54
|
+
type: 'object',
|
|
55
|
+
properties: {
|
|
56
|
+
action: { type: 'string', enum: ['search', 'create', 'entity'], description: 'Action to perform' },
|
|
57
|
+
summary: { type: 'string', description: 'Episode summary (for create)' },
|
|
58
|
+
outcome: { type: 'string', enum: ['success', 'failure', 'resolved', 'ongoing'] },
|
|
59
|
+
entities: { type: 'array', items: { type: 'string' }, description: 'Related entities' },
|
|
60
|
+
entity: { type: 'string', description: 'Entity to search episodes for (for entity action)' },
|
|
61
|
+
since: { type: 'string', description: 'ISO date filter (for search)' },
|
|
62
|
+
limit: { type: 'number', description: 'Max results' },
|
|
63
|
+
},
|
|
64
|
+
required: ['action'],
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: 'cognitive_profile',
|
|
69
|
+
description: 'Get or update cognitive profiles for entities',
|
|
70
|
+
inputSchema: {
|
|
71
|
+
type: 'object',
|
|
72
|
+
properties: {
|
|
73
|
+
action: { type: 'string', enum: ['get', 'update_traits', 'update_preferences', 'summary', 'list'] },
|
|
74
|
+
entity: { type: 'string', description: 'Entity name' },
|
|
75
|
+
traits: { type: 'object', description: 'Traits to update (name → score)' },
|
|
76
|
+
preferences: { type: 'object', description: 'Preferences to update (key → value)' },
|
|
77
|
+
},
|
|
78
|
+
required: ['action'],
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: 'memory_graph',
|
|
83
|
+
description: 'Query the entity relationship graph',
|
|
84
|
+
inputSchema: {
|
|
85
|
+
type: 'object',
|
|
86
|
+
properties: {
|
|
87
|
+
action: { type: 'string', enum: ['related', 'build'], description: 'Graph action' },
|
|
88
|
+
entity: { type: 'string', description: 'Entity to find relations for' },
|
|
89
|
+
depth: { type: 'number', description: 'Traversal depth (default 2)' },
|
|
90
|
+
limit: { type: 'number', description: 'Max related entities (default 20)' },
|
|
91
|
+
},
|
|
92
|
+
required: ['action'],
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: 'memory_stats',
|
|
97
|
+
description: 'Get memory system statistics',
|
|
98
|
+
inputSchema: {
|
|
99
|
+
type: 'object',
|
|
100
|
+
properties: {},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
];
|
|
104
|
+
// ============================================================================
|
|
105
|
+
// Tool Handlers
|
|
106
|
+
// ============================================================================
|
|
107
|
+
async function handleToolCall(name, args, config) {
|
|
108
|
+
switch (name) {
|
|
109
|
+
case 'memory_store': {
|
|
110
|
+
const result = await storeFact({
|
|
111
|
+
entity: args.entity,
|
|
112
|
+
value: args.value,
|
|
113
|
+
key: args.key,
|
|
114
|
+
category: args.category,
|
|
115
|
+
decay: args.decay,
|
|
116
|
+
importance: args.importance,
|
|
117
|
+
source: 'mcp',
|
|
118
|
+
}, config);
|
|
119
|
+
return { stored: true, id: result.id, entity: result.entity };
|
|
120
|
+
}
|
|
121
|
+
case 'memory_search': {
|
|
122
|
+
const mode = args.mode ?? 'keyword';
|
|
123
|
+
if (mode === 'hybrid') {
|
|
124
|
+
return searchFactsHybrid(args.query, config, {
|
|
125
|
+
limit: args.limit,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
return searchFacts(args.query, {
|
|
129
|
+
entity: args.entity,
|
|
130
|
+
category: args.category,
|
|
131
|
+
limit: args.limit,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
case 'memory_episodes': {
|
|
135
|
+
const action = args.action;
|
|
136
|
+
if (action === 'create') {
|
|
137
|
+
return createEpisode({
|
|
138
|
+
summary: args.summary,
|
|
139
|
+
outcome: args.outcome ?? 'ongoing',
|
|
140
|
+
entities: args.entities ?? ['system'],
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
if (action === 'entity') {
|
|
144
|
+
return getEntityEpisodes(args.entity, {
|
|
145
|
+
limit: args.limit,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return searchEpisodes({
|
|
149
|
+
since: args.since,
|
|
150
|
+
outcome: args.outcome,
|
|
151
|
+
limit: args.limit,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
case 'cognitive_profile': {
|
|
155
|
+
const action = args.action;
|
|
156
|
+
if (action === 'list')
|
|
157
|
+
return listProfiles();
|
|
158
|
+
if (action === 'summary')
|
|
159
|
+
return getProfileSummary(args.entity);
|
|
160
|
+
if (action === 'update_traits') {
|
|
161
|
+
updateTraits(args.entity, args.traits);
|
|
162
|
+
return { updated: true };
|
|
163
|
+
}
|
|
164
|
+
if (action === 'update_preferences') {
|
|
165
|
+
updatePreferences(args.entity, args.preferences);
|
|
166
|
+
return { updated: true };
|
|
167
|
+
}
|
|
168
|
+
return getProfile(args.entity);
|
|
169
|
+
}
|
|
170
|
+
case 'memory_graph': {
|
|
171
|
+
const action = args.action;
|
|
172
|
+
if (action === 'build')
|
|
173
|
+
return buildEntityGraph();
|
|
174
|
+
return getRelatedEntities(args.entity, {
|
|
175
|
+
depth: args.depth,
|
|
176
|
+
limit: args.limit,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
case 'memory_stats': {
|
|
180
|
+
const dbStats = getDbStats(config);
|
|
181
|
+
const epStats = getEpisodeStats();
|
|
182
|
+
return { database: dbStats, episodes: epStats };
|
|
183
|
+
}
|
|
184
|
+
default:
|
|
185
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// ============================================================================
|
|
189
|
+
// MCP Server (stdio transport)
|
|
190
|
+
// ============================================================================
|
|
191
|
+
function createResponse(id, result) {
|
|
192
|
+
return { jsonrpc: '2.0', id, result };
|
|
193
|
+
}
|
|
194
|
+
function createError(id, code, message) {
|
|
195
|
+
return { jsonrpc: '2.0', id, error: { code, message } };
|
|
196
|
+
}
|
|
197
|
+
export async function handleMessage(message, config) {
|
|
198
|
+
try {
|
|
199
|
+
switch (message.method) {
|
|
200
|
+
case 'initialize':
|
|
201
|
+
return createResponse(message.id, {
|
|
202
|
+
protocolVersion: '2024-11-05',
|
|
203
|
+
capabilities: { tools: {} },
|
|
204
|
+
serverInfo: { name: 'zouroboros-memory', version: '2.0.0' },
|
|
205
|
+
});
|
|
206
|
+
case 'tools/list':
|
|
207
|
+
return createResponse(message.id, { tools: TOOLS });
|
|
208
|
+
case 'tools/call': {
|
|
209
|
+
const params = message.params;
|
|
210
|
+
const result = await handleToolCall(params.name, params.arguments ?? {}, config);
|
|
211
|
+
return createResponse(message.id, {
|
|
212
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
case 'notifications/initialized':
|
|
216
|
+
// Client ack — no response needed for notifications, but return empty
|
|
217
|
+
return createResponse(message.id, {});
|
|
218
|
+
default:
|
|
219
|
+
return createError(message.id, -32601, `Method not found: ${message.method}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
catch (err) {
|
|
223
|
+
return createError(message.id, -32000, err instanceof Error ? err.message : String(err));
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Start the MCP server on stdio.
|
|
228
|
+
*/
|
|
229
|
+
export async function startMcpServer(config) {
|
|
230
|
+
// Initialize database
|
|
231
|
+
initDatabase(config);
|
|
232
|
+
ensureProfileSchema();
|
|
233
|
+
const decoder = new TextDecoder();
|
|
234
|
+
let buffer = '';
|
|
235
|
+
process.stdin.resume();
|
|
236
|
+
process.stdin.on('data', async (chunk) => {
|
|
237
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
238
|
+
// Process complete JSON-RPC messages (newline-delimited)
|
|
239
|
+
const lines = buffer.split('\n');
|
|
240
|
+
buffer = lines.pop() ?? '';
|
|
241
|
+
for (const line of lines) {
|
|
242
|
+
const trimmed = line.trim();
|
|
243
|
+
if (!trimmed)
|
|
244
|
+
continue;
|
|
245
|
+
try {
|
|
246
|
+
const request = JSON.parse(trimmed);
|
|
247
|
+
const response = await handleMessage(request, config);
|
|
248
|
+
// Don't respond to notifications (no id)
|
|
249
|
+
if (request.id !== undefined) {
|
|
250
|
+
process.stdout.write(JSON.stringify(response) + '\n');
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
// Skip malformed messages
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
process.on('SIGINT', () => {
|
|
259
|
+
closeDatabase();
|
|
260
|
+
process.exit(0);
|
|
261
|
+
});
|
|
262
|
+
process.on('SIGTERM', () => {
|
|
263
|
+
closeDatabase();
|
|
264
|
+
process.exit(0);
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
// CLI entrypoint
|
|
268
|
+
if (import.meta.main) {
|
|
269
|
+
const args = process.argv.slice(2);
|
|
270
|
+
const dbPathIdx = args.indexOf('--db-path');
|
|
271
|
+
const dbPath = dbPathIdx >= 0 ? args[dbPathIdx + 1] : undefined;
|
|
272
|
+
const config = {
|
|
273
|
+
enabled: true,
|
|
274
|
+
dbPath: dbPath ?? `${process.env.HOME}/.zouroboros/memory.db`,
|
|
275
|
+
vectorEnabled: false,
|
|
276
|
+
ollamaUrl: 'http://localhost:11434',
|
|
277
|
+
ollamaModel: 'nomic-embed-text',
|
|
278
|
+
autoCapture: false,
|
|
279
|
+
captureIntervalMinutes: 30,
|
|
280
|
+
graphBoost: true,
|
|
281
|
+
hydeExpansion: false,
|
|
282
|
+
decayConfig: { permanent: Infinity, long: 365, medium: 90, short: 30 },
|
|
283
|
+
};
|
|
284
|
+
startMcpServer(config);
|
|
285
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* metrics.ts — Memory System Metrics Collection & Reporting
|
|
4
|
+
* MEM-101: Memory System Metrics Dashboard
|
|
5
|
+
*
|
|
6
|
+
* Tracks operational metrics: facts by decay, search latency, capture stats,
|
|
7
|
+
* episode outcomes, open loop resolution time, and memory gate accuracy.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* bun metrics.ts report
|
|
11
|
+
* bun metrics.ts record --operation hybrid --latencyMs 230 --resultCount 5
|
|
12
|
+
* bun metrics.ts record --operation capture --factsStored 12 --factsSkipped 3 --contradictions 1
|
|
13
|
+
* bun metrics.ts clear
|
|
14
|
+
*/
|
|
15
|
+
import { Database } from "bun:sqlite";
|
|
16
|
+
export interface MemoryMetrics {
|
|
17
|
+
factsByDecay: Record<string, number>;
|
|
18
|
+
totalFacts: number;
|
|
19
|
+
totalEmbeddings: number;
|
|
20
|
+
totalEpisodes: number;
|
|
21
|
+
episodeOutcomes: Record<string, number>;
|
|
22
|
+
totalOpenLoops: number;
|
|
23
|
+
openLoopsByStatus: Record<string, number>;
|
|
24
|
+
totalProcedures: number;
|
|
25
|
+
captureStats: CaptureStats;
|
|
26
|
+
searchMetrics: SearchMetrics;
|
|
27
|
+
gateMetrics: GateMetrics;
|
|
28
|
+
}
|
|
29
|
+
export interface CaptureStats {
|
|
30
|
+
totalCaptures: number;
|
|
31
|
+
totalFactsStored: number;
|
|
32
|
+
totalFactsSkipped: number;
|
|
33
|
+
totalContradictions: number;
|
|
34
|
+
avgFactsPerCapture: number;
|
|
35
|
+
lastCaptureAt?: number;
|
|
36
|
+
}
|
|
37
|
+
export interface SearchMetrics {
|
|
38
|
+
totalSearches: number;
|
|
39
|
+
byOperation: Record<string, OperationStats>;
|
|
40
|
+
avgLatencyMs: number;
|
|
41
|
+
totalResults: number;
|
|
42
|
+
avgResultsPerSearch: number;
|
|
43
|
+
}
|
|
44
|
+
export interface OperationStats {
|
|
45
|
+
count: number;
|
|
46
|
+
totalLatencyMs: number;
|
|
47
|
+
avgLatencyMs: number;
|
|
48
|
+
totalResults: number;
|
|
49
|
+
minLatencyMs: number;
|
|
50
|
+
maxLatencyMs: number;
|
|
51
|
+
}
|
|
52
|
+
export interface GateMetrics {
|
|
53
|
+
totalClassifications: number;
|
|
54
|
+
injections: number;
|
|
55
|
+
noMemoryNeeded: number;
|
|
56
|
+
errors: number;
|
|
57
|
+
injectionRate: number;
|
|
58
|
+
}
|
|
59
|
+
export declare function recordSearchOperation(operation: string, latencyMs: number, resultCount: number, hydeUsed?: boolean, db?: Database): void;
|
|
60
|
+
export declare function recordCaptureOperation(factsStored: number, factsSkipped: number, contradictions: number, durationMs: number, db?: Database): void;
|
|
61
|
+
export declare function recordGateDecision(decision: "inject" | "skip" | "error", reason?: string, keywords?: string[], db?: Database): void;
|
|
62
|
+
export declare function collectMetrics(db?: Database): MemoryMetrics;
|
|
63
|
+
export declare function printReport(m: MemoryMetrics): void;
|
package/dist/metrics.js
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* metrics.ts — Memory System Metrics Collection & Reporting
|
|
4
|
+
* MEM-101: Memory System Metrics Dashboard
|
|
5
|
+
*
|
|
6
|
+
* Tracks operational metrics: facts by decay, search latency, capture stats,
|
|
7
|
+
* episode outcomes, open loop resolution time, and memory gate accuracy.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* bun metrics.ts report
|
|
11
|
+
* bun metrics.ts record --operation hybrid --latencyMs 230 --resultCount 5
|
|
12
|
+
* bun metrics.ts record --operation capture --factsStored 12 --factsSkipped 3 --contradictions 1
|
|
13
|
+
* bun metrics.ts clear
|
|
14
|
+
*/
|
|
15
|
+
import { Database } from "bun:sqlite";
|
|
16
|
+
import { randomUUID } from "crypto";
|
|
17
|
+
const DB_PATH = process.env.ZO_MEMORY_DB || "/home/workspace/.zo/memory/shared-facts.db";
|
|
18
|
+
// ─── Database ─────────────────────────────────────────────────────────────────
|
|
19
|
+
function getDb() {
|
|
20
|
+
const db = new Database(DB_PATH);
|
|
21
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
22
|
+
db.exec(`
|
|
23
|
+
CREATE TABLE IF NOT EXISTS memory_metrics (
|
|
24
|
+
id TEXT PRIMARY KEY,
|
|
25
|
+
metric TEXT NOT NULL UNIQUE,
|
|
26
|
+
value REAL NOT NULL DEFAULT 0,
|
|
27
|
+
created_at INTEGER DEFAULT (strftime('%s','now')),
|
|
28
|
+
updated_at INTEGER DEFAULT (strftime('%s','now'))
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
CREATE TABLE IF NOT EXISTS search_operations (
|
|
32
|
+
id TEXT PRIMARY KEY,
|
|
33
|
+
operation TEXT NOT NULL,
|
|
34
|
+
latency_ms REAL NOT NULL,
|
|
35
|
+
result_count INTEGER NOT NULL DEFAULT 0,
|
|
36
|
+
hyde_used INTEGER NOT NULL DEFAULT 0,
|
|
37
|
+
created_at INTEGER DEFAULT (strftime('%s','now'))
|
|
38
|
+
);
|
|
39
|
+
CREATE INDEX IF NOT EXISTS idx_search_ops_operation ON search_operations(operation);
|
|
40
|
+
CREATE INDEX IF NOT EXISTS idx_search_ops_created ON search_operations(created_at);
|
|
41
|
+
|
|
42
|
+
CREATE TABLE IF NOT EXISTS capture_operations (
|
|
43
|
+
id TEXT PRIMARY KEY,
|
|
44
|
+
facts_stored INTEGER NOT NULL DEFAULT 0,
|
|
45
|
+
facts_skipped INTEGER NOT NULL DEFAULT 0,
|
|
46
|
+
contradictions INTEGER NOT NULL DEFAULT 0,
|
|
47
|
+
duration_ms INTEGER NOT NULL DEFAULT 0,
|
|
48
|
+
created_at INTEGER DEFAULT (strftime('%s','now'))
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
CREATE TABLE IF NOT EXISTS gate_operations (
|
|
52
|
+
id TEXT PRIMARY KEY,
|
|
53
|
+
decision TEXT NOT NULL,
|
|
54
|
+
reason TEXT,
|
|
55
|
+
keywords TEXT,
|
|
56
|
+
created_at INTEGER DEFAULT (strftime('%s','now'))
|
|
57
|
+
);
|
|
58
|
+
CREATE INDEX IF NOT EXISTS idx_gate_created ON gate_operations(created_at);
|
|
59
|
+
`);
|
|
60
|
+
return db;
|
|
61
|
+
}
|
|
62
|
+
// ─── Metric helpers ────────────────────────────────────────────────────────────
|
|
63
|
+
function upsertMetric(metric, value, db) {
|
|
64
|
+
db.prepare(`
|
|
65
|
+
INSERT INTO memory_metrics (id, metric, value, updated_at)
|
|
66
|
+
VALUES (?, ?, ?, strftime('%s','now'))
|
|
67
|
+
ON CONFLICT(metric) DO UPDATE SET
|
|
68
|
+
value = memory_metrics.value + excluded.value,
|
|
69
|
+
updated_at = strftime('%s','now')
|
|
70
|
+
`).run(randomUUID(), metric, value);
|
|
71
|
+
}
|
|
72
|
+
function getMetric(metric, db) {
|
|
73
|
+
const row = db.prepare("SELECT value FROM memory_metrics WHERE metric = ?").get(metric);
|
|
74
|
+
return row?.value ?? 0;
|
|
75
|
+
}
|
|
76
|
+
// ─── Record operations ─────────────────────────────────────────────────────────
|
|
77
|
+
export function recordSearchOperation(operation, latencyMs, resultCount, hydeUsed = false, db) {
|
|
78
|
+
const _db = db || getDb();
|
|
79
|
+
_db.prepare(`INSERT INTO search_operations (id, operation, latency_ms, result_count, hyde_used) VALUES (?, ?, ?, ?, ?)`).run(randomUUID(), operation, latencyMs, resultCount, hydeUsed ? 1 : 0);
|
|
80
|
+
upsertMetric(`search_ops_${operation}`, 1, _db);
|
|
81
|
+
upsertMetric(`search_latency_${operation}_sum`, latencyMs, _db);
|
|
82
|
+
upsertMetric(`search_results_${operation}_sum`, resultCount, _db);
|
|
83
|
+
}
|
|
84
|
+
export function recordCaptureOperation(factsStored, factsSkipped, contradictions, durationMs, db) {
|
|
85
|
+
const _db = db || getDb();
|
|
86
|
+
_db.prepare(`INSERT INTO capture_operations (id, facts_stored, facts_skipped, contradictions, duration_ms) VALUES (?, ?, ?, ?, ?)`).run(randomUUID(), factsStored, factsSkipped, contradictions, durationMs);
|
|
87
|
+
upsertMetric("capture_total", 1, _db);
|
|
88
|
+
upsertMetric("capture_facts_stored", factsStored, _db);
|
|
89
|
+
upsertMetric("capture_facts_skipped", factsSkipped, _db);
|
|
90
|
+
upsertMetric("capture_contradictions", contradictions, _db);
|
|
91
|
+
}
|
|
92
|
+
export function recordGateDecision(decision, reason, keywords, db) {
|
|
93
|
+
const _db = db || getDb();
|
|
94
|
+
_db.prepare(`INSERT INTO gate_operations (id, decision, reason, keywords) VALUES (?, ?, ?, ?)`).run(randomUUID(), decision, reason || null, keywords ? JSON.stringify(keywords) : null);
|
|
95
|
+
upsertMetric(`gate_${decision}`, 1, _db);
|
|
96
|
+
}
|
|
97
|
+
// ─── Collect all metrics ─────────────────────────────────────────────────────
|
|
98
|
+
export function collectMetrics(db) {
|
|
99
|
+
const _db = db || getDb();
|
|
100
|
+
const decayRows = _db.prepare("SELECT decay_class, COUNT(*) as cnt FROM facts GROUP BY decay_class").all();
|
|
101
|
+
const factsByDecay = {};
|
|
102
|
+
for (const row of decayRows)
|
|
103
|
+
factsByDecay[row.decay_class] = row.cnt;
|
|
104
|
+
const totalFacts = _db.prepare("SELECT COUNT(*) as cnt FROM facts").get().cnt;
|
|
105
|
+
const totalEmbeddings = _db.prepare("SELECT COUNT(*) as cnt FROM fact_embeddings").get().cnt;
|
|
106
|
+
const totalEpisodes = _db.prepare("SELECT COUNT(*) as cnt FROM episodes").get().cnt;
|
|
107
|
+
const outcomeRows = _db.prepare("SELECT outcome, COUNT(*) as cnt FROM episodes GROUP BY outcome").all();
|
|
108
|
+
const episodeOutcomes = {};
|
|
109
|
+
for (const row of outcomeRows)
|
|
110
|
+
episodeOutcomes[row.outcome] = row.cnt;
|
|
111
|
+
const totalOpenLoops = _db.prepare("SELECT COUNT(*) as cnt FROM open_loops").get().cnt;
|
|
112
|
+
const loopStatusRows = _db.prepare("SELECT status, COUNT(*) as cnt FROM open_loops GROUP BY status").all();
|
|
113
|
+
const openLoopsByStatus = {};
|
|
114
|
+
for (const row of loopStatusRows)
|
|
115
|
+
openLoopsByStatus[row.status] = row.cnt;
|
|
116
|
+
const totalProcedures = _db.prepare("SELECT COUNT(*) as cnt FROM procedures").get().cnt;
|
|
117
|
+
const captureStats = {
|
|
118
|
+
totalCaptures: getMetric("capture_total", _db),
|
|
119
|
+
totalFactsStored: getMetric("capture_facts_stored", _db),
|
|
120
|
+
totalFactsSkipped: getMetric("capture_facts_skipped", _db),
|
|
121
|
+
totalContradictions: getMetric("capture_contradictions", _db),
|
|
122
|
+
avgFactsPerCapture: 0,
|
|
123
|
+
lastCaptureAt: undefined,
|
|
124
|
+
};
|
|
125
|
+
if (captureStats.totalCaptures > 0)
|
|
126
|
+
captureStats.avgFactsPerCapture = captureStats.totalFactsStored / captureStats.totalCaptures;
|
|
127
|
+
const lastCapture = _db.prepare("SELECT created_at FROM capture_operations ORDER BY created_at DESC LIMIT 1").get();
|
|
128
|
+
if (lastCapture)
|
|
129
|
+
captureStats.lastCaptureAt = lastCapture.created_at;
|
|
130
|
+
const operations = ["hybrid", "fts", "vector", "continuation"];
|
|
131
|
+
const byOperation = {};
|
|
132
|
+
for (const op of operations) {
|
|
133
|
+
const count = getMetric(`search_ops_${op}`, _db);
|
|
134
|
+
if (count === 0)
|
|
135
|
+
continue;
|
|
136
|
+
const latencySum = getMetric(`search_latency_${op}_sum`, _db);
|
|
137
|
+
const resultsSum = getMetric(`search_results_${op}_sum`, _db);
|
|
138
|
+
const latRows = _db.prepare("SELECT MIN(latency_ms) as min, MAX(latency_ms) as max FROM search_operations WHERE operation = ?").get(op);
|
|
139
|
+
byOperation[op] = { count, totalLatencyMs: latencySum, avgLatencyMs: latencySum / count, totalResults: resultsSum, minLatencyMs: latRows?.min ?? 0, maxLatencyMs: latRows?.max ?? 0 };
|
|
140
|
+
}
|
|
141
|
+
const totalSearches = operations.reduce((sum, op) => sum + getMetric(`search_ops_${op}`, _db), 0);
|
|
142
|
+
const totalResults = operations.reduce((sum, op) => sum + getMetric(`search_results_${op}_sum`, _db), 0);
|
|
143
|
+
const totalLatency = operations.reduce((sum, op) => sum + getMetric(`search_latency_${op}_sum`, _db), 0);
|
|
144
|
+
const searchMetrics = {
|
|
145
|
+
totalSearches,
|
|
146
|
+
byOperation,
|
|
147
|
+
avgLatencyMs: totalSearches > 0 ? totalLatency / totalSearches : 0,
|
|
148
|
+
totalResults,
|
|
149
|
+
avgResultsPerSearch: totalSearches > 0 ? totalResults / totalSearches : 0,
|
|
150
|
+
};
|
|
151
|
+
const gi = getMetric("gate_inject", _db), gs = getMetric("gate_skip", _db), ge = getMetric("gate_error", _db);
|
|
152
|
+
const gateTotal = gi + gs + ge;
|
|
153
|
+
const gateMetrics = {
|
|
154
|
+
totalClassifications: gateTotal, injections: gi, noMemoryNeeded: gs, errors: ge,
|
|
155
|
+
injectionRate: gateTotal > 0 ? gi / gateTotal : 0,
|
|
156
|
+
};
|
|
157
|
+
return { factsByDecay, totalFacts, totalEmbeddings, totalEpisodes, episodeOutcomes, totalOpenLoops, openLoopsByStatus, totalProcedures, captureStats, searchMetrics, gateMetrics };
|
|
158
|
+
}
|
|
159
|
+
// ─── Print formatted report ───────────────────────────────────────────────────
|
|
160
|
+
export function printReport(m) {
|
|
161
|
+
const cs = m.captureStats;
|
|
162
|
+
const gm = m.gateMetrics;
|
|
163
|
+
console.log(`\n══════════════════════════════════════════════════════════`);
|
|
164
|
+
console.log(` ZO MEMORY SYSTEM — METRICS REPORT`);
|
|
165
|
+
console.log(`══════════════════════════════════════════════════════════\n`);
|
|
166
|
+
console.log(` FACTS Total: ${m.totalFacts} Embeddings: ${m.totalEmbeddings}`);
|
|
167
|
+
for (const [d, n] of Object.entries(m.factsByDecay))
|
|
168
|
+
console.log(` ${d.padEnd(10)} ${n}`);
|
|
169
|
+
console.log();
|
|
170
|
+
console.log(` EPISODES Total: ${m.totalEpisodes}`);
|
|
171
|
+
for (const [o, n] of Object.entries(m.episodeOutcomes))
|
|
172
|
+
console.log(` ${o.padEnd(10)} ${n}`);
|
|
173
|
+
console.log();
|
|
174
|
+
console.log(` OPEN LOOPS Total: ${m.totalOpenLoops}`);
|
|
175
|
+
for (const [s, n] of Object.entries(m.openLoopsByStatus))
|
|
176
|
+
console.log(` ${s.padEnd(10)} ${n}`);
|
|
177
|
+
console.log();
|
|
178
|
+
console.log(` PROCEDURES Total: ${m.totalProcedures}`);
|
|
179
|
+
console.log();
|
|
180
|
+
console.log(` SEARCH Total: ${m.searchMetrics.totalSearches} Avg: ${m.searchMetrics.avgLatencyMs.toFixed(1)}ms Results/search: ${m.searchMetrics.avgResultsPerSearch.toFixed(1)}`);
|
|
181
|
+
for (const [op, s] of Object.entries(m.searchMetrics.byOperation))
|
|
182
|
+
console.log(` ${op.padEnd(10)} ${s.count} searches | ${s.avgLatencyMs.toFixed(0)}ms avg | ${s.minLatencyMs.toFixed(0)}-${s.maxLatencyMs.toFixed(0)}ms`);
|
|
183
|
+
console.log();
|
|
184
|
+
console.log(` CAPTURE Captures: ${cs.totalCaptures} Stored: ${cs.totalFactsStored} Skipped: ${cs.totalFactsSkipped} Contradictions: ${cs.totalFactsSkipped} Avg/capture: ${cs.avgFactsPerCapture.toFixed(1)}`);
|
|
185
|
+
console.log();
|
|
186
|
+
console.log(` MEMORY GATE Total: ${gm.totalClassifications} Injections: ${gm.injections} (${(gm.injectionRate * 100).toFixed(1)}%) Skip: ${gm.noMemoryNeeded} Errors: ${gm.errors}`);
|
|
187
|
+
console.log(`\n══════════════════════════════════════════════════════════\n`);
|
|
188
|
+
}
|
|
189
|
+
// ─── CLI ─────────────────────────────────────────────────────────────────────
|
|
190
|
+
async function main() {
|
|
191
|
+
const args = process.argv.slice(2);
|
|
192
|
+
if (args.length === 0 || args[0] === "report") {
|
|
193
|
+
printReport(collectMetrics());
|
|
194
|
+
process.exit(0);
|
|
195
|
+
}
|
|
196
|
+
const command = args[0];
|
|
197
|
+
switch (command) {
|
|
198
|
+
case "record": {
|
|
199
|
+
const db = getDb();
|
|
200
|
+
let operation = "unknown", latencyMs = 0, resultCount = 0, hydeUsed = false;
|
|
201
|
+
let factsStored = 0, factsSkipped = 0, contradictions = 0, durationMs = 0;
|
|
202
|
+
for (let i = 1; i < args.length; i++) {
|
|
203
|
+
if (args[i] === "--operation" || args[i] === "-o")
|
|
204
|
+
operation = args[i + 1] || "unknown";
|
|
205
|
+
if (args[i] === "--latencyMs" || args[i] === "-l")
|
|
206
|
+
latencyMs = parseFloat(args[i + 1] || "0");
|
|
207
|
+
if (args[i] === "--resultCount" || args[i] === "-r")
|
|
208
|
+
resultCount = parseInt(args[i + 1] || "0");
|
|
209
|
+
if (args[i] === "--hydeUsed")
|
|
210
|
+
hydeUsed = args[i + 1] === "true" || args[i + 1] === "1";
|
|
211
|
+
if (args[i] === "--factsStored")
|
|
212
|
+
factsStored = parseInt(args[i + 1] || "0");
|
|
213
|
+
if (args[i] === "--factsSkipped")
|
|
214
|
+
factsSkipped = parseInt(args[i + 1] || "0");
|
|
215
|
+
if (args[i] === "--contradictions")
|
|
216
|
+
contradictions = parseInt(args[i + 1] || "0");
|
|
217
|
+
if (args[i] === "--durationMs")
|
|
218
|
+
durationMs = parseInt(args[i + 1] || "0");
|
|
219
|
+
}
|
|
220
|
+
if (operation !== "unknown" && latencyMs > 0)
|
|
221
|
+
recordSearchOperation(operation, latencyMs, resultCount, hydeUsed, db);
|
|
222
|
+
if (factsStored > 0 || factsSkipped > 0)
|
|
223
|
+
recordCaptureOperation(factsStored, factsSkipped, contradictions, durationMs, db);
|
|
224
|
+
console.log(`Recorded: search[${operation}] latency=${latencyMs}ms, capture stored=${factsStored} skipped=${factsSkipped}`);
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
case "clear": {
|
|
228
|
+
const db = getDb();
|
|
229
|
+
db.exec("DELETE FROM memory_metrics");
|
|
230
|
+
db.exec("DELETE FROM search_operations");
|
|
231
|
+
db.exec("DELETE FROM capture_operations");
|
|
232
|
+
db.exec("DELETE FROM gate_operations");
|
|
233
|
+
console.log("All metrics cleared.");
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
default:
|
|
237
|
+
console.error(`Unknown command: ${command}`);
|
|
238
|
+
console.log("Usage: bun metrics.ts report | record --operation <op> --latencyMs <n> | clear");
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (import.meta.main)
|
|
243
|
+
main();
|
|
@@ -0,0 +1,30 @@
|
|
|
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
|
+
export interface HopResult {
|
|
15
|
+
hop: number;
|
|
16
|
+
factId: string;
|
|
17
|
+
entity: string;
|
|
18
|
+
key: string | null;
|
|
19
|
+
value: string;
|
|
20
|
+
relevance: number;
|
|
21
|
+
connection: string;
|
|
22
|
+
}
|
|
23
|
+
export interface MultiHopResult {
|
|
24
|
+
query: string;
|
|
25
|
+
hopsTaken: number;
|
|
26
|
+
confidence: number;
|
|
27
|
+
allResults: HopResult[];
|
|
28
|
+
summary: string;
|
|
29
|
+
reasoning: string;
|
|
30
|
+
}
|