@psiclawops/hypermem 0.1.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.
Files changed (94) hide show
  1. package/ARCHITECTURE.md +296 -0
  2. package/LICENSE +190 -0
  3. package/README.md +243 -0
  4. package/dist/background-indexer.d.ts +117 -0
  5. package/dist/background-indexer.d.ts.map +1 -0
  6. package/dist/background-indexer.js +732 -0
  7. package/dist/compaction-fence.d.ts +89 -0
  8. package/dist/compaction-fence.d.ts.map +1 -0
  9. package/dist/compaction-fence.js +153 -0
  10. package/dist/compositor.d.ts +139 -0
  11. package/dist/compositor.d.ts.map +1 -0
  12. package/dist/compositor.js +1109 -0
  13. package/dist/cross-agent.d.ts +57 -0
  14. package/dist/cross-agent.d.ts.map +1 -0
  15. package/dist/cross-agent.js +254 -0
  16. package/dist/db.d.ts +131 -0
  17. package/dist/db.d.ts.map +1 -0
  18. package/dist/db.js +398 -0
  19. package/dist/desired-state-store.d.ts +100 -0
  20. package/dist/desired-state-store.d.ts.map +1 -0
  21. package/dist/desired-state-store.js +212 -0
  22. package/dist/doc-chunk-store.d.ts +115 -0
  23. package/dist/doc-chunk-store.d.ts.map +1 -0
  24. package/dist/doc-chunk-store.js +278 -0
  25. package/dist/doc-chunker.d.ts +99 -0
  26. package/dist/doc-chunker.d.ts.map +1 -0
  27. package/dist/doc-chunker.js +324 -0
  28. package/dist/episode-store.d.ts +48 -0
  29. package/dist/episode-store.d.ts.map +1 -0
  30. package/dist/episode-store.js +135 -0
  31. package/dist/fact-store.d.ts +57 -0
  32. package/dist/fact-store.d.ts.map +1 -0
  33. package/dist/fact-store.js +175 -0
  34. package/dist/fleet-store.d.ts +144 -0
  35. package/dist/fleet-store.d.ts.map +1 -0
  36. package/dist/fleet-store.js +276 -0
  37. package/dist/hybrid-retrieval.d.ts +60 -0
  38. package/dist/hybrid-retrieval.d.ts.map +1 -0
  39. package/dist/hybrid-retrieval.js +340 -0
  40. package/dist/index.d.ts +611 -0
  41. package/dist/index.d.ts.map +1 -0
  42. package/dist/index.js +1042 -0
  43. package/dist/knowledge-graph.d.ts +110 -0
  44. package/dist/knowledge-graph.d.ts.map +1 -0
  45. package/dist/knowledge-graph.js +305 -0
  46. package/dist/knowledge-store.d.ts +72 -0
  47. package/dist/knowledge-store.d.ts.map +1 -0
  48. package/dist/knowledge-store.js +241 -0
  49. package/dist/library-schema.d.ts +22 -0
  50. package/dist/library-schema.d.ts.map +1 -0
  51. package/dist/library-schema.js +717 -0
  52. package/dist/message-store.d.ts +76 -0
  53. package/dist/message-store.d.ts.map +1 -0
  54. package/dist/message-store.js +273 -0
  55. package/dist/preference-store.d.ts +54 -0
  56. package/dist/preference-store.d.ts.map +1 -0
  57. package/dist/preference-store.js +109 -0
  58. package/dist/preservation-gate.d.ts +82 -0
  59. package/dist/preservation-gate.d.ts.map +1 -0
  60. package/dist/preservation-gate.js +150 -0
  61. package/dist/provider-translator.d.ts +40 -0
  62. package/dist/provider-translator.d.ts.map +1 -0
  63. package/dist/provider-translator.js +349 -0
  64. package/dist/rate-limiter.d.ts +76 -0
  65. package/dist/rate-limiter.d.ts.map +1 -0
  66. package/dist/rate-limiter.js +179 -0
  67. package/dist/redis.d.ts +188 -0
  68. package/dist/redis.d.ts.map +1 -0
  69. package/dist/redis.js +534 -0
  70. package/dist/schema.d.ts +15 -0
  71. package/dist/schema.d.ts.map +1 -0
  72. package/dist/schema.js +203 -0
  73. package/dist/secret-scanner.d.ts +51 -0
  74. package/dist/secret-scanner.d.ts.map +1 -0
  75. package/dist/secret-scanner.js +248 -0
  76. package/dist/seed.d.ts +108 -0
  77. package/dist/seed.d.ts.map +1 -0
  78. package/dist/seed.js +177 -0
  79. package/dist/system-store.d.ts +73 -0
  80. package/dist/system-store.d.ts.map +1 -0
  81. package/dist/system-store.js +182 -0
  82. package/dist/topic-store.d.ts +45 -0
  83. package/dist/topic-store.d.ts.map +1 -0
  84. package/dist/topic-store.js +136 -0
  85. package/dist/types.d.ts +329 -0
  86. package/dist/types.d.ts.map +1 -0
  87. package/dist/types.js +9 -0
  88. package/dist/vector-store.d.ts +132 -0
  89. package/dist/vector-store.d.ts.map +1 -0
  90. package/dist/vector-store.js +498 -0
  91. package/dist/work-store.d.ts +112 -0
  92. package/dist/work-store.d.ts.map +1 -0
  93. package/dist/work-store.js +273 -0
  94. package/package.json +57 -0
