Status: Superseded by ADR-297 — its D1-D3 are D7-D9 there
Date: September 16, 2026
Target Version: Nikaia 0.0.8
Related: ADR-006 D3 and D5 (parked cleanups, and the drain
with a deadline), ADR-038 D5 (cleanup-deadline lives in
nikaia-runtime.toml), Part I 6.4 (Cleanup) and 7.2 (the panic hook),
Part III 13.3b, Part III Appendix A
A buffered file flushes, a transaction rolls back, a TLS connection says
goodbye: tearing a resource down is I/O, and Cleanup is where a program
writes it. When a task is cancelled nobody can wait for its I/O, so the
runtime adopts the pending cleanups and finishes them before the program ends,
bounded by cleanup-deadline so that the end cannot hang.
What happens when the deadline expires was half said. The remaining cleanups
are cancelled, their synchronous fallback runs, and “the program exits with a
warning naming every resource that did not finish cleanly.” Which exit
status, and where the warning goes, was not said — and an unsaid exit status
is 0.
A cancelled cleanup is an unflushed file or a transaction left open. The
thing that started the program is almost never a person: it is systemd, cron,
a pipeline, a script under set -e, a container supervisor — and none of
them reads a warning. They read the exit status, and 0 tells them the page
landed when it did not. report.nika’s own reasoning, the page is worth
nothing if it did not land, applies to the program that wrote it.
cleanup-deadline ends the program with exit status 70.70 is EX_SOFTWARE from sysexits.h, the Unix convention for the program
could not complete its work correctly, which is what happened. It is fixed,
documented, and distinct from a panic’s status (an abort is 134 on Linux)
so that a script can tell the two apart. A program whose cleanups all
finished exits as it would have anyway.
The message names every resource that did not finish, and it is delivered as a panic’s message is (Part I 7.2): to standard error, and through the program’s panic hook, so that whatever collects crash reports collects this. It is never written to standard output, which belongs to the program’s output and may be the very file somebody is waiting for.
0 again.cleanup-deadline = "0" stays what it is — do not drain, end at once — and
a program built that way expires nothing and exits as its code decides.
That is a decision visible in the configuration. A key that turns an expired
deadline back into success would let an operator declare data loss
acceptable in a file the program’s author never sees, and there is none.
70 restarts, alerts or stops the pipeline; one
that sees 0 did not know. The difference is one number and it is the
whole point.Steps 2 and 3. An expired deadline ends the program with exit status
70 and says so on the panic path: nikaia_std::rt::expired raises a panic
with the message — so the program’s panic hook runs, which is D2 — catches the
unwind, and exits EXIT_CLEANUP_EXPIRED. Standard output is never written to.
cleanup-deadline = "0" does not drain and therefore never expires, which is
D3 and needed no code. Part III 13.3b’s comment and Appendix A’s row say it,
and an_expired_cleanup_deadline_is_exit_70_on_the_panic_path runs a program
whose deadline expires and reads both the status and standard error — it has
to be a second process, because a status is only a status once the process is
over.
Step 1 is ADR-006’s and is not built, which bounds what the message can say: the parked-cleanup queue D3 describes does not exist, so what expires today is the drain of pending I/O operations and the message counts those rather than naming resources. When the queue lands, the names go in the same message on the same path.
What panic = "abort" costs is the status and not the message. There the
process is gone before the exit code can be set, with the abort’s own status;
the hook has already run and said what happened. It is named here rather than
left to be discovered.
The order of work:
#188 carries the remainder.