일어나면 안 되는 resume
네 artifact class 는 다 read-out 아니면 render-out — 어느 것도 바깥 세상을 안 바꿔. 진짜 지뢰는 side-effect step 이야: commit, email 보내기, 파일 쓰기, 결제, DB INSERT. 의도를 persist하면 들어오는 데이터는 잃지 않아. 하지만 이미 밖으로 나간 행동까지 되돌리진 못해.
| step 종류 | cut 났을 때 |
|---|---|
| read-only (fetch / search / generate-request) | 그냥 다시 해. 최악의 결과도 중복이야 |
| write / side-effect (commit, email, 결제, INSERT) | 다시 하면 두 번 일어나 |
uncertain state 가 결정해
tool_call_started 는 log 됐는데 tool_result_persisted 가 없으면, 그 step 은 '안 돌았음' 이 아니라 '돌았는지 모름' 이야. read-only step 이면 괜찮아. attempt cap 까지는 다시 돌려. 2026-09-25 전까지 side-effect step 은 '불확실하면 멈춘다' 보다 더 엄격한 규칙을 받았어. loop 는 그런 step 을 절대 혼자 돌리지 않았고, next 나 resume 에서 끝나지 않은 side-effect step 은 첫 실행이든 불확실한 재진입이든 STOP 하고 아빠/피파한테 물었어. "이거 해도 돼?" 또는 "이거 일어났어?" 그 뒤로는 Council 이 확정한 규칙대로 불확실한 상태에서만 멈춰(이유는 아래 v10 메모에 있어). plan 시점에 정해진 step 의 side_effect flag 가 여전히 스위치의 전부야.
R8 lock, 그대로: "얼마나 영리하게 resume 하느냐가 아니라 — 언제 resume 을 멈추느냐야. Read-only: 자유롭게. Side-effect: STOP. Concurrency: atomic lease 하나."
side_effect: bool 을 들고 다녀서 loop 가 싸구려 retry 와 이중 전송을 구분할 수 있어.
unblock 동작이 durable confirmation 경로야. guard 를 전역으로 끄는 게 아니라 — 바로 그 막힌 step 의 다음 resume 을 딱 한 번 authorize 해. step 이 실제로 시작하면 confirmation 이 소모돼. 또 cut 나면, side effect 가 replay 안전하다고 가정하는 대신 guard 가 다시 막아.
상시 허가가 딱 하나 있어. 예약 작업의 턴 자체가 아빠가 미리 적어 둔 지시거든. 대화의 마지막 메시지가 [SCHEDULED TASK] 턴이고 task 가 그 실행 중에 만들어졌다면, 그 task 의 side-effect step 은 진행해도 돼. loop 는 side_effect_auto_confirmed event 를 남겨서 그 허가를 기록에 올려. 예전 실행에서 남은 task 는 이 통행증을 못 받아. 2026-09-26부터는 통행증이 더 좁아졌어. 그 step 의 호출이 한 번도 나가지 않았다고 JSONL 이 보여줄 때만 불확실한 side-effect step 을 다시 돌려.
atomic lease 하나가 이중 실행을 막아
schema 가 lease_until 컬럼을 들지만, 컬럼만으론 mutual exclusion 이 아냐. vessel 둘 — WebUI 피파랑 cron 피파 — 이 같은 task 를 고르면, 둘 다 자기 거라 생각하고 둘 다 같은 side-effect step 을 돌려. lease 는 atomic 하게 획득해야 해: 아무도 안 들고 있을 때만 task 를 claim 하는 SQL 한 문장 (code block 봐). SQLite WAL이 transaction을 보장해. 그 SQL 한 문장이 claim 이야. 다만 claim 이 전부는 아니야. atomic lease 가 보장하는 건 한 번에 쥔 쪽 하나지, 도는 쪽 하나가 아니거든. lease 보다 오래 살아남은 worker(멈춘 호출, 잠들었던 Mac)는 다른 쪽이 task 를 가져간 뒤에도 자기 step 을 끝낼 수 있어. 나머지 설계는 그 경우를 좁히지만 닫지는 못해. lease 는 진짜 일을 할 만큼 길고, 결과 없이 running 으로 남은 side-effect step 은 다시 돌리지 않고 막아. 대화가 살아 있다고 lease 가 갱신되진 않아. checkpoint wrapper 는 결과를 어디에 둘지 정할 때만 만료된 lease 를 쥔 걸로 치고, lease_until 은 자기 시계대로 끝나. model 이 모는 step 은 lease 를 최대 15분 들고 있어. model 은 next 와 commit 사이에 진짜 일을 하니까. 예전의 60초 lease 는 step 중간에 만료돼서 guard 를 조용히 떨어뜨렸어. (턴 길이 상한이 없어진 뒤로는 checkpoint wrapper 가 대화가 살아 있는 동안 step 의 lease 를 쥔 걸로 쳐. 결과를 어디에 둘지 정할 때만 그래. row 자체는 자기 시계대로 만료돼. 자세한 건 v10 메모에 있어.)