@crewx/memory 0.1.23-rc.12 β†’ 0.1.23-rc.121

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,58 @@ 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
- const CREWX_CLI = process.env.CREWX_CLI || 'npx crewx';
16
+ const sdk_1 = require("@crewx/sdk");
17
+ exports.SUMMARIZER_FAILURE_THRESHOLD = 3;
18
+ const SUMMARIZER_HEALTH_FILE = '.summarizer-health.json';
19
+ const FIND_RESULT_LIMIT = 50;
20
+ function resolveCrewxInvocation() {
21
+ const resolution = (0, sdk_1.resolveCrewxExecutable)({
22
+ env: process.env,
23
+ cwd: process.cwd(),
24
+ });
25
+ if (!resolution.ok) {
26
+ const error = new Error((0, sdk_1.formatCrewxExecutableFailure)(resolution));
27
+ Object.assign(error, { code: 'CLI_UNRESOLVABLE' });
28
+ throw error;
29
+ }
30
+ const env = (0, sdk_1.withoutCrewxRunnerIdentity)({
31
+ ...process.env,
32
+ CREWX_CLI_ARGV: JSON.stringify(resolution.argv),
33
+ });
34
+ const program = (0, sdk_1.resolveWindowsSpawnProgram)({
35
+ command: resolution.argv[0],
36
+ env,
37
+ cwd: process.cwd(),
38
+ packageName: 'crewx',
39
+ allowShellFallback: true,
40
+ });
41
+ const invocation = (0, sdk_1.materializeWindowsSpawnProgram)(program, resolution.argv.slice(1));
42
+ return {
43
+ command: invocation.command,
44
+ argv: invocation.argv,
45
+ env,
46
+ shell: invocation.shell,
47
+ };
48
+ }
49
+ function execCrewx(args, options) {
50
+ const invocation = resolveCrewxInvocation();
51
+ return (0, child_process_1.execFileSync)(invocation.command, [...invocation.argv, ...args], {
52
+ ...options,
53
+ env: invocation.env,
54
+ ...(invocation.shell === undefined ? {} : { shell: invocation.shell }),
55
+ });
56
+ }
57
+ exports.SUMMARIZE_LOCK_STALE_MS = 10 * 60 * 1000;
18
58
  function parseCrewxResponse(raw) {
19
59
  const responseMatch = raw.match(/πŸ“„ Response:\s*─+\s*([\s\S]*?)(?=πŸ“ Working Directory|$)/);
20
60
  if (responseMatch?.[1]) {
@@ -75,6 +115,7 @@ function getMemoryConfig() {
75
115
  }
76
116
  class MemoryEngine {
77
117
  constructor(config = {}) {
118
+ this.searchIndexCache = new Map();
78
119
  const yamlConfig = getMemoryConfig();
79
120
  this.dataDir =
80
121
  config.dataDir ??
@@ -103,7 +144,7 @@ class MemoryEngine {
103
144
  this.summarizerTimeout =
104
145
  config.summarizerTimeout ??
105
146
  (yamlConfig.summarizer_timeout != null ? Number(yamlConfig.summarizer_timeout) : undefined) ??
106
- 60000;
147
+ 480000;
107
148
  this.searcherTimeout =
108
149
  config.searcherTimeout ??
109
150
  (yamlConfig.searcher_timeout != null ? Number(yamlConfig.searcher_timeout) : undefined) ??
@@ -115,12 +156,59 @@ class MemoryEngine {
115
156
  getEntriesDir(agentId) {
116
157
  return path_1.default.join(this.getAgentDir(agentId), 'entries');
117
158
  }
159
+ getEntryFilesFingerprint(agentId) {
160
+ const entriesDir = this.getEntriesDir(agentId);
161
+ if (!fs_1.default.existsSync(entriesDir))
162
+ return { files: [], fingerprint: '' };
163
+ const files = fs_1.default.readdirSync(entriesDir)
164
+ .filter((file) => file.endsWith('.md'))
165
+ .sort();
166
+ const fingerprint = files.map((file) => {
167
+ const stat = fs_1.default.statSync(path_1.default.join(entriesDir, file));
168
+ return `${file}\u0000${stat.size}\u0000${stat.mtimeMs}`;
169
+ }).join('\u0001');
170
+ return { files, fingerprint };
171
+ }
172
+ buildSearchIndex(agentId, files) {
173
+ const entriesDir = this.getEntriesDir(agentId);
174
+ const entries = files.map((file) => {
175
+ const raw = fs_1.default.readFileSync(path_1.default.join(entriesDir, file), 'utf-8');
176
+ const { data, content: body } = (0, parser_1.parseFrontmatter)(raw);
177
+ return { ...data, file, body };
178
+ });
179
+ return (0, knowledge_core_1.createSearchIndex)(entries, { summary: 3, tags: 2, body: 1 });
180
+ }
181
+ getSearchIndex(agentId) {
182
+ const snapshot = this.getEntryFilesFingerprint(agentId);
183
+ const cached = this.searchIndexCache.get(agentId);
184
+ if (cached?.fingerprint === snapshot.fingerprint) {
185
+ return { files: snapshot.files, index: cached.index };
186
+ }
187
+ const index = this.buildSearchIndex(agentId, snapshot.files);
188
+ this.searchIndexCache.set(agentId, { fingerprint: snapshot.fingerprint, index });
189
+ return { files: snapshot.files, index };
190
+ }
191
+ invalidateSearchIndex(agentId) {
192
+ this.searchIndexCache.delete(agentId);
193
+ }
194
+ createFindActionContext(agentId, keyword, results) {
195
+ return {
196
+ commandId: 'memory.find',
197
+ params: { agentId, query: keyword },
198
+ resultTags: [...new Set(results.flatMap((entry) => entry.tags ?? []))].slice(0, 3),
199
+ resultIds: results.slice(0, 3).map((entry) => entry.id),
200
+ resultCount: results.length,
201
+ };
202
+ }
118
203
  getSummaryPath(agentId) {
119
204
  return path_1.default.join(this.getAgentDir(agentId), 'summary.md');
120
205
  }
121
206
  getDirtySummaryPath(agentId) {
122
207
  return path_1.default.join(this.getAgentDir(agentId), '.dirty-summary');
123
208
  }
209
+ getSummarizeLockPath() {
210
+ return path_1.default.join(this.dataDir, '.summarize.lock');
211
+ }
124
212
  ensureDir(dir) {
125
213
  if (!fs_1.default.existsSync(dir)) {
126
214
  fs_1.default.mkdirSync(dir, { recursive: true });
@@ -162,6 +250,57 @@ class MemoryEngine {
162
250
  this.ensureDir(this.getAgentDir(agentId));
163
251
  fs_1.default.writeFileSync(this.getDirtySummaryPath(agentId), String(Date.now()), 'utf-8');
164
252
  }
253
+ getSummarizerHealthPath() {
254
+ return path_1.default.join(this.dataDir, SUMMARIZER_HEALTH_FILE);
255
+ }
256
+ readSummarizerHealth() {
257
+ try {
258
+ const raw = fs_1.default.readFileSync(this.getSummarizerHealthPath(), 'utf-8');
259
+ const parsed = JSON.parse(raw);
260
+ return {
261
+ consecutiveFailures: typeof parsed.consecutiveFailures === 'number' ? parsed.consecutiveFailures : 0,
262
+ notified: Boolean(parsed.notified),
263
+ };
264
+ }
265
+ catch {
266
+ return { consecutiveFailures: 0, notified: false };
267
+ }
268
+ }
269
+ writeSummarizerHealth(state) {
270
+ this.ensureDir(this.dataDir);
271
+ fs_1.default.writeFileSync(this.getSummarizerHealthPath(), JSON.stringify(state), 'utf-8');
272
+ }
273
+ recordSummarizerFailure() {
274
+ const state = this.readSummarizerHealth();
275
+ state.consecutiveFailures += 1;
276
+ if (state.consecutiveFailures >= exports.SUMMARIZER_FAILURE_THRESHOLD && !state.notified) {
277
+ this.notifySummarizerDegraded(state.consecutiveFailures);
278
+ state.notified = true;
279
+ }
280
+ this.writeSummarizerHealth(state);
281
+ }
282
+ recordSummarizerSuccess() {
283
+ const state = this.readSummarizerHealth();
284
+ if (state.consecutiveFailures === 0 && !state.notified)
285
+ return;
286
+ this.writeSummarizerHealth({ consecutiveFailures: 0, notified: false });
287
+ }
288
+ notifySummarizerDegraded(streakCount) {
289
+ try {
290
+ execCrewx([
291
+ 'notify',
292
+ 'AI memory summarization keeps failing β€” falling back to mechanical summaries',
293
+ '--level=warning',
294
+ '--body=callSummarizer(agent=' + this.summarizerAgent +
295
+ ') failed ' + streakCount +
296
+ ' times in a row, so summarization switched to the fallback (mechanical) path.',
297
+ ], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 15000 });
298
+ }
299
+ catch (e) {
300
+ const err = e;
301
+ console.error(`[memory] failed to send summarizer-health notify: ${err.message}`);
302
+ }
303
+ }
165
304
  loadAllEntries(agentId) {
166
305
  const entriesDir = this.getEntriesDir(agentId);
167
306
  if (!fs_1.default.existsSync(entriesDir))
@@ -172,7 +311,7 @@ class MemoryEngine {
172
311
  const raw = fs_1.default.readFileSync(path_1.default.join(entriesDir, file), 'utf-8');
173
312
  const { data, content } = (0, parser_1.parseFrontmatter)(raw);
174
313
  const bodyText = content.replace(/^#[^\n]*\n*/m, '').trim();
175
- const hasBody = Boolean(bodyText) && bodyText !== '(상세 λ‚΄μš©μ„ 여기에 μΆ”κ°€)';
314
+ const hasBody = Boolean(bodyText) && bodyText !== '(Add details here)';
176
315
  const entry = data;
177
316
  entries.push({ ...entry, file, hasBody });
178
317
  }
@@ -217,16 +356,17 @@ class MemoryEngine {
217
356
  });
218
357
  const filename = `${id}.md`;
219
358
  const filepath = path_1.default.join(this.getEntriesDir(agentId), filename);
220
- const body = options.body ?? '(상세 λ‚΄μš©μ„ 여기에 μΆ”κ°€)';
359
+ const body = options.body ?? '(Add details here)';
221
360
  const content = `${frontmatter}\n\n# ${summary}\n\n${body}\n`;
222
361
  fs_1.default.writeFileSync(filepath, content, 'utf-8');
223
- console.log(`βœ… μ €μž₯ μ™„λ£Œ: data/${agentId}/entries/${filename}`);
362
+ this.invalidateSearchIndex(agentId);
363
+ console.log(`βœ… Saved: data/${agentId}/entries/${filename}`);
224
364
  console.log(` ID: ${id}`);
225
- console.log(` Tags: ${tags.join(', ') || '(μ—†μŒ)'}`);
365
+ console.log(` Tags: ${tags.join(', ') || '(none)'}`);
226
366
  console.log(` Category: ${category}`);
227
367
  try {
228
368
  this.markSummaryDirty(agentId);
229
- console.log(` πŸ“ summary.md κ°±μ‹  μ˜ˆμ•½λ¨`);
369
+ console.log(` πŸ“ summary.md update scheduled`);
230
370
  }
231
371
  catch (_e) {
232
372
  }
@@ -252,11 +392,11 @@ class MemoryEngine {
252
392
  }
253
393
  const cutoff24h = new Date(Date.now() - this.shortTermHours * 60 * 60 * 1000).toISOString();
254
394
  const shortTermEntries = entries.filter((e) => this.toEntryTimestamp(e) >= cutoff24h);
255
- console.log(`# ${agentId} κΈ°μ–΅ μš”μ•½\n`);
395
+ console.log(`# ${agentId} memory summary\n`);
256
396
  const summaryLastUpdated = fs_1.default.existsSync(summaryPath)
257
397
  ? (0, parser_1.parseFrontmatter)(fs_1.default.readFileSync(summaryPath, 'utf-8')).data.last_updated ?? '-'
258
398
  : '-';
259
- console.log(`> ${entries.length}건 | ν† ν”½ ${topics.length}개 | κ°±μ‹ : ${summaryLastUpdated}`);
399
+ console.log(`> ${entries.length} items | topics ${topics.length} items | updated: ${summaryLastUpdated}`);
260
400
  if (options.full) {
261
401
  const byCat = {};
262
402
  for (const entry of entries) {
@@ -278,7 +418,7 @@ class MemoryEngine {
278
418
  return bImp - aImp;
279
419
  return this.toEntryTimestamp(b).localeCompare(this.toEntryTimestamp(a));
280
420
  });
281
- console.log(`## πŸ”₯ 졜근 μž‘μ—… (24h)\n`);
421
+ console.log(`## πŸ”₯ last Task (24h)\n`);
282
422
  for (const e of sortedShortTerm) {
283
423
  const star = e.important ? '⭐ ' : '';
284
424
  console.log(`- ${star}[${e.id}] ${e.summary} (${this.getPrimaryTag(e)})`);
@@ -302,7 +442,7 @@ class MemoryEngine {
302
442
  const diffDays = Math.max(1, Math.floor((newest.getTime() - oldest.getTime()) / msPerDay) + 1);
303
443
  const toMD = (d) => `${String(d.getMonth() + 1).padStart(2, '0')}/${String(d.getDate()).padStart(2, '0')}`;
304
444
  console.log('---');
305
- console.log(`πŸ“‹ ν† ν”½ μš”μ•½ (${toMD(oldest)} ~ ${toMD(newest)}, ${diffDays}일간)\n`);
445
+ console.log(`πŸ“‹ Topic summary (${toMD(oldest)} ~ ${toMD(newest)}, ${diffDays} days)\n`);
306
446
  }
307
447
  }
308
448
  console.log(summaryBody.trim());
@@ -310,7 +450,7 @@ class MemoryEngine {
310
450
  else {
311
451
  for (const tag of topics.sort()) {
312
452
  const count = entries.filter((e) => this.getPrimaryTag(e) === tag).length;
313
- console.log(`- **${tag}** (${count}건)`);
453
+ console.log(`- **${tag}** (${count} items)`);
314
454
  }
315
455
  }
316
456
  }
@@ -343,15 +483,15 @@ class MemoryEngine {
343
483
  byCategory[category] = [];
344
484
  byCategory[category].push(entry);
345
485
  }
346
- let md = `# ${agentId} λ©”λͺ¨λ¦¬\n\n`;
486
+ let md = `# ${agentId} memory\n\n`;
347
487
  md += `> Last Updated: ${today}\n`;
348
488
  md += `> Total: ${entries.length} memories\n\n`;
349
- md += `## πŸ”₯ 졜근 ${this.recentDays}일\n\n`;
489
+ md += `## πŸ”₯ Last ${this.recentDays} days\n\n`;
350
490
  if (recent.length === 0) {
351
- md += `(μ—†μŒ)\n\n`;
491
+ md += `(none)\n\n`;
352
492
  }
353
493
  else {
354
- md += `| ID | λ‚ μ§œ | μΉ΄ν…Œκ³ λ¦¬ | μš”μ•½ | 상세 |\n`;
494
+ md += `| ID | Date | Category | Summary | Details |\n`;
355
495
  md += `|----|------|----------|------|------|\n`;
356
496
  for (const entry of recent) {
357
497
  const detailIcon = entry.hasBody ? 'πŸ“„' : '-';
@@ -359,47 +499,43 @@ class MemoryEngine {
359
499
  }
360
500
  md += `\n`;
361
501
  }
362
- md += `## 🏷️ μΉ΄ν…Œκ³ λ¦¬λ³„\n\n`;
502
+ md += `## 🏷️ By category\n\n`;
363
503
  for (const [category, items] of Object.entries(byCategory).sort()) {
364
- md += `- **${category}**: ${items.length}건\n`;
504
+ md += `- **${category}**: ${items.length} items\n`;
365
505
  }
366
506
  const olderCount = entries.length - recent.length;
367
507
  const oldestDate = entries[entries.length - 1]?.date ?? '-';
368
508
  const newestDate = entries[0]?.date ?? '-';
369
- md += `\n## πŸ“Š 톡계\n\n`;
370
- md += `- 전체: ${entries.length}건 (${oldestDate} ~ ${newestDate})\n`;
371
- md += `- 졜근 ${this.recentDays}일: ${recent.length}건\n`;
509
+ md += `\n## πŸ“Š Stats\n\n`;
510
+ md += `- Total: ${entries.length} items (${oldestDate} ~ ${newestDate})\n`;
511
+ md += `- Last ${this.recentDays} days: ${recent.length} items\n`;
372
512
  if (olderCount > 0)
373
- md += `- ${this.recentDays}일 이전: ${olderCount}건\n`;
513
+ md += `- Older than ${this.recentDays} days: ${olderCount} items\n`;
374
514
  return md;
375
515
  }
376
516
  topic(agentId, topicName) {
377
517
  const entries = this.loadAllEntries(agentId);
378
518
  const filtered = entries.filter((e) => this.getPrimaryTag(e) === topicName);
379
519
  if (filtered.length === 0) {
380
- console.log(`ν† ν”½ '${topicName}'에 ν•΄λ‹Ήν•˜λŠ” 기얡이 μ—†μŠ΅λ‹ˆλ‹€.`);
520
+ console.log(`No memories found for topic '${topicName}'.`);
381
521
  return;
382
522
  }
383
- console.log(`## πŸ“‚ ν† ν”½: ${topicName} (${filtered.length}건)\n`);
523
+ console.log(`## πŸ“‚ Topic: ${topicName} (${filtered.length} items)\n`);
384
524
  for (const entry of filtered) {
385
525
  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(', ') || '-'}`);
526
+ console.log(`- File: data/${agentId}/entries/${entry.file}`);
527
+ console.log(`- Category: ${entry.category ?? '-'}`);
528
+ console.log(`- Tags: ${(entry.tags ?? []).join(', ') || '-'}`);
389
529
  console.log('');
390
530
  }
391
531
  }
392
532
  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);
533
+ const filtered = this.getRecentEntries(agentId, days);
398
534
  if (filtered.length === 0) {
399
- console.log(`졜근 ${days}일 λ‚΄ 기얡이 μ—†μŠ΅λ‹ˆλ‹€.`);
535
+ console.log(`No memories found in the last ${days} days.`);
400
536
  return;
401
537
  }
402
- console.log(`## πŸ”₯ 졜근 ${days}일 (${filtered.length}건)\n`);
538
+ console.log(`## πŸ”₯ Last ${days} days (${filtered.length} items)\n`);
403
539
  for (const entry of filtered) {
404
540
  const detailIcon = entry.hasBody ? ' πŸ“„' : '';
405
541
  const timestamp = String(entry.date).includes('T')
@@ -408,44 +544,75 @@ class MemoryEngine {
408
544
  console.log(`[${entry.id}] [${timestamp}] [${entry.category ?? '-'}] ${entry.summary}${detailIcon}`);
409
545
  }
410
546
  }
411
- find(agentId, keyword) {
412
- const entriesDir = this.getEntriesDir(agentId);
413
- if (!fs_1.default.existsSync(entriesDir)) {
414
- console.log('기얡이 μ—†μŠ΅λ‹ˆλ‹€.');
415
- return;
547
+ getRecentEntries(agentId, days = 30, opts = {}) {
548
+ const entries = this.loadAllEntries(agentId);
549
+ const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000)
550
+ .toISOString()
551
+ .slice(0, 10);
552
+ const filtered = entries.filter((e) => this.resolveRecentDate(agentId, e).slice(0, 10) >= cutoff);
553
+ if (!opts.includeBody)
554
+ return filtered;
555
+ return filtered.map((e) => ({
556
+ ...e,
557
+ body: this.readEntryBody(agentId, e.file),
558
+ }));
559
+ }
560
+ resolveRecentDate(agentId, entry) {
561
+ const updated = this.toDateString(entry.updated);
562
+ if (updated)
563
+ return updated;
564
+ const createdAt = this.toDateString(entry['created_at']);
565
+ if (createdAt)
566
+ return createdAt;
567
+ const date = this.toDateString(entry.date);
568
+ if (date)
569
+ return date;
570
+ try {
571
+ const filePath = path_1.default.join(this.getEntriesDir(agentId), entry.file);
572
+ return fs_1.default.statSync(filePath).mtime.toISOString();
416
573
  }
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 });
574
+ catch {
575
+ return '';
424
576
  }
425
- if (entries.length === 0) {
426
- console.log('기얡이 μ—†μŠ΅λ‹ˆλ‹€.');
577
+ }
578
+ toDateString(value) {
579
+ if (value &&
580
+ typeof value === 'object' &&
581
+ typeof value.toISOString === 'function') {
582
+ return value.toISOString();
583
+ }
584
+ return typeof value === 'string' ? value : '';
585
+ }
586
+ readEntryBody(agentId, file) {
587
+ try {
588
+ const raw = fs_1.default.readFileSync(path_1.default.join(this.getEntriesDir(agentId), file), 'utf-8');
589
+ const { content } = (0, parser_1.parseFrontmatter)(raw);
590
+ return content.replace(/^#[^\n]*\n*/m, '').trim();
591
+ }
592
+ catch {
593
+ return '';
594
+ }
595
+ }
596
+ find(agentId, keyword) {
597
+ const { files, index } = this.getSearchIndex(agentId);
598
+ if (files.length === 0) {
599
+ console.log('No memories found.');
427
600
  return;
428
601
  }
429
- const kcEntries = entries;
430
- const results = (0, knowledge_core_1.searchEntries)(kcEntries, keyword, { summary: 3, tags: 2, body: 1 });
602
+ const indexedResults = index.search(keyword, FIND_RESULT_LIMIT + 1);
603
+ const truncated = indexedResults.length > FIND_RESULT_LIMIT;
604
+ const results = indexedResults.slice(0, FIND_RESULT_LIMIT);
431
605
  if (results.length === 0) {
432
- console.log(`'${keyword}' κ΄€λ ¨ 기얡을 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.`);
606
+ console.log(`No related memories found for '${keyword}'.`);
433
607
  return;
434
608
  }
435
- console.log(`## πŸ” 검색 κ²°κ³Ό: "${keyword}" (${results.length}건, BM25 점수순)\n`);
609
+ const limitNote = truncated ? `, top ${FIND_RESULT_LIMIT}` : '';
610
+ console.log(`## πŸ” Search results: "${keyword}" (${results.length} items${limitNote}, sorted by BM25 score)\n`);
436
611
  for (const entry of results) {
437
- const score = entry._score != null ? ` [${entry._score}점]` : '';
612
+ const score = entry._score != null ? ` [${entry._score}pts]` : '';
438
613
  console.log(`[${entry.id}] [${entry.date}] [${this.getPrimaryTag(entry)}]${score} ${entry.summary}`);
439
614
  }
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
- };
615
+ const findCtx = this.createFindActionContext(agentId, keyword, results);
449
616
  const findActions = (0, knowledge_core_1.generateNextActions)(findCtx);
450
617
  const findBlock = (0, knowledge_core_1.formatActions)(findActions);
451
618
  if (findBlock)
@@ -470,7 +637,7 @@ class MemoryEngine {
470
637
  const raw = fs_1.default.readFileSync(path_1.default.join(entriesDir, e.file), 'utf-8');
471
638
  const { content: rawContent } = (0, parser_1.parseFrontmatter)(raw);
472
639
  const body = rawContent.replace(/^#[^\n]*\n*/m, '').trim();
473
- if (body !== '(상세 λ‚΄μš©μ„ 여기에 μΆ”κ°€)')
640
+ if (body !== '(Add details here)')
474
641
  content = body;
475
642
  }
476
643
  catch (_err) {
@@ -494,10 +661,10 @@ class MemoryEngine {
494
661
  filters.push(`category=${options.category}`);
495
662
  if (options.tag)
496
663
  filters.push(`tag=${options.tag}`);
497
- console.log(`쑰건에 λ§žλŠ” 기얡이 μ—†μŠ΅λ‹ˆλ‹€. (${filters.join(', ') || '전체'})`);
664
+ console.log(`No memories match the filters. (${filters.join(', ') || 'all'})`);
498
665
  return;
499
666
  }
500
- console.log(`## πŸ“‹ λͺ©λ‘: ${agentId} (${filtered.length}건)\n`);
667
+ console.log(`## πŸ“‹ List: ${agentId} (${filtered.length} items)\n`);
501
668
  for (const entry of filtered) {
502
669
  console.log(`[${entry.id}] [${entry.date}] [${this.getPrimaryTag(entry)}] ${entry.summary}`);
503
670
  }
@@ -518,7 +685,7 @@ class MemoryEngine {
518
685
  stats(agentId) {
519
686
  const entries = this.loadAllEntries(agentId);
520
687
  if (entries.length === 0) {
521
- console.log('기얡이 μ—†μŠ΅λ‹ˆλ‹€.');
688
+ console.log('No memories found.');
522
689
  return;
523
690
  }
524
691
  const byCategory = {};
@@ -532,15 +699,15 @@ class MemoryEngine {
532
699
  byTag[tag] = (byTag[tag] ?? 0) + 1;
533
700
  }
534
701
  }
535
- console.log(`# ${agentId} 톡계 (${entries.length}건)\n`);
536
- console.log(`## μΉ΄ν…Œκ³ λ¦¬λ³„`);
702
+ console.log(`# ${agentId} Stats (${entries.length} items)\n`);
703
+ console.log(`## By category`);
537
704
  for (const [cat, count] of Object.entries(byCategory).sort((a, b) => b[1] - a[1])) {
538
- console.log(` ${cat}: ${count}건`);
705
+ console.log(` ${cat}: ${count} items`);
539
706
  }
540
- console.log(`\n## νƒœκ·Έ Top 20`);
707
+ console.log(`\n## Top 20 Tags`);
541
708
  const sortedTags = Object.entries(byTag).sort((a, b) => b[1] - a[1]).slice(0, 20);
542
709
  for (const [tag, count] of sortedTags) {
543
- console.log(` ${tag}: ${count}건`);
710
+ console.log(` ${tag}: ${count} items`);
544
711
  }
545
712
  const topCategories = Object.entries(byCategory).sort((a, b) => b[1] - a[1]).map(t => t[0]);
546
713
  const statsCtx = {
@@ -557,7 +724,7 @@ class MemoryEngine {
557
724
  tags(agentId) {
558
725
  const entries = this.loadAllEntries(agentId);
559
726
  if (entries.length === 0) {
560
- console.log('기얡이 μ—†μŠ΅λ‹ˆλ‹€.');
727
+ console.log('No memories found.');
561
728
  return;
562
729
  }
563
730
  const byTag = {};
@@ -568,18 +735,18 @@ class MemoryEngine {
568
735
  }
569
736
  const sortedTags = Object.entries(byTag).sort((a, b) => b[1] - a[1]);
570
737
  if (sortedTags.length === 0) {
571
- console.log('νƒœκ·Έκ°€ μ—†μŠ΅λ‹ˆλ‹€.');
738
+ console.log('No tags found.');
572
739
  return;
573
740
  }
574
- console.log(`## 🏷️ 전체 νƒœκ·Έ (${sortedTags.length}개)\n`);
741
+ console.log(`## 🏷️ All tags (${sortedTags.length} items)\n`);
575
742
  for (const [tag, count] of sortedTags) {
576
- console.log(` ${tag}: ${count}건`);
743
+ console.log(` ${tag}: ${count} items`);
577
744
  }
578
745
  }
579
746
  get(agentId, memoryId) {
580
747
  const entriesDir = this.getEntriesDir(agentId);
581
748
  if (!fs_1.default.existsSync(entriesDir)) {
582
- console.log('기얡이 μ—†μŠ΅λ‹ˆλ‹€.');
749
+ console.log('No memories found.');
583
750
  return;
584
751
  }
585
752
  const files = fs_1.default.readdirSync(entriesDir).filter((f) => f.endsWith('.md'));
@@ -591,10 +758,10 @@ class MemoryEngine {
591
758
  const d = data;
592
759
  console.log(`## πŸ“„ ${d.summary}\n`);
593
760
  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}`);
761
+ console.log(`- Date: ${d.date}`);
762
+ console.log(`- Category: ${d.category ?? '-'}`);
763
+ console.log(`- Tags: ${(d.tags ?? []).join(', ') || '-'}`);
764
+ console.log(`- File: data/${agentId}/entries/${file}`);
598
765
  console.log(`\n---\n`);
599
766
  console.log(content.trim());
600
767
  const graphPath = path_1.default.join(this.getAgentDir(agentId), 'graph.json');
@@ -611,7 +778,7 @@ class MemoryEngine {
611
778
  nodeMap[n.id] = n;
612
779
  });
613
780
  console.log(`\n---\n`);
614
- console.log(`## πŸ”— μ—°κ΄€ κΈ°μ–΅ (${relatedEdges.length}개)\n`);
781
+ console.log(`## πŸ”— Related memories (${relatedEdges.length} items)\n`);
615
782
  for (const edge of relatedEdges) {
616
783
  const otherId = edge.from === memoryId ? edge.to : edge.from;
617
784
  const other = nodeMap[otherId];
@@ -638,12 +805,12 @@ class MemoryEngine {
638
805
  return;
639
806
  }
640
807
  }
641
- console.log(`ID '${memoryId}'λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.`);
808
+ console.log(`ID '${memoryId}' not found.`);
642
809
  }
643
810
  update(agentId, memoryId, options = {}) {
644
811
  const entry = this.findEntryById(agentId, memoryId);
645
812
  if (!entry) {
646
- console.log(`❌ ID '${memoryId}'λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.`);
813
+ console.log(`❌ ID '${memoryId}' not found.`);
647
814
  process.exit(1);
648
815
  }
649
816
  const { filePath, data, content } = entry;
@@ -664,11 +831,12 @@ class MemoryEngine {
664
831
  else if (options['no-important']) {
665
832
  delete newData['important'];
666
833
  }
667
- const newBody = options.body ?? existingBody ?? '(상세 λ‚΄μš©μ„ 여기에 μΆ”κ°€)';
834
+ const newBody = options.body ?? existingBody ?? '(Add details here)';
668
835
  const newFrontmatter = (0, parser_1.stringifyFrontmatter)(newData);
669
836
  const newContent = `${newFrontmatter}\n\n# ${String(newData['summary'])}\n\n${newBody}\n`;
670
837
  fs_1.default.writeFileSync(filePath, newContent, 'utf-8');
671
- console.log(`βœ… μˆ˜μ • μ™„λ£Œ: ${entry.file}`);
838
+ this.invalidateSearchIndex(agentId);
839
+ console.log(`βœ… Updated: ${entry.file}`);
672
840
  console.log(` ID: ${memoryId}`);
673
841
  if (options.summary)
674
842
  console.log(` Summary: ${String(newData['summary'])}`);
@@ -677,28 +845,29 @@ class MemoryEngine {
677
845
  if (options.tags)
678
846
  console.log(` Tags: ${newData['tags'].join(', ')}`);
679
847
  if (options.body)
680
- console.log(` Body: (μ—…λ°μ΄νŠΈλ¨)`);
848
+ console.log(` Body: (updated)`);
681
849
  }
682
850
  delete(agentId, memoryId, options = {}) {
683
851
  const entry = this.findEntryById(agentId, memoryId);
684
852
  if (!entry) {
685
- console.log(`❌ ID '${memoryId}'λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.`);
853
+ console.log(`❌ ID '${memoryId}' not found.`);
686
854
  process.exit(1);
687
855
  }
688
856
  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`);
857
+ console.log(`⚠️ Delete target: [${entry.data.id}] ${entry.data.summary}`);
858
+ console.log(` File: ${entry.file}`);
859
+ console.log(`\nAdd --force to delete it.`);
860
+ console.log(`Example: memory delete ${agentId} ${memoryId} --force`);
693
861
  return;
694
862
  }
695
863
  fs_1.default.unlinkSync(entry.filePath);
696
- console.log(`πŸ—‘οΈ μ‚­μ œ μ™„λ£Œ: ${entry.file}`);
864
+ this.invalidateSearchIndex(agentId);
865
+ console.log(`πŸ—‘οΈ Deleted: ${entry.file}`);
697
866
  console.log(` ID: ${memoryId}`);
698
867
  console.log(` Summary: ${entry.data.summary}`);
699
868
  try {
700
869
  this.markSummaryDirty(agentId);
701
- console.log(` πŸ“ summary.md κ°±μ‹  μ˜ˆμ•½λ¨`);
870
+ console.log(` πŸ“ summary.md update scheduled`);
702
871
  }
703
872
  catch (_e) {
704
873
  }
@@ -707,27 +876,27 @@ class MemoryEngine {
707
876
  const entry1 = this.findEntryById(agentId, memoryId1);
708
877
  const entry2 = this.findEntryById(agentId, memoryId2);
709
878
  if (!entry1) {
710
- console.log(`❌ ID '${memoryId1}'λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.`);
879
+ console.log(`❌ ID '${memoryId1}' not found.`);
711
880
  process.exit(1);
712
881
  }
713
882
  if (!entry2) {
714
- console.log(`❌ ID '${memoryId2}'λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.`);
883
+ console.log(`❌ ID '${memoryId2}' not found.`);
715
884
  process.exit(1);
716
885
  }
717
886
  const body1 = entry1.content.replace(/^#[^\n]*\n*/m, '').trim();
718
887
  const body2 = entry2.content.replace(/^#[^\n]*\n*/m, '').trim();
719
888
  const mergedSummary = options.summary ?? `${entry1.data.summary} + ${entry2.data.summary}`;
720
- const mergedBody = `## λ³‘ν•©λœ κΈ°μ–΅ 1 (${entry1.data.id}, ${entry1.data.date})
889
+ const mergedBody = `## Merged memory 1 (${entry1.data.id}, ${entry1.data.date})
721
890
  ${entry1.data.summary}
722
891
 
723
- ${body1 !== '(상세 λ‚΄μš©μ„ 여기에 μΆ”κ°€)' ? body1 : '(상세 μ—†μŒ)'}
892
+ ${body1 !== '(Add details here)' ? body1 : '(No details)'}
724
893
 
725
894
  ---
726
895
 
727
- ## λ³‘ν•©λœ κΈ°μ–΅ 2 (${entry2.data.id}, ${entry2.data.date})
896
+ ## Merged memory 2 (${entry2.data.id}, ${entry2.data.date})
728
897
  ${entry2.data.summary}
729
898
 
730
- ${body2 !== '(상세 λ‚΄μš©μ„ 여기에 μΆ”κ°€)' ? body2 : '(상세 μ—†μŒ)'}`;
899
+ ${body2 !== '(Add details here)' ? body2 : '(No details)'}`;
731
900
  const mergedTags = [
732
901
  ...new Set([...(entry1.data.tags ?? []), ...(entry2.data.tags ?? [])]),
733
902
  ];
@@ -749,10 +918,11 @@ ${body2 !== '(상세 λ‚΄μš©μ„ 여기에 μΆ”κ°€)' ? body2 : '(상세 μ—†μŒ)'}`;
749
918
  fs_1.default.writeFileSync(filepath, newContent, 'utf-8');
750
919
  fs_1.default.unlinkSync(entry1.filePath);
751
920
  fs_1.default.unlinkSync(entry2.filePath);
752
- console.log(`πŸ”€ 병합 μ™„λ£Œ!`);
753
- console.log(` μƒˆ ID: ${newId}`);
754
- console.log(` μƒˆ 파일: ${filename}`);
755
- console.log(` λ³‘ν•©λœ κΈ°μ–΅: [${memoryId1}] + [${memoryId2}]`);
921
+ this.invalidateSearchIndex(agentId);
922
+ console.log(`πŸ”€ Merge complete!`);
923
+ console.log(` New ID: ${newId}`);
924
+ console.log(` New file: ${filename}`);
925
+ console.log(` Merged memory: [${memoryId1}] + [${memoryId2}]`);
756
926
  console.log(` Summary: ${mergedSummary}`);
757
927
  }
758
928
  buildFallbackSummary(entries) {
@@ -778,7 +948,7 @@ ${body2 !== '(상세 λ‚΄μš©μ„ 여기에 μΆ”κ°€)' ? body2 : '(상세 μ—†μŒ)'}`;
778
948
  const oldest = dates[0];
779
949
  const newest = dates[dates.length - 1];
780
950
  const rangeLabel = oldest && newest ? `, ${toMD(oldest)}~${toMD(newest)}` : '';
781
- body += `## ${topic} (${items.length}건${rangeLabel})\n`;
951
+ body += `## ${topic} (${items.length} items${rangeLabel})\n`;
782
952
  for (const e of items)
783
953
  body += `- [${e.date}] ${e.summary}\n`;
784
954
  for (const e of items)
@@ -788,22 +958,26 @@ ${body2 !== '(상세 λ‚΄μš©μ„ 여기에 μΆ”κ°€)' ? body2 : '(상세 μ—†μŒ)'}`;
788
958
  return body;
789
959
  }
790
960
  callSummarizer(prompt) {
791
- const tmpFile = path_1.default.join(os_1.default.tmpdir(), `memory-summarizer-${Date.now()}.txt`);
792
961
  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) {
962
+ const result = execCrewx(['q', this.summarizerAgent], {
963
+ encoding: 'utf-8',
964
+ timeout: this.summarizerTimeout,
965
+ stdio: ['pipe', 'pipe', 'pipe'],
966
+ input: prompt,
967
+ });
968
+ const parsed = parseCrewxResponse(result);
969
+ this.recordSummarizerSuccess();
970
+ return parsed;
971
+ }
972
+ catch (e) {
973
+ const err = e;
974
+ const reason = err.code === 'ETIMEDOUT' || err.killed
975
+ ? `timed out after ${this.summarizerTimeout}ms`
976
+ : err.message;
977
+ console.error(`[memory] callSummarizer failed (agent=${this.summarizerAgent}), falling back to raw entry list: ${reason}`);
978
+ this.recordSummarizerFailure();
798
979
  return null;
799
980
  }
800
- finally {
801
- try {
802
- fs_1.default.unlinkSync(tmpFile);
803
- }
804
- catch (_e) {
805
- }
806
- }
807
981
  }
808
982
  generateSummary(agentId, options = {}) {
809
983
  const allEntries = this.loadAllEntries(agentId);
@@ -861,13 +1035,13 @@ ${body2 !== '(상세 λ‚΄μš©μ„ 여기에 μΆ”κ°€)' ? body2 : '(상세 μ—†μŒ)'}`;
861
1035
  for (const entry of newItems) {
862
1036
  const importantTag = entry.important ? ' [important]' : '';
863
1037
  p += `- [${entry.id}] [${entry.date}] [${this.getPrimaryTag(entry)}]${importantTag} ${entry.summary}`;
864
- if (entry.body && entry.body !== '(상세 λ‚΄μš©μ„ 여기에 μΆ”κ°€)') {
1038
+ if (entry.body && entry.body !== '(Add details here)') {
865
1039
  p += ` β€” ${entry.body.slice(0, 200)}`;
866
1040
  }
867
1041
  p += ` (file: entries/${entry.file})\n`;
868
1042
  }
869
1043
  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`;
1044
+ 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
1045
  p += `- Write concise Korean summary per topic (2-5 sentences)\n`;
872
1046
  p += `- After each topic summary, list relevant entry file links as "β†’ [entries/filename](entries/filename)"\n`;
873
1047
  p += `- Highlight key decisions, insights, and action items\n`;
@@ -917,76 +1091,141 @@ ${body2 !== '(상세 λ‚΄μš©μ„ 여기에 μΆ”κ°€)' ? body2 : '(상세 μ—†μŒ)'}`;
917
1091
  summarize(agentId, options = {}) {
918
1092
  const entries = this.loadAllEntries(agentId);
919
1093
  if (entries.length === 0) {
920
- console.log('기얡이 μ—†μŠ΅λ‹ˆλ‹€.');
1094
+ console.log('No memories found.');
921
1095
  return;
922
1096
  }
923
- console.log(`πŸ“ μš”μ•½ 생성 μ‹œμž‘ (${entries.length}개 μ—”νŠΈλ¦¬)\n`);
1097
+ console.log(`πŸ“ Starting summary generation (${entries.length} items entries)\n`);
924
1098
  const result = this.generateSummary(agentId, { force: options.force });
925
1099
  if (result.status === 'up-to-date') {
926
- console.log('βœ… summary.md μ΅œμ‹  μƒνƒœμž…λ‹ˆλ‹€.');
1100
+ console.log('βœ… summary.md is up to date.');
927
1101
  }
928
1102
  else if (result.status === 'fallback') {
929
- console.log(`πŸ“‹ 폴백 μš”μ•½ 생성 μ™„λ£Œ (AI 호좜 μ‹€νŒ¨)`);
1103
+ console.log(`πŸ“‹ Fallback summary generated (AI call failed)`);
930
1104
  }
931
1105
  else if (result.status === 'updated') {
932
- console.log(`πŸ€– μš”μ•½ μ™„λ£Œ (${result.newEntries}건 반영)`);
1106
+ console.log(`πŸ€– Summary complete (${result.newEntries} items applied)`);
1107
+ }
1108
+ console.log(` File: data/${agentId}/summary.md`);
1109
+ console.log(` entries: ${result.entryCount} items`);
1110
+ }
1111
+ isSummarizeLockStale(lockPath) {
1112
+ let content;
1113
+ try {
1114
+ content = fs_1.default.readFileSync(lockPath, 'utf-8');
1115
+ }
1116
+ catch {
1117
+ return true;
1118
+ }
1119
+ const pidMatch = content.match(/^pid=(\d+)/m);
1120
+ const pid = pidMatch ? Number(pidMatch[1]) : NaN;
1121
+ if (Number.isFinite(pid)) {
1122
+ try {
1123
+ process.kill(pid, 0);
1124
+ return false;
1125
+ }
1126
+ catch (e) {
1127
+ const err = e;
1128
+ if (err.code === 'ESRCH')
1129
+ return true;
1130
+ }
1131
+ }
1132
+ try {
1133
+ const stat = fs_1.default.statSync(lockPath);
1134
+ return Date.now() - stat.mtimeMs > exports.SUMMARIZE_LOCK_STALE_MS;
1135
+ }
1136
+ catch {
1137
+ return true;
1138
+ }
1139
+ }
1140
+ acquireSummarizeLock() {
1141
+ const lockPath = this.getSummarizeLockPath();
1142
+ const content = `pid=${process.pid}\nmtime=${new Date().toISOString()}\n`;
1143
+ try {
1144
+ const fd = fs_1.default.openSync(lockPath, 'wx');
1145
+ fs_1.default.writeSync(fd, content);
1146
+ fs_1.default.closeSync(fd);
1147
+ return true;
1148
+ }
1149
+ catch (e) {
1150
+ const err = e;
1151
+ if (err.code !== 'EEXIST')
1152
+ throw err;
1153
+ }
1154
+ if (!this.isSummarizeLockStale(lockPath)) {
1155
+ return false;
1156
+ }
1157
+ fs_1.default.writeFileSync(lockPath, content, 'utf-8');
1158
+ return true;
1159
+ }
1160
+ releaseSummarizeLock() {
1161
+ try {
1162
+ fs_1.default.unlinkSync(this.getSummarizeLockPath());
1163
+ }
1164
+ catch {
933
1165
  }
934
- console.log(` 파일: data/${agentId}/summary.md`);
935
- console.log(` μ—”νŠΈλ¦¬: ${result.entryCount}개`);
936
1166
  }
937
1167
  summarizeDirty() {
938
1168
  if (!fs_1.default.existsSync(this.dataDir)) {
939
- console.log(`πŸ’‘ λ””λ°”μš΄μŠ€ cron 등둝:`);
1169
+ console.log(`πŸ’‘ Register debounce cron:`);
940
1170
  console.log(`npx cron add "*/1 * * * *" "npx memory summarize-dirty" --mode command --name "memory-debounce"`);
941
1171
  return;
942
1172
  }
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 μ΅œμ‹  μƒνƒœμž…λ‹ˆλ‹€.`);
1173
+ if (!this.acquireSummarizeLock()) {
1174
+ console.log(`⏭️ summarize-dirty is already running (lock held) β€” skipping this run.`);
1175
+ return;
1176
+ }
1177
+ try {
1178
+ const agentDirs = fs_1.default
1179
+ .readdirSync(this.dataDir, { withFileTypes: true })
1180
+ .filter((entry) => entry.isDirectory())
1181
+ .map((entry) => entry.name);
1182
+ for (const agentId of agentDirs) {
1183
+ const dirtyPath = this.getDirtySummaryPath(agentId);
1184
+ if (!fs_1.default.existsSync(dirtyPath))
1185
+ continue;
1186
+ try {
1187
+ const result = this.generateSummary(agentId);
1188
+ if (result.status === 'updated') {
1189
+ console.log(`πŸ€– ${agentId} summary.md updated (${result.newEntries} items applied)`);
1190
+ }
1191
+ else if (result.status === 'fallback') {
1192
+ console.log(`πŸ“‹ ${agentId} Fallback summary generated (AI call failed)`);
1193
+ }
1194
+ else if (result.status === 'up-to-date') {
1195
+ console.log(`βœ… ${agentId} summary.md is up to date.`);
1196
+ }
1197
+ else if (result.status === 'empty') {
1198
+ console.log(`ℹ️ ${agentId} has no memories to summarize.`);
1199
+ }
1200
+ fs_1.default.unlinkSync(dirtyPath);
961
1201
  }
962
- else if (result.status === 'empty') {
963
- console.log(`ℹ️ ${agentId} μš”μ•½ν•  기얡이 μ—†μŠ΅λ‹ˆλ‹€.`);
1202
+ catch (e) {
1203
+ console.error(`❌ ${agentId} summary.md update failed: ${e.message}`);
964
1204
  }
965
- fs_1.default.unlinkSync(dirtyPath);
966
- }
967
- catch (e) {
968
- console.error(`❌ ${agentId} summary.md κ°±μ‹  μ‹€νŒ¨: ${e.message}`);
969
1205
  }
970
1206
  }
971
- console.log(`πŸ’‘ λ””λ°”μš΄μŠ€ cron 등둝:`);
1207
+ finally {
1208
+ this.releaseSummarizeLock();
1209
+ }
1210
+ console.log(`πŸ’‘ Register debounce cron:`);
972
1211
  console.log(`npx cron add "*/1 * * * *" "npx memory summarize-dirty" --mode command --name "memory-debounce"`);
973
1212
  }
974
1213
  search(agentId, query) {
975
1214
  const entries = this.loadAllEntries(agentId);
976
1215
  if (entries.length === 0) {
977
- console.log('기얡이 μ—†μŠ΅λ‹ˆλ‹€.');
1216
+ console.log('No memories found.');
978
1217
  return;
979
1218
  }
980
1219
  const memoryList = entries
981
1220
  .map((e) => `[${e.id}] [${e.date}] [${this.getPrimaryTag(e)}] ${e.summary}`)
982
1221
  .join('\n');
983
- const task = `κΈ°μ–΅ λͺ©λ‘:\n${memoryList}\n\n질문: "${query}"`;
1222
+ const task = `Memory list:\n${memoryList}\n\nQuestion: "${query}"`;
984
1223
  try {
985
- console.log(`πŸ” "${query}" 검색 쀑... (${this.searcherAgent})\n`);
986
- 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'] });
1224
+ console.log(`πŸ” "${query}" searching... (${this.searcherAgent})\n`);
1225
+ const result = execCrewx(['q', `${this.searcherAgent} ${task}`], { encoding: 'utf-8', timeout: this.searcherTimeout, stdio: ['pipe', 'pipe', 'pipe'] });
987
1226
  const parsed = parseCrewxResponse(result);
988
1227
  if (parsed) {
989
- console.log(`## 🧠 μ‹œλ§¨ν‹± 검색 κ²°κ³Ό\n`);
1228
+ console.log(`## 🧠 Semantic search results\n`);
990
1229
  console.log(parsed);
991
1230
  }
992
1231
  else {
@@ -996,12 +1235,12 @@ ${body2 !== '(상세 λ‚΄μš©μ„ 여기에 μΆ”κ°€)' ? body2 : '(상세 μ—†μŒ)'}`;
996
1235
  catch (error) {
997
1236
  const err = error;
998
1237
  if (err.code === 'ETIMEDOUT' || err.killed) {
999
- console.log('검색 μ‹€νŒ¨: μ‹œκ°„ 초과 (μ΅œλŒ€ 5λΆ„)');
1238
+ console.log('Search failed: timed out (max 5 minutes)');
1000
1239
  }
1001
1240
  else {
1002
- console.log('검색 μ‹€νŒ¨:', err.message);
1241
+ console.log('Search failed:', err.message);
1003
1242
  }
1004
- console.log('\nπŸ’‘ Tip: find λͺ…λ ΉμœΌλ‘œ ν‚€μ›Œλ“œ 검색을 μ‹œλ„ν•΄λ³΄μ„Έμš”.');
1243
+ console.log('\nπŸ’‘ Tip: try keyword search with the find command.');
1005
1244
  }
1006
1245
  }
1007
1246
  }