@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.
- package/ARCHITECTURE.md +12 -3
- package/README.md +30 -6
- package/bin/hypermem-status.mjs +166 -0
- package/dist/background-indexer.d.ts +132 -0
- package/dist/background-indexer.d.ts.map +1 -0
- package/dist/background-indexer.js +1044 -0
- package/dist/cache.d.ts +110 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +495 -0
- package/dist/compaction-fence.d.ts +89 -0
- package/dist/compaction-fence.d.ts.map +1 -0
- package/dist/compaction-fence.js +153 -0
- package/dist/compositor.d.ts +226 -0
- package/dist/compositor.d.ts.map +1 -0
- package/dist/compositor.js +2558 -0
- package/dist/content-type-classifier.d.ts +41 -0
- package/dist/content-type-classifier.d.ts.map +1 -0
- package/dist/content-type-classifier.js +181 -0
- package/dist/cross-agent.d.ts +62 -0
- package/dist/cross-agent.d.ts.map +1 -0
- package/dist/cross-agent.js +259 -0
- package/dist/db.d.ts +131 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +402 -0
- package/dist/desired-state-store.d.ts +100 -0
- package/dist/desired-state-store.d.ts.map +1 -0
- package/dist/desired-state-store.js +222 -0
- package/dist/doc-chunk-store.d.ts +140 -0
- package/dist/doc-chunk-store.d.ts.map +1 -0
- package/dist/doc-chunk-store.js +391 -0
- package/dist/doc-chunker.d.ts +99 -0
- package/dist/doc-chunker.d.ts.map +1 -0
- package/dist/doc-chunker.js +324 -0
- package/dist/dreaming-promoter.d.ts +86 -0
- package/dist/dreaming-promoter.d.ts.map +1 -0
- package/dist/dreaming-promoter.js +381 -0
- package/dist/episode-store.d.ts +49 -0
- package/dist/episode-store.d.ts.map +1 -0
- package/dist/episode-store.js +135 -0
- package/dist/fact-store.d.ts +75 -0
- package/dist/fact-store.d.ts.map +1 -0
- package/dist/fact-store.js +236 -0
- package/dist/fleet-store.d.ts +144 -0
- package/dist/fleet-store.d.ts.map +1 -0
- package/dist/fleet-store.js +276 -0
- package/dist/fos-mod.d.ts +178 -0
- package/dist/fos-mod.d.ts.map +1 -0
- package/dist/fos-mod.js +416 -0
- package/dist/hybrid-retrieval.d.ts +64 -0
- package/dist/hybrid-retrieval.d.ts.map +1 -0
- package/dist/hybrid-retrieval.js +344 -0
- package/dist/image-eviction.d.ts +49 -0
- package/dist/image-eviction.d.ts.map +1 -0
- package/dist/image-eviction.js +251 -0
- package/dist/index.d.ts +650 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1072 -0
- package/dist/keystone-scorer.d.ts +51 -0
- package/dist/keystone-scorer.d.ts.map +1 -0
- package/dist/keystone-scorer.js +52 -0
- package/dist/knowledge-graph.d.ts +110 -0
- package/dist/knowledge-graph.d.ts.map +1 -0
- package/dist/knowledge-graph.js +305 -0
- package/dist/knowledge-lint.d.ts +29 -0
- package/dist/knowledge-lint.d.ts.map +1 -0
- package/dist/knowledge-lint.js +116 -0
- package/dist/knowledge-store.d.ts +72 -0
- package/dist/knowledge-store.d.ts.map +1 -0
- package/dist/knowledge-store.js +247 -0
- package/dist/library-schema.d.ts +22 -0
- package/dist/library-schema.d.ts.map +1 -0
- package/dist/library-schema.js +1038 -0
- package/dist/message-store.d.ts +89 -0
- package/dist/message-store.d.ts.map +1 -0
- package/dist/message-store.js +323 -0
- package/dist/metrics-dashboard.d.ts +114 -0
- package/dist/metrics-dashboard.d.ts.map +1 -0
- package/dist/metrics-dashboard.js +260 -0
- package/dist/obsidian-exporter.d.ts +57 -0
- package/dist/obsidian-exporter.d.ts.map +1 -0
- package/dist/obsidian-exporter.js +274 -0
- package/dist/obsidian-watcher.d.ts +147 -0
- package/dist/obsidian-watcher.d.ts.map +1 -0
- package/dist/obsidian-watcher.js +403 -0
- package/dist/open-domain.d.ts +46 -0
- package/dist/open-domain.d.ts.map +1 -0
- package/dist/open-domain.js +125 -0
- package/dist/preference-store.d.ts +54 -0
- package/dist/preference-store.d.ts.map +1 -0
- package/dist/preference-store.js +109 -0
- package/dist/preservation-gate.d.ts +82 -0
- package/dist/preservation-gate.d.ts.map +1 -0
- package/dist/preservation-gate.js +150 -0
- package/dist/proactive-pass.d.ts +63 -0
- package/dist/proactive-pass.d.ts.map +1 -0
- package/dist/proactive-pass.js +239 -0
- package/dist/profiles.d.ts +44 -0
- package/dist/profiles.d.ts.map +1 -0
- package/dist/profiles.js +227 -0
- package/dist/provider-translator.d.ts +50 -0
- package/dist/provider-translator.d.ts.map +1 -0
- package/dist/provider-translator.js +403 -0
- package/dist/rate-limiter.d.ts +76 -0
- package/dist/rate-limiter.d.ts.map +1 -0
- package/dist/rate-limiter.js +179 -0
- package/dist/repair-tool-pairs.d.ts +38 -0
- package/dist/repair-tool-pairs.d.ts.map +1 -0
- package/dist/repair-tool-pairs.js +138 -0
- package/dist/retrieval-policy.d.ts +51 -0
- package/dist/retrieval-policy.d.ts.map +1 -0
- package/dist/retrieval-policy.js +77 -0
- package/dist/schema.d.ts +15 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +229 -0
- package/dist/secret-scanner.d.ts +51 -0
- package/dist/secret-scanner.d.ts.map +1 -0
- package/dist/secret-scanner.js +248 -0
- package/dist/seed.d.ts +108 -0
- package/dist/seed.d.ts.map +1 -0
- package/dist/seed.js +177 -0
- package/dist/session-flusher.d.ts +53 -0
- package/dist/session-flusher.d.ts.map +1 -0
- package/dist/session-flusher.js +69 -0
- package/dist/session-topic-map.d.ts +41 -0
- package/dist/session-topic-map.d.ts.map +1 -0
- package/dist/session-topic-map.js +77 -0
- package/dist/spawn-context.d.ts +54 -0
- package/dist/spawn-context.d.ts.map +1 -0
- package/dist/spawn-context.js +159 -0
- package/dist/system-store.d.ts +73 -0
- package/dist/system-store.d.ts.map +1 -0
- package/dist/system-store.js +182 -0
- package/dist/temporal-store.d.ts +80 -0
- package/dist/temporal-store.d.ts.map +1 -0
- package/dist/temporal-store.js +149 -0
- package/dist/topic-detector.d.ts +35 -0
- package/dist/topic-detector.d.ts.map +1 -0
- package/dist/topic-detector.js +249 -0
- package/dist/topic-store.d.ts +45 -0
- package/dist/topic-store.d.ts.map +1 -0
- package/dist/topic-store.js +136 -0
- package/dist/topic-synthesizer.d.ts +51 -0
- package/dist/topic-synthesizer.d.ts.map +1 -0
- package/dist/topic-synthesizer.js +315 -0
- package/dist/trigger-registry.d.ts +63 -0
- package/dist/trigger-registry.d.ts.map +1 -0
- package/dist/trigger-registry.js +163 -0
- package/dist/types.d.ts +537 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +9 -0
- package/dist/vector-store.d.ts +170 -0
- package/dist/vector-store.d.ts.map +1 -0
- package/dist/vector-store.js +677 -0
- package/dist/version.d.ts +34 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +34 -0
- package/dist/wiki-page-emitter.d.ts +65 -0
- package/dist/wiki-page-emitter.d.ts.map +1 -0
- package/dist/wiki-page-emitter.js +258 -0
- package/dist/work-store.d.ts +112 -0
- package/dist/work-store.d.ts.map +1 -0
- package/dist/work-store.js +273 -0
- package/package.json +4 -1
package/dist/db.d.ts.map
ADDED
|
@@ -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"}
|