The bug shape: code that subtracts a saved timestamp from Date.now() and compares the result to a positive bound, without guarding against the case where the saved timestamp is in the future. For a past timestamp, the subtraction produces a positive age and the comparison works correctly. For a future timestamp, the subtraction produces a negative number, and negative is always less than any positive bound — so a future-mtime file passes "is this still fresh?" checks indefinitely.

The first time we shipped a fix for this was 4.5.32, in the lock-acquisition helper for upload bindings. A lock file's mtime was used to detect crashed lock holders; a forward NTP correction could fake-age an active lock and let another process steal it. The fix added a PID-liveness check as the primary signal, with mtime as a fallback. The mtime fallback still had the negative-age problem, but the PID check usually caught the case first.

The second was 4.5.57, in the cache TTL for index endpoints. A cached file with a future mtime was treated as fresh forever — until the wall clock caught up. The fix was explicit: if the computed age is negative, invalidate the entry.

The third was 4.5.86, in the session-ID file's freshness check. Same shape: Date.now() - data.ts < 2h passes for negative ages. Same fix: explicit negative-age guard.

This week's release ships the fourth, fifth, and sixth instances. All three are in the lock-acquisition layer — the same code path 4.5.32 originally touched, but with three sibling implementations across lib/binding.mjs, lib/message-binding.mjs, and lib/override.mjs. Each one has the mtime-fallback path that the PID check delegates to when the lock holder's identity is uncertain. Each one had the same comparison: if (Date.now() - st.mtimeMs > STALE_LOCK_MS) isStale = true. Each one failed the same way on future mtime.

How this round of probe worked

The probe was straightforward. Create a lock file with a future mtime. Try to acquire the lock. Observe whether the acquisition completes immediately (correct — stale detected, lock claimed) or blocks for the 5-second timeout (incorrect — future mtime defeating the stale check).

Empty + future-mtime case: blocked for the full 5 seconds before the fix. After the fix: 42 milliseconds. The difference is whether the stale-lock detection considers negative age as definitively stale (correct) or as "not yet expired" (incorrect).

The fix in each case is identical in shape. Compute age = Date.now() - st.mtimeMs once. Check age < 0 || age > STALE_LOCK_MS. One file got a small helper (mtimeStale) because it had three call sites for the same check; the other two got inline edits because they had two each.

Should this have been a shared helper from the start?

The question I keep arriving at with the recurring shapes is whether to extract a shared helper. For the readline-factory case (4.5.82) the answer was clearly yes — five identical sites, four of which the previous fix had missed. For this clock-skew case the answer is less obvious.

The check is small (three lines per call site, one if you accept a slightly less readable form). Each instance is in a slightly different context — different timestamp source, different stale threshold, different action when stale. A helper would have to be parameterized in awkward ways: it can't just take the timestamp because the action on negative age varies. The current shape — inline check, inline action — reads more clearly than the wrapped form.

The cost of the duplication is the cost of remembering to apply the guard at the next site. So far that cost has been low: most of the timestamps in the codebase have been audited, and the few remaining ones (TLS handshake timeouts, HTTP request timing, log rotation triggers) are either internal to libraries we don't control or use monotonic clocks that don't have the skew problem.

I think the right call here is to NOT extract a helper, and instead rely on the now-explicit pattern in code review. Every Date.now() - x comparison should have a quick mental check: does this need a negative-age guard? The answer is "yes" almost everywhere wall-clock subtraction is compared to a positive bound. If a future review or future engineer adds a new instance without the guard, the bug surfaces the next time someone clock-skews a file by accident, and the fix is small.

The cost of patterns

The probe series this month has surfaced about five recurring bug shapes: HTTP 200 with errors body, clock-skew on wall-time subtraction, loose substring matching for error detection, validation that's lenient on close-to-empty input, "feature defined but never wired." Each shape has shown up multiple times. The cumulative fix work has been roughly 50 small releases.

What I'd want to do next, if this project had more verification machinery, is automate the detection of each shape. A grep that finds Date.now() - .* < and warns if the next few lines don't include a negative-age check. A grep that finds err.message.includes( and asks whether the matched string is a known-specific signature. The static analyzer for the no-content-regex rule (shipped in 4.5.34) is the working example — that rule is now enforced in CI, so the bug shape can't reappear without being caught.

The other shapes deserve the same treatment. Until then, the probe series serves as a manual version of the same thing. Each new release fixes a specific instance and surfaces the shape it belongs to. Eventually the list of shapes becomes small enough that recognizing them is faster than running the probes — and at that point the probes can shift to a different layer of the system.

The version on the install URL is 4.5.87. The sixth instance of this particular shape. Probably not the last.