vektor-slipstream 1.0.2 → 1.0.3

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/slipstream-core.js +560 -192
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vektor-slipstream",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Hardware-accelerated persistent memory for AI agents. Local-first, zero cloud dependency, $0 embedding cost.",
5
5
  "main": "slipstream-core.js",
6
6
  "exports": {
@@ -2,50 +2,191 @@
2
2
 
3
3
  /**
4
4
  * VEKTOR SLIPSTREAM
5
- * slipstream-core.js — Main Entry Point
5
+ * slipstream-core.js — Resilient Self-Contained Build
6
6
  * ─────────────────────────────────────────────────────────────────────────────
7
- * The hardware-accelerated memory engine. Parallel to vektor-core.js but
8
- * with native ONNX embeddings, Hyper-PRAGMA SQLite, and the pre-warmed
9
- * inference pipeline.
7
+ * Drop-in replacement for the ONNX-dependent version.
8
+ * Same public API: createMemory({ agentId, dbPath, silent })
10
9
  *
11
- * Drop-in API replacement for vektor-core.js:
12
- * const { createMemory } = require('./slipstream-core');
10
+ * Embedding strategy (best available, in order):
11
+ * 1. @xenova/transformers — full transformer embeddings, pure JS, no ONNX native
12
+ * 2. Hash-projection — deterministic 384-dim vector, zero deps, instant boot
13
13
  *
14
- * Same methods. Faster engine underneath.
14
+ * SQLite strategy (best available, in order):
15
+ * 1. better-sqlite3 — native, fast
16
+ * 2. Falls back with clear error message
17
+ *
18
+ * All other files (slipstream-db, slipstream-embedder, detect-hardware) are
19
+ * inlined here — nothing to require().
15
20
  * ─────────────────────────────────────────────────────────────────────────────
16
21
  */
17
22
 
18
- const { initSlipstreamDB, getDBStats } = require('./slipstream-db');
19
- const { SlipstreamEmbedder } = require('./slipstream-embedder');
20
- const { getEPLabel } = require('./detect-hardware');
21
- const { validateLicence } = require('./vektor-licence');
23
+ const path = require('path');
24
+ const fs = require('fs');
25
+
26
+ // ─── SQLite loader — better-sqlite3 with clear error ─────────────────────────
27
+ function loadSQLite(dbPath) {
28
+ try {
29
+ const Database = require('better-sqlite3');
30
+ const db = new Database(dbPath);
31
+
32
+ // Hyper-PRAGMAs — same as original slipstream-db.js
33
+ db.pragma('journal_mode = WAL');
34
+ db.pragma('synchronous = NORMAL');
35
+ db.pragma('cache_size = -65536'); // 64 MB
36
+ db.pragma('mmap_size = 1073741824'); // 1 GB
37
+ db.pragma('temp_store = MEMORY');
38
+ db.pragma('page_size = 4096');
39
+
40
+ return db;
41
+ } catch (e) {
42
+ if (e.code === 'ERR_DLOPEN_FAILED' || e.message.includes('NODE_MODULE_VERSION')) {
43
+ console.error('');
44
+ console.error(' ✗ better-sqlite3 binary mismatch (compiled for different Node version)');
45
+ console.error(' → Fix: npm rebuild better-sqlite3');
46
+ console.error(' → Then restart the server');
47
+ console.error('');
48
+ throw new Error('better-sqlite3 rebuild required: npm rebuild better-sqlite3');
49
+ }
50
+ throw e;
51
+ }
52
+ }
22
53
 
23
- // ─── Boot Banner ──────────────────────────────────────────────────────────────
54
+ // ─── Embedding Engine ─────────────────────────────────────────────────────────
55
+ // Strategy 1: @xenova/transformers (pure JS, no native deps)
56
+ // Strategy 2: Hash-projection (zero deps, deterministic, always works)
57
+
58
+ const EMBED_DIM = 384;
59
+
60
+ // Hash-projection embedder — deterministic 384-dim float32 vector
61
+ // Uses FNV-1a family of hashes over character n-grams
62
+ // Sufficient for keyword recall; upgrade to xenova for semantic search
63
+ function hashEmbed(text) {
64
+ const vec = new Float32Array(EMBED_DIM);
65
+ const tokens = text.toLowerCase()
66
+ .replace(/[^\w\s]/g, ' ')
67
+ .split(/\s+/)
68
+ .filter(t => t.length > 1);
69
+
70
+ // Add character bigrams and trigrams for sub-word coverage
71
+ const allGrams = [...tokens];
72
+ for (const tok of tokens) {
73
+ for (let i = 0; i < tok.length - 1; i++) allGrams.push(tok.slice(i, i + 2));
74
+ for (let i = 0; i < tok.length - 2; i++) allGrams.push(tok.slice(i, i + 3));
75
+ }
24
76
 
25
- function printBanner(embedder, dbStats, bootMs) {
26
- const stats = embedder.getStats();
27
- const ep = stats.epLabel;
28
- const icon = stats.ep === 'cpu' ? '⚙️ ' : '🚀';
77
+ for (const gram of allGrams) {
78
+ // FNV-1a 32-bit over the gram characters
79
+ let h = 2166136261;
80
+ for (let i = 0; i < gram.length; i++) {
81
+ h ^= gram.charCodeAt(i);
82
+ h = (h * 16777619) >>> 0;
83
+ }
84
+ // Project into two dimensions for richer coverage
85
+ const idx1 = h % EMBED_DIM;
86
+ const idx2 = (h >>> 16) % EMBED_DIM;
87
+ const sign = (h & 1) ? 1 : -1;
88
+ vec[idx1] += sign;
89
+ vec[idx2] += sign * 0.5;
90
+ }
29
91
 
30
- console.log('');
31
- console.log(' ╔══════════════════════════════════════════════════════╗');
32
- console.log(' ║ VEKTOR SLIPSTREAM ACTIVE ║');
33
- console.log(' ╚══════════════════════════════════════════════════════╝');
34
- console.log('');
35
- console.log(` ${icon} EP: ${ep}`);
36
- console.log(` 🧠 Model: all-MiniLM-L6-v2 INT8 quantized`);
37
- console.log(` ⚡ Embed: ${stats.avgMs}ms (post-warmup)`);
38
- console.log(` 💾 DB: WAL | mmap:1GB | cache:64MB`);
39
- console.log(` 🔥 Warm: ✓`);
40
- console.log(` ⏱ Boot: ${bootMs}ms total`);
41
- console.log('');
92
+ // L2 normalize
93
+ let norm = 0;
94
+ for (let i = 0; i < EMBED_DIM; i++) norm += vec[i] * vec[i];
95
+ norm = Math.sqrt(norm) + 1e-10;
96
+ for (let i = 0; i < EMBED_DIM; i++) vec[i] /= norm;
97
+
98
+ return vec;
99
+ }
100
+
101
+ // Xenova embedder — lazy-loaded, falls back to hash if unavailable
102
+ let _xenovaPipeline = null;
103
+ let _xenovaFailed = false;
104
+ let _xenovaLoading = false;
105
+ let _xenovaQueue = [];
106
+
107
+ async function xenovaEmbed(text) {
108
+ if (_xenovaFailed) return null;
109
+
110
+ // If pipeline is ready, use it
111
+ if (_xenovaPipeline) {
112
+ try {
113
+ const output = await _xenovaPipeline(text, { pooling: 'mean', normalize: true });
114
+ return new Float32Array(output.data);
115
+ } catch (e) {
116
+ console.warn('[slipstream] xenova embed failed:', e.message);
117
+ return null;
118
+ }
119
+ }
120
+
121
+ // If loading, queue this call
122
+ if (_xenovaLoading) {
123
+ return new Promise((resolve) => {
124
+ _xenovaQueue.push({ text, resolve });
125
+ });
126
+ }
127
+
128
+ // Start loading
129
+ _xenovaLoading = true;
130
+ try {
131
+ const { pipeline, env } = require('@xenova/transformers');
132
+ // Use local cache, don't require internet
133
+ env.allowLocalModels = true;
134
+ env.allowRemoteModels = true;
135
+ env.cacheDir = path.join(require('os').homedir(), '.cache', 'xenova');
136
+
137
+ console.log('[slipstream] Loading @xenova/transformers (first run may download model ~25MB)...');
138
+ _xenovaPipeline = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2', {
139
+ quantized: true, // INT8 — matches original slipstream spec
140
+ });
141
+ console.log('[slipstream] ✓ Semantic embeddings active (all-MiniLM-L6-v2 INT8)');
142
+
143
+ // Drain the queue
144
+ _xenovaLoading = false;
145
+ const queued = [..._xenovaQueue];
146
+ _xenovaQueue = [];
147
+ for (const item of queued) {
148
+ try {
149
+ const out = await _xenovaPipeline(item.text, { pooling: 'mean', normalize: true });
150
+ item.resolve(new Float32Array(out.data));
151
+ } catch (_) { item.resolve(null); }
152
+ }
153
+
154
+ // Now embed the original text
155
+ const output = await _xenovaPipeline(text, { pooling: 'mean', normalize: true });
156
+ return new Float32Array(output.data);
157
+
158
+ } catch (e) {
159
+ _xenovaFailed = true;
160
+ _xenovaLoading = false;
161
+ // Drain queue with null
162
+ for (const item of _xenovaQueue) item.resolve(null);
163
+ _xenovaQueue = [];
164
+ if (e.code !== 'MODULE_NOT_FOUND') {
165
+ console.warn('[slipstream] @xenova/transformers failed:', e.message);
166
+ }
167
+ return null;
168
+ }
169
+ }
170
+
171
+ // Unified embed — tries xenova, falls back to hash
172
+ async function embed(text) {
173
+ const xVec = await xenovaEmbed(text);
174
+ if (xVec) return xVec;
175
+ return hashEmbed(text);
176
+ }
177
+
178
+ function getEmbedderStats() {
179
+ const mode = _xenovaPipeline ? 'xenova/all-MiniLM-L6-v2 INT8' : 'hash-projection (384-dim)';
180
+ const ep = _xenovaPipeline ? 'CPU·ONNX' : 'CPU·Hash';
181
+ return { mode, ep, epLabel: ep, avgMs: _xenovaPipeline ? '~20' : '<1', dim: EMBED_DIM };
42
182
  }
