Skip to content

symfonic.agent.cutover.continuation_guard_gap

continuation_guard_gap

The fourth door's missing configuration guards, measured (TA8.47).

Split from :mod:~symfonic.agent.cutover.continuation_admission the way arguments left policy and authorised left bundle: that module answers "what did re-driving TA8.46's thirteen rows find?", and this one answers a different question the same re-drive raised -- "does the continuation door refuse a retired configuration the way the three fresh doors do?" -- with a different answer and a different follow-up.

It does not. SymfonicAgent.run, .stream and .stream_typed each run four pre-dispatch configuration guards before they look at a switch: :data:CONTINUATION_CONFIG_GUARDS. SymfonicAgent.resume and .resume_interrupt run none of them; they run only _refuse_retired_arguments, which is why the four argument rows (callbacks, extra_metadata, state_overrides, is_admin) are the only RETIRE/REJECT rows whose refusal survives all four doors.

What that costs, in the inventory's own vocabulary. TA8.26 wrote the rule this is measured against: "a row that merely falls to legacy (default-deny, served silently) is a different fact from a row that refuses by name before dispatch", and RETIRE/REJECT was granted on driven refusal-by-name evidence at the public entry points. On the continuation door the forty-five rows in :data:GUARD_GAP are in the first half, not the second: the redemption falls back to the legacy continuation, which is the body that still reads them. So a deployment carrying a retired setting is told so, by name, when it starts a turn -- and served on the body that honours the setting when it resumes one.

Where the reading is taken, and by which deployment it is reachable. Say it before the consequence, because the two are easy to conflate. All four of :data:CONTINUATION_CONFIG_GUARDS run per invocation inside run, stream and stream_typed, so a deployment carrying one of these forty-five settings cannot complete a fresh turn on this build at all -- and therefore cannot mint a pause of its own. The continuation cell is obtainable only because the harness puts a token in the wallet (tests/agent/cutover/continuation_lane.py, agent._wallet.issued[...]), which is a state this build's own deployment does not reach unaided. The reachable path for the gap is therefore cross-version, and only that: a pause minted by a process whose configuration was still accepted -- one running before these guards landed -- and redeemed by a process that has them. That is a real compatibility case; it is not the same-process case, and this module does not claim the same-process one.

Why this task measured it and did not close it. Adding the four guards to the two continuation doors is a small edit and a large compatibility decision. Under the cross-version reading above, a continuation that began refusing would strand exactly the pauses a not-yet-migrated deployment minted before it upgraded -- in-flight turns whose owner did nothing wrong at mint time and has no way to redeem after. That argument belongs to a task that owns it, with its own migration window and its own evidence. Recording the gap costs nothing and closing it by reflex could cost an adopter their in-flight turns.

Closed by TA8.48, and this module is kept as the diagnosis rather than as a live state. :mod:~symfonic.agent.cutover.continuation_window carries all four guards to resume and resume_interrupt at the same pre-dispatch position, inside a bounded migration window that keeps a pause minted before they landed redeemable. :data:GUARD_GAP is therefore the historical set -- the forty-five rows whose disposition TA8.47 had to withdraw -- and the live claim about them is :data:GUARD_GAP_CLOSED_BY. The set is not deleted because the tests that prove the closure are exactly the tests that need to name what was closed, and a re-drive that found one of these rows unrefused at the fourth door would have no name to report it under.