package/dist/redis.js ADDED
@@ -0,0 +1,534 @@
1
+ /**
2
+ * HyperMem Redis Layer
3
+ *
4
+ * Manages the hot-state compositor cache.
5
+ * Per-agent, per-session keyspace with TTL management.
6
+ * Falls back gracefully when Redis is unavailable.
7
+ */
8
+ import { Redis as RedisClient } from 'ioredis';
9
+ const DEFAULT_CONFIG = {
10
+ host: 'localhost',
11
+ port: 6379,
12
+ keyPrefix: 'hm:',
13
+ sessionTTL: 14400, // 4 hours — slots like system/identity/meta
14
+ historyTTL: 86400, // 24 hours — history list ages out after a day
15
+ flushInterval: 1000, // 1 second
16
+ };
17
+ export class RedisLayer {
18
+ client = null;
19
+ config;
20
+ connected = false;
21
+ constructor(config) {
22
+ this.config = { ...DEFAULT_CONFIG, ...config };
23
+ }
24
+ /**
25
+ * Connect to Redis. Non-blocking — operations degrade gracefully if connection fails.
26
+ */
27
+ async connect() {
28
+ try {
29
+ this.client = new RedisClient({
30
+ host: this.config.host,
31
+ port: this.config.port,
32
+ password: this.config.password,
33
+ lazyConnect: false,
34
+ maxRetriesPerRequest: 1,
35
+ retryStrategy(times) {
36
+ if (times > 3)
37
+ return null; // stop retrying
38
+ return Math.min(times * 200, 2000);
39
+ },
40
+ enableReadyCheck: true,
41
+ });
42
+ this.client.on('connect', () => { this.connected = true; });
43
+ this.client.on('close', () => { this.connected = false; });
44
+ this.client.on('error', (err) => {
45
+ console.warn('[hypermem-redis] Connection error:', err.message);
46
+ this.connected = false;
47
+ });
48
+ // Wait briefly for connection
49
+ await new Promise((resolve) => {
50
+ const timeout = setTimeout(() => resolve(), 3000);
51
+ this.client.once('ready', () => {
52
+ clearTimeout(timeout);
53
+ this.connected = true;
54
+ resolve();
55
+ });
56
+ this.client.once('error', () => {
57
+ clearTimeout(timeout);
58
+ resolve();
59
+ });
60
+ });
61
+ return this.connected;
62
+ }
63
+ catch {
64
+ console.warn('[hypermem-redis] Failed to connect');
65
+ return false;
66
+ }
67
+ }
68
+ get isConnected() {
69
+ return this.connected && this.client !== null;
70
+ }
71
+ // ─── Key Helpers ─────────────────────────────────────────────
72
+ agentKey(agentId, suffix) {
73
+ return `${this.config.keyPrefix}${agentId}:${suffix}`;
74
+ }
75
+ sessionKey(agentId, sessionKey, slot) {
76
+ return `${this.config.keyPrefix}${agentId}:s:${sessionKey}:${slot}`;
77
+ }
78
+ // ─── Agent-Level Operations ──────────────────────────────────
79
+ /**
80
+ * Set the agent's profile in Redis.
81
+ */
82
+ async setProfile(agentId, profile) {
83
+ if (!this.isConnected)
84
+ return;
85
+ const key = this.agentKey(agentId, 'profile');
86
+ await this.client.set(key, JSON.stringify(profile));
87
+ }
88
+ /**
89
+ * Get the agent's profile.
90
+ */
91
+ async getProfile(agentId) {
92
+ if (!this.isConnected)
93
+ return null;
94
+ const key = this.agentKey(agentId, 'profile');
95
+ const val = await this.client.get(key);
96
+ return val ? JSON.parse(val) : null;
97
+ }
98
+ /**
99
+ * Track active sessions for an agent.
100
+ */
101
+ async addActiveSession(agentId, sessionKey) {
102
+ if (!this.isConnected)
103
+ return;
104
+ const key = this.agentKey(agentId, 'active_sessions');
105
+ await this.client.sadd(key, sessionKey);
106
+ }
107
+ async removeActiveSession(agentId, sessionKey) {
108
+ if (!this.isConnected)
109
+ return;
110
+ const key = this.agentKey(agentId, 'active_sessions');
111
+ await this.client.srem(key, sessionKey);
112
+ }
113
+ async getActiveSessions(agentId) {
114
+ if (!this.isConnected)
115
+ return [];
116
+ const key = this.agentKey(agentId, 'active_sessions');
117
+ return this.client.smembers(key);
118
+ }
119
+ // ─── Session Slot Operations ─────────────────────────────────
120
+ /**
121
+ * Set a session slot value.
122
+ */
123
+ async setSlot(agentId, sessionKey, slot, value) {
124
+ if (!this.isConnected)
125
+ return;
126
+ const key = this.sessionKey(agentId, sessionKey, slot);
127
+ await this.client.set(key, value, 'EX', this.config.sessionTTL);
128
+ }
129
+ /**
130
+ * Get a session slot value.
131
+ */
132
+ async getSlot(agentId, sessionKey, slot) {
133
+ if (!this.isConnected)
134
+ return null;
135
+ const key = this.sessionKey(agentId, sessionKey, slot);
136
+ return this.client.get(key);
137
+ }
138
+ /**
139
+ * Set session metadata.
140
+ */
141
+ async setSessionMeta(agentId, sessionKey, meta) {
142
+ if (!this.isConnected)
143
+ return;
144
+ const key = this.sessionKey(agentId, sessionKey, 'meta');
145
+ await this.client.hmset(key, {
146
+ agentId: meta.agentId,
147
+ sessionKey: meta.sessionKey,
148
+ provider: meta.provider || '',
149
+ model: meta.model || '',
150
+ channelType: meta.channelType,
151
+ tokenCount: String(meta.tokenCount),
152
+ lastActive: meta.lastActive,
153
+ status: meta.status,
154
+ });
155
+ await this.client.expire(key, this.config.sessionTTL);
156
+ }
157
+ /**
158
+ * Get session metadata.
159
+ */
160
+ async getSessionMeta(agentId, sessionKey) {
161
+ if (!this.isConnected)
162
+ return null;
163
+ const key = this.sessionKey(agentId, sessionKey, 'meta');
164
+ const data = await this.client.hgetall(key);
165
+ if (!data || !data.agentId)
166
+ return null;
167
+ return {
168
+ agentId: data.agentId,
169
+ sessionKey: data.sessionKey,
170
+ provider: data.provider || null,
171
+ model: data.model || null,
172
+ channelType: data.channelType,
173
+ tokenCount: parseInt(data.tokenCount || '0', 10),
174
+ lastActive: data.lastActive,
175
+ status: data.status,
176
+ };
177
+ }
178
+ /**
179
+ * Push messages to the session history list.
180
+ *
181
+ * History retention strategy:
182
+ * - No aggressive LTRIM. Cap at maxMessages (default 250) to prevent
183
+ * unbounded growth, but the real budget enforcement happens in the
184
+ * compositor at compose time.
185
+ * - TTL is historyTTL (default 24h), not sessionTTL. History outlives
186
+ * other session slots because it's the primary context source.
187
+ * - System/identity slots are refreshed on every warmSession() call,
188
+ * so they effectively never expire during an active session.
189
+ */
190
+ async pushHistory(agentId, sessionKey, messages, maxMessages = 250) {
191
+ if (!this.isConnected || messages.length === 0)
192
+ return;
193
+ const key = this.sessionKey(agentId, sessionKey, 'history');
194
+ // Tail-check dedup: read the last stored message id before appending.
195
+ // Filters out messages already present from a previous warm/push call.
196
+ // This is the primary defense against bootstrap re-runs duplicating history.
197
+ // Uses monotonically increasing StoredMessage.id for ordering guarantees.
198
+ let filteredMessages = messages;
199
+ try {
200
+ const tail = await this.client.lrange(key, -1, -1);
201
+ if (tail.length > 0) {
202
+ const lastStored = JSON.parse(tail[0]);
203
+ if (lastStored.id != null) {
204
+ filteredMessages = messages.filter(m => m.id > lastStored.id);
205
+ }
206
+ }
207
+ }
208
+ catch {
209
+ // Tail-check failure is non-fatal — fall through to full push
210
+ }
211
+ if (filteredMessages.length === 0)
212
+ return;
213
+ const pipeline = this.client.pipeline();
214
+ for (const msg of filteredMessages) {
215
+ pipeline.rpush(key, JSON.stringify(msg));
216
+ }
217
+ // Soft cap — only trim if we're way over. This is a safety net,
218
+ // not a context management strategy. The compositor handles budget.
219
+ pipeline.ltrim(key, -maxMessages, -1);
220
+ // History gets its own longer TTL
221
+ pipeline.expire(key, this.config.historyTTL);
222
+ await pipeline.exec();
223
+ }
224
+ /**
225
+ * Get session history from Redis.
226
+ *
227
+ * @param limit - When provided, return only the last N messages using
228
+ * negative indexing (LRANGE -limit -1). This is the correct enforcement
229
+ * point for historyDepth — previously the limit param was ignored on the
230
+ * Redis path and only applied to the SQLite fallback.
231
+ */
232
+ async getHistory(agentId, sessionKey, limit) {
233
+ if (!this.isConnected)
234
+ return [];
235
+ const key = this.sessionKey(agentId, sessionKey, 'history');
236
+ // When limit is provided, use LRANGE -limit -1 to fetch the last N messages.
237
+ // LRANGE 0 -1 returns everything; -limit -1 returns the last `limit` entries.
238
+ const start = limit ? -limit : 0;
239
+ const items = await this.client.lrange(key, start, -1);
240
+ return items.map((item) => JSON.parse(item));
241
+ }
242
+ /**
243
+ * Check whether a session already has history in Redis.
244
+ * Used by the bootstrap idempotency guard to avoid re-warming hot sessions.
245
+ *
246
+ * Single EXISTS call — sub-millisecond, no data transferred.
247
+ */
248
+ async sessionExists(agentId, sessionKey) {
249
+ if (!this.isConnected)
250
+ return false;
251
+ const key = this.sessionKey(agentId, sessionKey, 'history');
252
+ const exists = await this.client.exists(key);
253
+ return exists === 1;
254
+ }
255
+ /**
256
+ * Trim Redis history to fit within a token budget.
257
+ *
258
+ * Walks the history newest→oldest, accumulating estimated tokens.
259
+ * When the budget is exceeded, trims everything older via LTRIM.
260
+ * This is the guardrail against model switches: if an agent ran on a
261
+ * 1M-context model and accumulated a huge history, then switched to a
262
+ * 120K model, the first assemble() call trims the excess.
263
+ *
264
+ * Token estimation: text at length/4, tool JSON at length/2 (dense).
265
+ *
266
+ * @returns Number of messages trimmed, or 0 if already within budget.
267
+ */
268
+ async trimHistoryToTokenBudget(agentId, sessionKey, tokenBudget) {
269
+ if (!this.isConnected || tokenBudget <= 0)
270
+ return 0;
271
+ const key = this.sessionKey(agentId, sessionKey, 'history');
272
+ // Get total length first — avoid fetching everything if small
273
+ const totalLen = await this.client.llen(key);
274
+ if (totalLen <= 10)
275
+ return 0; // tiny history, skip
276
+ // Fetch all messages to estimate tokens
277
+ const items = await this.client.lrange(key, 0, -1);
278
+ if (items.length === 0)
279
+ return 0;
280
+ // Walk newest→oldest, find the cut point
281
+ let tokenSum = 0;
282
+ let keepFrom = 0; // index (0-based) of the oldest message to keep
283
+ for (let i = items.length - 1; i >= 0; i--) {
284
+ try {
285
+ const msg = JSON.parse(items[i]);
286
+ let msgTokens = Math.ceil((msg.textContent?.length ?? 0) / 4);
287
+ if (msg.toolCalls)
288
+ msgTokens += Math.ceil(JSON.stringify(msg.toolCalls).length / 2);
289
+ if (msg.toolResults)
290
+ msgTokens += Math.ceil(JSON.stringify(msg.toolResults).length / 2);
291
+ tokenSum += msgTokens;
292
+ if (tokenSum > tokenBudget) {
293
+ keepFrom = i + 1;
294
+ break;
295
+ }
296
+ }
297
+ catch {
298
+ // Unparseable message — count as 500 tokens
299
+ tokenSum += 500;
300
+ if (tokenSum > tokenBudget) {
301
+ keepFrom = i + 1;
302
+ break;
303
+ }
304
+ }
305
+ }
306
+ if (keepFrom <= 0)
307
+ return 0; // everything fits
308
+ // LTRIM keeps [keepFrom, -1] — drops everything before keepFrom
309
+ await this.client.ltrim(key, keepFrom, -1);
310
+ console.log(`[hypermem-redis] trimHistoryToTokenBudget: trimmed ${keepFrom} messages from ${agentId}/${sessionKey} (budget=${tokenBudget}, had=${items.length}, kept=${items.length - keepFrom})`);
311
+ return keepFrom;
312
+ }
313
+ // ─── Window Cache Operations ───────────────────────────────
314
+ /**
315
+ * Cache the compositor's assembled submission window.
316
+ *
317
+ * The window is the token-budgeted, deduplicated output of compose() —
318
+ * what actually gets sent to the provider. Separate from history (the
319
+ * append-only archive). Short TTL: 120s default, refreshed each compose.
320
+ */
321
+ async setWindow(agentId, sessionKey, messages, ttlSeconds = 120) {
322
+ if (!this.isConnected)
323
+ return;
324
+ const key = this.sessionKey(agentId, sessionKey, 'window');
325
+ await this.client.set(key, JSON.stringify(messages), 'EX', ttlSeconds);
326
+ }
327
+ /**
328
+ * Get the cached submission window.
329
+ * Returns null on cache miss — caller should run full compose().
330
+ */
331
+ async getWindow(agentId, sessionKey) {
332
+ if (!this.isConnected)
333
+ return null;
334
+ const key = this.sessionKey(agentId, sessionKey, 'window');
335
+ const val = await this.client.get(key);
336
+ return val ? JSON.parse(val) : null;
337
+ }
338
+ /**
339
+ * Invalidate the submission window cache.
340
+ * Called after afterTurn ingest writes new messages — ensures the next
341
+ * compose() runs fresh instead of serving a stale cached window.
342
+ */
343
+ async invalidateWindow(agentId, sessionKey) {
344
+ if (!this.isConnected)
345
+ return;
346
+ const key = this.sessionKey(agentId, sessionKey, 'window');
347
+ await this.client.del(key);
348
+ }
349
+ // ─── Session Cursor Operations ─────────────────────────────
350
+ /**
351
+ * Write the session cursor after compose().
352
+ * Tracks the boundary between "LLM has seen this" and "new since last compose."
353
+ * TTL matches history (24h) so the cursor outlives the window cache.
354
+ */
355
+ async setCursor(agentId, sessionKey, cursor) {
356
+ if (!this.isConnected)
357
+ return;
358
+ const key = this.sessionKey(agentId, sessionKey, 'cursor');
359
+ await this.client.set(key, JSON.stringify(cursor), 'EX', this.config.historyTTL);
360
+ }
361
+ /**
362
+ * Get the session cursor.
363
+ * Returns null if no cursor exists (first compose, or Redis eviction).
364
+ */
365
+ async getCursor(agentId, sessionKey) {
366
+ if (!this.isConnected)
367
+ return null;
368
+ const key = this.sessionKey(agentId, sessionKey, 'cursor');
369
+ const val = await this.client.get(key);
370
+ return val ? JSON.parse(val) : null;
371
+ }
372
+ // ─── Bulk Session Operations ─────────────────────────────────
373
+ /**
374
+ * Warm all slots for a session at once.
375
+ */
376
+ async warmSession(agentId, sessionKey, slots) {
377
+ if (!this.isConnected)
378
+ return;
379
+ const pipeline = this.client.pipeline();
380
+ if (slots.system) {
381
+ const key = this.sessionKey(agentId, sessionKey, 'system');
382
+ pipeline.set(key, slots.system, 'EX', this.config.sessionTTL);
383
+ }
384
+ if (slots.identity) {
385
+ const key = this.sessionKey(agentId, sessionKey, 'identity');
386
+ pipeline.set(key, slots.identity, 'EX', this.config.sessionTTL);
387
+ }
388
+ if (slots.context) {
389
+ const key = this.sessionKey(agentId, sessionKey, 'context');
390
+ pipeline.set(key, slots.context, 'EX', this.config.sessionTTL);
391
+ }
392
+ if (slots.facts) {
393
+ const key = this.sessionKey(agentId, sessionKey, 'facts');
394
+ pipeline.set(key, slots.facts, 'EX', this.config.sessionTTL);
395
+ }
396
+ if (slots.tools) {
397
+ const key = this.sessionKey(agentId, sessionKey, 'tools');
398
+ pipeline.set(key, slots.tools, 'EX', this.config.sessionTTL);
399
+ }
400
+ await pipeline.exec();
401
+ if (slots.meta) {
402
+ await this.setSessionMeta(agentId, sessionKey, slots.meta);
403
+ }
404
+ if (slots.history && slots.history.length > 0) {
405
+ await this.pushHistory(agentId, sessionKey, slots.history);
406
+ }
407
+ // Mark session as active
408
+ await this.addActiveSession(agentId, sessionKey);
409
+ }
410
+ /**
411
+ * Evict all keys for a session.
412
+ */
413
+ async evictSession(agentId, sessionKey) {
414
+ if (!this.isConnected)
415
+ return;
416
+ const slots = ['system', 'identity', 'history', 'window', 'cursor', 'context', 'facts', 'tools', 'meta'];
417
+ const keys = slots.map(s => this.sessionKey(agentId, sessionKey, s));
418
+ await this.client.del(...keys);
419
+ await this.removeActiveSession(agentId, sessionKey);
420
+ }
421
+ // ─── Touch / TTL ─────────────────────────────────────────────
422
+ /**
423
+ * Refresh TTL on all session keys.
424
+ * Uses historyTTL for the history slot, sessionTTL for everything else.
425
+ */
426
+ async touchSession(agentId, sessionKey) {
427
+ if (!this.isConnected)
428
+ return;
429
+ const slotTTLs = [
430
+ ['system', this.config.sessionTTL],
431
+ ['identity', this.config.sessionTTL],
432
+ ['history', this.config.historyTTL],
433
+ ['window', 120], // short-lived compose output cache
434
+ ['cursor', this.config.historyTTL], // lives as long as history
435
+ ['context', this.config.sessionTTL],
436
+ ['facts', this.config.sessionTTL],
437
+ ['tools', this.config.sessionTTL],
438
+ ['meta', this.config.sessionTTL],
439
+ ];
440
+ const pipeline = this.client.pipeline();
441
+ for (const [slot, ttl] of slotTTLs) {
442
+ pipeline.expire(this.sessionKey(agentId, sessionKey, slot), ttl);
443
+ }
444
+ await pipeline.exec();
445
+ }
446
+ /**
447
+ * Flush all keys matching this instance's prefix. For testing only.
448
+ */
449
+ async flushPrefix() {
450
+ if (!this.isConnected)
451
+ return 0;
452
+ const pattern = `${this.config.keyPrefix}*`;
453
+ const keys = await this.client.keys(pattern);
454
+ if (keys.length === 0)
455
+ return 0;
456
+ await this.client.del(...keys);
457
+ return keys.length;
458
+ }
459
+ // ─── Fleet Cache (Library L4 Hot Layer) ───────────────────────
460
+ /**
461
+ * Cache a fleet-level value. Used for library data that's read frequently.
462
+ * TTL defaults to 10 minutes — short enough to pick up changes, long enough
463
+ * to avoid hammering SQLite on every heartbeat.
464
+ */
465
+ async setFleetCache(key, value, ttl = 600) {
466
+ if (!this.isConnected)
467
+ return;
468
+ const redisKey = `${this.config.keyPrefix}fleet:${key}`;
469
+ await this.client.set(redisKey, value, 'EX', ttl);
470
+ }
471
+ /**
472
+ * Get a fleet-level cached value.
473
+ */
474
+ async getFleetCache(key) {
475
+ if (!this.isConnected)
476
+ return null;
477
+ const redisKey = `${this.config.keyPrefix}fleet:${key}`;
478
+ return this.client.get(redisKey);
479
+ }
480
+ /**
481
+ * Delete a fleet-level cached value.
482
+ */
483
+ async delFleetCache(key) {
484
+ if (!this.isConnected)
485
+ return;
486
+ const redisKey = `${this.config.keyPrefix}fleet:${key}`;
487
+ await this.client.del(redisKey);
488
+ }
489
+ /**
490
+ * Cache a fleet agent's full profile (fleet registry + capabilities + desired state).
491
+ * Structured so a single key read gives the dashboard everything it needs.
492
+ */
493
+ async cacheFleetAgent(agentId, data) {
494
+ await this.setFleetCache(`agent:${agentId}`, JSON.stringify(data));
495
+ }
496
+ /**
497
+ * Get a cached fleet agent profile.
498
+ */
499
+ async getCachedFleetAgent(agentId) {
500
+ const val = await this.getFleetCache(`agent:${agentId}`);
501
+ return val ? JSON.parse(val) : null;
502
+ }
503
+ /**
504
+ * Cache the fleet summary (agent count, drift count, etc.).
505
+ * Short TTL — recalculated frequently.
506
+ */
507
+ async cacheFleetSummary(summary) {
508
+ await this.setFleetCache('summary', JSON.stringify(summary), 120); // 2 min
509
+ }
510
+ /**
511
+ * Get the cached fleet summary.
512
+ */
513
+ async getCachedFleetSummary() {
514
+ const val = await this.getFleetCache('summary');
515
+ return val ? JSON.parse(val) : null;
516
+ }
517
+ /**
518
+ * Invalidate all fleet cache entries for a specific agent.
519
+ * Call after mutations to fleet registry / desired state.
520
+ */
521
+ async invalidateFleetAgent(agentId) {
522
+ await this.delFleetCache(`agent:${agentId}`);
523
+ await this.delFleetCache('summary'); // Summary includes this agent
524
+ }
525
+ // ─── Lifecycle ───────────────────────────────────────────────
526
+ async disconnect() {
527
+ if (this.client) {
528
+ await this.client.quit();
529
+ this.client = null;
530
+ this.connected = false;
531
+ }
532
+ }
533
+ }
534
+ //# sourceMappingURL=redis.js.map
@@ -0,0 +1,15 @@
1
+ /**
2
+ * HyperMem Agent Message Schema
3
+ *
4
+ * Per-agent database: ~/.openclaw/hypermem/agents/{agentId}/messages.db
5
+ * Write-heavy, temporal, rotatable.
6
+ * Contains ONLY conversation data — structured knowledge lives in library.db.
7
+ */
8
+ import type { DatabaseSync } from 'node:sqlite';
9
+ export declare const LATEST_SCHEMA_VERSION = 5;
10
+ /**
11
+ * Run migrations on an agent message database.
12
+ */
13
+ export declare function migrate(db: DatabaseSync): void;
14
+ export { LATEST_SCHEMA_VERSION as SCHEMA_VERSION };
15
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,eAAO,MAAM,qBAAqB,IAAI,CAAC;AAgJvC;;GAEG;AACH,wBAAgB,OAAO,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CA+D9C;AAED,OAAO,EAAE,qBAAqB,IAAI,cAAc,EAAE,CAAC"}