본문 바로가기
C.W.K.
Stream
Lesson 04 of 05 · published

답글엔 하나하나 답해

~16 min · acknowledgement, contracts, validation, reporting

Level 0메모 쪽지
0 XP0/36 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete

아무도 다시 안 읽는 답글

흔하게 망가지는 장면을 떠올려봐. 아빠가 todo를 맡기고, 한 시간 뒤에 뭔가 생각나. 다른 API를 써, iPad는 일단 빼, 이름은 바꾸자. 답글을 달아. 작업에 푹 빠진 워커는 todo를 다시 열 이유가 없어. 그래서 처음에 읽은 본문 기준으로 done을 치고, 그 수정은 누가 결과를 읽을 때에야 드러나. Coop의 원칙 문서엔 이런 답글을 부르는 이름이 있어. 아무도 다시 안 읽는 답글.

커밋 4e78f1e(2026-10-05)가 맞물려 도는 규칙 셋으로 그 틈을 막았어. 활성 작업에 달린 답글은 정확히 그 워커한테 전달돼. 워커는 자기가 본 걸 정확히 확인해야 해. 그리고 done은 답글 하나하나에 답해야 해. 읽는 건 확인이 아니고, 확인은 답이 아냐.

전달: 답글 리뷰

활성 바인딩이 걸린 todo에 답글이나 본문 수정이 들어오면, 엔진이 그 바인딩의 세션 앞으로 reply_review 봉투를 큐에 넣어. 안엔 현재 todo가 들어 있어. 본문, 그리고 모든 답글. 최신순이고 제일 새 답글엔 표시가 붙어. 앞서 넣은 리뷰가 아직 안 나갔으면 취소하고 새 걸로 바꿔. 워커는 낡은 그림 여러 장 대신 새 그림 한 장을 받아.

프롬프트는 단도직입이야. 계속하기 전에 수정된 본문과 모든 답글을 읽어. 지금의 작업 계약은 본문이니까, 이름이나 범위가 바뀌면 본문에 들어가야 해. 다음 마지막 답에 COOP_READ와 이 봉투 id로 확인해. 이걸로 작업이 끝나는 건 아냐. done 전에 evidence.reply_outcomes에서 답글마다 답해. 다른 봉투랑 똑같은 마무리 규칙도 실려 있어서, 토큰은 자기만의 숨은 줄로 가. [coop-read]: <COOP_READ id>.

확인: 보여준 것만 정확히

Engine.acknowledge_replies는 따지는 게 딱 정해져 있어. 봉투가 이 세대 이 세션 앞으로 온 답글 리뷰여야 해. 네이티브 영수증은 네이티브 메시지 id가 있는 완료된 마지막 답이어야 하고. 텍스트에 COOP_READ와 이 봉투 id가 온전한 토큰으로 있어야 하고. 바인딩은 아직 활성이어야 해. 그다음에 그 봉투에 적힌 메시지 id들을 확인 처리해. 그 밖의 건 하나도 안 해.

핵심은 그 마지막 대목이야. 리뷰가 나가고 1분 뒤에 아빠가 단 답글은 그 리뷰의 확인에 묻어서 처리되지 않아. 그 답글은 자기 리뷰를 따로 만들고, 워커는 그것도 읽어야 해. 그리고 마지막 답 하나에 COOP_READ랑 보고가 같이 실리면, 어댑터는 리뷰 봉투부터 처리해. 그래야 확인이 기록에 먼저 올라가. 완료는 그 확인에 막혀 있으니까.

영수증은 이름이 아니라 세션 거야

확인은 todo_reply_reviews에 독자 키로 저장돼. 호출에 세션이 실려 있으면 독자는 session:에 그 세션 id를 붙인 거야. 바인딩 없는 호출만 actor:에 자기 신원을 붙인 걸로 물러나. 같은 actor 이름으로 쓰는 Claude Code 세션 둘은 서로 다른 독자라서, 한쪽이 다른 쪽의 읽음을 빌려 쓸 수 없어. 영수증이 말하는 건 바로 이 대화가 바로 이 메시지들을 봤다는 거야. 알 가치가 있는 건 그것뿐이고.

안 읽은 답글은 전이 둘을 막아. take는 묶음 범위 take까지 포함해서 거절되고, 거절 메시지가 안 읽은 id를 짚어줘. done도 똑같이 거절돼. 강제 done조차 못 지나가.

답: 답글마다 한 줄

todo에 done을 치려면 reply_outcomes가 있어야 하고, 도메인의 검증기는 봐주는 게 없어.

  • 항목마다 키가 딱 셋이야. message_id, disposition, reason.
  • disposition은 applied 아니면 not_applied야.
  • reason은 비어 있으면 안 되고 한 줄이어야 해.
  • 같은 메시지 id가 두 번 나오면 안 되고, id 집합이 그 todo의 메시지 전부와 같아야 해.

