@crewx/memory 0.1.23-rc.9 β†’ 0.1.23-rc.91

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,21 @@ 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
+ const FIND_RESULT_LIMIT = 50;
20
+ exports.SUMMARIZE_LOCK_STALE_MS = 10 * 60 * 1000;
18
21
  function parseCrewxResponse(raw) {
19
22
  const responseMatch = raw.match(/πŸ“„ Response:\s*─+\s*([\s\S]*?)(?=πŸ“ Working Directory|$)/);
20
23
  if (responseMatch?.[1]) {
@@ -75,6 +78,7 @@ function getMemoryConfig() {
75
78
  }
76
79
  class MemoryEngine {
77
80
  constructor(config = {}) {
81
+ this.searchIndexCache = new Map();
78
82
  const yamlConfig = getMemoryConfig();
79
83
  this.dataDir =
80
84
  config.dataDir ??
@@ -103,7 +107,7 @@ class MemoryEngine {
103
107
  this.summarizerTimeout =
104
108
  config.summarizerTimeout ??
105
109
  (yamlConfig.summarizer_timeout != null ? Number(yamlConfig.summarizer_timeout) : undefined) ??
106
- 60000;
110
+ 480000;
107
111
  this.searcherTimeout =
108
112
  config.searcherTimeout ??
109
113
  (yamlConfig.searcher_timeout != null ? Number(yamlConfig.searcher_timeout) : undefined) ??
@@ -115,12 +119,59 @@ class MemoryEngine {
115
119
  getEntriesDir(agentId) {
116
120
  return path_1.default.join(this.getAgentDir(agentId), 'entries');
117
121
  }
122
+ getEntryFilesFingerprint(agentId) {
123
+ const entriesDir = this.getEntriesDir(agentId);
124
+ if (!fs_1.default.existsSync(entriesDir))
125
+ return { files: [], fingerprint: '' };
126
+ const files = fs_1.default.readdirSync(entriesDir)
127
+ .filter((file) => file.endsWith('.md'))
128
+ .sort();
129
+ const fingerprint = files.map((file) => {
130
+ const stat = fs_1.default.statSync(path_1.default.join(entriesDir, file));
131
+ return `${file}\u0000${stat.size}\u0000${stat.mtimeMs}`;
132
+ }).join('\u0001');
133
+ return { files, fingerprint };
134
+ }
135
+ buildSearchIndex(agentId, files) {
136
+ const entriesDir = this.getEntriesDir(agentId);
137
+ const entries = files.map((file) => {
138
+ const raw = fs_1.default.readFileSync(path_1.default.join(entriesDir, file), 'utf-8');
139
+ const { data, content: body } = (0, parser_1.parseFrontmatter)(raw);
140
+ return { ...data, file, body };
141
+ });
142
+ return (0, knowledge_core_1.createSearchIndex)(entries, { summary: 3, tags: 2, body: 1 });
143
+ }
144
+ getSearchIndex(agentId) {
145
+ const snapshot = this.getEntryFilesFingerprint(agentId);
146
+ const cached = this.searchIndexCache.get(agentId);
147
+ if (cached?.fingerprint === snapshot.fingerprint) {
148
+ return { files: snapshot.files, index: cached.index };
149
+ }
150
+ const index = this.buildSearchIndex(agentId, snapshot.files);
151
+ this.searchIndexCache.set(agentId, { fingerprint: snapshot.fingerprint, index });
152
+ return { files: snapshot.files, index };
153
+ }
154
+ invalidateSearchIndex(agentId) {
155
+ this.searchIndexCache.delete(agentId);
156
+ }
157
+ createFindActionContext(agentId, keyword, results) {
158
+ return {
159
+ commandId: 'memory.find',
160
+ params: { agentId, query: keyword },
161
+ resultTags: [...new Set(results.flatMap((entry) => entry.tags ?? []))].slice(0, 3),
162
+ resultIds: results.slice(0, 3).map((entry) => entry.id),
163
+ resultCount: results.length,
164
+ };
165
+ }
118
166
  getSummaryPath(agentId) {
119
167
  return path_1.default.join(this.getAgentDir(agentId), 'summary.md');
120
168
  }
121
169
  getDirtySummaryPath(agentId) {
122
170
  return path_1.default.join(this.getAgentDir(agentId), '.dirty-summary');
123
171
  }
172
+ getSummarizeLockPath() {
173
+ return path_1.default.join(this.dataDir, '.summarize.lock');
174
+ }
124
175
  ensureDir(dir) {
125
176
  if (!fs_1.default.existsSync(dir)) {
126
177
  fs_1.default.mkdirSync(dir, { recursive: true });
@@ -162,6 +213,50 @@ class MemoryEngine {
162
213
  this.ensureDir(this.getAgentDir(agentId));
163
214
  fs_1.default.writeFileSync(this.getDirtySummaryPath(agentId), String(Date.now()), 'utf-8');
164
215
  }
216
+ getSummarizerHealthPath() {
217
+ return path_1.default.join(this.dataDir, SUMMARIZER_HEALTH_FILE);
218
+ }
219
+ readSummarizerHealth() {
220
+ try {
221
+ const raw = fs_1.default.readFileSync(this.getSummarizerHealthPath(), 'utf-8');
222
+ const parsed = JSON.parse(raw);
223
+ return {
224
+ consecutiveFailures: typeof parsed.consecutiveFailures === 'number' ? parsed.consecutiveFailures : 0,
225
+ notified: Boolean(parsed.notified),
226
+ };
227
+ }
228
+ catch {
229
+ return { consecutiveFailures: 0, notified: false };
230
+ }
231
+ }
232
+ writeSummarizerHealth(state) {
233
+ this.ensureDir(this.dataDir);
234
+ fs_1.default.writeFileSync(this.getSummarizerHealthPath(), JSON.stringify(state), 'utf-8');
235
+ }
236
+ recordSummarizerFailure() {
237
+ const state = this.readSummarizerHealth();
238
+ state.consecutiveFailures += 1;
239
+ if (state.consecutiveFailures >= exports.SUMMARIZER_FAILURE_THRESHOLD && !state.notified) {
240
+ this.notifySummarizerDegraded(state.consecutiveFailures);
241
+ state.notified = true;
242
+ }
243
+ this.writeSummarizerHealth(state);
244
+ }
245
+ recordSummarizerSuccess() {
246
+ const state = this.readSummarizerHealth();
247
+ if (state.consecutiveFailures === 0 && !state.notified)
248
+ return;
249
+ this.writeSummarizerHealth({ consecutiveFailures: 0, notified: false });
250
+ }
251
+ notifySummarizerDegraded(streakCount) {
252
+ try {
253
+ (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 });
254
+ }
255
+ catch (e) {
256
+ const err = e;
257
+ console.error(`[memory] failed to send summarizer-health notify: ${err.message}`);
258
+ }
259
+ }
165
260
  loadAllEntries(agentId) {
166
261
  const entriesDir = this.getEntriesDir(agentId);
167
262
  if (!fs_1.default.existsSync(entriesDir))
@@ -172,7 +267,7 @@ class MemoryEngine {
172
267
  const raw = fs_1.default.readFileSync(path_1.default.join(entriesDir, file), 'utf-8');
173
268
  const { data, content } = (0, parser_1.parseFrontmatter)(raw);
174
269
  const bodyText = content.replace(/^#[^\n]*\n*/m, '').trim();
175
- const hasBody = Boolean(bodyText) && bodyText !== '(상세 λ‚΄μš©μ„ 여기에 μΆ”κ°€)';
270
+ const hasBody = Boolean(bodyText) && bodyText !== '(Add details here)';
176
271
  const entry = data;
177
272
  entries.push({ ...entry, file, hasBody });
178
273
  }
@@ -217,16 +312,17 @@ class MemoryEngine {
217
312
  });
218
313
  const filename = `${id}.md`;
219
314
  const filepath = path_1.default.join(this.getEntriesDir(agentId), filename);
220
- const body = options.body ?? '(상세 λ‚΄μš©μ„ 여기에 μΆ”κ°€)';
315
+ const body = options.body ?? '(Add details here)';
221
316
  const content = `${frontmatter}\n\n# ${summary}\n\n${body}\n`;
222
317
  fs_1.default.writeFileSync(filepath, content, 'utf-8');
223
- console.log(`βœ… μ €μž₯ μ™„λ£Œ: data/${agentId}/entries/${filename}`);
318
+ this.invalidateSearchIndex(agentId);
319
+ console.log(`βœ… Saved: data/${agentId}/entries/${filename}`);
224
320
  console.log(` ID: ${id}`);
225
- console.log(` Tags: ${tags.join(', ') || '(μ—†μŒ)'}`);
321
+ console.log(` Tags: ${tags.join(', ') || '(none)'}`);
226
322
  console.log(` Category: ${category}`);
227
323
  try {
228
324
  this.markSummaryDirty(agentId);
229
- console.log(` πŸ“ summary.md κ°±μ‹  μ˜ˆμ•½λ¨`);
325
+ console.log(` πŸ“ summary.md update scheduled`);
230
326
  }
231
327
  catch (_e) {
232
328
  }
@@ -252,11 +348,11 @@ class MemoryEngine {
252
348
  }
253
349
  const cutoff24h = new Date(Date.now() - this.shortTermHours * 60 * 60 * 1000).toISOString();
254
350
  const shortTermEntries = entries.filter((e) => this.toEntryTimestamp(e) >= cutoff24h);
255
- console.log(`# ${agentId} κΈ°μ–΅ μš”μ•½\n`);
351
+ console.log(`# ${agentId} memory summary\n`);
256
352
  const summaryLastUpdated = fs_1.default.existsSync(summaryPath)
257
353
  ? (0, parser_1.parseFrontmatter)(fs_1.default.readFileSync(summaryPath, 'utf-8')).data.last_updated ?? '-'
258
354
  : '-';
259
- console.log(`> ${entries.length}건 | ν† ν”½ ${topics.length}개 | κ°±μ‹ : ${summaryLastUpdated}`);
355
+ console.log(`> ${entries.length} items | topics ${topics.length} items | updated: ${summaryLastUpdated}`);
260
356
  if (options.full) {
261
357
  const byCat = {};
262
358
  for (const entry of entries) {
@@ -278,7 +374,7 @@ class MemoryEngine {
278
374
  return bImp - aImp;
279
375
  return this.toEntryTimestamp(b).localeCompare(this.toEntryTimestamp(a));
280
376
  });
281
- console.log(`## πŸ”₯ 졜근 μž‘μ—… (24h)\n`);
377
+ console.log(`## πŸ”₯ last Task (24h)\n`);
282
378
  for (const e of sortedShortTerm) {
283
379
  const star = e.important ? '⭐ ' : '';
284
380
  console.log(`- ${star}[${e.id}] ${e.summary} (${this.getPrimaryTag(e)})`);
@@ -302,7 +398,7 @@ class MemoryEngine {
302
398
  const diffDays = Math.max(1, Math.floor((newest.getTime() - oldest.getTime()) / msPerDay) + 1);
303
399
  const toMD = (d) => `${String(d.getMonth() + 1).padStart(2, '0')}/${String(d.getDate()).padStart(2, '0')}`;
304
400
  console.log('---');
305
- console.log(`πŸ“‹ ν† ν”½ μš”μ•½ (${toMD(oldest)} ~ ${toMD(newest)}, ${diffDays}일간)\n`);
401
+ console.log(`πŸ“‹ Topic summary (${toMD(oldest)} ~ ${toMD(newest)}, ${diffDays} days)\n`);
306
402
  }
307
403
  }
308
404
  console.log(summaryBody.trim());
@@ -310,7 +406,7 @@ class MemoryEngine {
310
406
  else {
311
407
  for (const tag of topics.sort()) {
312
408
  const count = entries.filter((e) => this.getPrimaryTag(e) === tag).length;
313
- console.log(`- **${tag}** (${count}건)`);
409
+ console.log(`- **${tag}** (${count} items)`);
314
410
  }
315
411
  }
316
412
  }
@@ -343,15 +439,15 @@ class MemoryEngine {
343
439
  byCategory[category] = [];
344
440
  byCategory[category].push(entry);
345
441
  }
346
- let md = `# ${agentId} λ©”λͺ¨λ¦¬\n\n`;
442
+ let md = `# ${agentId} memory\n\n`;
347
443
  md += `> Last Updated: ${today}\n`;
348
444
  md += `> Total: ${entries.length} memories\n\n`;
349
- md += `## πŸ”₯ 졜근 ${this.recentDays}일\n\n`;
445
+ md += `## πŸ”₯ Last ${this.recentDays} days\n\n`;
350
446
  if (recent.length === 0) {
351
- md += `(μ—†μŒ)\n\n`;
447
+ md += `(none)\n\n`;
352
448
  }
353
449
  else {
354
- md += `| ID | λ‚ μ§œ | μΉ΄ν…Œκ³ λ¦¬ | μš”μ•½ | 상세 |\n`;
450
+ md += `| ID | Date | Category | Summary | Details |\n`;
355
451
  md += `|----|------|----------|------|------|\n`;
356
452
  for (const entry of recent) {
357
453
  const detailIcon = entry.hasBody ? 'πŸ“„' : '-';
@@ -359,47 +455,43 @@ class MemoryEngine {
359
455
  }
360
456
  md += `\n`;
361
457
  }
362
- md += `## 🏷️ μΉ΄ν…Œκ³ λ¦¬λ³„\n\n`;
458
+ md += `## 🏷️ By category\n\n`;
363
459
  for (const [category, items] of Object.entries(byCategory).sort()) {
364
- md += `- **${category}**: ${items.length}건\n`;
460
+ md += `- **${category}**: ${items.length} items\n`;
365
461
  }
366
462
  const olderCount = entries.length - recent.length;
367
463
  const oldestDate = entries[entries.length - 1]?.date ?? '-';
368
464
  const newestDate = entries[0]?.date ?? '-';
369
- md += `\n## πŸ“Š 톡계\n\n`;
370
- md += `- 전체: ${entries.length}건 (${oldestDate} ~ ${newestDate})\n`;
371
- md += `- 졜근 ${this.recentDays}일: ${recent.length}건\n`;
465
+ md += `\n## πŸ“Š Stats\n\n`;
466
+ md += `- Total: ${entries.length} items (${oldestDate} ~ ${newestDate})\n`;
467
+ md += `- Last ${this.recentDays} days: ${recent.length} items\n`;
372
468
  if (olderCount > 0)
373
- md += `- ${this.recentDays}일 이전: ${olderCount}건\n`;
469
+ md += `- Older than ${this.recentDays} days: ${olderCount} items\n`;
374
470
  return md;
375
471
  }
376
472
  topic(agentId, topicName) {
377
473
  const entries = this.loadAllEntries(agentId);
378
474
  const filtered = entries.filter((e) => this.getPrimaryTag(e) === topicName);
379
475
  if (filtered.length === 0) {
380
- console.log(`ν† ν”½ '${topicName}'에 ν•΄λ‹Ήν•˜λŠ” 기얡이 μ—†μŠ΅λ‹ˆλ‹€.`);
476
+ console.log(`No memories found for topic '${topicName}'.`);
381
477
  return;
382
478
  }
383
- console.log(`## πŸ“‚ ν† ν”½: ${topicName} (${filtered.length}건)\n`);
479
+ console.log(`## πŸ“‚ Topic: ${topicName} (${filtered.length} items)\n`);
384
480
  for (const entry of filtered) {
385
481
  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(', ') || '-'}`);
482
+ console.log(`- File: data/${agentId}/entries/${entry.file}`);
483
+ console.log(`- Category: ${entry.category ?? '-'}`);
484
+ console.log(`- Tags: ${(entry.tags ?? []).join(', ') || '-'}`);
389
485
  console.log('');
390
486
  }
391
487
  }
392
488
  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);
489
+ const filtered = this.getRecentEntries(agentId, days);
398
490
  if (filtered.length === 0) {
399
- console.log(`졜근 ${days}일 λ‚΄ 기얡이 μ—†μŠ΅λ‹ˆλ‹€.`);
491
+ console.log(`No memories found in the last ${days} days.`);
400
492
  return;
401
493
  }
402
- console.log(`## πŸ”₯ 졜근 ${days}일 (${filtered.length}건)\n`);
494
+ console.log(`## πŸ”₯ Last ${days} days (${filtered.length} items)\n`);
403
495
  for (const entry of filtered) {
404
496
  const detailIcon = entry.hasBody ? ' πŸ“„' : '';
405
497
  const timestamp = String(entry.date).includes('T')
@@ -408,44 +500,75 @@ class MemoryEngine {
408
500
  console.log(`[${entry.id}] [${timestamp}] [${entry.category ?? '-'}] ${entry.summary}${detailIcon}`);
409
501
  }
410
502
  }
411
- find(agentId, keyword) {
412
- const entriesDir = this.getEntriesDir(agentId);
413
- if (!fs_1.default.existsSync(entriesDir)) {
414
- console.log('기얡이 μ—†μŠ΅λ‹ˆλ‹€.');
415
- return;
503
+ getRecentEntries(agentId, days = 30, opts = {}) {
504
+ const entries = this.loadAllEntries(agentId);
505
+ const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000)
506
+ .toISOString()
507
+ .slice(0, 10);
508
+ const filtered = entries.filter((e) => this.resolveRecentDate(agentId, e).slice(0, 10) >= cutoff);
509
+ if (!opts.includeBody)
510
+ return filtered;
511
+ return filtered.map((e) => ({
512
+ ...e,
513
+ body: this.readEntryBody(agentId, e.file),
514
+ }));
515
+ }
516
+ resolveRecentDate(agentId, entry) {
517
+ const updated = this.toDateString(entry.updated);
518
+ if (updated)
519
+ return updated;
520
+ const createdAt = this.toDateString(entry['created_at']);
521
+ if (createdAt)
522
+ return createdAt;
523
+ const date = this.toDateString(entry.date);
524
+ if (date)
525
+ return date;
526
+ try {
527
+ const filePath = path_1.default.join(this.getEntriesDir(agentId), entry.file);
528
+ return fs_1.default.statSync(filePath).mtime.toISOString();
416
529
  }
417
- const files = fs_1.default.readdirSync(entriesDir).filter((f) => f.endsWith('.md'));
418
- const entries = [];
419
- for (const file of files) {
420
- const content = fs_1.default.readFileSync(path_1.default.join(entriesDir, file), 'utf-8');
421
- const { data, content: body } = (0, parser_1.parseFrontmatter)(content);
422
- const entry = data;
423
- entries.push({ ...entry, file, body });
530
+ catch {
531
+ return '';
424
532
  }
425
- if (entries.length === 0) {
426
- console.log('기얡이 μ—†μŠ΅λ‹ˆλ‹€.');
533
+ }
534
+ toDateString(value) {
535
+ if (value &&
536
+ typeof value === 'object' &&
537
+ typeof value.toISOString === 'function') {
538
+ return value.toISOString();
539
+ }
540
+ return typeof value === 'string' ? value : '';
541
+ }
542
+ readEntryBody(agentId, file) {
543
+ try {
544
+ const raw = fs_1.default.readFileSync(path_1.default.join(this.getEntriesDir(agentId), file), 'utf-8');
545
+ const { content } = (0, parser_1.parseFrontmatter)(raw);
546
+ return content.replace(/^#[^\n]*\n*/m, '').trim();
547
+ }
548
+ catch {
549
+ return '';
550
+ }
551
+ }
552
+ find(agentId, keyword) {
553
+ const { files, index } = this.getSearchIndex(agentId);
554
+ if (files.length === 0) {
555
+ console.log('No memories found.');
427
556
  return;
428
557
  }
429
- const kcEntries = entries;
430
- const results = (0, knowledge_core_1.searchEntries)(kcEntries, keyword, { summary: 3, tags: 2, body: 1 });
558
+ const indexedResults = index.search(keyword, FIND_RESULT_LIMIT + 1);
559
+ const truncated = indexedResults.length > FIND_RESULT_LIMIT;
560
+ const results = indexedResults.slice(0, FIND_RESULT_LIMIT);
431
561
  if (results.length === 0) {
432
- console.log(`'${keyword}' κ΄€λ ¨ 기얡을 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.`);
562
+ console.log(`No related memories found for '${keyword}'.`);
433
563
  return;
434
564
  }
435
- console.log(`## πŸ” 검색 κ²°κ³Ό: "${keyword}" (${results.length}건, BM25 점수순)\n`);
565
+ const limitNote = truncated ? `, top ${FIND_RESULT_LIMIT}` : '';
566
+ console.log(`## πŸ” Search results: "${keyword}" (${results.length} items${limitNote}, sorted by BM25 score)\n`);
436
567
  for (const entry of results) {
437
- const score = entry._score != null ? ` [${entry._score}점]` : '';
568
+ const score = entry._score != null ? ` [${entry._score}pts]` : '';
438
569
  console.log(`[${entry.id}] [${entry.date}] [${this.getPrimaryTag(entry)}]${score} ${entry.summary}`);
439
570
  }
440
- const resultTags = [...new Set(results.flatMap(e => e.tags ?? []))].slice(0, 3);
441
- const resultIds = results.slice(0, 3).map(e => e.id);
442
- const findCtx = {
443
- commandId: 'memory.find',
444
- params: { agentId, query: keyword },
445
- resultTags,
446
- resultIds,
447
- resultCount: results.length,
448
- };
571
+ const findCtx = this.createFindActionContext(agentId, keyword, results);
449
572
  const findActions = (0, knowledge_core_1.generateNextActions)(findCtx);
450
573
  const findBlock = (0, knowledge_core_1.formatActions)(findActions);
451
574
  if (findBlock)
@@ -470,7 +593,7 @@ class MemoryEngine {
470
593
  const raw = fs_1.default.readFileSync(path_1.default.join(entriesDir, e.file), 'utf-8');
471
594
  const { content: rawContent } = (0, parser_1.parseFrontmatter)(raw);
472
595
  const body = rawContent.replace(/^#[^\n]*\n*/m, '').trim();
473
- if (body !== '(상세 λ‚΄μš©μ„ 여기에 μΆ”κ°€)')
596
+ if (body !== '(Add details here)')
474
597
  content = body;
475
598
  }
476
599
  catch (_err) {
@@ -494,10 +617,10 @@ class MemoryEngine {
494
617
  filters.push(`category=${options.category}`);
495
618
  if (options.tag)
496
619
  filters.push(`tag=${options.tag}`);
497
- console.log(`쑰건에 λ§žλŠ” 기얡이 μ—†μŠ΅λ‹ˆλ‹€. (${filters.join(', ') || '전체'})`);
620
+ console.log(`No memories match the filters. (${filters.join(', ') || 'all'})`);
498
621
  return;
499
622
  }
500
- console.log(`## πŸ“‹ λͺ©λ‘: ${agentId} (${filtered.length}건)\n`);
623
+ console.log(`## πŸ“‹ List: ${agentId} (${filtered.length} items)\n`);
501
624
  for (const entry of filtered) {
502
625
  console.log(`[${entry.id}] [${entry.date}] [${this.getPrimaryTag(entry)}] ${entry.summary}`);
503
626
  }
@@ -518,7 +641,7 @@ class MemoryEngine {
518
641
  stats(agentId) {
519
642
  const entries = this.loadAllEntries(agentId);
520
643
  if (entries.length === 0) {
521
- console.log('기얡이 μ—†μŠ΅λ‹ˆλ‹€.');
644
+ console.log('No memories found.');
522
645
  return;
523
646
  }
524
647
  const byCategory = {};
@@ -532,15 +655,15 @@ class MemoryEngine {
532
655
  byTag[tag] = (byTag[tag] ?? 0) + 1;
533
656
  }
534
657
  }
535
- console.log(`# ${agentId} 톡계 (${entries.length}건)\n`);
536
- console.log(`## μΉ΄ν…Œκ³ λ¦¬λ³„`);
658
+ console.log(`# ${agentId} Stats (${entries.length} items)\n`);
659
+ console.log(`## By category`);
537
660
  for (const [cat, count] of Object.entries(byCategory).sort((a, b) => b[1] - a[1])) {
538
- console.log(` ${cat}: ${count}건`);
661
+ console.log(` ${cat}: ${count} items`);
539
662
  }
540
- console.log(`\n## νƒœκ·Έ Top 20`);
663
+ console.log(`\n## Top 20 Tags`);
541
664
  const sortedTags = Object.entries(byTag).sort((a, b) => b[1] - a[1]).slice(0, 20);
542
665
  for (const [tag, count] of sortedTags) {
543
- console.log(` ${tag}: ${count}건`);
666
+ console.log(` ${tag}: ${count} items`);
544
667
  }
545
668
  const topCategories = Object.entries(byCategory).sort((a, b) => b[1] - a[1]).map(t => t[0]);
546
669
  const statsCtx = {
@@ -557,7 +680,7 @@ class MemoryEngine {
557
680
  tags(agentId) {
558
681
  const entries = this.loadAllEntries(agentId);
559
682
  if (entries.length === 0) {
560
- console.log('기얡이 μ—†μŠ΅λ‹ˆλ‹€.');
683
+ console.log('No memories found.');
561
684
  return;
562
685
  }
563
686
  const byTag = {};
@@ -568,18 +691,18 @@ class MemoryEngine {
568
691
  }
569
692
  const sortedTags = Object.entries(byTag).sort((a, b) => b[1] - a[1]);
570
693
  if (sortedTags.length === 0) {
571
- console.log('νƒœκ·Έκ°€ μ—†μŠ΅λ‹ˆλ‹€.');
694
+ console.log('No tags found.');
572
695
  return;
573
696
  }
574
- console.log(`## 🏷️ 전체 νƒœκ·Έ (${sortedTags.length}개)\n`);
697
+ console.log(`## 🏷️ All tags (${sortedTags.length} items)\n`);
575
698
  for (const [tag, count] of sortedTags) {
576
- console.log(` ${tag}: ${count}건`);
699
+ console.log(` ${tag}: ${count} items`);
577
700
  }
578
701
  }
579
702
  get(agentId, memoryId) {
580
703
  const entriesDir = this.getEntriesDir(agentId);
581
704
  if (!fs_1.default.existsSync(entriesDir)) {
582
- console.log('기얡이 μ—†μŠ΅λ‹ˆλ‹€.');
705
+ console.log('No memories found.');
583
706
  return;
584
707
  }
585
708
  const files = fs_1.default.readdirSync(entriesDir).filter((f) => f.endsWith('.md'));
@@ -591,10 +714,10 @@ class MemoryEngine {
591
714
  const d = data;
592
715
  console.log(`## πŸ“„ ${d.summary}\n`);
593
716
  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}`);
717
+ console.log(`- Date: ${d.date}`);
718
+ console.log(`- Category: ${d.category ?? '-'}`);
719
+ console.log(`- Tags: ${(d.tags ?? []).join(', ') || '-'}`);
720
+ console.log(`- File: data/${agentId}/entries/${file}`);
598
721
  console.log(`\n---\n`);
599
722
  console.log(content.trim());
600
723
  const graphPath = path_1.default.join(this.getAgentDir(agentId), 'graph.json');
@@ -611,7 +734,7 @@ class MemoryEngine {
611
734
  nodeMap[n.id] = n;
612
735
  });
613
736
  console.log(`\n---\n`);
614
- console.log(`## πŸ”— μ—°κ΄€ κΈ°μ–΅ (${relatedEdges.length}개)\n`);
737
+ console.log(`## πŸ”— Related memories (${relatedEdges.length} items)\n`);
615
738
  for (const edge of relatedEdges) {
616
739
  const otherId = edge.from === memoryId ? edge.to : edge.from;
617
740
  const other = nodeMap[otherId];
@@ -638,12 +761,12 @@ class MemoryEngine {
638
761
  return;
639
762
  }
640
763
  }
641
- console.log(`ID '${memoryId}'λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.`);
764
+ console.log(`ID '${memoryId}' not found.`);
642
765
  }
643
766
  update(agentId, memoryId, options = {}) {
644
767
  const entry = this.findEntryById(agentId, memoryId);
645
768
  if (!entry) {
646
- console.log(`❌ ID '${memoryId}'λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.`);
769
+ console.log(`❌ ID '${memoryId}' not found.`);
647
770
  process.exit(1);
648
771
  }
649
772
  const { filePath, data, content } = entry;
@@ -664,11 +787,12 @@ class MemoryEngine {
664
787
  else if (options['no-important']) {
665
788
  delete newData['important'];
666
789
  }
667
- const newBody = options.body ?? existingBody ?? '(상세 λ‚΄μš©μ„ 여기에 μΆ”κ°€)';
790
+ const newBody = options.body ?? existingBody ?? '(Add details here)';
668
791
  const newFrontmatter = (0, parser_1.stringifyFrontmatter)(newData);
669
792
  const newContent = `${newFrontmatter}\n\n# ${String(newData['summary'])}\n\n${newBody}\n`;
670
793
  fs_1.default.writeFileSync(filePath, newContent, 'utf-8');
671
- console.log(`βœ… μˆ˜μ • μ™„λ£Œ: ${entry.file}`);
794
+ this.invalidateSearchIndex(agentId);
795
+ console.log(`βœ… Updated: ${entry.file}`);
672
796
  console.log(` ID: ${memoryId}`);
673
797
  if (options.summary)
674
798
  console.log(` Summary: ${String(newData['summary'])}`);
@@ -677,28 +801,29 @@ class MemoryEngine {
677
801
  if (options.tags)
678
802
  console.log(` Tags: ${newData['tags'].join(', ')}`);
679
803
  if (options.body)
680
- console.log(` Body: (μ—…λ°μ΄νŠΈλ¨)`);
804
+ console.log(` Body: (updated)`);
681
805
  }
682
806
  delete(agentId, memoryId, options = {}) {
683
807
  const entry = this.findEntryById(agentId, memoryId);
684
808
  if (!entry) {
685
- console.log(`❌ ID '${memoryId}'λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.`);
809
+ console.log(`❌ ID '${memoryId}' not found.`);
686
810
  process.exit(1);
687
811
  }
688
812
  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`);
813
+ console.log(`⚠️ Delete target: [${entry.data.id}] ${entry.data.summary}`);
814
+ console.log(` File: ${entry.file}`);
815
+ console.log(`\nAdd --force to delete it.`);
816
+ console.log(`Example: memory delete ${agentId} ${memoryId} --force`);
693
817
  return;
694
818
  }
695
819
  fs_1.default.unlinkSync(entry.filePath);
696
- console.log(`πŸ—‘οΈ μ‚­μ œ μ™„λ£Œ: ${entry.file}`);
820
+ this.invalidateSearchIndex(agentId);
821
+ console.log(`πŸ—‘οΈ Deleted: ${entry.file}`);
697
822
  console.log(` ID: ${memoryId}`);
698
823
  console.log(` Summary: ${entry.data.summary}`);
699
824
  try {
700
825
  this.markSummaryDirty(agentId);
701
- console.log(` πŸ“ summary.md κ°±μ‹  μ˜ˆμ•½λ¨`);
826
+ console.log(` πŸ“ summary.md update scheduled`);
702
827
  }
703
828
  catch (_e) {
704
829
  }
@@ -707,27 +832,27 @@ class MemoryEngine {
707
832
  const entry1 = this.findEntryById(agentId, memoryId1);
708
833
  const entry2 = this.findEntryById(agentId, memoryId2);
709
834
  if (!entry1) {
710
- console.log(`❌ ID '${memoryId1}'λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.`);
835
+ console.log(`❌ ID '${memoryId1}' not found.`);
711
836
  process.exit(1);
712
837
  }
713
838
  if (!entry2) {
714
- console.log(`❌ ID '${memoryId2}'λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.`);
839
+ console.log(`❌ ID '${memoryId2}' not found.`);
715
840
  process.exit(1);
716
841
  }
717
842
  const body1 = entry1.content.replace(/^#[^\n]*\n*/m, '').trim();
718
843
  const body2 = entry2.content.replace(/^#[^\n]*\n*/m, '').trim();
719
844
  const mergedSummary = options.summary ?? `${entry1.data.summary} + ${entry2.data.summary}`;
720
- const mergedBody = `## λ³‘ν•©λœ κΈ°μ–΅ 1 (${entry1.data.id}, ${entry1.data.date})
845
+ const mergedBody = `## Merged memory 1 (${entry1.data.id}, ${entry1.data.date})
721
846
  ${entry1.data.summary}
722
847
 
723
- ${body1 !== '(상세 λ‚΄μš©μ„ 여기에 μΆ”κ°€)' ? body1 : '(상세 μ—†μŒ)'}
848
+ ${body1 !== '(Add details here)' ? body1 : '(No details)'}
724
849
 
725
850
  ---
726
851
 
727
- ## λ³‘ν•©λœ κΈ°μ–΅ 2 (${entry2.data.id}, ${entry2.data.date})
852
+ ## Merged memory 2 (${entry2.data.id}, ${entry2.data.date})
728
853
  ${entry2.data.summary}
729
854
 
730
- ${body2 !== '(상세 λ‚΄μš©μ„ 여기에 μΆ”κ°€)' ? body2 : '(상세 μ—†μŒ)'}`;
855
+ ${body2 !== '(Add details here)' ? body2 : '(No details)'}`;
731
856
  const mergedTags = [
732
857
  ...new Set([...(entry1.data.tags ?? []), ...(entry2.data.tags ?? [])]),
733
858
  ];
@@ -749,10 +874,11 @@ ${body2 !== '(상세 λ‚΄μš©μ„ 여기에 μΆ”κ°€)' ? body2 : '(상세 μ—†μŒ)'}`;
749
874
  fs_1.default.writeFileSync(filepath, newContent, 'utf-8');
750
875
  fs_1.default.unlinkSync(entry1.filePath);
751
876
  fs_1.default.unlinkSync(entry2.filePath);
752
- console.log(`πŸ”€ 병합 μ™„λ£Œ!`);
753
- console.log(` μƒˆ ID: ${newId}`);
754
- console.log(` μƒˆ 파일: ${filename}`);
755
- console.log(` λ³‘ν•©λœ κΈ°μ–΅: [${memoryId1}] + [${memoryId2}]`);
877
+ this.invalidateSearchIndex(agentId);
878
+ console.log(`πŸ”€ Merge complete!`);
879
+ console.log(` New ID: ${newId}`);
880
+ console.log(` New file: ${filename}`);
881
+ console.log(` Merged memory: [${memoryId1}] + [${memoryId2}]`);
756
882
  console.log(` Summary: ${mergedSummary}`);
757
883
  }
758
884
  buildFallbackSummary(entries) {
@@ -778,7 +904,7 @@ ${body2 !== '(상세 λ‚΄μš©μ„ 여기에 μΆ”κ°€)' ? body2 : '(상세 μ—†μŒ)'}`;
778
904
  const oldest = dates[0];
779
905
  const newest = dates[dates.length - 1];
780
906
  const rangeLabel = oldest && newest ? `, ${toMD(oldest)}~${toMD(newest)}` : '';
781
- body += `## ${topic} (${items.length}건${rangeLabel})\n`;
907
+ body += `## ${topic} (${items.length} items${rangeLabel})\n`;
782
908
  for (const e of items)
783
909
  body += `- [${e.date}] ${e.summary}\n`;
784
910
  for (const e of items)
@@ -788,22 +914,26 @@ ${body2 !== '(상세 λ‚΄μš©μ„ 여기에 μΆ”κ°€)' ? body2 : '(상세 μ—†μŒ)'}`;
788
914
  return body;
789
915
  }
790
916
  callSummarizer(prompt) {
791
- const tmpFile = path_1.default.join(os_1.default.tmpdir(), `memory-summarizer-${Date.now()}.txt`);
792
917
  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) {
918
+ const result = (0, child_process_1.execSync)(`${CREWX_CLI} q "${this.summarizerAgent}"`, {
919
+ encoding: 'utf-8',
920
+ timeout: this.summarizerTimeout,
921
+ stdio: ['pipe', 'pipe', 'pipe'],
922
+ input: prompt,
923
+ });
924
+ const parsed = parseCrewxResponse(result);
925
+ this.recordSummarizerSuccess();
926
+ return parsed;
927
+ }
928
+ catch (e) {
929
+ const err = e;
930
+ const reason = err.code === 'ETIMEDOUT' || err.killed
931
+ ? `timed out after ${this.summarizerTimeout}ms`
932
+ : err.message;
933
+ console.error(`[memory] callSummarizer failed (agent=${this.summarizerAgent}), falling back to raw entry list: ${reason}`);
934
+ this.recordSummarizerFailure();
798
935
  return null;
799
936
  }
800
- finally {
801
- try {
802
- fs_1.default.unlinkSync(tmpFile);
803
- }
804
- catch (_e) {
805
- }
806
- }
807
937
  }
808
938
  generateSummary(agentId, options = {}) {
809
939
  const allEntries = this.loadAllEntries(agentId);
@@ -861,13 +991,13 @@ ${body2 !== '(상세 λ‚΄μš©μ„ 여기에 μΆ”κ°€)' ? body2 : '(상세 μ—†μŒ)'}`;
861
991
  for (const entry of newItems) {
862
992
  const importantTag = entry.important ? ' [important]' : '';
863
993
  p += `- [${entry.id}] [${entry.date}] [${this.getPrimaryTag(entry)}]${importantTag} ${entry.summary}`;
864
- if (entry.body && entry.body !== '(상세 λ‚΄μš©μ„ 여기에 μΆ”κ°€)') {
994
+ if (entry.body && entry.body !== '(Add details here)') {
865
995
  p += ` β€” ${entry.body.slice(0, 200)}`;
866
996
  }
867
997
  p += ` (file: entries/${entry.file})\n`;
868
998
  }
869
999
  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`;
1000
+ 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
1001
  p += `- Write concise Korean summary per topic (2-5 sentences)\n`;
872
1002
  p += `- After each topic summary, list relevant entry file links as "β†’ [entries/filename](entries/filename)"\n`;
873
1003
  p += `- Highlight key decisions, insights, and action items\n`;
@@ -917,76 +1047,141 @@ ${body2 !== '(상세 λ‚΄μš©μ„ 여기에 μΆ”κ°€)' ? body2 : '(상세 μ—†μŒ)'}`;
917
1047
  summarize(agentId, options = {}) {
918
1048
  const entries = this.loadAllEntries(agentId);
919
1049
  if (entries.length === 0) {
920
- console.log('기얡이 μ—†μŠ΅λ‹ˆλ‹€.');
1050
+ console.log('No memories found.');
921
1051
  return;
922
1052
  }
923
- console.log(`πŸ“ μš”μ•½ 생성 μ‹œμž‘ (${entries.length}개 μ—”νŠΈλ¦¬)\n`);
1053
+ console.log(`πŸ“ Starting summary generation (${entries.length} items entries)\n`);
924
1054
  const result = this.generateSummary(agentId, { force: options.force });
925
1055
  if (result.status === 'up-to-date') {
926
- console.log('βœ… summary.md μ΅œμ‹  μƒνƒœμž…λ‹ˆλ‹€.');
1056
+ console.log('βœ… summary.md is up to date.');
927
1057
  }
928
1058
  else if (result.status === 'fallback') {
929
- console.log(`πŸ“‹ 폴백 μš”μ•½ 생성 μ™„λ£Œ (AI 호좜 μ‹€νŒ¨)`);
1059
+ console.log(`πŸ“‹ Fallback summary generated (AI call failed)`);
930
1060
  }
931
1061
  else if (result.status === 'updated') {
932
- console.log(`πŸ€– μš”μ•½ μ™„λ£Œ (${result.newEntries}건 반영)`);
1062
+ console.log(`πŸ€– Summary complete (${result.newEntries} items applied)`);
1063
+ }
1064
+ console.log(` File: data/${agentId}/summary.md`);
1065
+ console.log(` entries: ${result.entryCount} items`);
1066
+ }
1067
+ isSummarizeLockStale(lockPath) {
1068
+ let content;
1069
+ try {
1070
+ content = fs_1.default.readFileSync(lockPath, 'utf-8');
1071
+ }
1072
+ catch {
1073
+ return true;
1074
+ }
1075
+ const pidMatch = content.match(/^pid=(\d+)/m);
1076
+ const pid = pidMatch ? Number(pidMatch[1]) : NaN;
1077
+ if (Number.isFinite(pid)) {
1078
+ try {
1079
+ process.kill(pid, 0);
1080
+ return false;
1081
+ }
1082
+ catch (e) {
1083
+ const err = e;
1084
+ if (err.code === 'ESRCH')
1085
+ return true;
1086
+ }
1087
+ }
1088
+ try {
1089
+ const stat = fs_1.default.statSync(lockPath);
1090
+ return Date.now() - stat.mtimeMs > exports.SUMMARIZE_LOCK_STALE_MS;
1091
+ }
1092
+ catch {
1093
+ return true;
1094
+ }
1095
+ }
1096
+ acquireSummarizeLock() {
1097
+ const lockPath = this.getSummarizeLockPath();
1098
+ const content = `pid=${process.pid}\nmtime=${new Date().toISOString()}\n`;
1099
+ try {
1100
+ const fd = fs_1.default.openSync(lockPath, 'wx');
1101
+ fs_1.default.writeSync(fd, content);
1102
+ fs_1.default.closeSync(fd);
1103
+ return true;
1104
+ }
1105
+ catch (e) {
1106
+ const err = e;
1107
+ if (err.code !== 'EEXIST')
1108
+ throw err;
1109
+ }
1110
+ if (!this.isSummarizeLockStale(lockPath)) {
1111
+ return false;
1112
+ }
1113
+ fs_1.default.writeFileSync(lockPath, content, 'utf-8');
1114
+ return true;
1115
+ }
1116
+ releaseSummarizeLock() {
1117
+ try {
1118
+ fs_1.default.unlinkSync(this.getSummarizeLockPath());
1119
+ }
1120
+ catch {
933
1121
  }
934
- console.log(` 파일: data/${agentId}/summary.md`);
935
- console.log(` μ—”νŠΈλ¦¬: ${result.entryCount}개`);
936
1122
  }
937
1123
  summarizeDirty() {
938
1124
  if (!fs_1.default.existsSync(this.dataDir)) {
939
- console.log(`πŸ’‘ λ””λ°”μš΄μŠ€ cron 등둝:`);
1125
+ console.log(`πŸ’‘ Register debounce cron:`);
940
1126
  console.log(`npx cron add "*/1 * * * *" "npx memory summarize-dirty" --mode command --name "memory-debounce"`);
941
1127
  return;
942
1128
  }
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 μ΅œμ‹  μƒνƒœμž…λ‹ˆλ‹€.`);
1129
+ if (!this.acquireSummarizeLock()) {
1130
+ console.log(`⏭️ summarize-dirty is already running (lock held) β€” skipping this run.`);
1131
+ return;
1132
+ }
1133
+ try {
1134
+ const agentDirs = fs_1.default
1135
+ .readdirSync(this.dataDir, { withFileTypes: true })
1136
+ .filter((entry) => entry.isDirectory())
1137
+ .map((entry) => entry.name);
1138
+ for (const agentId of agentDirs) {
1139
+ const dirtyPath = this.getDirtySummaryPath(agentId);
1140
+ if (!fs_1.default.existsSync(dirtyPath))
1141
+ continue;
1142
+ try {
1143
+ const result = this.generateSummary(agentId);
1144
+ if (result.status === 'updated') {
1145
+ console.log(`πŸ€– ${agentId} summary.md updated (${result.newEntries} items applied)`);
1146
+ }
1147
+ else if (result.status === 'fallback') {
1148
+ console.log(`πŸ“‹ ${agentId} Fallback summary generated (AI call failed)`);
1149
+ }
1150
+ else if (result.status === 'up-to-date') {
1151
+ console.log(`βœ… ${agentId} summary.md is up to date.`);
1152
+ }
1153
+ else if (result.status === 'empty') {
1154
+ console.log(`ℹ️ ${agentId} has no memories to summarize.`);
1155
+ }
1156
+ fs_1.default.unlinkSync(dirtyPath);
961
1157
  }
962
- else if (result.status === 'empty') {
963
- console.log(`ℹ️ ${agentId} μš”μ•½ν•  기얡이 μ—†μŠ΅λ‹ˆλ‹€.`);
1158
+ catch (e) {
1159
+ console.error(`❌ ${agentId} summary.md update failed: ${e.message}`);
964
1160
  }
965
- fs_1.default.unlinkSync(dirtyPath);
966
- }
967
- catch (e) {
968
- console.error(`❌ ${agentId} summary.md κ°±μ‹  μ‹€νŒ¨: ${e.message}`);
969
1161
  }
970
1162
  }
971
- console.log(`πŸ’‘ λ””λ°”μš΄μŠ€ cron 등둝:`);
1163
+ finally {
1164
+ this.releaseSummarizeLock();
1165
+ }
1166
+ console.log(`πŸ’‘ Register debounce cron:`);
972
1167
  console.log(`npx cron add "*/1 * * * *" "npx memory summarize-dirty" --mode command --name "memory-debounce"`);
973
1168
  }
974
1169
  search(agentId, query) {
975
1170
  const entries = this.loadAllEntries(agentId);
976
1171
  if (entries.length === 0) {
977
- console.log('기얡이 μ—†μŠ΅λ‹ˆλ‹€.');
1172
+ console.log('No memories found.');
978
1173
  return;
979
1174
  }
980
1175
  const memoryList = entries
981
1176
  .map((e) => `[${e.id}] [${e.date}] [${this.getPrimaryTag(e)}] ${e.summary}`)
982
1177
  .join('\n');
983
- const task = `κΈ°μ–΅ λͺ©λ‘:\n${memoryList}\n\n질문: "${query}"`;
1178
+ const task = `Memory list:\n${memoryList}\n\nQuestion: "${query}"`;
984
1179
  try {
985
- console.log(`πŸ” "${query}" 검색 쀑... (${this.searcherAgent})\n`);
1180
+ console.log(`πŸ” "${query}" searching... (${this.searcherAgent})\n`);
986
1181
  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
1182
  const parsed = parseCrewxResponse(result);
988
1183
  if (parsed) {
989
- console.log(`## 🧠 μ‹œλ§¨ν‹± 검색 κ²°κ³Ό\n`);
1184
+ console.log(`## 🧠 Semantic search results\n`);
990
1185
  console.log(parsed);
991
1186
  }
992
1187
  else {
@@ -996,12 +1191,12 @@ ${body2 !== '(상세 λ‚΄μš©μ„ 여기에 μΆ”κ°€)' ? body2 : '(상세 μ—†μŒ)'}`;
996
1191
  catch (error) {
997
1192
  const err = error;
998
1193
  if (err.code === 'ETIMEDOUT' || err.killed) {
999
- console.log('검색 μ‹€νŒ¨: μ‹œκ°„ 초과 (μ΅œλŒ€ 5λΆ„)');
1194
+ console.log('Search failed: timed out (max 5 minutes)');
1000
1195
  }
1001
1196
  else {
1002
- console.log('검색 μ‹€νŒ¨:', err.message);
1197
+ console.log('Search failed:', err.message);
1003
1198
  }
1004
- console.log('\nπŸ’‘ Tip: find λͺ…λ ΉμœΌλ‘œ ν‚€μ›Œλ“œ 검색을 μ‹œλ„ν•΄λ³΄μ„Έμš”.');
1199
+ console.log('\nπŸ’‘ Tip: try keyword search with the find command.');
1005
1200
  }
1006
1201
  }
1007
1202
  }