Repository navigation
item 6d: Control grows a resume path, and the gateway holds elicitations - #11
Merged
Merged
Conversation
SPEC-v0.2 §6.9 amended, §11 and ARCHITECTURE §6 with it. A held reservation spans two HTTP requests and therefore two Control calls, and Control.execute writes a terminal outcome for any executor exception — so the gateway could not express "suspend, record nothing" through it. The kernel grows one signal and one method rather than the gateway growing a second reservation path. Suspension is modelled the way v0.1 models "nothing happened": an explicit opt-in an executor raises, never a default and never inferred. Suspended is handled above the generic branch, alongside NotExecuted. The record stays EXECUTING, the lease is extended, the continuation is held in the same transaction, EXECUTION_SUSPENDED is appended, no receipt is written — the same rule as an action awaiting a human — and the signal propagates so the gateway can relay the elicitation. Suspended with no effect key holds nothing and the event says so. Suspension without an effect key gives no protection at all, exactly as retries without one give none; it is logged rather than silently pretended. Control.resume consumes the continuation atomically, rehydrates the action from the store — which is what lets a process that restarted mid-round still finish one — and runs the executor through the SAME outcome mapping execute uses. That path is now one private function called by both. A resumption that decided outcomes differently would be a second implementation of the only question this library exists to answer. resume re-evaluates the policy for the receipt, not to re-decide: the action was decided and reserved on the first leg, and refusing at resume would strand a reservation the remote may already be acting on. The gateway maps input_required to Suspended(requestState), a follow-up carrying requestState to Control.resume, and an elicitation with no requestState to §6.9.3's floor. Both of §6.9.2's bounds are enforced: the lease via suspend_timeout, the rounds via continuation_rounds. Control remains the only module that composes the others; the gateway calls it. ARCHITECTURE §6 says so explicitly now.
Mutation-testing §6.9 found the same pattern this codebase keeps producing: a resolved record and a lapsed lease both refused a resumption with the same exception and wrote nothing, so removing either changed nothing a caller or a test could see. One branch now, naming the state it found; the tests distinguish the two by message. The harness had the ambiguous-anchor bug again — `except sqlite3.IntegrityError` appears in put_approval_request too, and replace(..., 1) hit that one. And compare_digest vs == is an equivalent mutant for correctness, so M21 now mutates the matching itself rather than the comparison primitive.
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.
Your §6.9 decision, implemented. SPEC-v0.2 §6.9 amended, §11 and ARCHITECTURE §6 with it. Item 6 is now complete — T26 and T30b land here.
Suspension is modelled the way v0.1 models "nothing happened"
An explicit opt-in an executor raises, never a default and never inferred.
Suspended(continuation)is handled inControl.executebefore the generic branch, alongsideNotExecuted. The record staysEXECUTING, the lease is extended byControl(suspend_timeout=…), the continuation is held viahold_continuationin the same transaction,EXECUTION_SUSPENDEDis appended, no receipt is written — the same rule as an action awaiting a human — and the signal propagates so the gateway can relay the elicitation.With no effect key it holds nothing, and the event records
held: false. Suspension without an effect key gives no protection at all — a concurrent caller can start the same work — exactly as retries without one give none. Logged, not silently pretended.Control.resume(continuation, executor)consumes the continuation atomically, rehydrates the action from the store (which is what lets a process that restarted mid-round still finish one), checks the record is stillEXECUTINGunder a live lease, appendsEXECUTION_RESUMED, and runs the executor through the same outcome mappingexecuteuses. That path is now one private function called by both. A resumption that decided outcomes differently would be a second implementation of the only question this library exists to answer.resumere-evaluates the policy for the receipt, not to re-decide: the action was decided and reserved on the first leg, and refusing at resume would strand a reservation the remote may already be acting on. Marked in the spec.Controlremains the only module that composes the others. ARCHITECTURE §6 now says so about the gateway explicitly, and says why: a gateway that owned the reservation would be a second implementation of v0.1 §5.5's asymmetry, the one rule here that must not drift.The gateway half
input_required→Suspended(requestState); a follow-up carryingrequestState→Control.resume; an elicitation with norequestState→ §6.9.3's floor (AMBIGUOUS, and the continuation refused with-41005). Both of §6.9.2's bounds enforced: the lease viasuspend_timeout, the rounds viacontinuation_rounds.A concurrent duplicate during an outstanding elicitation is refused with
-41004 in_progress— the guarantee that actually matters across a round trip.Mutation table — 30 mutations, store half and gateway half together
held:false, bytes, non-emptyresume: same mapping, same attempt,EXECUTION_RESUMED, consumed before the executor runsSuspended, §6.9.3's floor,resumerouting,-41009, the round bound, the metaid:stripped, and onlyid:Three started green. Two were harness bugs of kinds already seen — the ambiguous-anchor problem (
except sqlite3.IntegrityErrorappears input_approval_requesttoo, andreplace(…, 1)hit that one), and an equivalent mutant:hmac.compare_digestvs==differ in timing, not correctness, so no behavioural test can separate them. M21 now mutates the matching itself.The third was real, and it is the same defect this codebase keeps producing: a resolved record and a lapsed lease both refused a resumption with the same exception and wrote nothing, so removing either guard changed nothing visible. One branch now, naming the state it found.
That's items 2, 5, 6a, 6b and 6d. I'd write it into
CLAUDE.mdas its own rule when you next touch it — the existing "defence-in-depth hides mutations" bullet describes the symptom, but the specific form here is narrower and mechanical: a guard that can only fire when a later guard would also fire, with the same observable result, is documentation — and a test asserting only status/type/code cannot tell you which one you wrote.Verification
1162 tests pass (1098 → 1162),
mypy --strict src/,ruff checkandruff format --checkclean.Every §10 acceptance test now exists except T27/T28 (item 7), T29 and T30's named test (item 8).