@yun-zero/claw-memory 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 (131) hide show
  1. package/.claude/settings.local.json +68 -0
  2. package/README.md +323 -0
  3. package/dist/config/llm.d.ts +13 -0
  4. package/dist/config/llm.d.ts.map +1 -0
  5. package/dist/config/llm.js +96 -0
  6. package/dist/config/llm.js.map +1 -0
  7. package/dist/config/plugin.d.ts +15 -0
  8. package/dist/config/plugin.d.ts.map +1 -0
  9. package/dist/config/plugin.js +32 -0
  10. package/dist/config/plugin.js.map +1 -0
  11. package/dist/db/entityRepository.d.ts +21 -0
  12. package/dist/db/entityRepository.d.ts.map +1 -0
  13. package/dist/db/entityRepository.js +55 -0
  14. package/dist/db/entityRepository.js.map +1 -0
  15. package/dist/db/repository.d.ts +22 -0
  16. package/dist/db/repository.d.ts.map +1 -0
  17. package/dist/db/repository.js +77 -0
  18. package/dist/db/repository.js.map +1 -0
  19. package/dist/db/schema.d.ts +5 -0
  20. package/dist/db/schema.d.ts.map +1 -0
  21. package/dist/db/schema.js +112 -0
  22. package/dist/db/schema.js.map +1 -0
  23. package/dist/db/todoRepository.d.ts +26 -0
  24. package/dist/db/todoRepository.d.ts.map +1 -0
  25. package/dist/db/todoRepository.js +54 -0
  26. package/dist/db/todoRepository.js.map +1 -0
  27. package/dist/hooks/bootstrap.d.ts +3 -0
  28. package/dist/hooks/bootstrap.d.ts.map +1 -0
  29. package/dist/hooks/bootstrap.js +28 -0
  30. package/dist/hooks/bootstrap.js.map +1 -0
  31. package/dist/hooks/message.d.ts +18 -0
  32. package/dist/hooks/message.d.ts.map +1 -0
  33. package/dist/hooks/message.js +52 -0
  34. package/dist/hooks/message.js.map +1 -0
  35. package/dist/index.d.ts +3 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +46 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/mcp/tools.d.ts +26 -0
  40. package/dist/mcp/tools.d.ts.map +1 -0
  41. package/dist/mcp/tools.js +360 -0
  42. package/dist/mcp/tools.js.map +1 -0
  43. package/dist/plugin.d.ts +18 -0
  44. package/dist/plugin.d.ts.map +1 -0
  45. package/dist/plugin.js +62 -0
  46. package/dist/plugin.js.map +1 -0
  47. package/dist/services/entityGraphService.d.ts +87 -0
  48. package/dist/services/entityGraphService.d.ts.map +1 -0
  49. package/dist/services/entityGraphService.js +271 -0
  50. package/dist/services/entityGraphService.js.map +1 -0
  51. package/dist/services/memory.d.ts +26 -0
  52. package/dist/services/memory.d.ts.map +1 -0
  53. package/dist/services/memory.js +281 -0
  54. package/dist/services/memory.js.map +1 -0
  55. package/dist/services/memoryIndex.d.ts +34 -0
  56. package/dist/services/memoryIndex.d.ts.map +1 -0
  57. package/dist/services/memoryIndex.js +100 -0
  58. package/dist/services/memoryIndex.js.map +1 -0
  59. package/dist/services/metadataExtractor.d.ts +16 -0
  60. package/dist/services/metadataExtractor.d.ts.map +1 -0
  61. package/dist/services/metadataExtractor.js +75 -0
  62. package/dist/services/metadataExtractor.js.map +1 -0
  63. package/dist/services/retrieval.d.ts +24 -0
  64. package/dist/services/retrieval.d.ts.map +1 -0
  65. package/dist/services/retrieval.js +40 -0
  66. package/dist/services/retrieval.js.map +1 -0
  67. package/dist/services/scheduler.d.ts +122 -0
  68. package/dist/services/scheduler.d.ts.map +1 -0
  69. package/dist/services/scheduler.js +434 -0
  70. package/dist/services/scheduler.js.map +1 -0
  71. package/dist/services/summarizer.d.ts +43 -0
  72. package/dist/services/summarizer.d.ts.map +1 -0
  73. package/dist/services/summarizer.js +252 -0
  74. package/dist/services/summarizer.js.map +1 -0
  75. package/dist/services/tagService.d.ts +64 -0
  76. package/dist/services/tagService.d.ts.map +1 -0
  77. package/dist/services/tagService.js +281 -0
  78. package/dist/services/tagService.js.map +1 -0
  79. package/dist/tools/memory.d.ts +3 -0
  80. package/dist/tools/memory.d.ts.map +1 -0
  81. package/dist/tools/memory.js +114 -0
  82. package/dist/tools/memory.js.map +1 -0
  83. package/dist/types.d.ts +128 -0
  84. package/dist/types.d.ts.map +1 -0
  85. package/dist/types.js +6 -0
  86. package/dist/types.js.map +1 -0
  87. package/docs/plans/2026-03-02-claw-memory-design.md +445 -0
  88. package/docs/plans/2026-03-02-incremental-summary-design.md +157 -0
  89. package/docs/plans/2026-03-02-incremental-summary-implementation.md +468 -0
  90. package/docs/plans/2026-03-02-memory-index-design.md +163 -0
  91. package/docs/plans/2026-03-02-memory-index-implementation.md +836 -0
  92. package/docs/plans/2026-03-02-mvp-implementation.md +1703 -0
  93. package/docs/plans/2026-03-02-testing-implementation.md +395 -0
  94. package/docs/plans/2026-03-02-testing-plan.md +93 -0
  95. package/docs/plans/2026-03-03-claw-memory-openclaw-plugin-design.md +285 -0
  96. package/docs/plans/2026-03-03-claw-memory-plugin-implementation.md +642 -0
  97. package/docs/plans/2026-03-03-entity-graph-design.md +121 -0
  98. package/docs/plans/2026-03-03-entity-graph-implementation.md +687 -0
  99. package/docs/plans/2026-03-03-llm-generic-config-design.md +43 -0
  100. package/docs/plans/2026-03-03-llm-generic-config-implementation.md +186 -0
  101. package/docs/plans/2026-03-03-memory-e2e-stress-test-design.md +110 -0
  102. package/docs/plans/2026-03-03-memory-e2e-stress-test-implementation.md +464 -0
  103. package/docs/plans/2026-03-03-minimax-llm-fix.md +156 -0
  104. package/docs/plans/2026-03-03-scheduler-design.md +165 -0
  105. package/docs/plans/2026-03-03-scheduler-implementation.md +777 -0
  106. package/docs/plans/2026-03-03-tags-visualization-design.md +73 -0
  107. package/docs/plans/2026-03-03-tags-visualization-implementation.md +539 -0
  108. package/openclaw.plugin.json +11 -0
  109. package/package.json +41 -0
  110. package/src/config/llm.ts +129 -0
  111. package/src/config/plugin.ts +47 -0
  112. package/src/db/entityRepository.ts +80 -0
  113. package/src/db/repository.ts +106 -0
  114. package/src/db/schema.ts +121 -0
  115. package/src/db/todoRepository.ts +76 -0
  116. package/src/hooks/bootstrap.ts +36 -0
  117. package/src/hooks/message.ts +84 -0
  118. package/src/index.ts +50 -0
  119. package/src/plugin.ts +85 -0
  120. package/src/services/entityGraphService.ts +367 -0
  121. package/src/services/memory.ts +338 -0
  122. package/src/services/memoryIndex.ts +140 -0
  123. package/src/services/metadataExtractor.ts +89 -0
  124. package/src/services/retrieval.ts +71 -0
  125. package/src/services/scheduler.ts +529 -0
  126. package/src/services/summarizer.ts +318 -0
  127. package/src/services/tagService.ts +335 -0
  128. package/src/tools/memory.ts +137 -0
  129. package/src/types.ts +139 -0
  130. package/tsconfig.json +20 -0
  131. package/vitest.config.ts +16 -0
