Reopening a closed issue needs evidence the fault is newer than the close

Systemorph/MeshWeaver#4577. On 2026-09-17, between 07:4x and 07:5xZ, the log-incident bot reopened 125 closed issues in a few seconds (128 that day in total). Each carried a recurrence comment headed "Reopened — this fault is happening again.".

Measured afterwards across all 166 open issues, by the same evidence the new rule uses: for 53 of them the incident's newest occurrence predated the close it reverted — the oldest by five weeks (last seen 2026-08-10) — so nothing in the incident supported the sentence the comment printed. The other 75 had genuinely been seen after their close and were reopened correctly, if belatedly. The defect is therefore not "the bot reopens too much": it is that the bot could not tell the two apart, and said the same thing about both.

The rule that produced it

LogIncidentFiler.Reopen reopened on exactly one condition:

issue.State == GitHubIssueState.Closed
    ? repoClient.SetIssueState(…, GitHubIssueState.Open, …).Select(_ => true)
    : Observable.Return(false)

There was no comparison of any kind against when the fault was last seen. The sentence the comment prints — this fault is happening again — was therefore never a statement about the fault. It was a statement about the ticket: "this ticket was closed, and I am commenting on it now."

That was a safe enough conflation while the only thing that could trigger a comment was a fresh report. Ingest folded a new occurrence, decided a comment was due, and commented — so "I am commenting" really did imply "something just happened". The implication was structural, and nothing in the code said so.

What removed the implication

The comment-debt clock removed it deliberately, and for a good reason. An incident whose fault has stopped still owes its issue a final count — that is the whole point of the clock, and #1836 has the case where core#3780 sat frozen at occurrence 2 of 69 because the only settlement trigger was a report that would never arrive. So the clock now pays old debt on a timer, stamped in fault time, for incidents where by construction nothing new has happened.

Each of those late comments then met a reopen rule that read "closed ⇒ reopen". Every debt the clock settled on a ticket somebody had closed in the meantime reverted that close. The two changes were individually correct and jointly wrong, which is why neither side's tests caught it: the clock's tests assert the comment lands, and the reopen test asserted a reopen on a fixture whose incident had no relationship to the close time at all.

The rule now

A close is a person's judgement. Reverting it needs positive evidence, and there is exactly one piece of evidence that qualifies: the fault was seen after the close.

issue.ClosedAt is { } closedAt && incident.LastSeen > closedAt

GitHubIssue.ClosedAt is GitHub's own closed_at, already mapped by the platform's issue client, so this costs no extra call — the reopen path was already reading the issue.

Both unprovable cases stay closed, and say why

what GitHub reports what happens
open nothing to decide; comment as usual
closed, closed_at known, newest occurrence after it reopened — a genuine regression
closed, closed_at known, newest occurrence at or before it left closed, comment says the occurrence predates the close
closed, no closed_at left closed, comment says there is no close time to compare against

The asymmetry is deliberate. A missed reopen stays visible: the comment still lands on the ticket, still carries the counts, and still says the fault occurred — a human reading it can reopen in one click. A wrong reopen is indistinguishable from a real regression, which is what made the wave expensive: 130 tickets that each had to be read to find out they said nothing.

The two "cannot establish" branches are different printed sentences, in the comment and in the log, because "this predates the close" and "there is no close time" are different facts and only one of them is about the fault.

What did not change

The comment still posts, in all four rows above. The occurrences are real history and belong on the ticket — and the comment is also what settles the debt, so suppressing it on a closed issue would leave the clock re-asking for that incident forever. LastCommentedAt and OccurrencesAtLastComment advance exactly as before.

Pinned by LogIncidentFilingIdempotencyTest: the reopen case and the predates-the-close case are the same test twice over, differing in one value — whether the close falls before or after the newest occurrence.

How that set was measured

For every open issue in the repository, over the REST API: the most recent reopened event and its actor, the last closed event before that reopen, and the Last seen: line of the newest recurrence comment. The verdict is the new rule applied to those two timestamps.

Measured 2026-09-17 over 166 open issues: 136 were last reopened by the incident bot, 128 of them that same day. Of those 128 — 53 predate the close, 75 postdate it, and the remainder cannot be decided from the ticket alone (no close event, or no recurrence comment carrying a last-seen line). An undecidable row is reported as undecidable; it is never folded into either side.

The same read is what makes any remediation of the already-reopened tickets an evidence-based revert of the bot's action rather than a bulk close: it names, per issue, which of the two cases it is, and leaves every issue a human last touched — and every one whose fault really did recur — alone.

Reconnecting…
The connection to the server was interrupted. Trying to restore it…
Trying again…
The connection could not be restored. Reloading the page…
The server was updated. Reloading the page to pick up the latest version.