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.
Files changed (50) hide show
  1. package/AGENTS.md +660 -0
  2. package/CLAUDE.md +660 -0
  3. package/LICENSE +21 -0
  4. package/README.md +993 -0
  5. package/SKILL.md +717 -0
  6. package/bin/clawmem +75 -0
  7. package/package.json +72 -0
  8. package/src/amem.ts +797 -0
  9. package/src/beads.ts +263 -0
  10. package/src/clawmem.ts +1849 -0
  11. package/src/collections.ts +405 -0
  12. package/src/config.ts +178 -0
  13. package/src/consolidation.ts +123 -0
  14. package/src/directory-context.ts +248 -0
  15. package/src/errors.ts +41 -0
  16. package/src/formatter.ts +427 -0
  17. package/src/graph-traversal.ts +247 -0
  18. package/src/hooks/context-surfacing.ts +317 -0
  19. package/src/hooks/curator-nudge.ts +89 -0
  20. package/src/hooks/decision-extractor.ts +639 -0
  21. package/src/hooks/feedback-loop.ts +214 -0
  22. package/src/hooks/handoff-generator.ts +345 -0
  23. package/src/hooks/postcompact-inject.ts +226 -0
  24. package/src/hooks/precompact-extract.ts +314 -0
  25. package/src/hooks/pretool-inject.ts +79 -0
  26. package/src/hooks/session-bootstrap.ts +324 -0
  27. package/src/hooks/staleness-check.ts +130 -0
  28. package/src/hooks.ts +367 -0
  29. package/src/indexer.ts +327 -0
  30. package/src/intent.ts +294 -0
  31. package/src/limits.ts +26 -0
  32. package/src/llm.ts +1175 -0
  33. package/src/mcp.ts +2138 -0
  34. package/src/memory.ts +336 -0
  35. package/src/mmr.ts +93 -0
  36. package/src/observer.ts +269 -0
  37. package/src/openclaw/engine.ts +283 -0
  38. package/src/openclaw/index.ts +221 -0
  39. package/src/openclaw/plugin.json +83 -0
  40. package/src/openclaw/shell.ts +207 -0
  41. package/src/openclaw/tools.ts +304 -0
  42. package/src/profile.ts +346 -0
  43. package/src/promptguard.ts +218 -0
  44. package/src/retrieval-gate.ts +106 -0
  45. package/src/search-utils.ts +127 -0
  46. package/src/server.ts +783 -0
  47. package/src/splitter.ts +325 -0
  48. package/src/store.ts +4062 -0
  49. package/src/validation.ts +67 -0
  50. 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
+ }