agent-working-memory 0.7.8 → 0.7.10
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 +15 -1
- package/dist/api/routes.js +1 -1
- package/dist/api/routes.js.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/engine/activation.d.ts.map +1 -1
- package/dist/engine/activation.js +89 -61
- package/dist/engine/activation.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +2 -2
- package/dist/mcp.js.map +1 -1
- package/dist/storage/sqlite.d.ts +40 -0
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +174 -0
- package/dist/storage/sqlite.js.map +1 -1
- package/package.json +1 -1
- package/src/api/routes.ts +1 -1
- package/src/cli.ts +1 -1
- package/src/engine/activation.ts +87 -65
- package/src/index.ts +1 -1
- package/src/mcp.ts +2 -2
- package/src/storage/sqlite.ts +201 -0
package/src/storage/sqlite.ts
CHANGED
|
@@ -28,10 +28,33 @@ const DEFAULT_SALIENCE_FEATURES: SalienceFeatures = {
|
|
|
28
28
|
surprise: 0, decisionMade: false, causalDepth: 0, resolutionEffort: 0, eventType: 'observation',
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* In-memory slim entry — the minimum data the activation pipeline's pre-filter
|
|
33
|
+
* pass reads. Lives in EngramStore.slimCache to skip the SQL fetch + Buffer→
|
|
34
|
+
* Float32Array conversion on every recall. ~22 bytes overhead per entry plus
|
|
35
|
+
* the embedding (~1.5KB), so ~15MB at 10K engrams.
|
|
36
|
+
*/
|
|
37
|
+
type SlimCacheEntry = {
|
|
38
|
+
id: string;
|
|
39
|
+
agentId: string;
|
|
40
|
+
concept: string;
|
|
41
|
+
embedding: number[] | null;
|
|
42
|
+
stage: EngramStage;
|
|
43
|
+
retracted: boolean;
|
|
44
|
+
};
|
|
45
|
+
|
|
31
46
|
export class EngramStore {
|
|
32
47
|
private db: Database.Database;
|
|
33
48
|
private walTimer: ReturnType<typeof setInterval> | null = null;
|
|
34
49
|
|
|
50
|
+
// Slim cache for the activation pipeline pre-filter. Populated lazily on
|
|
51
|
+
// first call to getEngramsByAgentSlim(). Mutations to engrams keep this in
|
|
52
|
+
// sync via private cache* helpers. Disable via AWM_DISABLE_SLIM_CACHE=1
|
|
53
|
+
// (for A/B testing or if a regression appears).
|
|
54
|
+
private slimCache: Map<string, SlimCacheEntry> = new Map();
|
|
55
|
+
private slimCachePopulated: boolean = false;
|
|
56
|
+
private slimCacheEnabled: boolean = process.env.AWM_DISABLE_SLIM_CACHE !== '1';
|
|
57
|
+
|
|
35
58
|
constructor(dbPath: string = 'memory.db') {
|
|
36
59
|
this.db = new Database(dbPath);
|
|
37
60
|
this.db.pragma('journal_mode = WAL');
|
|
@@ -43,6 +66,62 @@ export class EngramStore {
|
|
|
43
66
|
this.startWalCheckpointTimer();
|
|
44
67
|
}
|
|
45
68
|
|
|
69
|
+
// --- Slim cache management ---
|
|
70
|
+
|
|
71
|
+
/** Lazy-populate the slim cache from the engrams table. Called on first slim fetch. */
|
|
72
|
+
private ensureSlimCachePopulated(): void {
|
|
73
|
+
if (this.slimCachePopulated || !this.slimCacheEnabled) return;
|
|
74
|
+
const rows = this.db.prepare(
|
|
75
|
+
'SELECT id, agent_id, concept, embedding, stage, retracted FROM engrams'
|
|
76
|
+
).all() as any[];
|
|
77
|
+
for (const r of rows) {
|
|
78
|
+
this.slimCache.set(r.id as string, {
|
|
79
|
+
id: r.id as string,
|
|
80
|
+
agentId: r.agent_id as string,
|
|
81
|
+
concept: r.concept as string,
|
|
82
|
+
embedding: r.embedding ? Array.from(bufferToFloat32Array(r.embedding)) : null,
|
|
83
|
+
stage: r.stage as EngramStage,
|
|
84
|
+
retracted: !!r.retracted,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
this.slimCachePopulated = true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Add a new engram to the slim cache. Called from createEngram. */
|
|
91
|
+
private cacheAdd(entry: SlimCacheEntry): void {
|
|
92
|
+
if (!this.slimCacheEnabled) return;
|
|
93
|
+
this.slimCache.set(entry.id, entry);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private cacheUpdateStage(id: string, stage: EngramStage): void {
|
|
97
|
+
if (!this.slimCacheEnabled) return;
|
|
98
|
+
const e = this.slimCache.get(id);
|
|
99
|
+
if (e) e.stage = stage;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private cacheUpdateEmbedding(id: string, embedding: number[]): void {
|
|
103
|
+
if (!this.slimCacheEnabled) return;
|
|
104
|
+
const e = this.slimCache.get(id);
|
|
105
|
+
if (e) e.embedding = embedding;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private cacheRetract(id: string): void {
|
|
109
|
+
if (!this.slimCacheEnabled) return;
|
|
110
|
+
const e = this.slimCache.get(id);
|
|
111
|
+
if (e) e.retracted = true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private cacheRemove(id: string): void {
|
|
115
|
+
if (!this.slimCacheEnabled) return;
|
|
116
|
+
this.slimCache.delete(id);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Reset cache (used by tests + after timeWarp/bulk operations). */
|
|
120
|
+
resetSlimCache(): void {
|
|
121
|
+
this.slimCache.clear();
|
|
122
|
+
this.slimCachePopulated = false;
|
|
123
|
+
}
|
|
124
|
+
|
|
46
125
|
/** Expose the raw database handle for the coordination module. */
|
|
47
126
|
getDb(): Database.Database {
|
|
48
127
|
return this.db;
|
|
@@ -306,6 +385,18 @@ export class EngramStore {
|
|
|
306
385
|
input.memoryType ?? 'unclassified',
|
|
307
386
|
);
|
|
308
387
|
|
|
388
|
+
// Add to slim cache (skip if not yet populated — first slim fetch will load it)
|
|
389
|
+
if (this.slimCachePopulated) {
|
|
390
|
+
this.cacheAdd({
|
|
391
|
+
id,
|
|
392
|
+
agentId: input.agentId,
|
|
393
|
+
concept: input.concept,
|
|
394
|
+
embedding: input.embedding ?? null,
|
|
395
|
+
stage: 'active',
|
|
396
|
+
retracted: false,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
|
|
309
400
|
return this.getEngram(id)!;
|
|
310
401
|
}
|
|
311
402
|
|
|
@@ -329,6 +420,112 @@ export class EngramStore {
|
|
|
329
420
|
return (this.db.prepare(query).all(...params) as any[]).map(r => this.rowToEngram(r));
|
|
330
421
|
}
|
|
331
422
|
|
|
423
|
+
/**
|
|
424
|
+
* Slim variant that returns only (id, concept, embedding) — the minimum needed
|
|
425
|
+
* for the activation pipeline's pre-filter pass (cosine sim + concept-jaccard
|
|
426
|
+
* survival check). Avoids materializing the content blob, tag JSON, salience
|
|
427
|
+
* features JSON, etc. for ~10K rows when only ~200 will be deep-scored.
|
|
428
|
+
*
|
|
429
|
+
* Why: phase-breakdown spike (2026-05-08) showed the full SELECT * over 10K
|
|
430
|
+
* engrams costs 440ms on a 17K-engram corpus — 40% of recall latency. Most
|
|
431
|
+
* of that is row materialization of fields we don't read in the filter pass.
|
|
432
|
+
*/
|
|
433
|
+
getEngramsByAgentSlim(
|
|
434
|
+
agentId: string,
|
|
435
|
+
stage?: EngramStage,
|
|
436
|
+
includeRetracted: boolean = false
|
|
437
|
+
): Array<{ id: string; concept: string; embedding: number[] | null }> {
|
|
438
|
+
if (this.slimCacheEnabled) {
|
|
439
|
+
this.ensureSlimCachePopulated();
|
|
440
|
+
const result: Array<{ id: string; concept: string; embedding: number[] | null }> = [];
|
|
441
|
+
for (const entry of this.slimCache.values()) {
|
|
442
|
+
if (entry.agentId !== agentId) continue;
|
|
443
|
+
if (stage && entry.stage !== stage) continue;
|
|
444
|
+
if (!includeRetracted && entry.retracted) continue;
|
|
445
|
+
result.push({ id: entry.id, concept: entry.concept, embedding: entry.embedding });
|
|
446
|
+
}
|
|
447
|
+
return result;
|
|
448
|
+
}
|
|
449
|
+
// Cache disabled — fall back to direct SQL
|
|
450
|
+
let query = 'SELECT id, concept, embedding FROM engrams WHERE agent_id = ?';
|
|
451
|
+
const params: any[] = [agentId];
|
|
452
|
+
|
|
453
|
+
if (stage) {
|
|
454
|
+
query += ' AND stage = ?';
|
|
455
|
+
params.push(stage);
|
|
456
|
+
}
|
|
457
|
+
if (!includeRetracted) {
|
|
458
|
+
query += ' AND retracted = 0';
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return (this.db.prepare(query).all(...params) as any[]).map(r => ({
|
|
462
|
+
id: r.id as string,
|
|
463
|
+
concept: r.concept as string,
|
|
464
|
+
embedding: r.embedding ? Array.from(bufferToFloat32Array(r.embedding)) : null,
|
|
465
|
+
}));
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** Slim variant for multi-agent (workspace-scoped) pre-filter. */
|
|
469
|
+
getEngramsByAgentsSlim(
|
|
470
|
+
agentIds: string[],
|
|
471
|
+
stage?: EngramStage,
|
|
472
|
+
includeRetracted: boolean = false
|
|
473
|
+
): Array<{ id: string; concept: string; embedding: number[] | null }> {
|
|
474
|
+
if (agentIds.length === 0) return [];
|
|
475
|
+
if (agentIds.length === 1) return this.getEngramsByAgentSlim(agentIds[0], stage, includeRetracted);
|
|
476
|
+
|
|
477
|
+
if (this.slimCacheEnabled) {
|
|
478
|
+
this.ensureSlimCachePopulated();
|
|
479
|
+
const agentSet = new Set(agentIds);
|
|
480
|
+
const result: Array<{ id: string; concept: string; embedding: number[] | null }> = [];
|
|
481
|
+
for (const entry of this.slimCache.values()) {
|
|
482
|
+
if (!agentSet.has(entry.agentId)) continue;
|
|
483
|
+
if (stage && entry.stage !== stage) continue;
|
|
484
|
+
if (!includeRetracted && entry.retracted) continue;
|
|
485
|
+
result.push({ id: entry.id, concept: entry.concept, embedding: entry.embedding });
|
|
486
|
+
}
|
|
487
|
+
return result;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const placeholders = agentIds.map(() => '?').join(',');
|
|
491
|
+
let query = `SELECT id, concept, embedding FROM engrams WHERE agent_id IN (${placeholders})`;
|
|
492
|
+
const params: any[] = [...agentIds];
|
|
493
|
+
|
|
494
|
+
if (stage) {
|
|
495
|
+
query += ' AND stage = ?';
|
|
496
|
+
params.push(stage);
|
|
497
|
+
}
|
|
498
|
+
if (!includeRetracted) {
|
|
499
|
+
query += ' AND retracted = 0';
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return (this.db.prepare(query).all(...params) as any[]).map(r => ({
|
|
503
|
+
id: r.id as string,
|
|
504
|
+
concept: r.concept as string,
|
|
505
|
+
embedding: r.embedding ? Array.from(bufferToFloat32Array(r.embedding)) : null,
|
|
506
|
+
}));
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Fetch full Engram rows for a list of IDs. Used after the pre-filter to hydrate
|
|
511
|
+
* only the survivors that need deep scoring. Chunks IN-clause queries to stay
|
|
512
|
+
* under SQLITE_LIMIT_VARIABLE_NUMBER (default 999).
|
|
513
|
+
*/
|
|
514
|
+
getEngramsByIds(ids: string[]): Engram[] {
|
|
515
|
+
if (ids.length === 0) return [];
|
|
516
|
+
const CHUNK = 800;
|
|
517
|
+
const result: Engram[] = [];
|
|
518
|
+
for (let i = 0; i < ids.length; i += CHUNK) {
|
|
519
|
+
const chunk = ids.slice(i, i + CHUNK);
|
|
520
|
+
const placeholders = chunk.map(() => '?').join(',');
|
|
521
|
+
const rows = this.db.prepare(
|
|
522
|
+
`SELECT * FROM engrams WHERE id IN (${placeholders})`
|
|
523
|
+
).all(...chunk) as any[];
|
|
524
|
+
for (const r of rows) result.push(this.rowToEngram(r));
|
|
525
|
+
}
|
|
526
|
+
return result;
|
|
527
|
+
}
|
|
528
|
+
|
|
332
529
|
/**
|
|
333
530
|
* Get engrams across multiple agents (workspace-scoped recall).
|
|
334
531
|
* Used when workspace mode is enabled for hive memory sharing.
|
|
@@ -432,6 +629,7 @@ export class EngramStore {
|
|
|
432
629
|
|
|
433
630
|
updateStage(id: string, stage: EngramStage): void {
|
|
434
631
|
this.db.prepare('UPDATE engrams SET stage = ? WHERE id = ?').run(stage, id);
|
|
632
|
+
this.cacheUpdateStage(id, stage);
|
|
435
633
|
}
|
|
436
634
|
|
|
437
635
|
updateConfidence(id: string, confidence: number): void {
|
|
@@ -447,16 +645,19 @@ export class EngramStore {
|
|
|
447
645
|
} else {
|
|
448
646
|
this.db.prepare('UPDATE engrams SET embedding = ? WHERE id = ?').run(blob, id);
|
|
449
647
|
}
|
|
648
|
+
this.cacheUpdateEmbedding(id, embedding);
|
|
450
649
|
}
|
|
451
650
|
|
|
452
651
|
retractEngram(id: string, retractedBy: string | null): void {
|
|
453
652
|
this.db.prepare(`
|
|
454
653
|
UPDATE engrams SET retracted = 1, retracted_by = ?, retracted_at = ? WHERE id = ?
|
|
455
654
|
`).run(retractedBy, new Date().toISOString(), id);
|
|
655
|
+
this.cacheRetract(id);
|
|
456
656
|
}
|
|
457
657
|
|
|
458
658
|
deleteEngram(id: string): void {
|
|
459
659
|
this.db.prepare('DELETE FROM engrams WHERE id = ?').run(id);
|
|
660
|
+
this.cacheRemove(id);
|
|
460
661
|
}
|
|
461
662
|
|
|
462
663
|
/**
|