Repository navigation
scripts: wait out the gateway's transient refusals in the recovery harness (P1R-C06) - #114
Merged
Merged
Conversation
…rness (P1R-C06) The full recovery run on the qualification host failed on three timing races in the harness, none of them in the product: - A load interval's termination sample was refused with RECOVERY_PENDING: it was sent about 70 ms after a restart, while the new gateway still reconciled `term`, and a missing termination sample fails the interval's p99. - The reconcile `approval` scenario held its next execution before the new gateway had reconciled `confirm`, since that gateway answered both resumes from its record at once. - The `kill-during-terminate` scenario started new work right after the gateway reported the terminated process exited, and was refused with WORKSPACE_BUSY: the Runner reports a process exited once it has reaped it, and the gateway frees the workspace only when the Runner's exit event follows, once the process's output has closed. The harness now sends new work again while it meets one of these refusals: - once_free waits through RECOVERY_PENDING and WORKSPACE_BUSY, for new work in a workspace whose process the gateway held (reconcile `running` and `kill-during-terminate`, observation `retention`). - The store's `writer-lock` and `schema` scenarios and the `approval` scenario's held execution wait for the reconciliation (once_reconciled). - A termination's target and a refusal's execution in the measurement are sent again under a new tag while the serving gateway refuses them with RECOVERY_PENDING, for up to 10 seconds; a target still refused then is an error, and each sample records the refusals it met. The protocol states both rules in English and Korean. A scenario that expects one of these refusals still checks it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This was referenced Oct 18, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Issue
Part of #101: P1R-C06. The full recovery run on the qualification host failed on three timing races in the harness, so this fixes the harness and its protocol text before the candidate is frozen again. It follows #113 and does not close an issue.
mainat41f83b6(the merge of #113)p1r-c06-recovery-2026-10-17-41f83b6on the qualification host,MANIFEST.jsonSHA-2561b2b57fbea66ae3a08b78dcd76d86f992dc4709a21e22a51343b97cf0f0b22a2;verifyaccepts it (recorded and recomputedfailed). Its candidate (37ed6aa3…) passed and its rehearsal (aad4c41e…) was inconclusive as designed6e7e065; both unchangedSummary
Before: right after a gateway restart, the harness sometimes sent new work a few milliseconds too early and counted CodeSpace's documented answer as a failure. In the full run, which otherwise passed (53 of 55 functional scenarios; 5 of 6 intervals; every pressure sample normal):
termworkspace. It was refused withRECOVERY_PENDINGand recorded as an error, and with fewer than 100 samples one error fails the termination p99.recovery-reconcile required approvalheld the executions of its 80 ms and 160 ms rounds about 12 ms after the eighth gateway started, before it had reconciledconfirm: that gateway had answered the previous round's two resumes from its record at once. Both were refused withRECOVERY_PENDING, so those two rounds never ran.recovery-reconcile required pipe kill-during-terminatestarted its next round, and then its fresh command, within 0.6 ms ofprocess_statusreporting the terminated process exited. Both were refused withWORKSPACE_BUSY: the Runner reports a process exited once it has reaped it, and the gateway frees the workspace only when the Runner's exit event follows, once the process's output has closed.No command ran twice and no refused command ran.
After: where the harness needs new work started, it sends it again while it meets one of these two refusals, for a bounded time, and the protocol says so. A scenario whose row expects one of these refusals still checks it, and a refusal that lasts is still a failure.
How.
once_free(scripts/qualify-devguard.py) sends new work again while it is refused withRECOVERY_PENDINGorWORKSPACE_BUSY, for up to 30 seconds.once_reconciledkeeps its meaning (onlyRECOVERY_PENDING) and now takes the codes to wait through.runningandkill-during-terminateand observationretentionstart new work throughonce_freewhere a process just exited; storewriter-lockandschemaand theapprovalscenario's held execution wait for the reconciliation throughonce_reconciled.kill-during-terminaterecords the refusals each round's start met.RECOVERY_PENDING, for up to 10 seconds (RECONCILE_WAIT_S). A target still refused then is an error, as before; a capacity refusal is still not a sample. Each row records the refusals it met (pending_refusals), and every attempt stays inexecs.jsonl, whose markers check still finds a refused command that ran.docs/recovery-qualification.mdand the Korean page): the functional suites state both refusals and the rule, and the measurement states the samples' wait. The registry hashes are recorded.The protocol freezes the harness with it, and a change after the first run is a new protocol, reviewed like any other change, whose affected runs are repeated: on this merge the candidate is frozen again and the whole
recoveryrun is repeated.Contract changes
None. The product is unchanged; only the harness, its tests and the protocol text change. Termination and refusal sample rows gain
pending_refusals, andkill-during-terminaterounds gainstart_refusalsandstart_waited_ms;verifyrecomputes the same metrics from them.A product note for the report:
process_statusreports a process exited once the Runner has reaped it, a little before the gateway frees its workspace on the Runner's exit event. The server's instructions for the independent lifetime say that new work in the workspace of a process found running "returns WORKSPACE_BUSY until the process exits or is terminated", which reads as the same moment. On the host both refusals came within 0.6 ms of the exit report.Tests
Verified here at the head of this branch, in a Linux cloud container, with debug builds of
codespace-mcpandcodespace-codex-runtime(the product is unchanged sinceb2cb042):python3 -B -m unittest discover -s scripts/tests: 124 passed, 2 of them new.once_reconciledwaits for the reconciliation only, whileonce_freealso waits while the workspace is still held; both answer at once to any other refusal, and at their limit to one that lasts. A termination target refused withRECOVERY_PENDINGis sent again under a new tag and timed once it started; one refused for capacity is not a sample and is not sent again; one still refused when the wait ends is an error. The refusal sample times the authority's refusal, not the reconciliation's.scripts/check_docs.py, and the docs site's test, build and check as the docs workflow runs them.recovery --rehearsal --participation offunder a soft limit of 256 (ulimit -Sn 256): functional 34 passed, 2 gaps not run; idle passed; load inconclusive by design here ("no load ran: there is no DevGuard service to run it"); the runinconclusive(exit 2),verifyverified; at most 43 open descriptors. It ran atda8383a, whose harness and tests are this head's (one Korean word of the protocol differs);MANIFEST.jsonSHA-2564cc876606194596aa9aaa3df1ad75922bbc247619b20e881ffc431fa80a4b78b.offgateway reconciles within a few milliseconds. No scenario or sample of that rehearsal met either refusal (each records the refusals it waited through, all 0). The unfixed harness at the protocol's durations (oneoffrepetition: functional, a 10-minute idle and a 30-minute load interval, 105 restarts) met neither refusal either: functional 34 passed, idle passed, load inconclusive by design,verifyverified.recovery-reconcileandrecovery-storepassed twice with each harness. Gateways then did refuse some requests withRECOVERY_PENDING, but only where the unfixed harness already waited (thekill-during-execprobes and theapprovalresumes).Not verified here:
required, which runs only on macOS, where reconciling asks the authority and took about 70 ms under load; and the measurement against the installed service. On the merge, the qualification host builds the mergedmain, and the owner's paste runs the five checks, freezes the candidate, and repeats the rehearsal and the full run.Security scenarios
Out of scope