# Task: conversation_memory.py 에이전트 미팅 합의 반영

## 개요
에이전트 미팅(2026-03-15) 합의사항을 conversation_memory.py에 반영한다.
미팅 기록: `/home/jay/workspace/memory/meetings/2026-03-15-groupchat-memory-intelligence.md`

## 대상 파일
- `/home/jay/workspace/services/multimodel-bot/conversation_memory.py` (주 수정 대상)
- `/home/jay/workspace/services/multimodel-bot/main_bot.py` (명령어 추가)
- `/home/jay/workspace/services/multimodel-bot/tests/test_conversation_memory.py` (테스트 추가)

## Phase 1: P0 즉시 수정 3건

### 1-1. _summary_counter 재시작 시 파일 덮어쓰기 버그
**현재 코드** (conversation_memory.py:349):
```python
num = self._summary_counter.get(chat_id, 0) + 1
self._summary_counter[chat_id] = num
```
봇 재시작 시 `_summary_counter`가 0으로 초기화 → 기존 `2026-03-15_001.json`이 덮어씌워짐.

**수정**: `_generate_summary()` 시작 부분에서 기존 파일 수를 확인:
```python
if chat_id not in self._summary_counter:
    existing = list(summaries_dir.glob(f"{today}_*.json"))
    self._summary_counter[chat_id] = len(existing)
num = self._summary_counter.get(chat_id, 0) + 1
```

### 1-2. 파일 퍼미션 700
`_append_to_jsonl()`과 `os.makedirs()` 호출 후 퍼미션 설정:
```python
os.makedirs(jsonl_path.parent, exist_ok=True)
os.chmod(jsonl_path.parent, 0o700)
```
파일 생성 시에도:
```python
os.chmod(jsonl_path, 0o600)
```
`_summaries_dir`, `_insights_dir` 생성 시에도 동일 적용.

### 1-3. PII 기본 필터
`_append_to_jsonl()` 호출 전에 PII 마스킹 적용. 별도 함수로 분리:
```python
import re

_PII_PATTERNS = [
    (re.compile(r'\d{6}-[1-4]\d{6}'), '[주민번호]'),           # 주민등록번호
    (re.compile(r'01[016789]-?\d{3,4}-?\d{4}'), '[전화번호]'),  # 휴대폰
    (re.compile(r'\d{3,4}-\d{2,4}-\d{4,6}'), '[계좌번호]'),     # 계좌번호
]

def _mask_pii(text: str) -> str:
    for pattern, replacement in _PII_PATTERNS:
        text = pattern.sub(replacement, text)
    return text
```
**주의**: 인메모리 deque에는 원본 저장, JSONL 파일에만 마스킹 적용.

## Phase 2: 미팅 합의 반영

### 2-1. JSONL 레코드에 topic_tag 필드 추가
`_append_to_jsonl()` 수정 — record에 `topic_tag` 필드 추가:
```python
record = {
    "sender": msg.sender,
    "text": msg.text,  # PII 마스킹된 텍스트
    "timestamp": msg.timestamp.isoformat(),
    "is_bot": msg.is_bot,
    "chat_id": chat_id,
    "topic_tag": self._current_topic.get(chat_id, "general"),
}
```
`_current_topic: dict[int, str] = {}` 딕셔너리 추가.
초기값: "general".

### 2-2. 규칙 기반 주제 전환 감지 (TopicDetector)
conversation_memory.py 내 내부 클래스 또는 함수로 구현:
```python
_TOPIC_CHANGE_KEYWORDS = ["다른 주제", "그건 그렇고", "화제를 바꿔", "다음 안건", "본론으로"]
_SILENCE_GAP_SECONDS = 300  # 5분

def _detect_topic_change(self, chat_id: int, msg: ChatMessage) -> bool:
    """침묵 갭 5분 초과 또는 전환 키워드 감지 시 True"""
    last = self._last_activity.get(chat_id)
    if last and (msg.timestamp - last).total_seconds() > _SILENCE_GAP_SECONDS:
        return True
    for kw in _TOPIC_CHANGE_KEYWORDS:
        if kw in msg.text:
            return True
    return False
```
주제 변경 감지 시 `_current_topic[chat_id]` 를 "pending"으로 변경.
다음 요약 생성 시 LLM이 실제 주제 태그를 확정.

