wai-quantum-service: a job sent again under its key is the same job #607

Merged
dcharlot merged 1 commit from wai/quantum-service-idempotency-2 into main 2026-11-04 10:17:26 -05:00
Owner

Replaces #596: the same commit (same patch id), on main at 2bef11f71. Main had moved by 15 commits, among them #603, which runs each workflow's gate on linux rather than behind the arm64 work it gates; a pull request runs its own head's workflows, so #596 would have run the old ones.

A client that lost an answer from the job service could not ask again safely: a second submission was a second job, counted against its limits. The three MCP tools that run also declared idempotentHint: true, which was not so: every call made a new job.

What changes

wai-quantum-service

  • POST /v1/jobs and POST /v1/run take an Idempotency-Key, and run_quantum_circuit, run_quantum_set and run_on_target take idempotency_key. A key is 1 to 255 visible ASCII characters, with no quote or backslash, given as a structured field's string or bare.
  • Sent again with the same key and the same request, a job is answered with the job the key names. That happens before any limit, so a client at its active-job limit still gets its job back, and the resend counts as no submission.
  • The same key with another request is refused: 422 and the new code J034. The refusal names no job, because a job's id is what reads and cancels it.
  • A key names its job for as long as the job is kept, whatever its status, with one exception: a job the service interrupted. That job's key runs the request again, as the interrupted job's own message invites.
  • A key is held per client. Its record keeps a BLAKE3 digest of the key with the client's name, never the key, and the key still names its job after a restart. The lookup and the insert happen under one lock, so many sends of one key at once make one job.
  • The three tools' idempotentHint is now false. Without a key, each call is a new job, and their descriptions say so.
  • Nothing matches a call without a key by its request. The service keeps no session, and a client is an address, so matching by request would hand one person's job id, and with it the power to cancel the job, to anyone at the same address who sent the same program.
  • GET /v1 states idempotency, the OpenAPI document describes the header on both routes, and the CORS preflight allows it.

wai-quantum: the refusal code J034, in the table and in docs/job-refusals.md.

wai-quantum-cli: submit and task send each job under a fresh 128-bit key. Where no answer comes back, they send it again under the same key, twice at most, and only to a service whose GET /v1 says it keeps keys. An older service is never sent a job twice.

wai-quantum for Python: Service.submit and Service.run do the same, and take idempotency_key. A job the service interrupted now ends the wait; before, _FINISHED left it out and the wait ran to its deadline.

The tool snapshot (wai-quantum-cli/docs/service-mcp.json) and the agent docs generated from it are regenerated.

Tests

  • The service:
    • A job sent again is the same job, past the active limit, with the client's window untouched.
    • Another client's key of the same text names that client's own job.
    • Another request under the key is refused, and the refusal names no job.
    • A cancelled job keeps its key; an interrupted one frees it.
    • The record holds the key's digest, never the key.
    • Sixteen threads sending one key at once make one job, one queue entry and one submission.
    • A key names its job across a restart of a file store, except the job that was running, which comes back interrupted.
    • What reads as a key, and what does not.
    • The header, quoted and bare.
    • The MCP tool answers with the key's job, takes the key out of the job, and refuses a key sent with another program, one that does not read, and one that is not a string.
    • The hints and schemas of the three tools, and none on the dry run.
  • The command line: against a stub that takes the job and drops the connection, the job is sent again with the same key. A stub that keeps no keys is sent the job once.
  • Python: the same two against the test stub, a key given is the key sent, and an interrupted job ends the wait. Against the service built from this checkout: the same key and job is one job (across submit and run), the key with another job is 422 J034, and a key with a space is 400 J034.

Checked

On the build host: wai-quantum's refusal tests; the service's tests and clippy with -D warnings; the command line's tests and clippy, with and without default features; and the Python package's whole check (ci/check.sh), which builds the command line, the service and the wheel and runs pytest and the notebooks. The public-name check is clean.

🤖 Generated with Claude Code

Replaces #596: the same commit (same patch id), on main at 2bef11f71. Main had moved by 15 commits, among them #603, which runs each workflow's gate on `linux` rather than behind the arm64 work it gates; a pull request runs its own head's workflows, so #596 would have run the old ones. A client that lost an answer from the job service could not ask again safely: a second submission was a second job, counted against its limits. The three MCP tools that run also declared `idempotentHint: true`, which was not so: every call made a new job. ## What changes **wai-quantum-service** - `POST /v1/jobs` and `POST /v1/run` take an `Idempotency-Key`, and `run_quantum_circuit`, `run_quantum_set` and `run_on_target` take `idempotency_key`. A key is 1 to 255 visible ASCII characters, with no quote or backslash, given as a structured field's string or bare. - Sent again with the same key and the same request, a job is answered with the job the key names. That happens before any limit, so a client at its active-job limit still gets its job back, and the resend counts as no submission. - The same key with another request is refused: `422` and the new code `J034`. The refusal names no job, because a job's id is what reads and cancels it. - A key names its job for as long as the job is kept, whatever its status, with one exception: a job the service interrupted. That job's key runs the request again, as the interrupted job's own message invites. - A key is held per client. Its record keeps a BLAKE3 digest of the key with the client's name, never the key, and the key still names its job after a restart. The lookup and the insert happen under one lock, so many sends of one key at once make one job. - The three tools' `idempotentHint` is now `false`. Without a key, each call is a new job, and their descriptions say so. - Nothing matches a call without a key by its request. The service keeps no session, and a client is an address, so matching by request would hand one person's job id, and with it the power to cancel the job, to anyone at the same address who sent the same program. - `GET /v1` states `idempotency`, the OpenAPI document describes the header on both routes, and the CORS preflight allows it. **wai-quantum:** the refusal code `J034`, in the table and in `docs/job-refusals.md`. **wai-quantum-cli:** `submit` and `task` send each job under a fresh 128-bit key. Where no answer comes back, they send it again under the same key, twice at most, and only to a service whose `GET /v1` says it keeps keys. An older service is never sent a job twice. **wai-quantum for Python:** `Service.submit` and `Service.run` do the same, and take `idempotency_key`. A job the service interrupted now ends the wait; before, `_FINISHED` left it out and the wait ran to its deadline. The tool snapshot (`wai-quantum-cli/docs/service-mcp.json`) and the agent docs generated from it are regenerated. ## Tests - **The service:** - A job sent again is the same job, past the active limit, with the client's window untouched. - Another client's key of the same text names that client's own job. - Another request under the key is refused, and the refusal names no job. - A cancelled job keeps its key; an interrupted one frees it. - The record holds the key's digest, never the key. - Sixteen threads sending one key at once make one job, one queue entry and one submission. - A key names its job across a restart of a file store, except the job that was running, which comes back interrupted. - What reads as a key, and what does not. - The header, quoted and bare. - The MCP tool answers with the key's job, takes the key out of the job, and refuses a key sent with another program, one that does not read, and one that is not a string. - The hints and schemas of the three tools, and none on the dry run. - **The command line:** against a stub that takes the job and drops the connection, the job is sent again with the same key. A stub that keeps no keys is sent the job once. - **Python:** the same two against the test stub, a key given is the key sent, and an interrupted job ends the wait. Against the service built from this checkout: the same key and job is one job (across `submit` and `run`), the key with another job is `422 J034`, and a key with a space is `400 J034`. ## Checked On the build host: wai-quantum's refusal tests; the service's tests and clippy with `-D warnings`; the command line's tests and clippy, with and without default features; and the Python package's whole check (`ci/check.sh`), which builds the command line, the service and the wheel and runs pytest and the notebooks. The public-name check is clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
wai-quantum-service: a job sent again under its key is the same job
All checks were successful
wai-quantum WebGPU member / member · software Vulkan (linux) (pull_request) Successful in 2m22s
wai-quantum views in agent hosts / views · a scripted host (pull_request) Successful in 4m2s
wai-quantum WebGPU member / shader checks · browser build · wasip2 inertness · MSRV (pull_request) Successful in 4m20s
wai-web reach builds / wai-web reach builds · the package of record, every output as recorded (pull_request) Successful in 2m36s
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Successful in 5m47s
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (pull_request) Successful in 21m18s
wai-web reach builds / wai-web reach builds · this tree's wai-web, every rung on its engine line (pull_request) Successful in 6m41s
WAI energy receipts / energy receipts · linux (pull_request) Successful in 15m13s
wai-quantum tensor-engine member / member · tensor engine, the kernel toolchain's simulator (linux) (pull_request) Successful in 14m43s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 1m13s
WAI Studio / Studio in WebKit · linux (pull_request) Successful in 14m19s
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Successful in 16s
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Successful in 21s
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Successful in 53s
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Successful in 2m56s
WAI examples / WAI examples · android-app · linux-arm64 (pull_request) Successful in 8m7s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 8m30s
wai-quantum HIP member / member · HIP absent (linux-arm64) (pull_request) Successful in 1m29s
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Successful in 18m57s
wai-quantum npu-cores member / member · npu-cores absent (linux-arm64) (pull_request) Successful in 29s
wai-quantum pulse and dynamics / the same bits · linux-arm64 (pull_request) Successful in 2m13s
WAI conformance / each feature of the independent verifiers' steps compiles alone (pull_request) Successful in 18m46s
wai-quantum tensor-engine member / member · tensor engine absent (linux-arm64) (pull_request) Successful in 35s
wai-quantum extension points / the registry and the example · linux-arm64 (pull_request) Successful in 2m27s
wai-quantum variational and algorithm library / parameters, gradients, optimisers, VQE, QAOA and builders · linux-arm64, release (pull_request) Successful in 2m38s
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (pull_request) Successful in 2m36s
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Successful in 29m24s
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Successful in 1h3m13s
wai-quantum WebGPU member / member · DX12 and Vulkan (cbio-windows) (pull_request) Successful in 12m51s
wai-quantum studio and desktop / desktop app · windows (pull_request) Successful in 10m40s
fd3b7c60d4
A client that lost an answer could not ask again safely: a second
submission was a second job, counted against its limits. And the three MCP
tools that run declared idempotentHint: true, which was not so.

POST /v1/jobs and POST /v1/run take an Idempotency-Key, and the three tools
an idempotency_key. Sent again with the same key and request, a job is
answered with the key's job, before any limit, counting as no submission;
with another request the key is refused (J034), naming no job. A key names
its job while the job is kept, but for a job the service interrupted, which
the key runs again. Records keep the key's digest with the client's name;
one lock makes concurrent sends of one key one job. The tools' hint is now
false: a call without a key is a new job. Calls without a key are not
matched by their request, since a client is an address and that would give
one person's job id to anyone at the address sending the same program.

The command line and the Python client send each job under a fresh key, and
send it again under it where no answer came back, only to a service that
says it keeps keys. The Python client's wait also ends on an interrupted
job, where it ran to its deadline before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Transaction-Science/open-standards!607
No description provided.