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

돌아가기 전에 후속 확인을 걸어 둬

~15 min · scheduling, probes, idempotency, notifications, agents

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

일보다 턴이 먼저 끝나

Pippa가 coop 도구로 todo를 맡기거나, 배포를 시작하거나, 재시작을 요청해. 일은 10분 뒤에, 혹은 한 시간 뒤에 끝나. 턴은 지금 끝나고. 그러면 나쁜 선택지 둘이 남아. 하나는 계속 붙어서 들여다보는 거야. 그러면 대화가 인질로 잡히고, 상태 필드 하나 지켜보느라 토큰을 태워. 다른 하나는 "나중에 확인할게"라고 말하는 건데, 그건 지켜 줄 누구도 없는 약속이야. 턴이 끝나면 모델한텐 시계도 없고, 약속한 기억도 없고, 스스로 깨어날 방법도 없거든.

cwkPippa의 답은 followup 가족 도구야. 위임한 턴이 돌아가기 전에, 오래 남고 읽기만 하는 확인을 하나 걸어 둬. 뭘 볼지, 뭘 끝난 걸로 칠지, 뭘 막힌 걸로 칠지, 얼마나 자주, 언제까지, 그리고 경우마다 아빠한테 할 한 줄까지. 거는 시점에 대해선 도구 설명부터 단호해. 턴을 끝내기 전에 걸고, 돌려받은 wake plan을 말하라고.

깨우기 계약

걸기 호출은 딱 이 필드만 받아.

  • key: 이 일의 안정적인 이름.
  • inspect: 어떤 증거를 읽을지 말로 적은 것.
  • probe: 기계가 읽는 확인. coop 아니면 http.
  • success_message와 blocked_message: 각각 한 줄, 아빠의 언어로. 줄바꿈이 들어가면 거절이야("Notifications must be one line").
  • interval_seconds(기본 30)와 timeout_seconds(기본 3,600): 5 이상의 정수고, 기한은 간격보다 짧으면 안 돼.

출처는 아빠 본인의 Pippa 대화에 저장된 턴이어야 해. coop 도구가 지키는 것과 같은 규칙이야. 돌려받는 건 wake_plan이 풀어 적힌 저장 기록이고. pending이면 조용히 다시 봐. busy, idle, 대기열 영수증은 완료가 아니니까. success면 성공 줄을 Master Chief에 한 번 올리고 바깥 알림은 없어. blocked면 아빠한테 바로 알리고 기한까지 계속 봐. 기한이 되면 확인을 멈추고 완료를 확인하지 못했다고 보고해. 일을 다시 하는 일은 절대 없고.

탐침 둘, 둘 다 읽기 전용

coop 탐침은 work_kind와 item_id로 작업을 지목하고, 지난 레슨의 그 제어 파사드를 통해 읽어. 쓰는 건 읽기 동작인 show뿐이야. 판정은 일부러 좁아.

  • 성공은 done이나 closed인 항목뿐이야.
  • 최신 보고가 blocked, abandoned, released면 막힘이야. 아직 아무도 작업을 가져가지 않았는데 배정이 실패하거나 취소되거나 만료돼도 막힘이고. 그렇다고 옛날에 만료된 초대 하나가 수락한 워커의 보고를 덮어쓸 순 없어.
  • 나머진 전부 대기야. busy, idle, 대기열 영수증, 심지어 저장된 disconnected 상태도. 그건 지금 턴보다 오래된 관찰일 수 있거든.

message_id를 붙이면 탐침은 그 대신 답글 하나를 지켜봐. 5트랙에서 본 applied나 not_applied 답이 그 답글에 오길 기다리는 거야. 답글 전달이 실패했거나 불확실하거나, 워커가 연결돼 있지 않으면 막힘이야.

http 탐침은 자격 정보 없는 JSON 상태 URL을 GET해. URL에 사용자나 비밀번호 없고, 인증 헤더 없고, 리다이렉트 안 따라가고, 환경의 프록시 설정도 안 써. success 조건은 필수야. 점으로 이은 키를 정확한 스칼라 값에 대응시키는데, 타입까지 맞아야 해. blocked 조건은 있어도 되고 없어도 되고. 401이나 403은 막힘이야. 기다린다고 권한 문제가 풀리진 않으니까. 다른 실패는 잠깐 지나가는 대기로 쳐. 저장하는 증거는 조건이 지목한 필드뿐이고, 응답 전체는 절대 안 남겨. 도구는 호출하는 쪽한테 기대하는 빌드나 버전을 못 박으라고 해. 옛날 프로세스가 멀쩡하게 200을 돌려주는 건 끝난 배포가 아니거든.

같은 키, 같은 감시

후속 확인의 id는 대화 id와 키를 묶은 SHA-256이야. 같은 키, 같은 계약으로 다시 걸면 있던 감시를 그대로 돌려받아. 같은 키에 다른 계약을 걸면 거절이야. 이 키는 이미 다른 깨우기 계약을 가리키고 있으니 먼저 살펴보고 새 키를 고르라고. 이게 막는 경우는 평범하고 흔해. 도구 결과가 사라지고, 아빠가 다음 턴에 다시 묻고, Pippa가 또 걸어. 일 하나에 감시 하나.

