Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- **On `flow.google.com`, an agent-only composer now exits `25` (`FlowAgentUiError`) where
it previously exited `23` (`UiSelectorDriftError`)** — and reports `retryable: false`
rather than inheriting that class's retryable default. Scripts branching on `23` for
this failure must add `25`. Nothing else moved: a trigger missing from the DOM is still
exit 23. See the `### Fixed` entry below for why
([#799](https://github.com/ffroliva/gflow-cli/issues/799)).

### Fixed

- **Migrated-host i2v: the Frames picker is confirmed when it does not commit on the
Expand Down Expand Up @@ -61,6 +70,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
`credits` itself is still unavailable on migrated accounts; the balance surface for that
cohort has not been located. #795 stays open for it.

- **The migrated agent-only composer is named before submit instead of reported as
selector drift (#799).** Google has put some accounts on a `flow.google.com` composer
that has **no classic arm at all** — the prompt box is the agent panel, and aspect,
model and count live in Agent settings as defaults rather than per-request controls.
gflow waited 30 s for a control that is structurally absent and raised
`UiSelectorDriftError` (exit 23), which reads as *our* frontend bug and invites a retry
that cannot work.

It now raises `FlowAgentUiError` — **exit 25, `retryable: false`** — naming the cohort
and saying plainly that no flag or profile change helps. The discriminator is the chip:
the DOM is identical to #749's recoverable agent mode (settings trigger present under a
bare `hidden`), and what separates them is that a recoverable account still has a
`button.agent-mode-chip` to turn off while this one has none. A trigger that has left
the DOM entirely is unchanged — that is a renamed selector, our bug, and still drift.

`retryable` moved from `FlowAppError` to the error base to make that possible — its own
comment set the condition, *"move it up if, and only if, a second class needs it"*, and
this is the second. No existing error's retryability changed.

Reported with the DOM evidence that made it diagnosable by **@Cstanish127**. gflow-cli
still has no driver for that composer; #799 stays open for it.
## [0.73.2] — 2026-09-12

### Fixed
Expand Down
34 changes: 34 additions & 0 deletions KNOWN_ISSUES.md
Original file line number Diff line number Diff line change
Expand Up @@ -1247,6 +1247,40 @@ continue as a seeded I2V generation — tracked as backlog.

## Mitigated

### Migrated accounts can have an agent-only composer, which gflow cannot drive

Some accounts on `flow.google.com` are served a composer with **no classic arm at all**:
the only prompt box is the agent panel, there is no `agent-mode-chip` to turn off, and
aspect / model / count are Agent-settings **defaults** rather than per-request controls.
Every control the migrated driver reaches for is structurally absent, so `gflow video`
and `gflow image` cannot run there at all.

**Mitigation (v0.74.0):** the state is named before submit — `FlowAgentUiError`, exit 25,
`retryable: false`, $0 spent — instead of the generic `UiSelectorDriftError` (exit 23)
that reads as a gflow frontend bug and invites a doomed retry. The discriminator is the
chip, because the DOM is otherwise identical to the *recoverable* agent mode of
[#749](https://github.com/ffroliva/gflow-cli/issues/749): a hidden settings trigger **with**
a chip is recoverable and gflow turns it back itself; a hidden trigger with **no chip
anywhere** is this cohort.

**No workaround inside gflow.** No flag, `--ui-mode`, or profile change reaches it — the
composer is a property of the Google account. Generating from the Flow web UI still works.
A driver for the agent panel is not implemented;
[#799](https://github.com/ffroliva/gflow-cli/issues/799) stays open for it.

**Prevalence is unmeasured.** One reporter, one account (Windows 11, ru locale), whose DOM
capture is what made this diagnosable. No account available to the maintainers is in this
cohort, so the **positive** case is verified against the reporter's captured markup driven
by a real Chromium (`tests/api/transports/test_agent_only_composer.py`) and **not** against
live Flow — that needs an account in the cohort and is the named blocker on #799.

The **negative** controls were measured live at $0 on 2026-09-13, which is what makes the
discriminator more than a guess: a healthy migrated composer carries
`button.agent-mode-chip` **present and un-pressed** with its settings trigger visible. So a
migrated account normally has a chip, and having none is the anomaly this keys on.

---

### Flow can pin the agentic cohort server-side for hours

The classic↔agentic editor arm is server-assigned per page load and normally
Expand Down
31 changes: 22 additions & 9 deletions docs/MCP.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,15 +141,19 @@ marks a transient failure a scheduler can re-run without operator intervention
WAF/reCAPTCHA bounce (`WafRejectionError`), rate-limit (`RateLimitError`),
transport timeout (`TransportTimeoutError`), network blip (`NetworkError`), a
dropped browser session (`BrowserSessionClosedError`), a Flow web-app crash
(`FlowAppError`), an agentic-cohort flap (`FlowAgentUiError`), an unreachable
(`FlowAppError`), an agentic-cohort flap (`FlowAgentUiError` — but see the
agent-only exception in § 6.1, which overrides it to `false`), an unreachable
UI arm (`UiModeUnavailableError`), and a partially-completed sync
(`SyncPartialError`). That list is `errors.RETRYABLE_ERRORS`, but it is no longer
the whole answer: `errors.is_retryable` consults the **instance** first, so a raise
site can override its class. One does today — Flow's `/about` redirect raises
site can override its class. Two do today — Flow's `/about` redirect raises
`FlowAppError` with `retryable: false`, and since 2026-09-11 that is a **measurement**, not a
preserved default: caught during a live occurrence, 5/5 consecutive attempts over ~3 minutes
landed on `/about` again, on the account's own project with a healthy session — so a retry is
doomed and costs ~35 s each ([#756](https://github.com/ffroliva/gflow-cli/issues/756)). The
second is the migrated **agent-only composer**, which raises `FlowAgentUiError` with
`retryable: false` because which composer an account gets is server-assigned and does not
flap ([#799](https://github.com/ffroliva/gflow-cli/issues/799)). The
*cause*, and whether it ever clears, remain unmeasured.
Read the flag off the envelope; never re-derive it from the class list.
Everything
Expand Down Expand Up @@ -350,8 +354,9 @@ gflow credits user # does the Bearer path still work for this account?
| verified | fails, `"credits endpoint returned 401"` | migrated; a token exists but aisandbox-pa rejects it | Same outcome, different raise site. Since v0.74.0 this one names the cause too ([#795](https://github.com/ffroliva/gflow-cli/issues/795)); through v0.73.2 it read `"aisandbox-pa returned 401 after token refresh"`. Measured on a migrated account, 2026-09-13 |
| "Signed in to Google, but not to the Flow app" **forever** | — | migrated; labs no longer mints a Flow session at all | Login cannot complete on the released build. Tracked in [#791](https://github.com/ffroliva/gflow-cli/issues/791) |

In every migrated row, **generation over the migrated composer still works** — only the
aisandbox REST reads fail.
In every migrated row above, **generation over the migrated composer still works** — only
the aisandbox REST reads fail. The one exception is the agent-only composer described
below, where there is no composer for gflow to drive at all.

> **A `credits` failure does not mean your cookies are stale.** Two raise sites produce it
> — labs answering with no token, and aisandbox-pa rejecting the token labs did issue —
Expand All @@ -362,11 +367,19 @@ aisandbox REST reads fail.
> believe `auth status`.** Through v0.73.2 the second site carried the class-default
> SAPISID advice instead — see the CHANGELOG for why.

The migrated composer itself also comes in more than one shape. If `gflow_generate_video`
fails pre-submit with a selector-drift envelope naming `.settings-trigger-button` as
present-but-hidden, and no `agent-mode-chip` exists on the page, the account is on the
**agent-only composer**, which has no classic composer at all — there is nothing to drive
yet ([#799](https://github.com/ffroliva/gflow-cli/issues/799)).
The migrated composer itself also comes in more than one shape. Since v0.74.0 an account
whose composer is **agent-only** — no classic composer at all — is named as such before
submit: `FlowAgentUiError`, **exit-25-equivalent**, `retryable: false`, saying the settings trigger is
present but hidden and that no `agent-mode-chip` exists to turn off. Through v0.73.2 that
same account got a generic selector-drift envelope (exit 23), which reads as *our* bug and
invites a retry that cannot work ([#799](https://github.com/ffroliva/gflow-cli/issues/799)).

The distinction that matters, because the DOM is identical either way: a **pressed chip**
means the classic arm exists and gflow turns it back on itself
([#749](https://github.com/ffroliva/gflow-cli/issues/749)); **no chip at all** means there
is no classic arm to return to, aspect / model / count are Agent-settings defaults rather
than per-request controls, and no flag or profile change helps. Generating from the Flow
web UI still works.

### 6.2 Envelope → cause → what to do

Expand Down
2 changes: 1 addition & 1 deletion docs/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1789,7 +1789,7 @@ shell scripts can branch on the failure mode without parsing stderr.
| `22` | `UpscaleUnavailableError` | 4K upscale is gated to Flow **Ultra** accounts (HTTP 403) | Use `--scale 2k`, or upgrade the Flow plan |
| `23` | `UiSelectorDriftError` | A Flow editor control could not be located — Google changed the frontend (issues #183, #493) — **or** a blocking announcement overlay survived dismissal, so no control below it can be clicked (probe `overlay_close_button`, #593). On the migrated host a missing submit control is checked against the wallet first and reported as **37** when Flow swapped in its insufficient-credits warning, so a short balance no longer arrives here | Update gflow-cli; file a bug with the probe name + the diagnostics JSON / debug screenshot referenced in the error message, plus the incident bundle's `report.md` when one was written. For `overlay_close_button`: open the project once in Chrome and dismiss the announcement — the dismissal persists on your account |
| `24` | `BrowserEngineUnavailableError` | `GFLOW_CLI_BROWSER_ENGINE=patchright` but the engine is not installed | `pip install 'gflow-cli[patchright]'`, or unset `GFLOW_CLI_BROWSER_ENGINE` |
| `25` | `FlowAgentUiError` | The profile is on Flow's Agentic UI cohort and the classic media panel is unrecoverable for this operation | Rare since v0.38.0 (#332): the mode controller reliably recovers agentic→classic, so first retry with `--ui-mode classic`; if it persists, see KNOWN_ISSUES on the agentic cohort |
| `25` | `FlowAgentUiError` | The profile is on Flow's Agentic UI cohort and the classic media panel is unrecoverable for this operation — **or**, on `flow.google.com`, the account's composer is agent-only and there is no classic arm at all ([#799](https://github.com/ffroliva/gflow-cli/issues/799)) | On labs this is rare since v0.38.0 (#332): the mode controller reliably recovers agentic→classic, so first retry with `--ui-mode classic`. The migrated agent-only cohort reports `retryable: false` — no flag or profile change helps; use the Flow web UI. See KNOWN_ISSUES |
| `26` | `MediaAttributionError` | Generated media could not be reliably attributed to this request (issue #281) | Re-run; a dedicated project with fewer pre-existing assets avoids the ambiguity |
| `27` | `MediaUploadRejectedError` | Flow's upload endpoint refused the input file (`uploadImage` 4xx, issue #287) | Re-encode the image (`ffmpeg -q:v 2 -map_metadata -1`), or reference the asset by its media UUID |
| `28` | `UiModeUnavailableError` | The Flow UI arm this command required (`--ui-mode`/`GFLOW_CLI_UI_MODE`; `-i` forces agentic for images; **video always requires classic** — no agentic video driver exists) couldn't be reached after a switch attempt; aborted before submitting — no credits spent (issue #299) | Retry (the cohort flaps per load); try another `--profile`; for images you can also relax `GFLOW_CLI_UI_MODE` — for video there is nothing to relax |
Expand Down
1 change: 1 addition & 0 deletions skills/gflow-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ Documented errors agents commonly make — negative examples for the SkillOpt tr
| Python: `client = FlowApiClient(...)` then method calls | Must use `async with FlowApiClient(...) as client:` — it's an async context manager |
| Python: `from gflow_cli import FlowApiClient` | `from gflow_cli.api.client import FlowApiClient` |
| `gflow video t2v`/`i2v`/`r2v` without `--project` on an account Google moved to `flow.google.com` (exit 11), or an unported UUID/entity reference or model (exit 36) | Pass `--project <id>` — migrated hosts support video t2v, local-file i2v/r2v, and image t2i/i2i (local refs) only; exit 36 is non-retryable, `GFLOW_CLI_FLOW_HOST=labs.google` is the kill switch (see USAGE § gflow video t2v / i2v / r2v and image sections) |
| Telling a user on `flow.google.com` whose run exits **25** to retry, switch profile or pass `--ui-mode classic` | Their account's composer is **agent-only** — no classic arm exists, so aspect/model/count are Agent-settings defaults and gflow has no driver for it (`retryable: false`, $0, pre-submit). Nothing in gflow reaches it; the Flow web UI still works ([#799](https://github.com/ffroliva/gflow-cli/issues/799)). Exit 25 on `labs.google` IS the retryable A/B cohort — check the host before advising |
| Suggesting a native `batch` subcommand under `gflow video` | It doesn't exist — that stub never worked and was removed. Loop `gflow video t2v`/`i2v` from the shell for multi-clip runs (`gflow image batch manifest.tsv\|json` is the real, working batch command, but it's image-only) |

## Disclaimer
Expand Down
67 changes: 67 additions & 0 deletions src/gflow_cli/api/transports/migrated_composer.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
)
from gflow_cli.errors import (
ConfigurationError,
FlowAgentUiError,
FlowHostMigratedError,
InsufficientCreditsError,
MediaUploadRejectedError,
Expand Down Expand Up @@ -114,6 +115,11 @@
#: Accounts, measurements and the transition inventory:
#: docs/superpowers/spikes/2026-09-08-migrated-composer-agent-mode-hides-settings.md
AGENT_MODE_CHIP = "button.agent-mode-chip[aria-pressed='true']"
#: The same chip in ANY state. :data:`AGENT_MODE_CHIP` matches only a PRESSED chip, so it
#: cannot tell "this account has no agent/classic split" from "the split exists and is
#: currently off" — the distinction #799 turns on. Never click this one: it is not
#: self-guarding, and a click would toggle a healthy composer INTO agent mode.
AGENT_MODE_CHIP_ANY = "button.agent-mode-chip"
#: How long the classic composer gets to come back after the chip is clicked. The swap is
#: a local Angular re-render, not a navigation — measured well under a second on both
#: accounts — so this is headroom, not an expectation.
Expand Down Expand Up @@ -726,6 +732,39 @@ async def ensure_editor(self, page: Page, project_id: str, *, timeout_s: float =
# healthy run nothing — no extra query is issued unless the gate has failed.
state, click_error = await self._exit_agent_mode(page)
if state == "absent":
# #799. Before calling this drift, separate the third cohort from our own
# bug, because the DOM they leave is the same one: the trigger present
# under a bare `hidden`. With a pressed chip that is agent mode and the
# branch above recovers it. With NO chip in the page at all, there is no
# classic arm to return to — the account's only composer is the agent
# panel, so model/aspect/count live in Agent settings as defaults and
# nothing gflow drives is on the page. A trigger that has LEFT the DOM is
# the opposite finding, a renamed selector, and stays drift.
if await self._is_agent_only_composer(page, trigger):
log.info("migrated.agent_only_composer", issue_ref="#799", url=page.url)
raise FlowAgentUiError(
detail=(
f"migrated host: this account's composer is agent-only — the "
f"settings trigger ({READY_ANCHOR}) is in the page but hidden, "
f"and no agent-mode chip ({AGENT_MODE_CHIP_ANY}) exists to turn "
f"off, so there is no classic composer to drive on {page.url} "
f"(host=migrated). Not selector drift, and not the recoverable "
f"agent mode of #749. Readiness gate: {e}"
),
remediation_hint=(
"Google has put this account on Flow's agent-only composer, "
"where aspect, model and count are Agent-settings defaults "
"rather than per-request controls. gflow-cli has no driver for "
"it yet, so no flag or profile change helps and a re-run will "
"not either — this is tracked in issue #799. Generating from "
"the Flow web UI still works."
),
# Not retryable: #749's chip flips per click, but which composer an
# account gets is server-assigned per account, so a retry lands the
# same page. Same reasoning that keeps FlowHostMigratedError out of
# RETRYABLE_ERRORS; FlowAgentUiError is in it for the labs A/B.
retryable=False,
) from e
raise UiSelectorDriftError(
detail=(
f"migrated host: the settings trigger ({READY_ANCHOR}) did not "
Expand Down Expand Up @@ -813,6 +852,34 @@ async def _agent_chip_pressed(page: Page) -> bool | None:
log.warning("migrated.agent_mode_probe_failed", error=str(e)[:200])
return None

@staticmethod
async def _is_agent_only_composer(page: Page, trigger: Any) -> bool:
"""True when the page is #799's agent-only cohort rather than selector drift.

Two facts, and it takes both. The readiness anchor is **in the DOM but not
visible** — a trigger that is simply gone is a renamed selector, our bug, and
must keep saying so. And **no agent-mode chip exists at all**, pressed or not:
the chip is what makes a hidden trigger recoverable (#749), so its absence is
what says this account has no classic arm to go back to.

An un-pressed chip is deliberately excluded from the claim, and measuring it is
what makes the rest of this worth doing: on a live healthy migrated composer
(ffroliva, 2026-09-13, $0) the chip IS present and un-pressed while the trigger
is visible — so a normal account on this host has a chip, and having none is the
anomaly. An un-pressed chip beside a HIDDEN trigger is the state never observed;
it contradicts the measured mechanism (pressed ⇒ hidden) and falls through to
drift rather than being asserted as a cohort.

Fail-closed: an unreadable page is not evidence of a cohort.
"""
try:
if not await trigger.count() or await trigger.is_visible():
return False
return not await page.locator(AGENT_MODE_CHIP_ANY).first.count()
except Exception as exc: # noqa: BLE001 - an unreadable page is not an answer
log.warning("migrated.agent_only_probe_failed", error=str(exc)[:200])
return False

@classmethod
async def _exit_agent_mode(cls, page: Page) -> tuple[AgentModeExit, Exception | None]:
"""Turn Flow's agent mode off if it is on.
Expand Down
Loading