Repository navigation
Conversation
…ipts read from the body (r31 finding 7)
astra's r31 finding 7: acknowledgements depended only on HTTP 200/201,
response bodies were ignored, and a rejected 'failed' report was merely
logged, with no durable retry. The gateway's own routes show why that
matters: the evidence relay answers 200 {"stored": false} when its insert
fails, and the relay status route answers 200 {"updated": false} for a
job it does not know.
- ws_client: evidence counts as acknowledged only with {"stored": true}.
PATCH status counts only when the returned job carries the requested
status; a 2xx that did not record it is NOT forced through the relay.
The relay counts only with {"updated": true}.
- outbox.StatusOutbox: terminal reports ('failed', 'completed') the gateway
did not acknowledge are kept on disk (atomic write), retried with capped
exponential backoff (5s base, 10-minute cap) and dropped loudly after 24h.
There is one report per job and a bounded size; a corrupt file is set
aside; nothing touches disk until something is queued. Evidence pushes are
NOT retried: the relay does not deduplicate, so a lost acknowledgement
could store a bundle twice. The 'running' claim is never queued.
- JobExecutor: every terminal report goes through one reporter that queues
on failure; flush_outbox() retries what is due. execute() now reports
'completed' only when the completion evidence was stored, as the
device-reported path already did.
- daemon: the outbox lives at ~/.pcc-node/status-outbox.json and is flushed
every cycle, guarded like poll_awaiting.
Tests: tests/test_outbox.py, 19 new. pcc-node 1196 passed, 2 failed (the
headless pair fixed by #388). 10 mutants killed.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PkeiQMnVePeBGU9svhREFy
…e outbox Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PkeiQMnVePeBGU9svhREFy
The r31 round-1 verdict lists, under existing dependencies, that JobExecutor's awaiting registry is memory-only: a daemon restart forgets every accepted job (an lp spool, an OctoPrint print) and it stays "running" upstream with nobody watching. AwaitingStore keeps those records on disk so a restarted daemon can resume observing them. It stores only public fields (job id and evidence binding, device id, completion kind, handle, wall-clock acceptance and deadline) and refuses any other field, so a device record and its credential (an OctoPrint API key) never reach disk. Writes are atomic (temp file, fsync, rename); an unreadable file is set aside and the store starts empty, which strands jobs rather than reporting them. Not wired into JobExecutor or the daemon yet: that follows once #377's round-1 fixes are merged forward into this stack. Tests: 16 new (tests/test_awaiting_store.py). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Cross-family round-1 review of #377 @d5623112 (review-router/ r31-pccnode-r2-astra/verdict.md) was DO-NOT-SHIP: a merely submitted operation, or an observation of a different print attempt, could still produce execution_completed. Every blocking finding is addressed here. F1 (critical) generic HTTP completed while the work was queued: - No device-agnostic completion vocabulary remains. A generic HTTP answer completes a job ONLY under device["completionContract"] v1 (validate_completion_contract, _contract_outcome). The contract fixes the method and path (a job cannot choose another); names the exact completionField and completionValues (a value naming an acceptance/running or failure word, False, a float or a container is refused); and names a correlationField that must hold exactly this PCC job's id. The node sends the id as X-PCC-Job-Id, and in the body under correlationRequestField when one is named. - Any acceptance/running word ANYWHERE in the body, under ANY key or in any list, blocks completion (_outcome_statements, key-agnostic). - An invalid contract, or a job asking for another operation, sends nothing. - Without a contract nothing completes; a recognised acceptance statement is still acceptance (non-terminal). - 204 never completes. F2 (critical) IPP answers not bound to the spooled job, absent reasons read as safe: - Get-Job-Attributes requests job-id, job-state and job-state-reasons. - ipp_completion_verdict(..., job_id) requires exactly one integer job-id equal to the spooled id for ANY terminal verdict (ipp_job_id). - COMPLETED also requires a well-formed, non-empty keyword job-state-reasons (ipp_job_state_reasons, strict). - State 9 with a cancel, abort or stop reason is a failure. F3 (critical) an OctoPrint snapshot identified a file, not a print attempt: - The adapter prints a per-job PRIVATE COPY: it creates folder pcc-<job id>, copies the file in, reads the copy's print-history baseline, then selects and prints the copy. The first failing step stops the sequence. - Completion comes only from OctoPrint's own durable per-file print history for the copy (prints.success/failure/last.success; OctoPrint logs a success only on PrintDone, a failure on cancel or failure), compared with the baseline, because a copy may carry the source's metadata (octoprint_history_verdict). - /api/job and seen_active are gone. Any failure recorded after the baseline is a failure; history going backwards is unobservable. F4 (high) parsing could erase a failure: - http_util refuses duplicate keys (object_pairs_hook) and NaN/Infinity (parse_constant), and never parses invalid UTF-8. A refused body comes back as text, which no reader takes for a completion. - Reads are bounded (max_bytes; an oversized answer is a transport failure). - _body_shape_problem counts EVERY node, scalars included, before any reader runs. F5 (high) deadlines not enforced after I/O: - Opentrons: the budget is clamped to 3600 s, the interval to >= 0.5 s, and the request count is bounded. Each request gets min(30, remaining) with no floor; the deadline is rechecked after every answer (a late answer is discarded). - IPP/OctoPrint polls: min(10, remaining), a recheck after the answer, at most one request per job per 5 s, budgets clamped to 7 days. - Clock and sleep are injectable. Matrix: an Opentrons body counts only when data.id == run_id; 204 above. Not here, by owner: atomic claim / exactly-once (gateway, escrow R33); the durable status outbox and awaiting registry (stacked #423). Tests: pcc-node 1389 passed, 2 failed. The 2 (test_cli start_flow, test_discovery start_with_discover_flag) fail identically at d547e7e and predate the stack. - tests/test_r31_round2.py (203 cases) was written from the verdict by a sonnet subagent against a spec fixed before it saw the code. - tests/test_completion_pollers.py was ported to the new semantics by a second sonnet subagent. Tests encoding rejected behavior were replaced by stricter ones, never dropped; for example, the captured CUPS answer with no job-id is now WAITING, not FAILED. - tests/test_job_executor.py was rewritten by the lane where it encoded the rejected vocabulary. The lane reviewed all three, and a 31-probe replay of the reviewer's own attack inputs is closed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…into the outbox Brings #420 @9059b126, which carries #377's r31 round-1 fixes (@63663616), under the status outbox. One conflict: JobExecutor.__init__ gained sleep= on the round-2 side and outbox= on this side. Both are kept, and outbox is documented. pcc-node 1455 passed. The 2 failures predate the stack (test_cli start_flow, test_discovery start_with_discover_flag). A replay of the round-1 reviewer's own attack inputs: 31/31 closed on the merged tree. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The r31 round-1 verdict, under existing dependencies: "_awaiting is memory-only ... a restart can strand a running job", and "do not promote a money-triggering completion path before the dependency has an explicit recovery policy". - JobExecutor(awaiting_store=AwaitingStore(...)) stores each accepted job (public fields only; the device is looked up again by id). A new executor on the same store resumes observing it (_restore_awaiting). - A stored record is dropped WITHOUT a status, with an ERROR naming it, when its device is no longer configured, when its handle does not fit that device, or when its budget ran out while the daemon was down. For OctoPrint the configured device must still serve the handle's base URL: the poll sends that device's API key, which must never go to a URL it does not belong to. - Exactly one terminal report across restarts. A job leaves the registry, on disk too, before anything is reported. If its stored record cannot be removed, nothing is reported now (_forget_awaiting), and the process that restores it reports once. - Recovery policy, now written where it applies (_report_device_outcome): an unacknowledged COMPLETION evidence push is not retried by the node, because the evidence relay does not deduplicate (bus #3307) and a retry after a lost acknowledgement could store the bundle twice. The job stays "running" with an ERROR until the relay deduplicates by bundle hash. Terminal STATUS reports, which are idempotent, stay durably retried by the outbox. - daemon.py wires it next to the outbox: ~/.pcc-node/awaiting.json. Tests: 10 new (tests/test_awaiting_restart.py): resume and report once; the key only goes to the device's own URL; a moved URL, a missing device or an expired budget is dropped without a status; a failed disk removal defers the single report to after the restart; a failed store still tracks in memory; no store touches no disk; IPP resumes too. 5 of 5 mutants of the durability safeguards killed. pcc-node 1465 passed; the 2 failures predate the stack. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Head 4153973. #420 @9059b126 is merged forward. The new registry (AwaitingStore) closes the round-1 dependency "a restart can strand a running job":
pcc-node: 1465 passed; the 2 failures predate the stack. 5 of 5 durability mutants were killed. Cross-family pack: 43. |
Evidence lane: R31 semantics review of the round-2 stack (#377 @63663616, #423 @4153973e)Board R31 names evidence as the semantics reviewer. The cross-family depth is packs 42/43. This covers the evidence contract only. #377 completion semantics: OK from the evidence side.
#423 durable awaiting store: one finding (should fix). The restart boundary does not apply the binding rules that #420 enforces at the assignment boundary.
After a restart, Fix: one rule for the binding, whether it comes from the gateway or from disk. In
Reuse 🤖 Generated with Claude Code |
…into the outbox #377 @ 67875cd withdrew OctoPrint completion tracking (acceptance-only; its REST API names no print attempt) and made generic HTTP acceptance-only. Only IPP jobs are tracked now, so the durable awaiting registry follows: - job_executor.py (conflict): _valid_stored_handle keeps only its IPP branch; a stored record of any other kind is refused. New: a restored IPP handle must still point at the printer the device configured under that id sends its jobs to (_ipp_printer_host, shared with execute_ipp_print). A CUPS job-id means something only on the printer that issued it, so polling a printer the device was re-pointed to could read ANOTHER job with the same id and report it as ours. Such a record is dropped without a status, like a device no longer configured. - awaiting_store.py: AWAITING_KINDS is ("ipp",); an "octoprint" record is malformed and dropped on load. Docstrings follow. - tests: test_awaiting_restart.py and test_awaiting_store.py are rebuilt on IPP. They keep every property: public fields only; resume and report exactly once; the poll goes only to the device's own printer; a re-pointed device, a device no longer configured, and a budget that ran out are all dropped without a status; an unremovable record is not reported until the restart; an unstorable job is still tracked in memory; no store means no disk. New: a stored octoprint record is dropped; an OctoPrint acceptance is never stored; a direct test of _valid_stored_handle (kind, printer, id, queue). pcc-node: 1359 passed. The 2 failures predate this (test_cli start_flow, test_discovery start_with_discover_flag). The mutants for the printer-host check, the kind check and the store's kind list are all killed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
New rule: a restored IPP job is polled only if its device still points at the printer that issued the CUPS job-id. Otherwise a re-pointed device could read another job with the same id and report it as ours. The restart and store tests are rebuilt on IPP, with every property kept. pcc-node: 1359 passed (the 2 failures predate this). The delta review is pack 79. 🤖 Generated with Claude Code |
What
r31 astra finding 7, which is adk's to fix (steward).
The gateway's own routes show why a 2xx is not a receipt:
200 {"stored": false}when its insert fails (operator-relay.ts);200 {"updated": false}for a job it does not know.Change
ws_client: receipts are read from the body.{"stored": true}.PATCHstatus counts only when the returnedjob.statusis the requested one. A 2xx that did not record it is not forced through the relay, which would bypass what the route declined.{"updated": true}.outbox.StatusOutbox(new): terminal reports (failed,completed) the gateway did not acknowledge are kept on disk.fsync, rename).runningclaim is never queued; it must land before the device is touched.JobExecutor. Every terminal report goes through one reporter that queues on failure, andflush_outbox()retries what is due.execute()now reportscompletedonly when the completion evidence was stored, as the device-reported path already did.daemon. The outbox lives at~/.pcc-node/status-outbox.jsonand is flushed every cycle, guarded likepoll_awaiting.Tests
tests/test_outbox.py, 19 tests:10, 20, 40, 40, 40with a cap of 40;failedandcompleted;completedwithout stored evidence;runningqueued, the executor never queues,completedwithout stored evidence, three receipt checks, the daemon never flushing).Stacking and review
Stacked on #420, then #377. Evidence reviews the settlement-facing semantics (
completedgated on stored evidence). Coord-watch's review is required by rule.🤖 Generated with Claude Code
https://claude.ai/code/session_01PkeiQMnVePeBGU9svhREFy