fix(buyer): an unanswered relay read is not proof the offer is gone (#291) - #298
Merged
Conversation
…291) `fetch_events` resolves `Ok(empty)` on timeout, so "the relay holds no offer for this job" and "we stopped waiting" reached the auto-award loop as the same bytes. The loop read them as the first and parked — terminally, since the driver never retries a parked row — under the reason "offer no longer on the relay". In the field that reason was false in every clause: the offer was present with 5.8 hours left on its deadline and the seller had already claimed. The discriminator is one layer out, so it is now asked for. `JobView` carries `read_confirmed`: any event returned proves the session was serving us, and when all three filters come back empty the fetch spends one round trip on a `limit(0)` REQ and waits for the `EOSE` the relay owes us. A response, never a broadcast — an event we merely hope to receive proves nothing by its absence. `plan_missing_offer` is the single place those two facts become an action, so the two sites that disagreed about identical evidence cannot drift apart again. Refusing without a bound would be an infinite loop, because the deadline check that terminates the normal path lives inside the branch that requires an offer; the bound parks on the READ and its reason says so, rather than smuggling the old conclusion back in a minute later. Also clears the intent row on a manual award. It was advanced only by the auto path, so `status` went on reporting a parked reason for jobs that were awarded and running. Tests: 655 -> 661. Red-proved by reintroducing the defect — the two guards fail, and `a_confirmed_empty_read_still_parks` stays green, so the fix discriminates rather than disables. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #291.
The conflation
fetch_eventsresolvesOk(empty)on timeout. So in the auto-award loop, "the relay holds no offer for this job" and "we stopped waiting" arrived as the same bytes, and the loop read them as the first:parkedis terminal — the driver never retries it. In the field that reason was false in every clause: the offer was on the relay with 5.8 hours left on its deadline, and the seller had already claimed. Twenty-five lines up, the award-presence check already treats its empty read as unknown. Two sites, identical evidence, opposite conclusions, and only one of them right.The fix
Ask for the discriminator, because it is one layer out.
JobViewgainsread_confirmed:limit(0)REQ and waits for theEOSEthe relay owes usReusing
probe_relay_serves_our_reqs, the buyer's existing liveness probe. Deliberately a response, not a broadcast: an event we merely hope to receive proves nothing by its absence.plan_missing_offeris the only place those two facts become an action, so the two sites cannot drift apart again:ParkOfferAbsent— absence established, park is honestRetry— conclude nothingParkUnreadable— park on the read, never the offer⚠ The bound is a floor, not a fallback to the old behaviour. Refusing forever is an infinite loop, because the deadline check that terminates the normal path lives inside the branch that requires an offer and is never reached on an empty read. A ceiling without a floor is half a fix — but the floor parks on what was observed (
N consecutive job reads went unanswered by the relay; the offer's presence was never established either way), which is the one thing that stops it from reintroducing the defect a minute later.Park reasons live beside the decision rather than at the call site, so a row's reason and the evidence that produced it cannot drift.
Second defect, also from the issue
The intent row was advanced only by the auto path, so after a manual award
statuswent on reportingparked / offer no longer on the relayfor a job that was awarded and running. Both success shapes now clear it —AlreadyAwardedincluded, since a parked reason sitting beside a recorded award is false whichever call published it.Evidence
Denominator moves 655 → 661, so the six new tests actually ran rather than being filtered to a vacuous
ok.Red-proved. Reintroducing the defect (
if read_confirmed→if true) fails exactly the two guards and no others:That third line is the point of the suite. "A timed-out read must not park" is trivially satisfied by never parking, and a suite testing only the timeout leg would pass on that —
a_confirmed_empty_read_still_parksis the positive control showing the guard discriminates rather than disables. Restore verified by grep before the green re-run, not assumed.The reason-wording test is anchored on absence (
no longer on the relay,no offer,offer is gone,expiredmust not appear) plus two positives, because the original defect was a reason asserting what the evidence could not support — and a presence-only check passes on a string that also contains the bad words.ⓘ Not a timeout raise. Raising 5s is mitigation; a slower relay day reopens it. Per #253 the five fetch timeouts stay distinct — some are bounded above by an MCP deadline, others below by relay latency.
ⓘ
cargo fmt --checkreports 748 drift sites crate-wide ondevand no workflow gates on it, so this branch leaves formatting alone rather than shipping a whole-crate reflow inside a behaviour fix.🤖 Generated with Claude Code