43
183
 
44
184
  // ─── Vector Utilities ─────────────────────────────────────────────────────────
45
185
 
46
186
  function cosineSimilarity(a, b) {
47
187
  let dot = 0, normA = 0, normB = 0;
48
- for (let i = 0; i < a.length; i++) {
188
+ const len = Math.min(a.length, b.length);
189
+ for (let i = 0; i < len; i++) {
49
190
  dot += a[i] * b[i];
50
191
  normA += a[i] * a[i];
51
192
  normB += b[i] * b[i];
@@ -58,7 +199,10 @@ function serializeVector(float32Array) {
58
199
  }
59
200
 
60
201
  function deserializeVector(buffer) {
61
- return new Float32Array(buffer.buffer, buffer.byteOffset, buffer.byteLength / 4);
202
+ if (!buffer || buffer.length < 4) return new Float32Array(0);
203
+ return new Float32Array(
204
+ buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength)
205
+ );
62
206
  }
63
207
 
64
208
  // ─── Schema ───────────────────────────────────────────────────────────────────
@@ -67,9 +211,12 @@ function initSchema(db) {
67
211
  db.exec(`
68
212
  CREATE TABLE IF NOT EXISTS memories (
69
213
  id INTEGER PRIMARY KEY AUTOINCREMENT,
70
- agent_id TEXT NOT NULL,
214
+ agent_id TEXT NOT NULL DEFAULT 'default',
71
215
  content TEXT NOT NULL,
216
+ edge_type TEXT DEFAULT 'semantic',
72
217
  summary TEXT,
218
+ tags TEXT DEFAULT '',
219
+ source_agent TEXT DEFAULT 'system',
73
220
  importance REAL DEFAULT 1.0,
74
221
  vector BLOB,
75
222
  created_at INTEGER DEFAULT (strftime('%s', 'now')),
@@ -78,262 +225,483 @@ function initSchema(db) {
78
225
 
79
226
  CREATE INDEX IF NOT EXISTS idx_memories_agent
80
227
  ON memories(agent_id);
81
-
228
+ CREATE INDEX IF NOT EXISTS idx_memories_type
229
+ ON memories(edge_type);
82
230
  CREATE INDEX IF NOT EXISTS idx_memories_importance
83
231
  ON memories(agent_id, importance DESC);
232
+ CREATE INDEX IF NOT EXISTS idx_memories_created
233
+ ON memories(created_at DESC);
84
234
 
85
235
  CREATE TABLE IF NOT EXISTS memory_edges (
86
236
  id INTEGER PRIMARY KEY AUTOINCREMENT,
87
- agent_id TEXT NOT NULL,
237
+ agent_id TEXT NOT NULL DEFAULT 'default',
88
238
  source_id INTEGER NOT NULL,
89
239
  target_id INTEGER NOT NULL,
90
- edge_type TEXT NOT NULL,
240
+ edge_type TEXT NOT NULL DEFAULT 'related',
91
241
  weight REAL DEFAULT 1.0,
92
242
  created_at INTEGER DEFAULT (strftime('%s', 'now')),
93
- FOREIGN KEY (source_id) REFERENCES memories(id),
94
- FOREIGN KEY (target_id) REFERENCES memories(id)
243
+ FOREIGN KEY (source_id) REFERENCES memories(id) ON DELETE CASCADE,
244
+ FOREIGN KEY (target_id) REFERENCES memories(id) ON DELETE CASCADE
95
245
  );
96
246
 
97
247
  CREATE INDEX IF NOT EXISTS idx_edges_agent
98
248
  ON memory_edges(agent_id, edge_type);
249
+
250
+ CREATE TABLE IF NOT EXISTS agent_capabilities (
251
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
252
+ agent_id TEXT NOT NULL,
253
+ engine_id TEXT NOT NULL,
254
+ label TEXT,
255
+ created_at TEXT DEFAULT (datetime('now')),
256
+ UNIQUE(agent_id, engine_id)
257
+ );
258
+
259
+ CREATE TABLE IF NOT EXISTS directives (
260
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
261
+ title TEXT NOT NULL,
262
+ description TEXT DEFAULT '',
263
+ status TEXT DEFAULT 'BACKLOG',
264
+ priority TEXT DEFAULT 'MED',
265
+ agent TEXT DEFAULT '',
266
+ project_id INTEGER DEFAULT NULL,
267
+ result TEXT,
268
+ created_at INTEGER DEFAULT (strftime('%s','now')*1000),
269
+ updated_at INTEGER DEFAULT (strftime('%s','now')*1000)
270
+ );
271
+
272
+ CREATE TABLE IF NOT EXISTS hitl_queue (
273
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
274
+ directive_id TEXT,
275
+ agent TEXT,
276
+ action TEXT,
277
+ status TEXT DEFAULT 'PENDING',
278
+ result TEXT,
279
+ meta TEXT,
280
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
281
+ );
99
282
  `);
283
+
284
+ // Safe migrations — add columns that may not exist in older DBs
285
+ const migrations = [
286
+ "ALTER TABLE memories ADD COLUMN edge_type TEXT DEFAULT 'semantic'",
287
+ "ALTER TABLE memories ADD COLUMN tags TEXT DEFAULT ''",
288
+ "ALTER TABLE memories ADD COLUMN source_agent TEXT DEFAULT 'system'",
289
+ "ALTER TABLE directives ADD COLUMN result TEXT",
290
+ "ALTER TABLE directives ADD COLUMN description TEXT DEFAULT ''",
291
+ "ALTER TABLE hitl_queue ADD COLUMN action TEXT",
292
+ ];
293
+ for (const m of migrations) {
294
+ try { db.exec(m); } catch (_) { /* column already exists — safe */ }
295
+ }
296
+ }
297
+
298
+ // ─── AUDN Contradiction Detector ─────────────────────────────────────────────
299
+ // Returns true if newText likely contradicts existingText.
300
+ // Three signals: negation words, opposing numeric values, reversal phrases.
301
+
302
+ const NEGATION_WORDS = new Set([
303
+ 'not','no','never','none','cannot','cant',"can't",'wont',"won't",
304
+ 'isnt',"isn't",'arent',"aren't",'wasnt',"wasn't",'doesnt',"doesn't",
305
+ 'didnt',"didn't",'shouldnt',"shouldn't",'false','incorrect','wrong',
306
+ 'opposite','contrary','denied','declined','rejected',
307
+ ]);
308
+
309
+ const REVERSAL_PHRASES = [
310
+ /no longer/i, /used to/i, /changed (to|from)/i, /now (is|are|was)\b/i,
311
+ /instead of/i, /rather than/i, /replaced by/i, /switched (to|from)/i,
312
+ /corrected to/i, /updated to/i, /actually/i, /in fact/i,
313
+ ];
314
+
315
+ function _tokenize(text) {
316
+ return text.toLowerCase().replace(/[^\w\s]/g, ' ').split(/\s+/).filter(t => t.length > 1);
317
+ }
318
+
319
+ function _extractNumbers(text) {
320
+ return (text.match(/-?\d+(?:\.\d+)?/g) || []).map(Number);
321
+ }
322
+
323
+ function _isContradiction(newText, existingText) {
324
+ const newTok = _tokenize(newText);
325
+ const exTok = _tokenize(existingText);
326
+
327
+ // Signal 1: shared keywords + negation polarity mismatch
328
+ const sharedKeywords = newTok.filter(t => exTok.includes(t) && t.length > 3);
329
+ if (sharedKeywords.length > 0) {
330
+ const newNegated = newTok.some(t => NEGATION_WORDS.has(t));
331
+ const exNegated = exTok.some(t => NEGATION_WORDS.has(t));
332
+ if (newNegated !== exNegated) return true;
333
+ }
334
+
335
+ // Signal 2: opposing numeric values on same subject (>20% change)
336
+ const newNums = _extractNumbers(newText);
337
+ const exNums = _extractNumbers(existingText);
338
+ if (newNums.length && exNums.length && sharedKeywords.length > 0) {
339
+ const exVal = exNums[0];
340
+ if (exVal !== 0 && Math.abs(newNums[0] - exVal) / Math.abs(exVal) > 0.20) return true;
341
+ }
342
+
343
+ // Signal 3: explicit reversal language in new text
344
+ if (REVERSAL_PHRASES.some(p => p.test(newText))) return true;
345
+
346
+ return false;
100
347
  }
101
348
 
102
349
  // ─── SlipstreamMemory ─────────────────────────────────────────────────────────
103
350
 
104
351
  class SlipstreamMemory {
105
- constructor(db, embedder, agentId) {
106
- this.db = db;
107
- this.embedder = embedder;
108
- this.agentId = agentId;
352
+ constructor(db, agentId) {
353
+ this.db = db;
354
+ this.agentId = agentId;
355
+ // Expose embedder stats for boot banner
356
+ this.embedder = { getStats: getEmbedderStats };
357
+ this.ep = getEmbedderStats().ep;
358
+ this.model = getEmbedderStats().mode;
109
359
  }
110
360
 
111
- /**
112
- * remember(text)
113
- * Stores a memory with its vector embedding.
114
- * Synchronous write path no batching, full durability guaranteed.
115
- *
116
- * @param {string} text
117
- * @returns {Promise<{id: number}>}
118
- */
361
+ // ── remember(text, opts) — AUDN router ───────────────────────────────────
362
+ // Routes each incoming memory through:
363
+ // NO_OP — exact duplicate, discard
364
+ // UPDATE high similarity + contradiction signal, update existing
365
+ // ADD — genuinely new, insert
366
+ //
367
+ // Thresholds (tunable via opts):
368
+ // NO_OP : cosine >= 0.98 (near-identical wording)
369
+ // UPDATE : cosine >= 0.82 AND contradiction score > 0
370
+ // ADD : everything else
371
+
119
372
  async remember(text, opts = {}) {
120
- const vector = await this.embedder.embed(text);
121
- const vectorBlob = serializeVector(vector);
122
- const importance = Number(opts.importance) || 1;
373
+ const NO_OP_THRESHOLD = opts.noop_threshold ?? 0.98;
374
+ const UPDATE_THRESHOLD = opts.update_threshold ?? 0.82;
375
+
376
+ const vec = await embed(text);
377
+ const vectorBlob = serializeVector(vec);
378
+ const importance = Number(opts.importance) || 1;
379
+ const edgeType = opts.type || opts.edge_type || 'semantic';
380
+ const tags = Array.isArray(opts.tags) ? opts.tags.join(',') : (opts.tags || '');
381
+ const sourceAgent = opts.agent || opts.source_agent || this.agentId;
382
+
383
+ // ── Pull top candidates by cosine similarity ──────────────────────────
384
+ const candidates = this.db.prepare(`
385
+ SELECT id, content, edge_type, importance, vector
386
+ FROM memories
387
+ WHERE agent_id = ? AND vector IS NOT NULL
388
+ ORDER BY importance DESC, created_at DESC
389
+ LIMIT 200
390
+ `).all(this.agentId);
123
391
 
124
- const stmt = this.db.prepare(`
125
- INSERT INTO memories (agent_id, content, vector, importance)
126
- VALUES (?, ?, ?, ?)
127
- `);
392
+ let bestScore = 0;
393
+ let bestRow = null;
128
394
 
129
- const result = stmt.run(this.agentId, text, vectorBlob, importance);
130
- return { id: result.lastInsertRowid };
395
+ for (const row of candidates) {
396
+ const rowVec = deserializeVector(row.vector);
397
+ if (!rowVec.length) continue;
398
+ const score = cosineSimilarity(vec, rowVec);
399
+ if (score > bestScore) { bestScore = score; bestRow = row; }
400
+ }
401
+
402
+ // ── NO_OP — near-identical wording ───────────────────────────────────
403
+ if (bestScore >= NO_OP_THRESHOLD) {
404
+ return { action: 'NO_OP', id: bestRow.id, score: bestScore, reason: 'exact duplicate' };
405
+ }
406
+
407
+ // ── UPDATE — similar + contradiction detected ─────────────────────────
408
+ if (bestScore >= UPDATE_THRESHOLD && bestRow && _isContradiction(text, bestRow.content)) {
409
+ const newImportance = Math.max(bestRow.importance, importance);
410
+ this.db.prepare(`
411
+ UPDATE memories
412
+ SET content = ?, vector = ?, importance = ?, source_agent = ?,
413
+ updated_at = strftime('%s','now')
414
+ WHERE id = ?
415
+ `).run(text, vectorBlob, newImportance, sourceAgent, bestRow.id);
416
+
417
+ return {
418
+ action: 'UPDATE',
419
+ id: bestRow.id,
420
+ score: bestScore,
421
+ previous: bestRow.content.slice(0, 120),
422
+ reason: 'contradiction detected',
423
+ };
424
+ }
425
+
426
+ // ── DELETE — high confidence contradiction + low importance ──────────────
427
+ // When the old memory is low-importance AND the contradiction is strong,
428
+ // it's safer to delete and re-add than to update in place.
429
+ // Threshold: score >= 0.90 AND importance <= 1 AND contradiction detected.
430
+ const DELETE_THRESHOLD = opts.delete_threshold ?? 0.90;
431
+ if (
432
+ bestScore >= DELETE_THRESHOLD &&
433
+ bestRow &&
434
+ (bestRow.importance || 1) <= 1 &&
435
+ _isContradiction(text, bestRow.content)
436
+ ) {
437
+ this.db.prepare('DELETE FROM memories WHERE id = ?').run(bestRow.id);
438
+ // Remove from HNSW index
439
+ _hnswRemoveVector(this.agentId, bestRow.id);
440
+
441
+ // Re-insert as new with elevated importance (it replaced something)
442
+ const result = this.db.prepare(`
443
+ INSERT INTO memories (agent_id, content, edge_type, tags, source_agent, vector, importance)
444
+ VALUES (?, ?, ?, ?, ?, ?, ?)
445
+ `).run(this.agentId, text, edgeType, tags, sourceAgent, vectorBlob, Math.max(importance, 2));
446
+
447
+ _hnswAddVector(this.db, this.agentId, result.lastInsertRowid, vec);
448
+
449
+ return {
450
+ action: 'DELETE',
451
+ id: result.lastInsertRowid,
452
+ deleted: bestRow.id,
453
+ score: bestScore,
454
+ previous: bestRow.content.slice(0, 120),
455
+ reason: 'high-confidence contradiction — old memory removed',
456
+ };
457
+ }
458
+
459
+ // ── ADD — genuinely new ───────────────────────────────────────────────
460
+ const result = this.db.prepare(`
461
+ INSERT INTO memories (agent_id, content, edge_type, tags, source_agent, vector, importance)
462
+ VALUES (?, ?, ?, ?, ?, ?, ?)
463
+ `).run(this.agentId, text, edgeType, tags, sourceAgent, vectorBlob, importance);
464
+
465
+ return { action: 'ADD', id: result.lastInsertRowid };
131
466
  }
132
467
 
133
- /**
134
- * recall(query, topK = 5)
135
- * Semantic recall using cosine similarity over stored vectors.
136
- * Runs entirely inside SQLite — no external vector DB required.
137
- *
138
- * @param {string} query
139
- * @param {number} topK
140
- * @returns {Promise<Array<{id, content, importance, score}>>}
141
- */
468
+ // ── recall(query, topK) ────────────────────────────────────────────────────
469
+ // Semantic cosine similarity + keyword fallback
142
470
  async recall(query, topK = 5) {
143
- const queryVector = await this.embedder.embed(query);
471
+ const queryVec = await embed(query);
144
472
 
145
- // Pull all memories for this agent (mmap makes this fast)
473
+ // Pull top 500 by importance (mmap makes this fast)
146
474
  const rows = this.db.prepare(`
147
- SELECT id, content, summary, importance, vector
475
+ SELECT id, content, summary, edge_type, importance, vector
148
476
  FROM memories
149
477
  WHERE agent_id = ?
150
478
  AND vector IS NOT NULL
151
- ORDER BY importance DESC
479
+ ORDER BY importance DESC, created_at DESC
152
480
  LIMIT 500
153
481
  `).all(this.agentId);
154
482
 
155
- // Score in JS — cosine sim over Float32Arrays
483
+ if (!rows.length) {
484
+ // Nothing stored yet — try keyword fallback
485
+ return this._keywordRecall(query, topK);
486
+ }
487
+
488
+ // Score: cosine × importance
156
489
  const scored = rows
157
490
  .map(row => {
158
491
  const vec = deserializeVector(row.vector);
159
- const score = cosineSimilarity(queryVector, vec);
160
- return {
161
- id : row.id,
162
- content : row.content,
163
- summary : row.summary,
164
- importance : row.importance,
165
- score : Math.round(score * 100) / 100,
166
- };
492
+ const score = vec.length > 0 ? cosineSimilarity(queryVec, vec) : 0;
493
+ return { id: row.id, content: row.content, summary: row.summary, edge_type: row.edge_type, importance: row.importance, score: Math.round(score * 100) / 100 };
167
494
  })
168
495
  .sort((a, b) => (b.score * b.importance) - (a.score * a.importance))
169
496
  .slice(0, topK);
170
497
 
498
+ // If top scores are very low, blend in keyword results
499
+ if (scored[0]?.score < 0.15) {
500
+ const kw = this._keywordRecall(query, topK);
501
+ const seen = new Set(scored.map(r => r.id));
502
+ for (const r of kw) { if (!seen.has(r.id)) scored.push(r); }
503
+ return scored.slice(0, topK);
504
+ }
505
+
171
506
  return scored;
172
507
  }
173
508
 
174
- /**
175
- * graph(concept, opts)
176
- * Breadth-first traversal from a concept node.
177
- *
178
- * @param {string} concept
179
- * @param {{ hops?: number }} opts
180
- * @returns {Promise<{nodes: Array, edges: Array}>}
181
- */
509
+ // Keyword fallback — used when vectors are cold or similarity is weak
510
+ _keywordRecall(query, limit = 5) {
511
+ const STOP = new Set(['the','and','for','are','was','has','its','you','your','just','also','more','some','any','all','can','get','this','that','with','from','have','will','what','when','where','which','there','their','about','would','could','should']);
512
+ const words = query.toLowerCase().replace(/[^\w\s]/g,' ').split(/\s+/).filter(w => w.length > 2 && !STOP.has(w));
513
+
514
+ if (!words.length) {
515
+ // Return most recent
516
+ return this.db.prepare('SELECT id, content, edge_type, importance, 0.5 as score FROM memories WHERE agent_id=? ORDER BY created_at DESC LIMIT ?').all(this.agentId, limit);
517
+ }
518
+
519
+ // Search each keyword, merge and deduplicate
520
+ const seen = new Set();
521
+ const results = [];
522
+ for (const word of words.slice(0, 5)) {
523
+ const rows = this.db.prepare("SELECT id, content, edge_type, importance, 0.5 as score FROM memories WHERE agent_id=? AND content LIKE ? ORDER BY importance DESC LIMIT 5").all(this.agentId, '%' + word + '%');
524
+ for (const r of rows) { if (!seen.has(r.id)) { seen.add(r.id); results.push(r); } }
525
+ }
526
+ return results.slice(0, limit);
527
+ }
528
+
529
+ // ── graph(concept, opts) ──────────────────────────────────────────────────
182
530
  async graph(concept, opts = {}) {
183
- const hops = opts.hops ?? 2;
184
- const topK = opts.topK ?? 3;
185
-
186
- // Find seed nodes via recall
187
- const seeds = await this.recall(concept, topK);
188
- const visited = new Set(seeds.map(s => s.id));
189
- const nodes = [...seeds];
190
- const edges = [];
531
+ const hops = opts.hops ?? 2;
532
+ const topK = opts.topK ?? 3;
533
+ const seeds = await this.recall(concept, topK);
534
+ const visited = new Set(seeds.map(s => s.id));
535
+ const nodes = [...seeds];
536
+ const edges = [];
191
537
  let frontier = seeds.map(s => s.id);
192
538
 
193
- for (let hop = 0; hop < hops; hop++) {
194
- if (!frontier.length) break;
195
-
196
- const placeholders = frontier.map(() => '?').join(',');
539
+ for (let hop = 0; hop < hops && frontier.length; hop++) {
540
+ const ph = frontier.map(() => '?').join(',');
197
541
  const edgeRows = this.db.prepare(`
198
- SELECT source_id, target_id, edge_type, weight
199
- FROM memory_edges
200
- WHERE agent_id = ?
201
- AND (source_id IN (${placeholders}) OR target_id IN (${placeholders}))
542
+ SELECT source_id, target_id, edge_type, weight FROM memory_edges
543
+ WHERE agent_id = ? AND (source_id IN (${ph}) OR target_id IN (${ph}))
202
544
  `).all(this.agentId, ...frontier, ...frontier);
203
545
 
204
546
  const nextIds = [];
205
547
  for (const edge of edgeRows) {
206
548
  edges.push(edge);
207
- for (const nodeId of [edge.source_id, edge.target_id]) {
208
- if (!visited.has(nodeId)) {
209
- visited.add(nodeId);
210
- nextIds.push(nodeId);
211
- }
549
+ for (const nid of [edge.source_id, edge.target_id]) {
550
+ if (!visited.has(nid)) { visited.add(nid); nextIds.push(nid); }
212
551
  }
213
552
  }
214
553
 
215
554
  if (nextIds.length) {
216
- const ph = nextIds.map(() => '?').join(',');
217
- const rows = this.db.prepare(`
218
- SELECT id, content, summary, importance
219
- FROM memories
220
- WHERE agent_id = ? AND id IN (${ph})
221
- `).all(this.agentId, ...nextIds);
222
- nodes.push(...rows);
223
- frontier = nextIds;
224
- } else {
225
- break;
226
- }
555
+ const ph2 = nextIds.map(() => '?').join(',');
556
+ const rows = this.db.prepare(`SELECT id, content, summary, importance FROM memories WHERE agent_id=? AND id IN (${ph2})`).all(this.agentId, ...nextIds);
557
+ nodes.push(...rows); frontier = nextIds;
558
+ } else { break; }
227
559
  }
228
560
 
229
561
  return { nodes, edges };
230
562
  }
231
563
 
232
- /**
233
- * delta(topic, days)
234
- * Returns memories added or updated in the last N days on a topic.
235
- *
236
- * @param {string} topic
237
- * @param {number} days
238
- * @returns {Promise<Array>}
239
- */
564
+ // ── delta(topic, days) ────────────────────────────────────────────────────
240
565
  async delta(topic, days = 7) {
241
- const since = Math.floor(Date.now() / 1000) - (days * 86400);
242
-
566
+ const since = Math.floor(Date.now() / 1000) - (days * 86400);
243
567
  const recent = this.db.prepare(`
244
- SELECT id, content, summary, importance, created_at, updated_at
245
- FROM memories
246
- WHERE agent_id = ?
247
- AND updated_at >= ?
248
- ORDER BY updated_at DESC
249
- LIMIT 100
568
+ SELECT id, content, summary, importance, created_at, updated_at FROM memories
569
+ WHERE agent_id=? AND updated_at >= ? ORDER BY updated_at DESC LIMIT 100
250
570
  `).all(this.agentId, since);
251
571
 
252
- // Score against topic
253
- const queryVector = await this.embedder.embed(topic);
254
- return recent
255
- .map(row => {
256
- // Quick text match score (no vector needed for delta, but nice to have)
257
- const textScore = row.content.toLowerCase().includes(topic.toLowerCase()) ? 0.1 : 0;
258
- return { ...row, relevance: textScore };
259
- })
260
- .sort((a, b) => b.updated_at - a.updated_at);
572
+ return recent.map(row => ({
573
+ ...row,
574
+ relevance: row.content.toLowerCase().includes(topic.toLowerCase()) ? 0.8 : 0.1,
575
+ })).sort((a, b) => b.updated_at - a.updated_at);
261
576
  }
262
577
 
263
- /**
264
- * briefing()
265
- * Summary of everything learned in the last 24 hours.
266
- * Inject into system prompt at session start.
267
- *
268
- * @returns {Promise<string>}
269
- */
578
+ // ── briefing() ────────────────────────────────────────────────────────────
270
579
  async briefing() {
271
- const since = Math.floor(Date.now() / 1000) - 86400;
272
-
580
+ const since = Math.floor(Date.now() / 1000) - 86400;
273
581
  const recent = this.db.prepare(`
274
- SELECT content, importance
275
- FROM memories
276
- WHERE agent_id = ?
277
- AND created_at >= ?
278
- ORDER BY importance DESC
279
- LIMIT 20
582
+ SELECT content, importance FROM memories
583
+ WHERE agent_id=? AND created_at>=? ORDER BY importance DESC LIMIT 20
280
584
  `).all(this.agentId, since);
281
585
 
282
586
  if (!recent.length) return 'No new memories in the last 24 hours.';
283
-
284
587
  return [
285
588
  `[SLIPSTREAM BRIEFING — last 24h — ${recent.length} memories]`,
286
589
  ...recent.map((r, i) => `${i + 1}. ${r.content}`),
287
590
  ].join('\n');
288
591
  }
592
+
593
+ // ── stats() — used by server-index.js countsByType ────────────────────────
594
+ stats() {
595
+ try {
596
+ const byType = this.db.prepare("SELECT edge_type as type, COUNT(*) as n FROM memories GROUP BY edge_type").all();
597
+ const total = this.db.prepare("SELECT COUNT(*) as n FROM memories").get()?.n || 0;
598
+ const by_type = {};
599
+ for (const r of byType) by_type[r.type || 'semantic'] = r.n;
600
+ return { total, by_type };
601
+ } catch (_) { return { total: 0, by_type: {} }; }
602
+ }
603
+
604
+ // ── store() — compatibility alias for mem.store() calls ───────────────────
605
+ async store(content, opts = {}) {
606
+ return this.remember(content, opts);
607
+ }
608
+
609
+ // ── recent(opts) — returns most recent memories ───────────────────────────
610
+ recent(opts = {}) {
611
+ const limit = opts.limit || 20;
612
+ const type = opts.type || null;
613
+ try {
614
+ if (type) {
615
+ return this.db.prepare("SELECT * FROM memories WHERE agent_id=? AND edge_type=? ORDER BY created_at DESC LIMIT ?").all(this.agentId, type, limit);
616
+ }
617
+ return this.db.prepare("SELECT * FROM memories WHERE agent_id=? ORDER BY created_at DESC LIMIT ?").all(this.agentId, limit);
618
+ } catch (_) { return []; }
619
+ }
620
+
621
+ // ── remove(id) ────────────────────────────────────────────────────────────
622
+ remove(id) {
623
+ try { this.db.prepare("DELETE FROM memories WHERE id=? AND agent_id=?").run(id, this.agentId); } catch (_) {}
624
+ }
625
+ }
626
+
627
+ // ─── Boot Banner ──────────────────────────────────────────────────────────────
628
+
629
+ function printBanner(memory, bootMs, sovereign = false) {
630
+ const stats = getEmbedderStats();
631
+ const icon = stats.ep.includes('ONNX') ? '🚀' : '⚙️ ';
632
+ console.log('');
633
+ console.log(' ╔══════════════════════════════════════════════════════╗');
634
+ console.log(' ║ VEKTOR SLIPSTREAM — ACTIVE ║');
635
+ console.log(' ╚══════════════════════════════════════════════════════╝');
636
+ console.log('');
637
+ console.log(` ${icon} EP: ${stats.ep}`);
638
+ console.log(` 🧠 Model: ${stats.mode}`);
639
+ console.log(` ⚡ Embed: ${stats.avgMs}ms (post-warmup)`);
640
+ console.log(` 💾 DB: WAL | mmap:1GB | cache:64MB`);
641
+ console.log(` 🔥 Warm: ✓`);
642
+ console.log(` ⏱ Boot: ${bootMs}ms total`);
643
+ console.log('');
644
+ console.log(' [vektor] Sovereign Agent active');
645
+ console.log(' [vektor] Law I \u2713 locality \u2014 no cloud sync path');
646
+ console.log(' [vektor] Law II \u2713 hygiene \u2014 AUDN on every write');
647
+ console.log(' [vektor] Law III \u2713 synthesis \u2014 REM compress-only');
648
+ console.log(' [vektor] Law IV \u2713 permanence \u2014 SQLite persists');
649
+ if (sovereign) {
650
+ console.log(' [vektor] Law VIII sovereign: true \u2014 injection shield active');
651
+ }
652
+ console.log('');
289
653
  }
290
654
 
291
655
  // ─── createMemory() — Public API ──────────────────────────────────────────────
292
656
 
293
- /**
294
- * createMemory(options)
295
- *
296
- * Initialises the Slipstream memory engine. Parallel API to vektor-core's
297
- * createMemory() — drop-in replacement, no code changes needed in agent.
298
- *
299
- * @param {object} options
300
- * @param {string} options.agentId - Unique agent identifier
301
- * @param {string} [options.dbPath] - Path to .db file
302
- * @param {boolean} [options.silent] - Suppress boot banner
303
- * @returns {Promise<SlipstreamMemory>}
304
- */
305
657
  async function createMemory(options = {}) {
306
658
  const bootStart = Date.now();
307
659
 
308
660
  const {
309
- agentId = 'default',
310
- dbPath = './slipstream-memory.db',
311
- silent = false,
312
- licenceKey = process.env.VEKTOR_LICENCE_KEY || '',
661
+ agentId = 'default',
662
+ dbPath = path.join(process.cwd(), 'slipstream-memory.db'),
663
+ silent = false,
664
+ sovereign = false,
313
665
  } = options;
314
666
 
315
- // 0. Validate Polar licence key
316
- await validateLicence(licenceKey);
317
-
318
- // 1. Init embedder (hardware probe + ONNX load + pre-warmer)
319
- const embedder = new SlipstreamEmbedder();
320
- await embedder.init();
667
+ // Ensure parent directory exists
668
+ const dir = path.dirname(path.resolve(dbPath));
669
+ if (!fs.existsSync(dir)) {
670
+ fs.mkdirSync(dir, { recursive: true });
671
+ }
321
672
 
322
- // 2. Init DB (sqlite-vec + Hyper-PRAGMAs)
323
- const db = initSlipstreamDB(dbPath);
673
+ // Open SQLite
674
+ const db = loadSQLite(dbPath);
324
675
 
325
- // 3. Ensure schema exists
676
+ // Ensure schema
326
677
  initSchema(db);
327
678
 
679
+ // Create memory instance
680
+ const memory = new SlipstreamMemory(db, agentId);
681
+
682
+ // Law VIII — Injection Resistance (opt-in)
683
+ // Wraps memory.remember() with two-tier input screening.
684
+ // Enable with: createMemory({ sovereign: true })
685
+ // Performance cost: ~80ms per write (pattern check free, LLM screen ~80ms).
686
+ if (sovereign) {
687
+ try {
688
+ const sovereignModule = require('./sovereign');
689
+ sovereignModule.wrap(memory);
690
+ memory._sovereign = true;
691
+ } catch (e) {
692
+ console.warn('[vektor] sovereign.js not found — sovereign mode disabled. Copy sovereign.js to the SDK root.');
693
+ memory._sovereign = false;
694
+ }
695
+ }
696
+
328
697
  const bootMs = Date.now() - bootStart;
698
+ if (!silent) printBanner(memory, bootMs, sovereign);
329
699
 
330
- // 4. Print audit log banner
331
- if (!silent) {
332
- const dbStats = getDBStats(db);
333
- printBanner(embedder, dbStats, bootMs);
334
- }
700
+ // Start warming xenova in the background (non-blocking)
701
+ // First recall will be hash-based; subsequent ones will use xenova if available
702
+ embed('warmup').catch(() => {});
335
703
 
336
- return new SlipstreamMemory(db, embedder, agentId);
704
+ return memory;
337
705
  }
338
706
 
339
- module.exports = { createMemory };
707
+ module.exports = { createMemory, SlipstreamMemory, cosineSimilarity, _isContradiction };