mergedhumanPR #278Other
fix(track): report an application whose acknowledgement was fenced
Follow-up to #264–#277, from the same correctness audit of main at df0b5f1.
Author
@JamesCarnley
github profile →- Status
- Merged
- Merged on
- Sep 14
- Branch
- fix/track-fenced-application-record
Description
The pitch.
Follow-up to #264–#277, from the same correctness audit of `main` at `df0b5f1`. It merges cleanly with every other open PR in that batch. This one was found by chasing an intermittent CI failure instead of re-running the job. ### Problem A seller can apply a quote request and send its reply, then have its acknowledgement refused as a stale fence — or die outright — before the town records it. Either way the redelivery was acknowledged as a bare duplicate, so the `processed` stage reported "processed acknowledgements carry no application record" and the run came out `incomplete`. That blames the subject for the town's own lease timing, although the work was applied exactly once. In the `quote-crash-restart` profile the lease is 1.5 s, and on a slow machine the restarted seller can lose it between applying the work and acknowledging it. ### Change - The journal row carries an `unreported` mark, written in the same statement as the application, before any acknowledgement is attempted. The mark therefore exists from the instant the work does, so a death that nothing could observe cannot lose it. - Only an accepted acknowledgement clears it. - A redelivery of still-unreported work is acknowledged as a duplicate that also carries the application actually performed. - The inbox loop gained one optional accepted-only callback. Its refusal path does no bookkeeping at all. ### Older journals upgrade in place The mark is a new column on the journal's `processed` table, and `CREATE TABLE IF NOT EXISTS` does not alter a table that already exists. So a journal file written by an older Town would have crashed the participant with `no such column: unreported`. Normal runs use a fresh state directory each time, but `test-agent --wait` and `run --agent ROLE=external` print a `STATE_DIR` that an operator is told to replace with their own writable directory, so one reused across an upgrade would have hit this. A participant crashing that way is exactly the "Town fault that looks like the subject's" failure this PR removes. The journal now adds the column when it is missing. Existing rows read as reported, which is correct: those applications were acknowledged under the older code, or are gone with the run that made them, so the upgrade cannot manufacture a second application record. The migration also protects the buyer, the A2A bridge and the model harness, which all build journals even though only the seller sets the mark. ### Compatibility - The coordinator HTTP contract, the bundled profiles, the bundle format and the evaluator are all unchanged. - Exactly-once application still holds. The journal still prevents a second apply, and a duplicate whose application already reached the record is acknowledged without an application record, so an ordinary duplicate delivery still shows exactly one. - `record()`'s default leaves the buyer, the A2A bridge and the model harness unaffected. ### Verification - **Regression tests** cover the fenced acknowledgement, a crash between applying and acknowledging, two consecutive fences, and the unchanged duplicate case. The crash-window test fails on an earlier design that only reacted to a refusal, and passes on this one. - **Delay sweep** around the 1.5 s lease (1.0, 1.4, 1.6, 2.5 and 5.0 s): 3/3 each. - **Crash-restart end to end:** 30/30. - **All eight Track profiles** pass, and so does `examples/byoa_seller.py`. - **`nandatown verify`:** 36/36 bundles. - **Full suite:** 823 passed, twice. ### Known limitation Duplicate delivery *with the applied acknowledgement fenced* ends `incomplete` at `duplicate_recognized`. The single redelivery acknowledgement carries both `applied` and `duplicate`, which satisfies the runner's quiescence heuristic, so the run ends before the town re-offers the work. - It needs a stall of more than 5 s, which no shipped profile can produce. - It is not a regression: `main` fails the same case earlier, at `processed`. This change strictly …
Try it
Open PR on GitHubView diffCheckout locally
git fetch origin pull/278/head:pr-278
git checkout pr-278