@psiclawops/hypermem 0.5.0 → 0.5.2

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 (163) hide show
  1. package/ARCHITECTURE.md +12 -3
  2. package/README.md +30 -6
  3. package/bin/hypermem-status.mjs +166 -0
  4. package/dist/background-indexer.d.ts +132 -0
  5. package/dist/background-indexer.d.ts.map +1 -0
  6. package/dist/background-indexer.js +1044 -0
  7. package/dist/cache.d.ts +110 -0
  8. package/dist/cache.d.ts.map +1 -0
  9. package/dist/cache.js +495 -0
  10. package/dist/compaction-fence.d.ts +89 -0
  11. package/dist/compaction-fence.d.ts.map +1 -0
  12. package/dist/compaction-fence.js +153 -0
  13. package/dist/compositor.d.ts +226 -0
  14. package/dist/compositor.d.ts.map +1 -0
  15. package/dist/compositor.js +2558 -0
  16. package/dist/content-type-classifier.d.ts +41 -0
  17. package/dist/content-type-classifier.d.ts.map +1 -0
  18. package/dist/content-type-classifier.js +181 -0
  19. package/dist/cross-agent.d.ts +62 -0
  20. package/dist/cross-agent.d.ts.map +1 -0
  21. package/dist/cross-agent.js +259 -0
  22. package/dist/db.d.ts +131 -0
  23. package/dist/db.d.ts.map +1 -0
  24. package/dist/db.js +402 -0
  25. package/dist/desired-state-store.d.ts +100 -0
  26. package/dist/desired-state-store.d.ts.map +1 -0
  27. package/dist/desired-state-store.js +222 -0
  28. package/dist/doc-chunk-store.d.ts +140 -0
  29. package/dist/doc-chunk-store.d.ts.map +1 -0
  30. package/dist/doc-chunk-store.js +391 -0
  31. package/dist/doc-chunker.d.ts +99 -0
  32. package/dist/doc-chunker.d.ts.map +1 -0
  33. package/dist/doc-chunker.js +324 -0
  34. package/dist/dreaming-promoter.d.ts +86 -0
  35. package/dist/dreaming-promoter.d.ts.map +1 -0
  36. package/dist/dreaming-promoter.js +381 -0
  37. package/dist/episode-store.d.ts +49 -0
  38. package/dist/episode-store.d.ts.map +1 -0
  39. package/dist/episode-store.js +135 -0
  40. package/dist/fact-store.d.ts +75 -0
  41. package/dist/fact-store.d.ts.map +1 -0
  42. package/dist/fact-store.js +236 -0
  43. package/dist/fleet-store.d.ts +144 -0
  44. package/dist/fleet-store.d.ts.map +1 -0
  45. package/dist/fleet-store.js +276 -0
  46. package/dist/fos-mod.d.ts +178 -0
  47. package/dist/fos-mod.d.ts.map +1 -0
  48. package/dist/fos-mod.js +416 -0
  49. package/dist/hybrid-retrieval.d.ts +64 -0
  50. package/dist/hybrid-retrieval.d.ts.map +1 -0
  51. package/dist/hybrid-retrieval.js +344 -0
  52. package/dist/image-eviction.d.ts +49 -0
  53. package/dist/image-eviction.d.ts.map +1 -0
  54. package/dist/image-eviction.js +251 -0
  55. package/dist/index.d.ts +650 -0
  56. package/dist/index.d.ts.map +1 -0
  57. package/dist/index.js +1072 -0
  58. package/dist/keystone-scorer.d.ts +51 -0
  59. package/dist/keystone-scorer.d.ts.map +1 -0
  60. package/dist/keystone-scorer.js +52 -0
  61. package/dist/knowledge-graph.d.ts +110 -0
  62. package/dist/knowledge-graph.d.ts.map +1 -0
  63. package/dist/knowledge-graph.js +305 -0
  64. package/dist/knowledge-lint.d.ts +29 -0
  65. package/dist/knowledge-lint.d.ts.map +1 -0
  66. package/dist/knowledge-lint.js +116 -0
  67. package/dist/knowledge-store.d.ts +72 -0
  68. package/dist/knowledge-store.d.ts.map +1 -0
  69. package/dist/knowledge-store.js +247 -0
  70. package/dist/library-schema.d.ts +22 -0
  71. package/dist/library-schema.d.ts.map +1 -0
  72. package/dist/library-schema.js +1038 -0
  73. package/dist/message-store.d.ts +89 -0
  74. package/dist/message-store.d.ts.map +1 -0
  75. package/dist/message-store.js +323 -0
  76. package/dist/metrics-dashboard.d.ts +114 -0
  77. package/dist/metrics-dashboard.d.ts.map +1 -0
  78. package/dist/metrics-dashboard.js +260 -0
  79. package/dist/obsidian-exporter.d.ts +57 -0
  80. package/dist/obsidian-exporter.d.ts.map +1 -0
  81. package/dist/obsidian-exporter.js +274 -0
  82. package/dist/obsidian-watcher.d.ts +147 -0
  83. package/dist/obsidian-watcher.d.ts.map +1 -0
  84. package/dist/obsidian-watcher.js +403 -0
  85. package/dist/open-domain.d.ts +46 -0
  86. package/dist/open-domain.d.ts.map +1 -0
  87. package/dist/open-domain.js +125 -0
  88. package/dist/preference-store.d.ts +54 -0
  89. package/dist/preference-store.d.ts.map +1 -0
  90. package/dist/preference-store.js +109 -0
  91. package/dist/preservation-gate.d.ts +82 -0
  92. package/dist/preservation-gate.d.ts.map +1 -0
  93. package/dist/preservation-gate.js +150 -0
  94. package/dist/proactive-pass.d.ts +63 -0
  95. package/dist/proactive-pass.d.ts.map +1 -0
  96. package/dist/proactive-pass.js +239 -0
  97. package/dist/profiles.d.ts +44 -0
  98. package/dist/profiles.d.ts.map +1 -0
  99. package/dist/profiles.js +227 -0
  100. package/dist/provider-translator.d.ts +50 -0
  101. package/dist/provider-translator.d.ts.map +1 -0
  102. package/dist/provider-translator.js +403 -0
  103. package/dist/rate-limiter.d.ts +76 -0
  104. package/dist/rate-limiter.d.ts.map +1 -0
  105. package/dist/rate-limiter.js +179 -0
  106. package/dist/repair-tool-pairs.d.ts +38 -0
  107. package/dist/repair-tool-pairs.d.ts.map +1 -0
  108. package/dist/repair-tool-pairs.js +138 -0
  109. package/dist/retrieval-policy.d.ts +51 -0
  110. package/dist/retrieval-policy.d.ts.map +1 -0
  111. package/dist/retrieval-policy.js +77 -0
  112. package/dist/schema.d.ts +15 -0
  113. package/dist/schema.d.ts.map +1 -0
  114. package/dist/schema.js +229 -0
  115. package/dist/secret-scanner.d.ts +51 -0
  116. package/dist/secret-scanner.d.ts.map +1 -0
  117. package/dist/secret-scanner.js +248 -0
  118. package/dist/seed.d.ts +108 -0
  119. package/dist/seed.d.ts.map +1 -0
  120. package/dist/seed.js +177 -0
  121. package/dist/session-flusher.d.ts +53 -0
  122. package/dist/session-flusher.d.ts.map +1 -0
  123. package/dist/session-flusher.js +69 -0
  124. package/dist/session-topic-map.d.ts +41 -0
  125. package/dist/session-topic-map.d.ts.map +1 -0
  126. package/dist/session-topic-map.js +77 -0
  127. package/dist/spawn-context.d.ts +54 -0
  128. package/dist/spawn-context.d.ts.map +1 -0
  129. package/dist/spawn-context.js +159 -0
  130. package/dist/system-store.d.ts +73 -0
  131. package/dist/system-store.d.ts.map +1 -0
  132. package/dist/system-store.js +182 -0
  133. package/dist/temporal-store.d.ts +80 -0
  134. package/dist/temporal-store.d.ts.map +1 -0
  135. package/dist/temporal-store.js +149 -0
  136. package/dist/topic-detector.d.ts +35 -0
  137. package/dist/topic-detector.d.ts.map +1 -0
  138. package/dist/topic-detector.js +249 -0
  139. package/dist/topic-store.d.ts +45 -0
  140. package/dist/topic-store.d.ts.map +1 -0
  141. package/dist/topic-store.js +136 -0
  142. package/dist/topic-synthesizer.d.ts +51 -0
  143. package/dist/topic-synthesizer.d.ts.map +1 -0
  144. package/dist/topic-synthesizer.js +315 -0
  145. package/dist/trigger-registry.d.ts +63 -0
  146. package/dist/trigger-registry.d.ts.map +1 -0
  147. package/dist/trigger-registry.js +163 -0
  148. package/dist/types.d.ts +537 -0
  149. package/dist/types.d.ts.map +1 -0
  150. package/dist/types.js +9 -0
  151. package/dist/vector-store.d.ts +170 -0
  152. package/dist/vector-store.d.ts.map +1 -0
  153. package/dist/vector-store.js +677 -0
  154. package/dist/version.d.ts +34 -0
  155. package/dist/version.d.ts.map +1 -0
  156. package/dist/version.js +34 -0
  157. package/dist/wiki-page-emitter.d.ts +65 -0
  158. package/dist/wiki-page-emitter.d.ts.map +1 -0
  159. package/dist/wiki-page-emitter.js +258 -0
  160. package/dist/work-store.d.ts +112 -0
  161. package/dist/work-store.d.ts.map +1 -0
  162. package/dist/work-store.js +273 -0
  163. package/package.json +4 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAqC3C,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,MAAM,CAAC;CACjB;AAQD;;;GAGG;AACH,iBAAS,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAI9C;AAED;;;GAGG;AACH,iBAAS,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAIvD;AAmBD,OAAO,EAAE,eAAe,EAAE,uBAAuB,EAAE,CAAC;AAEpD,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAmC;IAC9D,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAmC;IAC7D,OAAO,CAAC,SAAS,CAA6B;IAC9C,OAAO,CAAC,aAAa,CAAwB;IAE7C,6EAA6E;IAC7E,IAAI,YAAY,IAAI,OAAO,CAE1B;gBAEW,MAAM,CAAC,EAAE,OAAO,CAAC,qBAAqB,CAAC;IAKnD;;;OAGG;IACH,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,YAAY;IAiB3C;;;;OAIG;IACH,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI;IA0BjD;;;;;;OAMG;IACH,iBAAiB,IAAI,YAAY,GAAG,IAAI;IAsBxC;;;OAGG;IACH,YAAY,IAAI,YAAY;IAa5B;;;OAGG;IACH,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,YAAY;IAIzC;;OAEG;IACH,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAClC,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,GAAG,CAAC,EAAE,MAAM,CAAC;KACd,GAAG,IAAI;IA2BR;;OAEG;IACH,OAAO,CAAC,gBAAgB;IA+BxB;;OAEG;IACH,UAAU,IAAI,MAAM,EAAE;IAWtB;;OAEG;IACH,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM;IAKpC;;OAEG;IACH,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE;IAQzC;;OAEG;IACH,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM;IAWzC;;;;;;;;;;;;OAYG;IACH,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IA8C/C;;;;;;;OAOG;IACH,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QACnC,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,GAAG;QAAE,MAAM,EAAE,MAAM,GAAG,KAAK,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;IA4BzE;;OAEG;IACH,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,YAAY;IAc9D;;OAEG;IACH,KAAK,IAAI,IAAI;CAgBd"}