### 2-3. 요약 메타데이터 스키마 확장
`_generate_summary()` 수정 — 프롬프트 개선 + 메타데이터 추가:

프롬프트 변경:
```python
prompt = (
    "아래 대화를 분석해줘. 반드시 JSON 형식으로만 응답해.\n"
    "<user_content>\n"
    f"{conversation_text}\n"
    "</user_content>\n\n"
    '{"summary": "3~5줄 요약", "key_topics": ["주제1", "주제2"], '
    '"topic_tag": "주요 주제 한 단어(영문 snake_case)", '
    '"key_decisions": ["결정사항1"], '
    '"action_items": ["액션1"], '
    '"consensus_level": "exploratory|tentative|agreed|decided"}'
)
```
**핵심**: `<user_content>` XML 태그로 사용자 대화 내용을 시스템 프롬프트와 분리 (인젝션 방지).

요약 파일명 규칙 변경:
```python
# 기존: f"{today}_{num:03d}.json"
# 변경: f"{today}_{topic_slug}_{num:03d}.json"
topic_slug = re.sub(r'[^a-zA-Z0-9가-힣_-]', '', parsed.get("topic_tag", "general"))[:20]
file_path = summaries_dir / f"{today}_{topic_slug}_{num:03d}.json"
```

요약 데이터 구조:
```python
summary_data = {
    "timestamp": datetime.now().isoformat(),
    "date": today,
    "message_range": {"from": msg_from, "to": msg_to},
    "summary": summary_text,
    "key_topics": key_topics,
    "topic_tag": topic_slug,
    "key_decisions": parsed.get("key_decisions", []),
    "action_items": parsed.get("action_items", []),
    "consensus_level": parsed.get("consensus_level", "exploratory"),
    "participants": participants,
}
```

### 2-4. generate_insight 프롬프트도 XML 태그 분리
```python
prompt = (
    "아래 대화를 정리해줘.\n"
    "핵심 논점, 결론, 액션 아이템을 마크다운 형식으로 요약해줘.\n\n"
    "<user_content>\n"
    f"{conversation_text}\n"
    "</user_content>"
)
```

## Phase 3: Telegram 검색 명령어

### 3-1. /메모리 명령어
`main_bot.py`에 CommandHandler 추가. 클로디 봇에 등록.
summaries/ 디렉토리에서 최근 5개 요약을 불러와 InlineKeyboard로 표시:
```
📚 대화 메모리
최근 요약 5건:
1. [03/15] insuwiki — InsuWiki 아키텍처 논의
2. [03/15] firebase — Firebase Auth 방식 결정
...
```

### 3-2. /기억 <키워드> 명령어
summaries/ 파일들의 key_topics와 summary를 검색하여 매칭 결과 반환:
```python
async def cmd_search_memory(update, context):
    keyword = ' '.join(context.args) if context.args else ''
    if not keyword:
        await update.message.reply_text("사용법: /기억 <키워드>")
        return
    # summaries/ glob → key_topics/summary에서 키워드 검색
    ...
```

### 3-3. /목차 명령어
날짜별 요약 파일 목록을 목차 형태로 표시:
```
📋 대화 목차
2026-03-15:
  - general_001: AI집단지성 메모리 설계 (agreed)
  - insuwiki_002: InsuWiki 인증 방식 (tentative)
2026-03-14:
  - ...
```

## 테스트 요구사항
- 기존 64개 테스트 회귀 없음
- 신규 테스트 추가:
  - _summary_counter 복구 테스트 (기존 파일 존재 시)
  - PII 마스킹 테스트 (전화번호, 주민번호, 계좌번호)
  - topic_tag 필드 포함 확인
  - 주제 전환 감지 (침묵 갭, 키워드)
  - 요약 메타데이터 스키마 확인 (key_decisions, consensus_level)
  - XML 태그 분리 확인
- pytest 전체 통과 + pyright 0 에러

## 절대 규칙
- `main_bot.py`의 `bot_settings.json` 모델 설정 변경 금지
- 기존 동작 하위호환 유지 (요약 JSON 파싱 실패 시 fallback)
- 인메모리 deque에는 원본 저장, JSONL에만 PII 마스킹
