@dotsetlabs/dotclaw 1.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 (170) hide show
  1. package/.env.example +54 -0
  2. package/LICENSE +21 -0
  3. package/README.md +111 -0
  4. package/config-examples/groups/global/CLAUDE.md +21 -0
  5. package/config-examples/groups/main/CLAUDE.md +47 -0
  6. package/config-examples/mount-allowlist.json +25 -0
  7. package/config-examples/plugin-http.json +18 -0
  8. package/config-examples/runtime.json +30 -0
  9. package/config-examples/tool-budgets.json +24 -0
  10. package/config-examples/tool-policy.json +51 -0
  11. package/container/.dockerignore +6 -0
  12. package/container/Dockerfile +74 -0
  13. package/container/agent-runner/package-lock.json +92 -0
  14. package/container/agent-runner/package.json +20 -0
  15. package/container/agent-runner/src/agent-config.ts +295 -0
  16. package/container/agent-runner/src/container-protocol.ts +73 -0
  17. package/container/agent-runner/src/daemon.ts +91 -0
  18. package/container/agent-runner/src/index.ts +1428 -0
  19. package/container/agent-runner/src/ipc.ts +321 -0
  20. package/container/agent-runner/src/memory.ts +336 -0
  21. package/container/agent-runner/src/prompt-packs.ts +341 -0
  22. package/container/agent-runner/src/tools.ts +1720 -0
  23. package/container/agent-runner/tsconfig.json +19 -0
  24. package/container/build.sh +23 -0
  25. package/container/skills/agent-browser.md +159 -0
  26. package/dist/admin-commands.d.ts +7 -0
  27. package/dist/admin-commands.d.ts.map +1 -0
  28. package/dist/admin-commands.js +87 -0
  29. package/dist/admin-commands.js.map +1 -0
  30. package/dist/agent-context.d.ts +42 -0
  31. package/dist/agent-context.d.ts.map +1 -0
  32. package/dist/agent-context.js +92 -0
  33. package/dist/agent-context.js.map +1 -0
  34. package/dist/agent-execution.d.ts +68 -0
  35. package/dist/agent-execution.d.ts.map +1 -0
  36. package/dist/agent-execution.js +169 -0
  37. package/dist/agent-execution.js.map +1 -0
  38. package/dist/agent-semaphore.d.ts +2 -0
  39. package/dist/agent-semaphore.d.ts.map +1 -0
  40. package/dist/agent-semaphore.js +52 -0
  41. package/dist/agent-semaphore.js.map +1 -0
  42. package/dist/behavior-config.d.ts +14 -0
  43. package/dist/behavior-config.d.ts.map +1 -0
  44. package/dist/behavior-config.js +52 -0
  45. package/dist/behavior-config.js.map +1 -0
  46. package/dist/cli.d.ts +3 -0
  47. package/dist/cli.d.ts.map +1 -0
  48. package/dist/cli.js +626 -0
  49. package/dist/cli.js.map +1 -0
  50. package/dist/config.d.ts +31 -0
  51. package/dist/config.d.ts.map +1 -0
  52. package/dist/config.js +38 -0
  53. package/dist/config.js.map +1 -0
  54. package/dist/container-protocol.d.ts +72 -0
  55. package/dist/container-protocol.d.ts.map +1 -0
  56. package/dist/container-protocol.js +3 -0
  57. package/dist/container-protocol.js.map +1 -0
  58. package/dist/container-runner.d.ts +59 -0
  59. package/dist/container-runner.d.ts.map +1 -0
  60. package/dist/container-runner.js +813 -0
  61. package/dist/container-runner.js.map +1 -0
  62. package/dist/cost.d.ts +9 -0
  63. package/dist/cost.d.ts.map +1 -0
  64. package/dist/cost.js +11 -0
  65. package/dist/cost.js.map +1 -0
  66. package/dist/dashboard.d.ts +58 -0
  67. package/dist/dashboard.d.ts.map +1 -0
  68. package/dist/dashboard.js +471 -0
  69. package/dist/dashboard.js.map +1 -0
  70. package/dist/db.d.ts +99 -0
  71. package/dist/db.d.ts.map +1 -0
  72. package/dist/db.js +423 -0
  73. package/dist/db.js.map +1 -0
  74. package/dist/error-messages.d.ts +17 -0
  75. package/dist/error-messages.d.ts.map +1 -0
  76. package/dist/error-messages.js +109 -0
  77. package/dist/error-messages.js.map +1 -0
  78. package/dist/index.d.ts +2 -0
  79. package/dist/index.d.ts.map +1 -0
  80. package/dist/index.js +2072 -0
  81. package/dist/index.js.map +1 -0
  82. package/dist/locks.d.ts +2 -0
  83. package/dist/locks.d.ts.map +1 -0
  84. package/dist/locks.js +26 -0
  85. package/dist/locks.js.map +1 -0
  86. package/dist/logger.d.ts +4 -0
  87. package/dist/logger.d.ts.map +1 -0
  88. package/dist/logger.js +15 -0
  89. package/dist/logger.js.map +1 -0
  90. package/dist/maintenance.d.ts +13 -0
  91. package/dist/maintenance.d.ts.map +1 -0
  92. package/dist/maintenance.js +151 -0
  93. package/dist/maintenance.js.map +1 -0
  94. package/dist/memory-embeddings.d.ts +13 -0
  95. package/dist/memory-embeddings.d.ts.map +1 -0
  96. package/dist/memory-embeddings.js +126 -0
  97. package/dist/memory-embeddings.js.map +1 -0
  98. package/dist/memory-recall.d.ts +8 -0
  99. package/dist/memory-recall.d.ts.map +1 -0
  100. package/dist/memory-recall.js +127 -0
  101. package/dist/memory-recall.js.map +1 -0
  102. package/dist/memory-store.d.ts +149 -0
  103. package/dist/memory-store.d.ts.map +1 -0
  104. package/dist/memory-store.js +787 -0
  105. package/dist/memory-store.js.map +1 -0
  106. package/dist/metrics.d.ts +12 -0
  107. package/dist/metrics.d.ts.map +1 -0
  108. package/dist/metrics.js +134 -0
  109. package/dist/metrics.js.map +1 -0
  110. package/dist/model-registry.d.ts +67 -0
  111. package/dist/model-registry.d.ts.map +1 -0
  112. package/dist/model-registry.js +230 -0
  113. package/dist/model-registry.js.map +1 -0
  114. package/dist/mount-security.d.ts +37 -0
  115. package/dist/mount-security.d.ts.map +1 -0
  116. package/dist/mount-security.js +284 -0
  117. package/dist/mount-security.js.map +1 -0
  118. package/dist/paths.d.ts +80 -0
  119. package/dist/paths.d.ts.map +1 -0
  120. package/dist/paths.js +149 -0
  121. package/dist/paths.js.map +1 -0
  122. package/dist/personalization.d.ts +6 -0
  123. package/dist/personalization.d.ts.map +1 -0
  124. package/dist/personalization.js +180 -0
  125. package/dist/personalization.js.map +1 -0
  126. package/dist/progress.d.ts +15 -0
  127. package/dist/progress.d.ts.map +1 -0
  128. package/dist/progress.js +92 -0
  129. package/dist/progress.js.map +1 -0
  130. package/dist/runtime-config.d.ts +227 -0
  131. package/dist/runtime-config.d.ts.map +1 -0
  132. package/dist/runtime-config.js +297 -0
  133. package/dist/runtime-config.js.map +1 -0
  134. package/dist/task-scheduler.d.ts +9 -0
  135. package/dist/task-scheduler.d.ts.map +1 -0
  136. package/dist/task-scheduler.js +195 -0
  137. package/dist/task-scheduler.js.map +1 -0
  138. package/dist/telegram-format.d.ts +3 -0
  139. package/dist/telegram-format.d.ts.map +1 -0
  140. package/dist/telegram-format.js +200 -0
  141. package/dist/telegram-format.js.map +1 -0
  142. package/dist/tool-budgets.d.ts +16 -0
  143. package/dist/tool-budgets.d.ts.map +1 -0
  144. package/dist/tool-budgets.js +83 -0
  145. package/dist/tool-budgets.js.map +1 -0
  146. package/dist/tool-policy.d.ts +18 -0
  147. package/dist/tool-policy.d.ts.map +1 -0
  148. package/dist/tool-policy.js +84 -0
  149. package/dist/tool-policy.js.map +1 -0
  150. package/dist/trace-writer.d.ts +39 -0
  151. package/dist/trace-writer.d.ts.map +1 -0
  152. package/dist/trace-writer.js +27 -0
  153. package/dist/trace-writer.js.map +1 -0
  154. package/dist/types.d.ts +81 -0
  155. package/dist/types.d.ts.map +1 -0
  156. package/dist/types.js +2 -0
  157. package/dist/types.js.map +1 -0
  158. package/dist/utils.d.ts +4 -0
  159. package/dist/utils.d.ts.map +1 -0
  160. package/dist/utils.js +30 -0
  161. package/dist/utils.js.map +1 -0
  162. package/launchd/com.dotclaw.plist +32 -0
  163. package/package.json +89 -0
  164. package/scripts/autotune.js +53 -0
  165. package/scripts/bootstrap.js +348 -0
  166. package/scripts/configure.js +200 -0
  167. package/scripts/doctor.js +164 -0
  168. package/scripts/init.js +209 -0
  169. package/scripts/install.sh +219 -0
  170. package/systemd/dotclaw.service +22 -0