결과마다 하는 일

  • 대기는 조용해. 확인할 때마다 관찰 기록엔 남지만 아무한테도 안 알려.
  • 성공이면 알림은 필요 없음으로 표시하고, 레슨 2의 그 영속 master_chief.report로 success_message를 Master Chief에 한 번 올려. 키로는 그 후속 확인을 써. 텔레그램은 없어. 아빠는 다음에 Pippa랑 얘기할 때 읽게 돼.
  • 기한 전에 막힘이 오면 첫 막힘을 영수증과 함께 한 번만 저장해. 그리고 아빠한테 텔레그램으로 알린 다음 Master Chief에 올려. 감시는 계속 대기 상태로 남아. 나중에 성공하면 그건 그대로 따로 보고돼.
  • 성공 없이 기한이 오면 deadline_without_completion_evidence라는 이유를 단 막힘이 돼. 마지막 탐침 자체는 막힘을 못 봤다면(잠깐의 장애 같은 거), 가장 최근의 진짜 막힘을 시각과 함께 지우지 않고 남겨. 성공으로 치는 일은 절대 없고, 원래 일을 다시 시도하지도 않아. 이때 아빠가 읽는 줄도 진짜 막힘 때와 같은 blocked_message야. _blocked_text엔 기한 전용 문구가 따로 없거든. 마지막 탐침에 for_dad가 없을 때 직전 진짜 막힘의 for_dad를 빌려 올 뿐이야. 그러니 blocked_message는 두 경우 모두에 맞게 써("실패했거나 기한 안에 확인이 안 됐어"). 일이 실패했다고 단정하는 문장으로 쓰면 안 돼.

