jcp-wire: a request presented again gets its first answer, so a lost response is recoverable #349
Merged
dcharlot
merged 1 commit from 2026-10-20 03:49:38 -04:00
fix/jcp-wire-retry-and-single-writer into main 2026-10-20
jcp-wire: a request presented again gets its first answer, so a lost response is recoverable
All checks were successful
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 16s
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 1m10s
standards workspaces build + test / joule-ui-rs (joule-ui/joule-ui-rs, joule-ui-rs, linux) (pull_request) Successful in 1m22s
standards workspaces build + test / jcp-rs (jcp-conformance, jcp/jcp-rs, -p jcp-energy --features nvml, jcp-rs, linux) (pull_request) Successful in 1m43s
standards workspaces build + test / joulehook-rs (joulehook/joulehook-rs, joulehook-rs, linux) (pull_request) Successful in 1m19s
standards workspaces build + test / sandbox-rs (sandbox-conformance, sandbox/sandbox-rs, sandbox-rs, linux) (pull_request) Successful in 2m22s
standards workspaces build + test / mesh-rs (mesh/mesh-rs, -p mesh-engine-gguf -p mesh-openai --features mesh-engine-gguf/gguf,mesh-openai/server, mesh-rs, linux-arm64) (pull_request) Successful in 3m0s
standards workspaces build + test / jouleclaw-rs (-nodebug, jouleclaw/jouleclaw-rs, -p jouleclaw-energy --features rapl,nvml, jouleclaw-rs, linux-arm64, -C target-feature=+fp16) (pull_request) Successful in 5m35s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 5m32s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 9m7s
standards workspaces build + test / jouledesk-rs (jouledesk/jouledesk-rs, jouledesk-rs, macos) (pull_request) Successful in 4m18s
The endpoint remembered each request it decided inside the freshness
window and refused a repeat as `replayed`. That stopped a captured request
being decided and debited twice. It also stranded a caller whose response
was lost: the identical request was refused, so the caller could not learn
the decision or get its receipt. Its only recourse was to stamp the request
again, which is a new request, so the same action was decided and debited a
second time. The guard against double debits caused one.
Now the endpoint remembers the answer as well as the request. A repeat
inside the window gets that answer unchanged: the same decision and the same
receipt bytes. It is not decided, debited, recorded or sealed a second time.
This holds across a restart. The journal replay re-decides each request at
the time it was decided, and both the decision and the Ed25519 seal are
deterministic, so the replay rebuilds each response byte for byte.
A captured request is worth nothing beyond what its first exchange carried.
A request refused before it was decided (stale, unauthenticated, or
`unrecorded`) is not remembered, and is decided when presented again. A
repeat after a revocation still gets the answer it got before the
revocation; the next new request is refused `revoked`.
SPEC §11 ("A request is decided once") and the C8 conformance check now say
this. `DenyReason::Replayed` stays, so that receipts sealed with it still
decode.
Tests: 4 new and 2 rewritten. All 7 mutations of the new logic are killed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>