일어나면 안 되는 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 은 '안 돌았음' 이 아니라 — '돌았는지 모름' 이야. resume 규칙이 plan 시점에 정해진 step 의 side_effect flag 를 키로 써. read-only + uncertain → SAFE, 다시 돌려. side-effect + uncertain → STOP, 아빠/피파한테 surface: "이거 일어났어?" 사람이나 피파가 확인하고, 결정해.
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 가 다시 막아.
atomic lease 하나가 이중 실행을 막아
schema 가 lease_until 컬럼을 들지만, 컬럼만으론 mutual exclusion 이 아냐. vessel 둘 — WebUI 피파랑 cron 피파 — 이 같은 task 를 고르면, 둘 다 자기 거라 생각하고 둘 다 같은 side-effect step 을 돌려. lease 는 atomic 하게 획득해야 해: 아무도 안 들고 있을 때만 task 를 claim 하는 SQL 한 문장 (code block 봐). SQLite WAL 이 transaction 을 주고; 그 한 문장이 concurrency 안전의 전부야.