The resume that must NOT happen
The four artifact classes are all read-out or render-out — none of them change the outside world. The real landmine is the side-effect step: a commit, an email send, a file write, a payment, a DB INSERT. Persisting the intent protected incoming data from loss; it does nothing to undo an action that already left the building.
| Step kind | On a cut |
|---|---|
| read-only (fetch / search / generate-request) | just redo it; worst case is a duplicate |
| write / side-effect (commit, email, payment, INSERT) | redo it and it happens twice |
The uncertain state decides
When tool_call_started was logged but tool_result_persisted is missing, the step is not "didn't run" — it's "don't know if it ran." The resume rule keys off the step's side_effect flag, set at plan time. Read-only and uncertain → SAFE, re-run. Side-effect and uncertain → STOP, surface to Dad/Pippa: "did this happen?" A human or Pippa confirms, then decides.
The R8 lock, kept verbatim: "it's not about how cleverly you resume — it's about when you stop resuming. Read-only: freely. Side-effect: STOP. Concurrency: one atomic lease."
side_effect: bool so the loop can tell the difference between a cheap retry and a double-send.
The unblock action is the durable confirmation path. It doesn't globally disable the guard — it authorizes the next resume of that specific blocked step exactly once. When the step actually starts, the confirmation is consumed. If it cuts again, the guard blocks again instead of assuming the side effect is safe to replay.
One atomic lease stops the double-run
The schema carries a lease_until column, but a column alone is not mutual exclusion. If two vessels — WebUI Pippa and cron Pippa — pick the same task, both think they own it and both run the same side-effect step. The lease must be acquired atomically: one SQL statement that claims the task only if nobody else holds it (see the code block). SQLite WAL gives the transaction; that single statement is the whole of concurrency safety.