ADR 0012: Idempotency keys and resumable runs¶
Status¶
Accepted — 2026-09-19
Context¶
A batch over a 12-video playlist with a provider that takes 30–60 s per image (Antigravity, pending spike S7) runs for many minutes and will be interrupted: Ctrl-C, a timeout, a transient provider error, a laptop lid. Re-running must not regenerate images that already succeeded, must retry the ones that failed a bounded number of times, and must never double-count.
Decision¶
- Idempotency key (per iteration):
sha256(template.spec_hash + provider_profile.id + video.youtube_id + str(part_number) + rendered_prompt + str(seed) + reference_asset.sha256)[:32], stored oniteration.idempotency_key UNIQUE. For hero runspart_numberis""and the ordinal is folded intoseed(orstr(ordinal)when the provider has no seed) so N iterations get N keys. Helpers live incore/ids.py. - Run states:
pending → running → {completed, failed, cancelled, paused};pausedandfailedreturn torunningviaruns resume.pending → cancelledis allowed. - Batch / resume algorithm: for every selected
playlist_item, compute the key. If an iteration with that key exists and iscompleted→ skip (counted as done). Ifrunningandstarted_atis older thanbatch.stale_after_s(default 900 s) → treat asfailed. Iffailed→ retry whileprovider_response_json.attempts < --max-retries(default 2). Otherwise create apendingiteration. - Concurrency:
asyncio.Semaphore(min(--concurrency, capabilities.max_concurrency)); Antigravity is therefore serialised. - Interrupt: SIGINT → cancel in-flight tasks, mark those iterations
failedwitherror_text = "interrupted", mark runpaused, exit130. Finished iterations are never touched. - Partial completion: run ends with ≥1 failed and ≥1 completed → run
failed, exit6(PartialBatchError), message shows the resume command. All failed → runfailed, exit4. - Budget guard:
--max-images Naborts before creating more than N new iterations (pre-flight count, exit2). - Retries within one attempt (transient/timeout only) use
tenacityperPLAN.md§7.2; the attempt counter that--max-retriesreads is the cross-invocation one inprovider_response_json.attempts.
Consequences¶
runs resume <run>is the only recovery command a user needs;--resume RUN_IDonbatchis an alias.- Changing the template version, provider profile, prompt, seed or reference asset changes the key, so a "resume" after such a change correctly regenerates.
- The
UNIQUEconstraint makes accidental double insertion a database error rather than a duplicate image. - Tests use
FakeProviderwith[[FAIL_TRANSIENT]]/[[FAIL_PERMANENT]]markers anddelay_msto simulate every transition.
Alternatives considered¶
- Sequence-number bookkeeping (
last_completed_position) — rejected: breaks with--only 3,7-9, parallelism and retries of earlier items. - Hashing only
(playlist_id, video_id)— rejected: a template or reference change would wrongly skip regeneration. - A job queue (Celery, RQ, Huey) — rejected: needs a broker or a worker process; SQLite rows plus a semaphore give the same guarantees for one process.