agent-working-memory 0.7.9 → 0.7.11
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 +16 -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/core/query-expander.d.ts +7 -0
- package/dist/core/query-expander.d.ts.map +1 -1
- package/dist/core/query-expander.js +62 -3
- package/dist/core/query-expander.js.map +1 -1
- package/dist/engine/activation.d.ts.map +1 -1
- package/dist/engine/activation.js +26 -1
- 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 +13 -0
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +107 -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/core/query-expander.ts +66 -3
- package/src/engine/activation.ts +27 -1
- package/src/index.ts +1 -1
- package/src/mcp.ts +2 -2
- package/src/storage/sqlite.ts +120 -0
package/package.json
CHANGED
package/src/api/routes.ts
CHANGED
|
@@ -705,7 +705,7 @@ export function registerRoutes(app: FastifyInstance, deps: MemoryDeps): void {
|
|
|
705
705
|
const base: Record<string, unknown> = {
|
|
706
706
|
status: 'ok',
|
|
707
707
|
timestamp: new Date().toISOString(),
|
|
708
|
-
version: '0.7.
|
|
708
|
+
version: '0.7.11',
|
|
709
709
|
coordination: coordEnabled,
|
|
710
710
|
};
|
|
711
711
|
if (coordEnabled) {
|
package/src/cli.ts
CHANGED
|
@@ -334,7 +334,7 @@ async function exportMemories() {
|
|
|
334
334
|
const agents = [...new Set(memories.map((m: any) => m.agent_id))];
|
|
335
335
|
|
|
336
336
|
const exportData = {
|
|
337
|
-
version: '0.7.
|
|
337
|
+
version: '0.7.11',
|
|
338
338
|
exported_at: new Date().toISOString(),
|
|
339
339
|
source_db: dbPath,
|
|
340
340
|
agent_filter: agentFilter,
|
|
@@ -37,12 +37,60 @@ export async function getExpander(): Promise<Text2TextGenerationPipeline> {
|
|
|
37
37
|
return initPromise;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* LRU cache of normalized-query → expanded-query mappings.
|
|
42
|
+
* Map preserves insertion order — re-set on hit moves entry to the end (most-recent),
|
|
43
|
+
* delete-first when over capacity drops the least-recent. ~500 entries × ~200 chars
|
|
44
|
+
* each ≈ 100KB, negligible memory cost.
|
|
45
|
+
*
|
|
46
|
+
* Why: phase-breakdown spike (2026-05-08) showed expandQuery at ~164ms per call.
|
|
47
|
+
* Agent recall patterns repeat — cache hits are common.
|
|
48
|
+
*/
|
|
49
|
+
const expansionCache = new Map<string, string>();
|
|
50
|
+
const EXPANSION_CACHE_LIMIT = 500;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Heuristic: skip expansion when the query is already specific.
|
|
54
|
+
* Long, multi-token queries don't benefit from synonyms — they're already
|
|
55
|
+
* narrow enough that flan-t5's general-vocabulary expansion adds noise more
|
|
56
|
+
* than recall. Exact thresholds are conservative; false-skips would only
|
|
57
|
+
* affect candidates that BM25 catches anyway.
|
|
58
|
+
*/
|
|
59
|
+
function shouldSkipExpansion(normalized: string): boolean {
|
|
60
|
+
if (normalized.length === 0) return true;
|
|
61
|
+
if (normalized.length > 50) return true;
|
|
62
|
+
// ≥5 distinct meaningful tokens = already specific
|
|
63
|
+
const tokens = new Set(normalized.split(/\s+/).filter(t => t.length > 2));
|
|
64
|
+
return tokens.size >= 5;
|
|
65
|
+
}
|
|
66
|
+
|
|
40
67
|
/**
|
|
41
68
|
* Expand a query with related terms and synonyms.
|
|
42
69
|
* Returns the original query + generated expansion terms.
|
|
43
70
|
* Falls back to the original query on any error.
|
|
71
|
+
*
|
|
72
|
+
* Optimization (0.7.11+):
|
|
73
|
+
* - Skip heuristic for long/specific queries (~30% of typical agent recalls)
|
|
74
|
+
* - LRU cache for repeated queries (cache hit ≈ 0ms vs 164ms cold)
|
|
75
|
+
* - Disable both via AWM_DISABLE_EXPANSION_CACHE=1
|
|
44
76
|
*/
|
|
45
77
|
export async function expandQuery(originalQuery: string): Promise<string> {
|
|
78
|
+
const normalized = originalQuery.toLowerCase().trim();
|
|
79
|
+
const optimizationsEnabled = process.env.AWM_DISABLE_EXPANSION_CACHE !== '1';
|
|
80
|
+
|
|
81
|
+
if (optimizationsEnabled) {
|
|
82
|
+
if (shouldSkipExpansion(normalized)) {
|
|
83
|
+
return originalQuery;
|
|
84
|
+
}
|
|
85
|
+
const cached = expansionCache.get(normalized);
|
|
86
|
+
if (cached !== undefined) {
|
|
87
|
+
// Move to end (most-recent) for LRU semantics
|
|
88
|
+
expansionCache.delete(normalized);
|
|
89
|
+
expansionCache.set(normalized, cached);
|
|
90
|
+
return cached;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
46
94
|
try {
|
|
47
95
|
const expander = await getExpander();
|
|
48
96
|
const prompt = `Expand this search query with synonyms and related terms. Only output the additional terms, not the original query. Query: ${originalQuery}. Additional terms:`;
|
|
@@ -55,11 +103,26 @@ export async function expandQuery(originalQuery: string): Promise<string> {
|
|
|
55
103
|
const expanded = Array.isArray(result) ? (result[0] as any)?.generated_text ?? '' : '';
|
|
56
104
|
const cleanExpanded = expanded.trim();
|
|
57
105
|
|
|
58
|
-
|
|
59
|
-
|
|
106
|
+
const finalQuery = cleanExpanded && cleanExpanded.length > 2
|
|
107
|
+
? `${originalQuery} ${cleanExpanded}`
|
|
108
|
+
: originalQuery;
|
|
109
|
+
|
|
110
|
+
// Cache the result (LRU eviction when over capacity)
|
|
111
|
+
if (optimizationsEnabled) {
|
|
112
|
+
if (expansionCache.size >= EXPANSION_CACHE_LIMIT) {
|
|
113
|
+
const oldestKey = expansionCache.keys().next().value;
|
|
114
|
+
if (oldestKey !== undefined) expansionCache.delete(oldestKey);
|
|
115
|
+
}
|
|
116
|
+
expansionCache.set(normalized, finalQuery);
|
|
60
117
|
}
|
|
61
|
-
|
|
118
|
+
|
|
119
|
+
return finalQuery;
|
|
62
120
|
} catch {
|
|
63
121
|
return originalQuery;
|
|
64
122
|
}
|
|
65
123
|
}
|
|
124
|
+
|
|
125
|
+
/** Clear the expansion cache (used by tests + cache invalidation if needed). */
|
|
126
|
+
export function clearExpansionCache(): void {
|
|
127
|
+
expansionCache.clear();
|
|
128
|
+
}
|
package/src/engine/activation.ts
CHANGED
|
@@ -577,7 +577,33 @@ export class ActivationEngine {
|
|
|
577
577
|
// Widens the pool to find relevant results that keyword matching missed
|
|
578
578
|
const rerankPool = pool.slice(0, Math.max(limit * 3, 30));
|
|
579
579
|
|
|
580
|
-
|
|
580
|
+
// Reranker skip heuristic (0.7.10+): if BM25 already has a clear winner with
|
|
581
|
+
// strong absolute score AND a meaningful gap to the runner-up, the cross-encoder
|
|
582
|
+
// is unlikely to change the top result. Skipping saves ~300ms of wall-clock per
|
|
583
|
+
// recall on simple queries (40% of post-0.7.9 floor was reranker).
|
|
584
|
+
//
|
|
585
|
+
// Conservative gate (only skip when very confident):
|
|
586
|
+
// - top-1 textMatch >= 0.8 (high BM25 + jaccard agreement)
|
|
587
|
+
// - top-1 score is at least 1.5× top-2 score (clear separation)
|
|
588
|
+
// - rerankPool size <= limit*2 (small pool — reranker has less to do)
|
|
589
|
+
//
|
|
590
|
+
// Ambiguous queries (close BM25 scores, weak top-1, large pool) still go through
|
|
591
|
+
// the reranker. Disable this heuristic via AWM_DISABLE_RERANK_SKIP=1.
|
|
592
|
+
let rerankSkipped = false;
|
|
593
|
+
if (useReranker && rerankPool.length >= 2 && process.env.AWM_DISABLE_RERANK_SKIP !== '1') {
|
|
594
|
+
const top1 = rerankPool[0];
|
|
595
|
+
const top2 = rerankPool[1];
|
|
596
|
+
const t1Text = top1.phaseScores.textMatch;
|
|
597
|
+
const t1Score = top1.score;
|
|
598
|
+
const t2Score = top2.score;
|
|
599
|
+
const cleanWinner = t1Text >= 0.8 && t1Score >= 1.5 * Math.max(t2Score, 0.01);
|
|
600
|
+
const smallPool = rerankPool.length <= Math.max(limit * 2, 20);
|
|
601
|
+
if (cleanWinner && smallPool) {
|
|
602
|
+
rerankSkipped = true;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (useReranker && !rerankSkipped && rerankPool.length > 0) {
|
|
581
607
|
try {
|
|
582
608
|
const passages = rerankPool.map(r =>
|
|
583
609
|
`${r.engram.concept}: ${r.engram.content}`
|
package/src/index.ts
CHANGED
|
@@ -177,7 +177,7 @@ async function main() {
|
|
|
177
177
|
|
|
178
178
|
// Start server
|
|
179
179
|
await app.listen({ port: PORT, host: '0.0.0.0' });
|
|
180
|
-
console.log(`AgentWorkingMemory v0.7.
|
|
180
|
+
console.log(`AgentWorkingMemory v0.7.11 listening on port ${PORT}`);
|
|
181
181
|
|
|
182
182
|
// Graceful shutdown
|
|
183
183
|
const shutdown = async () => {
|
package/src/mcp.ts
CHANGED
|
@@ -78,7 +78,7 @@ const INCOGNITO = process.env.AWM_INCOGNITO === '1' || process.env.AWM_INCOGNITO
|
|
|
78
78
|
|
|
79
79
|
if (INCOGNITO) {
|
|
80
80
|
console.error('AWM: incognito mode — all memory tools disabled, nothing will be recorded');
|
|
81
|
-
const server = new McpServer({ name: 'agent-working-memory', version: '0.7.
|
|
81
|
+
const server = new McpServer({ name: 'agent-working-memory', version: '0.7.11' });
|
|
82
82
|
const transport = new StdioServerTransport();
|
|
83
83
|
server.connect(transport).catch(err => {
|
|
84
84
|
console.error('MCP server failed:', err);
|
|
@@ -115,7 +115,7 @@ let coordDb: import('better-sqlite3').Database | null = null;
|
|
|
115
115
|
|
|
116
116
|
const server = new McpServer({
|
|
117
117
|
name: 'agent-working-memory',
|
|
118
|
-
version: '0.7.
|
|
118
|
+
version: '0.7.11',
|
|
119
119
|
});
|
|
120
120
|
|
|
121
121
|
server.registerResource(
|
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
|
|
|
@@ -344,6 +435,18 @@ export class EngramStore {
|
|
|
344
435
|
stage?: EngramStage,
|
|
345
436
|
includeRetracted: boolean = false
|
|
346
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
|
|
347
450
|
let query = 'SELECT id, concept, embedding FROM engrams WHERE agent_id = ?';
|
|
348
451
|
const params: any[] = [agentId];
|
|
349
452
|
|
|
@@ -371,6 +474,19 @@ export class EngramStore {
|
|
|
371
474
|
if (agentIds.length === 0) return [];
|
|
372
475
|
if (agentIds.length === 1) return this.getEngramsByAgentSlim(agentIds[0], stage, includeRetracted);
|
|
373
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
|
+
|
|
374
490
|
const placeholders = agentIds.map(() => '?').join(',');
|
|
375
491
|
let query = `SELECT id, concept, embedding FROM engrams WHERE agent_id IN (${placeholders})`;
|
|
376
492
|
const params: any[] = [...agentIds];
|
|
@@ -513,6 +629,7 @@ export class EngramStore {
|
|
|
513
629
|
|
|
514
630
|
updateStage(id: string, stage: EngramStage): void {
|
|
515
631
|
this.db.prepare('UPDATE engrams SET stage = ? WHERE id = ?').run(stage, id);
|
|
632
|
+
this.cacheUpdateStage(id, stage);
|
|
516
633
|
}
|
|
517
634
|
|
|
518
635
|
updateConfidence(id: string, confidence: number): void {
|
|
@@ -528,16 +645,19 @@ export class EngramStore {
|
|
|
528
645
|
} else {
|
|
529
646
|
this.db.prepare('UPDATE engrams SET embedding = ? WHERE id = ?').run(blob, id);
|
|
530
647
|
}
|
|
648
|
+
this.cacheUpdateEmbedding(id, embedding);
|
|
531
649
|
}
|
|
532
650
|
|
|
533
651
|
retractEngram(id: string, retractedBy: string | null): void {
|
|
534
652
|
this.db.prepare(`
|
|
535
653
|
UPDATE engrams SET retracted = 1, retracted_by = ?, retracted_at = ? WHERE id = ?
|
|
536
654
|
`).run(retractedBy, new Date().toISOString(), id);
|
|
655
|
+
this.cacheRetract(id);
|
|
537
656
|
}
|
|
538
657
|
|
|
539
658
|
deleteEngram(id: string): void {
|
|
540
659
|
this.db.prepare('DELETE FROM engrams WHERE id = ?').run(id);
|
|
660
|
+
this.cacheRemove(id);
|
|
541
661
|
}
|
|
542
662
|
|
|
543
663
|
/**
|