Skip to content

chore(release): v0.73.2 - #798

Merged
ffroliva merged 8 commits into
mainfrom
chore/release-v0.73.2
Sep 12, 2026
Merged

ffroliva merged 8 commits into
mainfrom
chore/release-v0.73.2

Conversation

@ffroliva

Copy link
Copy Markdown
Owner

Release prep for v0.73.2 — a diagnostics and error-clarity patch.

Cut from develop@15c58af0. Tag v0.73.2 is signed and already pushed, so the release workflow has run; this PR brings the bump commit and the full integration history onto main.

What ships

Explicitly NOT fixed

Migrated login (#791) and #792's i2v dispatch failure are open. The release notes say so plainly rather than letting a migrated user infer otherwise from "auth errors fixed".

Gates

4241 offline tests · uv run pyright src 0 errors · ruff clean · all six CI doc gates · wheel builds at 0.73.2 with no ZIP duplicates · /gflow:doc-review PASS (8 fixes, two of them false claims this release contradicts) · live A/B on the affected surface by a committed e2e test, at zero cost.

Full evidence: docs/LIVE_VERIFICATION_v0.73.2.md.

…ainst a live bar (#784)

v0.73.1 shipped with two items recorded as NOT verified. Both are now measured, and both
turned out to be cheap once the right question was asked.

PREVALENCE. A count of affected users is not obtainable here -- there is no telemetry --
but the gate is, and it is the more durable answer:

    localStorage["glue.CookieNotificationBar"]   on the flow.google.com origin

Not a cookie, which is why no profile carried SOCS and why grepping for a consent cookie
found nothing. Measured by diffing cookies, localStorage and sessionStorage across a
reject click on a throwaway profile: zero cookie changes, one new key.

Three independent observations of the bar, not one:
- a clean browser profile, never signed in: visible 4/4
- pr389fresh2, an authenticated profile this spike had never opened: visible 5/5
- ci-probe: visible 6/6 on the editor, both controls covered

And proven by INTERVENTION rather than correlation. Removing that one key on ci-probe --
touching no cookie, so no auth -- flipped the editor from both controls hit-testable to
both covered by span#glue-cookie-notification-bar-1-label. So the bar is the origin's
DEFAULT state and a stored dismissal is what removes it. ci-probe was not singled out;
every fresh gflow profile meets it on its first Flow load.

THE CURE, AGAINST A LIVE BAR. With the bar restored by that intervention, the ordinary
CLI command on the RELEASED build: cli_version 0.73.1, migrated.editor_ready ->
migrated.cookie_bar_dismissed 86 ms later -> image_settings_applied -> prompt_typed ->
status ok, media 257d8f79, a 651,576-byte ffd8ffe0 JPEG Pillow reads as 1024x1024 RGB.

Still not measured, and said so: the covering geometry on a SECOND account's editor.
denon82 and pr389fresh2 both redirect to /about, so origin-level presence is measured on
three profiles and the occlusion on one.

The live-verification ledger keeps its tag-time text and carries this as an addendum
rather than being rewritten -- what shipped unverified should stay legible.

Refs #780

Claude-Session: https://claude.ai/code/session_01Nr4XuvvZv3PrL3pKEzkrLc
…al width (#785)

TWO INDEPENDENT FINDINGS, both from catching a live occurrence.

1. THE /about RETRY FLAG IS NOW MEASURED.

The 2026-09-10 spike got 0/5 -- the redirect had stopped reproducing on ci-probe -- and
recorded that as unmeasured rather than as transience. It then named exactly what would
settle it: catch a live occurrence and re-run with --attempts 5.

While measuring the #780 consent bar, denon82 turned out to be redirecting. The script
was run UNMODIFIED, so its pre-registered reading still governs:

    5 of 5 landed on /about, over ~3 minutes, 31-35 s each.

The session was healthy throughout (flow_session_cookie_present, not expired, sapisid
present, 68 cookies) and the project is the account's OWN, listed by `gflow project list`
on that same profile. So this is neither an expired session nor a project the account
cannot see.

Per the inherited reading, N/N means stable: a retry is doomed for an account in this
state. `retryable=False` at raise_if_known_landing was a PRESERVED default and both the
code comment and FlowAppError's docstring said so in as many words. Those two statements
became false the moment this ran, and they are corrected here. Behaviour is unchanged --
the flag was already right; it just wasn't known to be.

Still not measured, and said so: WHY the redirect happens, and whether it ever clears.
What is newly excluded is narrow but real: not an expired session, not a missing project,
not transience.

This also explains, rather than merely notes, why the #780 spike could not measure the
covering geometry on a second account's editor: denon82 and pr389fresh2 both land here,
so neither mounts a composer at all.

2. AN AUTH TEST DEPENDED ON TERMINAL WIDTH.

Found while running the suite for (1), and proven NOT to be my change by a control: same
--basetemp, my edits stashed, still fails.

test_auth_steps.py asserts substrings of Rich's output, which hard-wraps to the terminal.
A temp path landing near the wrap column split the word:

    --basetemp=.../t9    -> "profile_e\nxperiments"  -> FAILS
    --basetemp=.../t10b  -> "profile_experiments"    -> passes

Same code, same assertion, one character of path. That is a latent CI flake waiting for a
runner whose tmp path is a different length.

Pinned COLUMNS at the shared CliRunner fixture rather than normalising newlines at each
of the ten assertion sites -- the fixture is the single chokepoint, and fixing the symptom
ten times would let the eleventh one in. Verified across the four path lengths that
flipped it, then the full suite at the path that exposed it: 4234 passed.

Refs #756

Claude-Session: https://claude.ai/code/session_01Nr4XuvvZv3PrL3pKEzkrLc
…g Flow anything (#786)

* spike(756): the /about redirect is decided client-side, without asking Flow anything

#756 measured the redirect and declined to measure its cause; the 2026-09-10 spike could
not investigate one because it had stopped reproducing. It reproduces stably on denon82
right now, and a stable reproducer is the condition a cause becomes measurable in -- so
it was measured while the state was there. ci-probe went from reproducing to not in two
days with nobody watching.

Designed as an A/B rather than an inspection: reading only the failing account shows a
page that redirects and a pile of requests, with nothing to say which is abnormal. Same
capture on denon82 (fails) and ci-probe (opens); the finding is the difference.

                          denon82            ci-probe
  document                200, no Location   200, no Location
  first navigation        146 ms /project    90 ms /project
  second navigation       338 ms /ABOUT      700 ms same /project
  total responses         7                  43
  batchexecute calls      0                  20, first at 275 ms
  non-2xx from Flow       none               none

NOT a server-side redirect: both documents are 200 with no Location, so the hop is the
app deciding client-side 192 ms after its own first navigation commits.

And it decides WITHOUT ASKING FLOW. The failing arm issued zero batchexecute calls --
not "called and was refused", never called. There is no 401/403/404 to point at because
there is no request. The 16 rpcids the control makes are simply absent.

The cross-arm timing is what makes that a finding rather than a race: the control had
already made its FIRST rpc at 275 ms, before the failing arm hopped at 338 ms. Two runs,
so suggestive rather than airtight -- but with zero calls in the 5.5 s after the hop
too, the app is plainly not waiting on a Flow answer.

Account-wide, not project-specific: a second denon82 project redirects identically, 2/2.

Also already in hand: `gflow project list` on denon82 returns the redirecting project.
The backend grants access while the frontend declines to open it.

So the signal is present at page load rather than fetched. NOT established: which signal
-- naming it means reading the document's embedded bootstrap payload, which is exactly
where bearer tokens and account data live on this origin, and it was not read. #756's
warning stands: a lead is not a cause, and no remedy should assert a mechanism this
cannot see. What would settle it is the HAR harness, named in the doc.

No code changes. This narrows the search for whoever picks up #756: it is not a
permissions call being refused, so looking for one is a dead end.

Refs #756

Claude-Session: https://claude.ai/code/session_01Nr4XuvvZv3PrL3pKEzkrLc

* spike(756): classify every response, and prove the hop is client-side properly

CodeRabbit raised two findings on the first cut that could have falsified the headline,
and both were right about the METHOD even though the conclusion survives. Fixed by
re-running with better reporting rather than by arguing.

1. "Not a server-side redirect" did not follow from what was reported.

`page.goto` resolves to the FINAL main resource after following any server redirect, so
a 200 with no Location is equally consistent with a client hop and with a 302 to /about.
`report()` only surfaced non-2xx, so a document 3xx would never have appeared at all.

Now reported, and both discriminate: the document's RESOLVED url is /project/<id>, not
/about (a server redirect resolves to the destination), and there is no 3xx anywhere in
the failing arm. The conclusion holds; it is now supported.

2. "Zero batchexecute calls" is not "no Flow request".

True, and the wording claimed more than the method checked -- the rpcid analysis only
recognised routes carrying `rpcids=`, leaving every other Flow endpoint unexamined by
omission. Every response is now bucketed as flow_batchexecute / flow_other / non_flow.

Classified in full the finding is STRONGER than first stated: the failing arm's only
request to flow.google.com is the document itself. Not zero of one route shape -- zero
of everything else too.

  failing: batchexecute 0, other flow 1 (the document), non-flow 6
  control: batchexecute 20, other flow 15, non-flow 8

3. Status comparison collapsed a multiset to a set, so [200] and [200,200] read as
identical and a call made half as often would have hidden. Compares sorted lists now.

Re-run end to end: identical on every row, ~3.5 hours after the first capture.

Refs #756

Claude-Session: https://claude.ai/code/session_01Nr4XuvvZv3PrL3pKEzkrLc
…erifiable (#790)

* spike(787): Nano Banana 2 Lite is real and usable; its binding is unverifiable

The capability spike the model-governance waiver demanded before this tier ships. PR #787
removes that waiver; this is what it was waiting for.

RUNG 1 CHANGED THE QUESTION, TWICE.

The 2026-08-26 cost/quota catalogue already establishes that image generation costs 0
Flow credits, so "cost" for an image tier means daily quota -- and that "daily quota has
no such oracle. It is observable ONLY on exhaustion, via the 429." So the cost half of
"cost/quality" cannot be answered without deliberately burning an account's day. Recorded
as unanswerable rather than inferred.

It also exposed a sharper question. migrated_composer builds its result with
model_name_type=request.model.value -- it ECHOES our own request. The labs path reads
generated["modelNameType"] from the response; this one does not. And image_records
decodes ids, url, seed, prompt and dimensions with no model field at all. So "it
generated an image with --model nano2-lite" proves the UI did not error, not that the
tier bound.

WHAT WAS MEASURED (4 generations, 0 credits).

The tier is real: HARBOR_SEAL and "Nano Banana 2 Lite" both appear in HTrJv, Flow's own
model catalogue for this account -- corroboration from a surface that is not our request
reflected back.

The quality is fine and is NOT ranked: both arms returned 1024x1024 RGB at 428,799 and
384,036 bytes with no visible degradation, lite slightly more textured. One prompt cannot
rank two tiers and this does not try to. It supports the narrower claim the waiver
needed: lite works and returns a usable image at full resolution.

The binding is UNVERIFIABLE. Both ogiZ0b submit replies were real payloads (~1141 B) and
neither carried any model token. Re-opening the project, HTrJv carries every model token
and no media ids; Zzl0ze carries both media ids and no model tokens. The catalogue lists
models, the media listing lists media, and nothing joins them. So neither #787's live
check nor mine can establish that --model nano2-lite bound rather than silently falling
back. That is the third pre-registered reading, reported as unmeasured.

Surfaced and not about Lite: model_name_type on the migrated path is an unbacked claim on
EVERY migrated image, for every tier. Pre-existing on develop, filed separately.

Refs #787

Claude-Session: https://claude.ai/code/session_01Nr4XuvvZv3PrL3pKEzkrLc

* spike(789): widen the attribution search — the join is absent from a whole project load

"Unverifiable" was challenged as too strong. Fair, and the boundary has moved.

The first pass searched two replies and FILTERED, recording only ones already containing
a media id or a model token. This pass removes the filter: every batchexecute reply on a
project load, 20-21 of them, all checked.

  HTrJv    24,760 B   all six model tokens   no media ids
  Zzl0ze   41,906 B   no model tokens        both media ids
  tRARke   31,883 B   none                   none
  17 more  139-3,964 B  none                 none

No reply carries both. The join is absent from a whole project load, not merely unread on
the two surfaces first checked.

The page was checked too, because a UI that displays it proves the data is reachable. Two
tier labels exist -- span.settings-summary and span.model-select-trigger-content -- but
the first appears ON LOAD, before any interaction, so both are composer picker state
rather than a property of an image.

ONE READING REMAINS OPEN and would flip this: whether a tier label tracks the media you
open. In one run the label read "Nano Banana 2" on load and "Nano Banana 2 Lite" after a
thumbnail click -- either the settings pane showing current state, or the opened media
loading its own settings into the composer, which would be attribution. The two could not
be separated: thumbnail src does not carry the media id, so a per-media A/B could not be
targeted, and a later run matched zero tiles. Named rather than buried.

Two notes that reframe #789: gflow drives the SAME web app a user drives, so no
privileged surface is being missed -- the difference is between HOSTS, since labs REST
returns modelNameType per image and the migrated wire does not appear to. And Flow does
store a model per media for VIDEO on the labs shape, so the concept exists in their data
model, which is why "find the join" is worth keeping open rather than writing off.

No evidence this is regional: region gating changes which models an account is OFFERED --
and this account is offered Lite -- not whether a reply carries a field. Untested from
another region, so no claim either way.

Refs #789

Claude-Session: https://claude.ai/code/session_01Nr4XuvvZv3PrL3pKEzkrLc
A missing .gflow_browser_strategy marker made the Playwright cookie reader
raise SecurityError, which escaped normalization and was flattened into
VERIFICATION_ERROR — whose guidance is 'check network connectivity', for a
file on the user's own disk. It is also the state a failed FIRST login
leaves behind, and VERIFICATION_ERROR is not GOOGLE_SESSION_ONLY, so a
migrated-host fallback keyed on the latter would never run on those
profiles. Adds a distinct PROFILE_MARKER_MISSING outcome, mirrored on the
MCP twin as 409 / retryable=false.

gflow credits raised 'aisandbox-pa authentication failed' with the
remediation 'SAPISID cookie missing, expired, or unreadable' when
labs.google answered 200 with no access_token — the normal shape for a
migrated account. aisandbox-pa had not been contacted and SAPISID was
fine, so the suggested re-login loop could never terminate. The raise now
carries an accurate hint; credits stays labs-only on migrated accounts,
tracked in #795.

Refs #795, #796, #791
…ked (#794)

The migrated composer parked its pooled page on about:blank in a bare
`finally`, so on the FAILURE path it navigated away before
FlowApiClient._capture_incident read the page -- whose own docstring
promises to stage the bundle 'while the page is still alive'. Every
migrated video and image failure shipped tag_counts.div = 0, a white
screenshot and host_category = 'other' beside a network journal proving
the app was alive. Reported in #792 as the driver losing its page handle
on a portrait run; it is neither portrait-specific nor a lost handle --
the reporter's own viewport (1280x720, client.py:495) proves it was the
driven page, captured one navigation too late.

The park is deferred and drained at the top of the next locked body,
before the route decision reads page.url. That placement covers the
post_with_retry path, where a retryable 5xx re-enters the transport
without passing the client's failure boundary -- attempt 2 would
otherwise resume on a still-mounted composer and risk a double submit.

Also removes a 4 s stall: _settle_if_redirecting on about:blank matches
neither short-circuit and waits out URL_SETTLE_TIMEOUT_MS for a pattern a
blank page can never take. The goto/settle ratchet carries the exemption
with its reason.

Verified live on a migrated account at zero cost by a new committed e2e
case: FAILED in 46.3 s on develop, passed in 37.8 s with the fix, with
div 0 -> 25, title 0 -> 81, host_category other -> flow_app.

Refs #792
@coderabbitai

coderabbitai Bot commented Sep 12, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: e45d2e6d-fe81-4fab-89b5-1b80d1096d8c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ffroliva
ffroliva merged commit 25fffae into main Sep 12, 2026
17 checks passed
@ffroliva
ffroliva deleted the chore/release-v0.73.2 branch September 12, 2026 22:54

This branch was previously deployed

1 inactive deployment
pypi — 1e0222c4 Deployed Sep 12, 2026 by ffroliva via build-and-publish #98
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant