diff --git a/.agents/references/known-issues.md b/.agents/references/known-issues.md index bfb0a8e..d55d916 100644 --- a/.agents/references/known-issues.md +++ b/.agents/references/known-issues.md @@ -219,3 +219,8 @@ - **원인**: AI 응답이나 코딩 작업이 5초(폴링 주기) 미만으로 매우 빠르게 끝나면, 확장이 `IDLE -> IDLE` 상태만 관찰하며 `wasRunning` 플래그가 `false`로 유지됨. 기존 `[RESPONSE-CAPTURE]` 조건식(`wasRunning && !isRunning && currentCount > ...`)이 `wasRunning=false`로 인해 블록되어 캡처 자체를 완전히 건너뛰게 됨. - **해결**: `wasRunning` 검증을 삭제하고 `!isRunning && currentCount > lastResponseCaptureStep` 조건으로 완화하여 누락된 step이 있을 때 무조건 캡처하도록 변경. 추가로 오래된 `[RESPONSE-CAPTURE]` 내 하드코딩 파서를 `extractPlannerText`로 일원화 적용. - **주의**: 폴링 방식에서는 상태(RUNNING->IDLE) 전이를 확신할 수 없으므로, Step Count(인덱스 전진)라는 100% 신뢰 가능한 마커를 통해 새 응답 여부를 감지해야 함. +### [2026-04-10] [Bot] chat_snapshot_scanner 무한 Abort 및 파일 적체 (Exception 누락) +- **증상**: 봇이 디스코드로 AI 답변(채팅 스냅샷)을 전혀 전송하지 못하고 렉이 걸림. ridge/chat_snapshots/에 처리되지 않은 JSON 파일이 수십 개 적체됨. +- **원인**: ot.py의 chat_snapshot_scanner에서 파일을 순회 파싱할 때 내부의 .unlink() 과정에서 발생하는 예외나 discord.Embed 생성 예외 등을 루프 안에서 잡아주지 못함. 첫 에러 파일(poison pill)을 만나는 순간 루프 전체가 폭파되어 뒤쪽의 정상 파일들도 영원히 처리되지 않고 다음 폴 스케줄에서 다시 첫 파일에 막힘. +- **해결**: 루프 내부에 except Exception을 추가하여 전역 예외를 잡아 방어. 실패한 파일은 glob에서 반복 시도되지 않게 .json.failed로 우회(rename)시켜 큐를 비워줌. +- **주의**: 폴링/스캐너 or 루프 내부에서는 개별 아이템 파싱 단계에서 발생 가능한 모든 예외 상태에 대한 Defensive Catch 및 Continue(우회) 로직이 필수임. diff --git a/bot.py b/bot.py index e25b3c5..fff7cf7 100644 --- a/bot.py +++ b/bot.py @@ -1381,8 +1381,12 @@ class GravityBot(commands.Bot): logger.error(f"[SNAPSHOT] Discord send failed for {project}: {e}") f.unlink() # Cleanup - except (json.JSONDecodeError, OSError) as e: + except Exception as e: logger.warning(f"Bad chat snapshot {f.name}: {e}") + try: + f.rename(f.with_suffix('.json.failed')) + except OSError: + pass except Exception as e: logger.error(f"Error scanning chat snapshots: {e}") diff --git a/docs/devlog/2026-04-10.md b/docs/devlog/2026-04-10.md index 9f8de9f..b39e4ff 100644 --- a/docs/devlog/2026-04-10.md +++ b/docs/devlog/2026-04-10.md @@ -7,3 +7,4 @@ | 001 | 15:53 | Gravity Bridge AI 응답 텍스트가 누락되는 버그 픽스 (extractPlannerText 적용 및 Nested 조회 추가) | TBD | ✅ | | 002 | 16:05 | Gravity Bridge 빠른 응답 누락 오류 해결 (IDLE-to-IDLE 패스 로직 완화) | TBD | ✅ || 003 | 16:12 | [Bridge] DOM Observer Ž (PATS Ȱȭ) ڵ ˸ | TBD | ? | +| 004 | 16:31 | [Bot] chat_snapshot_scanner 미처리 예외 큐 막힘 현상 해결 및 Hermes Gateway 재시작 | TBD | ✅ | diff --git a/extension/src/step-utils.ts b/extension/src/step-utils.ts index b9a97f7..1e3a07f 100644 --- a/extension/src/step-utils.ts +++ b/extension/src/step-utils.ts @@ -46,17 +46,25 @@ export function extractPlannerText(step: any): string | null { } } - // Try other step fields - for (const key of Object.keys(step)) { - if (SKIP_FIELDS.has(key) || key === 'plannerResponse') continue; - const val = step[key]; - if (typeof val === 'string' && val.length > 50) { - const filtered = filterEphemeral(val); - if (filtered) { - return filtered; - } - } + // Fallback: nested fields not caught by top-level string iteration + if (step.content?.parts) { + let txt = ''; + for (const p of step.content.parts) { if (p?.text) txt += p.text; } + if (txt.length > 10) return filterEphemeral(txt); } + if (step.parts) { + let txt = ''; + for (const p of step.parts) { if (p?.text) txt += p.text; } + if (txt.length > 10) return filterEphemeral(txt); + } + if (step.metadata?.text && step.metadata.text.length > 10) { + return filterEphemeral(step.metadata.text); + } + if (step.rawOutput) { + const txt = typeof step.rawOutput === 'string' ? step.rawOutput : JSON.stringify(step.rawOutput); + if (txt.length > 10) return filterEphemeral(txt); + } + return null; }