# task-609.1 완료 보고서: 집단지성 토론 수렴 메커니즘 구현

## SCQA

**S**: 멀티모델 봇(잼민이/코덱스/클로디) 3봇 집단지성 채팅이 라운드 로빈 토론 모드로 운영 중이다. `get_context(limit=10)`으로 최근 10개 메시지만 참조하며, `DiscussionManager`에 수렴 메커니즘이 없다.

**C**: 장시간 토론(~45메시지) 시 3라운드 전 내용이 소실되고, "새로운 관점을 제시해라" 지시만 있어 발산만 반복된다. Phase 없이 동일 패턴으로 끝까지 대화하여 구조적 합의 도출이 불가능하다.

**Q**: 컨텍스트 확장 + 롤링 서머리 + Phase 자동 전환(발산→수렴→합의) + 자동 합의문 생성으로 토론의 구조적 수렴을 달성할 수 있는가?

**A**: 3개 구현 항목을 모두 완료. (1) `get_context` limit 10→30 + `generate_rolling_summary()` 3라운드마다 자동 생성, (2) `DiscussionPhase` enum + `get_current_phase()` + Phase별 프롬프트 주입, (3) 토론 종료 시 `generate_consensus_report()` 자동 호출. pytest 248건 전체 통과, pyright 에러 0건 (프로젝트 디렉토리 내 실행 기준).

## 생성/수정 파일 목록

- `/home/jay/workspace/services/multimodel-bot/discussion_manager.py` (수정)
  - `DiscussionPhase(str, Enum)` 추가: DIVERGE/CONVERGE/CONSENSUS
  - `DiscussionState`에 `round_count`, `bot_response_count` 필드 추가
  - `get_current_phase(chat_id)` 메서드 추가 (시간 비율/라운드 기반 계산)
  - `on_bot_response()`에 라운드 카운트 로직 추가
  - `stop_discussion()`에 round_count/bot_response_count 초기화 추가

- `/home/jay/workspace/services/multimodel-bot/conversation_memory.py` (수정)
  - `get_context()` 기본 limit 10→30
  - `_rolling_summaries` 인메모리 dict 추가
  - `generate_rolling_summary(chat_id)` async 메서드 추가
  - `format_context()` 시그니처에 `phase` 파라미터 추가
  - `format_context()`에 롤링 서머리 섹션 + Phase별 프롬프트 로직 추가

- `/home/jay/workspace/services/multimodel-bot/main_bot.py` (수정)
  - `_generate_rolling_summary_safe()` 함수 추가
  - `generate_consensus_report()` 함수 추가
  - `_generate_consensus_report_safe()` 함수 추가 (그룹챗 전송 + 아누 DM)
  - `trigger_next_bot_response()` 수정: phase 전달, 롤링 서머리 트리거, 합의문 자동 생성

- `/home/jay/workspace/services/multimodel-bot/tests/test_discussion_manager.py` (수정)
  - `TestDiscussionPhase` 클래스 추가 (10개 테스트)
  - 기존 `test_idle_timeout_stops_discussion` 수정 (IDLE_TIMEOUT 값 불일치 수정)

- `/home/jay/workspace/services/multimodel-bot/tests/test_conversation_memory.py` (수정)
  - `TestGetContextDefaultLimit` 클래스 추가 (1개 테스트)
  - `TestRollingSummary` 클래스 추가 (3개 테스트)
  - `TestFormatContextWithPhase` 클래스 추가 (5개 테스트)

- `/home/jay/workspace/services/multimodel-bot/tests/test_thinking_mode.py` (수정)
  - `test_trigger_next_bot_response_builds_correct_prompt` 수정: `phase` 파라미터 추가에 따른 mock 기대값 갱신

## 테스트 결과

- pytest: **248/248 PASS** (전체 테스트 스위트, 신규 19개 포함)
- pyright: **0 errors, 0 warnings** (discussion_manager.py, conversation_memory.py)
- main_bot.py의 pyright reportMissingImports 10건은 기존 모듈 경로 이슈 (변경 전부터 존재)

## 발견 이슈 및 해결

### 자체 해결 (3건)
1. **`test_idle_timeout_stops_discussion` 기존 테스트 실패** — IDLE_TIMEOUT=900초인데 테스트가 360초(6분)를 사용하는 불일치를 `mgr.IDLE_TIMEOUT + 60`으로 수정
   - 상세: `tests/test_discussion_manager.py:154-163` 변경. 기존 하드코딩 값 대신 동적 참조.
2. **`test_trigger_next_bot_response_builds_correct_prompt` 테스트 실패** — `format_context()` 시그니처에 `phase` 파라미터 추가 후 mock 기대값 불일치. `mock_dm.get_current_phase.return_value.value = "diverge"` 설정 및 assert 갱신.
   - 상세: `tests/test_thinking_mode.py:358,379` 변경.
3. **`on_bot_response()` 내 `stop_discussion()` 호출 후 `round_count` 리셋 타이밍 이슈** — 토론 종료(next_next=None) 시 round_count가 이미 0으로 리셋되어 롤링 서머리 조건 체크 불가. `next_next` 존재 시에만 롤링 서머리 체크하도록 로직 분기.

### 범위 외 미해결 (1건)
1. **main_bot.py pyright reportMissingImports 10건** — 범위 외 사유: pyright가 프로젝트 외부에서 실행될 때 sys.path 기반 import를 해석 못하는 기존 이슈. pyrightconfig.json 수정이 필요하나 별도 작업 범위.

## 설계 결정

- **Phase별 프롬프트**: `format_context()`에 `phase` 파라미터로 전달. `DiscussionManager` 인스턴스를 `ConversationMemory`에 전달하는 대신 느슨한 결합 선택.
- **롤링 서머리 타이밍**: `on_bot_response()` → `stop_discussion()` 호출 시 round_count 리셋 문제 회피를 위해 `next_next`가 존재(토론 계속)할 때만 롤링 서머리 체크.
- **합의문 전송**: `bot_apps` dict에서 첫 번째 사용 가능한 봇으로 전송. 실패 시 다음 봇 시도.

## 비고

- 봇 재시작 필요 (변경사항 반영을 위해 multimodel-bot 프로세스 재시작 필요)
- 기존 `/정리` 커맨드와 롤링 서머리는 별개 기능 (롤링=토론 중 자동, /정리=수동)
- Phase 프롬프트는 기존 페르소나 프롬프트에 추가 (대체 아님)

## QC 자동 검증 결과

```json
{
  "task_id": "task-609.1",
  "overall": "WARN",
  "checks": {
    "file_check": "PASS",
    "data_integrity": "PASS",
    "test_runner": "PASS (248 passed)",
    "tdd_check": "PASS",
    "pyright_check": "WARN (reportMissingImports - 기존 프로젝트 경로 이슈)",
    "style_check": "PASS (black + isort OK)",
    "critical_gap": "PASS"
  }
}
```
