실행이 멈추는 양상은 두 가지입니다. 같은 도구를 같은 인자로 무한히 도는 경우와, 이벤트 발화가 끊긴 채 매달려 있는 경우입니다. 둘 다 디스크에 남는 두 개의 append-only JSONL이 증거를 줍니다. 턴 단위 대화는 transcript, 파이프라인 이벤트는 runlog입니다. 멈춘 실행 디버깅은 이 둘을 찾아 마지막 이벤트를 읽고, 원인을 분류해 복구하는 순서로 진행합니다.
1. transcript와 runlog를 찾습니다
세션 transcript는 SessionTranscript(core/observability/transcript.py)가 ~/.geode/transcripts/<project-slug>/<session_id>.jsonl 또는 run_dir에 묶인 경우 dialogue.jsonl에 기록합니다. user/assistant 메시지, 도구 호출과 결과, 비용, 오류, 라이프사이클 이벤트가 한 줄씩 들어갑니다. runlog는 RunLog(core/orchestration/run_log.py)가 ~/.geode/runs/<session_key>.jsonl에 기록하며, 각 줄은 event, node, status, duration_ms를 가진 RunLogEntry입니다.
ls -t ~/.geode/transcripts/*/ # most recent session jsonl ls -t ~/.geode/runs/ # most recent run_key jsonl
2. 마지막 이벤트를 읽습니다
어디서 멈췄는지는 마지막 줄들이 알려줍니다. transcript는 read_events(limit=N)로 최근 이벤트를 읽고, runlog는 read(limit=N, ...)로 newest-first로 읽으며 event_filter / node_filter / status_filter로 좁힐 수 있습니다.
uv run python -c "
from core.observability.transcript import SessionTranscript
tx = SessionTranscript('<session_id>')
for e in tx.read_events(limit=10):
print(e.get('event'), e.get('tool', ''), e.get('status', ''))
"마지막이 tool_call인데 짝이 되는 tool_result가 없으면 도구 실행에서 매달린 것이고, 같은 tool_call이 같은 인자로 반복되면 진전 없는 루프입니다.
3. 원인을 분류합니다
두 가지 정지 양상을 구분합니다.
- 매달림(이벤트 끊김).
SessionTranscript.is_stale(threshold_s)는 파일 mtime을 보고 일정 시간 이벤트가 안 붙었는지 알려줍니다.last_touched_at()가 한참 전이면PIPELINE_TIMEOUT/PIPELINE_ERROR를 발화하지 못한 채 멈춘 것입니다. - 노드 stuck.
StuckDetector(core/orchestration/stuck_detection.py)가NODE_ENTERED/NODE_EXITED/NODE_ERROR훅으로 실행 중인 노드를 추적합니다.get_running()은 현재 실행 중인 세션 키와 경과 시간을 돌려주고, 타임아웃(timeout_s, 기본 2시간)을 넘으면check_stuck()이 그 작업을 자동 release하고PIPELINE_ERROR를 발화합니다.
uv run python -c "
from core.orchestration.stuck_detection import StuckDetector
d = StuckDetector()
print(d.get_running()) # {session_key: elapsed_seconds}
print(d.check_stuck()) # released keys past timeout
"4. 복구합니다
루프가 진전 없이 도는 경우는 ConvergenceDetector가 동일 도구·동일 인자 반복을 감지해 끊습니다. 매달림이 확인되면 StuckDetector.check_stuck()이 작업을 release하고 PIPELINE_ERROR를 발화해 세션을 정리합니다. transcript와 runlog가 가리키는 마지막 도구·노드를 보고 근본 원인(외부 호출 타임아웃, 자격증명 오류, 입력 오류)을 고친 뒤 다시 실행합니다. transcript와 runlog는 30일 자동 정리되므로 진단 근거가 사라지기 전에 보존하세요.
확인
복구 후 새 실행의 transcript가 session_end까지 도달하는지, runlog의 마지막 이벤트가 pipeline_end(status: ok)인지 확인합니다.
tail -n 3 ~/.geode/runs/<session_key>.jsonl # last events, expect pipeline_end ok