Repository navigation
fix: close the migrated-host race, and stop character create exiting 1 - #701
Merged
Merged
Conversation
Two fixes, same family: a labs-only path that fails unclassified on the migrated host instead of exiting 36. 1. THE RECAPTCHA RE-CHECK LOST A RACE IT DID NOT NEED TO (#692 residual). #694 re-checks migration when a mint fails, but read page.url ONCE, at the instant of failure. Playwright updates page.url on framenavigated, so a mint that dies WHILE the navigation is committing reads the old host on an account that is in fact migrated -- and the run exits 1 anyway. It now polls for a bounded 1.5s (0.25s interval), which turns "who won this instant" into "did the hop land at all". Error path only: a successful mint never waits, and a genuine labs-side RecaptchaError is delayed by at most the budget on its way to the user. 2. CHARACTER CREATE DIED AS A BARE RuntimeError, EXIT 1, AFTER 20 SECONDS. The character editor is labs-only; flow.google.com renders no prompt textbox for it, ever. So the readiness gate burned its full 20s timeout and raised RuntimeError("Character editor not ready: prompt textbox not visible within 20 s"), which the CLI reports as "Unexpected error". raise_if_migrated now runs after the post-goto settles and before the wait -- after, so the client-side hop has landed; before, so nobody pays 20s to be told something knowable immediately. Both found by dogfooding: producing a real two-character piece on a moved account, where character create is the first thing you reach for. Verified: * 2 new tests, each watched fail first. The character one asserts the guard fires BEFORE the DOM is probed (page.locator must never be called), mirroring the existing TestMigratedOriginFailsFast, and pins exit 36 + non-retryable. * live: `gflow character create` on a moved account now exits 36 with the migration message, was exit 1 after 20s * tests/api 1525 passed; ui_mode 32 passed * ruff / ruff format / pyright src (0 errors) / doc links This is the fourth instance today of the same shape, after #673 (image path), #690 (health_check knew only ".google") and #694 (a handler catching only RecaptchaError). Each is a check that encodes an assumption true on labs. Refs #692, #639
4 tasks done
ffroliva
added a commit
that referenced
this pull request
Sep 6, 2026
The ledger recorded the bug as the fix. Its exit-code table presented `character create` on a moved account exiting 36 as a v0.70.0 improvement, and its not-verified list said CHARACTER entities "cannot run on any account available here". Both were false, from one root: a 20 s readiness timeout was read as proof the feature does not exist on flow.google.com, and #701 then added a guard that aborted before probing the DOM — which made the claim unfalsifiable, because no run could look. #703 disproved it. The editor is fully present on the migrated host, on the same labs tRPC + aisandbox backend; only the view layer differs (React + Slate vs Angular + ProseMirror). Replaces both statements with what was actually run: a complete character on ci-probe — name, personality, voice, portrait and body, two distinct workflow ids, two distinct media ids, both images on disk with different checksums, confirmed by read-back. Plus `--model` determinism over four alternating runs and the orphan-rollback A/B (1 -> 2 entities without the fix, 2 -> 2 with). Records what was NOT verified rather than omitting it: the labs.google path for all seven changed anchors, since every account here is migrated. Also updates the #692 entry with evidence that changes what that issue is. flow.google.com CAN mint a reCAPTCHA Enterprise token: on /project/<id> the enterprise script and site key are present and a mint returns a 2404-char token, while the root grid carries no script at all. The mint runs on the pooled bootstrap page, which after the handoff IS the root grid — which is exactly why the reporter's bundle showed route: "/". So "any mint failure on a migrated page yields exit 36" refuses the host rather than fixing the cause. The probe and its write-up are deliberately not linked from here while they sit on the spike-skill branch: `check_doc_links.py` does not cover this file, so a cross-branch reference would have shipped dangling with every gate green.
ffroliva
added a commit
that referenced
this pull request
Sep 6, 2026
The ledger recorded the bug as the fix. Its exit-code table presented `character create` on a moved account exiting 36 as a v0.70.0 improvement, and its not-verified list said CHARACTER entities "cannot run on any account available here". Both were false, from one root: a 20 s readiness timeout was read as proof the feature does not exist on flow.google.com, and #701 then added a guard that aborted before probing the DOM — which made the claim unfalsifiable, because no run could look. #703 disproved it. The editor is fully present on the migrated host, on the same labs tRPC + aisandbox backend; only the view layer differs (React + Slate vs Angular + ProseMirror). Replaces both statements with what was actually run: a complete character on ci-probe — name, personality, voice, portrait and body, two distinct workflow ids, two distinct media ids, both images on disk with different checksums, confirmed by read-back. Plus `--model` determinism over four alternating runs and the orphan-rollback A/B (1 -> 2 entities without the fix, 2 -> 2 with). Records what was NOT verified rather than omitting it: the labs.google path for all seven changed anchors, since every account here is migrated. Also updates the #692 entry with evidence that changes what that issue is. flow.google.com CAN mint a reCAPTCHA Enterprise token: on /project/<id> the enterprise script and site key are present and a mint returns a 2404-char token, while the root grid carries no script at all. The mint runs on the pooled bootstrap page, which after the handoff IS the root grid — which is exactly why the reporter's bundle showed route: "/". So "any mint failure on a migrated page yields exit 36" refuses the host rather than fixing the cause. The probe and its write-up are deliberately not linked from here while they sit on the spike-skill branch: `check_doc_links.py` does not cover this file, so a cross-branch reference would have shipped dangling with every gate green.
ffroliva
added a commit
that referenced
this pull request
Sep 7, 2026
…ch half only The project owner asked why a five-shot film created two Flow character entities and then anchored every shot on a loose JPEG. The answer was that gflow refuses entity references on this host -- a refusal I had made stricter that morning (#716) and then never questioned, which is the guard-before-probe shape (#701) arriving through the front door of a correct bug fix. MEASURED FIRST, twice, before any code: 1. `@` + the character's name + Enter commits <span class="mention-chip" data-reference-type="entity" data-entity-id="75cd2b39-...">Kael</span> ArrowDown instead of a query commits reference_type="media" -- a JPEG that merely shares the name. Flow lists characters and media under ONE search and does not rank them. 2. A route-aborted submit (nothing generated) carried that entity id in the MZZa6b payload TWICE: as a mention node beside the prompt text, and in its own reference slot at the tail. Mode key `abra_r2v_8s`. So the host takes character references and gflow does not drive them: a porting gap, not a capability gap. WHAT THIS COMMIT ADDS, and it is verified: `MigratedComposer.attach_character_entities` drives that gesture through the existing `_mention_by_name`, then READS THE CHIPS BACK and refuses unless every chip it added is reference_type="entity" AND every requested entity_id is present. `clear=False` lets an r2v run carry media references and characters together without wiping the media mentions, judging only the chips it added. The read-back is the point, not a nicety. Because Flow does not rank the two, a driver that trusts the gesture will eventually bind an image where a character was asked for and produce footage that looks right and drifts on the next cut. The labs path already asserts this on the wire (`_assert_entities_attached`); this is parity. Four tests, each watched red first: entity chips land; a MEDIA chip where a character was asked for is refused; the WRONG entity id is refused even though a chip landed; and the gate still refuses. Also: the Ingredients submode is now selected whenever an entity is attached, whatever mode the caller named, because Flow treats a character reference as reference-to-video -- the captured payload said `abra_r2v_8s`. Evidence-led, and kept, though it did not fix the submit. WHAT DOES NOT WORK, AND WHY THE GATE STAYS SHUT: The submit after a successful attach produces no YhhmEf/eb1hJf/MZZa6b reply in 60s. Three runs: Frames+omni-flash, Ingredients+omni-flash, Ingredients+Flow's own default model. Attach succeeded every time -- the trace even shows Flow fetching the character's voice sample -- and the submit never replied. Cause unknown. I relaxed `_unported_form` while testing and PUT IT BACK. An open gate trades a clear instant exit 36 for a 60-second timeout, which is worse for the user. A test pins the refusal with that reasoning in its docstring so nobody reopens it just because the attach half works. All three runs were undiagnosable because the video path parks the page at about:blank before the incident capture runs (#722), so every bundle reported an empty DOM and a blank screenshot. #722 is a blocker for this issue, not an annoyance: fixing the capture ordering turns "unknown" into "look at the page". The attach code ships unused, deliberately -- proven, tested, and what the eventual fix builds on. Refs #723, #722, #716
ffroliva
added a commit
that referenced
this pull request
Sep 7, 2026
…ch half only The project owner asked why a five-shot film created two Flow character entities and then anchored every shot on a loose JPEG. The answer was that gflow refuses entity references on this host -- a refusal I had made stricter that morning (#716) and then never questioned, which is the guard-before-probe shape (#701) arriving through the front door of a correct bug fix. MEASURED FIRST, twice, before any code: 1. `@` + the character's name + Enter commits <span class="mention-chip" data-reference-type="entity" data-entity-id="75cd2b39-...">Kael</span> ArrowDown instead of a query commits reference_type="media" -- a JPEG that merely shares the name. Flow lists characters and media under ONE search and does not rank them. 2. A route-aborted submit (nothing generated) carried that entity id in the MZZa6b payload TWICE: as a mention node beside the prompt text, and in its own reference slot at the tail. Mode key `abra_r2v_8s`. So the host takes character references and gflow does not drive them: a porting gap, not a capability gap. WHAT THIS COMMIT ADDS, and it is verified: `MigratedComposer.attach_character_entities` drives that gesture through the existing `_mention_by_name`, then READS THE CHIPS BACK and refuses unless every chip it added is reference_type="entity" AND every requested entity_id is present. `clear=False` lets an r2v run carry media references and characters together without wiping the media mentions, judging only the chips it added. The read-back is the point, not a nicety. Because Flow does not rank the two, a driver that trusts the gesture will eventually bind an image where a character was asked for and produce footage that looks right and drifts on the next cut. The labs path already asserts this on the wire (`_assert_entities_attached`); this is parity. Four tests, each watched red first: entity chips land; a MEDIA chip where a character was asked for is refused; the WRONG entity id is refused even though a chip landed; and the gate still refuses. Also: the Ingredients submode is now selected whenever an entity is attached, whatever mode the caller named, because Flow treats a character reference as reference-to-video -- the captured payload said `abra_r2v_8s`. Evidence-led, and kept, though it did not fix the submit. WHAT DOES NOT WORK, AND WHY THE GATE STAYS SHUT: The submit after a successful attach produces no YhhmEf/eb1hJf/MZZa6b reply in 60s. Three runs: Frames+omni-flash, Ingredients+omni-flash, Ingredients+Flow's own default model. Attach succeeded every time -- the trace even shows Flow fetching the character's voice sample -- and the submit never replied. Cause unknown. I relaxed `_unported_form` while testing and PUT IT BACK. An open gate trades a clear instant exit 36 for a 60-second timeout, which is worse for the user. A test pins the refusal with that reasoning in its docstring so nobody reopens it just because the attach half works. All three runs were undiagnosable because the video path parks the page at about:blank before the incident capture runs (#722), so every bundle reported an empty DOM and a blank screenshot. #722 is a blocker for this issue, not an annoyance: fixing the capture ordering turns "unknown" into "look at the page". The attach code ships unused, deliberately -- proven, tested, and what the eventual fix builds on. Refs #723, #722, #716
5 tasks done
ffroliva
added a commit
that referenced
this pull request
Sep 7, 2026
Nine-dimension council on 7c1d4c6 returned 4 GREEN / 5 YELLOW. Every finding below was raised by a reviewer, not by the author. D1/D14 - the retained 2026-07-27 comment still said, present tense, that the EN `span:text-is('Format')` fallback "has to be" the structural anchor, describing an entry the same commit deletes. The block also told the spike doc's story a second time, in the copy that will drift. Cut to the two DOM captures (the evidence) plus four lines, and a link to the finding. D4/D6 - `test_clicks_ligature_selector_first` kept its name and docstring while only its assertion moved to the custom element; the e2e's failure message still told a future drift victim the cascade is "anchored on the personal_recommendations ligature". A label that lies outlives the bug it describes. Renamed, reworded, and the e2e message now names the probe to run. Assertions themselves untouched - only the string a failure prints. D4 - the fall-through test drove only `PROMPT_FORMAT_SELECTORS[-1]`, so the new `<mat-icon>` entry at index 1 was never exercised. Parametrized over the whole cascade. D6 - `is_visible(timeout=1000)` is inert: Playwright documents the argument as ignored, and the call answers from the current DOM without waiting. Dropped, with the reason, so nobody re-derives "a 4-entry cascade costs 4s before the submit". D9 - the CHANGELOG called the capture "DOM read only" while the method it cites does a free `createEntity`, which is a write. Two files, one method, disagreeing. D5 - `flow-locale-leak-icon-ligatures.md` still prescribed the `<i>`-only pattern that caused this: #703 fixed three constants for the carrier split and never updated the memory that teaches it, so the fourth was written the old way on purpose. Corrected, and the split promoted to its own entry. D13/D2 - the spike put its guard in FRONT of its probe: a control miss raised before the dump, the screenshots and the report, deleting the evidence that would correct it. That is the #701 failure this file cites in its own docstring. Both controls now raise from a `finally` that has already written everything. A second control was missing entirely - nothing checked that the typed text landed, so a click intercepted by a consent overlay would have recorded a false absence indistinguishable from a real measurement. The scratch entity is deleted again, and the output path is root-anchored: `.gitignore`'s entry is slash-anchored, so running the spike from `scripts/dev/` wrote an UNIGNORED capture carrying the profile name, live URL, entity ids and authenticated outer_html. D14 - the occlusion probe measured the prompt box, not the button under investigation, and the spike doc filed that under "confirmed, not assumed". Deleted; the live e2e click landing is stronger evidence anyway. Re-verified live on denon82 after the rewrite ($0, 14s, clean exit): control_check count=1 typed_control chars=47 flow-format-prompt-button button -> 1 button:has(mat-icon:text-is('personal_recommendations')) -> 1 button:has(i.google-symbols:text-is(...)) -> 0 button:has(i:text-is(...)) -> 0 Declined, with reason: dropping `button:has(i.google-symbols:...)` as redundant with the broader `button:has(i:...)`. The superset argument is correct, but those are the labs anchors and no unmoved account exists here to re-verify against - narrowing an untested surface to save one line is the wrong trade. Refs #727, #730, #731
ffroliva
added a commit
that referenced
this pull request
Sep 7, 2026
/code-review at high effort, run on the full branch - the first review to see anything past 7c1d4c6. Seven findings; these are the four that are claims rather than behaviour, and a claim is what outlives the code. CHANGELOG (finding 3) sat under "### Fixed" describing the bug as "the image quota spent on a run whose requested prompt-engineering step never happened", which implies that step now happens. It does not. The entry is scoped to what was measured - the anchor - and a new "### Known issues" section states plainly that --format-prompt still submits the prompt as typed. --help (finding 2) promised Flow would rewrite the prompt and named "button not found" as the only failure. That is the one case it gets wrong: the prompt is submitted as typed even when the button IS found and clicked. Now names the real limitation and keeps the best-effort promise it can actually honour. The framing is deliberate and comes from a peer session reviewing the same class of defect. The bug is NOT that the wait is too short - that reads as tuning. It is that gflow reports success without observing it: prompt_formatted is emitted on the click, so the absence of a completion inside a window we chose is recorded as a completion. Same class as a 20s selector timeout read as "the feature is absent" (#701) and a bundle photographed after teardown read as "the DOM was gone" (#722). This one is the worst of the three because it fails toward SUCCESS - a premature green looks exactly like the feature working, and nobody investigates a pass. test_cascade_carries_no_display_label_anchor (finding 6) did not enforce the rule its own docstring stated. It banned the byte sequence 'Format' in single quotes, so it passed for "Format", for [aria-label*="Format"], and for 'Formatar' - the exact string the spike observed on the pt account. Banning spellings loses to translation by construction. Replaced with a whitelist: every text argument in the cascade must BE a known ligature. Proven non-vacuous - accepts all four shipped selectors, rejects all four bad spellings including 'Formatar'. The is_visible comment (finding 7) claimed "a miss is one round trip". True for a miss, false for a click timeout: entries 0 and 1 resolve to the SAME element on migrated, 2 and 3 to the same element on labs, so a timeout is retried on an identical element and the 5s budget is paid twice. Comment now states the real worst case. Not de-duplicating the resolved elements here - that belongs with the observed-rewrite rewrite, not in front of it. NOT fixed here, recorded instead: - finding 1/5, the ~5.4s wait itself. /gflow:predict returned GO with conditions (8.2/10); it is a behaviour change on a paid path and gets its own PR. - finding 4, format_character_prompt resolves .first page-globally while _submit_body_prompt deliberately targets the LAST mounted box. On a two-box cohort the format click lands on the PORTRAIT composer. Real, and I have no two-box cohort to verify a fix against - naming it rather than blind-fixing it. Verified: ruff + format clean, pyright 0 errors, 295 passed, doc links, hygiene (1022 files), website mirror, 49 memory files, PII scan, and the corrected --help renders.
ffroliva
added a commit
that referenced
this pull request
Sep 7, 2026
Fixes the second half of #727, and the half that actually made the flag useless. The anchor fix made the button findable; this makes the click mean something. Flow rewrites SERVER-SIDE. Measured 2026-09-07 on denon82: the click fires a batchexecute round trip and the reshaped text reaches the composer at ~5.4s. The old code waited _jitter_ms(500), logged ui_automation.prompt_formatted and returned True; _send_prompt submits on the very next line. So --format-prompt shipped the prompt the user typed and threw the rewrite away, on every run, with a success event in the log and exit 0. THE DEFECT WAS NOT THE DURATION. It was reporting a success we had not observed: the absence of a completion inside a window we chose, recorded as a completion. Same class as a 20s selector timeout read as "the feature is absent" (#701) and a bundle captured after teardown read as "the DOM was gone" (#722) - a measurement that could not be taken, recorded as a measurement. This one is the worst of the three because it fails toward SUCCESS: a premature green looks exactly like the feature working, so nobody investigates it. Framing owed to the peer session that named the class while reviewing the same shape in its own judge. format_character_prompt now polls the bound composer until its text actually changes, and True means observed-rewrite, not button-pressed. Design per /gflow:predict (GO with conditions, 8.2/10, five personas): - Gate on the DOM, NOT on the wire. eAenfb's response arrives 4.2s BEFORE the text settles, so awaiting it reproduces the same early submit - and an undocumented batchexecute rpcid is not an anchor worth depending on. Comparing against the string we inserted never reads a display label, so it is locale-invariant by construction. - Length delta, not `!=`: Slate/ProseMirror normalise whitespace and bare inequality would fire on churn, re-reporting the false success with better telemetry. Normalisation moves a handful of chars; the observed rewrite moved 632 (19 -> 651). Threshold 16. - Baseline is a PARAMETER, not instance state - holding it on self would couple it across generations on a reused page. Both callers already had the string. - Existing settle-gate idiom (monotonic deadline + jittered poll), not a new helper; module constants, no env var nobody would set. - Still best-effort, still `-> bool`: promoting a miss to an exception would land in EXIT_CODE_MAP, the MCP schema and the docs at once, and the body-prompt call site runs AFTER the face image already spent quota, so failing there aborts a saga mid-spend. - 30s budget, generous on purpose. Costs are asymmetric: too long is bounded latency on an opt-in flag whose expiry falls back to exactly the old behaviour; too short silently reintroduces the bug. elapsed_ms is logged on every success so a real distribution accrues instead of being fitted to n=1. - Lengths and a stable hash only, never the text. Flow ELABORATES a terse description into a detailed physical one, so the rewrite is more PII-dense than the input, and structlog is not governed by GFLOW_CLI_HISTORY_PROMPTS. Pinned by test_never_logs_the_prompt_text. Also fixes /code-review finding 4, which no other reviewer caught: the button was resolved page-globally with .first while _locate_body_prompt_box documents "the LAST mounted box is the target - never the portrait's". On a two-box cohort the format click landed on the PORTRAIT composer, reshaping the wrong prompt or finding a disabled button and giving up. _locate_format_button now mirrors that existing convention instead of inventing a second one. And a disabled match `continue`s rather than `return False` - one stale anchor used to kill every selector behind it, including the one that would have worked. Events: prompt_formatted now means the observed rewrite (it always claimed to); format_button_clicked carries the old semantics; format_not_observed reports a timeout without claiming Flow failed - an unchanged box also covers Flow declining, erroring, or judging the prompt already formatted, none of which were provoked, so none are claimed. Five tests written first and watched failing: TypeError: format_character_prompt() got an unexpected keyword argument 'prompt_box' Then 912 passed in tests/api/transports/, pyright 0 errors, ruff clean. An autouse fixture shrinks the 30s budget for the offline suite - wait_for_timeout is mocked, so the loop would otherwise spin on wall-clock for a full minute across two never-lands cases. Still owed and not claimed: a live run proving the gate on real Flow.
tranzmatt
pushed a commit
to tranzmatt/gflow-cli-unofficial
that referenced
this pull request
Sep 9, 2026
gflow-cli drives a product it does not own, so every claim about what Flow
does is either measured or guessed — and a guess written into code or docs
becomes a fact nobody re-checks. The harness for measuring existed but was
not routed to, so it was not used.
Adds `skills/spike/SKILL.md` (canonical, agent-agnostic) plus its
`/gflow:spike` wrapper, and wires it in where it actually bites:
* AGENTS.md Skill Routing — first row, because it gates the others
* the Standard Workflow Sequence gains Phase 0 (Recon) before Triage
* `issue-assessment` gets a "Before you classify an absence" section: a
verdict of INVALID / "not supported on this host" is a claim about the
live product, and that skill is read-only, so it cannot produce one from
the code alone
The rule it enforces:
A selector that does not match is evidence about the SELECTOR.
It is never evidence about the FEATURE.
Never put a guard in front of a probe.
Both halves are written from the same failure. "character create is a
labs-only surface, it renders no prompt textbox EVER" came from one 20 s
selector timeout and reached four places — a code comment, the CHANGELOG,
the v0.70.0 release ledger, and a test class NAME. ffroliva#701 then added a guard
that aborted before probing the DOM, which made the claim unfalsifiable:
no run could look. The feature had worked the whole time.
Also records the recon itself, including a result that is new and not yet
acted on: flow.google.com CAN mint a reCAPTCHA Enterprise token. On
/project/<id> the enterprise script and site key are present and a mint
returns a 2404-char token; on the root grid there is no script at all. The
mint guard refuses the host when the pooled page is simply on the wrong
route — the mechanism behind ffroliva#692, whose reporter's bundle showed
route: "/". Deliberately NOT claimed: that fixing the mint unblocks
`gflow image`. That path is UI-driven and the migrated project composer
inventories as video-only, so the mint is necessary, not sufficient.
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.
Summary
Two fixes, same family: a labs-only path that fails unclassified on the migrated host instead of exiting 36.
1. The reCAPTCHA re-check lost a race it did not need to (#692 residual)
#694 re-checks migration when a mint fails, but read
page.urlonce, at the instant of failure. Playwright updatespage.urlonframenavigated, so a mint that dies while the navigation is committing reads the old host on an account that is in fact migrated — and the run exits 1 anyway.It now polls for a bounded 1.5 s (0.25 s interval), turning "who won this instant" into "did the hop land at all". Error path only: a successful mint never waits.
2.
character createdied as a bareRuntimeError, exit 1, after 20 secondsThe character editor is labs-only —
flow.google.comrenders no prompt textbox for it, ever. The readiness gate burned its full 20 s timeout, then raisedRuntimeError("Character editor not ready: prompt textbox not visible within 20 s"), which the CLI reports as "Unexpected error".raise_if_migratednow runs after the post-gotosettles (so the client-side hop has landed) and before the wait (so nobody pays 20 s to be told something knowable immediately).How both were found
Dogfooding. Producing a real two-character piece on a moved account, where
character createis the first thing you reach for.Fourth instance of one shape
health_checkmatched only".google"RecaptchaErrorEach is a check encoding an assumption that was true on
labs.google.Test plan
page.locatormust never be called), mirroring the existingTestMigratedOriginFailsFast, and pins exit 36 + non-retryablegflow character createon a moved account → exit 36 with the migration message (was exit 1 after 20 s)tests/api— 1525 passed;test_ui_mode.py— 32 passedruff check/ruff format --check/uv run pyright src(0 errors) / doc linksMCP parity
No MCP surface. Both changes are inside the transport/client error-classification paths, below both doors. The MCP tools reach the identical code and inherit the corrected exit class automatically. No leaf, option, payload key or docstring changes.
Refs #692, #639