Skip to content

No CLI path to a billed clip whose signed URL missed the 20s grace — Flow's own UI can download it #871

Closed

Description

@ffroliva

A billed clip that misses the 20s URL grace is unreachable from the CLI, forever

migrated_composer.download() can only save a clip whose signed CDN URL was observed
during generation
. When it is not, the run raises:

WireFormatError: migrated host: the generation finished but no signed media URL was
observed within the 20s grace

The generation still happened and the credit is still spent. The clip is in the
project. There is no CLI path to it: gflow data is list / media / prune / sync, and
sync only reconciles names. gflow data list videos shows the row with
local_path: null, copy_count: 0 — it knows the asset exists and cannot fetch it.

The code says as much, and it is currently true:

The labs media.getMediaUrlRedirect route answers 404 for a migrated media id
(measured 2026-09-05), so there is no second source: a record with no URL is a
wire-format failure.

There is a second source. Flow's own UI downloads it.

Measured, 2026-09-17

Two s025 clips were billed and orphaned this way (23620c34-… at 13:52:59,
9ad33c78-… at 13:56:27, both exit 7). One was recovered by hand, at no extra cost,
by driving flow.google.com in a browser:

  1. open https://flow.google.com/project/<project_id>
  2. left nav → Videos
  3. hover the tile → More options (more_vert) → Download
  4. a submenu offers: 270p Animated GIF · 720p Original size · 1080p Upscaled ·
    4K Upscaled (Upgrade)
  5. 720p Original size produced
    Artist_opening_door_on_street_20260917210326.mp4, 2 702 168 B,
    h264 720x1280 + aac, 8.000s — byte-identical in shape to what a successful
    in-run download writes.

Every one of those steps is an ordinary Playwright interaction on a page this driver
already owns. The download itself is page.expect_download().

Proposed

gflow data download <MEDIA_ID> [--out-dir DIR] [--profile NAME]

  • Resolve the media id in the local catalog (it is already there — that is how the
    orphan is discovered) to get its project_id.
  • Drive the project page, bind the tile, open the menu, choose Original size, and
    save via expect_download().
  • Reuse the existing safety posture in _fetch_mp4: the _is_allowed_download_host
    allowlist, max_redirects=0, and the ftyp magic-byte check. None of that should be
    re-implemented; a UI-sourced file must clear the same bar as a URL-sourced one.
  • Write the local_files row so gflow data list stops reporting copy_count: 0.

The hard part, stated up front

Flow's DOM does not carry the media id. Checked on the live page: searching the full
outerHTML (448 436 chars) for either orphaned UUID returned nothing, and no tile
img/video/background-image URL contains a UUID either. The manual recovery
identified the right tile by prompt text and creation order — the catalog's prompt
matched the tile's Flow-generated title ("…draws the door gently back…" →
"Artist opening door on street"), and the ordering was newest-first.

That heuristic is not good enough to ship: two takes of the same prompt produce two
tiles with near-identical titles, which is exactly the orphan case.

So this needs a spike before an implementation: find the wire response that lists a
project's media with ids in the same order the grid renders, and bind tile → id from
that rather than from display text. If no such correlation exists, the honest fallback is
--index N plus a printed table for the human to choose from — still infinitely better
than "unreachable".

Why it is worth it

A missed 20s grace currently converts a paid generation into a permanent loss with no
recovery verb. The failure is transient and timing-based, so it will recur. The
recovery is already possible by hand in about ten seconds; encoding it is a small driver
addition on a surface gflow already automates.

Activity

  1. ffroliva commented on Sep 17, 2026

    @ffroliva
    OwnerAuthor

    Implemented in #872. The spike this issue asked for is docs/superpowers/spikes/2026-09-17-stranded-clip-recovery.md, and it came back simpler than the proposal.

    The stated blocker does not exist

    Flow's DOM does not carry the media id. [...] That heuristic is not good enough to ship [...] So this needs a spike before an implementation: find the wire response that lists a project's media with ids in the same order the grid renders.

    Correct that the id is absent from the DOM as text — but it is in the clip's own URL. Clicking a tile navigates to flow.google.com/project/<project_id>/edit/<media_id>, and both ids are already in the catalog row the orphan has. So the route is built from local data and no tile is involved.

    Opening it fires as29s, whose reply carries the signed flow-content.google/video/<workflow_id>?Expires&KeyName&Signature URL. as29s is already in STATUS_RPCS and generation_record() already decodes that exact slot.

    Why the tile approach would have looked fine and then failed

    I did measure the binding you suggested, and it works: the tile's img and the listing record share an /asb/<token>, matching 13/13 tiles with no display text. But the grid is virtualized — 13 rendered tiles against 183 records, recycling to 0 on scroll. A tile-driven implementation reaches only the newest page, so it would have passed on these two recent orphans and silently failed on any older clip. The --index fallback would have inherited the same ceiling.

    One correction to the reuse plan

    Reuse the existing safety posture in _fetch_mp4: the _is_allowed_download_host allowlist, max_redirects=0, and the ftyp magic-byte check.

    All three are reused, and flow-content.google is already allowlisted — but ftyp is not sufficient here. The listing token also answers =m18 and =m22 with valid MP4s that are 360p/720p transcodes (356 946 B and 1 209 675 B against a 2 410 295 B original). 25 suffix variants were measured; none returns the original. The command therefore verifies len(body) == size_bytes and errors on mismatch.

    Recovered both orphans; 9ad33c78 came back at 2 702 168 B — the same byte count your manual UI download produced.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions