@crewx/memory 0.1.23-rc.6 β†’ 0.1.23-rc.61

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.
@@ -3,18 +3,20 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.CATEGORIES = exports.MemoryEngine = void 0;
6
+ exports.CATEGORIES = exports.MemoryEngine = exports.SUMMARIZE_LOCK_STALE_MS = exports.SUMMARIZER_FAILURE_THRESHOLD = void 0;
7
7
  exports.getMemoryConfig = getMemoryConfig;
8
8
  exports.createMemoryEngine = createMemoryEngine;
9
9
  const fs_1 = __importDefault(require("fs"));
10
10
  const path_1 = __importDefault(require("path"));
11
- const os_1 = __importDefault(require("os"));
12
11
  const child_process_1 = require("child_process");
13
12
  const nanoid_1 = require("nanoid");
14
13
  const js_yaml_1 = __importDefault(require("js-yaml"));
15
14
  const parser_1 = require("./parser");
16
15
  const knowledge_core_1 = require("@crewx/knowledge-core");
17
16
  const CREWX_CLI = process.env.CREWX_CLI || 'npx crewx';
17
+ exports.SUMMARIZER_FAILURE_THRESHOLD = 3;
18
+ const SUMMARIZER_HEALTH_FILE = '.summarizer-health.json';
19
+ exports.SUMMARIZE_LOCK_STALE_MS = 10 * 60 * 1000;
18
20
  function parseCrewxResponse(raw) {
19
21
  const responseMatch = raw.match(/πŸ“„ Response:\s*─+\s*([\s\S]*?)(?=πŸ“ Working Directory|$)/);
20
22
  if (responseMatch?.[1]) {
@@ -103,7 +105,7 @@ class MemoryEngine {
103
105
  this.summarizerTimeout =
104
106
  config.summarizerTimeout ??
105
107
  (yamlConfig.summarizer_timeout != null ? Number(yamlConfig.summarizer_timeout) : undefined) ??
106
- 60000;
108
+ 480000;
107
109
  this.searcherTimeout =
108
110
  config.searcherTimeout ??
109
111
  (yamlConfig.searcher_timeout != null ? Number(yamlConfig.searcher_timeout) : undefined) ??
@@ -121,6 +123,9 @@ class MemoryEngine {
121
123
  getDirtySummaryPath(agentId) {
122
124
  return path_1.default.join(this.getAgentDir(agentId), '.dirty-summary');
123
125
  }
126
+ getSummarizeLockPath() {
127
+ return path_1.default.join(this.dataDir, '.summarize.lock');
128
+ }
124
129
  ensureDir(dir) {
125
130
  if (!fs_1.default.existsSync(dir)) {
126
131
  fs_1.default.mkdirSync(dir, { recursive: true });
@@ -162,6 +167,50 @@ class MemoryEngine {
162
167
  this.ensureDir(this.getAgentDir(agentId));
163
168
  fs_1.default.writeFileSync(this.getDirtySummaryPath(agentId), String(Date.now()), 'utf-8');
164
169
  }
170
+ getSummarizerHealthPath() {
171
+ return path_1.default.join(this.dataDir, SUMMARIZER_HEALTH_FILE);
172
+ }
173
+ readSummarizerHealth() {
174
+ try {
175
+ const raw = fs_1.default.readFileSync(this.getSummarizerHealthPath(), 'utf-8');
176
+ const parsed = JSON.parse(raw);
177
+ return {
178
+ consecutiveFailures: typeof parsed.consecutiveFailures === 'number' ? parsed.consecutiveFailures : 0,
179
+ notified: Boolean(parsed.notified),
180
+ };
181
+ }
182
+ catch {
183
+ return { consecutiveFailures: 0, notified: false };
184
+ }
185
+ }
186
+ writeSummarizerHealth(state) {
187
+ this.ensureDir(this.dataDir);
188
+ fs_1.default.writeFileSync(this.getSummarizerHealthPath(), JSON.stringify(state), 'utf-8');
189
+ }
190
+ recordSummarizerFailure() {
191
+ const state = this.readSummarizerHealth();
192
+ state.consecutiveFailures += 1;
193
+ if (state.consecutiveFailures >= exports.SUMMARIZER_FAILURE_THRESHOLD && !state.notified) {
194
+ this.notifySummarizerDegraded(state.consecutiveFailures);
195
+ state.notified = true;
196
+ }
197
+ this.writeSummarizerHealth(state);
198
+ }
199
+ recordSummarizerSuccess() {
200
+ const state = this.readSummarizerHealth();
201
+ if (state.consecutiveFailures === 0 && !state.notified)
202
+ return;
203
+ this.writeSummarizerHealth({ consecutiveFailures: 0, notified: false });
204
+ }
205
+ notifySummarizerDegraded(streakCount) {
206
+ try {
207
+ (0, child_process_1.execSync)(`${CREWX_CLI} notify "AI memory summarization keeps failing β€” falling back to mechanical summaries" --level=warning --body="callSummarizer(agent=${this.summarizerAgent}) failed ${streakCount} times in a row, so summarization switched to the fallback (mechanical) path."`, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 15000 });
208
+ }
209
+ catch (e) {
210
+ const err = e;
211
+ console.error(`[memory] failed to send summarizer-health notify: ${err.message}`);
212
+ }
213
+ }
165
214
  loadAllEntries(agentId) {
166
215
  const entriesDir = this.getEntriesDir(agentId);
167
216
  if (!fs_1.default.existsSync(entriesDir))
@@ -172,7 +221,7 @@ class MemoryEngine {
172
221
  const raw = fs_1.default.readFileSync(path_1.default.join(entriesDir, file), 'utf-8');
173
222
  const { data, content } = (0, parser_1.parseFrontmatter)(raw);
174
223
  const bodyText = content.replace(/^#[^\n]*\n*/m, '').trim();
175
- const hasBody = Boolean(bodyText) && bodyText !== '(상세 λ‚΄μš©μ„ 여기에 μΆ”κ°€)';
224
+ const hasBody = Boolean(bodyText) && bodyText !== '(Add details here)';
176
225
  const entry = data;
177
226
  entries.push({ ...entry, file, hasBody });
178
227
  }
@@ -217,16 +266,16 @@ class MemoryEngine {
217
266
  });
218
267
  const filename = `${id}.md`;
219
268
  const filepath = path_1.default.join(this.getEntriesDir(agentId), filename);
220
- const body = options.body ?? '(상세 λ‚΄μš©μ„ 여기에 μΆ”κ°€)';
269
+ const body = options.body ?? '(Add details here)';
221
270
  const content = `${frontmatter}\n\n# ${summary}\n\n${body}\n`;
222
271
  fs_1.default.writeFileSync(filepath, content, 'utf-8');
223
- console.log(`βœ… μ €μž₯ μ™„λ£Œ: data/${agentId}/entries/${filename}`);
272
+ console.log(`βœ… Saved: data/${agentId}/entries/${filename}`);
224
273
  console.log(` ID: ${id}`);
225
- console.log(` Tags: ${tags.join(', ') || '(μ—†μŒ)'}`);
274
+ console.log(` Tags: ${tags.join(', ') || '(none)'}`);
226
275
  console.log(` Category: ${category}`);
227
276
  try {
228
277
  this.markSummaryDirty(agentId);
229
- console.log(` πŸ“ summary.md κ°±μ‹  μ˜ˆμ•½λ¨`);
278
+ console.log(` πŸ“ summary.md update scheduled`);
230
279
  }
231
280
  catch (_e) {
232
281
  }
@@ -252,11 +301,11 @@ class MemoryEngine {
252
301
  }
253
302
  const cutoff24h = new Date(Date.now() - this.shortTermHours * 60 * 60 * 1000).toISOString();
254
303
  const shortTermEntries = entries.filter((e) => this.toEntryTimestamp(e) >= cutoff24h);
255
- console.log(`# ${agentId} κΈ°μ–΅ μš”μ•½\n`);
304
+ console.log(`# ${agentId} memory summary\n`);
256
305
  const summaryLastUpdated = fs_1.default.existsSync(summaryPath)
257
306
  ? (0, parser_1.parseFrontmatter)(fs_1.default.readFileSync(summaryPath, 'utf-8')).data.last_updated ?? '-'
258
307
  : '-';
259
- console.log(`> ${entries.length}건 | ν† ν”½ ${topics.length}개 | κ°±μ‹ : ${summaryLastUpdated}`);
308
+ console.log(`> ${entries.length} items | topics ${topics.length} items | updated: ${summaryLastUpdated}`);
260
309
  if (options.full) {
261
310
  const byCat = {};
262
311
  for (const entry of entries) {
@@ -278,7 +327,7 @@ class MemoryEngine {
278
327
  return bImp - aImp;
279
328
  return this.toEntryTimestamp(b).localeCompare(this.toEntryTimestamp(a));
280
329
  });
281
- console.log(`## πŸ”₯ 졜근 μž‘μ—… (24h)\n`);
330
+ console.log(`## πŸ”₯ last Task (24h)\n`);
282
331
  for (const e of sortedShortTerm) {
283
332
  const star = e.important ? '⭐ ' : '';
284
333
  console.log(`- ${star}[${e.id}] ${e.summary} (${this.getPrimaryTag(e)})`);
@@ -302,7 +351,7 @@ class MemoryEngine {
302
351
  const diffDays = Math.max(1, Math.floor((newest.getTime() - oldest.getTime()) / msPerDay) + 1);
303
352
  const toMD = (d) => `${String(d.getMonth() + 1).padStart(2, '0')}/${String(d.getDate()).padStart(2, '0')}`;
304
353
  console.log('---');
305
- console.log(`πŸ“‹ ν† ν”½ μš”μ•½ (${toMD(oldest)} ~ ${toMD(newest)}, ${diffDays}일간)\n`);
354
+ console.log(`πŸ“‹ Topic summary (${toMD(oldest)} ~ ${toMD(newest)}, ${diffDays} days)\n`);
306
355
  }
307
356
  }
308
357
  console.log(summaryBody.trim());
@@ -310,7 +359,7 @@ class MemoryEngine {
310
359
  else {
311
360
  for (const tag of topics.sort()) {
312
361
  const count = entries.filter((e) => this.getPrimaryTag(e) === tag).length;
313
- console.log(`- **${tag}** (${count}건)`);
362
+ console.log(`- **${tag}** (${count} items)`);
314
363
  }
315
364
  }
316
365
  }
@@ -343,15 +392,15 @@ class MemoryEngine {
343
392
  byCategory[category] = [];
344
393
  byCategory[category].push(entry);
345
394
  }
346
- let md = `# ${agentId} λ©”λͺ¨λ¦¬\n\n`;
395
+ let md = `# ${agentId} memory\n\n`;
347
396
  md += `> Last Updated: ${today}\n`;
348
397
  md += `> Total: ${entries.length} memories\n\n`;
349
- md += `## πŸ”₯ 졜근 ${this.recentDays}일\n\n`;
398
+ md += `## πŸ”₯ Last ${this.recentDays} days\n\n`;
350
399
  if (recent.length === 0) {
351
- md += `(μ—†μŒ)\n\n`;
400
+ md += `(none)\n\n`;
352
401
  }
353
402
  else {
354
- md += `| ID | λ‚ μ§œ | μΉ΄ν…Œκ³ λ¦¬ | μš”μ•½ | 상세 |\n`;
403
+ md += `| ID | Date | Category | Summary | Details |\n`;
355
404
  md += `|----|------|----------|------|------|\n`;
356
405
  for (const entry of recent) {
357
406
  const detailIcon = entry.hasBody ? 'πŸ“„' : '-';
@@ -359,47 +408,43 @@ class MemoryEngine {
359
408
  }
360
409
  md += `\n`;
361
410
  }
362
- md += `## 🏷️ μΉ΄ν…Œκ³ λ¦¬λ³„\n\n`;
411
+ md += `## 🏷️ By category\n\n`;
363
412
  for (const [category, items] of Object.entries(byCategory).sort()) {
364
- md += `- **${category}**: ${items.length}건\n`;
413
+ md += `- **${category}**: ${items.length} items\n`;
365
414
  }
366
415
  const olderCount = entries.length - recent.length;
367
416
  const oldestDate = entries[entries.length - 1]?.date ?? '-';
368
417
  const newestDate = entries[0]?.date ?? '-';
369
- md += `\n## πŸ“Š 톡계\n\n`;
370
- md += `- 전체: ${entries.length}건 (${oldestDate} ~ ${newestDate})\n`;
371
- md += `- 졜근 ${this.recentDays}일: ${recent.length}건\n`;
418
+ md += `\n## πŸ“Š Stats\n\n`;
419
+ md += `- Total: ${entries.length} items (${oldestDate} ~ ${newestDate})\n`;
420
+ md += `- Last ${this.recentDays} days: ${recent.length} items\n`;
372
421
  if (olderCount > 0)
373
- md += `- ${this.recentDays}일 이전: ${olderCount}건\n`;
422
+ md += `- Older than ${this.recentDays} days: ${olderCount} items\n`;
374
423
  return md;
375
424
  }
376
425
  topic(agentId, topicName) {
377
426
  const entries = this.loadAllEntries(agentId);
378
427
  const filtered = entries.filter((e) => this.getPrimaryTag(e) === topicName);
379
428
  if (filtered.length === 0) {
380
- console.log(`ν† ν”½ '${topicName}'에 ν•΄λ‹Ήν•˜λŠ” 기얡이 μ—†μŠ΅λ‹ˆλ‹€.`);
429
+ console.log(`No memories found for topic '${topicName}'.`);
381
430
  return;
382
431
  }
383
- console.log(`## πŸ“‚ ν† ν”½: ${topicName} (${filtered.length}건)\n`);
432
+ console.log(`## πŸ“‚ Topic: ${topicName} (${filtered.length} items)\n`);
384
433
  for (const entry of filtered) {
385
434
  console.log(`### [${entry.date}] ${entry.summary}`);
386
- console.log(`- 파일: data/${agentId}/entries/${entry.file}`);
387
- console.log(`- μΉ΄ν…Œκ³ λ¦¬: ${entry.category ?? '-'}`);
388
- console.log(`- νƒœκ·Έ: ${(entry.tags ?? []).join(', ') || '-'}`);
435
+ console.log(`- File: data/${agentId}/entries/${entry.file}`);
436
+ console.log(`- Category: ${entry.category ?? '-'}`);
437
+ console.log(`- Tags: ${(entry.tags ?? []).join(', ') || '-'}`);
389
438
  console.log('');
390
439
  }
391
440
  }
392
441
  recent(agentId, days = 30) {
393
- const entries = this.loadAllEntries(agentId);
394
- const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000)
395
- .toISOString()
396
- .slice(0, 10);
397
- const filtered = entries.filter((e) => e.date >= cutoff);
442
+ const filtered = this.getRecentEntries(agentId, days);
398
443
  if (filtered.length === 0) {
399
- console.log(`졜근 ${days}일 λ‚΄ 기얡이 μ—†μŠ΅λ‹ˆλ‹€.`);
444
+ console.log(`No memories found in the last ${days} days.`);
400
445
  return;
401
446
  }
402
- console.log(`## πŸ”₯ 졜근 ${days}일 (${filtered.length}건)\n`);
447
+ console.log(`## πŸ”₯ Last ${days} days (${filtered.length} items)\n`);
403
448
  for (const entry of filtered) {
404
449
  const detailIcon = entry.hasBody ? ' πŸ“„' : '';
405
450
  const timestamp = String(entry.date).includes('T')
@@ -408,10 +453,59 @@ class MemoryEngine {
408
453
  console.log(`[${entry.id}] [${timestamp}] [${entry.category ?? '-'}] ${entry.summary}${detailIcon}`);
409
454
  }
410
455
  }
456
+ getRecentEntries(agentId, days = 30, opts = {}) {
457
+ const entries = this.loadAllEntries(agentId);
458
+ const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000)
459
+ .toISOString()
460
+ .slice(0, 10);
461
+ const filtered = entries.filter((e) => this.resolveRecentDate(agentId, e).slice(0, 10) >= cutoff);
462
+ if (!opts.includeBody)
463
+ return filtered;
464
+ return filtered.map((e) => ({
465
+ ...e,
466
+ body: this.readEntryBody(agentId, e.file),
467
+ }));
468
+ }
469
+ resolveRecentDate(agentId, entry) {
470
+ const updated = this.toDateString(entry.updated);
471
+ if (updated)
472
+ return updated;
473
+ const createdAt = this.toDateString(entry['created_at']);
474
+ if (createdAt)
475
+ return createdAt;
476
+ const date = this.toDateString(entry.date);
477
+ if (date)
478
+ return date;
479
+ try {
480
+ const filePath = path_1.default.join(this.getEntriesDir(agentId), entry.file);
481
+ return fs_1.default.statSync(filePath).mtime.toISOString();
482
+ }
483
+ catch {
484
+ return '';
485
+ }
486
+ }
487
+ toDateString(value) {
488
+ if (value &&
489
+ typeof value === 'object' &&
490
+ typeof value.toISOString === 'function') {
491
+ return value.toISOString();
492
+ }
493
+ return typeof value === 'string' ? value : '';
494
+ }
495
+ readEntryBody(agentId, file) {
496
+ try {
497
+ const raw = fs_1.default.readFileSync(path_1.default.join(this.getEntriesDir(agentId), file), 'utf-8');
498
+ const { content } = (0, parser_1.parseFrontmatter)(raw);
499
+ return content.replace(/^#[^\n]*\n*/m, '').trim();
500
+ }
501
+ catch {
502
+ return '';
503
+ }
504
+ }
411
505
  find(agentId, keyword) {
412
506
  const entriesDir = this.getEntriesDir(agentId);
413
507
  if (!fs_1.default.existsSync(entriesDir)) {
414
- console.log('기얡이 μ—†μŠ΅λ‹ˆλ‹€.');
508
+ console.log('No memories found.');
415
509
  return;
416
510
  }
417
511
  const files = fs_1.default.readdirSync(entriesDir).filter((f) => f.endsWith('.md'));
@@ -423,18 +517,18 @@ class MemoryEngine {
423
517
  entries.push({ ...entry, file, body });
424
518
  }
425
519
  if (entries.length === 0) {
426
- console.log('기얡이 μ—†μŠ΅λ‹ˆλ‹€.');
520
+ console.log('No memories found.');
427
521
  return;
428
522
  }
429
523
  const kcEntries = entries;
430
524
  const results = (0, knowledge_core_1.searchEntries)(kcEntries, keyword, { summary: 3, tags: 2, body: 1 });
431
525
  if (results.length === 0) {
432
- console.log(`'${keyword}' κ΄€λ ¨ 기얡을 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.`);
526
+ console.log(`No related memories found for '${keyword}'.`);
433
527
  return;
434
528
  }
435
- console.log(`## πŸ” 검색 κ²°κ³Ό: "${keyword}" (${results.length}건, BM25 점수순)\n`);
529
+ console.log(`## πŸ” Search results: "${keyword}" (${results.length} items, sorted by BM25 score)\n`);
436
530
  for (const entry of results) {
437
- const score = entry._score != null ? ` [${entry._score}점]` : '';
531
+ const score = entry._score != null ? ` [${entry._score}pts]` : '';
438
532
  console.log(`[${entry.id}] [${entry.date}] [${this.getPrimaryTag(entry)}]${score} ${entry.summary}`);
439
533
  }
440
534
  const resultTags = [...new Set(results.flatMap(e => e.tags ?? []))].slice(0, 3);
@@ -470,7 +564,7 @@ class MemoryEngine {
470
564
  const raw = fs_1.default.readFileSync(path_1.default.join(entriesDir, e.file), 'utf-8');
471
565
  const { content: rawContent } = (0, parser_1.parseFrontmatter)(raw);
472
566
  const body = rawContent.replace(/^#[^\n]*\n*/m, '').trim();
473
- if (body !== '(상세 λ‚΄μš©μ„ 여기에 μΆ”κ°€)')
567
+ if (body !== '(Add details here)')
474
568
  content = body;
475
569
  }
476
570
  catch (_err) {
@@ -494,10 +588,10 @@ class MemoryEngine {
494
588
  filters.push(`category=${options.category}`);
495
589
  if (options.tag)
496
590
  filters.push(`tag=${options.tag}`);
497
- console.log(`쑰건에 λ§žλŠ” 기얡이 μ—†μŠ΅λ‹ˆλ‹€. (${filters.join(', ') || '전체'})`);
591
+ console.log(`No memories match the filters. (${filters.join(', ') || 'all'})`);
498
592
  return;
499
593
  }
500
- console.log(`## πŸ“‹ λͺ©λ‘: ${agentId} (${filtered.length}건)\n`);
594
+ console.log(`## πŸ“‹ List: ${agentId} (${filtered.length} items)\n`);
501
595
  for (const entry of filtered) {
502
596
  console.log(`[${entry.id}] [${entry.date}] [${this.getPrimaryTag(entry)}] ${entry.summary}`);
503
597
  }
@@ -518,7 +612,7 @@ class MemoryEngine {
518
612
  stats(agentId) {
519
613
  const entries = this.loadAllEntries(agentId);
520
614
  if (entries.length === 0) {
521
- console.log('기얡이 μ—†μŠ΅λ‹ˆλ‹€.');
615
+ console.log('No memories found.');
522
616
  return;
523
617
  }
524
618
  const byCategory = {};
@@ -532,15 +626,15 @@ class MemoryEngine {
532
626
  byTag[tag] = (byTag[tag] ?? 0) + 1;
533
627
  }
534
628
  }
535
- console.log(`# ${agentId} 톡계 (${entries.length}건)\n`);
536
- console.log(`## μΉ΄ν…Œκ³ λ¦¬λ³„`);
629
+ console.log(`# ${agentId} Stats (${entries.length} items)\n`);
630
+ console.log(`## By category`);
537
631
  for (const [cat, count] of Object.entries(byCategory).sort((a, b) => b[1] - a[1])) {
538
- console.log(` ${cat}: ${count}건`);
632
+ console.log(` ${cat}: ${count} items`);
539
633
  }
540
- console.log(`\n## νƒœκ·Έ Top 20`);
634
+ console.log(`\n## Top 20 Tags`);
541
635
  const sortedTags = Object.entries(byTag).sort((a, b) => b[1] - a[1]).slice(0, 20);
542
636
  for (const [tag, count] of sortedTags) {
543
- console.log(` ${tag}: ${count}건`);
637
+ console.log(` ${tag}: ${count} items`);
544
638
  }
545
639
  const topCategories = Object.entries(byCategory).sort((a, b) => b[1] - a[1]).map(t => t[0]);
546
640
  const statsCtx = {
@@ -557,7 +651,7 @@ class MemoryEngine {
557
651
  tags(agentId) {
558
652
  const entries = this.loadAllEntries(agentId);
559
653
  if (entries.length === 0) {
560
- console.log('기얡이 μ—†μŠ΅λ‹ˆλ‹€.');
654
+ console.log('No memories found.');
561
655
  return;
562
656
  }
563
657
  const byTag = {};
@@ -568,18 +662,18 @@ class MemoryEngine {
568
662
  }
569
663
  const sortedTags = Object.entries(byTag).sort((a, b) => b[1] - a[1]);
570
664
  if (sortedTags.length === 0) {
571
- console.log('νƒœκ·Έκ°€ μ—†μŠ΅λ‹ˆλ‹€.');
665
+ console.log('No tags found.');
572
666
  return;
573
667
  }
574
- console.log(`## 🏷️ 전체 νƒœκ·Έ (${sortedTags.length}개)\n`);
668
+ console.log(`## 🏷️ All tags (${sortedTags.length} items)\n`);
575
669
  for (const [tag, count] of sortedTags) {
576
- console.log(` ${tag}: ${count}건`);
670
+ console.log(` ${tag}: ${count} items`);
577
671
  }
578
672
  }
579
673
  get(agentId, memoryId) {
580
674
  const entriesDir = this.getEntriesDir(agentId);
581
675
  if (!fs_1.default.existsSync(entriesDir)) {
582
- console.log('기얡이 μ—†μŠ΅λ‹ˆλ‹€.');
676
+ console.log('No memories found.');
583
677
  return;
584
678
  }
585
679
  const files = fs_1.default.readdirSync(entriesDir).filter((f) => f.endsWith('.md'));
@@ -591,10 +685,10 @@ class MemoryEngine {
591
685
  const d = data;
592
686
  console.log(`## πŸ“„ ${d.summary}\n`);
593
687
  console.log(`- ID: ${d.id}`);
594
- console.log(`- λ‚ μ§œ: ${d.date}`);
595
- console.log(`- μΉ΄ν…Œκ³ λ¦¬: ${d.category ?? '-'}`);
596
- console.log(`- νƒœκ·Έ: ${(d.tags ?? []).join(', ') || '-'}`);
597
- console.log(`- 파일: data/${agentId}/entries/${file}`);
688
+ console.log(`- Date: ${d.date}`);
689
+ console.log(`- Category: ${d.category ?? '-'}`);
690
+ console.log(`- Tags: ${(d.tags ?? []).join(', ') || '-'}`);
691
+ console.log(`- File: data/${agentId}/entries/${file}`);
598
692
  console.log(`\n---\n`);
599
693
  console.log(content.trim());
600
694
  const graphPath = path_1.default.join(this.getAgentDir(agentId), 'graph.json');
@@ -611,7 +705,7 @@ class MemoryEngine {
611
705
  nodeMap[n.id] = n;
612
706
  });
613
707
  console.log(`\n---\n`);
614
- console.log(`## πŸ”— μ—°κ΄€ κΈ°μ–΅ (${relatedEdges.length}개)\n`);
708
+ console.log(`## πŸ”— Related memories (${relatedEdges.length} items)\n`);
615
709
  for (const edge of relatedEdges) {
616
710
  const otherId = edge.from === memoryId ? edge.to : edge.from;
617
711
  const other = nodeMap[otherId];
@@ -638,12 +732,12 @@ class MemoryEngine {
638
732
  return;
639
733
  }
640
734
  }
641
- console.log(`ID '${memoryId}'λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.`);
735
+ console.log(`ID '${memoryId}' not found.`);
642
736
  }
643
737
  update(agentId, memoryId, options = {}) {
644
738
  const entry = this.findEntryById(agentId, memoryId);
645
739
  if (!entry) {
646
- console.log(`❌ ID '${memoryId}'λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.`);
740
+ console.log(`❌ ID '${memoryId}' not found.`);
647
741
  process.exit(1);
648
742
  }
649
743
  const { filePath, data, content } = entry;
@@ -664,11 +758,11 @@ class MemoryEngine {
664
758
  else if (options['no-important']) {
665
759
  delete newData['important'];
666
760
  }
667
- const newBody = options.body ?? existingBody ?? '(상세 λ‚΄μš©μ„ 여기에 μΆ”κ°€)';
761
+ const newBody = options.body ?? existingBody ?? '(Add details here)';
668
762
  const newFrontmatter = (0, parser_1.stringifyFrontmatter)(newData);
669
763
  const newContent = `${newFrontmatter}\n\n# ${String(newData['summary'])}\n\n${newBody}\n`;
670
764
  fs_1.default.writeFileSync(filePath, newContent, 'utf-8');
671
- console.log(`βœ… μˆ˜μ • μ™„λ£Œ: ${entry.file}`);
765
+ console.log(`βœ… Updated: ${entry.file}`);
672
766
  console.log(` ID: ${memoryId}`);
673
767
  if (options.summary)
674
768
  console.log(` Summary: ${String(newData['summary'])}`);
@@ -677,28 +771,28 @@ class MemoryEngine {
677
771
  if (options.tags)
678
772
  console.log(` Tags: ${newData['tags'].join(', ')}`);
679
773
  if (options.body)
680
- console.log(` Body: (μ—…λ°μ΄νŠΈλ¨)`);
774
+ console.log(` Body: (updated)`);
681
775
  }
682
776
  delete(agentId, memoryId, options = {}) {
683
777
  const entry = this.findEntryById(agentId, memoryId);
684
778
  if (!entry) {
685
- console.log(`❌ ID '${memoryId}'λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.`);
779
+ console.log(`❌ ID '${memoryId}' not found.`);
686
780
  process.exit(1);
687
781
  }
688
782
  if (!options.force) {
689
- console.log(`⚠️ μ‚­μ œ λŒ€μƒ: [${entry.data.id}] ${entry.data.summary}`);
690
- console.log(` 파일: ${entry.file}`);
691
- console.log(`\n--force μ˜΅μ…˜μ„ μΆ”κ°€ν•˜λ©΄ μ‚­μ œλ©λ‹ˆλ‹€.`);
692
- console.log(`예: memory delete ${agentId} ${memoryId} --force`);
783
+ console.log(`⚠️ Delete target: [${entry.data.id}] ${entry.data.summary}`);
784
+ console.log(` File: ${entry.file}`);
785
+ console.log(`\nAdd --force to delete it.`);
786
+ console.log(`Example: memory delete ${agentId} ${memoryId} --force`);
693
787
  return;
694
788
  }
695
789
  fs_1.default.unlinkSync(entry.filePath);
696
- console.log(`πŸ—‘οΈ μ‚­μ œ μ™„λ£Œ: ${entry.file}`);
790
+ console.log(`πŸ—‘οΈ Deleted: ${entry.file}`);
697
791
  console.log(` ID: ${memoryId}`);
698
792
  console.log(` Summary: ${entry.data.summary}`);
699
793
  try {
700
794
  this.markSummaryDirty(agentId);
701
- console.log(` πŸ“ summary.md κ°±μ‹  μ˜ˆμ•½λ¨`);
795
+ console.log(` πŸ“ summary.md update scheduled`);
702
796
  }
703
797
  catch (_e) {
704
798
  }
@@ -707,27 +801,27 @@ class MemoryEngine {
707
801
  const entry1 = this.findEntryById(agentId, memoryId1);
708
802
  const entry2 = this.findEntryById(agentId, memoryId2);
709
803
  if (!entry1) {
710
- console.log(`❌ ID '${memoryId1}'λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.`);
804
+ console.log(`❌ ID '${memoryId1}' not found.`);
711
805
  process.exit(1);
712
806
  }
713
807
  if (!entry2) {
714
- console.log(`❌ ID '${memoryId2}'λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.`);
808
+ console.log(`❌ ID '${memoryId2}' not found.`);
715
809
  process.exit(1);
716
810
  }
717
811
  const body1 = entry1.content.replace(/^#[^\n]*\n*/m, '').trim();
718
812
  const body2 = entry2.content.replace(/^#[^\n]*\n*/m, '').trim();
719
813
  const mergedSummary = options.summary ?? `${entry1.data.summary} + ${entry2.data.summary}`;
720
- const mergedBody = `## λ³‘ν•©λœ κΈ°μ–΅ 1 (${entry1.data.id}, ${entry1.data.date})
814
+ const mergedBody = `## Merged memory 1 (${entry1.data.id}, ${entry1.data.date})
721
815
  ${entry1.data.summary}
722
816
 
723
- ${body1 !== '(상세 λ‚΄μš©μ„ 여기에 μΆ”κ°€)' ? body1 : '(상세 μ—†μŒ)'}
817
+ ${body1 !== '(Add details here)' ? body1 : '(No details)'}
724
818
 
725
819
  ---
726
820
 
727
- ## λ³‘ν•©λœ κΈ°μ–΅ 2 (${entry2.data.id}, ${entry2.data.date})
821
+ ## Merged memory 2 (${entry2.data.id}, ${entry2.data.date})
728
822
  ${entry2.data.summary}
729
823
 
730
- ${body2 !== '(상세 λ‚΄μš©μ„ 여기에 μΆ”κ°€)' ? body2 : '(상세 μ—†μŒ)'}`;
824
+ ${body2 !== '(Add details here)' ? body2 : '(No details)'}`;
731
825
  const mergedTags = [
732
826
  ...new Set([...(entry1.data.tags ?? []), ...(entry2.data.tags ?? [])]),
733
827
  ];
@@ -749,10 +843,10 @@ ${body2 !== '(상세 λ‚΄μš©μ„ 여기에 μΆ”κ°€)' ? body2 : '(상세 μ—†μŒ)'}`;
749
843
  fs_1.default.writeFileSync(filepath, newContent, 'utf-8');
750
844
  fs_1.default.unlinkSync(entry1.filePath);
751
845
  fs_1.default.unlinkSync(entry2.filePath);
752
- console.log(`πŸ”€ 병합 μ™„λ£Œ!`);
753
- console.log(` μƒˆ ID: ${newId}`);
754
- console.log(` μƒˆ 파일: ${filename}`);
755
- console.log(` λ³‘ν•©λœ κΈ°μ–΅: [${memoryId1}] + [${memoryId2}]`);
846
+ console.log(`πŸ”€ Merge complete!`);
847
+ console.log(` New ID: ${newId}`);
848
+ console.log(` New file: ${filename}`);
849
+ console.log(` Merged memory: [${memoryId1}] + [${memoryId2}]`);
756
850
  console.log(` Summary: ${mergedSummary}`);
757
851
  }
758
852
  buildFallbackSummary(entries) {
@@ -778,7 +872,7 @@ ${body2 !== '(상세 λ‚΄μš©μ„ 여기에 μΆ”κ°€)' ? body2 : '(상세 μ—†μŒ)'}`;
778
872
  const oldest = dates[0];
779
873
  const newest = dates[dates.length - 1];
780
874
  const rangeLabel = oldest && newest ? `, ${toMD(oldest)}~${toMD(newest)}` : '';
781
- body += `## ${topic} (${items.length}건${rangeLabel})\n`;
875
+ body += `## ${topic} (${items.length} items${rangeLabel})\n`;
782
876
  for (const e of items)
783
877
  body += `- [${e.date}] ${e.summary}\n`;
784
878
  for (const e of items)
@@ -788,22 +882,26 @@ ${body2 !== '(상세 λ‚΄μš©μ„ 여기에 μΆ”κ°€)' ? body2 : '(상세 μ—†μŒ)'}`;
788
882
  return body;
789
883
  }
790
884
  callSummarizer(prompt) {
791
- const tmpFile = path_1.default.join(os_1.default.tmpdir(), `memory-summarizer-${Date.now()}.txt`);
792
885
  try {
793
- fs_1.default.writeFileSync(tmpFile, prompt, 'utf-8');
794
- const result = (0, child_process_1.execSync)(`${CREWX_CLI} q "${this.summarizerAgent} $(cat '${tmpFile}')"`, { encoding: 'utf-8', timeout: this.summarizerTimeout, stdio: ['pipe', 'pipe', 'pipe'] });
795
- return parseCrewxResponse(result);
796
- }
797
- catch (_e) {
886
+ const result = (0, child_process_1.execSync)(`${CREWX_CLI} q "${this.summarizerAgent}"`, {
887
+ encoding: 'utf-8',
888
+ timeout: this.summarizerTimeout,
889
+ stdio: ['pipe', 'pipe', 'pipe'],
890
+ input: prompt,
891
+ });
892
+ const parsed = parseCrewxResponse(result);
893
+ this.recordSummarizerSuccess();
894
+ return parsed;
895
+ }
896
+ catch (e) {
897
+ const err = e;
898
+ const reason = err.code === 'ETIMEDOUT' || err.killed
899
+ ? `timed out after ${this.summarizerTimeout}ms`
900
+ : err.message;
901
+ console.error(`[memory] callSummarizer failed (agent=${this.summarizerAgent}), falling back to raw entry list: ${reason}`);
902
+ this.recordSummarizerFailure();
798
903
  return null;
799
904
  }
800
- finally {
801
- try {
802
- fs_1.default.unlinkSync(tmpFile);
803
- }
804
- catch (_e) {
805
- }
806
- }
807
905
  }
808
906
  generateSummary(agentId, options = {}) {
809
907
  const allEntries = this.loadAllEntries(agentId);
@@ -861,13 +959,13 @@ ${body2 !== '(상세 λ‚΄μš©μ„ 여기에 μΆ”κ°€)' ? body2 : '(상세 μ—†μŒ)'}`;
861
959
  for (const entry of newItems) {
862
960
  const importantTag = entry.important ? ' [important]' : '';
863
961
  p += `- [${entry.id}] [${entry.date}] [${this.getPrimaryTag(entry)}]${importantTag} ${entry.summary}`;
864
- if (entry.body && entry.body !== '(상세 λ‚΄μš©μ„ 여기에 μΆ”κ°€)') {
962
+ if (entry.body && entry.body !== '(Add details here)') {
865
963
  p += ` β€” ${entry.body.slice(0, 200)}`;
866
964
  }
867
965
  p += ` (file: entries/${entry.file})\n`;
868
966
  }
869
967
  p += `\n## Instructions:\n`;
870
- p += `- Group by topic, each topic as ## heading with entry count and date range, e.g. "## mcp-http (4건, 01/28~02/14)"\n`;
968
+ p += `- Group by topic, each topic as ## heading with entry count and date range, e.g. "## mcp-http (4 items, 01/28~02/14)"\n`;
871
969
  p += `- Write concise Korean summary per topic (2-5 sentences)\n`;
872
970
  p += `- After each topic summary, list relevant entry file links as "β†’ [entries/filename](entries/filename)"\n`;
873
971
  p += `- Highlight key decisions, insights, and action items\n`;
@@ -917,76 +1015,141 @@ ${body2 !== '(상세 λ‚΄μš©μ„ 여기에 μΆ”κ°€)' ? body2 : '(상세 μ—†μŒ)'}`;
917
1015
  summarize(agentId, options = {}) {
918
1016
  const entries = this.loadAllEntries(agentId);
919
1017
  if (entries.length === 0) {
920
- console.log('기얡이 μ—†μŠ΅λ‹ˆλ‹€.');
1018
+ console.log('No memories found.');
921
1019
  return;
922
1020
  }
923
- console.log(`πŸ“ μš”μ•½ 생성 μ‹œμž‘ (${entries.length}개 μ—”νŠΈλ¦¬)\n`);
1021
+ console.log(`πŸ“ Starting summary generation (${entries.length} items entries)\n`);
924
1022
  const result = this.generateSummary(agentId, { force: options.force });
925
1023
  if (result.status === 'up-to-date') {
926
- console.log('βœ… summary.md μ΅œμ‹  μƒνƒœμž…λ‹ˆλ‹€.');
1024
+ console.log('βœ… summary.md is up to date.');
927
1025
  }
928
1026
  else if (result.status === 'fallback') {
929
- console.log(`πŸ“‹ 폴백 μš”μ•½ 생성 μ™„λ£Œ (AI 호좜 μ‹€νŒ¨)`);
1027
+ console.log(`πŸ“‹ Fallback summary generated (AI call failed)`);
930
1028
  }
931
1029
  else if (result.status === 'updated') {
932
- console.log(`πŸ€– μš”μ•½ μ™„λ£Œ (${result.newEntries}건 반영)`);
1030
+ console.log(`πŸ€– Summary complete (${result.newEntries} items applied)`);
1031
+ }
1032
+ console.log(` File: data/${agentId}/summary.md`);
1033
+ console.log(` entries: ${result.entryCount} items`);
1034
+ }
1035
+ isSummarizeLockStale(lockPath) {
1036
+ let content;
1037
+ try {
1038
+ content = fs_1.default.readFileSync(lockPath, 'utf-8');
1039
+ }
1040
+ catch {
1041
+ return true;
1042
+ }
1043
+ const pidMatch = content.match(/^pid=(\d+)/m);
1044
+ const pid = pidMatch ? Number(pidMatch[1]) : NaN;
1045
+ if (Number.isFinite(pid)) {
1046
+ try {
1047
+ process.kill(pid, 0);
1048
+ return false;
1049
+ }
1050
+ catch (e) {
1051
+ const err = e;
1052
+ if (err.code === 'ESRCH')
1053
+ return true;
1054
+ }
1055
+ }
1056
+ try {
1057
+ const stat = fs_1.default.statSync(lockPath);
1058
+ return Date.now() - stat.mtimeMs > exports.SUMMARIZE_LOCK_STALE_MS;
1059
+ }
1060
+ catch {
1061
+ return true;
1062
+ }
1063
+ }
1064
+ acquireSummarizeLock() {
1065
+ const lockPath = this.getSummarizeLockPath();
1066
+ const content = `pid=${process.pid}\nmtime=${new Date().toISOString()}\n`;
1067
+ try {
1068
+ const fd = fs_1.default.openSync(lockPath, 'wx');
1069
+ fs_1.default.writeSync(fd, content);
1070
+ fs_1.default.closeSync(fd);
1071
+ return true;
1072
+ }
1073
+ catch (e) {
1074
+ const err = e;
1075
+ if (err.code !== 'EEXIST')
1076
+ throw err;
1077
+ }
1078
+ if (!this.isSummarizeLockStale(lockPath)) {
1079
+ return false;
1080
+ }
1081
+ fs_1.default.writeFileSync(lockPath, content, 'utf-8');
1082
+ return true;
1083
+ }
1084
+ releaseSummarizeLock() {
1085
+ try {
1086
+ fs_1.default.unlinkSync(this.getSummarizeLockPath());
1087
+ }
1088
+ catch {
933
1089
  }
934
- console.log(` 파일: data/${agentId}/summary.md`);
935
- console.log(` μ—”νŠΈλ¦¬: ${result.entryCount}개`);
936
1090
  }
937
1091
  summarizeDirty() {
938
1092
  if (!fs_1.default.existsSync(this.dataDir)) {
939
- console.log(`πŸ’‘ λ””λ°”μš΄μŠ€ cron 등둝:`);
1093
+ console.log(`πŸ’‘ Register debounce cron:`);
940
1094
  console.log(`npx cron add "*/1 * * * *" "npx memory summarize-dirty" --mode command --name "memory-debounce"`);
941
1095
  return;
942
1096
  }
943
- const agentDirs = fs_1.default
944
- .readdirSync(this.dataDir, { withFileTypes: true })
945
- .filter((entry) => entry.isDirectory())
946
- .map((entry) => entry.name);
947
- for (const agentId of agentDirs) {
948
- const dirtyPath = this.getDirtySummaryPath(agentId);
949
- if (!fs_1.default.existsSync(dirtyPath))
950
- continue;
951
- try {
952
- const result = this.generateSummary(agentId);
953
- if (result.status === 'updated') {
954
- console.log(`πŸ€– ${agentId} summary.md κ°±μ‹  (${result.newEntries}건 반영)`);
955
- }
956
- else if (result.status === 'fallback') {
957
- console.log(`πŸ“‹ ${agentId} 폴백 μš”μ•½ 생성 μ™„λ£Œ (AI 호좜 μ‹€νŒ¨)`);
958
- }
959
- else if (result.status === 'up-to-date') {
960
- console.log(`βœ… ${agentId} summary.md μ΅œμ‹  μƒνƒœμž…λ‹ˆλ‹€.`);
1097
+ if (!this.acquireSummarizeLock()) {
1098
+ console.log(`⏭️ summarize-dirty is already running (lock held) β€” skipping this run.`);
1099
+ return;
1100
+ }
1101
+ try {
1102
+ const agentDirs = fs_1.default
1103
+ .readdirSync(this.dataDir, { withFileTypes: true })
1104
+ .filter((entry) => entry.isDirectory())
1105
+ .map((entry) => entry.name);
1106
+ for (const agentId of agentDirs) {
1107
+ const dirtyPath = this.getDirtySummaryPath(agentId);
1108
+ if (!fs_1.default.existsSync(dirtyPath))
1109
+ continue;
1110
+ try {
1111
+ const result = this.generateSummary(agentId);
1112
+ if (result.status === 'updated') {
1113
+ console.log(`πŸ€– ${agentId} summary.md updated (${result.newEntries} items applied)`);
1114
+ }
1115
+ else if (result.status === 'fallback') {
1116
+ console.log(`πŸ“‹ ${agentId} Fallback summary generated (AI call failed)`);
1117
+ }
1118
+ else if (result.status === 'up-to-date') {
1119
+ console.log(`βœ… ${agentId} summary.md is up to date.`);
1120
+ }
1121
+ else if (result.status === 'empty') {
1122
+ console.log(`ℹ️ ${agentId} has no memories to summarize.`);
1123
+ }
1124
+ fs_1.default.unlinkSync(dirtyPath);
961
1125
  }
962
- else if (result.status === 'empty') {
963
- console.log(`ℹ️ ${agentId} μš”μ•½ν•  기얡이 μ—†μŠ΅λ‹ˆλ‹€.`);
1126
+ catch (e) {
1127
+ console.error(`❌ ${agentId} summary.md update failed: ${e.message}`);
964
1128
  }
965
- fs_1.default.unlinkSync(dirtyPath);
966
- }
967
- catch (e) {
968
- console.error(`❌ ${agentId} summary.md κ°±μ‹  μ‹€νŒ¨: ${e.message}`);
969
1129
  }
970
1130
  }
971
- console.log(`πŸ’‘ λ””λ°”μš΄μŠ€ cron 등둝:`);
1131
+ finally {
1132
+ this.releaseSummarizeLock();
1133
+ }
1134
+ console.log(`πŸ’‘ Register debounce cron:`);
972
1135
  console.log(`npx cron add "*/1 * * * *" "npx memory summarize-dirty" --mode command --name "memory-debounce"`);
973
1136
  }
974
1137
  search(agentId, query) {
975
1138
  const entries = this.loadAllEntries(agentId);
976
1139
  if (entries.length === 0) {
977
- console.log('기얡이 μ—†μŠ΅λ‹ˆλ‹€.');
1140
+ console.log('No memories found.');
978
1141
  return;
979
1142
  }
980
1143
  const memoryList = entries
981
1144
  .map((e) => `[${e.id}] [${e.date}] [${this.getPrimaryTag(e)}] ${e.summary}`)
982
1145
  .join('\n');
983
- const task = `κΈ°μ–΅ λͺ©λ‘:\n${memoryList}\n\n질문: "${query}"`;
1146
+ const task = `Memory list:\n${memoryList}\n\nQuestion: "${query}"`;
984
1147
  try {
985
- console.log(`πŸ” "${query}" 검색 쀑... (${this.searcherAgent})\n`);
1148
+ console.log(`πŸ” "${query}" searching... (${this.searcherAgent})\n`);
986
1149
  const result = (0, child_process_1.execSync)(`${CREWX_CLI} q "${this.searcherAgent} ${task.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { encoding: 'utf-8', timeout: this.searcherTimeout, stdio: ['pipe', 'pipe', 'pipe'] });
987
1150
  const parsed = parseCrewxResponse(result);
988
1151
  if (parsed) {
989
- console.log(`## 🧠 μ‹œλ§¨ν‹± 검색 κ²°κ³Ό\n`);
1152
+ console.log(`## 🧠 Semantic search results\n`);
990
1153
  console.log(parsed);
991
1154
  }
992
1155
  else {
@@ -996,12 +1159,12 @@ ${body2 !== '(상세 λ‚΄μš©μ„ 여기에 μΆ”κ°€)' ? body2 : '(상세 μ—†μŒ)'}`;
996
1159
  catch (error) {
997
1160
  const err = error;
998
1161
  if (err.code === 'ETIMEDOUT' || err.killed) {
999
- console.log('검색 μ‹€νŒ¨: μ‹œκ°„ 초과 (μ΅œλŒ€ 5λΆ„)');
1162
+ console.log('Search failed: timed out (max 5 minutes)');
1000
1163
  }
1001
1164
  else {
1002
- console.log('검색 μ‹€νŒ¨:', err.message);
1165
+ console.log('Search failed:', err.message);
1003
1166
  }
1004
- console.log('\nπŸ’‘ Tip: find λͺ…λ ΉμœΌλ‘œ ν‚€μ›Œλ“œ 검색을 μ‹œλ„ν•΄λ³΄μ„Έμš”.');
1167
+ console.log('\nπŸ’‘ Tip: try keyword search with the find command.');
1005
1168
  }
1006
1169
  }
1007
1170
  }