ADR-100
Trace retention — bounded by default, never silently, never a pending gate
accepted · 2026-07-05 · L2 L4 · cites 2
0ADR-100 — Trace retention · bounded by default, never silently, never a pending gate #
- Status: Accepted (2026-07-05) — implemented by the engine
feat/trace-retentionarc: the D1 policy (keep-10 per workflow · 30d age cap · 256MB budget · the paused + newest-per-workflow absolute exemptions) · D2 opportunistic GC atnika runstart (fail-open · exactly ONE stderr line ·--no-gc) · D3nika trace ls(age · size · workflow · state · the ★ resume-candidate marker) +nika trace rm(paused refuses without--forceand names the task) · D4 knobs on theNIKA_TRACE_*env family, reported bynika doctor· the 4 conformance fixtures as engine tests (crates/nika-cli/src/verbs/trace/{retention,manage}.rs). - Surface: CLI + runtime housekeeping ONLY. Zero envelope change · zero new YAML.
- Home: the CANONICAL prose lives in the
nika-specrepo'sadr/(language-surface decisions); this copy registers the number in the shared Nika ADR series (the enginedocs/adr/registry is the series' index).
Context #
.nika/traces/ grows without bound: every nika run appends one NDJSON
journal and nothing ever removes one (verified 2026-07-05 · zero
retention/GC language anywhere in spec v1). This was tolerable when a
trace was a debugging artifact. Since ADR-099 a trace is three things
at once:
- an observability record (
nika trace show|replayand the round-7 comprehension surfaces), - the checkpoint (
--resumefolds it · deleting a trace deletes resumability), - a pending human gate when its last state is
workflow_paused— deleting THAT trace silently destroys an unanswered approval.
Unbounded growth is a real cost (traces carry full task outputs since ADR-099 rehydration — multi-MB per run on payload-heavy workflows), but naive GC would violate the resume and pause contracts. The legacy engine had trace auto-GC; v1 must get the bounded-but-safe version.
Decision #
D1 · Retention policy (defaults · engine-enforced) #
Per workflow (keyed by workflow name within a project's .nika/traces/):
- Rule · Default · Rationale
- keep the last **N** completed-run traces · `10` · the observability window
- age cap · `30d` · stale traces exit even under N
- project total size budget · `256MB` · the hard stop
Absolute exemptions — never collected regardless of any cap:
- a trace whose last workflow state is
paused(a pending gate is an OBLIGATION, not garbage) — it ages out only once answered or explicitly removed withnika trace rm, - the most recent trace of each workflow (the standing resume candidate).
D2 · When GC runs #
Opportunistically at nika run start (maintenance rides usage — no
daemon, no cron, the Nextflow/cargo school). A collection that removes
anything prints exactly ONE line to stderr:
trace gc · removed 3 (2 aged · 1 over-budget) · kept 12 · 41MBSilent deletion is forbidden (the anti-hidden-magic invariant). --no-gc
skips collection for that invocation.
D3 · The explicit surface #
nika trace ls— traces with age · size · workflow · terminal state (completed/failed/`paused`) · the resume-candidate marker.nika trace rm <trace>|--older-than <dur>|--all— explicit removal · removing apausedtrace requires--forceand says what it destroys (this trace carries an unanswered prompt for task <id>).
D4 · Configuration #
The three knobs ride the engine's existing config surface (nika doctor
reports the active values). Config names and wiring are the
implementation arc's to fix; the DEFAULTS above are the spec contract.
Alternatives considered #
- No GC (status quo) — rejected: unbounded disk on every real user · the docs would have to teach manual cleanup of a dir the tool owns.
- A `nika trace gc` verb only (fully manual) — rejected: nobody runs maintenance verbs · bounded-by-default is the 2026 baseline · the explicit surface exists IN ADDITION (D3).
- GC daemon / background thread — rejected: no daemon is a v1 identity trait · opportunistic-at-run is sufficient and testable.
- Content-addressed dedup of outputs before GC — deferred: real (outputs repeat across runs) but it is a storage-format change · retention must not wait for it.
Consequences #
- Disk is bounded by default; resume and pause contracts survive GC by construction (the exemptions are the contract).
--resumeagainst a collected trace keeps its ADR-099 behavior (readable error · nothing skips) — retention adds thetrace lssurface to SEE what is resumable.- Traces stop being an unbounded liability in CI (the size budget is the cap that matters there).
Conformance sketch (ships with the implementation) #
- paused-survives · a
workflow_pausedtrace older than every cap survives GC ·trace lsmarks itpaused. - resume-candidate-survives · the newest trace of a workflow survives even when over-budget alone.
- visible-collection · an over-N history collects oldest-first ·
exactly one stderr line reports it ·
--no-gcleaves it intact. - forced-removal-speaks ·
trace rmon a paused trace refuses without--forceand names the unanswered task.
Notes #
- Interplay with idempotency keys (the §Persistence deferral) is unchanged — retention never re-runs anything by itself.
- The engine index registration +
nika trace ls|rmland with the implementation arc.
read at v0.107.0 · the decision record ships with the engine