claude-session-continuity-mcp 1.13.1 → 1.13.2
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.
|
@@ -265,6 +265,7 @@ async function main() {
|
|
|
265
265
|
process.exit(0);
|
|
266
266
|
}
|
|
267
267
|
const db = new Database(dbPath);
|
|
268
|
+
db.pragma('journal_mode = WAL'); // 다중 hook 프로세스 동시성 보장
|
|
268
269
|
// hot_paths 추적 (모든 추적 도구)
|
|
269
270
|
try {
|
|
270
271
|
const pathType = filePath.includes('.') ? 'file' : 'directory';
|
|
@@ -209,6 +209,7 @@ async function main() {
|
|
|
209
209
|
process.exit(0);
|
|
210
210
|
}
|
|
211
211
|
const db = new Database(dbPath);
|
|
212
|
+
db.pragma('journal_mode = WAL'); // 다중 hook 프로세스 동시성 보장
|
|
212
213
|
// 핸드오버 컨텍스트 빌드
|
|
213
214
|
const handover = input.transcript ? buildHandoverContext(input.transcript) : null;
|
|
214
215
|
// active_context 업데이트 (memories에는 저장하지 않음)
|
|
@@ -284,7 +285,8 @@ async function main() {
|
|
|
284
285
|
process.exit(0);
|
|
285
286
|
}
|
|
286
287
|
catch (e) {
|
|
287
|
-
//
|
|
288
|
+
// fail-soft: 컴팩션이 멈추지 않도록 continue:true 반드시 반환
|
|
289
|
+
process.stdout.write(JSON.stringify({ continue: true }));
|
|
288
290
|
process.exit(0);
|
|
289
291
|
}
|
|
290
292
|
}
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import * as fs from 'fs';
|
|
13
13
|
import * as path from 'path';
|
|
14
14
|
import * as readline from 'readline';
|
|
15
|
+
import * as crypto from 'crypto';
|
|
15
16
|
import Database from 'better-sqlite3';
|
|
16
17
|
function detectWorkspaceRoot(cwd) {
|
|
17
18
|
let current = cwd;
|
|
@@ -238,6 +239,8 @@ async function parseTranscriptSinglePass(transcriptPath) {
|
|
|
238
239
|
userRequests: { firstRequest: '', allRequests: [] },
|
|
239
240
|
recentAssistantMessages: [],
|
|
240
241
|
errorFixPairs: [],
|
|
242
|
+
firstTimestamp: null,
|
|
243
|
+
lastTimestamp: null,
|
|
241
244
|
};
|
|
242
245
|
if (!transcriptPath || !fs.existsSync(transcriptPath))
|
|
243
246
|
return result;
|
|
@@ -267,6 +270,12 @@ async function parseTranscriptSinglePass(transcriptPath) {
|
|
|
267
270
|
const entry = JSON.parse(line);
|
|
268
271
|
const role = entry.type || entry.role || '';
|
|
269
272
|
const content = entry.message?.content;
|
|
273
|
+
// === 0. Timestamp 추출 (세션 duration 계산용) ===
|
|
274
|
+
if (entry.timestamp) {
|
|
275
|
+
if (!result.firstTimestamp)
|
|
276
|
+
result.firstTimestamp = entry.timestamp;
|
|
277
|
+
result.lastTimestamp = entry.timestamp;
|
|
278
|
+
}
|
|
270
279
|
// === 1. Commit 추출 (tool_use 블록에서) ===
|
|
271
280
|
if (line.includes('git commit') && Array.isArray(content)) {
|
|
272
281
|
for (const block of content) {
|
|
@@ -343,9 +352,9 @@ async function parseTranscriptSinglePass(transcriptPath) {
|
|
|
343
352
|
}
|
|
344
353
|
}
|
|
345
354
|
result.decisions = [...decisionSet].slice(0, 3);
|
|
346
|
-
// Error-Fix pairs
|
|
347
|
-
const errorRe = /(?:error|Error|ERROR
|
|
348
|
-
const fixRe = /(?:fixed|resolved
|
|
355
|
+
// Error-Fix pairs (한국어 패턴 보강)
|
|
356
|
+
const errorRe = /(?:error|Error|ERROR|오류|에러|버그|예외|실패|FAILED|Exception|TypeError|ReferenceError|SyntaxError|crash|crashed|충돌|문제)[:\s](.{5,80})/;
|
|
357
|
+
const fixRe = /(?:fixed|resolved|patched|수정|해결|고침|처리|완료|변경|적용|반영|커밋|Added|수정 완료|문제 해결|해결됨|되돌림)/i;
|
|
349
358
|
const pairSet = new Set();
|
|
350
359
|
for (let i = 0; i < recentEntries.length - 1; i++) {
|
|
351
360
|
const errorMatch = recentEntries[i].text.match(errorRe);
|
|
@@ -370,6 +379,45 @@ async function parseTranscriptSinglePass(transcriptPath) {
|
|
|
370
379
|
result.recentAssistantMessages = result.recentAssistantMessages.slice(-5);
|
|
371
380
|
return result;
|
|
372
381
|
}
|
|
382
|
+
/**
|
|
383
|
+
* 슬래시 커맨드 prefix 제거 — "/mcp-dev 측정해줘" → "측정해줘"
|
|
384
|
+
* 첫 토큰이 `/`로 시작하면 다음 의미 토큰까지 스킵
|
|
385
|
+
* 슬래시뿐이면 빈 문자열 반환 (호출자가 폴백 처리)
|
|
386
|
+
*/
|
|
387
|
+
function stripSlashPrefix(text) {
|
|
388
|
+
const trimmed = text.trim();
|
|
389
|
+
if (!trimmed.startsWith('/'))
|
|
390
|
+
return trimmed;
|
|
391
|
+
// 첫 줄에서 슬래시 토큰을 제거
|
|
392
|
+
const firstLine = trimmed.split('\n')[0];
|
|
393
|
+
const tokens = firstLine.split(/\s+/);
|
|
394
|
+
let i = 0;
|
|
395
|
+
while (i < tokens.length && tokens[i].startsWith('/'))
|
|
396
|
+
i++;
|
|
397
|
+
const rest = tokens.slice(i).join(' ').trim();
|
|
398
|
+
if (rest.length >= 3)
|
|
399
|
+
return rest;
|
|
400
|
+
// 다음 줄에 의미 있는 본문이 있으면 사용
|
|
401
|
+
const lines = trimmed.split('\n').slice(1).map(l => l.trim()).filter(l => l.length >= 3 && !l.startsWith('/'));
|
|
402
|
+
return lines[0] || '';
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Jaccard 유사도 (토큰 단위) — 0~1 사이
|
|
406
|
+
* 동일하면 1, 완전 다르면 0
|
|
407
|
+
*/
|
|
408
|
+
function jaccardSimilarity(a, b) {
|
|
409
|
+
const tokenize = (s) => new Set(s.toLowerCase().replace(/[^\p{L}\p{N}\s]/gu, ' ').split(/\s+/).filter(t => t.length >= 2));
|
|
410
|
+
const setA = tokenize(a);
|
|
411
|
+
const setB = tokenize(b);
|
|
412
|
+
if (setA.size === 0 || setB.size === 0)
|
|
413
|
+
return 0;
|
|
414
|
+
let intersect = 0;
|
|
415
|
+
for (const t of setA)
|
|
416
|
+
if (setB.has(t))
|
|
417
|
+
intersect++;
|
|
418
|
+
const union = setA.size + setB.size - intersect;
|
|
419
|
+
return union === 0 ? 0 : intersect / union;
|
|
420
|
+
}
|
|
373
421
|
/**
|
|
374
422
|
* 사용자 메시지들을 세션 요약으로 압축
|
|
375
423
|
* 예: ["MCP 테스트해줘", "개선해줘", "npm 배포하고 커밋해줘"] → "MCP 테스트 + 개선 + npm 배포/커밋"
|
|
@@ -377,12 +425,24 @@ async function parseTranscriptSinglePass(transcriptPath) {
|
|
|
377
425
|
function summarizeUserRequests(requests) {
|
|
378
426
|
if (requests.length === 0)
|
|
379
427
|
return '';
|
|
380
|
-
|
|
381
|
-
|
|
428
|
+
// 슬래시 커맨드 도움말 본문(/work, /clone-pro 등이 첫 줄에 박히는 케이스) 제외
|
|
429
|
+
// → 36건 동일 last_work 누적 문제 해결
|
|
430
|
+
const meaningful = requests
|
|
431
|
+
.map(r => stripSlashPrefix(r))
|
|
432
|
+
.filter(r => {
|
|
433
|
+
if (!r)
|
|
434
|
+
return false;
|
|
435
|
+
if (/^[A-Z][a-z]+\s+(skill|command):/i.test(r))
|
|
436
|
+
return false;
|
|
437
|
+
return r.length > 0;
|
|
438
|
+
});
|
|
439
|
+
const source = meaningful.length > 0 ? meaningful : requests;
|
|
440
|
+
if (source.length === 1)
|
|
441
|
+
return source[0];
|
|
382
442
|
// 중복/유사 요청 제거 (앞 20글자 기준)
|
|
383
443
|
const unique = [];
|
|
384
444
|
const seen = new Set();
|
|
385
|
-
for (const req of
|
|
445
|
+
for (const req of source) {
|
|
386
446
|
const key = req.slice(0, 20).toLowerCase();
|
|
387
447
|
if (!seen.has(key)) {
|
|
388
448
|
seen.add(key);
|
|
@@ -407,20 +467,59 @@ async function main() {
|
|
|
407
467
|
inputData += chunk;
|
|
408
468
|
}
|
|
409
469
|
const input = inputData ? JSON.parse(inputData) : {};
|
|
470
|
+
// 중복 호출 가드 1: stop_hook_active 플래그 (Claude Code 공식 플래그)
|
|
471
|
+
if (input.stop_hook_active === true) {
|
|
472
|
+
process.exit(0);
|
|
473
|
+
}
|
|
410
474
|
const cwd = input.cwd || process.cwd();
|
|
411
475
|
const project = detectProject(cwd);
|
|
412
476
|
const dbPath = getDbPath(cwd);
|
|
477
|
+
// 중복 호출 가드 2: transcript_path 해시 기반 5초 윈도우 파일락
|
|
478
|
+
// Phase 3: session_id 5초 락 도입 (sid 있는 호출만 차단됨)
|
|
479
|
+
// Phase 5: 실측에서 모든 stop이 [sid 있는 호출 + sid 없는 호출] 페어로 들어옴
|
|
480
|
+
// → transcript_path 해시를 우선 키로 사용해야 같은 페어가 같은 락을 공유
|
|
481
|
+
// → transcript_path 없을 때만 session_id 폴백
|
|
482
|
+
const lockKey = input.transcript_path
|
|
483
|
+
? crypto.createHash('md5').update(input.transcript_path).digest('hex').slice(0, 16)
|
|
484
|
+
: (input.session_id || null);
|
|
485
|
+
if (lockKey) {
|
|
486
|
+
const lockPath = path.join(path.dirname(dbPath), `.session-end-${lockKey}.lock`);
|
|
487
|
+
const now = Date.now();
|
|
488
|
+
try {
|
|
489
|
+
// Phase 5: atomic `wx` (존재 시 EEXIST throw) → 두 hook 인스턴스가 거의 동시에 진입하는 race 차단
|
|
490
|
+
// 베이스라인: id=1606/1607이 ~500ms 차이로 동시 INSERT 됨 (debug.log 13:26:51.205 + 13:26:51.702)
|
|
491
|
+
fs.writeFileSync(lockPath, String(now), { flag: 'wx' });
|
|
492
|
+
}
|
|
493
|
+
catch (e) {
|
|
494
|
+
// 파일이 이미 존재 (다른 hook 인스턴스가 처리 중) → mtime 확인 후 5초 내면 차단
|
|
495
|
+
if (e.code === 'EEXIST') {
|
|
496
|
+
try {
|
|
497
|
+
const lockMtime = fs.statSync(lockPath).mtimeMs;
|
|
498
|
+
if (now - lockMtime < 5000) {
|
|
499
|
+
process.exit(0); // 5초 내 재발화 차단
|
|
500
|
+
}
|
|
501
|
+
// 5초 지난 stale 락 → 덮어쓰기 (이 호출이 새 작업)
|
|
502
|
+
fs.writeFileSync(lockPath, String(now));
|
|
503
|
+
}
|
|
504
|
+
catch {
|
|
505
|
+
// stat/write 실패는 fail-soft
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
// 그 외 락 파일 에러는 무시 (fail-soft)
|
|
509
|
+
}
|
|
510
|
+
}
|
|
413
511
|
// 디버그 로그
|
|
414
512
|
const debugLogPath = path.join(path.dirname(dbPath), 'session-end-debug.log');
|
|
415
513
|
const inputKeys = Object.keys(input);
|
|
416
514
|
const lastMsgLen = input.last_assistant_message?.length || 0;
|
|
417
|
-
const debugLine = `[${new Date().toISOString()}] project=${project} keys=[${inputKeys.join(',')}] transcript_path=${input.transcript_path || 'none'} last_msg_len=${lastMsgLen}\n`;
|
|
515
|
+
const debugLine = `[${new Date().toISOString()}] project=${project} sid=${input.session_id?.slice(0, 8) || 'none'} keys=[${inputKeys.join(',')}] transcript_path=${input.transcript_path || 'none'} last_msg_len=${lastMsgLen}\n`;
|
|
418
516
|
fs.appendFileSync(debugLogPath, debugLine);
|
|
419
517
|
if (!fs.existsSync(dbPath)) {
|
|
420
518
|
console.log('[SessionEnd] No DB found, skipping');
|
|
421
519
|
process.exit(0);
|
|
422
520
|
}
|
|
423
521
|
const db = new Database(dbPath);
|
|
522
|
+
db.pragma('journal_mode = WAL'); // 다중 hook 프로세스 동시성 보장
|
|
424
523
|
// === 추출 시작 ===
|
|
425
524
|
let lastWork = '';
|
|
426
525
|
let nextTasks = [];
|
|
@@ -433,6 +532,8 @@ async function main() {
|
|
|
433
532
|
userRequests: { firstRequest: '', allRequests: [] },
|
|
434
533
|
recentAssistantMessages: [],
|
|
435
534
|
errorFixPairs: [],
|
|
535
|
+
firstTimestamp: null,
|
|
536
|
+
lastTimestamp: null,
|
|
436
537
|
};
|
|
437
538
|
if (input.transcript_path) {
|
|
438
539
|
transcript = await parseTranscriptSinglePass(input.transcript_path);
|
|
@@ -441,7 +542,9 @@ async function main() {
|
|
|
441
542
|
decisions = transcript.decisions;
|
|
442
543
|
}
|
|
443
544
|
// lastWork 결정 (우선순위 폴백)
|
|
444
|
-
const { firstRequest, allRequests } = transcript.userRequests;
|
|
545
|
+
const { firstRequest: rawFirstRequest, allRequests } = transcript.userRequests;
|
|
546
|
+
// firstRequest 슬래시 prefix 제거 (예: "/mcp-dev 측정" → "측정")
|
|
547
|
+
const firstRequest = rawFirstRequest ? (stripSlashPrefix(rawFirstRequest) || rawFirstRequest) : '';
|
|
445
548
|
// 2a: 사용자 요청 + 커밋 메시지 조합 (가장 이상적)
|
|
446
549
|
if (firstRequest && commitMessages.length > 0) {
|
|
447
550
|
lastWork = `${firstRequest} → ${commitMessages.slice(0, 2).join('; ')}`;
|
|
@@ -519,23 +622,52 @@ async function main() {
|
|
|
519
622
|
const fileNames = modifiedFiles.slice(0, 5).map(f => path.basename(f)).join(', ');
|
|
520
623
|
lastWork = `Modified files: ${fileNames}`;
|
|
521
624
|
}
|
|
625
|
+
// Phase 5: 모든 last_work 결정 경로에 stripSlashPrefix 강제 적용
|
|
626
|
+
// 베이스라인: 2a 경로(firstRequest)만 stripSlashPrefix 적용되고 2c~2e 폴백은 미적용
|
|
627
|
+
// → 같은 transcript에서 두 hook 인스턴스가 서로 다른 경로로 들어가
|
|
628
|
+
// 한쪽은 "측정", 한쪽은 "/mcp-dev 측정"으로 분기 → Jaccard 0.85 미달로 둘 다 통과
|
|
629
|
+
// stripSlashPrefix가 빈 문자열을 반환하면 원본 유지 (의미 토큰이 없는 경우)
|
|
630
|
+
if (lastWork) {
|
|
631
|
+
const stripped = stripSlashPrefix(lastWork);
|
|
632
|
+
if (stripped)
|
|
633
|
+
lastWork = stripped;
|
|
634
|
+
}
|
|
522
635
|
// 빈 세션 skip
|
|
523
636
|
if (!lastWork) {
|
|
524
637
|
console.log(`[SessionEnd] Skipping empty session for ${project} (no meaningful last_work)`);
|
|
525
638
|
db.close();
|
|
526
639
|
process.exit(0);
|
|
527
640
|
}
|
|
528
|
-
// 중복 저장 방지
|
|
529
|
-
|
|
641
|
+
// 중복 저장 방지 — Jaccard 유사도 기반 (1시간 이내, 동일 프로젝트)
|
|
642
|
+
// 베이스라인: 동일 last_work가 4~5분 간격으로 반복 INSERT되는 케이스 5건/24h 발견
|
|
643
|
+
// 정확 일치 + Jaccard >= 0.85 둘 다 차단
|
|
644
|
+
const recentExact = db.prepare(`
|
|
530
645
|
SELECT id FROM sessions
|
|
531
646
|
WHERE project = ? AND last_work = ? AND timestamp > datetime('now', '-1 hour')
|
|
532
647
|
LIMIT 1
|
|
533
648
|
`).get(project, lastWork);
|
|
534
|
-
if (
|
|
535
|
-
console.log(`[SessionEnd] Skipping duplicate
|
|
649
|
+
if (recentExact) {
|
|
650
|
+
console.log(`[SessionEnd] Skipping duplicate (exact) for ${project}`);
|
|
536
651
|
db.close();
|
|
537
652
|
process.exit(0);
|
|
538
653
|
}
|
|
654
|
+
const recentRows = db.prepare(`
|
|
655
|
+
SELECT last_work FROM sessions
|
|
656
|
+
WHERE project = ? AND timestamp > datetime('now', '-1 hour')
|
|
657
|
+
ORDER BY timestamp DESC LIMIT 10
|
|
658
|
+
`).all(project);
|
|
659
|
+
for (const row of recentRows) {
|
|
660
|
+
if (!row.last_work)
|
|
661
|
+
continue;
|
|
662
|
+
// Phase 5: 비교 전 양쪽 모두 stripSlashPrefix 정규화 → 표현 차이 흡수
|
|
663
|
+
const normalizedCurrent = stripSlashPrefix(lastWork) || lastWork;
|
|
664
|
+
const normalizedRow = stripSlashPrefix(row.last_work) || row.last_work;
|
|
665
|
+
if (jaccardSimilarity(normalizedCurrent, normalizedRow) >= 0.85) {
|
|
666
|
+
console.log(`[SessionEnd] Skipping near-duplicate (jaccard >= 0.85) for ${project}`);
|
|
667
|
+
db.close();
|
|
668
|
+
process.exit(0);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
539
671
|
// 구조화 메타데이터 (issues 컬럼 활용)
|
|
540
672
|
const metadata = {
|
|
541
673
|
commits: commitMessages,
|
|
@@ -543,11 +675,20 @@ async function main() {
|
|
|
543
675
|
errorsSolved
|
|
544
676
|
};
|
|
545
677
|
const hasMetadata = commitMessages.length > 0 || decisions.length > 0 || errorsSolved.length > 0;
|
|
678
|
+
// 세션 duration 계산 (transcript first ↔ last timestamp)
|
|
679
|
+
let durationMinutes = null;
|
|
680
|
+
if (transcript.firstTimestamp && transcript.lastTimestamp) {
|
|
681
|
+
const start = new Date(transcript.firstTimestamp).getTime();
|
|
682
|
+
const end = new Date(transcript.lastTimestamp).getTime();
|
|
683
|
+
if (!isNaN(start) && !isNaN(end) && end >= start) {
|
|
684
|
+
durationMinutes = Math.round((end - start) / 60000);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
546
687
|
// 세션 기록 저장
|
|
547
688
|
db.prepare(`
|
|
548
|
-
INSERT INTO sessions (project, last_work, next_tasks, modified_files, issues)
|
|
549
|
-
VALUES (?, ?, ?, ?, ?)
|
|
550
|
-
`).run(project, lastWork, JSON.stringify([...new Set(nextTasks)].slice(0, 5)), JSON.stringify(modifiedFiles.slice(0, 15)), hasMetadata ? JSON.stringify(metadata) : null);
|
|
689
|
+
INSERT INTO sessions (project, last_work, next_tasks, modified_files, issues, duration_minutes)
|
|
690
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
691
|
+
`).run(project, lastWork, JSON.stringify([...new Set(nextTasks)].slice(0, 5)), JSON.stringify(modifiedFiles.slice(0, 15)), hasMetadata ? JSON.stringify(metadata) : null, durationMinutes);
|
|
551
692
|
// 활성 컨텍스트 업데이트
|
|
552
693
|
db.prepare(`
|
|
553
694
|
INSERT OR REPLACE INTO active_context (project, current_state, recent_files, updated_at)
|
|
@@ -588,6 +729,35 @@ async function main() {
|
|
|
588
729
|
}
|
|
589
730
|
catch { /* project_context table may not exist */ }
|
|
590
731
|
}
|
|
732
|
+
// 고품질 자동 메모리 추출 (v1.10 노이즈 제거 정책 유지하면서 가치 있는 것만)
|
|
733
|
+
// - decisions: 의미있는 의사결정 (importance=7)
|
|
734
|
+
// - commits: feat/fix만 (importance=6)
|
|
735
|
+
// - 동일 content 중복 방지 (검색해서 없을 때만 INSERT)
|
|
736
|
+
try {
|
|
737
|
+
const memoryDupCheck = db.prepare('SELECT id FROM memories WHERE project = ? AND content = ? LIMIT 1');
|
|
738
|
+
const memoryInsert = db.prepare(`
|
|
739
|
+
INSERT INTO memories (content, memory_type, tags, project, importance)
|
|
740
|
+
VALUES (?, ?, ?, ?, ?)
|
|
741
|
+
`);
|
|
742
|
+
for (const decision of decisions) {
|
|
743
|
+
if (decision.length < 15)
|
|
744
|
+
continue;
|
|
745
|
+
if (!memoryDupCheck.get(project, decision)) {
|
|
746
|
+
memoryInsert.run(decision, 'decision', JSON.stringify(['auto-extracted']), project, 7);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
// feat/fix 커밋만 (chore, docs, style은 학습 가치 낮음)
|
|
750
|
+
for (const commit of commitMessages) {
|
|
751
|
+
if (!/^(feat|fix)(\(.+\))?:/i.test(commit))
|
|
752
|
+
continue;
|
|
753
|
+
if (commit.length < 20)
|
|
754
|
+
continue;
|
|
755
|
+
if (!memoryDupCheck.get(project, commit)) {
|
|
756
|
+
memoryInsert.run(commit, 'learning', JSON.stringify(['auto-extracted', 'commit']), project, 6);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
catch { /* memories table issue, skip */ }
|
|
591
761
|
// 세션 임베딩 사전 생성 (search_sessions 성능 최적화)
|
|
592
762
|
try {
|
|
593
763
|
const lastSession = db.prepare('SELECT id FROM sessions WHERE project = ? ORDER BY timestamp DESC LIMIT 1').get(project);
|
|
@@ -72,6 +72,7 @@ function loadContext(dbPath, project) {
|
|
|
72
72
|
return null;
|
|
73
73
|
try {
|
|
74
74
|
const db = new Database(dbPath);
|
|
75
|
+
db.pragma('journal_mode = WAL'); // 다중 hook 프로세스 동시성 보장
|
|
75
76
|
// 노이즈 메모리 자동 정리
|
|
76
77
|
cleanupNoiseMemories(db);
|
|
77
78
|
const lines = [`# ${project} - Session Resumed\n`];
|
|
@@ -46,14 +46,19 @@ function getProject(cwd, workspaceRoot) {
|
|
|
46
46
|
}
|
|
47
47
|
// ===== 과거 참조 자동 감지 =====
|
|
48
48
|
const PAST_REFERENCE_PATTERNS = [
|
|
49
|
-
// 한국어
|
|
49
|
+
// 한국어 - 시간 참조
|
|
50
50
|
/(?:저번에|전에|이전에|그때|지난번에|예전에|아까)\s+(.+?)(?:\s*(?:어떻게|뭐|무엇|왜|어디|언제))/,
|
|
51
51
|
/(?:했던|했었던|만들었던|수정했던|구현했던|해결했던)\s*(.+)/,
|
|
52
52
|
/(?:지난|이전|전)\s*(?:세션|작업|시간|번).*?(?:에서|때)\s*(.+)/,
|
|
53
|
+
// 한국어 - 보유/기억 질문 ("내꺼 GCP 코인 정보 가지고 있나?")
|
|
54
|
+
/(?:내|내꺼|우리)\s+(.+?)\s+(?:가지고\s*있|있어|있나|있지|있냐|남아|남았|저장)/,
|
|
55
|
+
/(.+?)\s+(?:기억해|기억하고|기억나|알고\s*있|아는)/,
|
|
56
|
+
/(?:저장한|기록한|적어둔|메모한|남긴)\s+(.+)/,
|
|
53
57
|
// 영어
|
|
54
58
|
/(?:last time|before|previously|earlier)\s+(?:.*?)\s*((?:how|what|why|where|when).*)/i,
|
|
55
59
|
/(?:did we|did I|have we|have I)\s+(.+)\s+(?:before|last time|earlier)/i,
|
|
56
|
-
/(?:remember when|recall when)\s+(.+)/i,
|
|
60
|
+
/(?:remember when|recall when|do you remember|do you recall)\s+(.+)/i,
|
|
61
|
+
/(?:do you have|do you know|got)\s+(.+?)\s+(?:info|information|saved|stored|record)/i,
|
|
57
62
|
];
|
|
58
63
|
function extractPastKeywords(prompt) {
|
|
59
64
|
for (const pattern of PAST_REFERENCE_PATTERNS) {
|
|
@@ -176,6 +181,7 @@ const MAX_DIRECTIVES = 20;
|
|
|
176
181
|
function extractAndSaveDirectives(dbPath, project, prompt) {
|
|
177
182
|
try {
|
|
178
183
|
const db = new Database(dbPath);
|
|
184
|
+
db.pragma('journal_mode = WAL'); // 다중 hook 프로세스 동시성 보장
|
|
179
185
|
for (const { pattern, priority } of DIRECTIVE_PATTERNS) {
|
|
180
186
|
const match = prompt.match(pattern);
|
|
181
187
|
if (match && match[1]) {
|