package/dist/db.js ADDED
@@ -0,0 +1,402 @@
1
+ /**
2
+ * hypermem Database Manager
3
+ *
4
+ * Three-file architecture per agent:
5
+ * agents/{agentId}/messages.db — write-heavy conversation log (rotatable)
6
+ * agents/{agentId}/vectors.db — search index (reconstructable)
7
+ * library.db — fleet-wide structured knowledge (crown jewel)
8
+ *
9
+ * Uses node:sqlite (built into Node 22+) for synchronous, zero-dependency access.
10
+ */
11
+ import { DatabaseSync } from 'node:sqlite';
12
+ import fs from 'node:fs';
13
+ import os from 'node:os';
14
+ import path from 'node:path';
15
+ import { migrate } from './schema.js';
16
+ import { migrateLibrary } from './library-schema.js';
17
+ import { ENGINE_VERSION } from './version.js';
18
+ // sqlite-vec extension loading — optional dependency
19
+ import { createRequire } from 'node:module';
20
+ let sqliteVecAvailable = null;
21
+ let sqliteVecLoad = null;
22
+ function loadSqliteVec(db) {
23
+ if (sqliteVecAvailable === null) {
24
+ try {
25
+ const require = createRequire(import.meta.url);
26
+ const mod = require('sqlite-vec');
27
+ sqliteVecLoad = mod.load;
28
+ sqliteVecAvailable = true;
29
+ }
30
+ catch {
31
+ sqliteVecAvailable = false;
32
+ sqliteVecLoad = null;
33
+ }
34
+ }
35
+ if (!sqliteVecAvailable || !sqliteVecLoad)
36
+ return false;
37
+ try {
38
+ sqliteVecLoad(db);
39
+ return true;
40
+ }
41
+ catch {
42
+ return false;
43
+ }
44
+ }
45
+ const DEFAULT_DATA_DIR = path.join(process.env.HOME || os.homedir(), '.openclaw', 'hypermem');
46
+ /**
47
+ * Validate agentId to prevent path traversal.
48
+ * Must match [a-z0-9][a-z0-9-]* (lowercase alphanumeric + hyphens, no dots or slashes).
49
+ */
50
+ function validateAgentId(agentId) {
51
+ if (!agentId || !/^[a-z0-9][a-z0-9-]*$/.test(agentId)) {
52
+ throw new Error(`Invalid agentId: "${agentId}". Must match [a-z0-9][a-z0-9-]*`);
53
+ }
54
+ }
55
+ /**
56
+ * Validate rotated DB filename to prevent path traversal.
57
+ * Must match the expected rotation pattern: messages_YYYYQN(_N)?.db
58
+ */
59
+ function validateRotatedFilename(filename) {
60
+ if (!/^messages_\d{4}Q[1-4](_\d+)?\.db$/.test(filename)) {
61
+ throw new Error(`Invalid rotated DB filename: "${filename}". Must match messages_YYYYQN.db pattern`);
62
+ }
63
+ }
64
+ /**
65
+ * Apply standard pragmas to a database connection.
66
+ */
67
+ function applyPragmas(db) {
68
+ db.exec('PRAGMA journal_mode = WAL');
69
+ db.exec('PRAGMA synchronous = NORMAL');
70
+ db.exec('PRAGMA foreign_keys = ON');
71
+ db.exec('PRAGMA busy_timeout = 5000');
72
+ }
73
+ /**
74
+ * Get the directory for an agent's databases.
75
+ */
76
+ function agentDir(dataDir, agentId) {
77
+ return path.join(dataDir, 'agents', agentId);
78
+ }
79
+ export { validateAgentId, validateRotatedFilename };
80
+ export class DatabaseManager {
81
+ dataDir;
82
+ messageDbs = new Map();
83
+ vectorDbs = new Map();
84
+ libraryDb = null;
85
+ _vecAvailable = null;
86
+ /** Whether sqlite-vec was successfully loaded on the most recent DB open. */
87
+ get vecAvailable() {
88
+ return this._vecAvailable === true;
89
+ }
90
+ constructor(config) {
91
+ this.dataDir = config?.dataDir || DEFAULT_DATA_DIR;
92
+ fs.mkdirSync(this.dataDir, { recursive: true });
93
+ }
94
+ /**
95
+ * Get or create the message database for an agent.
96
+ * This is the write-heavy, rotatable conversation log.
97
+ */
98
+ getMessageDb(agentId) {
99
+ validateAgentId(agentId);
100
+ let db = this.messageDbs.get(agentId);
101
+ if (db)
102
+ return db;
103
+ const dir = agentDir(this.dataDir, agentId);
104
+ fs.mkdirSync(dir, { recursive: true });
105
+ const dbPath = path.join(dir, 'messages.db');
106
+ db = new DatabaseSync(dbPath);
107
+ applyPragmas(db);
108
+ migrate(db);
109
+ this.messageDbs.set(agentId, db);
110
+ return db;
111
+ }
112
+ /**
113
+ * Get or create the vector database for an agent.
114
+ * This is the search index — fully reconstructable.
115
+ * Returns null if sqlite-vec is not available.
116
+ */
117
+ getVectorDb(agentId) {
118
+ validateAgentId(agentId);
119
+ let db = this.vectorDbs.get(agentId);
120
+ if (db)
121
+ return db;
122
+ const dir = agentDir(this.dataDir, agentId);
123
+ fs.mkdirSync(dir, { recursive: true });
124
+ const dbPath = path.join(dir, 'vectors.db');
125
+ db = new DatabaseSync(dbPath, { allowExtension: true });
126
+ applyPragmas(db);
127
+ const vecLoaded = loadSqliteVec(db);
128
+ this._vecAvailable = vecLoaded;
129
+ if (!vecLoaded) {
130
+ // Close and don't cache — no point without vec extension
131
+ try {
132
+ db.close();
133
+ }
134
+ catch { /* ignore */ }
135
+ return null;
136
+ }
137
+ // Create vector tables (managed by VectorStore, but we ensure the DB is ready)
138
+ this.vectorDbs.set(agentId, db);
139
+ return db;
140
+ }
141
+ /**
142
+ * Get or create the shared (fleet-wide) vector database.
143
+ * Unlike per-agent vector DBs, this is a single vectors.db at the root of dataDir,
144
+ * shared across all agents. Facts and episodes from all agents are indexed together,
145
+ * keyed by (source_table, source_id) in vec_index_map.
146
+ * Returns null if sqlite-vec is not available.
147
+ */
148
+ getSharedVectorDb() {
149
+ const sharedKey = '__shared__';
150
+ let db = this.vectorDbs.get(sharedKey);
151
+ if (db)
152
+ return db;
153
+ const dbPath = path.join(this.dataDir, 'vectors.db');
154
+ db = new DatabaseSync(dbPath, { allowExtension: true });
155
+ applyPragmas(db);
156
+ const vecLoaded = loadSqliteVec(db);
157
+ this._vecAvailable = vecLoaded;
158
+ if (!vecLoaded) {
159
+ try {
160
+ db.close();
161
+ }
162
+ catch { /* ignore */ }
163
+ return null;
164
+ }
165
+ this.vectorDbs.set(sharedKey, db);
166
+ return db;
167
+ }
168
+ /**
169
+ * Get or create the shared library database.
170
+ * This is the fleet-wide knowledge store — the crown jewel.
171
+ */
172
+ getLibraryDb() {
173
+ if (this.libraryDb)
174
+ return this.libraryDb;
175
+ const dbPath = path.join(this.dataDir, 'library.db');
176
+ this.libraryDb = new DatabaseSync(dbPath);
177
+ applyPragmas(this.libraryDb);
178
+ migrateLibrary(this.libraryDb, ENGINE_VERSION);
179
+ return this.libraryDb;
180
+ }
181
+ // ── Legacy compatibility ──────────────────────────────────────
182
+ /**
183
+ * @deprecated Use getMessageDb() instead. Kept for migration period.
184
+ * Maps to getMessageDb() for backward compatibility.
185
+ */
186
+ getAgentDb(agentId) {
187
+ return this.getMessageDb(agentId);
188
+ }
189
+ /**
190
+ * Ensure agent metadata exists in the message DB.
191
+ */
192
+ ensureAgent(agentId, meta) {
193
+ validateAgentId(agentId);
194
+ const db = this.getMessageDb(agentId);
195
+ const now = new Date().toISOString();
196
+ const existing = db
197
+ .prepare('SELECT id FROM agent_meta WHERE id = ?')
198
+ .get(agentId);
199
+ if (!existing) {
200
+ db.prepare(`
201
+ INSERT INTO agent_meta (id, display_name, tier, org, created_at, updated_at)
202
+ VALUES (?, ?, ?, ?, ?, ?)
203
+ `).run(agentId, meta?.displayName || agentId, meta?.tier || 'unknown', meta?.org || 'unknown', now, now);
204
+ }
205
+ // Also register in fleet registry (library)
206
+ this.ensureFleetAgent(agentId, meta);
207
+ }
208
+ /**
209
+ * Ensure agent exists in the fleet registry (library DB).
210
+ */
211
+ ensureFleetAgent(agentId, meta) {
212
+ const db = this.getLibraryDb();
213
+ const now = new Date().toISOString();
214
+ const existing = db
215
+ .prepare('SELECT id FROM fleet_agents WHERE id = ?')
216
+ .get(agentId);
217
+ if (!existing) {
218
+ db.prepare(`
219
+ INSERT INTO fleet_agents (id, display_name, tier, org_id, status, created_at, updated_at)
220
+ VALUES (?, ?, ?, ?, 'active', ?, ?)
221
+ `).run(agentId, meta?.displayName || agentId, meta?.tier || 'unknown', meta?.org || null, now, now);
222
+ }
223
+ else {
224
+ // Update last_seen
225
+ db.prepare('UPDATE fleet_agents SET last_seen = ?, updated_at = ? WHERE id = ?')
226
+ .run(now, now, agentId);
227
+ }
228
+ }
229
+ /**
230
+ * List all agents with message databases.
231
+ */
232
+ listAgents() {
233
+ const agentsDir = path.join(this.dataDir, 'agents');
234
+ if (!fs.existsSync(agentsDir))
235
+ return [];
236
+ const VALID_AGENT_ID = /^[a-z0-9][a-z0-9-]*$/;
237
+ return fs.readdirSync(agentsDir).filter(f => {
238
+ if (!VALID_AGENT_ID.test(f))
239
+ return false;
240
+ const stat = fs.statSync(path.join(agentsDir, f));
241
+ return stat.isDirectory();
242
+ });
243
+ }
244
+ /**
245
+ * Get the path to an agent's directory.
246
+ */
247
+ getAgentDir(agentId) {
248
+ validateAgentId(agentId);
249
+ return agentDir(this.dataDir, agentId);
250
+ }
251
+ /**
252
+ * List rotated message DB files for an agent.
253
+ */
254
+ listRotatedDbs(agentId) {
255
+ const dir = agentDir(this.dataDir, agentId);
256
+ if (!fs.existsSync(dir))
257
+ return [];
258
+ return fs.readdirSync(dir)
259
+ .filter(f => f.startsWith('messages_') && f.endsWith('.db'))
260
+ .sort();
261
+ }
262
+ /**
263
+ * Get the size of the active messages.db for an agent (in bytes).
264
+ */
265
+ getMessageDbSize(agentId) {
266
+ const dir = agentDir(this.dataDir, agentId);
267
+ const dbPath = path.join(dir, 'messages.db');
268
+ try {
269
+ const stat = fs.statSync(dbPath);
270
+ return stat.size;
271
+ }
272
+ catch {
273
+ return 0;
274
+ }
275
+ }
276
+ /**
277
+ * Rotate the message database for an agent.
278
+ *
279
+ * 1. Closes the active messages.db connection
280
+ * 2. Renames messages.db → messages_{YYYYQN}.db (e.g., messages_2026Q1.db)
281
+ * 3. Removes associated WAL/SHM files
282
+ * 4. Next call to getMessageDb() creates a fresh database
283
+ *
284
+ * The rotated file is read-only archive material. The vector index
285
+ * retains references to it via source_db in vec_index_map.
286
+ *
287
+ * Returns the path to the rotated file, or null if rotation wasn't needed.
288
+ */
289
+ rotateMessageDb(agentId) {
290
+ const dir = agentDir(this.dataDir, agentId);
291
+ const activePath = path.join(dir, 'messages.db');
292
+ if (!fs.existsSync(activePath))
293
+ return null;
294
+ // Close the active connection first
295
+ const existingDb = this.messageDbs.get(agentId);
296
+ if (existingDb) {
297
+ try {
298
+ // Checkpoint WAL into the main database before rotating
299
+ existingDb.exec('PRAGMA wal_checkpoint(TRUNCATE)');
300
+ existingDb.close();
301
+ }
302
+ catch { /* ignore */ }
303
+ this.messageDbs.delete(agentId);
304
+ }
305
+ // Generate rotation name: messages_YYYYQN.db
306
+ const now = new Date();
307
+ const year = now.getFullYear();
308
+ const quarter = Math.ceil((now.getMonth() + 1) / 3);
309
+ let rotatedName = `messages_${year}Q${quarter}.db`;
310
+ let rotatedPath = path.join(dir, rotatedName);
311
+ // Handle collision — append a suffix if this quarter already has a rotation
312
+ let suffix = 1;
313
+ while (fs.existsSync(rotatedPath)) {
314
+ rotatedName = `messages_${year}Q${quarter}_${suffix}.db`;
315
+ rotatedPath = path.join(dir, rotatedName);
316
+ suffix++;
317
+ }
318
+ // Rename the active DB to the rotated name
319
+ fs.renameSync(activePath, rotatedPath);
320
+ // Clean up WAL and SHM files
321
+ for (const ext of ['-wal', '-shm']) {
322
+ const walPath = activePath + ext;
323
+ if (fs.existsSync(walPath)) {
324
+ try {
325
+ fs.unlinkSync(walPath);
326
+ }
327
+ catch { /* ignore */ }
328
+ }
329
+ }
330
+ return rotatedPath;
331
+ }
332
+ /**
333
+ * Check if an agent's message database needs rotation.
334
+ * Triggers on:
335
+ * - Size exceeds threshold (default 100MB)
336
+ * - Time since creation exceeds threshold (default 90 days)
337
+ *
338
+ * Returns the reason for rotation, or null if no rotation needed.
339
+ */
340
+ shouldRotate(agentId, opts) {
341
+ const maxSize = opts?.maxSizeBytes ?? 100 * 1024 * 1024; // 100MB
342
+ const maxAge = opts?.maxAgeDays ?? 90;
343
+ // Check size
344
+ const size = this.getMessageDbSize(agentId);
345
+ if (size > maxSize) {
346
+ return { reason: 'size', current: size, threshold: maxSize };
347
+ }
348
+ // Check age — look at the earliest conversation in the DB
349
+ const db = this.getMessageDb(agentId);
350
+ const oldest = db.prepare('SELECT MIN(created_at) as earliest FROM conversations').get();
351
+ if (oldest?.earliest) {
352
+ const created = new Date(oldest.earliest);
353
+ const ageMs = Date.now() - created.getTime();
354
+ const ageDays = ageMs / (1000 * 60 * 60 * 24);
355
+ if (ageDays > maxAge) {
356
+ return { reason: 'age', current: Math.round(ageDays), threshold: maxAge };
357
+ }
358
+ }
359
+ return null;
360
+ }
361
+ /**
362
+ * Open a rotated message database as read-only for querying.
363
+ */
364
+ openRotatedDb(agentId, filename) {
365
+ validateAgentId(agentId);
366
+ validateRotatedFilename(filename);
367
+ const dir = agentDir(this.dataDir, agentId);
368
+ const dbPath = path.join(dir, filename);
369
+ if (!fs.existsSync(dbPath)) {
370
+ throw new Error(`Rotated DB not found: ${dbPath}`);
371
+ }
372
+ const db = new DatabaseSync(dbPath, { readOnly: true });
373
+ return db;
374
+ }
375
+ /**
376
+ * Close all open database connections.
377
+ */
378
+ close() {
379
+ for (const [, db] of this.messageDbs) {
380
+ try {
381
+ db.close();
382
+ }
383
+ catch { /* ignore */ }
384
+ }
385
+ this.messageDbs.clear();
386
+ for (const [, db] of this.vectorDbs) {
387
+ try {
388
+ db.close();
389
+ }
390
+ catch { /* ignore */ }
391
+ }
392
+ this.vectorDbs.clear();
393
+ if (this.libraryDb) {
394
+ try {
395
+ this.libraryDb.close();
396
+ }
397
+ catch { /* ignore */ }
398
+ this.libraryDb = null;
399
+ }
400
+ }
401
+ }
402
+ //# sourceMappingURL=db.js.map
@@ -0,0 +1,100 @@
1
+ /**
2
+ * hypermem Agent Desired State Store
3
+ *
4
+ * Stores intended configuration for each agent and tracks drift.
5
+ * Enables fleet-wide config visibility and enforcement.
6
+ *
7
+ * Config keys are dot-path strings matching openclaw.json structure:
8
+ * model, thinkingDefault, provider, workspace, tools.exec.host, etc.
9
+ *
10
+ * Drift statuses:
11
+ * - 'ok' — actual matches desired
12
+ * - 'drifted' — actual differs from desired
13
+ * - 'unknown' — not yet checked
14
+ * - 'error' — check failed
15
+ */
16
+ import type { DatabaseSync } from 'node:sqlite';
17
+ export type DriftStatus = 'ok' | 'drifted' | 'unknown' | 'error';
18
+ export interface DesiredStateEntry {
19
+ agentId: string;
20
+ configKey: string;
21
+ desiredValue: unknown;
22
+ actualValue: unknown | null;
23
+ source: string;
24
+ setBy: string | null;
25
+ driftStatus: DriftStatus;
26
+ lastChecked: string | null;
27
+ createdAt: string;
28
+ updatedAt: string;
29
+ notes: string | null;
30
+ }
31
+ export interface ConfigEvent {
32
+ id: number;
33
+ agentId: string;
34
+ configKey: string;
35
+ eventType: string;
36
+ oldValue: unknown | null;
37
+ newValue: unknown | null;
38
+ changedBy: string | null;
39
+ createdAt: string;
40
+ }
41
+ export declare class DesiredStateStore {
42
+ private readonly db;
43
+ constructor(db: DatabaseSync);
44
+ /**
45
+ * Set desired state for a config key on an agent.
46
+ */
47
+ setDesired(agentId: string, configKey: string, desiredValue: unknown, opts?: {
48
+ source?: string;
49
+ setBy?: string;
50
+ notes?: string;
51
+ }): DesiredStateEntry;
52
+ /**
53
+ * Report actual state observed at runtime.
54
+ * Compares against desired and updates drift status.
55
+ */
56
+ reportActual(agentId: string, configKey: string, actualValue: unknown): DriftStatus;
57
+ /**
58
+ * Bulk report actual state for an agent (e.g., on heartbeat).
59
+ */
60
+ reportActualBulk(agentId: string, actuals: Record<string, unknown>): Record<string, DriftStatus>;
61
+ /**
62
+ * Get a specific desired state entry.
63
+ */
64
+ getEntry(agentId: string, configKey: string): DesiredStateEntry | null;
65
+ /**
66
+ * Get all desired state for an agent.
67
+ */
68
+ getAgentState(agentId: string): DesiredStateEntry[];
69
+ /**
70
+ * Get desired state as a flat config object (key → value).
71
+ */
72
+ getAgentConfig(agentId: string): Record<string, unknown>;
73
+ /**
74
+ * Get all drifted entries across the fleet.
75
+ */
76
+ getDrifted(): DesiredStateEntry[];
77
+ /**
78
+ * Get fleet-wide view of a specific config key.
79
+ */
80
+ getFleetConfig(configKey: string): DesiredStateEntry[];
81
+ /**
82
+ * Get config change history for an agent/key.
83
+ */
84
+ getHistory(agentId: string, configKey?: string, limit?: number): ConfigEvent[];
85
+ /**
86
+ * Remove a desired state entry.
87
+ */
88
+ removeDesired(agentId: string, configKey: string, removedBy?: string): void;
89
+ /**
90
+ * Get fleet drift summary.
91
+ */
92
+ getDriftSummary(): {
93
+ total: number;
94
+ ok: number;
95
+ drifted: number;
96
+ unknown: number;
97
+ error: number;
98
+ };
99
+ }
100
+ //# sourceMappingURL=desired-state-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"desired-state-store.d.ts","sourceRoot":"","sources":["../src/desired-state-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAMhD,MAAM,MAAM,WAAW,GAAG,IAAI,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,CAAC;AAEjE,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,OAAO,CAAC;IACtB,WAAW,EAAE,OAAO,GAAG,IAAI,CAAC;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,WAAW,EAAE,WAAW,CAAC;IACzB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,OAAO,GAAG,IAAI,CAAC;IACzB,QAAQ,EAAE,OAAO,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;CACnB;AAyCD,qBAAa,iBAAiB;IAChB,OAAO,CAAC,QAAQ,CAAC,EAAE;gBAAF,EAAE,EAAE,YAAY;IAE7C;;OAEG;IACH,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE;QAC3E,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,GAAG,iBAAiB;IA8DrB;;;OAGG;IACH,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,OAAO,GAAG,WAAW;IA6BnF;;OAEG;IACH,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC;IAQhG;;OAEG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,iBAAiB,GAAG,IAAI;IAQtE;;OAEG;IACH,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,iBAAiB,EAAE;IAQnD;;OAEG;IACH,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IASxD;;OAEG;IACH,UAAU,IAAI,iBAAiB,EAAE;IAQjC;;OAEG;IACH,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,iBAAiB,EAAE;IAQtD;;OAEG;IACH,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,EAAE,KAAK,GAAE,MAAW,GAAG,WAAW,EAAE;IAgBlF;;OAEG;IACH,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI;IAgB3E;;OAEG;IACH,eAAe,IAAI;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE;CAelG"}