@@ -0,0 +1,529 @@
1
+ /**
2
+ * Scheduler Service
3
+ * Manages scheduled tasks for deduplication and summary generation
4
+ */
5
+
6
+ import Database from 'better-sqlite3';
7
+ import cron, { ScheduledTask } from 'node-cron';
8
+ import { generateSummaryWithLLM } from '../config/llm.js';
9
+ import { Summarizer } from './summarizer.js';
10
+
11
+ /**
12
+ * Scheduler configuration interface
13
+ */
14
+ export interface SchedulerConfig {
15
+ /** Time to run deduplication task (HH:mm format, e.g., "01:00") */
16
+ deduplicateTime: string;
17
+ /** Time to run daily summary task (HH:mm format) */
18
+ dailyTime: string;
19
+ /** Time to run weekly summary task (HH:mm format) */
20
+ weeklyTime: string;
21
+ /** Time to run monthly summary task (HH:mm format) */
22
+ monthlyTime: string;
23
+ /** Whether the scheduler is enabled */
24
+ enabled: boolean;
25
+ }
26
+
27
+ /**
28
+ * Default scheduler configuration
29
+ */
30
+ export const DEFAULT_CONFIG: SchedulerConfig = {
31
+ deduplicateTime: '01:00',
32
+ dailyTime: '02:00',
33
+ weeklyTime: '03:00',
34
+ monthlyTime: '04:00',
35
+ enabled: true
36
+ };
37
+
38
+ /**
39
+ * Task type enum
40
+ */
41
+ type TaskType = 'deduplicate' | 'daily' | 'weekly' | 'monthly';
42
+
43
+ /**
44
+ * Queued task item
45
+ */
46
+ interface QueuedTask {
47
+ type: TaskType;
48
+ scheduledTime: Date;
49
+ }
50
+
51
+ /**
52
+ * Scheduler Service Class
53
+ * Manages scheduled tasks with execution locking and task queue
54
+ */
55
+ export class Scheduler {
56
+ private config: SchedulerConfig;
57
+ private isRunning: boolean;
58
+ private taskQueue: QueuedTask[];
59
+ private tasks: Map<TaskType, ScheduledTask>;
60
+ private db: Database.Database;
61
+
62
+ /**
63
+ * Creates a new Scheduler instance
64
+ * @param db - Database instance
65
+ * @param config - Optional configuration (uses DEFAULT_CONFIG if not provided)
66
+ */
67
+ constructor(db: Database.Database, config?: Partial<SchedulerConfig>) {
68
+ // 合并环境变量配置
69
+ const envConfig: Partial<SchedulerConfig> = {};
70
+
71
+ if (process.env.SCHEDULER_DEDUPE_TIME) {
72
+ envConfig.deduplicateTime = process.env.SCHEDULER_DEDUPE_TIME;
73
+ }
74
+ if (process.env.SCHEDULER_DAILY_TIME) {
75
+ envConfig.dailyTime = process.env.SCHEDULER_DAILY_TIME;
76
+ }
77
+ if (process.env.SCHEDULER_WEEKLY_TIME) {
78
+ envConfig.weeklyTime = process.env.SCHEDULER_WEEKLY_TIME;
79
+ }
80
+ if (process.env.SCHEDULER_MONTHLY_TIME) {
81
+ envConfig.monthlyTime = process.env.SCHEDULER_MONTHLY_TIME;
82
+ }
83
+ if (process.env.SCHEDULER_ENABLED !== undefined) {
84
+ envConfig.enabled = process.env.SCHEDULER_ENABLED !== 'false';
85
+ }
86
+
87
+ this.db = db;
88
+ this.config = { ...DEFAULT_CONFIG, ...envConfig, ...config };
89
+ this.isRunning = false;
90
+ this.taskQueue = [];
91
+ this.tasks = new Map();
92
+ }
93
+
94
+ /**
95
+ * Starts the scheduler and all scheduled tasks
96
+ */
97
+ start(): void {
98
+ if (this.isRunning) {
99
+ console.log('[Scheduler] Already running');
100
+ return;
101
+ }
102
+
103
+ if (!this.config.enabled) {
104
+ console.log('[Scheduler] Scheduler is disabled');
105
+ return;
106
+ }
107
+
108
+ console.log('[Scheduler] Starting scheduler...');
109
+
110
+ this.scheduleDeduplicate();
111
+ this.scheduleDailySummary();
112
+ this.scheduleWeeklySummary();
113
+ this.scheduleMonthlySummary();
114
+
115
+ this.isRunning = true;
116
+ console.log('[Scheduler] Scheduler started successfully');
117
+ }
118
+
119
+ /**
120
+ * Stops the scheduler and all scheduled tasks
121
+ */
122
+ stop(): void {
123
+ if (!this.isRunning) {
124
+ console.log('[Scheduler] Not running');
125
+ return;
126
+ }
127
+
128
+ console.log('[Scheduler] Stopping scheduler...');
129
+
130
+ for (const [type, task] of this.tasks) {
131
+ task.stop();
132
+ console.log(`[Scheduler] Stopped ${type} task`);
133
+ }
134
+
135
+ this.tasks.clear();
136
+ this.taskQueue = [];
137
+ this.isRunning = false;
138
+
139
+ console.log('[Scheduler] Scheduler stopped');
140
+ }
141
+
142
+ /**
143
+ * Checks if the scheduler is enabled
144
+ * @returns true if enabled, false otherwise
145
+ */
146
+ isEnabled(): boolean {
147
+ return this.config.enabled;
148
+ }
149
+
150
+ /**
151
+ * Checks if the scheduler is currently running
152
+ * @returns true if running, false otherwise
153
+ */
154
+ getIsRunning(): boolean {
155
+ return this.isRunning;
156
+ }
157
+
158
+ /**
159
+ * Converts HH:mm time format to cron expression
160
+ * @param time - Time in HH:mm format
161
+ * @returns Cron expression
162
+ */
163
+ private timeToCron(time: string): string {
164
+ const [hour, minute] = time.split(':');
165
+ return `${minute} ${hour} * * *`;
166
+ }
167
+
168
+ /**
169
+ * Schedules the deduplicate task
170
+ */
171
+ private scheduleDeduplicate(): void {
172
+ const cronExpression = this.timeToCron(this.config.deduplicateTime);
173
+ const task = cron.schedule(cronExpression, () => {
174
+ this.executeWithLock('deduplicate');
175
+ });
176
+ this.tasks.set('deduplicate', task);
177
+ console.log(`[Scheduler] Scheduled deduplicate task at ${this.config.deduplicateTime}`);
178
+ }
179
+
180
+ /**
181
+ * Schedules the daily summary task
182
+ */
183
+ private scheduleDailySummary(): void {
184
+ const cronExpression = this.timeToCron(this.config.dailyTime);
185
+ const task = cron.schedule(cronExpression, () => {
186
+ this.executeWithLock('daily');
187
+ });
188
+ this.tasks.set('daily', task);
189
+ console.log(`[Scheduler] Scheduled daily summary task at ${this.config.dailyTime}`);
190
+ }
191
+
192
+ /**
193
+ * Schedules the weekly summary task
194
+ */
195
+ private scheduleWeeklySummary(): void {
196
+ const cronExpression = this.timeToCron(this.config.weeklyTime);
197
+ const task = cron.schedule(cronExpression, () => {
198
+ this.executeWithLock('weekly');
199
+ });
200
+ this.tasks.set('weekly', task);
201
+ console.log(`[Scheduler] Scheduled weekly summary task at ${this.config.weeklyTime}`);
202
+ }
203
+
204
+ /**
205
+ * Schedules the monthly summary task
206
+ */
207
+ private scheduleMonthlySummary(): void {
208
+ const cronExpression = this.timeToCron(this.config.monthlyTime);
209
+ const task = cron.schedule(cronExpression, () => {
210
+ this.executeWithLock('monthly');
211
+ });
212
+ this.tasks.set('monthly', task);
213
+ console.log(`[Scheduler] Scheduled monthly summary task at ${this.config.monthlyTime}`);
214
+ }
215
+
216
+ /**
217
+ * Executes a task with locking mechanism and queue support
218
+ * @param type - Task type
219
+ */
220
+ private executeWithLock(type: TaskType): void {
221
+ // Add to queue
222
+ const queuedTask: QueuedTask = {
223
+ type,
224
+ scheduledTime: new Date()
225
+ };
226
+ this.taskQueue.push(queuedTask);
227
+ console.log(`[Scheduler] Queued ${type} task (queue size: ${this.taskQueue.length})`);
228
+
229
+ // Process queue
230
+ // Note: Not awaiting async processQueue to avoid blocking the cron scheduler
231
+ this.processQueue().catch(err => {
232
+ console.error(`[Scheduler] Error processing queue:`, err);
233
+ });
234
+ }
235
+
236
+ /**
237
+ * Processes the task queue
238
+ */
239
+ private async processQueue(): Promise<void> {
240
+ if (this.taskQueue.length === 0) {
241
+ return;
242
+ }
243
+
244
+ const task = this.taskQueue.shift();
245
+ if (!task) return;
246
+
247
+ console.log(`[Scheduler] Processing ${task.type} task...`);
248
+
249
+ switch (task.type) {
250
+ case 'deduplicate':
251
+ await this.deduplicate();
252
+ break;
253
+ case 'daily':
254
+ await this.dailySummary();
255
+ break;
256
+ case 'weekly':
257
+ await this.weeklySummary();
258
+ break;
259
+ case 'monthly':
260
+ await this.monthlySummary();
261
+ break;
262
+ }
263
+
264
+ // Continue processing if more tasks in queue
265
+ if (this.taskQueue.length > 0) {
266
+ setTimeout(() => this.processQueue(), 1000);
267
+ }
268
+ }
269
+
270
+ /**
271
+ * Executes deduplication task - finds and marks duplicate memories
272
+ */
273
+ private async deduplicate(): Promise<void> {
274
+ console.log('[Scheduler] Running deduplication...');
275
+
276
+ // Get all non-archived memories that are not already marked as duplicates
277
+ const memories = this.db.prepare(`
278
+ SELECT id, content_path, importance
279
+ FROM memories
280
+ WHERE is_archived = FALSE AND is_duplicate = FALSE
281
+ ORDER BY created_at DESC
282
+ `).all() as any[];
283
+
284
+ const processed = new Set<string>();
285
+
286
+ for (const memory of memories) {
287
+ if (processed.has(memory.id)) continue;
288
+
289
+ // Find similar memories (via shared entities)
290
+ const similar = this.db.prepare(`
291
+ SELECT m2.id, m2.content_path, m2.importance
292
+ FROM memories m1
293
+ JOIN memory_entities me1 ON m1.id = me1.memory_id
294
+ JOIN memory_entities me2 ON me1.entity_id = me2.entity_id
295
+ JOIN memories m2 ON me2.memory_id = m2.id
296
+ WHERE m1.id = ? AND m2.id != m1.id
297
+ AND m2.is_archived = FALSE AND m2.is_duplicate = FALSE
298
+ `).all(memory.id) as any[];
299
+
300
+ for (const similarMem of similar) {
301
+ if (processed.has(similarMem.id)) continue;
302
+
303
+ // Mark as duplicate
304
+ this.db.prepare(`
305
+ UPDATE memories
306
+ SET is_duplicate = TRUE, duplicate_of = ?
307
+ WHERE id = ?
308
+ `).run(memory.id, similarMem.id);
309
+
310
+ // Merge importance
311
+ const newImportance = Math.min(1, memory.importance + similarMem.importance * 0.5);
312
+ this.db.prepare(`
313
+ UPDATE memories SET importance = ? WHERE id = ?
314
+ `).run(newImportance, memory.id);
315
+
316
+ processed.add(similarMem.id);
317
+ console.log(`[Scheduler] Marked ${similarMem.id} as duplicate of ${memory.id}`);
318
+ }
319
+
320
+ processed.add(memory.id);
321
+ }
322
+
323
+ console.log('[Scheduler] Deduplication completed');
324
+ }
325
+
326
+ /**
327
+ * Executes daily summary task - generates summary for previous day's memories
328
+ */
329
+ private async dailySummary(): Promise<void> {
330
+ console.log('[Scheduler] Running daily summary...');
331
+
332
+ const yesterday = new Date();
333
+ yesterday.setDate(yesterday.getDate() - 1);
334
+ const dateStr = yesterday.toISOString().split('T')[0];
335
+
336
+ // 检查是否已有总结
337
+ const existing = this.db.prepare(`
338
+ SELECT summary FROM time_buckets WHERE date = ?
339
+ `).get(dateStr) as { summary?: string } | undefined;
340
+
341
+ if (existing?.summary) {
342
+ console.log(`[Scheduler] Daily summary for ${dateStr} already exists`);
343
+ return;
344
+ }
345
+
346
+ // 获取当天的记忆
347
+ const memories = this.db.prepare(`
348
+ SELECT id, content_path FROM memories
349
+ WHERE date(created_at) = date(?)
350
+ `).all(dateStr) as any[];
351
+
352
+ if (memories.length === 0) {
353
+ console.log(`[Scheduler] No memories for ${dateStr}, skipping`);
354
+ return;
355
+ }
356
+
357
+ // 读取记忆内容
358
+ const fs = await import('fs/promises');
359
+ const contents: string[] = [];
360
+
361
+ for (const mem of memories) {
362
+ try {
363
+ const content = await fs.readFile(mem.content_path, 'utf-8');
364
+ contents.push(content.slice(0, 1000));
365
+ } catch (e) {
366
+ console.error(`[Scheduler] Failed to read ${mem.content_path}`);
367
+ }
368
+ }
369
+
370
+ // 构建报告字符串并调用 LLM 生成总结
371
+ const reportString = `日期: ${dateStr}\n记忆数量: ${memories.length}\n\n记忆内容:\n${contents.join('\n---\n')}`;
372
+
373
+ try {
374
+ const summary = await generateSummaryWithLLM(reportString);
375
+
376
+ // 保存到 time_buckets
377
+ this.db.prepare(`
378
+ INSERT OR REPLACE INTO time_buckets (date, summary, summary_generated_at, memory_count)
379
+ VALUES (?, ?, datetime('now'), ?)
380
+ `).run(dateStr, summary, memories.length);
381
+
382
+ console.log(`[Scheduler] Daily summary generated for ${dateStr}`);
383
+ } catch (error) {
384
+ console.error('[Scheduler] Failed to generate daily summary:', error);
385
+ }
386
+ }
387
+
388
+ /**
389
+ * Executes weekly summary task
390
+ */
391
+ private async weeklySummary(): Promise<void> {
392
+ console.log('[Scheduler] Running weekly summary...');
393
+
394
+ const now = new Date();
395
+ const dayOfWeek = now.getDay();
396
+ const diff = now.getDate() - dayOfWeek + (dayOfWeek === 0 ? -6 : 1);
397
+ const weekStart = new Date(now.getFullYear(), now.getMonth(), diff);
398
+ weekStart.setHours(0, 0, 0, 0);
399
+ const weekStartStr = weekStart.toISOString().split('T')[0];
400
+
401
+ // Check if summary already exists
402
+ const existing = this.db.prepare(`
403
+ SELECT summary FROM time_buckets WHERE date = ?
404
+ `).get(weekStartStr) as { summary?: string } | undefined;
405
+
406
+ if (existing?.summary) {
407
+ console.log(`[Scheduler] Weekly summary for ${weekStartStr} already exists`);
408
+ return;
409
+ }
410
+
411
+ // Get all memories for this week
412
+ const memories = this.db.prepare(`
413
+ SELECT id, content_path FROM memories
414
+ WHERE date(created_at) >= date(?) AND date(created_at) <= date('now')
415
+ `).all(weekStartStr) as any[];
416
+
417
+ if (memories.length === 0) {
418
+ console.log(`[Scheduler] No memories for week ${weekStartStr}, skipping`);
419
+ return;
420
+ }
421
+
422
+ // Read memory contents and generate weekly summary
423
+ const fs = await import('fs/promises');
424
+ const contents: string[] = [];
425
+
426
+ for (const mem of memories.slice(0, 10)) {
427
+ try {
428
+ const content = await fs.readFile(mem.content_path, 'utf-8');
429
+ contents.push(content.slice(0, 500));
430
+ } catch (e) {
431
+ // Skip files that can't be read
432
+ }
433
+ }
434
+
435
+ const summarizer = new Summarizer(this.db);
436
+ const report = {
437
+ period: 'week',
438
+ startDate: weekStartStr,
439
+ endDate: now.toISOString().split('T')[0],
440
+ memoryCount: memories.length,
441
+ memories: contents
442
+ };
443
+
444
+ try {
445
+ const summary = await summarizer.generateWeeklySummary(report);
446
+ this.db.prepare(`
447
+ INSERT OR REPLACE INTO time_buckets (date, summary, summary_generated_at, memory_count)
448
+ VALUES (?, ?, datetime('now'), ?)
449
+ `).run(weekStartStr, summary, memories.length);
450
+ console.log(`[Scheduler] Weekly summary generated for ${weekStartStr}`);
451
+ } catch (error) {
452
+ console.error('[Scheduler] Failed to generate weekly summary:', error);
453
+ }
454
+ }
455
+
456
+ /**
457
+ * Executes monthly summary task
458
+ */
459
+ private async monthlySummary(): Promise<void> {
460
+ console.log('[Scheduler] Running monthly summary...');
461
+
462
+ const now = new Date();
463
+ const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
464
+ const monthStartStr = monthStart.toISOString().split('T')[0];
465
+
466
+ const existing = this.db.prepare(`
467
+ SELECT summary FROM time_buckets WHERE date = ?
468
+ `).get(monthStartStr) as { summary?: string } | undefined;
469
+
470
+ if (existing?.summary) {
471
+ console.log(`[Scheduler] Monthly summary for ${monthStartStr} already exists`);
472
+ return;
473
+ }
474
+
475
+ const memories = this.db.prepare(`
476
+ SELECT id FROM memories
477
+ WHERE date(created_at) >= date(?) AND date(created_at) <= date('now')
478
+ `).all(monthStartStr) as any[];
479
+
480
+ if (memories.length === 0) {
481
+ console.log(`[Scheduler] No memories for month ${monthStartStr}, skipping`);
482
+ return;
483
+ }
484
+
485
+ const summarizer = new Summarizer(this.db);
486
+ const report = {
487
+ period: 'month',
488
+ startDate: monthStartStr,
489
+ endDate: now.toISOString().split('T')[0],
490
+ memoryCount: memories.length
491
+ };
492
+
493
+ try {
494
+ const summary = await summarizer.generateMonthlySummary(report);
495
+ this.db.prepare(`
496
+ INSERT OR REPLACE INTO time_buckets (date, summary, summary_generated_at, memory_count)
497
+ VALUES (?, ?, datetime('now'), ?)
498
+ `).run(monthStartStr, summary, memories.length);
499
+ console.log(`[Scheduler] Monthly summary generated for ${monthStartStr}`);
500
+ } catch (error) {
501
+ console.error('[Scheduler] Failed to generate monthly summary:', error);
502
+ }
503
+ }
504
+
505
+ /**
506
+ * Updates the scheduler configuration
507
+ * @param config - New configuration (partial)
508
+ */
509
+ updateConfig(config: Partial<SchedulerConfig>): void {
510
+ this.config = { ...this.config, ...config };
511
+ console.log('[Scheduler] Configuration updated');
512
+ }
513
+
514
+ /**
515
+ * Gets the current configuration
516
+ * @returns Current scheduler configuration
517
+ */
518
+ getConfig(): SchedulerConfig {
519
+ return { ...this.config };
520
+ }
521
+
522
+ /**
523
+ * Gets the current queue size
524
+ * @returns Number of pending tasks in queue
525
+ */
526
+ getQueueSize(): number {
527
+ return this.taskQueue.length;
528
+ }
529
+ }