@jungjaehoon/mama-core 1.3.3 → 1.4.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/README.md +67 -19
- package/db/migrations/001-initial-decision-graph.sql +5 -0
- package/db/migrations/014-add-is-static-column.sql +5 -0
- package/db/migrations/015-add-fts5-search.sql +34 -0
- package/db/migrations/016-add-memory-v2-columns.sql +14 -0
- package/db/migrations/017-create-memory-scopes.sql +10 -0
- package/db/migrations/018-create-memory-scope-bindings.sql +15 -0
- package/db/migrations/019-create-memory-events.sql +18 -0
- package/db/migrations/020-create-audit-findings.sql +18 -0
- package/db/migrations/021-create-memory-truth.sql +20 -0
- package/db/migrations/022-create-channel-summaries.sql +9 -0
- package/db/migrations/023-create-channel-summary-state.sql +9 -0
- package/db/migrations/024-add-event-date.sql +9 -0
- package/dist/db-adapter/base-adapter.d.ts +1 -1
- package/dist/db-adapter/index.js +2 -8
- package/dist/db-adapter/node-sqlite-adapter.d.ts +6 -1
- package/dist/db-adapter/node-sqlite-adapter.js +99 -45
- package/dist/db-manager.d.ts +30 -2
- package/dist/db-manager.js +131 -6
- package/dist/decision-formatter.d.ts +2 -0
- package/dist/embedding-server/mobile/session-manager.js +22 -7
- package/dist/index.d.ts +5 -1
- package/dist/index.js +31 -3
- package/dist/mama-api.d.ts +67 -2
- package/dist/mama-api.js +295 -80
- package/dist/memory/api.d.ts +81 -0
- package/dist/memory/api.js +972 -0
- package/dist/memory/bootstrap-builder.d.ts +10 -0
- package/dist/memory/bootstrap-builder.js +66 -0
- package/dist/memory/channel-summary-state-store.d.ts +19 -0
- package/dist/memory/channel-summary-state-store.js +223 -0
- package/dist/memory/channel-summary-store.d.ts +8 -0
- package/dist/memory/channel-summary-store.js +41 -0
- package/dist/memory/event-store.d.ts +5 -0
- package/dist/memory/event-store.js +72 -0
- package/dist/memory/evolution-engine.d.ts +22 -0
- package/dist/memory/evolution-engine.js +133 -0
- package/dist/memory/extraction-prompt.d.ts +4 -0
- package/dist/memory/extraction-prompt.js +89 -0
- package/dist/memory/finding-store.d.ts +5 -0
- package/dist/memory/finding-store.js +75 -0
- package/dist/memory/profile-builder.d.ts +3 -0
- package/dist/memory/profile-builder.js +23 -0
- package/dist/memory/scope-store.d.ts +5 -0
- package/dist/memory/scope-store.js +13 -0
- package/dist/memory/truth-store.d.ts +11 -0
- package/dist/memory/truth-store.js +102 -0
- package/dist/memory/types.d.ts +267 -0
- package/dist/memory/types.js +74 -0
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -74,12 +74,40 @@ const mamaApi = require('@jungjaehoon/mama-core/mama-api');
|
|
|
74
74
|
- CRUD operations for decisions
|
|
75
75
|
- Vector similarity search
|
|
76
76
|
|
|
77
|
-
###
|
|
77
|
+
### Memory API
|
|
78
78
|
|
|
79
|
-
- **
|
|
79
|
+
- **memory/api** - Scoped memory operations
|
|
80
|
+
- `saveMemory(input)` - Save typed memory with scopes and optional event_date (preference, fact, decision, lesson, constraint)
|
|
81
|
+
- `recallMemory(query, options)` - Truth-aware recall with scope filtering
|
|
82
|
+
- `buildProfile(scopes)` - Build memory profile (static/dynamic/evidence)
|
|
83
|
+
- `ingestMemory(input)` - Ingest raw content as memory
|
|
84
|
+
- `ingestConversation(input)` - Decompose conversations into typed memory units via optional LLM extraction
|
|
85
|
+
- `evolveMemory(input)` - Resolve graph edges between memories
|
|
86
|
+
- `buildMemoryBootstrap(params)` - Build memory agent bootstrap context
|
|
87
|
+
- `createAuditAck(input)` - Create audit acknowledgment
|
|
88
|
+
- `recordMemoryAudit(input)` - Record channel audit with state management
|
|
89
|
+
|
|
90
|
+
- **memory/truth-store** - Truth projection layer
|
|
91
|
+
- `projectMemoryTruth(row)` - Write truth projection
|
|
92
|
+
- `queryRelevantTruth(params)` - Query current truth with scope/query filtering
|
|
93
|
+
- `queryTruthByTopic(topic)` - Get truth rows for a topic
|
|
94
|
+
|
|
95
|
+
- **memory/evolution-engine** - Graph edge resolution
|
|
96
|
+
- `resolveMemoryEvolution(input)` - Determine supersedes/builds_on edges
|
|
97
|
+
|
|
98
|
+
- **memory/extraction-prompt** - LLM extraction for conversation ingestion
|
|
99
|
+
- `buildExtractionPrompt(messages)` - Build structured prompt for memory extraction
|
|
100
|
+
- `parseExtractionResponse(response)` - Parse LLM JSON response into typed units
|
|
101
|
+
|
|
102
|
+
- **memory/channel-summary-state-store** - Channel state management
|
|
103
|
+
- `recordChannelAudit(input)` - Accumulate audit outcomes into channel state
|
|
104
|
+
|
|
105
|
+
### Core API (Legacy)
|
|
106
|
+
|
|
107
|
+
- **mama-api** - High-level API interface (wraps memory API)
|
|
80
108
|
- `save(decision)` - Save decision
|
|
81
109
|
- `recall(topic)` - Retrieve decision history
|
|
82
|
-
- `suggest(query)` - Semantic search
|
|
110
|
+
- `suggest(query)` - Semantic search with hybrid FTS5 + vector + recency
|
|
83
111
|
- `updateOutcome(id, outcome)` - Update decision outcome
|
|
84
112
|
|
|
85
113
|
- **decision-tracker** - Decision graph management
|
|
@@ -125,12 +153,15 @@ pnpm test:watch
|
|
|
125
153
|
|
|
126
154
|
## Test Coverage
|
|
127
155
|
|
|
128
|
-
-
|
|
156
|
+
- 72 unit tests across 17 test files
|
|
129
157
|
- 100% passing
|
|
130
158
|
- Tests cover:
|
|
131
|
-
- Config loader
|
|
132
|
-
-
|
|
133
|
-
-
|
|
159
|
+
- Config loader, database initialization, module exports
|
|
160
|
+
- Memory API (save, recall, profile)
|
|
161
|
+
- Truth store, evolution engine, scope schema
|
|
162
|
+
- Channel summary, channel summary state
|
|
163
|
+
- Event store, finding store, bootstrap builder
|
|
164
|
+
- Legacy shim compatibility
|
|
134
165
|
|
|
135
166
|
## Architecture
|
|
136
167
|
|
|
@@ -139,23 +170,40 @@ MAMA Core uses CommonJS modules and is designed to be shared across multiple pac
|
|
|
139
170
|
```
|
|
140
171
|
packages/mama-core/
|
|
141
172
|
├── src/
|
|
142
|
-
│ ├── index.
|
|
143
|
-
│ ├── embeddings.
|
|
144
|
-
│ ├── db-manager.
|
|
145
|
-
│ ├── mama-api.
|
|
146
|
-
│
|
|
147
|
-
|
|
148
|
-
|
|
173
|
+
│ ├── index.ts # Main exports
|
|
174
|
+
│ ├── embeddings.ts # Embedding generation
|
|
175
|
+
│ ├── db-manager.ts # Database management
|
|
176
|
+
│ ├── mama-api.ts # High-level API (wraps memory API)
|
|
177
|
+
│ ├── db-adapter/ # Database adapter (SQLite)
|
|
178
|
+
│ └── memory/ # Memory infrastructure
|
|
179
|
+
│ ├── types.ts # MemoryRecord, MemoryScopeRef, RecallBundle, etc.
|
|
180
|
+
│ ├── api.ts # saveMemory, recallMemory, buildProfile, etc.
|
|
181
|
+
│ ├── truth-store.ts # Truth projection layer
|
|
182
|
+
│ ├── extraction-prompt.ts # LLM extraction prompt + parser
|
|
183
|
+
│ ├── evolution-engine.ts # Graph edge resolution
|
|
184
|
+
│ ├── scope-store.ts # Scope management
|
|
185
|
+
│ ├── event-store.ts # Audit event persistence
|
|
186
|
+
│ ├── finding-store.ts # Audit finding persistence
|
|
187
|
+
│ ├── channel-summary-store.ts # Channel summaries
|
|
188
|
+
│ ├── channel-summary-state-store.ts # Channel state reducer
|
|
189
|
+
│ ├── bootstrap-builder.ts # Memory agent bootstrap
|
|
190
|
+
│ └── profile-builder.ts # Profile classification
|
|
191
|
+
├── db/migrations/ # SQLite migrations (001-023)
|
|
192
|
+
└── tests/ # 16 test files, 59 tests
|
|
149
193
|
```
|
|
150
194
|
|
|
151
195
|
## Migration Files
|
|
152
196
|
|
|
153
|
-
Database migrations are included in `db/migrations
|
|
197
|
+
Database migrations are included in `db/migrations/` (001-023):
|
|
154
198
|
|
|
155
|
-
- 001-
|
|
156
|
-
-
|
|
157
|
-
-
|
|
158
|
-
-
|
|
199
|
+
- 001-013: Core schema (decisions, embeddings, graph edges)
|
|
200
|
+
- 014: Add is_static column
|
|
201
|
+
- 015: FTS5 full-text search index
|
|
202
|
+
- 016: Memory kind/status/summary columns
|
|
203
|
+
- 017-018: Memory scopes and scope bindings
|
|
204
|
+
- 019-020: Memory events and audit findings
|
|
205
|
+
- 021: Memory truth projection
|
|
206
|
+
- 022-023: Channel summaries and state
|
|
159
207
|
|
|
160
208
|
## License
|
|
161
209
|
|
|
@@ -100,7 +100,12 @@ CREATE TABLE IF NOT EXISTS sessions (
|
|
|
100
100
|
-- Timestamps
|
|
101
101
|
started_at INTEGER DEFAULT (unixepoch() * 1000),
|
|
102
102
|
last_active_at INTEGER DEFAULT (unixepoch() * 1000),
|
|
103
|
+
last_active TEXT,
|
|
104
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
103
105
|
ended_at INTEGER,
|
|
106
|
+
status TEXT DEFAULT 'active',
|
|
107
|
+
pid INTEGER,
|
|
108
|
+
client_id TEXT,
|
|
104
109
|
|
|
105
110
|
CHECK (action_count >= 0),
|
|
106
111
|
CHECK (decision_count >= 0)
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
-- Add is_static column for user profile (long-term preferences vs current work)
|
|
2
|
+
ALTER TABLE decisions ADD COLUMN is_static INTEGER DEFAULT 0;
|
|
3
|
+
|
|
4
|
+
INSERT OR IGNORE INTO schema_version (version, description)
|
|
5
|
+
VALUES (14, 'Add is_static column for user profile preferences');
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
-- FTS5 virtual table for keyword search
|
|
2
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS decisions_fts USING fts5(
|
|
3
|
+
topic, decision, reasoning,
|
|
4
|
+
content='decisions',
|
|
5
|
+
content_rowid='rowid'
|
|
6
|
+
);
|
|
7
|
+
|
|
8
|
+
-- Sync triggers for external content table
|
|
9
|
+
CREATE TRIGGER IF NOT EXISTS decisions_ai AFTER INSERT ON decisions BEGIN
|
|
10
|
+
INSERT INTO decisions_fts(rowid, topic, decision, reasoning)
|
|
11
|
+
VALUES (new.rowid, new.topic, new.decision, new.reasoning);
|
|
12
|
+
END;
|
|
13
|
+
|
|
14
|
+
CREATE TRIGGER IF NOT EXISTS decisions_ad BEFORE DELETE ON decisions BEGIN
|
|
15
|
+
INSERT INTO decisions_fts(decisions_fts, rowid, topic, decision, reasoning)
|
|
16
|
+
VALUES('delete', old.rowid, old.topic, old.decision, old.reasoning);
|
|
17
|
+
END;
|
|
18
|
+
|
|
19
|
+
CREATE TRIGGER IF NOT EXISTS decisions_au BEFORE UPDATE ON decisions BEGIN
|
|
20
|
+
INSERT INTO decisions_fts(decisions_fts, rowid, topic, decision, reasoning)
|
|
21
|
+
VALUES('delete', old.rowid, old.topic, old.decision, old.reasoning);
|
|
22
|
+
END;
|
|
23
|
+
|
|
24
|
+
CREATE TRIGGER IF NOT EXISTS decisions_au2 AFTER UPDATE ON decisions BEGIN
|
|
25
|
+
INSERT INTO decisions_fts(rowid, topic, decision, reasoning)
|
|
26
|
+
VALUES (new.rowid, new.topic, new.decision, new.reasoning);
|
|
27
|
+
END;
|
|
28
|
+
|
|
29
|
+
-- Backfill existing decisions into FTS index
|
|
30
|
+
INSERT INTO decisions_fts(rowid, topic, decision, reasoning)
|
|
31
|
+
SELECT rowid, topic, decision, reasoning FROM decisions;
|
|
32
|
+
|
|
33
|
+
INSERT OR IGNORE INTO schema_version (version, description)
|
|
34
|
+
VALUES (15, 'Add FTS5 keyword search with sync triggers');
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
ALTER TABLE decisions ADD COLUMN kind TEXT DEFAULT 'decision'
|
|
2
|
+
CHECK (kind IN ('decision', 'preference', 'constraint', 'lesson', 'fact'));
|
|
3
|
+
|
|
4
|
+
ALTER TABLE decisions ADD COLUMN status TEXT DEFAULT 'active'
|
|
5
|
+
CHECK (status IN ('active', 'superseded', 'contradicted', 'stale'));
|
|
6
|
+
|
|
7
|
+
ALTER TABLE decisions ADD COLUMN summary TEXT;
|
|
8
|
+
|
|
9
|
+
UPDATE decisions
|
|
10
|
+
SET summary = decision
|
|
11
|
+
WHERE summary IS NULL;
|
|
12
|
+
|
|
13
|
+
INSERT OR IGNORE INTO schema_version (version, description)
|
|
14
|
+
VALUES (16, 'Add memory v2 kind/status/summary columns');
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS memory_scopes (
|
|
2
|
+
id TEXT PRIMARY KEY,
|
|
3
|
+
kind TEXT NOT NULL CHECK (kind IN ('global', 'user', 'channel', 'project')),
|
|
4
|
+
external_id TEXT NOT NULL,
|
|
5
|
+
created_at INTEGER DEFAULT (unixepoch() * 1000),
|
|
6
|
+
UNIQUE(kind, external_id)
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
INSERT OR IGNORE INTO schema_version (version, description)
|
|
10
|
+
VALUES (17, 'Create memory scopes table');
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS memory_scope_bindings (
|
|
2
|
+
memory_id TEXT NOT NULL,
|
|
3
|
+
scope_id TEXT NOT NULL,
|
|
4
|
+
is_primary INTEGER DEFAULT 0 CHECK (is_primary IN (0, 1)),
|
|
5
|
+
created_at INTEGER DEFAULT (unixepoch() * 1000),
|
|
6
|
+
PRIMARY KEY (memory_id, scope_id),
|
|
7
|
+
FOREIGN KEY (memory_id) REFERENCES decisions(id) ON DELETE CASCADE,
|
|
8
|
+
FOREIGN KEY (scope_id) REFERENCES memory_scopes(id) ON DELETE CASCADE
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
CREATE INDEX IF NOT EXISTS idx_memory_scope_bindings_scope_id
|
|
12
|
+
ON memory_scope_bindings(scope_id);
|
|
13
|
+
|
|
14
|
+
INSERT OR IGNORE INTO schema_version (version, description)
|
|
15
|
+
VALUES (18, 'Create memory scope bindings table');
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS memory_events (
|
|
2
|
+
event_id TEXT PRIMARY KEY,
|
|
3
|
+
event_type TEXT NOT NULL,
|
|
4
|
+
actor TEXT NOT NULL,
|
|
5
|
+
source_turn_id TEXT,
|
|
6
|
+
memory_id TEXT,
|
|
7
|
+
topic TEXT,
|
|
8
|
+
scope_refs TEXT NOT NULL,
|
|
9
|
+
evidence_refs TEXT,
|
|
10
|
+
reason TEXT,
|
|
11
|
+
created_at INTEGER NOT NULL
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
CREATE INDEX IF NOT EXISTS idx_memory_events_topic_created_at
|
|
15
|
+
ON memory_events(topic, created_at DESC);
|
|
16
|
+
|
|
17
|
+
INSERT OR IGNORE INTO schema_version (version, description)
|
|
18
|
+
VALUES (19, 'Create memory events table');
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS audit_findings (
|
|
2
|
+
finding_id TEXT PRIMARY KEY,
|
|
3
|
+
kind TEXT NOT NULL,
|
|
4
|
+
severity TEXT NOT NULL,
|
|
5
|
+
summary TEXT NOT NULL,
|
|
6
|
+
evidence_refs TEXT NOT NULL,
|
|
7
|
+
affected_memory_ids TEXT NOT NULL,
|
|
8
|
+
recommended_action TEXT NOT NULL,
|
|
9
|
+
status TEXT NOT NULL DEFAULT 'open',
|
|
10
|
+
created_at INTEGER NOT NULL,
|
|
11
|
+
resolved_at INTEGER
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
CREATE INDEX IF NOT EXISTS idx_audit_findings_status_created_at
|
|
15
|
+
ON audit_findings(status, created_at DESC);
|
|
16
|
+
|
|
17
|
+
INSERT OR IGNORE INTO schema_version (version, description)
|
|
18
|
+
VALUES (20, 'Create audit findings table');
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS memory_truth (
|
|
2
|
+
memory_id TEXT PRIMARY KEY,
|
|
3
|
+
topic TEXT NOT NULL,
|
|
4
|
+
truth_status TEXT NOT NULL,
|
|
5
|
+
effective_summary TEXT NOT NULL,
|
|
6
|
+
effective_details TEXT NOT NULL,
|
|
7
|
+
trust_score REAL NOT NULL,
|
|
8
|
+
scope_refs TEXT NOT NULL,
|
|
9
|
+
supporting_event_ids TEXT NOT NULL,
|
|
10
|
+
superseded_by TEXT,
|
|
11
|
+
contradicted_by TEXT,
|
|
12
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000),
|
|
13
|
+
updated_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
CREATE INDEX IF NOT EXISTS idx_memory_truth_topic_updated_at
|
|
17
|
+
ON memory_truth(topic, updated_at DESC);
|
|
18
|
+
|
|
19
|
+
INSERT OR IGNORE INTO schema_version (version, description)
|
|
20
|
+
VALUES (21, 'Create memory truth projection table');
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS channel_summaries (
|
|
2
|
+
channel_key TEXT PRIMARY KEY,
|
|
3
|
+
summary_markdown TEXT NOT NULL,
|
|
4
|
+
delta_hash TEXT,
|
|
5
|
+
updated_at INTEGER NOT NULL
|
|
6
|
+
);
|
|
7
|
+
|
|
8
|
+
INSERT OR IGNORE INTO schema_version (version, description)
|
|
9
|
+
VALUES (22, 'Create channel summaries table');
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS channel_summary_state (
|
|
2
|
+
channel_key TEXT PRIMARY KEY,
|
|
3
|
+
state_json TEXT NOT NULL,
|
|
4
|
+
state_hash TEXT NOT NULL,
|
|
5
|
+
updated_at INTEGER NOT NULL
|
|
6
|
+
);
|
|
7
|
+
|
|
8
|
+
INSERT OR IGNORE INTO schema_version (version, description)
|
|
9
|
+
VALUES (23, 'Create channel summary state table');
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
-- Migration 024: Add event_date column to decisions table
|
|
2
|
+
-- event_date stores the ISO 8601 date when the event actually occurred
|
|
3
|
+
-- (distinct from created_at which is ingestion time)
|
|
4
|
+
ALTER TABLE decisions ADD COLUMN event_date TEXT;
|
|
5
|
+
|
|
6
|
+
CREATE INDEX IF NOT EXISTS idx_decisions_event_date ON decisions(event_date);
|
|
7
|
+
|
|
8
|
+
INSERT OR IGNORE INTO schema_version (version, description)
|
|
9
|
+
VALUES (24, 'Add event_date column for temporal tracking');
|
|
@@ -47,7 +47,7 @@ export declare abstract class DatabaseAdapter {
|
|
|
47
47
|
* @param limit - Max results
|
|
48
48
|
* @returns Search results with distance
|
|
49
49
|
*/
|
|
50
|
-
abstract vectorSearch(embedding: Float32Array | number[], limit: number): VectorSearchResult[] | null;
|
|
50
|
+
abstract vectorSearch(embedding: Float32Array | number[], limit: number, topicPrefix?: string): VectorSearchResult[] | null;
|
|
51
51
|
/**
|
|
52
52
|
* Insert vector embedding
|
|
53
53
|
* @param rowid - Decision rowid
|
package/dist/db-adapter/index.js
CHANGED
|
@@ -24,14 +24,8 @@ Object.defineProperty(exports, "DatabaseAdapter", { enumerable: true, get: funct
|
|
|
24
24
|
* @returns Configured SQLite adapter instance
|
|
25
25
|
*/
|
|
26
26
|
function createAdapter(config = {}) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
configuredDriver !== 'node-sqlite' &&
|
|
30
|
-
configuredDriver !== 'node:sqlite' &&
|
|
31
|
-
configuredDriver !== 'auto') {
|
|
32
|
-
throw new Error(`Unsupported SQLite driver "${configuredDriver}". MAMA now requires node:sqlite.`);
|
|
33
|
-
}
|
|
34
|
-
(0, debug_logger_js_1.info)('[db-adapter] Using node:sqlite adapter');
|
|
27
|
+
// SQLiteAdapter (extends NodeSQLiteAdapter) auto-detects: better-sqlite3 (preferred, FTS5) → node:sqlite (fallback)
|
|
28
|
+
(0, debug_logger_js_1.info)('[db-adapter] Creating SQLite adapter (auto-detect driver)');
|
|
35
29
|
const dbPath = config.dbPath || process.env.MAMA_DB_PATH;
|
|
36
30
|
return new sqlite_adapter_js_1.SQLiteAdapter({ dbPath });
|
|
37
31
|
}
|
|
@@ -24,6 +24,7 @@ interface NodeSQLiteStatementLike {
|
|
|
24
24
|
declare class NodeSQLiteConnection {
|
|
25
25
|
private db;
|
|
26
26
|
private connected;
|
|
27
|
+
private isBetterSQLite3;
|
|
27
28
|
constructor(db: NodeSQLiteDatabaseLike);
|
|
28
29
|
prepare(sql: string): NodeSQLiteStatementLike;
|
|
29
30
|
exec(sql: string): void;
|
|
@@ -37,16 +38,20 @@ export declare class NodeSQLiteAdapter extends DatabaseAdapter {
|
|
|
37
38
|
private config;
|
|
38
39
|
private db;
|
|
39
40
|
private _vectorSearchEnabled;
|
|
41
|
+
private vectorCache;
|
|
42
|
+
private topicCache;
|
|
40
43
|
constructor(config?: SQLiteAdapterConfig);
|
|
41
44
|
get vectorSearchEnabled(): boolean;
|
|
42
45
|
getDbPath(): string;
|
|
43
46
|
connect(): NodeSQLiteConnection;
|
|
47
|
+
reloadVectorCache(): void;
|
|
48
|
+
private loadVectorCache;
|
|
44
49
|
disconnect(): void;
|
|
45
50
|
isConnected(): boolean;
|
|
46
51
|
prepare(sql: string): Statement;
|
|
47
52
|
exec(sql: string): void;
|
|
48
53
|
transaction<T>(fn: () => T): T;
|
|
49
|
-
vectorSearch(embedding: Float32Array | number[], limit?: number): VectorSearchResult[] | null;
|
|
54
|
+
vectorSearch(embedding: Float32Array | number[], limit?: number, topicPrefix?: string): VectorSearchResult[] | null;
|
|
50
55
|
insertEmbedding(rowid: number, embedding: Float32Array | number[]): RunResult | null;
|
|
51
56
|
getLastInsertRowid(): number;
|
|
52
57
|
runMigrations(migrationsDir: string): void;
|
|
@@ -18,19 +18,34 @@ const debug_logger_js_1 = require("../debug-logger.js");
|
|
|
18
18
|
const embeddings_js_1 = require("../embeddings.js");
|
|
19
19
|
const LEGACY_DB_PATH = path_1.default.join(os_1.default.homedir(), '.spinelift', 'memories.db');
|
|
20
20
|
const DEFAULT_DB_PATH = path_1.default.join(os_1.default.homedir(), '.claude', 'mama-memory.db');
|
|
21
|
+
// Prefer better-sqlite3 (includes FTS5) over node:sqlite (lacks FTS5)
|
|
22
|
+
let BetterSQLite3 = null;
|
|
21
23
|
let DatabaseSync = null;
|
|
22
24
|
try {
|
|
23
25
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
24
|
-
|
|
26
|
+
const bs3 = require('better-sqlite3');
|
|
27
|
+
BetterSQLite3 = 'default' in bs3 ? bs3.default : bs3;
|
|
25
28
|
}
|
|
26
29
|
catch {
|
|
27
|
-
|
|
30
|
+
BetterSQLite3 = null;
|
|
31
|
+
}
|
|
32
|
+
if (!BetterSQLite3) {
|
|
33
|
+
try {
|
|
34
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
35
|
+
({ DatabaseSync } = require('node:sqlite'));
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
DatabaseSync = null;
|
|
39
|
+
}
|
|
28
40
|
}
|
|
29
41
|
class NodeSQLiteConnection {
|
|
30
42
|
db;
|
|
31
43
|
connected = true;
|
|
44
|
+
isBetterSQLite3;
|
|
32
45
|
constructor(db) {
|
|
33
46
|
this.db = db;
|
|
47
|
+
// better-sqlite3 databases have a native .pragma() method
|
|
48
|
+
this.isBetterSQLite3 = typeof db.pragma === 'function';
|
|
34
49
|
}
|
|
35
50
|
prepare(sql) {
|
|
36
51
|
return this.db.prepare(sql);
|
|
@@ -40,6 +55,11 @@ class NodeSQLiteConnection {
|
|
|
40
55
|
}
|
|
41
56
|
pragma(sql, options) {
|
|
42
57
|
const query = sql.trim().replace(/^PRAGMA\s+/i, '');
|
|
58
|
+
if (this.isBetterSQLite3) {
|
|
59
|
+
// better-sqlite3 has a native pragma method that handles both read and write pragmas
|
|
60
|
+
return this.db.pragma(query, options);
|
|
61
|
+
}
|
|
62
|
+
// node:sqlite fallback: use prepare()
|
|
43
63
|
const stmt = this.db.prepare(`PRAGMA ${query}`);
|
|
44
64
|
if (options?.simple) {
|
|
45
65
|
const row = stmt.get();
|
|
@@ -65,6 +85,8 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
|
|
|
65
85
|
config;
|
|
66
86
|
db = null;
|
|
67
87
|
_vectorSearchEnabled = true;
|
|
88
|
+
vectorCache = new Map();
|
|
89
|
+
topicCache = new Map();
|
|
68
90
|
constructor(config = {}) {
|
|
69
91
|
super();
|
|
70
92
|
this.config = config;
|
|
@@ -90,27 +112,67 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
|
|
|
90
112
|
if (this.db) {
|
|
91
113
|
return this.db;
|
|
92
114
|
}
|
|
93
|
-
|
|
94
|
-
|
|
115
|
+
const Driver = BetterSQLite3 || DatabaseSync;
|
|
116
|
+
if (!Driver) {
|
|
117
|
+
throw new Error('No SQLite driver available. Install better-sqlite3 or use Node 22.13+ (node:sqlite).');
|
|
95
118
|
}
|
|
119
|
+
const driverName = BetterSQLite3 ? 'better-sqlite3' : 'node:sqlite';
|
|
96
120
|
const dbPath = this.getDbPath();
|
|
97
121
|
const dbDir = path_1.default.dirname(dbPath);
|
|
98
122
|
if (!fs_1.default.existsSync(dbDir)) {
|
|
99
123
|
fs_1.default.mkdirSync(dbDir, { recursive: true });
|
|
100
|
-
(0, debug_logger_js_1.info)(`[
|
|
124
|
+
(0, debug_logger_js_1.info)(`[sqlite-adapter] Created database directory: ${dbDir}`);
|
|
101
125
|
}
|
|
102
|
-
const database = new
|
|
126
|
+
const database = new Driver(dbPath);
|
|
103
127
|
this.db = new NodeSQLiteConnection(database);
|
|
104
|
-
(0, debug_logger_js_1.info)(`[
|
|
128
|
+
(0, debug_logger_js_1.info)(`[sqlite-adapter] Opened database at: ${dbPath} (driver: ${driverName})`);
|
|
105
129
|
this.db.pragma('journal_mode = WAL');
|
|
106
130
|
this.db.pragma('busy_timeout = 5000');
|
|
107
131
|
this.db.pragma('synchronous = NORMAL');
|
|
108
132
|
this.db.pragma('cache_size = -64000');
|
|
109
133
|
this.db.pragma('temp_store = MEMORY');
|
|
110
134
|
this.db.pragma('foreign_keys = ON');
|
|
111
|
-
(0, debug_logger_js_1.info)('[
|
|
135
|
+
(0, debug_logger_js_1.info)('[sqlite-adapter] Vector search: pure TS cosine similarity (no native extensions)');
|
|
136
|
+
this.loadVectorCache();
|
|
112
137
|
return this.db;
|
|
113
138
|
}
|
|
139
|
+
reloadVectorCache() {
|
|
140
|
+
this.loadVectorCache();
|
|
141
|
+
}
|
|
142
|
+
loadVectorCache() {
|
|
143
|
+
if (!this.db)
|
|
144
|
+
return;
|
|
145
|
+
const tableCheck = this.db
|
|
146
|
+
.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='embeddings'`)
|
|
147
|
+
.all();
|
|
148
|
+
if (tableCheck.length === 0) {
|
|
149
|
+
this.vectorCache.clear();
|
|
150
|
+
this.topicCache.clear();
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const start = Date.now();
|
|
154
|
+
const rows = this.db.prepare('SELECT rowid, embedding FROM embeddings').all();
|
|
155
|
+
const CACHE_WARN_THRESHOLD = 100_000;
|
|
156
|
+
this.vectorCache.clear();
|
|
157
|
+
for (const row of rows) {
|
|
158
|
+
const vec = bytesToVector(row.embedding);
|
|
159
|
+
if (vec) {
|
|
160
|
+
this.vectorCache.set(row.rowid, vec);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// Load topic cache for scoped vector search
|
|
164
|
+
this.topicCache.clear();
|
|
165
|
+
const topicRows = this.db.prepare('SELECT rowid, topic FROM decisions').all();
|
|
166
|
+
for (const row of topicRows) {
|
|
167
|
+
this.topicCache.set(row.rowid, row.topic);
|
|
168
|
+
}
|
|
169
|
+
const count = this.vectorCache.size;
|
|
170
|
+
const elapsed = Date.now() - start;
|
|
171
|
+
(0, debug_logger_js_1.info)(`[node-sqlite-adapter] Vector cache loaded: ${count} embeddings in ${elapsed}ms`);
|
|
172
|
+
if (count > CACHE_WARN_THRESHOLD) {
|
|
173
|
+
(0, debug_logger_js_1.warn)(`[node-sqlite-adapter] Vector cache holds ${count} embeddings — consider LRU eviction or on-demand loading for large datasets`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
114
176
|
disconnect() {
|
|
115
177
|
if (this.db) {
|
|
116
178
|
this.db.close();
|
|
@@ -157,53 +219,39 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
|
|
|
157
219
|
throw error;
|
|
158
220
|
}
|
|
159
221
|
}
|
|
160
|
-
vectorSearch(embedding, limit = 5) {
|
|
222
|
+
vectorSearch(embedding, limit = 5, topicPrefix) {
|
|
161
223
|
if (!this.isConnected()) {
|
|
162
224
|
throw new Error('Database not connected');
|
|
163
225
|
}
|
|
164
226
|
const queryVector = embedding instanceof Float32Array ? embedding : Float32Array.from(embedding);
|
|
165
|
-
const tableCheck = this.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='embeddings'`).all();
|
|
166
|
-
if (tableCheck.length === 0) {
|
|
167
|
-
throw new Error('Embeddings table missing');
|
|
168
|
-
}
|
|
169
227
|
const effectiveLimit = Math.max(limit, 1);
|
|
170
|
-
const batchSize = 500;
|
|
171
228
|
const bestMatches = [];
|
|
172
|
-
let
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const rows = this.prepare('SELECT rowid, embedding FROM embeddings LIMIT ? OFFSET ?').all(batchSize, offset);
|
|
176
|
-
if (rows.length === 0) {
|
|
177
|
-
hasMoreRows = false;
|
|
229
|
+
let minScore = -Infinity;
|
|
230
|
+
for (const [rowid, candidate] of this.vectorCache) {
|
|
231
|
+
if (candidate.length !== queryVector.length)
|
|
178
232
|
continue;
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const
|
|
182
|
-
if (!
|
|
183
|
-
continue;
|
|
184
|
-
if (candidate.length !== queryVector.length) {
|
|
185
|
-
(0, debug_logger_js_1.warn)(`Skipping rowid ${row.rowid}: dimension mismatch (${candidate.length} vs ${queryVector.length})`);
|
|
233
|
+
// Pre-filter by topic prefix before computing similarity
|
|
234
|
+
if (topicPrefix) {
|
|
235
|
+
const topic = this.topicCache.get(rowid);
|
|
236
|
+
if (!topic || !topic.startsWith(topicPrefix))
|
|
186
237
|
continue;
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
distance: 1 - similarity,
|
|
193
|
-
};
|
|
194
|
-
if (bestMatches.length < effectiveLimit) {
|
|
195
|
-
bestMatches.push(scoredRow);
|
|
196
|
-
bestMatches.sort((a, b) => (b.similarity ?? 0) - (a.similarity ?? 0));
|
|
197
|
-
continue;
|
|
198
|
-
}
|
|
199
|
-
const weakestMatch = bestMatches[bestMatches.length - 1];
|
|
200
|
-
if ((scoredRow.similarity ?? 0) > (weakestMatch.similarity ?? 0)) {
|
|
201
|
-
bestMatches.pop();
|
|
202
|
-
bestMatches.push(scoredRow);
|
|
238
|
+
}
|
|
239
|
+
const similarity = (0, embeddings_js_1.cosineSimilarity)(candidate, queryVector);
|
|
240
|
+
if (bestMatches.length < effectiveLimit) {
|
|
241
|
+
bestMatches.push({ rowid, similarity, distance: 1 - similarity });
|
|
242
|
+
if (bestMatches.length === effectiveLimit) {
|
|
203
243
|
bestMatches.sort((a, b) => (b.similarity ?? 0) - (a.similarity ?? 0));
|
|
244
|
+
minScore = bestMatches[bestMatches.length - 1].similarity ?? -Infinity;
|
|
204
245
|
}
|
|
205
246
|
}
|
|
206
|
-
|
|
247
|
+
else if (similarity > minScore) {
|
|
248
|
+
bestMatches[bestMatches.length - 1] = { rowid, similarity, distance: 1 - similarity };
|
|
249
|
+
bestMatches.sort((a, b) => (b.similarity ?? 0) - (a.similarity ?? 0));
|
|
250
|
+
minScore = bestMatches[bestMatches.length - 1].similarity ?? -Infinity;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (bestMatches.length < effectiveLimit) {
|
|
254
|
+
bestMatches.sort((a, b) => (b.similarity ?? 0) - (a.similarity ?? 0));
|
|
207
255
|
}
|
|
208
256
|
return bestMatches;
|
|
209
257
|
}
|
|
@@ -217,7 +265,13 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
|
|
|
217
265
|
INSERT OR REPLACE INTO embeddings(rowid, embedding)
|
|
218
266
|
VALUES (?, ?)
|
|
219
267
|
`);
|
|
220
|
-
|
|
268
|
+
const result = stmt.run(rowid, buffer);
|
|
269
|
+
// Keep in-memory caches in sync
|
|
270
|
+
this.vectorCache.set(rowid, vec);
|
|
271
|
+
const topicRow = this.prepare('SELECT topic FROM decisions WHERE rowid = ?').get(rowid);
|
|
272
|
+
if (topicRow)
|
|
273
|
+
this.topicCache.set(rowid, topicRow.topic);
|
|
274
|
+
return result;
|
|
221
275
|
}
|
|
222
276
|
getLastInsertRowid() {
|
|
223
277
|
if (!this.isConnected()) {
|