Repository navigation
Description
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.getMediaUrlRedirectroute 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:
- open
https://flow.google.com/project/<project_id> - left nav → Videos
- hover the tile → More options (
more_vert) → Download - a submenu offers:
270p Animated GIF·720p Original size·1080p Upscaled·
4K Upscaled (Upgrade) 720p Original sizeproduced
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 itsproject_id. - Drive the project page, bind the tile, open the menu, choose Original size, and
save viaexpect_download(). - Reuse the existing safety posture in
_fetch_mp4: the_is_allowed_download_host
allowlist,max_redirects=0, and theftypmagic-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_filesrow sogflow data liststops reportingcopy_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
ffroliva commentedon Sep 17, 2026
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
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 signedflow-content.google/video/<workflow_id>?Expires&KeyName&SignatureURL.as29sis already inSTATUS_RPCSandgeneration_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
imgand 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--indexfallback would have inherited the same ceiling.One correction to the reuse plan
All three are reused, and
flow-content.googleis already allowlisted — butftypis not sufficient here. The listing token also answers=m18and=m22with 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 verifieslen(body) == size_bytesand errors on mismatch.Recovered both orphans;
9ad33c78came back at 2 702 168 B — the same byte count your manual UI download produced.feat(data): `gflow data download <media_id>` — recover a billed asset…