Skip to content

orchestrate: escalation records have no lifecycle — a worker that self-resolves leaves stale records that wake every later watch with exit 5 #128

Description

@choiyounggi

증상 (stickygap1 런, 2026-08-21, dev-loop 1.8.3 실측)

한 런에서 worktree_escape 에스컬레이션이 읽기 전용 명령으로 3회 발생했다 (메인 워크트리의 .env*ls/grep으로 조회 — dev 서버용 카카오 키 확인 목적). 1회차는 코디네이터가 승인을 전달해 정상 해소. 그런데 2·3회차(14:35)는 워커가 답을 기다리지 않고 다른 경로로 우회해 그대로 impl_done까지 진행했고, 레코드는 escalations/에 남았다. 그 결과:

  • 다음 watch-status.sh ... done 1이 시작하자마자 exit 5 — 그러나 답을 전달할 대상이 없다 (워커는 이미 지나감).
  • 코디네이터가 레코드 ts와 status updatedAt을 수동 대조해 "스테일"을 판별하고 rm 해야 했다. exit 5는 레코드가 있는 한 재발하도록 설계돼 있으므로(의도된 동작), 스테일 레코드는 사실상 watch를 영구히 깨우는 잔재가 된다.

원인

  • 에스컬레이션 레코드에 생성만 있고 소멸 경로가 없다: 정상 플로우(코디네이터 approve/deny → rm)만 정의돼 있고, "워커가 거부를 받아들이고 우회해 진행한" 경우를 아무도 정리하지 않는다.
  • watch-status.sh의 exit 5 메시지(task:rule 나열)에는 레코드 시각·해당 태스크의 현재 phase가 없어, 코디네이터가 매번 파일 2개를 열어 대조해야 스테일 여부를 안다.
  • 같은 명령 계열이 반복 에스컬레이션돼도 dedupe가 없어 왕복이 배가된다.

제안

  1. triage 정보를 exit 5 출력에 포함 (가장 저비용): [watch] escalation pending — <task>:<rule> (recorded <ts>; task phase=<phase> @<updatedAt>). 레코드 ts보다 phase 전이가 나중이면 코디네이터가 즉시 "워커가 이미 지나감 → 답장 없이 rm"을 판단할 수 있다. 스킬 문서의 escalation playbook에도 이 분기(대기 중인 워커에게만 답장 전달, 지나간 워커는 레코드만 삭제)를 명시.
  2. 워커 측 정리 규약: 워커 프로토콜에 "거부된 명령을 우회하기로 했으면 자신의 escalation 레코드를 삭제(또는 resolved: worker-bypassed 마킹)하고 진행"을 추가. 레코드의 의미가 '답변 대기 중'으로 유지된다.
  3. worktree_escape의 읽기 전용 구분 또는 dedupe: 메인 워크트리 대상이라도 쓰기 동사/리다이렉션이 없는 명령(ls, grep, cat)은 escalate 대신 allow(혹은 로그만)하거나, 최소한 동일 cmd 해시의 재-에스컬레이션을 런 내에서 접기. 이번 런의 3건은 전부 동일한 read-only 패턴이었다.

1은 즉시 적용 가능하고 위험이 없다. 2가 근본 해결, 3은 발생 자체를 줄인다. (자동 스테일 판정 후 무시하는 방식은 비추 — 워커 턴 중간의 정당한 대기와 구분이 안 됨. 판단은 코디네이터에 남기되 판단 재료를 주는 방향.)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions