clawmem 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +660 -0
- package/CLAUDE.md +660 -0
- package/LICENSE +21 -0
- package/README.md +993 -0
- package/SKILL.md +717 -0
- package/bin/clawmem +75 -0
- package/package.json +72 -0
- package/src/amem.ts +797 -0
- package/src/beads.ts +263 -0
- package/src/clawmem.ts +1849 -0
- package/src/collections.ts +405 -0
- package/src/config.ts +178 -0
- package/src/consolidation.ts +123 -0
- package/src/directory-context.ts +248 -0
- package/src/errors.ts +41 -0
- package/src/formatter.ts +427 -0
- package/src/graph-traversal.ts +247 -0
- package/src/hooks/context-surfacing.ts +317 -0
- package/src/hooks/curator-nudge.ts +89 -0
- package/src/hooks/decision-extractor.ts +639 -0
- package/src/hooks/feedback-loop.ts +214 -0
- package/src/hooks/handoff-generator.ts +345 -0
- package/src/hooks/postcompact-inject.ts +226 -0
- package/src/hooks/precompact-extract.ts +314 -0
- package/src/hooks/pretool-inject.ts +79 -0
- package/src/hooks/session-bootstrap.ts +324 -0
- package/src/hooks/staleness-check.ts +130 -0
- package/src/hooks.ts +367 -0
- package/src/indexer.ts +327 -0
- package/src/intent.ts +294 -0
- package/src/limits.ts +26 -0
- package/src/llm.ts +1175 -0
- package/src/mcp.ts +2138 -0
- package/src/memory.ts +336 -0
- package/src/mmr.ts +93 -0
- package/src/observer.ts +269 -0
- package/src/openclaw/engine.ts +283 -0
- package/src/openclaw/index.ts +221 -0
- package/src/openclaw/plugin.json +83 -0
- package/src/openclaw/shell.ts +207 -0
- package/src/openclaw/tools.ts +304 -0
- package/src/profile.ts +346 -0
- package/src/promptguard.ts +218 -0
- package/src/retrieval-gate.ts +106 -0
- package/src/search-utils.ts +127 -0
- package/src/server.ts +783 -0
- package/src/splitter.ts +325 -0
- package/src/store.ts +4062 -0
- package/src/validation.ts +67 -0
- package/src/watcher.ts +58 -0
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collections configuration management
|
|
3
|
+
*
|
|
4
|
+
* This module manages the YAML-based collection configuration at ~/.config/clawmem/config.yaml.
|
|
5
|
+
* Collections define which directories to index and their associated contexts.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
import { homedir } from "os";
|
|
11
|
+
import YAML from "yaml";
|
|
12
|
+
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Types
|
|
15
|
+
// ============================================================================
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Context definitions for a collection
|
|
19
|
+
* Key is path prefix (e.g., "/", "/2024", "/Board of Directors")
|
|
20
|
+
* Value is the context description
|
|
21
|
+
*/
|
|
22
|
+
export type ContextMap = Record<string, string>;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A single collection configuration
|
|
26
|
+
*/
|
|
27
|
+
export interface Collection {
|
|
28
|
+
path: string; // Absolute path to index
|
|
29
|
+
pattern: string; // Glob pattern (e.g., "**/*.md")
|
|
30
|
+
context?: ContextMap; // Optional context definitions
|
|
31
|
+
update?: string; // Optional bash command to run during qmd update
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The complete configuration file structure
|
|
36
|
+
*/
|
|
37
|
+
export interface LifecyclePolicy {
|
|
38
|
+
archive_after_days: number;
|
|
39
|
+
type_overrides: Record<string, number | null>;
|
|
40
|
+
purge_after_days: number | null;
|
|
41
|
+
exempt_collections: string[];
|
|
42
|
+
dry_run: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface CollectionConfig {
|
|
46
|
+
global_context?: string; // Context applied to all collections
|
|
47
|
+
collections: Record<string, Collection>; // Collection name -> config
|
|
48
|
+
directoryContext?: boolean; // Opt-in: auto-generate CLAUDE.md in directories
|
|
49
|
+
lifecycle?: LifecyclePolicy; // Lifecycle management policy
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Collection with its name (for return values)
|
|
54
|
+
*/
|
|
55
|
+
export interface NamedCollection extends Collection {
|
|
56
|
+
name: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ============================================================================
|
|
60
|
+
// Configuration paths
|
|
61
|
+
// ============================================================================
|
|
62
|
+
|
|
63
|
+
function getConfigDir(): string {
|
|
64
|
+
// Allow override via CLAWMEM_CONFIG_DIR for testing
|
|
65
|
+
if (process.env.CLAWMEM_CONFIG_DIR) {
|
|
66
|
+
return process.env.CLAWMEM_CONFIG_DIR;
|
|
67
|
+
}
|
|
68
|
+
return join(homedir(), ".config", "clawmem");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function getConfigFilePath(): string {
|
|
72
|
+
const dir = getConfigDir();
|
|
73
|
+
const preferred = join(dir, "config.yaml");
|
|
74
|
+
if (existsSync(preferred)) return preferred;
|
|
75
|
+
return join(dir, "index.yml");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Ensure config directory exists
|
|
80
|
+
*/
|
|
81
|
+
function ensureConfigDir(): void {
|
|
82
|
+
const configDir = getConfigDir();
|
|
83
|
+
if (!existsSync(configDir)) {
|
|
84
|
+
mkdirSync(configDir, { recursive: true });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ============================================================================
|
|
89
|
+
// Core functions
|
|
90
|
+
// ============================================================================
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Load configuration from ~/.config/clawmem/config.yaml
|
|
94
|
+
* Returns empty config if file doesn't exist
|
|
95
|
+
*/
|
|
96
|
+
export function loadConfig(): CollectionConfig {
|
|
97
|
+
const configPath = getConfigFilePath();
|
|
98
|
+
if (!existsSync(configPath)) {
|
|
99
|
+
return { collections: {} };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const content = readFileSync(configPath, "utf-8");
|
|
104
|
+
const config = YAML.parse(content) as CollectionConfig;
|
|
105
|
+
|
|
106
|
+
// Ensure collections object exists
|
|
107
|
+
if (!config.collections) {
|
|
108
|
+
config.collections = {};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Parse lifecycle policy if present
|
|
112
|
+
const raw = YAML.parse(content);
|
|
113
|
+
if (raw?.lifecycle && typeof raw.lifecycle === "object") {
|
|
114
|
+
const lc = raw.lifecycle;
|
|
115
|
+
config.lifecycle = {
|
|
116
|
+
archive_after_days: typeof lc.archive_after_days === "number" ? lc.archive_after_days : 90,
|
|
117
|
+
type_overrides: typeof lc.type_overrides === "object" && lc.type_overrides !== null ? lc.type_overrides : {},
|
|
118
|
+
purge_after_days: typeof lc.purge_after_days === "number" ? lc.purge_after_days : null,
|
|
119
|
+
exempt_collections: Array.isArray(lc.exempt_collections) ? lc.exempt_collections : [],
|
|
120
|
+
dry_run: lc.dry_run !== false,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return config;
|
|
125
|
+
} catch (error) {
|
|
126
|
+
throw new Error(`Failed to parse ${configPath}: ${error}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Save configuration to ~/.config/clawmem/index.yml
|
|
132
|
+
*/
|
|
133
|
+
export function saveConfig(config: CollectionConfig): void {
|
|
134
|
+
ensureConfigDir();
|
|
135
|
+
const configPath = getConfigFilePath();
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const yaml = YAML.stringify(config, {
|
|
139
|
+
indent: 2,
|
|
140
|
+
lineWidth: 0, // Don't wrap lines
|
|
141
|
+
});
|
|
142
|
+
writeFileSync(configPath, yaml, "utf-8");
|
|
143
|
+
} catch (error) {
|
|
144
|
+
throw new Error(`Failed to write ${configPath}: ${error}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Get a specific collection by name
|
|
150
|
+
* Returns null if not found
|
|
151
|
+
*/
|
|
152
|
+
export function getCollection(name: string): NamedCollection | null {
|
|
153
|
+
const config = loadConfig();
|
|
154
|
+
const collection = config.collections[name];
|
|
155
|
+
|
|
156
|
+
if (!collection) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return { name, ...collection };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* List all collections
|
|
165
|
+
*/
|
|
166
|
+
export function listCollections(): NamedCollection[] {
|
|
167
|
+
const config = loadConfig();
|
|
168
|
+
return Object.entries(config.collections).map(([name, collection]) => ({
|
|
169
|
+
name,
|
|
170
|
+
...collection,
|
|
171
|
+
}));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Add or update a collection
|
|
176
|
+
*/
|
|
177
|
+
export function addCollection(
|
|
178
|
+
name: string,
|
|
179
|
+
path: string,
|
|
180
|
+
pattern: string = "**/*.md"
|
|
181
|
+
): void {
|
|
182
|
+
const config = loadConfig();
|
|
183
|
+
|
|
184
|
+
config.collections[name] = {
|
|
185
|
+
path,
|
|
186
|
+
pattern,
|
|
187
|
+
context: config.collections[name]?.context, // Preserve existing context
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
saveConfig(config);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Remove a collection
|
|
195
|
+
*/
|
|
196
|
+
export function removeCollection(name: string): boolean {
|
|
197
|
+
const config = loadConfig();
|
|
198
|
+
|
|
199
|
+
if (!config.collections[name]) {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
delete config.collections[name];
|
|
204
|
+
saveConfig(config);
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Rename a collection
|
|
210
|
+
*/
|
|
211
|
+
export function renameCollection(oldName: string, newName: string): boolean {
|
|
212
|
+
const config = loadConfig();
|
|
213
|
+
|
|
214
|
+
if (!config.collections[oldName]) {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (config.collections[newName]) {
|
|
219
|
+
throw new Error(`Collection '${newName}' already exists`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
config.collections[newName] = config.collections[oldName];
|
|
223
|
+
delete config.collections[oldName];
|
|
224
|
+
saveConfig(config);
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ============================================================================
|
|
229
|
+
// Context management
|
|
230
|
+
// ============================================================================
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Get global context
|
|
234
|
+
*/
|
|
235
|
+
export function getGlobalContext(): string | undefined {
|
|
236
|
+
const config = loadConfig();
|
|
237
|
+
return config.global_context;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Set global context
|
|
242
|
+
*/
|
|
243
|
+
export function setGlobalContext(context: string | undefined): void {
|
|
244
|
+
const config = loadConfig();
|
|
245
|
+
config.global_context = context;
|
|
246
|
+
saveConfig(config);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Get all contexts for a collection
|
|
251
|
+
*/
|
|
252
|
+
export function getContexts(collectionName: string): ContextMap | undefined {
|
|
253
|
+
const collection = getCollection(collectionName);
|
|
254
|
+
return collection?.context;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Add or update a context for a specific path in a collection
|
|
259
|
+
*/
|
|
260
|
+
export function addContext(
|
|
261
|
+
collectionName: string,
|
|
262
|
+
pathPrefix: string,
|
|
263
|
+
contextText: string
|
|
264
|
+
): boolean {
|
|
265
|
+
const config = loadConfig();
|
|
266
|
+
const collection = config.collections[collectionName];
|
|
267
|
+
|
|
268
|
+
if (!collection) {
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (!collection.context) {
|
|
273
|
+
collection.context = {};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
collection.context[pathPrefix] = contextText;
|
|
277
|
+
saveConfig(config);
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Remove a context from a collection
|
|
283
|
+
*/
|
|
284
|
+
export function removeContext(
|
|
285
|
+
collectionName: string,
|
|
286
|
+
pathPrefix: string
|
|
287
|
+
): boolean {
|
|
288
|
+
const config = loadConfig();
|
|
289
|
+
const collection = config.collections[collectionName];
|
|
290
|
+
|
|
291
|
+
if (!collection?.context?.[pathPrefix]) {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
delete collection.context[pathPrefix];
|
|
296
|
+
|
|
297
|
+
// Remove empty context object
|
|
298
|
+
if (Object.keys(collection.context).length === 0) {
|
|
299
|
+
delete collection.context;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
saveConfig(config);
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* List all contexts across all collections
|
|
308
|
+
*/
|
|
309
|
+
export function listAllContexts(): Array<{
|
|
310
|
+
collection: string;
|
|
311
|
+
path: string;
|
|
312
|
+
context: string;
|
|
313
|
+
}> {
|
|
314
|
+
const config = loadConfig();
|
|
315
|
+
const results: Array<{ collection: string; path: string; context: string }> = [];
|
|
316
|
+
|
|
317
|
+
// Add global context if present
|
|
318
|
+
if (config.global_context) {
|
|
319
|
+
results.push({
|
|
320
|
+
collection: "*",
|
|
321
|
+
path: "/",
|
|
322
|
+
context: config.global_context,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Add collection contexts
|
|
327
|
+
for (const [name, collection] of Object.entries(config.collections)) {
|
|
328
|
+
if (collection.context) {
|
|
329
|
+
for (const [path, context] of Object.entries(collection.context)) {
|
|
330
|
+
results.push({
|
|
331
|
+
collection: name,
|
|
332
|
+
path,
|
|
333
|
+
context,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return results;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Find best matching context for a given collection and path
|
|
344
|
+
* Returns the most specific matching context (longest path prefix match)
|
|
345
|
+
*/
|
|
346
|
+
export function findContextForPath(
|
|
347
|
+
collectionName: string,
|
|
348
|
+
filePath: string
|
|
349
|
+
): string | undefined {
|
|
350
|
+
const config = loadConfig();
|
|
351
|
+
const collection = config.collections[collectionName];
|
|
352
|
+
|
|
353
|
+
if (!collection?.context) {
|
|
354
|
+
return config.global_context;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Find all matching prefixes
|
|
358
|
+
const matches: Array<{ prefix: string; context: string }> = [];
|
|
359
|
+
|
|
360
|
+
for (const [prefix, context] of Object.entries(collection.context)) {
|
|
361
|
+
// Normalize paths for comparison
|
|
362
|
+
const normalizedPath = filePath.startsWith("/") ? filePath : `/${filePath}`;
|
|
363
|
+
const normalizedPrefix = prefix.startsWith("/") ? prefix : `/${prefix}`;
|
|
364
|
+
|
|
365
|
+
if (normalizedPath.startsWith(normalizedPrefix)) {
|
|
366
|
+
matches.push({ prefix: normalizedPrefix, context });
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Return most specific match (longest prefix)
|
|
371
|
+
if (matches.length > 0) {
|
|
372
|
+
matches.sort((a, b) => b.prefix.length - a.prefix.length);
|
|
373
|
+
return matches[0]!.context;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Fallback to global context
|
|
377
|
+
return config.global_context;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ============================================================================
|
|
381
|
+
// Utility functions
|
|
382
|
+
// ============================================================================
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Get the config file path (useful for error messages)
|
|
386
|
+
*/
|
|
387
|
+
export function getConfigPath(): string {
|
|
388
|
+
return getConfigFilePath();
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Check if config file exists
|
|
393
|
+
*/
|
|
394
|
+
export function configExists(): boolean {
|
|
395
|
+
return existsSync(getConfigFilePath());
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Validate a collection name
|
|
400
|
+
* Collection names must be valid and not contain special characters
|
|
401
|
+
*/
|
|
402
|
+
export function isValidCollectionName(name: string): boolean {
|
|
403
|
+
// Allow alphanumeric, hyphens, underscores
|
|
404
|
+
return /^[a-zA-Z0-9_-]+$/.test(name);
|
|
405
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClawMem Configuration — Vault routing, lifecycle policy, performance profiles.
|
|
3
|
+
*
|
|
4
|
+
* Multi-vault support: ClawMem can manage multiple independent SQLite vaults,
|
|
5
|
+
* each with its own documents, embeddings, and graphs. The default (unnamed)
|
|
6
|
+
* vault lives at ~/.cache/clawmem/index.sqlite. Named vaults are configured
|
|
7
|
+
* via config.yaml or environment variables.
|
|
8
|
+
*
|
|
9
|
+
* Single vault is the default. Multi-vault is opt-in.
|
|
10
|
+
*
|
|
11
|
+
* Configuration sources (highest priority first):
|
|
12
|
+
* 1. Environment variables (CLAWMEM_VAULTS JSON map)
|
|
13
|
+
* 2. Config file (~/.config/clawmem/config.yaml, vaults section)
|
|
14
|
+
*
|
|
15
|
+
* Example config.yaml with multiple vaults:
|
|
16
|
+
* vaults:
|
|
17
|
+
* work: ~/.cache/clawmem/work.sqlite
|
|
18
|
+
* personal: ~/.cache/clawmem/personal.sqlite
|
|
19
|
+
*
|
|
20
|
+
* When no vaults are configured, ClawMem operates as a single-vault system.
|
|
21
|
+
* All tools work without the vault parameter — it's always optional.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { existsSync, readFileSync } from "fs";
|
|
25
|
+
import { resolve, join } from "path";
|
|
26
|
+
import { homedir } from "os";
|
|
27
|
+
import YAML from "yaml";
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Helpers
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
/** Expand leading ~ to user home directory */
|
|
34
|
+
function expandHome(p: string): string {
|
|
35
|
+
return p.startsWith("~/") || p === "~" ? join(homedir(), p.slice(1)) : p;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Types
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
export interface VaultConfig {
|
|
43
|
+
/** Vault name → absolute path to SQLite file */
|
|
44
|
+
[name: string]: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface LifecyclePolicy {
|
|
48
|
+
archive_after_days: number;
|
|
49
|
+
type_overrides: Record<string, number | null>;
|
|
50
|
+
purge_after_days: number | null;
|
|
51
|
+
exempt_collections: string[];
|
|
52
|
+
dry_run: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface ClawMemConfig {
|
|
56
|
+
/** Named vault paths (empty = single-vault mode) */
|
|
57
|
+
vaults: VaultConfig;
|
|
58
|
+
/** Lifecycle management policy */
|
|
59
|
+
lifecycle?: LifecyclePolicy;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Performance Profiles
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
export type PerformanceProfile = "speed" | "balanced" | "deep";
|
|
67
|
+
|
|
68
|
+
export interface ProfileConfig {
|
|
69
|
+
tokenBudget: number;
|
|
70
|
+
maxResults: number;
|
|
71
|
+
useVector: boolean;
|
|
72
|
+
vectorTimeout: number;
|
|
73
|
+
minScore: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const PROFILES: Record<PerformanceProfile, ProfileConfig> = {
|
|
77
|
+
speed: { tokenBudget: 400, maxResults: 5, useVector: false, vectorTimeout: 0, minScore: 0.55 },
|
|
78
|
+
balanced: { tokenBudget: 800, maxResults: 10, useVector: true, vectorTimeout: 900, minScore: 0.45 },
|
|
79
|
+
deep: { tokenBudget: 1200, maxResults: 15, useVector: true, vectorTimeout: 2000, minScore: 0.35 },
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export function getActiveProfile(): ProfileConfig {
|
|
83
|
+
const profileName = (process.env.CLAWMEM_PROFILE || "balanced") as PerformanceProfile;
|
|
84
|
+
return PROFILES[profileName] || PROFILES.balanced;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// Config loading
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
let _cachedConfig: ClawMemConfig | null = null;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Load vault configuration from env vars and config file.
|
|
95
|
+
* Priority: env vars override config file values.
|
|
96
|
+
*/
|
|
97
|
+
export function loadVaultConfig(): ClawMemConfig {
|
|
98
|
+
if (_cachedConfig) return _cachedConfig;
|
|
99
|
+
|
|
100
|
+
const vaults: VaultConfig = {};
|
|
101
|
+
|
|
102
|
+
// 1. Load from config.yaml (vaults section)
|
|
103
|
+
const configDir = process.env.CLAWMEM_CONFIG_DIR || join(homedir(), ".config", "clawmem");
|
|
104
|
+
const configPath = join(configDir, "config.yaml");
|
|
105
|
+
|
|
106
|
+
let parsedYaml: any = null;
|
|
107
|
+
if (existsSync(configPath)) {
|
|
108
|
+
try {
|
|
109
|
+
const content = readFileSync(configPath, "utf-8");
|
|
110
|
+
parsedYaml = YAML.parse(content);
|
|
111
|
+
if (parsedYaml?.vaults && typeof parsedYaml.vaults === "object") {
|
|
112
|
+
for (const [name, path] of Object.entries(parsedYaml.vaults)) {
|
|
113
|
+
if (typeof path === "string") {
|
|
114
|
+
vaults[name] = resolve(expandHome(path));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
} catch {
|
|
119
|
+
// Config parse failure — continue with env vars only
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 2. Override with env vars (higher priority)
|
|
124
|
+
if (process.env.CLAWMEM_VAULTS) {
|
|
125
|
+
try {
|
|
126
|
+
const envVaults = JSON.parse(process.env.CLAWMEM_VAULTS);
|
|
127
|
+
if (typeof envVaults === "object") {
|
|
128
|
+
for (const [name, path] of Object.entries(envVaults)) {
|
|
129
|
+
if (typeof path === "string") {
|
|
130
|
+
vaults[name] = resolve(expandHome(path as string));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
} catch {
|
|
135
|
+
// Invalid JSON — ignore
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 3. Lifecycle policy (optional)
|
|
140
|
+
let lifecycle: LifecyclePolicy | undefined;
|
|
141
|
+
if (parsedYaml?.lifecycle && typeof parsedYaml.lifecycle === "object") {
|
|
142
|
+
const lc = parsedYaml.lifecycle;
|
|
143
|
+
lifecycle = {
|
|
144
|
+
archive_after_days: typeof lc.archive_after_days === "number" ? lc.archive_after_days : 90,
|
|
145
|
+
type_overrides: typeof lc.type_overrides === "object" && lc.type_overrides !== null ? lc.type_overrides : {},
|
|
146
|
+
purge_after_days: typeof lc.purge_after_days === "number" ? lc.purge_after_days : null,
|
|
147
|
+
exempt_collections: Array.isArray(lc.exempt_collections) ? lc.exempt_collections : [],
|
|
148
|
+
dry_run: lc.dry_run !== false,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
_cachedConfig = { vaults, lifecycle };
|
|
153
|
+
return _cachedConfig;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Get the SQLite path for a named vault.
|
|
158
|
+
* Returns undefined if vault is not configured.
|
|
159
|
+
*/
|
|
160
|
+
export function getVaultPath(vaultName: string): string | undefined {
|
|
161
|
+
const config = loadVaultConfig();
|
|
162
|
+
return config.vaults[vaultName];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* List all configured vault names.
|
|
167
|
+
*/
|
|
168
|
+
export function listVaults(): string[] {
|
|
169
|
+
const config = loadVaultConfig();
|
|
170
|
+
return Object.keys(config.vaults);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Clear cached config (for testing or after env var changes).
|
|
175
|
+
*/
|
|
176
|
+
export function clearConfigCache(): void {
|
|
177
|
+
_cachedConfig = null;
|
|
178
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClawMem Consolidation Worker
|
|
3
|
+
*
|
|
4
|
+
* Background worker that enriches documents missing A-MEM metadata.
|
|
5
|
+
* Runs periodically to backfill memory notes for documents indexed before A-MEM.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Store } from "./store.ts";
|
|
9
|
+
import type { LlamaCpp } from "./llm.ts";
|
|
10
|
+
|
|
11
|
+
// =============================================================================
|
|
12
|
+
// Types
|
|
13
|
+
// =============================================================================
|
|
14
|
+
|
|
15
|
+
interface DocumentToEnrich {
|
|
16
|
+
id: number;
|
|
17
|
+
hash: string;
|
|
18
|
+
title: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// =============================================================================
|
|
22
|
+
// Worker State
|
|
23
|
+
// =============================================================================
|
|
24
|
+
|
|
25
|
+
let consolidationTimer: Timer | null = null;
|
|
26
|
+
let isRunning = false;
|
|
27
|
+
|
|
28
|
+
// =============================================================================
|
|
29
|
+
// Worker Functions
|
|
30
|
+
// =============================================================================
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Starts the consolidation worker that enriches documents missing A-MEM metadata.
|
|
34
|
+
*
|
|
35
|
+
* @param store - Store instance with A-MEM methods
|
|
36
|
+
* @param llm - LLM instance for memory note construction
|
|
37
|
+
* @param intervalMs - Tick interval in milliseconds (default: 300000 = 5 min)
|
|
38
|
+
*/
|
|
39
|
+
export function startConsolidationWorker(
|
|
40
|
+
store: Store,
|
|
41
|
+
llm: LlamaCpp,
|
|
42
|
+
intervalMs: number = 300000
|
|
43
|
+
): void {
|
|
44
|
+
// Clamp interval to minimum 15 seconds
|
|
45
|
+
const interval = Math.max(15000, intervalMs);
|
|
46
|
+
|
|
47
|
+
console.log(`[consolidation] Starting worker with ${interval}ms interval`);
|
|
48
|
+
|
|
49
|
+
// Set up periodic tick
|
|
50
|
+
consolidationTimer = setInterval(async () => {
|
|
51
|
+
await tick(store, llm);
|
|
52
|
+
}, interval);
|
|
53
|
+
|
|
54
|
+
// Use unref() to avoid blocking process exit
|
|
55
|
+
consolidationTimer.unref();
|
|
56
|
+
|
|
57
|
+
console.log("[consolidation] Worker started");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Stops the consolidation worker.
|
|
62
|
+
*/
|
|
63
|
+
export function stopConsolidationWorker(): void {
|
|
64
|
+
if (consolidationTimer) {
|
|
65
|
+
clearInterval(consolidationTimer);
|
|
66
|
+
consolidationTimer = null;
|
|
67
|
+
console.log("[consolidation] Worker stopped");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Single worker tick: find and enrich up to 3 documents missing A-MEM metadata.
|
|
73
|
+
*/
|
|
74
|
+
async function tick(store: Store, llm: LlamaCpp): Promise<void> {
|
|
75
|
+
// Reentrancy guard
|
|
76
|
+
if (isRunning) {
|
|
77
|
+
console.log("[consolidation] Skipping tick (already running)");
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
isRunning = true;
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
// Find documents missing A-MEM keywords (primary indicator of unenriched docs)
|
|
85
|
+
const docs = store.db
|
|
86
|
+
.prepare<DocumentToEnrich, []>(
|
|
87
|
+
`SELECT id, hash, title
|
|
88
|
+
FROM documents
|
|
89
|
+
WHERE amem_keywords IS NULL AND active = 1
|
|
90
|
+
ORDER BY created_at ASC
|
|
91
|
+
LIMIT 3`
|
|
92
|
+
)
|
|
93
|
+
.all();
|
|
94
|
+
|
|
95
|
+
if (docs.length === 0) {
|
|
96
|
+
// No work to do
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
console.log(`[consolidation] Enriching ${docs.length} documents`);
|
|
101
|
+
|
|
102
|
+
// Enrich each document (note + links, skip evolution to avoid cascades)
|
|
103
|
+
for (const doc of docs) {
|
|
104
|
+
try {
|
|
105
|
+
// Construct and store memory note
|
|
106
|
+
const note = await store.constructMemoryNote(llm, doc.id);
|
|
107
|
+
await store.storeMemoryNote(doc.id, note);
|
|
108
|
+
|
|
109
|
+
// Generate memory links (skip evolution for backlog)
|
|
110
|
+
await store.generateMemoryLinks(llm, doc.id);
|
|
111
|
+
|
|
112
|
+
console.log(`[consolidation] Enriched doc ${doc.id} (${doc.title})`);
|
|
113
|
+
} catch (err) {
|
|
114
|
+
console.error(`[consolidation] Failed to enrich doc ${doc.id}:`, err);
|
|
115
|
+
// Continue with remaining docs (don't let one failure block the queue)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
} catch (err) {
|
|
119
|
+
console.error("[consolidation] Tick failed:", err);
|
|
120
|
+
} finally {
|
|
121
|
+
isRunning = false;
|
|
122
|
+
}
|
|
123
|
+
}
|