하나라도 어긋나면 done은 거절되고, 거절 메시지가 기대한 메시지 id를 전부 짚어줘. 통과하면 답은 네 영수증에 붙어 저장되고, 눈에 보이는 결과 요약 밑에 Reply outcomes:로 한 줄씩 붙어. 아빠는 자기 수정이 읽혔는지 궁금해할 필요가 없어. 기록에 반영했는지 안 했는지, 왜인지가 적혀 있으니까.

not_applied는 실패가 아니라 엄연한 답이야. 뒤 답글에 밀려났을 수도 있고, 아빠가 변경을 요구한 게 아니라 질문을 한 걸 수도 있어. 규칙이 금지하는 건 침묵이야.

계약은 본문이야

스레드가 명세가 되지 않게 막는 규칙이 하나 더 있어. 답글이 작업의 이름이나 범위를 바꾸면, 그걸 쓴 쪽이 todo 본문도 고쳐. 답글은 토론이고, 본문이 진실의 원천이야. 다음 주에 이 todo를 집는 워커가 뭘 만들어야 하는지 알자고 대화를 처음부터 다시 훑을 일은 없어야 해. 아빠는 소급 감사도 배제했어. 이 관문은 활성 작업에만 걸리고, 바뀌기 전에 끝난 todo는 다시 열리지도 감사받지도 않아.

전달, 확인, 답은 서로 다른 영수증이고, 고리를 닫는 건 마지막 하나뿐이야. 전달에서 멈추는 시스템은 메시지가 도착했다고 알려줘. 확인에서 멈추면 그게 읽혔다고 알려주고. 메시지마다 답이 있어야만, 그걸 쓴 사람한테 자기 말이 어떻게 됐는지 알려줄 수 있어.

Code

도메인에서 발췌, 일부 생략: 독자가 누구인지, 그리고 빠짐없는 답이 어떤 모양인지·python
def _reply_reader(actor=None):
    session = _context.metadata('session')
    return 'session:' + session if session else 'actor:' + get_identity(actor)

def _reply_outcomes(conn, todo_id, actor, outcomes):
    _require_reply_read(conn, todo_id, actor)
    messages = _todo_messages(conn, todo_id, actor)
    outcomes = [] if outcomes is None else outcomes
    validated = {}
    for entry in outcomes:
        if (not isinstance(entry, dict) or set(entry) != {'message_id', 'disposition', 'reason'}
            or type(entry['message_id']) is not int or entry['message_id'] in validated
            or entry['disposition'] not in ('applied', 'not_applied')
            or not isinstance(entry['reason'], str) or not entry['reason'].strip()
            or '\n' in entry['reason'] or '\r' in entry['reason']):
            raise LifecycleError('Each reply needs one applied/not_applied outcome and a nonempty one-line reason')
        validated[entry['message_id']] = entry
    if set(validated) != {m['id'] for m in messages}:
        raise LifecycleError(f'todo #{todo_id}: answer every reply in reply_outcomes before completing; message IDs: '
                             + ', '.join(str(m['id']) for m in messages))
    ...
한 워커의 마지막 답 둘, id는 지어낸 값: 지금은 확인, done에서 답·text
--- a final after a reply review arrives (the work continues) ---
아빠, 답글 봤어. iPad는 빼고 iPhone만 먼저 할게.

[coop-read]: <COOP_READ r-7f3a>

--- the done, turns later ---
아빠, iPhone 설정 화면 끝났어. 커밋 푸시했고 TestFlight 빌드 올렸어.
아빠가 할 일은 빌드 확인뿐이야.

[coop-report]: <{"coop_reports":[{"binding_id":"b-1","outcome":"done","body":"iPhone settings screen shipped; iPad deferred per reply.","for_dad":"아빠, iPhone 설정 화면 끝났어. 커밋 푸시했고 TestFlight 빌드 올렸어. 아빠가 할 일은 빌드 확인뿐이야.","evidence":{"commits":["abc1234"],"reply_outcomes":[{"message_id":41,"disposition":"applied","reason":"Dropped iPad from this pass as asked"},{"message_id":40,"disposition":"not_applied","reason":"Superseded by reply 41"}]}}]}>

External links

Exercise

네 트래커에서 배정된 뒤에 댓글이 쌓인 티켓을 하나 골라. 닫은 사람이 남겼어야 할 답글 처리 결과를 써봐. 댓글마다 한 줄, 반영했는지 안 했는지, 이유와 함께. 그다음 트래커가 그 줄들이 생길 때까지 닫기를 거절할 수 있었는지, 그리고 읽음 상태가 사람별인지 세션별인지 확인해.
Hint
트래커 대부분은 댓글이 달렸다는 것, 잘하면 열람됐다는 것까지는 기록해. 담당자가 그걸 두고 뭘 했는지 기록하는 곳은 드물어. 커밋을 뒤져야만 결과를 쓸 수 있다면, 그 뒤지는 수고가 바로 답글별 답이 다음 독자한테 아껴주는 일이야.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인 — 댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.