tsunami-memory 1.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/README.md +501 -0
- package/README.zh-CN.md +485 -0
- package/package.json +46 -0
- package/server/api.ts +125 -0
- package/server/mcp.ts +221 -0
- package/src/bun_memory_store.ts +340 -0
- package/src/classifier_keywords.ts +115 -0
- package/src/core/project_state.ts +88 -0
- package/src/index.ts +54 -0
- package/src/legacy_compat/tsunami_compat.ts +22 -0
- package/src/legacy_compat/tsunami_legacy_identity.ts +13 -0
- package/src/legacy_compat/tsunami_legacy_taxonomy.ts +197 -0
- package/src/memory_audit.ts +32 -0
- package/src/memory_conflict_resolver.ts +14 -0
- package/src/memory_fabric.ts +31 -0
- package/src/memory_manager.ts +10 -0
- package/src/memory_promotion.ts +22 -0
- package/src/memory_recovery.ts +14 -0
- package/src/memory_runtime.ts +7 -0
- package/src/migration.ts +163 -0
- package/src/provider.ts +68 -0
- package/src/runtime/checkpoints/durable_recovery.ts +24 -0
- package/src/runtime/paths.ts +11 -0
- package/src/storm/basins.ts +57 -0
- package/src/storm/boundary.ts +52 -0
- package/src/storm/budget.ts +42 -0
- package/src/storm/center.ts +396 -0
- package/src/storm/confidence.ts +88 -0
- package/src/storm/coverage.ts +44 -0
- package/src/storm/directive.ts +94 -0
- package/src/storm/gate.ts +43 -0
- package/src/storm/helpers.ts +172 -0
- package/src/storm/horizon.ts +52 -0
- package/src/storm/intake.ts +80 -0
- package/src/storm/mode.ts +21 -0
- package/src/storm/pressure.ts +56 -0
- package/src/storm/readiness.ts +29 -0
- package/src/storm/saturation.ts +45 -0
- package/src/storm/selection.ts +49 -0
- package/src/storm/signals.ts +105 -0
- package/src/storm/types.ts +216 -0
- package/src/tsunami_bun_backend.ts +705 -0
- package/src/tsunami_chinese_dialect.ts +19 -0
- package/src/tsunami_classifier.ts +137 -0
- package/src/tsunami_client.ts +710 -0
- package/src/tsunami_execution_gate.ts +232 -0
- package/src/tsunami_graph_runtime.ts +359 -0
- package/src/tsunami_identity.ts +35 -0
- package/src/tsunami_routing.ts +169 -0
- package/src/tsunami_runtime_graph_sync.ts +17 -0
- package/src/tsunami_schema.ts +403 -0
- package/src/tsunami_storage_paths.ts +8 -0
- package/src/tsunami_storm_center.ts +53 -0
package/server/mcp.ts
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* TSUNAMI MCP Server — Model Context Protocol
|
|
4
|
+
*
|
|
5
|
+
* Any MCP-compatible agent (Claude Code, Cursor, Windsurf, Codex, Hermes) can connect.
|
|
6
|
+
* Example config (~/.claude/mcp.json):
|
|
7
|
+
* { "tsunami": { "command": "bun", "args": ["run", "server/mcp.ts"] } }
|
|
8
|
+
*
|
|
9
|
+
* Exposes 8 MCP tools: tsunami_add, tsunami_search, tsunami_recall, tsunami_storm,
|
|
10
|
+
* tsunami_status, tsunami_timeline, tsunami_diary, tsunami_wings
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const HOME = process.env.TSUNAMI_HOME || '.tsunami';
|
|
14
|
+
|
|
15
|
+
// ── JSON-RPC 2.0 over stdio ────────────────────────────────
|
|
16
|
+
async function readStdin(): Promise<any> {
|
|
17
|
+
const buf = await Bun.stdin.stream().getReader().read();
|
|
18
|
+
if (!buf.value) return null;
|
|
19
|
+
const text = new TextDecoder().decode(buf.value);
|
|
20
|
+
if (!text.trim()) return null;
|
|
21
|
+
try { return JSON.parse(text); } catch { return null; }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function writeStdout(data: any): void {
|
|
25
|
+
process.stdout.write(JSON.stringify(data) + '\n');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function ok(id: number, result: any) {
|
|
29
|
+
writeStdout({ jsonrpc: '2.0', id, result });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function err(id: number, code: number, message: string) {
|
|
33
|
+
writeStdout({ jsonrpc: '2.0', id, error: { code, message } });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ── Tool Definitions ───────────────────────────────────────
|
|
37
|
+
const TOOLS = [
|
|
38
|
+
{
|
|
39
|
+
name: 'tsunami_add',
|
|
40
|
+
description: 'Add a memory to TSUNAMI. Returns confirmation with the stored content.',
|
|
41
|
+
inputSchema: {
|
|
42
|
+
type: 'object',
|
|
43
|
+
properties: {
|
|
44
|
+
content: { type: 'string', description: 'Memory content to store' },
|
|
45
|
+
wing: { type: 'string', description: 'Memory wing/basin (default: general)', default: 'general' },
|
|
46
|
+
room: { type: 'string', description: 'Room/topic within the wing (default: inbox)', default: 'inbox' },
|
|
47
|
+
energy: { type: 'number', description: 'Energy/importance level 1-5 (default: 3)', default: 3 },
|
|
48
|
+
},
|
|
49
|
+
required: ['content'],
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'tsunami_search',
|
|
54
|
+
description: 'Full-text search across TSUNAMI memories. Returns ranked results.',
|
|
55
|
+
inputSchema: {
|
|
56
|
+
type: 'object',
|
|
57
|
+
properties: {
|
|
58
|
+
query: { type: 'string', description: 'Search query' },
|
|
59
|
+
wing: { type: 'string', description: 'Filter by wing/basin' },
|
|
60
|
+
limit: { type: 'number', description: 'Max results (default: 5)', default: 5 },
|
|
61
|
+
},
|
|
62
|
+
required: ['query'],
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: 'tsunami_recall',
|
|
67
|
+
description: 'Recall recent memories from a specific wing or room. Context-aware retrieval.',
|
|
68
|
+
inputSchema: {
|
|
69
|
+
type: 'object',
|
|
70
|
+
properties: {
|
|
71
|
+
wing: { type: 'string', description: 'Memory wing/basin to recall from' },
|
|
72
|
+
room: { type: 'string', description: 'Specific room/topic' },
|
|
73
|
+
limit: { type: 'number', description: 'Max results (default: 10)', default: 10 },
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: 'tsunami_storm',
|
|
79
|
+
description: 'Get the TSUNAMI storm center — active signals, memory flow, execution budget, and storm mode. Use this to understand current context.',
|
|
80
|
+
inputSchema: {
|
|
81
|
+
type: 'object',
|
|
82
|
+
properties: {
|
|
83
|
+
projectDir: { type: 'string', description: 'Project directory for context' },
|
|
84
|
+
query: { type: 'string', description: 'Current task/query for relevance' },
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: 'tsunami_status',
|
|
90
|
+
description: 'Get TSUNAMI system status — storage size, memory counts per wing, graph stats.',
|
|
91
|
+
inputSchema: { type: 'object', properties: {} },
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: 'tsunami_timeline',
|
|
95
|
+
description: 'Get a timeline of recent memories.',
|
|
96
|
+
inputSchema: {
|
|
97
|
+
type: 'object',
|
|
98
|
+
properties: {
|
|
99
|
+
limit: { type: 'number', description: 'Max entries (default: 20)', default: 20 },
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: 'tsunami_diary',
|
|
105
|
+
description: 'Write a diary entry to TSUNAMI. Automatically timestamped.',
|
|
106
|
+
inputSchema: {
|
|
107
|
+
type: 'object',
|
|
108
|
+
properties: {
|
|
109
|
+
entry: { type: 'string', description: 'Diary entry content' },
|
|
110
|
+
agent: { type: 'string', description: 'Agent name (default: external)', default: 'external' },
|
|
111
|
+
wing: { type: 'string', description: 'Memory wing (default: diary)', default: 'diary' },
|
|
112
|
+
},
|
|
113
|
+
required: ['entry'],
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: 'tsunami_wings',
|
|
118
|
+
description: 'List all available memory wings/basins and their memory counts.',
|
|
119
|
+
inputSchema: { type: 'object', properties: {} },
|
|
120
|
+
},
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
// ── Tool Handlers ──────────────────────────────────────────
|
|
124
|
+
async function handleTool(name: string, args: Record<string, any>): Promise<any> {
|
|
125
|
+
// Dynamic import to defer heavy module loading
|
|
126
|
+
const client = await import('../src/tsunami_client');
|
|
127
|
+
const storm = await import('../src/tsunami_storm_center');
|
|
128
|
+
|
|
129
|
+
switch (name) {
|
|
130
|
+
case 'tsunami_add':
|
|
131
|
+
return { result: await client.tsunamiAdd(args.wing || 'general', args.room || 'inbox', args.content, args.energy || 3) };
|
|
132
|
+
|
|
133
|
+
case 'tsunami_search':
|
|
134
|
+
return { result: await client.tsunamiSearch(args.query, args.wing, undefined, args.limit || 5) };
|
|
135
|
+
|
|
136
|
+
case 'tsunami_recall':
|
|
137
|
+
return { result: await client.tsunamiRecall(args.wing, args.room, args.limit || 10) };
|
|
138
|
+
|
|
139
|
+
case 'tsunami_storm': {
|
|
140
|
+
const center = storm.buildTsunamiStormCenter({
|
|
141
|
+
projectDir: args.projectDir || process.cwd(),
|
|
142
|
+
query: args.query || '',
|
|
143
|
+
refreshGraph: false,
|
|
144
|
+
});
|
|
145
|
+
return {
|
|
146
|
+
flow: center.flow,
|
|
147
|
+
stormMode: center.stormMode,
|
|
148
|
+
stormPressure: center.stormPressure,
|
|
149
|
+
stormGate: center.stormGate,
|
|
150
|
+
stormBudget: center.stormBudget,
|
|
151
|
+
stormAction: center.stormAction,
|
|
152
|
+
summary: storm.formatTsunamiStormCenterText(center),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
case 'tsunami_status': {
|
|
157
|
+
const s = await client.tsunamiStatus();
|
|
158
|
+
return { ...s };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
case 'tsunami_timeline':
|
|
162
|
+
return { timeline: await client.tsunamiTimeline(args.limit || 20) };
|
|
163
|
+
|
|
164
|
+
case 'tsunami_diary':
|
|
165
|
+
return { result: await client.tsunamiDiary(args.entry, args.agent || 'external', args.wing || 'diary', 3) };
|
|
166
|
+
|
|
167
|
+
case 'tsunami_wings': {
|
|
168
|
+
const wings = await client.tsunamiListWings();
|
|
169
|
+
return { wings };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
default:
|
|
173
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ── Main Loop ──────────────────────────────────────────────
|
|
178
|
+
async function main() {
|
|
179
|
+
// MCP initialization handshake
|
|
180
|
+
const initReq = await readStdin();
|
|
181
|
+
if (!initReq || initReq.method !== 'initialize') {
|
|
182
|
+
err(initReq?.id || 0, -32600, 'Expected initialize request');
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
ok(initReq.id, {
|
|
186
|
+
protocolVersion: '2024-11-05',
|
|
187
|
+
serverInfo: { name: 'tsunami-memory', version: '1.0.0' },
|
|
188
|
+
capabilities: { tools: {} },
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Main loop: handle tools/list and tools/call
|
|
192
|
+
while (true) {
|
|
193
|
+
const req = await readStdin();
|
|
194
|
+
if (!req) break; // stdin closed
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
switch (req.method) {
|
|
198
|
+
case 'tools/list':
|
|
199
|
+
ok(req.id, { tools: TOOLS });
|
|
200
|
+
break;
|
|
201
|
+
|
|
202
|
+
case 'tools/call': {
|
|
203
|
+
const result = await handleTool(req.params?.name, req.params?.arguments || {});
|
|
204
|
+
ok(req.id, { content: [{ type: 'text', text: typeof result === 'string' ? result : JSON.stringify(result, null, 2) }] });
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
case 'notifications/initialized':
|
|
209
|
+
// Acknowledge silently — no response needed per MCP spec
|
|
210
|
+
break;
|
|
211
|
+
|
|
212
|
+
default:
|
|
213
|
+
err(req.id, -32601, `Method not found: ${req.method}`);
|
|
214
|
+
}
|
|
215
|
+
} catch (e: any) {
|
|
216
|
+
err(req.id, -32000, e?.message || String(e));
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
main().catch(e => { console.error(e); process.exit(1); });
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TSUNAMI Bun Memory Store — SQLite-backed persistent memory
|
|
3
|
+
*
|
|
4
|
+
* Stores memories as "drawers" organized by wing/room, with importance scoring
|
|
5
|
+
* and content fingerprinting for deduplication. Schema managed by migration runner.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Database } from 'bun:sqlite';
|
|
9
|
+
import { existsSync, mkdirSync } from 'fs';
|
|
10
|
+
import { dirname } from 'path';
|
|
11
|
+
import { BUN_MEMORY_DB_PATH } from './tsunami_storage_paths';
|
|
12
|
+
import { runMigrations, getMigrations } from './migration';
|
|
13
|
+
export { BUN_MEMORY_DB_PATH };
|
|
14
|
+
|
|
15
|
+
// ── Database initialization ──────────────────────────────────
|
|
16
|
+
|
|
17
|
+
function ensureDbDir(): void {
|
|
18
|
+
const dir = dirname(BUN_MEMORY_DB_PATH);
|
|
19
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let _db: Database | null = null;
|
|
23
|
+
|
|
24
|
+
function getDb(): Database {
|
|
25
|
+
if (_db) return _db;
|
|
26
|
+
ensureDbDir();
|
|
27
|
+
_db = new Database(BUN_MEMORY_DB_PATH);
|
|
28
|
+
_db.run('PRAGMA journal_mode = WAL');
|
|
29
|
+
const applied = runMigrations(_db, getMigrations());
|
|
30
|
+
if (applied > 0) {
|
|
31
|
+
// First-run or upgrade — log for observability
|
|
32
|
+
const v = (_db.prepare('SELECT MAX(version) as v FROM schema_version').get() as any)?.v ?? 0;
|
|
33
|
+
console.log(`[TSUNAMI] migrations applied: ${applied}, now at v${v}`);
|
|
34
|
+
}
|
|
35
|
+
return _db;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ── ID Generation ────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
function generateId(): string {
|
|
41
|
+
const ts = Date.now().toString(36);
|
|
42
|
+
const rand = Math.random().toString(36).slice(2, 8);
|
|
43
|
+
return `bunmem_${ts}_${rand}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── Query Result Type ────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
export interface BunMemoryRow {
|
|
49
|
+
id: string;
|
|
50
|
+
wing: string;
|
|
51
|
+
room: string;
|
|
52
|
+
content: string;
|
|
53
|
+
importance: number;
|
|
54
|
+
source: string;
|
|
55
|
+
session_id: string | null;
|
|
56
|
+
project_dir: string | null;
|
|
57
|
+
fingerprint: string | null;
|
|
58
|
+
created_at: number;
|
|
59
|
+
updated_at: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface BunMemoryInsertEntry {
|
|
63
|
+
wing?: string;
|
|
64
|
+
room?: string;
|
|
65
|
+
content: string;
|
|
66
|
+
importance?: number;
|
|
67
|
+
source?: string;
|
|
68
|
+
sessionId?: string;
|
|
69
|
+
projectDir?: string;
|
|
70
|
+
fingerprint?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface BunMemorySearchOpts {
|
|
74
|
+
query: string;
|
|
75
|
+
wing?: string;
|
|
76
|
+
room?: string;
|
|
77
|
+
limit?: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface BunMemoryRecallOpts {
|
|
81
|
+
wing?: string;
|
|
82
|
+
room?: string;
|
|
83
|
+
limit?: number;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface BunMemoryWakeOpts {
|
|
87
|
+
wing?: string;
|
|
88
|
+
limit?: number;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface BunMemoryTaxonomyEntry {
|
|
92
|
+
wing: string;
|
|
93
|
+
rooms: Record<string, number>;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── Public API ───────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
export function insertBunMemoryEntry(entry: BunMemoryInsertEntry): string {
|
|
99
|
+
const db = getDb();
|
|
100
|
+
const id = generateId();
|
|
101
|
+
const now = Date.now();
|
|
102
|
+
|
|
103
|
+
const stmt = db.prepare(`
|
|
104
|
+
INSERT INTO memory_entries (id, wing, room, content, importance, source, session_id, project_dir, fingerprint, created_at, updated_at)
|
|
105
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
106
|
+
`);
|
|
107
|
+
|
|
108
|
+
stmt.run(
|
|
109
|
+
id,
|
|
110
|
+
entry.wing || 'general',
|
|
111
|
+
entry.room || 'inbox',
|
|
112
|
+
entry.content,
|
|
113
|
+
entry.importance ?? 3,
|
|
114
|
+
entry.source || 'direct',
|
|
115
|
+
entry.sessionId || null,
|
|
116
|
+
entry.projectDir || null,
|
|
117
|
+
entry.fingerprint || null,
|
|
118
|
+
now,
|
|
119
|
+
now,
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
return id;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function searchBunMemoryRows(opts: BunMemorySearchOpts): BunMemoryRow[] {
|
|
126
|
+
const db = getDb();
|
|
127
|
+
const query = String(opts.query ?? '').trim();
|
|
128
|
+
|
|
129
|
+
if (!query) {
|
|
130
|
+
// Fall back to recall-style query if no search query provided
|
|
131
|
+
return recallBunMemoryRows({ wing: opts.wing, room: opts.room, limit: opts.limit });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Use FTS5 for full-text search
|
|
135
|
+
const ftsQuery = query.replace(/[^\w\s\u4e00-\u9fa5]/g, ' ').trim();
|
|
136
|
+
if (!ftsQuery) return [];
|
|
137
|
+
|
|
138
|
+
const limit = Math.max(1, Math.min(100, Number(opts.limit ?? 5)));
|
|
139
|
+
|
|
140
|
+
const sql = `
|
|
141
|
+
SELECT m.*
|
|
142
|
+
FROM memory_entries m
|
|
143
|
+
JOIN memory_fts fts ON m.rowid = fts.rowid
|
|
144
|
+
WHERE memory_fts MATCH ?
|
|
145
|
+
${opts.wing ? 'AND m.wing = ?' : ''}
|
|
146
|
+
${opts.room ? 'AND m.room = ?' : ''}
|
|
147
|
+
ORDER BY rank
|
|
148
|
+
LIMIT ?
|
|
149
|
+
`;
|
|
150
|
+
|
|
151
|
+
const params: any[] = [ftsQuery];
|
|
152
|
+
if (opts.wing) params.push(opts.wing);
|
|
153
|
+
if (opts.room) params.push(opts.room);
|
|
154
|
+
params.push(limit);
|
|
155
|
+
|
|
156
|
+
const stmt = db.prepare(sql);
|
|
157
|
+
return stmt.all(...params) as BunMemoryRow[];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function recallBunMemoryRows(opts: BunMemoryRecallOpts): BunMemoryRow[] {
|
|
161
|
+
const db = getDb();
|
|
162
|
+
const limit = Math.max(1, Math.min(100, Number(opts.limit ?? 10)));
|
|
163
|
+
|
|
164
|
+
let sql = `
|
|
165
|
+
SELECT * FROM memory_entries
|
|
166
|
+
WHERE 1=1
|
|
167
|
+
${opts.wing ? 'AND wing = ?' : ''}
|
|
168
|
+
${opts.room ? 'AND room = ?' : ''}
|
|
169
|
+
ORDER BY importance DESC, created_at DESC
|
|
170
|
+
LIMIT ?
|
|
171
|
+
`;
|
|
172
|
+
|
|
173
|
+
const params: any[] = [];
|
|
174
|
+
if (opts.wing) params.push(opts.wing);
|
|
175
|
+
if (opts.room) params.push(opts.room);
|
|
176
|
+
params.push(limit);
|
|
177
|
+
|
|
178
|
+
const stmt = db.prepare(sql);
|
|
179
|
+
return stmt.all(...params) as BunMemoryRow[];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function wakeBunMemoryRows(opts: BunMemoryWakeOpts): BunMemoryRow[] {
|
|
183
|
+
const db = getDb();
|
|
184
|
+
const limit = Math.max(1, Math.min(50, Number(opts.limit ?? 10)));
|
|
185
|
+
|
|
186
|
+
let sql = `
|
|
187
|
+
SELECT * FROM memory_entries
|
|
188
|
+
WHERE 1=1
|
|
189
|
+
${opts.wing ? 'AND wing = ?' : ''}
|
|
190
|
+
ORDER BY importance DESC, created_at DESC
|
|
191
|
+
LIMIT ?
|
|
192
|
+
`;
|
|
193
|
+
|
|
194
|
+
const params: any[] = [];
|
|
195
|
+
if (opts.wing) params.push(opts.wing);
|
|
196
|
+
params.push(limit);
|
|
197
|
+
|
|
198
|
+
const stmt = db.prepare(sql);
|
|
199
|
+
return stmt.all(...params) as BunMemoryRow[];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function listBunMemoryTimeline(limit?: number): BunMemoryRow[] {
|
|
203
|
+
const db = getDb();
|
|
204
|
+
const max = Math.max(1, Math.min(200, Number(limit ?? 20)));
|
|
205
|
+
|
|
206
|
+
const stmt = db.prepare(`
|
|
207
|
+
SELECT * FROM memory_entries
|
|
208
|
+
ORDER BY created_at DESC
|
|
209
|
+
LIMIT ?
|
|
210
|
+
`);
|
|
211
|
+
|
|
212
|
+
return stmt.all(max) as BunMemoryRow[];
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function countBunMemoryEntries(wing?: string): number {
|
|
216
|
+
const db = getDb();
|
|
217
|
+
|
|
218
|
+
if (wing) {
|
|
219
|
+
const stmt = db.prepare('SELECT COUNT(*) as count FROM memory_entries WHERE wing = ?');
|
|
220
|
+
const row = stmt.get(wing) as { count: number };
|
|
221
|
+
return row.count;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const stmt = db.prepare('SELECT COUNT(*) as count FROM memory_entries');
|
|
225
|
+
const row = stmt.get() as { count: number };
|
|
226
|
+
return row.count;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function getBunMemoryStatus(): Record<string, unknown> {
|
|
230
|
+
const db = getDb();
|
|
231
|
+
|
|
232
|
+
const total = (db.prepare('SELECT COUNT(*) as count FROM memory_entries').get() as { count: number }).count;
|
|
233
|
+
const wings = (db.prepare('SELECT COUNT(DISTINCT wing) as count FROM memory_entries').get() as { count: number }).count;
|
|
234
|
+
const totalSize = (db.prepare("SELECT COUNT(*) as count FROM memory_entries WHERE length(content) > 0").get() as { count: number }).count;
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
total,
|
|
238
|
+
wings,
|
|
239
|
+
total_drawers: total,
|
|
240
|
+
backend: 'bun_native',
|
|
241
|
+
db_path: BUN_MEMORY_DB_PATH,
|
|
242
|
+
totalSize,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function listBunMemoryWingCounts(): Record<string, number> {
|
|
247
|
+
const db = getDb();
|
|
248
|
+
const rows = db.prepare('SELECT wing, COUNT(*) as count FROM memory_entries GROUP BY wing ORDER BY count DESC').all() as Array<{ wing: string; count: number }>;
|
|
249
|
+
|
|
250
|
+
const result: Record<string, number> = {};
|
|
251
|
+
for (const row of rows) {
|
|
252
|
+
result[row.wing] = row.count;
|
|
253
|
+
}
|
|
254
|
+
return result;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function listBunMemoryRoomCounts(wing?: string): Record<string, number> {
|
|
258
|
+
const db = getDb();
|
|
259
|
+
|
|
260
|
+
let rows: Array<{ room: string; count: number }>;
|
|
261
|
+
if (wing) {
|
|
262
|
+
rows = db.prepare('SELECT room, COUNT(*) as count FROM memory_entries WHERE wing = ? GROUP BY room ORDER BY count DESC').all(wing) as Array<{ room: string; count: number }>;
|
|
263
|
+
} else {
|
|
264
|
+
rows = db.prepare('SELECT room, COUNT(*) as count FROM memory_entries GROUP BY room ORDER BY count DESC').all() as Array<{ room: string; count: number }>;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const result: Record<string, number> = {};
|
|
268
|
+
for (const row of rows) {
|
|
269
|
+
result[row.room] = row.count;
|
|
270
|
+
}
|
|
271
|
+
return result;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function getBunMemoryTaxonomy(): BunMemoryTaxonomyEntry[] {
|
|
275
|
+
const db = getDb();
|
|
276
|
+
const wings = db.prepare('SELECT DISTINCT wing FROM memory_entries ORDER BY wing').all() as Array<{ wing: string }>;
|
|
277
|
+
|
|
278
|
+
return wings.map((w) => {
|
|
279
|
+
const rooms = db.prepare('SELECT room, COUNT(*) as count FROM memory_entries WHERE wing = ? GROUP BY room ORDER BY count DESC').all(w.wing) as Array<{ room: string; count: number }>;
|
|
280
|
+
const roomCounts: Record<string, number> = {};
|
|
281
|
+
for (const r of rooms) {
|
|
282
|
+
roomCounts[r.room] = r.count;
|
|
283
|
+
}
|
|
284
|
+
return { wing: w.wing, rooms: roomCounts };
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export function checkBunMemoryDuplicate(
|
|
289
|
+
content: string,
|
|
290
|
+
threshold = 0.9,
|
|
291
|
+
): { is_duplicate: boolean; matches: Array<{ id: string; wing: string; room: string; similarity: number; content: string }> } {
|
|
292
|
+
const query = String(content ?? '').trim();
|
|
293
|
+
if (!query) return { is_duplicate: false, matches: [] };
|
|
294
|
+
|
|
295
|
+
// Tokenize the query content for comparison
|
|
296
|
+
const qTokens = new Set(tokenize(query));
|
|
297
|
+
|
|
298
|
+
const db = getDb();
|
|
299
|
+
const rows = db.prepare('SELECT id, wing, room, content FROM memory_entries ORDER BY created_at DESC LIMIT 500').all() as BunMemoryRow[];
|
|
300
|
+
|
|
301
|
+
const matches: Array<{ id: string; wing: string; room: string; similarity: number; content: string }> = [];
|
|
302
|
+
|
|
303
|
+
for (const row of rows) {
|
|
304
|
+
const dTokens = new Set(tokenize(row.content));
|
|
305
|
+
const inter = [...qTokens].filter((t) => dTokens.has(t)).length;
|
|
306
|
+
const union = new Set([...qTokens, ...dTokens]).size || 1;
|
|
307
|
+
const sim = inter / union;
|
|
308
|
+
|
|
309
|
+
if (sim >= threshold) {
|
|
310
|
+
matches.push({
|
|
311
|
+
id: row.id,
|
|
312
|
+
wing: row.wing,
|
|
313
|
+
room: row.room,
|
|
314
|
+
similarity: Number(sim.toFixed(3)),
|
|
315
|
+
content: row.content.slice(0, 200),
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
matches.sort((a, b) => b.similarity - a.similarity);
|
|
321
|
+
return { is_duplicate: matches.length > 0, matches: matches.slice(0, 5) };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export function deleteBunMemoryEntry(id: string): boolean {
|
|
325
|
+
const db = getDb();
|
|
326
|
+
const result = db.prepare('DELETE FROM memory_entries WHERE id = ?').run(id);
|
|
327
|
+
return result.changes > 0;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function buildBunMemoryPreview(row: BunMemoryRow, maxLen: number): string {
|
|
331
|
+
const text = String(row.content ?? '').replace(/\s+/g, ' ').trim();
|
|
332
|
+
if (!text) return '';
|
|
333
|
+
return text.length > maxLen ? `${text.slice(0, maxLen)}...` : text;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ── Internal Helpers ─────────────────────────────────────────
|
|
337
|
+
|
|
338
|
+
function tokenize(text: string): string[] {
|
|
339
|
+
return (text.toLowerCase().match(/[\u4e00-\u9fa5a-z0-9_]+/g) ?? []).filter(Boolean);
|
|
340
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TSUNAMI Classifier Keywords — wing/room keyword data
|
|
3
|
+
*
|
|
4
|
+
* Extracted from classifier logic for maintainability.
|
|
5
|
+
* Each keyword maps to a weight in the range [1.0, 4.0]:
|
|
6
|
+
* 1.0–1.5 — weak signal (common words, low specificity)
|
|
7
|
+
* 2.0–2.5 — moderate signal
|
|
8
|
+
* 3.0–3.5 — strong signal (domain-specific terms)
|
|
9
|
+
* 4.0 — definitive signal (reserved for exact-match terms)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export type KeywordPair = readonly [string, number];
|
|
13
|
+
|
|
14
|
+
/** Wing-level keywords — determines which basin a memory belongs to. */
|
|
15
|
+
export const WING_KEYWORDS: Record<string, KeywordPair[]> = {
|
|
16
|
+
identity: [
|
|
17
|
+
['identity', 3.0], ['whoami', 3.0], ['name', 2.0], ['role', 3.0],
|
|
18
|
+
['personality', 3.0], ['character', 2.5], ['behavior', 2.0],
|
|
19
|
+
['preference', 2.0], ['style', 2.0], ['tone', 2.0],
|
|
20
|
+
['principle', 3.0], ['rule', 2.5], ['iron_law', 3.5], ['red_line', 3.5],
|
|
21
|
+
['communication', 2.0], ['speech', 2.0],
|
|
22
|
+
],
|
|
23
|
+
brain: [
|
|
24
|
+
['brain', 2.5], ['model', 2.0],
|
|
25
|
+
['provider', 2.5], ['engine', 2.0],
|
|
26
|
+
['port', 2.0], ['config', 2.0],
|
|
27
|
+
['start', 1.5], ['stop', 1.5], ['restart', 1.5], ['shutdown', 1.5],
|
|
28
|
+
['tool', 2.0], ['loader', 3.0],
|
|
29
|
+
['register', 2.0], ['executor', 2.5],
|
|
30
|
+
['tts', 3.0], ['voice', 2.0], ['synthesis', 2.0], ['audio', 1.5],
|
|
31
|
+
['architecture', 2.0], ['module', 2.0],
|
|
32
|
+
['directory', 1.5], ['path', 1.5], ['file', 1.0],
|
|
33
|
+
['code', 1.5], ['implementation', 1.5], ['bug', 2.0], ['fix', 1.5],
|
|
34
|
+
],
|
|
35
|
+
decision: [
|
|
36
|
+
['decision', 3.0], ['decision_making', 3.0], ['chose', 2.5],
|
|
37
|
+
['approach', 2.5], ['tech_selection', 3.0], ['adopted', 2.0],
|
|
38
|
+
['abandoned', 2.0], ['conclusion', 2.0], ['final_approach', 3.0],
|
|
39
|
+
['decided', 3.0], ['finalized', 2.5], ['selected', 2.0],
|
|
40
|
+
['architecture', 2.5], ['tech_stack', 3.0], ['framework', 2.0],
|
|
41
|
+
['feature', 1.5], ['requirement', 1.5],
|
|
42
|
+
],
|
|
43
|
+
memory: [
|
|
44
|
+
['memory', 3.0], ['graphmemory', 3.0], ['knowledge_graph', 3.0],
|
|
45
|
+
['relationship', 2.0], ['recall', 2.5], ['search', 1.5],
|
|
46
|
+
['chromadb', 2.5], ['vector', 2.5], ['embedding', 2.5],
|
|
47
|
+
['semantic_search', 3.0], ['semantic', 2.0],
|
|
48
|
+
['recollection', 2.0], ['remember', 1.5], ['forget', 1.5], ['archive', 2.0],
|
|
49
|
+
['session', 2.0], ['context', 1.5],
|
|
50
|
+
['compress', 2.0],
|
|
51
|
+
['wrapper', 2.5], ['long_term_memory', 3.0], ['memory_fabric', 2.5], ['tsunami', 2.5],
|
|
52
|
+
],
|
|
53
|
+
task: [
|
|
54
|
+
['task', 2.0], ['todo', 2.0], ['project', 1.5],
|
|
55
|
+
['feature', 1.5], ['implement', 2.0], ['development', 1.5],
|
|
56
|
+
['bug', 2.0], ['fix', 1.5], ['testing', 1.5],
|
|
57
|
+
['deploy', 1.5], ['deployment', 1.5], ['done', 1.5],
|
|
58
|
+
['in_progress', 2.0], ['plan', 1.5],
|
|
59
|
+
['daily', 2.0], ['routine', 2.0],
|
|
60
|
+
['core_task', 3.0], ['phase_task', 2.5], ['phase_1', 2.5],
|
|
61
|
+
['heartbeat_check', 3.0], ['scheduled_check', 2.5], ['verify', 2.5],
|
|
62
|
+
['check', 1.5],
|
|
63
|
+
],
|
|
64
|
+
people: [
|
|
65
|
+
['user', 3.0], ['boss', 3.0],
|
|
66
|
+
['partner', 3.0],
|
|
67
|
+
['team', 1.5], ['colleague', 1.5],
|
|
68
|
+
],
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** Room-level keywords — refines the current within a wing. */
|
|
72
|
+
export const ROOM_KEYWORDS: Record<string, Record<string, KeywordPair[]>> = {
|
|
73
|
+
identity: {
|
|
74
|
+
core: [
|
|
75
|
+
['identity', 3.0], ['whoami', 3.0], ['name', 2.5], ['role_positioning', 3.0],
|
|
76
|
+
['core_identity', 3.0], ['deputy', 3.0], ['platform', 1.0],
|
|
77
|
+
],
|
|
78
|
+
personality: [
|
|
79
|
+
['personality', 3.0], ['assertive', 3.0],
|
|
80
|
+
['speech_style', 3.0], ['tone', 2.5], ['communication', 2.0],
|
|
81
|
+
['elite', 2.0], ['professional', 2.0],
|
|
82
|
+
],
|
|
83
|
+
rules: [
|
|
84
|
+
['rule', 3.0], ['principle', 3.0], ['hard_line', 3.0],
|
|
85
|
+
['red_line', 3.0], ['iron_law', 3.0], ['ban', 3.0],
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
brain: {
|
|
89
|
+
model: [['brain', 3.0], ['model', 2.5], ['provider', 2.5], ['engine', 2.5], ['intent', 2.0]],
|
|
90
|
+
tts: [['tts', 3.0], ['voice', 3.0], ['synthesis', 3.0], ['audio', 2.0], ['read_aloud', 2.0]],
|
|
91
|
+
tools: [['tool', 3.0], ['register', 2.5]],
|
|
92
|
+
architecture: [['architecture', 3.0], ['module', 2.5], ['design', 2.0], ['structure', 2.0], ['directory', 2.0], ['path', 1.5], ['file', 1.5], ['backend', 2.5], ['execution_loop', 2.5], ['context', 1.5]],
|
|
93
|
+
config: [['config', 3.0], ['port', 2.5], ['setting', 2.0]],
|
|
94
|
+
control: [['control_plane', 2.5], ['restart', 1.5]],
|
|
95
|
+
},
|
|
96
|
+
decision: {
|
|
97
|
+
strategy: [['tech_selection', 3.0], ['approach', 2.5], ['architecture', 2.5], ['tech_stack', 3.0], ['framework', 2.0], ['language', 2.0]],
|
|
98
|
+
product: [['product', 3.0], ['direction', 2.5], ['feature', 2.5], ['requirement', 2.5]],
|
|
99
|
+
direction: [['strategy', 3.0], ['direction', 2.5], ['priority', 2.5]],
|
|
100
|
+
},
|
|
101
|
+
memory: {
|
|
102
|
+
tsunami: [['compress', 2.5], ['embedding', 2.0], ['integration', 2.0], ['long_term_memory', 3.0], ['memory_fabric', 2.5], ['tsunami', 2.5]],
|
|
103
|
+
graph: [['graphmemory', 3.0], ['knowledge_graph', 3.0], ['relationship', 2.0], ['timeline', 2.5]],
|
|
104
|
+
context: [['context', 2.5], ['history', 2.0], ['message', 1.5]],
|
|
105
|
+
},
|
|
106
|
+
task: {
|
|
107
|
+
project: [['project', 3.0], ['feature', 2.5], ['implement', 2.5], ['development', 2.0], ['phase', 2.5], ['integration', 1.5], ['task', 1.5], ['core_task', 3.0], ['phase_task', 2.5], ['phase_1', 3.0]],
|
|
108
|
+
routine: [['daily', 3.0], ['routine', 3.0], ['scheduled', 2.5], ['heartbeat', 3.0], ['verify', 2.0], ['check', 1.5], ['heartbeat_check', 3.5], ['scheduled_check', 3.0]],
|
|
109
|
+
},
|
|
110
|
+
people: {
|
|
111
|
+
user: [['user', 3.0], ['boss', 3.0]],
|
|
112
|
+
partner: [['partner', 3.0]],
|
|
113
|
+
team: [['team', 2.0], ['colleague', 2.0]],
|
|
114
|
+
},
|
|
115
|
+
};
|