agent-working-memory 0.6.0 → 0.6.1
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 -9
- package/dist/adapters/claude-code.d.ts +4 -0
- package/dist/adapters/claude-code.d.ts.map +1 -0
- package/dist/adapters/claude-code.js +218 -0
- package/dist/adapters/claude-code.js.map +1 -0
- package/dist/adapters/codex.d.ts +4 -0
- package/dist/adapters/codex.d.ts.map +1 -0
- package/dist/adapters/codex.js +226 -0
- package/dist/adapters/codex.js.map +1 -0
- package/dist/adapters/common.d.ts +34 -0
- package/dist/adapters/common.d.ts.map +1 -0
- package/dist/adapters/common.js +145 -0
- package/dist/adapters/common.js.map +1 -0
- package/dist/adapters/cursor.d.ts +4 -0
- package/dist/adapters/cursor.d.ts.map +1 -0
- package/dist/adapters/cursor.js +138 -0
- package/dist/adapters/cursor.js.map +1 -0
- package/dist/adapters/http.d.ts +4 -0
- package/dist/adapters/http.d.ts.map +1 -0
- package/dist/adapters/http.js +88 -0
- package/dist/adapters/http.js.map +1 -0
- package/dist/adapters/index.d.ts +7 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +21 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/types.d.ts +65 -0
- package/dist/adapters/types.d.ts.map +1 -0
- package/dist/adapters/types.js +4 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/cli.js +104 -230
- package/dist/cli.js.map +1 -1
- package/dist/coordination/events.d.ts +59 -0
- package/dist/coordination/events.d.ts.map +1 -0
- package/dist/coordination/events.js +28 -0
- package/dist/coordination/events.js.map +1 -0
- package/dist/coordination/index.d.ts +10 -1
- package/dist/coordination/index.d.ts.map +1 -1
- package/dist/coordination/index.js +87 -3
- package/dist/coordination/index.js.map +1 -1
- package/dist/coordination/peer-decisions.d.ts +40 -0
- package/dist/coordination/peer-decisions.d.ts.map +1 -0
- package/dist/coordination/peer-decisions.js +82 -0
- package/dist/coordination/peer-decisions.js.map +1 -0
- package/dist/coordination/plugin-loader.d.ts +18 -0
- package/dist/coordination/plugin-loader.d.ts.map +1 -0
- package/dist/coordination/plugin-loader.js +55 -0
- package/dist/coordination/plugin-loader.js.map +1 -0
- package/dist/coordination/plugin.d.ts +40 -0
- package/dist/coordination/plugin.d.ts.map +1 -0
- package/dist/coordination/plugin.js +22 -0
- package/dist/coordination/plugin.js.map +1 -0
- package/dist/coordination/routes.d.ts +2 -1
- package/dist/coordination/routes.d.ts.map +1 -1
- package/dist/coordination/routes.js +899 -76
- package/dist/coordination/routes.js.map +1 -1
- package/dist/coordination/schema.d.ts.map +1 -1
- package/dist/coordination/schema.js +72 -14
- package/dist/coordination/schema.js.map +1 -1
- package/dist/coordination/schemas.d.ts +84 -3
- package/dist/coordination/schemas.d.ts.map +1 -1
- package/dist/coordination/schemas.js +71 -1
- package/dist/coordination/schemas.js.map +1 -1
- package/dist/coordination/stale.d.ts.map +1 -1
- package/dist/coordination/stale.js +2 -1
- package/dist/coordination/stale.js.map +1 -1
- package/dist/coordination/types.d.ts +252 -0
- package/dist/coordination/types.d.ts.map +1 -0
- package/dist/coordination/types.js +8 -0
- package/dist/coordination/types.js.map +1 -0
- package/dist/coordination/write-mutex.d.ts +26 -0
- package/dist/coordination/write-mutex.d.ts.map +1 -0
- package/dist/coordination/write-mutex.js +63 -0
- package/dist/coordination/write-mutex.js.map +1 -0
- package/dist/core/embeddings.d.ts +2 -0
- package/dist/core/embeddings.d.ts.map +1 -1
- package/dist/core/embeddings.js +4 -0
- package/dist/core/embeddings.js.map +1 -1
- package/dist/engine/activation.d.ts.map +1 -1
- package/dist/engine/activation.js +16 -3
- package/dist/engine/activation.js.map +1 -1
- package/dist/engine/consolidation.d.ts.map +1 -1
- package/dist/engine/consolidation.js +15 -6
- package/dist/engine/consolidation.js.map +1 -1
- package/dist/engine/retraction.d.ts +3 -1
- package/dist/engine/retraction.d.ts.map +1 -1
- package/dist/engine/retraction.js +19 -6
- package/dist/engine/retraction.js.map +1 -1
- package/dist/index.js +6 -18
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +52 -3
- package/dist/mcp.js.map +1 -1
- package/dist/storage/sqlite.d.ts +6 -1
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +39 -3
- package/dist/storage/sqlite.js.map +1 -1
- package/package.json +1 -1
- package/src/adapters/claude-code.ts +234 -0
- package/src/adapters/codex.ts +262 -0
- package/src/adapters/common.ts +172 -0
- package/src/adapters/cursor.ts +150 -0
- package/src/adapters/http.ts +100 -0
- package/src/adapters/index.ts +31 -0
- package/src/adapters/types.ts +75 -0
- package/src/cli.ts +107 -238
- package/src/coordination/events.ts +90 -0
- package/src/coordination/index.ts +102 -3
- package/src/coordination/peer-decisions.ts +105 -0
- package/src/coordination/plugin-loader.ts +60 -0
- package/src/coordination/plugin.ts +44 -0
- package/src/coordination/routes.ts +1176 -105
- package/src/coordination/schema.ts +67 -14
- package/src/coordination/schemas.ts +85 -1
- package/src/coordination/stale.ts +3 -2
- package/src/coordination/types.ts +311 -0
- package/src/coordination/write-mutex.ts +69 -0
- package/src/core/embeddings.ts +5 -0
- package/src/engine/activation.ts +13 -3
- package/src/engine/consolidation.ts +15 -6
- package/src/engine/retraction.ts +22 -6
- package/src/index.ts +6 -15
- package/src/mcp.ts +73 -9
- package/src/storage/sqlite.ts +39 -3
package/src/engine/activation.ts
CHANGED
|
@@ -203,10 +203,16 @@ export class ActivationEngine {
|
|
|
203
203
|
// Phase 0: Query expansion — add related terms to improve BM25 recall
|
|
204
204
|
let searchContext = queryContext;
|
|
205
205
|
if (useExpansion) {
|
|
206
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
206
207
|
try {
|
|
207
|
-
searchContext = await
|
|
208
|
+
searchContext = await Promise.race([
|
|
209
|
+
expandQuery(query.context),
|
|
210
|
+
new Promise<string>((_, reject) => { timer = setTimeout(() => reject(new Error('expansion timeout')), 5000); }),
|
|
211
|
+
]);
|
|
208
212
|
} catch {
|
|
209
|
-
// Expansion unavailable — use original query
|
|
213
|
+
// Expansion unavailable or timed out — use original query
|
|
214
|
+
} finally {
|
|
215
|
+
if (timer) clearTimeout(timer);
|
|
210
216
|
}
|
|
211
217
|
}
|
|
212
218
|
|
|
@@ -522,7 +528,11 @@ export class ActivationEngine {
|
|
|
522
528
|
const passages = rerankPool.map(r =>
|
|
523
529
|
`${r.engram.concept}: ${r.engram.content}`
|
|
524
530
|
);
|
|
525
|
-
|
|
531
|
+
let rerankTimer: ReturnType<typeof setTimeout> | undefined;
|
|
532
|
+
const rerankResults = await Promise.race([
|
|
533
|
+
rerank(query.context, passages),
|
|
534
|
+
new Promise<never>((_, reject) => { rerankTimer = setTimeout(() => reject(new Error('reranker timeout')), 10000); }),
|
|
535
|
+
]).finally(() => { if (rerankTimer) clearTimeout(rerankTimer); });
|
|
526
536
|
|
|
527
537
|
// Adaptive reranker blend (Codex recommendation):
|
|
528
538
|
// When BM25/text signals are strong, trust them more; when weak, lean on reranker.
|
|
@@ -144,13 +144,22 @@ export class ConsolidationEngine {
|
|
|
144
144
|
const needsEmbedding = allActive.filter(e => !e.embedding || e.embedding.length === 0);
|
|
145
145
|
if (needsEmbedding.length > 0) {
|
|
146
146
|
try {
|
|
147
|
-
const {
|
|
148
|
-
|
|
147
|
+
const { embedBatch, getModelId } = await import('../core/embeddings.js');
|
|
148
|
+
const BATCH_SIZE = 32;
|
|
149
|
+
const modelId = getModelId?.() ?? 'unknown';
|
|
150
|
+
for (let b = 0; b < needsEmbedding.length; b += BATCH_SIZE) {
|
|
151
|
+
const batch = needsEmbedding.slice(b, b + BATCH_SIZE);
|
|
149
152
|
try {
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
153
|
+
const texts = batch.map(e => `${e.concept} ${e.content}`);
|
|
154
|
+
const vecs = await embedBatch(texts);
|
|
155
|
+
for (let j = 0; j < batch.length; j++) {
|
|
156
|
+
this.store.updateEmbedding(batch[j].id, vecs[j], modelId);
|
|
157
|
+
batch[j].embedding = vecs[j];
|
|
158
|
+
}
|
|
159
|
+
} catch { /* batch failed, skip — non-fatal */ }
|
|
160
|
+
}
|
|
161
|
+
if (needsEmbedding.length > 0) {
|
|
162
|
+
console.log(`[consolidation] Backfilled ${needsEmbedding.filter(e => e.embedding?.length).length}/${needsEmbedding.length} embeddings (model: ${modelId})`);
|
|
154
163
|
}
|
|
155
164
|
} catch { /* embeddings module unavailable */ }
|
|
156
165
|
}
|
package/src/engine/retraction.ts
CHANGED
|
@@ -60,40 +60,56 @@ export class RetractionEngine {
|
|
|
60
60
|
this.store.retractEngram(target.id, correction.id);
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
// Reduce confidence of
|
|
64
|
-
|
|
63
|
+
// Reduce confidence of associated engrams (contamination spread)
|
|
64
|
+
// Depth 2 with 50% decay per hop, capped at 20 total affected nodes
|
|
65
|
+
const associatesAffected = this.propagateConfidenceReduction(target.id, 0.1, 2);
|
|
65
66
|
|
|
66
67
|
return { retractedId: target.id, correctionId, associatesAffected };
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
/**
|
|
70
71
|
* Reduce confidence of engrams associated with a retracted engram.
|
|
71
|
-
*
|
|
72
|
+
* Propagates up to maxDepth hops with decaying penalty (50% per hop).
|
|
73
|
+
* Capped at MAX_AFFECTED to prevent cascading through the graph.
|
|
72
74
|
*/
|
|
75
|
+
private static readonly MAX_AFFECTED = 20;
|
|
76
|
+
|
|
73
77
|
private propagateConfidenceReduction(
|
|
74
78
|
engramId: string,
|
|
75
79
|
penalty: number,
|
|
76
80
|
maxDepth: number,
|
|
77
|
-
currentDepth: number = 0
|
|
81
|
+
currentDepth: number = 0,
|
|
82
|
+
visited: Set<string> = new Set(),
|
|
78
83
|
): number {
|
|
79
84
|
if (currentDepth >= maxDepth) return 0;
|
|
85
|
+
if (visited.size >= RetractionEngine.MAX_AFFECTED) return 0;
|
|
86
|
+
visited.add(engramId);
|
|
80
87
|
|
|
81
88
|
let affected = 0;
|
|
82
89
|
const associations = this.store.getAssociationsFor(engramId);
|
|
83
90
|
for (const assoc of associations) {
|
|
84
91
|
if (assoc.type === 'invalidation') continue; // Don't penalize corrections
|
|
92
|
+
if (visited.size >= RetractionEngine.MAX_AFFECTED) break;
|
|
85
93
|
|
|
86
94
|
const neighborId = assoc.fromEngramId === engramId
|
|
87
95
|
? assoc.toEngramId
|
|
88
96
|
: assoc.fromEngramId;
|
|
97
|
+
if (visited.has(neighborId)) continue;
|
|
98
|
+
|
|
89
99
|
const neighbor = this.store.getEngram(neighborId);
|
|
90
100
|
if (!neighbor || neighbor.retracted) continue;
|
|
91
101
|
|
|
92
|
-
// Scale penalty by association weight
|
|
93
|
-
const
|
|
102
|
+
// Scale penalty by association weight and decay per hop (50% per depth level)
|
|
103
|
+
const depthDecay = Math.pow(0.5, currentDepth);
|
|
104
|
+
const scaledPenalty = penalty * assoc.weight * depthDecay;
|
|
94
105
|
const newConfidence = Math.max(0.1, neighbor.confidence - scaledPenalty);
|
|
95
106
|
this.store.updateConfidence(neighborId, newConfidence);
|
|
96
107
|
affected++;
|
|
108
|
+
|
|
109
|
+
// Recurse to next depth
|
|
110
|
+
affected += this.propagateConfidenceReduction(
|
|
111
|
+
neighborId, penalty, maxDepth, currentDepth + 1, visited
|
|
112
|
+
);
|
|
97
113
|
}
|
|
98
114
|
return affected;
|
|
99
115
|
}
|
package/src/index.ts
CHANGED
|
@@ -101,7 +101,8 @@ async function main() {
|
|
|
101
101
|
const consolidationScheduler = new ConsolidationScheduler(store, consolidationEngine);
|
|
102
102
|
|
|
103
103
|
// API — disable Fastify's default request logging (too noisy for hive polling)
|
|
104
|
-
|
|
104
|
+
// bodyLimit: 512KB to prevent Content-Length mismatch errors with large task payloads
|
|
105
|
+
const app = Fastify({ logger: false, bodyLimit: 512_000 });
|
|
105
106
|
|
|
106
107
|
// Bearer token auth — only enforced when AWM_API_KEY is explicitly set and non-empty
|
|
107
108
|
if (API_KEY && API_KEY !== 'NONE' && API_KEY.length > 1) {
|
|
@@ -122,19 +123,9 @@ async function main() {
|
|
|
122
123
|
});
|
|
123
124
|
|
|
124
125
|
// Coordination module (opt-in via AWM_COORDINATION=true)
|
|
125
|
-
|
|
126
|
-
const { isCoordinationEnabled, initCoordination } = await import('./coordination/index.js');
|
|
126
|
+
const { isCoordinationEnabled, initCoordination, stopCoordinationCleanup } = await import('./coordination/index.js');
|
|
127
127
|
if (isCoordinationEnabled()) {
|
|
128
|
-
initCoordination(app, store.getDb());
|
|
129
|
-
// Prune stale heartbeat events every 30s (keeps assignment/command events permanently)
|
|
130
|
-
// Purge dead agents older than 24h every 30s to prevent table bloat
|
|
131
|
-
const { pruneOldHeartbeats, purgeDeadAgents } = await import('./coordination/stale.js');
|
|
132
|
-
heartbeatPruneTimer = setInterval(() => {
|
|
133
|
-
const pruned = pruneOldHeartbeats(store.getDb());
|
|
134
|
-
if (pruned > 0) console.log(`[coordination] pruned ${pruned} old heartbeat event(s)`);
|
|
135
|
-
const purged = purgeDeadAgents(store.getDb());
|
|
136
|
-
if (purged > 0) console.log(`[coordination] purged ${purged} dead agent(s) older than 24h`);
|
|
137
|
-
}, 30_000);
|
|
128
|
+
initCoordination(app, store.getDb(), store);
|
|
138
129
|
} else {
|
|
139
130
|
console.log(' Coordination module disabled (set AWM_COORDINATION=true to enable)');
|
|
140
131
|
}
|
|
@@ -189,9 +180,9 @@ async function main() {
|
|
|
189
180
|
console.log(`AgentWorkingMemory v0.6.0 listening on port ${PORT}`);
|
|
190
181
|
|
|
191
182
|
// Graceful shutdown
|
|
192
|
-
const shutdown = () => {
|
|
183
|
+
const shutdown = async () => {
|
|
193
184
|
clearInterval(backupTimer);
|
|
194
|
-
|
|
185
|
+
await stopCoordinationCleanup();
|
|
195
186
|
consolidationScheduler.stop();
|
|
196
187
|
stagingBuffer.stop();
|
|
197
188
|
try { store.walCheckpoint(); } catch { /* non-fatal */ }
|
package/src/mcp.ts
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
import { readFileSync } from 'node:fs';
|
|
28
28
|
import { resolve } from 'node:path';
|
|
29
|
-
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
29
|
+
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
30
30
|
|
|
31
31
|
// Load .env file if present (no external dependency)
|
|
32
32
|
try {
|
|
@@ -42,6 +42,12 @@ try {
|
|
|
42
42
|
if (!process.env[key]) process.env[key] = val;
|
|
43
43
|
}
|
|
44
44
|
} catch { /* No .env file */ }
|
|
45
|
+
|
|
46
|
+
// MCP uses stdout for JSON-RPC. Redirect console.log to stderr so engine
|
|
47
|
+
// startup messages (ConsolidationScheduler, model loading, etc.) don't
|
|
48
|
+
// corrupt the transport. This MUST happen before any engine imports.
|
|
49
|
+
console.log = console.error;
|
|
50
|
+
|
|
45
51
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
46
52
|
import { z } from 'zod';
|
|
47
53
|
|
|
@@ -62,6 +68,7 @@ import { DEFAULT_AGENT_CONFIG } from './types/agent.js';
|
|
|
62
68
|
import { embed } from './core/embeddings.js';
|
|
63
69
|
import { startSidecar } from './hooks/sidecar.js';
|
|
64
70
|
import { initLogger, log, getLogPath } from './core/logger.js';
|
|
71
|
+
import { queryPeerDecisions, formatPeerDecisions } from './coordination/peer-decisions.js';
|
|
65
72
|
|
|
66
73
|
// --- Incognito Mode ---
|
|
67
74
|
// When AWM_INCOGNITO=1, register zero tools. Claude won't see memory tools at all.
|
|
@@ -106,12 +113,64 @@ consolidationScheduler.start();
|
|
|
106
113
|
// Coordination DB handle — set when AWM_COORDINATION=true, used by memory_write for decision propagation
|
|
107
114
|
let coordDb: import('better-sqlite3').Database | null = null;
|
|
108
115
|
|
|
109
|
-
const server = new McpServer({
|
|
110
|
-
name: 'agent-working-memory',
|
|
111
|
-
version: '0.6.0',
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
|
|
116
|
+
const server = new McpServer({
|
|
117
|
+
name: 'agent-working-memory',
|
|
118
|
+
version: '0.6.0',
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
server.registerResource(
|
|
122
|
+
'awm-overview',
|
|
123
|
+
'awm://server/overview',
|
|
124
|
+
{
|
|
125
|
+
title: 'AWM Overview',
|
|
126
|
+
description: 'AgentWorkingMemory MCP server metadata and discovery notes',
|
|
127
|
+
mimeType: 'text/markdown',
|
|
128
|
+
},
|
|
129
|
+
async () => ({
|
|
130
|
+
contents: [{
|
|
131
|
+
uri: 'awm://server/overview',
|
|
132
|
+
text: [
|
|
133
|
+
'# Agent Working Memory',
|
|
134
|
+
'',
|
|
135
|
+
`Agent: ${AGENT_ID}`,
|
|
136
|
+
`DB: ${DB_PATH}`,
|
|
137
|
+
`Coordination: ${process.env.AWM_COORDINATION === 'true' || process.env.AWM_COORDINATION === '1' ? 'enabled' : 'disabled'}`,
|
|
138
|
+
'',
|
|
139
|
+
'This MCP server primarily exposes tools such as `memory_restore`, `memory_recall`, `memory_write`, and task/checkpoint operations.',
|
|
140
|
+
'The resources below exist so generic MCP clients can discover the server through `resources/list` and `resources/templates/list`.',
|
|
141
|
+
].join('\n'),
|
|
142
|
+
mimeType: 'text/markdown',
|
|
143
|
+
}],
|
|
144
|
+
})
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
server.registerResource(
|
|
148
|
+
'awm-memory-template',
|
|
149
|
+
new ResourceTemplate('awm://memory/{id}', { list: undefined }),
|
|
150
|
+
{
|
|
151
|
+
title: 'AWM Memory By ID',
|
|
152
|
+
description: 'Metadata resource template for a memory identifier',
|
|
153
|
+
mimeType: 'text/markdown',
|
|
154
|
+
},
|
|
155
|
+
async (_uri, variables) => ({
|
|
156
|
+
contents: [{
|
|
157
|
+
uri: `awm://memory/${variables.id ?? ''}`,
|
|
158
|
+
text: [
|
|
159
|
+
'# AWM Memory Reference',
|
|
160
|
+
'',
|
|
161
|
+
`Requested memory id: ${variables.id ?? ''}`,
|
|
162
|
+
'',
|
|
163
|
+
'Use the AWM memory tools for actual retrieval and mutation:',
|
|
164
|
+
'- `memory_recall` for cognitive retrieval',
|
|
165
|
+
'- `memory_restore` for session state',
|
|
166
|
+
'- `memory_feedback`, `memory_retract`, `memory_supersede` for memory maintenance',
|
|
167
|
+
].join('\n'),
|
|
168
|
+
mimeType: 'text/markdown',
|
|
169
|
+
}],
|
|
170
|
+
})
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
// --- Auto-classification for memory types ---
|
|
115
174
|
|
|
116
175
|
function classifyMemoryType(content: string): 'episodic' | 'semantic' | 'procedural' | 'unclassified' {
|
|
117
176
|
const lower = content.toLowerCase();
|
|
@@ -349,11 +408,16 @@ Returns the most relevant memories ranked by text relevance, temporal recency, a
|
|
|
349
408
|
|
|
350
409
|
log(AGENT_ID, 'recall', `"${queryText.slice(0, 80)}" → ${results.length} results`);
|
|
351
410
|
|
|
411
|
+
// Peer decisions: append recent decisions by other agents relevant to this query
|
|
412
|
+
const peerSuffix = coordDb
|
|
413
|
+
? formatPeerDecisions(queryPeerDecisions(coordDb, AGENT_ID, queryText))
|
|
414
|
+
: '';
|
|
415
|
+
|
|
352
416
|
if (results.length === 0) {
|
|
353
417
|
return {
|
|
354
418
|
content: [{
|
|
355
419
|
type: 'text' as const,
|
|
356
|
-
text: 'No relevant memories found.',
|
|
420
|
+
text: 'No relevant memories found.' + peerSuffix,
|
|
357
421
|
}],
|
|
358
422
|
};
|
|
359
423
|
}
|
|
@@ -365,7 +429,7 @@ Returns the most relevant memories ranked by text relevance, temporal recency, a
|
|
|
365
429
|
return {
|
|
366
430
|
content: [{
|
|
367
431
|
type: 'text' as const,
|
|
368
|
-
text: lines.join('\n'),
|
|
432
|
+
text: lines.join('\n') + peerSuffix,
|
|
369
433
|
}],
|
|
370
434
|
};
|
|
371
435
|
}
|
package/src/storage/sqlite.ts
CHANGED
|
@@ -30,6 +30,7 @@ const DEFAULT_SALIENCE_FEATURES: SalienceFeatures = {
|
|
|
30
30
|
|
|
31
31
|
export class EngramStore {
|
|
32
32
|
private db: Database.Database;
|
|
33
|
+
private walTimer: ReturnType<typeof setInterval> | null = null;
|
|
33
34
|
|
|
34
35
|
constructor(dbPath: string = 'memory.db') {
|
|
35
36
|
this.db = new Database(dbPath);
|
|
@@ -37,7 +38,9 @@ export class EngramStore {
|
|
|
37
38
|
this.db.pragma('foreign_keys = ON');
|
|
38
39
|
this.db.pragma('busy_timeout = 5000');
|
|
39
40
|
this.db.pragma('synchronous = NORMAL');
|
|
41
|
+
this.db.pragma('wal_autocheckpoint = 1000');
|
|
40
42
|
this.init();
|
|
43
|
+
this.startWalCheckpointTimer();
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
/** Expose the raw database handle for the coordination module. */
|
|
@@ -63,7 +66,27 @@ export class EngramStore {
|
|
|
63
66
|
|
|
64
67
|
/** Flush WAL to main database file. */
|
|
65
68
|
walCheckpoint(): void {
|
|
66
|
-
|
|
69
|
+
try {
|
|
70
|
+
this.db.pragma('wal_checkpoint(TRUNCATE)');
|
|
71
|
+
} catch {
|
|
72
|
+
// Checkpoint can fail if another connection holds the DB; non-fatal
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Start periodic WAL checkpoint every 5 minutes to prevent unbounded WAL growth. */
|
|
77
|
+
private startWalCheckpointTimer(): void {
|
|
78
|
+
this.walTimer = setInterval(() => {
|
|
79
|
+
this.walCheckpoint();
|
|
80
|
+
}, 5 * 60 * 1000);
|
|
81
|
+
this.walTimer.unref();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Stop the WAL checkpoint timer (call before close). */
|
|
85
|
+
stopWalCheckpointTimer(): void {
|
|
86
|
+
if (this.walTimer) {
|
|
87
|
+
clearInterval(this.walTimer);
|
|
88
|
+
this.walTimer = null;
|
|
89
|
+
}
|
|
67
90
|
}
|
|
68
91
|
|
|
69
92
|
private init(): void {
|
|
@@ -212,6 +235,13 @@ export class EngramStore {
|
|
|
212
235
|
`);
|
|
213
236
|
}
|
|
214
237
|
|
|
238
|
+
// Migration: add embedding_model for version tracking (prevents drift on model change)
|
|
239
|
+
try {
|
|
240
|
+
this.db.prepare('SELECT embedding_model FROM engrams LIMIT 0').get();
|
|
241
|
+
} catch {
|
|
242
|
+
this.db.exec(`ALTER TABLE engrams ADD COLUMN embedding_model TEXT`);
|
|
243
|
+
}
|
|
244
|
+
|
|
215
245
|
// Migration: add conscious_state table for checkpointing
|
|
216
246
|
this.db.exec(`
|
|
217
247
|
CREATE TABLE IF NOT EXISTS conscious_state (
|
|
@@ -327,9 +357,13 @@ export class EngramStore {
|
|
|
327
357
|
);
|
|
328
358
|
}
|
|
329
359
|
|
|
330
|
-
updateEmbedding(id: string, embedding: number[]): void {
|
|
360
|
+
updateEmbedding(id: string, embedding: number[], modelId?: string): void {
|
|
331
361
|
const blob = Buffer.from(new Float32Array(embedding).buffer);
|
|
332
|
-
|
|
362
|
+
if (modelId) {
|
|
363
|
+
this.db.prepare('UPDATE engrams SET embedding = ?, embedding_model = ? WHERE id = ?').run(blob, modelId, id);
|
|
364
|
+
} else {
|
|
365
|
+
this.db.prepare('UPDATE engrams SET embedding = ? WHERE id = ?').run(blob, id);
|
|
366
|
+
}
|
|
333
367
|
}
|
|
334
368
|
|
|
335
369
|
retractEngram(id: string, retractedBy: string | null): void {
|
|
@@ -1012,6 +1046,8 @@ export class EngramStore {
|
|
|
1012
1046
|
}
|
|
1013
1047
|
|
|
1014
1048
|
close(): void {
|
|
1049
|
+
this.stopWalCheckpointTimer();
|
|
1050
|
+
this.walCheckpoint();
|
|
1015
1051
|
this.db.close();
|
|
1016
1052
|
}
|
|
1017
1053
|
}
|