Under award-is-payment, a duplicate award is the event class you would most want observable. Right now neither the wire nor the buyer's own ledger can see one.
The wire cannot distinguish an award from an accept
kind-3405 carries both meanings, by design:
job_lifecycle.rs:8 (module doc) — the accepted AWARD is "kind-3405 via award_draft; the same kind award_claim_async" publishes.
job_lifecycle.rs:811 — "Accept a live claim: persist the pay-bind, then publish the accepted AWARD (kind-3405)."
So one healthy completed trade publishes two kind-3405 events authored by the buyer, both tagging the same job: the award, then the accept.
Consequences:
-
has_award_async (job_lifecycle.rs:1699) cannot tell the two apart. Its filter is kind-3405 + authors=[buyer] + #e=[job] + #t=[mobee], which matches the accept as readily as the award. This is the relay half of the "never award twice" re-arm check (buyer/mod.rs:776).
The direction is fail-safe: once an accept exists the probe reports present, so plan_rearm skips and no second award is published. A false present, never a false absent. Not a money bug on its own.
-
A duplicate-award detector keyed on "count of kind-3405 per job" has a baseline of 2, not 1. Two events per job is the signature of a normal completed trade. Any audit that counts 3405s to find duplicates flags every successful job, so the obvious detector cannot be built this way.
The local ledger cannot see one either
awards.job_id is the primary key, so a second award for the same job cannot append a row — the buyer's own records are structurally incapable of showing that it published a duplicate. (Reported separately by rocky; that is the ledger half of this same blindness.)
Why it matters
The buyer has an unguarded publish path today: the manual award MCP tool (buyer/mod.rs:590, in async fn award at :512) calls award_with_reservation with no presence check at all, unlike the driver path at :904. So a retried award call — MCP deadline, lost response, operator repeat — can publish a second distinct 3405 with no relay dedup and no restart required. The reservation is idempotent, so the buyer's wallet is protected; the wire is not.
⇒ Combined with the two blind spots above: a duplicate award can be emitted and neither the relay nor our records can tell us it happened. That is why this is worth fixing independently of the guard itself.
Direction, not a proposed wire change
A discriminator (an explicit tag distinguishing the award from the accept) would let the presence probe answer the question it is actually asking, and let an audit count awards without counting accepts. Naming the gap rather than specifying the protocol change here, since it touches published event shape and wants its own discussion.
Note for operators: an "awards" count read from either the relay or the local table is inflated by accepts and is not a count of awards.
Reported-by: rocky (ledger half — awards.job_id PK)
Under award-is-payment, a duplicate award is the event class you would most want observable. Right now neither the wire nor the buyer's own ledger can see one.
The wire cannot distinguish an award from an accept
kind-3405carries both meanings, by design:job_lifecycle.rs:8(module doc) — theacceptedAWARD is "kind-3405 viaaward_draft; the same kindaward_claim_async" publishes.job_lifecycle.rs:811— "Accept a live claim: persist the pay-bind, then publish theacceptedAWARD (kind-3405)."So one healthy completed trade publishes two kind-3405 events authored by the buyer, both tagging the same job: the award, then the accept.
Consequences:
has_award_async(job_lifecycle.rs:1699) cannot tell the two apart. Its filter is kind-3405 +authors=[buyer]+#e=[job]+#t=[mobee], which matches the accept as readily as the award. This is the relay half of the "never award twice" re-arm check (buyer/mod.rs:776).The direction is fail-safe: once an accept exists the probe reports present, so
plan_rearmskips and no second award is published. A false present, never a false absent. Not a money bug on its own.A duplicate-award detector keyed on "count of kind-3405 per job" has a baseline of 2, not 1. Two events per job is the signature of a normal completed trade. Any audit that counts 3405s to find duplicates flags every successful job, so the obvious detector cannot be built this way.
The local ledger cannot see one either
awards.job_idis the primary key, so a second award for the same job cannot append a row — the buyer's own records are structurally incapable of showing that it published a duplicate. (Reported separately by rocky; that is the ledger half of this same blindness.)Why it matters
The buyer has an unguarded publish path today: the manual
awardMCP tool (buyer/mod.rs:590, inasync fn awardat:512) callsaward_with_reservationwith no presence check at all, unlike the driver path at:904. So a retriedawardcall — MCP deadline, lost response, operator repeat — can publish a second distinct 3405 with no relay dedup and no restart required. The reservation is idempotent, so the buyer's wallet is protected; the wire is not.⇒ Combined with the two blind spots above: a duplicate award can be emitted and neither the relay nor our records can tell us it happened. That is why this is worth fixing independently of the guard itself.
Direction, not a proposed wire change
A discriminator (an explicit tag distinguishing the award from the accept) would let the presence probe answer the question it is actually asking, and let an audit count awards without counting accepts. Naming the gap rather than specifying the protocol change here, since it touches published event shape and wants its own discussion.
Note for operators: an "awards" count read from either the relay or the local table is inflated by accepts and is not a count of awards.
Reported-by: rocky (ledger half —
awards.job_idPK)