No expiry is a sentinel, and the two expiry paths differ on purpose
An interrupt with no expiry waits indefinitely, and must survive every sweep. When one does expire, whether that ends the run depends on which path expired it, and that asymmetry is the rule, not an oversight.
The rule
- An interrupt waits indefinitely unless it carries a positive expiry: an absent or zero expiry is the no-expiry sentinel, is never treated as overdue, and must be excluded by any sweep, so an open-ended question to a human is never reaped.
- An overdue interrupt is expired by two paths that differ in exactly one observable.
- A sweep expires the interrupt, records it, and announces the expiry, which is what ends the run holding it open (INT-18).
- Answering an already-overdue interrupt expires and records it as a side effect of refusing the answer, and announces nothing: that path is one caller answering one question, and announcing there would let a late click end the whole run.
What it means
Two different shapes both mean "wait indefinitely" — a missing expiry and one set to zero — and a sweep has to exclude both, or an open-ended question to a human would be reaped the first time the sweep runs. Treating only one of the two as the sentinel is the mistake this rule rules out.
The two paths that do expire an interrupt differ in exactly one observable, and that asymmetry is deliberate rather than an oversight. A sweep finding an overdue row expires it and announces the expiry — that announcement is what ends the run holding it open (INT-18). A caller answering a row that turns out to already be overdue also expires it, as a side effect of refusing the answer, but announces nothing: that path is one person answering one question late, and letting it end the run would let a late click end a run nobody meant to stop.
Example
{"expires": null}{"expires": 0}Both stay pending through every sweep, and both are still answerable long after they were created.