@crewx/memory 0.1.23-rc.14 β†’ 0.1.23-rc.141

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