막힘에서 아빠가 읽는 건 걸 때 적어 둔 blocked_message와 워커 본인의 for_dad 줄, 그게 다야. 이것도 고친 결과야. 2026-10-10까지는 알림 뒤에 워커의 보고 본문 전체, 그러니까 영어 이력 기록이랑 이유 코드까지 붙었어. 그래서 막힘 하나(todo #373)가 텔레그램과 Master Chief에 영어 벽으로 도착했지. 커밋 7a404b4e(todo #375)가 그걸 머리 줄과 for_dad로 줄였고, 영어 이유는 status 동작이 볼 수 있게 관찰 기록에 남겨 뒀어. 같은 커밋이 성공 때 붙던 영어 "recovered" 꼬리도 뗐어. 아빠가 읽는 건 success_message야. (cwkPippa의 follow-up 문서엔 아직 예전처럼 이유를 덧붙인다고 적혀 있어. 맞는 건 코드 쪽이야.)

먼저 예약하고, 그다음 보내

레슨 2의 notify 문은 텔레그램을 보낸 뒤에 보냈다고 표시해. 드문 중복은 감수하는 거지. 후속 확인의 배달은 반대야. 텔레그램을 부르기 전에 alert_state='sending'을 저장하고, 끝나면 sent로 바꿔(채널이 거절하면 다시 pending으로 돌려서 재시도하고). 두 저장 사이에 프로세스가 죽으면, 다음 실행은 sending을 보고 다시 보내는 대신 uncertain을 기록해. status 동작은 그 불확실함을 작업 결과와 따로 보여주고. 이렇게 해도 되는 이유는 바로 다음 단계에 있어. Master Chief 보고는 그래도 배달되니까, 알림이 불확실해도 그 줄은 아빠 대화에 남아. 급한 텔레그램을 먼저 보내는 것도 바로 그래서야. 보고 호출에 2초 제한이 걸려 있긴 해도, 바쁜 Chief가 그 알림까지 늦추게 둘 순 없으니까.

새 cron은 없어

이 중 어느 것도 스케줄러를 새로 만들지 않아. 시작할 때 cwkPippa는 HeartbeatService가 이미 쓰는 APScheduler에 5초짜리 interval 작업 followup-checks를 하나 등록해. 그 스케줄러는 프로세스 간 스케줄러 임대를 쥔 프로세스에서만 돌고. 틱마다 때가 된 확인을 나열하고, 행마다 2분짜리 임대를 잡고, 탐침은 별도 태스크로 띄워. 그래서 틱 자체는 가벼워. 기록은 대화 데이터베이스 옆에 있는 자기 SQLite 파일에 들어 있어서, 재시작해도 살아남아. cron 스냅숏 복원이 스케줄러를 비우면 리스너가 그 작업을 다시 넣어. launchd 작업도 없고, 확인마다 모델을 부르지도 않아. 그리고 cancel은 확인만 거둬들이지, 지켜보던 일은 절대 안 건드려.

걸어 뒀다고 일이 끝났다는 주장이 되는 건 아냐. 이 도구가 돌려주는 건 결과가 아니라 계약이야. Pippa가 "끝나면 알려줄게"로 턴을 마칠 때, 그 문장을 참으로 만드는 게 걸어 둔 기록이야. 그걸 소식으로 바꿀 수 있는 건 done 항목이나 못 박은 빌드를 직접 본 탐침뿐이고.

Code

cwkPippa services/followups.py의 wake plan과 arm() (발췌)·python
def wake_plan(spec):
    return {"inspect": spec["inspect"], "probe": spec["probe"],
            "pending": "Recheck quietly; busy/idle or a queued receipt is not completion.",
            "succeeded": "Post success_message once to Chief for Dad's next turn; no external alert.",
            "blocked": "Alert Dad immediately with blocked_message and the worker's for_dad lines, record it in Chief, and keep checking until the deadline.",
            "deadline": "Stop checking and report that completion could not be verified; never repeat the work."}


def arm(spec: dict, source: dict) -> dict:
    ...
    if any("\n" in spec[key] for key in ("success_message", "blocked_message")):
        raise ValueError("Notifications must be one line")
    followup_probes.validate(spec.get("probe"))
    spec = {"interval_seconds": 30, "timeout_seconds": 3600, **spec}
    ...
    # Stable within the originating conversation even if a tool result was lost
    # and Dad asks again on the next turn. A new operation needs a new key.
    wid = hashlib.sha256(_json([source["conversation_id"], spec["key"]]).encode()).hexdigest()
    now = time.time()
    with _tx() as db:
        db.execute("BEGIN IMMEDIATE")
        old = db.execute("SELECT spec FROM followups WHERE id=?", (wid,)).fetchone()
        if old and old["spec"] != _json(spec):
            raise ValueError("This key already names a different wake contract; inspect it before choosing a new key")
        if not old:
            db.execute("INSERT INTO followups(id,conversation_id,spec,source,state,created,deadline,due) VALUES (?,?,?,?,?,?,?,?)",
                       (wid, source["conversation_id"], _json(spec), _json(source), "pending", now, now + spec["timeout_seconds"], now))
    return get(wid, source["conversation_id"])
cwkPippa services/followups.py의 _deliver() (발췌)·python
if blocked and delivery["alert_state"] == "pending":
    await save(alert_state="sending")
    delivered = await notify(text)
    await save(alert_state="sent" if delivered else "pending")
if blocked and delivery["alert_state"] == "pending":
    raise RuntimeError("Blocker notification channel is unavailable")
if blocked and delivery["alert_state"] == "sending":
    # A process died after reserving the send: no blind duplicate alert.
    await save(alert_state="uncertain")
# Urgent alerts precede the possibly busy Chief conversation lock.
async with asyncio.timeout(2):
    mid = await report(text, source_id, row["conversation_id"])
스케치: 걸기 호출 두 개의 인자 (여기선 메시지가 영어지만, Pippa는 아빠의 언어로 써)·json
{"action": "arm", "key": "todo-123-delegated",
 "inspect": "Coop todo 123: wait for the worker's done or blocked report.",
 "probe": {"kind": "coop", "work_kind": "todo", "item_id": 123},
 "success_message": "Todo 123 is done.",
 "blocked_message": "Todo 123 is blocked or not confirmed done in time; it needs you.",
 "interval_seconds": 60, "timeout_seconds": 5400}

{"action": "arm", "key": "site-deploy-2.4.1",
 "inspect": "The status page must report build 2.4.1 live; an old healthy build is not success.",
 "probe": {"kind": "http", "url": "https://status.example.com/deploy.json",
           "success": {"build.version": "2.4.1", "state": "live"},
           "blocked": {"state": "failed"}},
 "success_message": "Build 2.4.1 is live.",
 "blocked_message": "The 2.4.1 deploy failed or was not confirmed live in time.",
 "interval_seconds": 30, "timeout_seconds": 1800}

External links

Exercise

손으로 시작하는 오래 걸리는 일을 하나 골라봐. 배포, CI 파이프라인, 데이터 내보내기, 모델 학습. 그 일의 깨우기 계약을 JSON으로 써. 안정적인 키, 읽을 증거, 정확한 필드와 값을 지목하는 성공 조건(기대하는 버전이나 작업 id 포함), 넣어도 되고 빼도 되는 막힘 조건, 간격, 기한, 그리고 성공과 막힘 때 할 말 한 줄씩. 그다음, 두 조건 다 참이 아닌 채로 기한이 지나면 시스템이 뭘 해야 하는지 글로 답해봐. 그리고 그 답에 일을 다시 돌리는 게 절대 안 들어 있는지 확인해.
Hint
상태 엔드포인트가 healthy: true만 말한다면 배포를 증명할 수 없어. 빌드 id부터 거기 넣어. 그리고 어느 결과가 시끄러워야 하는지 정해. Pippa 설계에선 막힘만 아빠를 방해하고, 성공은 다음 대화까지 조용히 기다려.

Progress

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

댓글 0

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

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