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
48 changes: 48 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,54 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- **A finished, billed clip is no longer thrown away when its download hits a connection
reset (#895).** The signed-media GET was issued once, with no retry: a single
`ECONNRESET` mid-transfer failed the whole command *after* Veo had produced the clip and
charged for it — reported at 2 failures in 6 consecutive runs. The transfer now survives
a dropped connection using Playwright's own `max_retries`, which retries on the driver's
`ECONNRESET` and never on an HTTP status, and whose backoff is charged to the same
timeout rather than multiplying it: three attempts stay inside one ~180 s budget instead
of stretching to nine minutes on a lock shared with every other generation. Measured
with an A/B control, not read from the docs —
[spike](docs/superpowers/spikes/2026-09-22-playwright-max-retries-econnreset.md).
Fixed at **all three** download sites, not just the one the traceback named: the
migrated composer, `gflow data download`'s own recovery GET (so the escape hatch cannot
be stranded by the fault it exists to rescue), and the labs `media.getMediaUrlRedirect`
path.
- **A lost transfer now says the clip survived, instead of `Unexpected error` (#895).**
`playwright.async_api.Error` is not a gflow error class, so it escaped the typed-error
contract entirely and rendered as *"Unexpected error … exit code 1, retryable: False"* —
of which the last two are wrong and the first is useless. It is now `NetworkError`
(exit 6, correctly retryable) carrying the media id and `gflow data download <id>`, so a
user is not left re-generating a clip they already own. The exception message is
deliberately **not** forwarded into `detail`: Playwright concatenates its server-side
call log into it, and `detail` reaches stderr and `--json` stdout without passing
through redaction, so a signed URL would leak. Both MCP twins gain this for free — until
now `gflow_download_media` returned *"Unexpected Error; details were logged
server-side"* with no remediation field at all, and the queued `gflow_generate_video`
path returned `detail: "sha256:<hash>"` with neither `remediation_hint` nor `retryable`.
- **A failed generation now keeps its media id on the queue row, so an MCP agent can
actually recover the clip (#895).** The remediation added above names the media id and
tells the user to recover it — but over MCP that advice was unusable. The worker's
success branch recorded `flow_media_id` on the queue row; the failure branch did not,
so the row stayed `NULL` and `gflow_generate_video`'s failed envelope carried no id at
all. The agent's only copy was a UUID buried in an English sentence, suggesting a
*shell command* an MCP client cannot run, when its actual tool takes `media_id=`.
`update_task_status` already COALESCEs, so a task that failed before Flow named
anything still records nothing.

- **An expired signed link stops blaming your prompt (#895).** A late GET answers 4xx, and
every one of those branches fell through to `WireFormatError`'s class default — *"retry
with a simpler prompt text"* — on paths that carry no prompt and no payload. The same
wrong-advice class removed in #875. Each now names the short link lifetime and the
credit-free re-mint.
- **The labs video download verifies where its redirect landed (#895).** That route is a
302 by design, so it follows up to 5 hops, and nothing checked the final host — the
posture the migrated arm gets from refusing redirects outright. The bytes are now
refused unless the landing host is an allowed Google host.

### Changed

- **The nightly canary stops crying wolf about an account that cannot reach Flow.**
Expand Down
2 changes: 1 addition & 1 deletion docs/MCP.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ The server registers three protocol surfaces:
* `gflow_character_show(project, entity_id, name, profile)`: Shows one character by id or exact display name (exactly one selector required). An ambiguous name is refused rather than resolved arbitrarily — which is the reason to prefer the id. Read-only; drives a browser session.
* `gflow_character_voices()`: Lists the preset voices available for Character TTS (name, description, sample URL). A static in-process lookup — no network, no browser, no profile, no cost. Call it to pick a valid voice name.
* `gflow_list_projects(profile, limit, offset)`: Queries the SQLite catalog for recent generation folders, paginated — the response carries `count` (rows in this page), `offset`, `has_more`, and `next_offset` (pass it back as `offset` for the next page; `null` on the last page).
* `gflow_download_media(media_id, out_dir, profile)`: Fetches an already-generated asset from Flow by its media ID and writes it to disk — the recovery path for a generation that was billed but whose download failed. Spends no credits. The bytes are verified against the size Flow reports before the file is written, and the catalog's `local_files` row is updated. Returns `path`, `bytes`, `media_id`, `workflow_id`, `project_id` and `profile`.
* `gflow_download_media(media_id, out_dir, profile)`: Fetches an already-generated asset from Flow by its media ID and writes it to disk — the recovery path for a generation that was billed but whose download failed. Spends no credits. The bytes are verified against the size Flow reports before the file is written, and the catalog's `local_files` row is updated. Returns `path`, `bytes`, `media_id`, `workflow_id`, `project_id` and `profile`. The transfer retries a dropped connection internally ([#895](https://github.com/ffroliva/gflow-cli/issues/895)), so a failure marked `retryable` means pause and call again — not immediately, and not in a tight loop: each call opens a browser under a per-profile lease, and a second concurrent call fails on that lease instead (exit 11).
* `gflow_list_tools()`: Lists the prompt tools (name/title/description/category) accepted by the generate tools' `tools` param.
* `gflow_instructions_list(project, profile)`: Lists a project's persistent Agent-Mode instruction cards (live server brief; credits-free).
* `gflow_instructions_add(project, title, text, refs, enabled, profile)`: Adds a persistent instruction card. Each ref is classified automatically — local image path → uploaded image reference, asset UUID → image reference, anything else → character id/name (mirrors CLI `instructions add --ref`).
Expand Down
9 changes: 8 additions & 1 deletion docs/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1345,6 +1345,13 @@ was to run the generation again and pay a second time.

Recovering costs **nothing**. The asset was already billed.

The transfer **retries a dropped connection by itself** — three attempts, sub-second
backoff, all inside the same timeout rather than multiplying it. A reset that survives
all three exits **6** (`NetworkError`, retryable) and says what to do, instead of the
bare `Unexpected error` it used to raise
([#895](https://github.com/ffroliva/gflow-cli/issues/895)). Re-running is safe and still
costs nothing: each attempt asks Flow for a fresh signed link.

```text
gflow data download MEDIA_ID [--out DIR] [--profile NAME] [--json]

Expand Down Expand Up @@ -1925,7 +1932,7 @@ shell scripts can branch on the failure mode without parsing stderr.
| `3` | `AuthExpiredError` | Session cookies rejected by Flow (401/403), or Flow served one of its OAuth/sign-in routes instead of the page gflow asked for ([#756](https://github.com/ffroliva/gflow-cli/issues/756)) | `gflow auth login --profile <name>` — **but read the error's own `remediation_hint` first.** `AisandboxAuthError` shares this code, and when `gflow credits` fails on an account migrated to `flow.google.com` re-logging in cannot help and can roll the profile's browser-strategy marker back ([#795](https://github.com/ffroliva/gflow-cli/issues/795), [#791](https://github.com/ffroliva/gflow-cli/issues/791)) |
| `4` | `RateLimitError` | Quota / rate limit hit, exhausted retries | Wait + reduce `GFLOW_CLI_CONCURRENCY` |
| `5` | `ContentPolicyError` | Flow rejected the prompt (200 + empty `media[]`) | Soften prompt wording |
| `6` | `NetworkError` | Network failure persisted across 3 attempts | Check connectivity |
| `6` | `NetworkError` | Network failure persisted across 3 attempts — including a signed-media download whose connection dropped on every attempt ([#895](https://github.com/ffroliva/gflow-cli/issues/895)) | Check connectivity — **but read the error's own `remediation_hint` first.** When a *generation* download fails this way the clip was already produced and billed: the hint names it, and `gflow data download <media_id>` recovers it for free. Re-generating pays twice |
| `7` | `WireFormatError` | Unexpected response shape — Flow API changed | File a bug (do NOT include captured tokens or signed URLs) |
| `8` | `AuthMissingError` | Required auth credential is absent from profile | `gflow auth login --profile <name>` |
| `9` | `TransportTimeoutError` | Browser/API operation exceeded its timeout (incl. an i2v frame UUID not found in the media picker, #287; or a wedged submission stage — the error names the stage and your Playwright version) | Retry; raise the relevant timeout — for a frame-UUID miss verify the UUID belongs to the `--project` passed; for a `stage_stalled` abort check your Playwright is in range (see [KNOWN_ISSUES](../KNOWN_ISSUES.md)) |
Expand Down
Loading
Loading