@@ -0,0 +1,787 @@
1
+ import Database from 'better-sqlite3';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import crypto from 'crypto';
5
+ import { MAIN_GROUP_FOLDER, STORE_DIR } from './config.js';
6
+ import { loadRuntimeConfig } from './runtime-config.js';
7
+ const MEMORY_DB_PATH = path.join(STORE_DIR, 'memory.db');
8
+ let memoryDb = null;
9
+ let ftsEnabled = true;
10
+ const MEMORY_SCHEMA_VERSION = 3;
11
+ function getUserVersion(db) {
12
+ const version = db.pragma('user_version', { simple: true });
13
+ return Number.isFinite(version) ? version : 0;
14
+ }
15
+ function setUserVersion(db, version) {
16
+ db.pragma(`user_version = ${Math.max(0, Math.floor(version))}`);
17
+ }
18
+ function getMemoryColumns(db) {
19
+ const rows = db.prepare(`PRAGMA table_info(memory_items)`).all();
20
+ const columns = new Set();
21
+ for (const row of rows) {
22
+ if (row?.name)
23
+ columns.add(String(row.name));
24
+ }
25
+ return columns;
26
+ }
27
+ function ensureColumn(db, columns, name, ddl) {
28
+ if (columns.has(name))
29
+ return;
30
+ db.exec(ddl);
31
+ columns.add(name);
32
+ }
33
+ function ensureMemorySchema(db) {
34
+ const currentVersion = getUserVersion(db);
35
+ const columns = getMemoryColumns(db);
36
+ if (columns.size === 0) {
37
+ // Table might not exist yet; caller should have created it.
38
+ return;
39
+ }
40
+ ensureColumn(db, columns, 'embedding_json', `ALTER TABLE memory_items ADD COLUMN embedding_json TEXT`);
41
+ ensureColumn(db, columns, 'embedding_model', `ALTER TABLE memory_items ADD COLUMN embedding_model TEXT`);
42
+ ensureColumn(db, columns, 'embedding_updated_at', `ALTER TABLE memory_items ADD COLUMN embedding_updated_at TEXT`);
43
+ ensureColumn(db, columns, 'kind', `ALTER TABLE memory_items ADD COLUMN kind TEXT`);
44
+ ensureColumn(db, columns, 'conflict_key', `ALTER TABLE memory_items ADD COLUMN conflict_key TEXT`);
45
+ ensureColumn(db, columns, 'access_count', `ALTER TABLE memory_items ADD COLUMN access_count INTEGER DEFAULT 0`);
46
+ ensureColumn(db, columns, 'priority', `ALTER TABLE memory_items ADD COLUMN priority TEXT DEFAULT 'normal'`);
47
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_memory_conflict ON memory_items(group_folder, scope, subject_id, type, conflict_key)`);
48
+ db.exec(`
49
+ CREATE TABLE IF NOT EXISTS memory_meta (
50
+ key TEXT PRIMARY KEY,
51
+ value TEXT
52
+ );
53
+ `);
54
+ if (currentVersion !== MEMORY_SCHEMA_VERSION) {
55
+ setUserVersion(db, MEMORY_SCHEMA_VERSION);
56
+ }
57
+ }
58
+ function getMemoryMeta(db, key) {
59
+ const row = db.prepare(`SELECT value FROM memory_meta WHERE key = ?`).get(key);
60
+ return row?.value ?? null;
61
+ }
62
+ function setMemoryMeta(db, key, value) {
63
+ db.prepare(`
64
+ INSERT INTO memory_meta (key, value)
65
+ VALUES (?, ?)
66
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value
67
+ `).run(key, value);
68
+ }
69
+ function getDb() {
70
+ if (!memoryDb) {
71
+ initMemoryStore();
72
+ }
73
+ if (!memoryDb) {
74
+ throw new Error('Memory store is not initialized');
75
+ }
76
+ return memoryDb;
77
+ }
78
+ export function initMemoryStore() {
79
+ if (memoryDb)
80
+ return;
81
+ fs.mkdirSync(STORE_DIR, { recursive: true });
82
+ memoryDb = new Database(MEMORY_DB_PATH);
83
+ memoryDb.pragma('journal_mode = WAL');
84
+ memoryDb.pragma('busy_timeout = 3000');
85
+ memoryDb.exec(`
86
+ CREATE TABLE IF NOT EXISTS memory_items (
87
+ id TEXT PRIMARY KEY,
88
+ group_folder TEXT NOT NULL,
89
+ scope TEXT NOT NULL,
90
+ subject_id TEXT,
91
+ type TEXT NOT NULL,
92
+ kind TEXT,
93
+ conflict_key TEXT,
94
+ content TEXT NOT NULL,
95
+ normalized TEXT NOT NULL,
96
+ importance REAL NOT NULL,
97
+ confidence REAL NOT NULL,
98
+ tags_json TEXT,
99
+ tags_text TEXT,
100
+ created_at TEXT NOT NULL,
101
+ updated_at TEXT NOT NULL,
102
+ last_accessed_at TEXT,
103
+ expires_at TEXT,
104
+ source TEXT,
105
+ metadata_json TEXT,
106
+ embedding_json TEXT,
107
+ embedding_model TEXT,
108
+ embedding_updated_at TEXT
109
+ );
110
+ CREATE INDEX IF NOT EXISTS idx_memory_group_scope ON memory_items(group_folder, scope, subject_id);
111
+ CREATE INDEX IF NOT EXISTS idx_memory_updated_at ON memory_items(updated_at);
112
+
113
+ CREATE TABLE IF NOT EXISTS memory_sources (
114
+ id TEXT PRIMARY KEY,
115
+ group_folder TEXT NOT NULL,
116
+ source_type TEXT NOT NULL,
117
+ source_path TEXT NOT NULL,
118
+ source_hash TEXT,
119
+ indexed_at TEXT NOT NULL
120
+ );
121
+ CREATE INDEX IF NOT EXISTS idx_memory_sources_group ON memory_sources(group_folder, source_type);
122
+ `);
123
+ try {
124
+ memoryDb.exec(`
125
+ CREATE VIRTUAL TABLE IF NOT EXISTS memory_fts USING fts5(
126
+ id,
127
+ content,
128
+ tags,
129
+ tokenize = 'porter'
130
+ );
131
+
132
+ CREATE TRIGGER IF NOT EXISTS memory_items_ai AFTER INSERT ON memory_items BEGIN
133
+ INSERT INTO memory_fts (id, content, tags)
134
+ VALUES (new.id, new.content, new.tags_text);
135
+ END;
136
+ CREATE TRIGGER IF NOT EXISTS memory_items_au AFTER UPDATE ON memory_items BEGIN
137
+ UPDATE memory_fts
138
+ SET content = new.content, tags = new.tags_text
139
+ WHERE id = new.id;
140
+ END;
141
+ CREATE TRIGGER IF NOT EXISTS memory_items_ad AFTER DELETE ON memory_items BEGIN
142
+ DELETE FROM memory_fts WHERE id = old.id;
143
+ END;
144
+ `);
145
+ ftsEnabled = true;
146
+ }
147
+ catch {
148
+ ftsEnabled = false;
149
+ }
150
+ try {
151
+ ensureMemorySchema(memoryDb);
152
+ }
153
+ catch (err) {
154
+ console.error(`[memory-store] Schema check failed: ${err instanceof Error ? err.message : String(err)}`);
155
+ throw err;
156
+ }
157
+ }
158
+ function normalizeContent(content) {
159
+ return content
160
+ .toLowerCase()
161
+ .replace(/[^a-z0-9\s]/g, ' ')
162
+ .replace(/\s+/g, ' ')
163
+ .trim();
164
+ }
165
+ function clamp(value, min, max) {
166
+ if (!Number.isFinite(value))
167
+ return min;
168
+ return Math.min(max, Math.max(min, value));
169
+ }
170
+ function tagsToText(tags) {
171
+ if (!tags || tags.length === 0)
172
+ return '';
173
+ return tags
174
+ .map(tag => tag.trim().toLowerCase())
175
+ .filter(Boolean)
176
+ .join(' ');
177
+ }
178
+ function getExpiresAt(ttlDays) {
179
+ if (!ttlDays || !Number.isFinite(ttlDays) || ttlDays <= 0)
180
+ return null;
181
+ const ms = ttlDays * 24 * 60 * 60 * 1000;
182
+ return new Date(Date.now() + ms).toISOString();
183
+ }
184
+ function resolveScope(input, groupFolder) {
185
+ if (input.scope === 'global' && groupFolder !== MAIN_GROUP_FOLDER) {
186
+ return 'group';
187
+ }
188
+ return input.scope;
189
+ }
190
+ function resolveKind(input) {
191
+ if (input.kind === 'semantic' || input.kind === 'episodic' || input.kind === 'procedural' || input.kind === 'preference') {
192
+ return input.kind;
193
+ }
194
+ if (input.type === 'preference')
195
+ return 'preference';
196
+ if (input.type === 'task' || input.type === 'project')
197
+ return 'procedural';
198
+ if (input.type === 'archive')
199
+ return 'episodic';
200
+ return 'semantic';
201
+ }
202
+ function normalizeConflictKey(value) {
203
+ if (!value || typeof value !== 'string')
204
+ return null;
205
+ const trimmed = value.trim().toLowerCase();
206
+ return trimmed ? trimmed : null;
207
+ }
208
+ function parseJsonArray(raw) {
209
+ if (!raw)
210
+ return [];
211
+ try {
212
+ const parsed = JSON.parse(raw);
213
+ if (Array.isArray(parsed)) {
214
+ return parsed.filter((item) => typeof item === 'string');
215
+ }
216
+ }
217
+ catch {
218
+ // ignore parse errors
219
+ }
220
+ return [];
221
+ }
222
+ function parseJsonRecord(raw) {
223
+ if (!raw)
224
+ return null;
225
+ try {
226
+ const parsed = JSON.parse(raw);
227
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
228
+ return parsed;
229
+ }
230
+ }
231
+ catch {
232
+ // ignore parse errors
233
+ }
234
+ return null;
235
+ }
236
+ export function upsertMemoryItems(groupFolder, items, source) {
237
+ if (items.length === 0)
238
+ return [];
239
+ const db = getDb();
240
+ const now = new Date().toISOString();
241
+ const results = [];
242
+ const selectStmt = db.prepare(`
243
+ SELECT id, content, importance, confidence, tags_json, tags_text, embedding_json, embedding_model, embedding_updated_at, kind, conflict_key
244
+ FROM memory_items
245
+ WHERE group_folder = ? AND scope = ? AND IFNULL(subject_id, '') = ? AND type = ? AND normalized = ?
246
+ `);
247
+ const deleteConflictStmt = db.prepare(`
248
+ DELETE FROM memory_items
249
+ WHERE group_folder = ? AND scope = ? AND IFNULL(subject_id, '') = ? AND type = ? AND conflict_key = ?
250
+ `);
251
+ const insertStmt = db.prepare(`
252
+ INSERT INTO memory_items (
253
+ id, group_folder, scope, subject_id, type, kind, conflict_key, content, normalized,
254
+ importance, confidence, tags_json, tags_text, created_at, updated_at,
255
+ last_accessed_at, expires_at, source, metadata_json,
256
+ embedding_json, embedding_model, embedding_updated_at
257
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
258
+ `);
259
+ const updateStmt = db.prepare(`
260
+ UPDATE memory_items
261
+ SET content = ?, importance = ?, confidence = ?, tags_json = ?, tags_text = ?,
262
+ updated_at = ?, expires_at = ?, source = ?, metadata_json = ?,
263
+ embedding_json = ?, embedding_model = ?, embedding_updated_at = ?,
264
+ kind = ?, conflict_key = ?
265
+ WHERE id = ?
266
+ `);
267
+ const transaction = db.transaction((inputs) => {
268
+ for (const item of inputs) {
269
+ if (!item.content || typeof item.content !== 'string')
270
+ continue;
271
+ const normalized = normalizeContent(item.content);
272
+ if (!normalized)
273
+ continue;
274
+ const scope = resolveScope(item, groupFolder);
275
+ const kind = resolveKind(item);
276
+ const conflictKey = normalizeConflictKey(item.conflict_key);
277
+ const subjectId = scope === 'user' ? (item.subject_id || null) : null;
278
+ const importance = clamp(item.importance ?? 0.5, 0, 1);
279
+ const confidence = clamp(item.confidence ?? 0.6, 0, 1);
280
+ const tagsJson = item.tags ? JSON.stringify(item.tags) : null;
281
+ const tagsText = tagsToText(item.tags);
282
+ const expiresAt = getExpiresAt(item.ttl_days ?? null);
283
+ const metadataJson = item.metadata ? JSON.stringify(item.metadata) : null;
284
+ if (conflictKey) {
285
+ deleteConflictStmt.run(groupFolder, scope, subjectId || '', item.type, conflictKey);
286
+ }
287
+ const existing = selectStmt.get(groupFolder, scope, subjectId || '', item.type, normalized);
288
+ if (existing) {
289
+ const mergedImportance = Math.max(existing.importance, importance);
290
+ const mergedConfidence = Math.max(existing.confidence, confidence);
291
+ const mergedContent = existing.content.length >= item.content.length ? existing.content : item.content;
292
+ const mergedTags = Array.from(new Set([...(existing.tags_text || '').split(' ').filter(Boolean), ...tagsText.split(' ').filter(Boolean)])).join(' ');
293
+ const contentChanged = mergedContent !== existing.content;
294
+ const nextEmbeddingJson = contentChanged ? null : existing.embedding_json;
295
+ const nextEmbeddingModel = contentChanged ? null : existing.embedding_model;
296
+ const nextEmbeddingUpdatedAt = contentChanged ? null : existing.embedding_updated_at;
297
+ updateStmt.run(mergedContent, mergedImportance, mergedConfidence, tagsJson || existing.tags_json, mergedTags || existing.tags_text || '', now, expiresAt, item.source || source, metadataJson, nextEmbeddingJson, nextEmbeddingModel, nextEmbeddingUpdatedAt, kind, conflictKey, existing.id);
298
+ results.push({
299
+ id: existing.id,
300
+ group_folder: groupFolder,
301
+ scope,
302
+ subject_id: subjectId,
303
+ type: item.type,
304
+ kind,
305
+ conflict_key: conflictKey,
306
+ content: mergedContent,
307
+ normalized,
308
+ importance: mergedImportance,
309
+ confidence: mergedConfidence,
310
+ tags: item.tags,
311
+ tags_text: mergedTags || existing.tags_text || '',
312
+ created_at: now,
313
+ updated_at: now,
314
+ last_accessed_at: null,
315
+ expires_at: expiresAt,
316
+ ttl_days: item.ttl_days,
317
+ source: item.source || source,
318
+ metadata: item.metadata,
319
+ embedding_json: nextEmbeddingJson,
320
+ embedding_model: nextEmbeddingModel,
321
+ embedding_updated_at: nextEmbeddingUpdatedAt
322
+ });
323
+ continue;
324
+ }
325
+ const id = `mem-${crypto.randomUUID()}`;
326
+ insertStmt.run(id, groupFolder, scope, subjectId, item.type, kind, conflictKey, item.content, normalized, importance, confidence, tagsJson, tagsText, now, now, null, expiresAt, item.source || source, metadataJson, null, null, null);
327
+ results.push({
328
+ id,
329
+ group_folder: groupFolder,
330
+ scope,
331
+ subject_id: subjectId,
332
+ type: item.type,
333
+ kind,
334
+ conflict_key: conflictKey,
335
+ content: item.content,
336
+ normalized,
337
+ importance,
338
+ confidence,
339
+ tags: item.tags,
340
+ tags_text: tagsText,
341
+ created_at: now,
342
+ updated_at: now,
343
+ last_accessed_at: null,
344
+ expires_at: expiresAt,
345
+ ttl_days: item.ttl_days,
346
+ source: item.source || source,
347
+ metadata: item.metadata,
348
+ embedding_json: null,
349
+ embedding_model: null,
350
+ embedding_updated_at: null
351
+ });
352
+ }
353
+ });
354
+ transaction(items);
355
+ return results;
356
+ }
357
+ function buildFtsQuery(text) {
358
+ const tokens = text.toLowerCase().match(/[a-z0-9]+/g) || [];
359
+ if (tokens.length === 0)
360
+ return null;
361
+ const unique = Array.from(new Set(tokens)).slice(0, 10);
362
+ return unique.map(token => `${token}*`).join(' OR ');
363
+ }
364
+ export function searchMemories(params) {
365
+ const db = getDb();
366
+ if (!ftsEnabled) {
367
+ return searchMemoriesFallback(params);
368
+ }
369
+ const ftsQuery = buildFtsQuery(params.query);
370
+ if (!ftsQuery)
371
+ return [];
372
+ const now = new Date().toISOString();
373
+ const limit = Math.min(params.limit || 12, 50);
374
+ const rows = db.prepare(`
375
+ SELECT m.*, bm25(memory_fts) AS bm25
376
+ FROM memory_fts
377
+ JOIN memory_items m ON m.id = memory_fts.id
378
+ WHERE memory_fts MATCH ?
379
+ AND (m.group_folder = ? OR m.group_folder = 'global')
380
+ AND (m.scope != 'user' OR m.subject_id = ?)
381
+ AND (m.expires_at IS NULL OR m.expires_at > ?)
382
+ ORDER BY bm25
383
+ LIMIT ?
384
+ `).all(ftsQuery, params.groupFolder, params.userId || '', now, limit);
385
+ const scored = rows.map(row => {
386
+ const ageDays = row.updated_at ? (Date.now() - new Date(row.updated_at).getTime()) / (1000 * 60 * 60 * 24) : 365;
387
+ const recency = Math.exp(-ageDays / 30);
388
+ const bm25Score = 1 / (1 + (row.bm25 || 0));
389
+ const score = (bm25Score * 0.55) + (row.importance * 0.3) + (recency * 0.15);
390
+ return { ...row, score };
391
+ });
392
+ scored.sort((a, b) => b.score - a.score);
393
+ return scored;
394
+ }
395
+ function searchMemoriesFallback(params) {
396
+ const db = getDb();
397
+ const normalizedQuery = normalizeContent(params.query);
398
+ const tokens = normalizedQuery.split(' ').filter(Boolean).slice(0, 10);
399
+ if (tokens.length === 0)
400
+ return [];
401
+ const clauses = tokens.map(() => '(m.normalized LIKE ? OR m.tags_text LIKE ?)');
402
+ const values = [];
403
+ for (const token of tokens) {
404
+ const pattern = `%${token}%`;
405
+ values.push(pattern, pattern);
406
+ }
407
+ const now = new Date().toISOString();
408
+ const limit = Math.min(params.limit || 12, 50);
409
+ const rows = db.prepare(`
410
+ SELECT m.*
411
+ FROM memory_items m
412
+ WHERE ${clauses.join(' AND ')}
413
+ AND (m.group_folder = ? OR m.group_folder = 'global')
414
+ AND (m.scope != 'user' OR m.subject_id = ?)
415
+ AND (m.expires_at IS NULL OR m.expires_at > ?)
416
+ ORDER BY m.updated_at DESC
417
+ LIMIT ?
418
+ `).all(...values, params.groupFolder, params.userId || '', now, limit);
419
+ const scored = rows.map(row => {
420
+ const normalized = row.normalized || '';
421
+ const tagsText = row.tags_text || '';
422
+ let matches = 0;
423
+ for (const token of tokens) {
424
+ if (normalized.includes(token) || tagsText.includes(token)) {
425
+ matches += 1;
426
+ }
427
+ }
428
+ const matchScore = tokens.length > 0 ? matches / tokens.length : 0;
429
+ const ageDays = row.updated_at ? (Date.now() - new Date(row.updated_at).getTime()) / (1000 * 60 * 60 * 24) : 365;
430
+ const recency = Math.exp(-ageDays / 30);
431
+ const score = (matchScore * 0.5) + (row.importance * 0.3) + (recency * 0.2);
432
+ return { ...row, bm25: 0, score };
433
+ });
434
+ scored.sort((a, b) => b.score - a.score);
435
+ return scored;
436
+ }
437
+ export function listMemories(params) {
438
+ const db = getDb();
439
+ const clauses = ['group_folder = ?'];
440
+ const values = [params.groupFolder];
441
+ if (params.scope) {
442
+ clauses.push('scope = ?');
443
+ values.push(params.scope);
444
+ }
445
+ if (params.type) {
446
+ clauses.push('type = ?');
447
+ values.push(params.type);
448
+ }
449
+ if (params.scope === 'user') {
450
+ clauses.push('subject_id = ?');
451
+ values.push(params.userId || '');
452
+ }
453
+ const limit = Math.min(params.limit || 50, 200);
454
+ const sql = `
455
+ SELECT * FROM memory_items
456
+ WHERE ${clauses.join(' AND ')}
457
+ ORDER BY updated_at DESC
458
+ LIMIT ?
459
+ `;
460
+ return db.prepare(sql).all(...values, limit);
461
+ }
462
+ export function forgetMemories(params) {
463
+ const db = getDb();
464
+ if (params.ids && params.ids.length > 0) {
465
+ const placeholders = params.ids.map(() => '?').join(',');
466
+ const stmt = db.prepare(`DELETE FROM memory_items WHERE group_folder = ? AND id IN (${placeholders})`);
467
+ const info = stmt.run(params.groupFolder, ...params.ids);
468
+ return info.changes;
469
+ }
470
+ if (params.content) {
471
+ const normalized = normalizeContent(params.content);
472
+ const clauses = ['group_folder = ?', 'normalized = ?'];
473
+ const values = [params.groupFolder, normalized];
474
+ if (params.scope) {
475
+ clauses.push('scope = ?');
476
+ values.push(params.scope);
477
+ }
478
+ if (params.scope === 'user') {
479
+ clauses.push('subject_id = ?');
480
+ values.push(params.userId || '');
481
+ }
482
+ const stmt = db.prepare(`DELETE FROM memory_items WHERE ${clauses.join(' AND ')}`);
483
+ const info = stmt.run(...values);
484
+ return info.changes;
485
+ }
486
+ return 0;
487
+ }
488
+ export function getMemoryStats(params) {
489
+ const db = getDb();
490
+ const total = db.prepare(`SELECT COUNT(*) as count FROM memory_items WHERE group_folder = ? OR group_folder = 'global'`)
491
+ .get(params.groupFolder);
492
+ const user = db.prepare(`
493
+ SELECT COUNT(*) as count FROM memory_items
494
+ WHERE group_folder = ? AND scope = 'user' AND subject_id = ?
495
+ `).get(params.groupFolder, params.userId || '');
496
+ const group = db.prepare(`
497
+ SELECT COUNT(*) as count FROM memory_items
498
+ WHERE group_folder = ? AND scope = 'group'
499
+ `).get(params.groupFolder);
500
+ const global = db.prepare(`
501
+ SELECT COUNT(*) as count FROM memory_items
502
+ WHERE group_folder = 'global'
503
+ `).get();
504
+ return {
505
+ total: total?.count || 0,
506
+ user: user?.count || 0,
507
+ group: group?.count || 0,
508
+ global: global?.count || 0
509
+ };
510
+ }
511
+ export function cleanupExpiredMemories() {
512
+ const db = getDb();
513
+ const now = new Date().toISOString();
514
+ const info = db.prepare(`DELETE FROM memory_items WHERE expires_at IS NOT NULL AND expires_at <= ?`).run(now);
515
+ return info.changes;
516
+ }
517
+ /**
518
+ * Record memory access: update last_accessed_at, increment access_count, and boost importance
519
+ * Called when memories are recalled for context
520
+ */
521
+ export function recordMemoryAccess(ids) {
522
+ if (ids.length === 0)
523
+ return;
524
+ const db = getDb();
525
+ const now = new Date().toISOString();
526
+ // Boost importance by 2% per access, capped at 1.0
527
+ const placeholders = ids.map(() => '?').join(',');
528
+ db.prepare(`
529
+ UPDATE memory_items
530
+ SET last_accessed_at = ?,
531
+ access_count = COALESCE(access_count, 0) + 1,
532
+ importance = MIN(1.0, importance + 0.02)
533
+ WHERE id IN (${placeholders})
534
+ `).run(now, ...ids);
535
+ }
536
+ /**
537
+ * Decay importance for memories not accessed recently
538
+ * Reduces importance by 1% for memories not accessed in 30+ days (minimum 0.1)
539
+ * Also doesn't decay critical priority memories
540
+ */
541
+ export function decayUnusedMemories() {
542
+ const db = getDb();
543
+ const info = db.prepare(`
544
+ UPDATE memory_items
545
+ SET importance = MAX(0.1, importance - 0.01)
546
+ WHERE (last_accessed_at IS NULL OR last_accessed_at < datetime('now', '-30 days'))
547
+ AND importance > 0.1
548
+ AND (priority IS NULL OR priority != 'critical')
549
+ `).run();
550
+ return info.changes;
551
+ }
552
+ /**
553
+ * Set priority for a memory item
554
+ * Priority values: 'critical', 'normal', 'low'
555
+ * Critical memories are never pruned
556
+ */
557
+ export function setMemoryPriority(id, priority) {
558
+ const db = getDb();
559
+ const info = db.prepare(`UPDATE memory_items SET priority = ? WHERE id = ?`).run(priority, id);
560
+ return info.changes > 0;
561
+ }
562
+ /**
563
+ * Get memory by ID
564
+ */
565
+ export function getMemoryById(id) {
566
+ const db = getDb();
567
+ const row = db.prepare(`SELECT * FROM memory_items WHERE id = ?`).get(id);
568
+ return row ? row : null;
569
+ }
570
+ export function pruneMemoryOverflow(params) {
571
+ const db = getDb();
572
+ const maxItems = Math.max(0, Math.floor(params.maxItems));
573
+ if (maxItems <= 0)
574
+ return 0;
575
+ const total = db.prepare(`SELECT COUNT(*) as count FROM memory_items`).get();
576
+ if (!total || total.count <= maxItems)
577
+ return 0;
578
+ let remaining = total.count - maxItems;
579
+ let removed = 0;
580
+ // First pass: delete low-importance items (excluding critical priority)
581
+ const deleteByThreshold = db.prepare(`
582
+ DELETE FROM memory_items
583
+ WHERE id IN (
584
+ SELECT id FROM memory_items
585
+ WHERE importance <= ?
586
+ AND (priority IS NULL OR priority != 'critical')
587
+ ORDER BY importance ASC, updated_at ASC
588
+ LIMIT ?
589
+ )
590
+ `);
591
+ const infoThreshold = deleteByThreshold.run(params.importanceThreshold, remaining);
592
+ removed += infoThreshold.changes;
593
+ remaining -= infoThreshold.changes;
594
+ // Second pass: delete by overflow (still excluding critical)
595
+ if (remaining > 0) {
596
+ const deleteByOverflow = db.prepare(`
597
+ DELETE FROM memory_items
598
+ WHERE id IN (
599
+ SELECT id FROM memory_items
600
+ WHERE (priority IS NULL OR priority != 'critical')
601
+ ORDER BY importance ASC, updated_at ASC
602
+ LIMIT ?
603
+ )
604
+ `);
605
+ const infoOverflow = deleteByOverflow.run(remaining);
606
+ removed += infoOverflow.changes;
607
+ }
608
+ return removed;
609
+ }
610
+ export function runMemoryMaintenance() {
611
+ const db = getDb();
612
+ const expired = cleanupExpiredMemories();
613
+ // Decay importance for unused memories
614
+ const decayed = decayUnusedMemories();
615
+ const runtime = loadRuntimeConfig();
616
+ const maintenance = runtime.host.memory.maintenance;
617
+ const maxItems = maintenance.maxItems;
618
+ const threshold = maintenance.pruneImportanceThreshold;
619
+ const pruned = pruneMemoryOverflow({ maxItems, importanceThreshold: threshold });
620
+ const shouldVacuum = maintenance.vacuumEnabled;
621
+ const vacuumIntervalDays = maintenance.vacuumIntervalDays;
622
+ const analyzeEnabled = maintenance.analyzeEnabled;
623
+ let vacuumed = false;
624
+ let analyzed = false;
625
+ if ((expired + pruned) > 0 && shouldVacuum && vacuumIntervalDays > 0) {
626
+ const now = Date.now();
627
+ const lastVacuumRaw = getMemoryMeta(db, 'last_vacuum_at');
628
+ const lastVacuum = lastVacuumRaw ? Date.parse(lastVacuumRaw) : 0;
629
+ const intervalMs = vacuumIntervalDays * 24 * 60 * 60 * 1000;
630
+ if (!Number.isFinite(lastVacuum) || (now - lastVacuum) >= intervalMs) {
631
+ try {
632
+ db.exec('VACUUM');
633
+ setMemoryMeta(db, 'last_vacuum_at', new Date(now).toISOString());
634
+ vacuumed = true;
635
+ }
636
+ catch {
637
+ // ignore vacuum errors
638
+ }
639
+ }
640
+ }
641
+ if ((expired + pruned) > 0 && analyzeEnabled) {
642
+ try {
643
+ db.exec('ANALYZE');
644
+ analyzed = true;
645
+ }
646
+ catch {
647
+ // ignore analyze errors
648
+ }
649
+ }
650
+ return { expired, pruned, decayed, vacuumed, analyzed };
651
+ }
652
+ export function buildUserProfile(params) {
653
+ if (!params.userId)
654
+ return null;
655
+ const db = getDb();
656
+ const limit = Math.min(params.limit || 8, 20);
657
+ const rows = db.prepare(`
658
+ SELECT content, type
659
+ FROM memory_items
660
+ WHERE group_folder = ? AND scope = 'user' AND subject_id = ?
661
+ AND type IN ('identity', 'preference', 'relationship', 'project')
662
+ ORDER BY importance DESC, updated_at DESC
663
+ LIMIT ?
664
+ `).all(params.groupFolder, params.userId, limit);
665
+ if (rows.length === 0)
666
+ return null;
667
+ return rows.map(row => `- (${row.type}) ${row.content}`).join('\n');
668
+ }
669
+ export function listMemoriesMissingEmbeddings(params) {
670
+ const db = getDb();
671
+ const limit = Math.min(params.limit || 100, 1000);
672
+ if (params.groupFolder) {
673
+ return db.prepare(`
674
+ SELECT id, content, group_folder
675
+ FROM memory_items
676
+ WHERE group_folder = ?
677
+ AND content IS NOT NULL
678
+ AND (embedding_json IS NULL OR embedding_json = '')
679
+ ORDER BY updated_at DESC
680
+ LIMIT ?
681
+ `).all(params.groupFolder, limit);
682
+ }
683
+ return db.prepare(`
684
+ SELECT id, content, group_folder
685
+ FROM memory_items
686
+ WHERE content IS NOT NULL
687
+ AND (embedding_json IS NULL OR embedding_json = '')
688
+ ORDER BY updated_at DESC
689
+ LIMIT ?
690
+ `).all(limit);
691
+ }
692
+ export function countMemoriesMissingEmbeddings(params) {
693
+ const db = getDb();
694
+ if (params.groupFolder) {
695
+ const row = db.prepare(`
696
+ SELECT COUNT(*) as count
697
+ FROM memory_items
698
+ WHERE group_folder = ?
699
+ AND content IS NOT NULL
700
+ AND (embedding_json IS NULL OR embedding_json = '')
701
+ `).get(params.groupFolder);
702
+ return row?.count ? Number(row.count) : 0;
703
+ }
704
+ const row = db.prepare(`
705
+ SELECT COUNT(*) as count
706
+ FROM memory_items
707
+ WHERE content IS NOT NULL
708
+ AND (embedding_json IS NULL OR embedding_json = '')
709
+ `).get();
710
+ return row?.count ? Number(row.count) : 0;
711
+ }
712
+ export function updateMemoryEmbedding(params) {
713
+ const db = getDb();
714
+ const now = new Date().toISOString();
715
+ const embeddingJson = JSON.stringify(params.embedding);
716
+ db.prepare(`
717
+ UPDATE memory_items
718
+ SET embedding_json = ?, embedding_model = ?, embedding_updated_at = ?
719
+ WHERE id = ?
720
+ `).run(embeddingJson, params.model, now, params.id);
721
+ }
722
+ export function listEmbeddedMemories(params) {
723
+ const db = getDb();
724
+ const limit = Math.min(params.limit || 2000, 5000);
725
+ const now = new Date().toISOString();
726
+ return db.prepare(`
727
+ SELECT id, content, type, importance, updated_at, embedding_json
728
+ FROM memory_items
729
+ WHERE (group_folder = ? OR group_folder = 'global')
730
+ AND (scope != 'user' OR subject_id = ?)
731
+ AND (expires_at IS NULL OR expires_at > ?)
732
+ AND embedding_json IS NOT NULL
733
+ ORDER BY updated_at DESC
734
+ LIMIT ?
735
+ `).all(params.groupFolder, params.userId || '', now, limit);
736
+ }
737
+ export function listPreferenceMemories(params) {
738
+ const db = getDb();
739
+ const limit = Math.min(params.limit || 50, 200);
740
+ if (params.userId) {
741
+ const rows = db.prepare(`
742
+ SELECT conflict_key, content, tags_json, metadata_json, scope, subject_id, updated_at, importance
743
+ FROM memory_items
744
+ WHERE (group_folder = ? OR group_folder = 'global')
745
+ AND type = 'preference'
746
+ AND conflict_key IS NOT NULL
747
+ AND (
748
+ (scope = 'user' AND subject_id = ?)
749
+ OR scope = 'group'
750
+ OR scope = 'global'
751
+ )
752
+ ORDER BY updated_at DESC
753
+ LIMIT ?
754
+ `).all(params.groupFolder, params.userId, limit);
755
+ return rows.map(row => ({
756
+ conflict_key: String(row.conflict_key),
757
+ content: row.content ? String(row.content) : '',
758
+ tags: parseJsonArray(row.tags_json ? String(row.tags_json) : null),
759
+ metadata: parseJsonRecord(row.metadata_json ? String(row.metadata_json) : null),
760
+ scope: row.scope === 'user' || row.scope === 'group' || row.scope === 'global' ? row.scope : 'group',
761
+ subject_id: row.subject_id ? String(row.subject_id) : null,
762
+ updated_at: row.updated_at ? String(row.updated_at) : new Date(0).toISOString(),
763
+ importance: typeof row.importance === 'number' ? row.importance : Number(row.importance || 0)
764
+ }));
765
+ }
766
+ const rows = db.prepare(`
767
+ SELECT conflict_key, content, tags_json, metadata_json, scope, subject_id, updated_at, importance
768
+ FROM memory_items
769
+ WHERE (group_folder = ? OR group_folder = 'global')
770
+ AND type = 'preference'
771
+ AND conflict_key IS NOT NULL
772
+ AND (scope = 'group' OR scope = 'global')
773
+ ORDER BY updated_at DESC
774
+ LIMIT ?
775
+ `).all(params.groupFolder, limit);
776
+ return rows.map(row => ({
777
+ conflict_key: String(row.conflict_key),
778
+ content: row.content ? String(row.content) : '',
779
+ tags: parseJsonArray(row.tags_json ? String(row.tags_json) : null),
780
+ metadata: parseJsonRecord(row.metadata_json ? String(row.metadata_json) : null),
781
+ scope: row.scope === 'user' || row.scope === 'group' || row.scope === 'global' ? row.scope : 'group',
782
+ subject_id: row.subject_id ? String(row.subject_id) : null,
783
+ updated_at: row.updated_at ? String(row.updated_at) : new Date(0).toISOString(),
784
+ importance: typeof row.importance === 'number' ? row.importance : Number(row.importance || 0)
785
+ }));
786
+ }
787
+ //# sourceMappingURL=memory-store.js.map