feat: post-video hard-refresh harvest — the agent's 'queued' message lies, the fresh page has the asset

- video path: wait for done signal (grid asset OR agent completion message incl.
  'scheduled/waiting in the queue' which usually means DONE), settle 2s, persist
  harvest state, hard refresh — never re-submit (double-bills credits)
- runPostReloadHarvest: on load with pendingHarvest, diff fresh grid vs persisted
  pre-submit snapshot (https urls only — blobs don't survive reloads), download,
  report; up to 2 extra refreshes if the asset hasn't surfaced; 30-min staleness cap
- fixes the 'Timed out — no new asset' failure class in results.jsonl

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-07 18:48:55 +10:00
parent c66cc79e90
commit 7fbde35e47

View File

@ -37,13 +37,18 @@ let successCount = 0;
let consecutiveRateLimits = 0;
function init() {
chrome.storage.local.get(settings, (data) => {
chrome.storage.local.get({ ...settings, pendingHarvest: null }, (data) => {
const ph = data.pendingHarvest; delete data.pendingHarvest;
settings = { ...settings, ...data };
if (settings.autoStart && !settings.isRunning) {
settings.isRunning = true;
chrome.storage.local.set({ isRunning: true }); // so the popup reflects it
}
log('system', `Content script connected (${settings.isRunning ? 'ACTIVE' : 'idle'}).`);
if (ph) { // a video finished right before the crucial post-gen refresh — harvest it now
runPostReloadHarvest(ph).then(() => { if (settings.isRunning) startPolling(); });
return;
}
if (settings.isRunning) startPolling();
});
}
@ -216,6 +221,15 @@ function persistentError() {
(e.textContent || '').length < 180);
}
// The agent's end-of-generation chatter. John's law: this message LIES — "scheduled /
// waiting in the queue due to high demand" often means it's actually DONE, and the
// stale page won't show the asset until a refresh.
function agentSaysDone() {
return [...document.querySelectorAll('*')].some(e => visible(e) &&
/has been scheduled|waiting in the queue|check back in a few|is ready|i'?ve (generated|created)|here('s| is) (your|the) video/i.test(e.textContent || '') &&
(e.textContent || '').length < 240);
}
// Flow's "Try again" button, shown beside the "Something went wrong" banner.
// Clicking it re-runs the SAME prompt (no re-injection needed) — the manual fix.
function findTryAgain() {
@ -354,8 +368,36 @@ async function executeTask(task) {
log('info', 'Submitted (stop square seen). Waiting for generation…');
// Wait for a new asset, a Flow error (fast-fail), or timeout. Images should be quick.
const timeoutMs = task.kind === 'image' ? 3 * 60 * 1000 : settings.genTimeoutMs;
if (task.kind === 'video') {
// John's law: after a video gen the page is stale AND the agent's completion
// message lies ("queued due to high demand" = usually done). The post-gen HARD
// REFRESH is crucial. Wait for a done-ish signal, let it settle, persist the
// harvest state (so the reload can't re-submit and burn credits), then reload.
const t0 = Date.now();
let signal = null;
while (Date.now() - t0 < timeoutMs) {
if (snapshotAssets().size > before.size) { signal = 'asset in grid'; break; }
if (agentSaysDone()) { signal = 'agent completion message'; break; }
if (Date.now() - t0 > 6000 && persistentError()) { await handleRateLimit(task); return; }
if (Date.now() - t0 > 8000 && (findTryAgain() || flowError()))
return await requeueWithReload(task, 'Flow errored during video generation');
await sleep(2500);
}
if (!signal) throw new Error('Timed out — no completion signal for video.');
log('info', `Video done (${signal}). Settling 2s, then the crucial post-gen hard refresh…`);
await sleep(2500);
await new Promise(r => chrome.storage.local.set({ pendingHarvest: {
id: task.id, kind: task.kind, category: task.category, tries: 0, t: Date.now(),
before: [...before].filter(u => /^https?:/.test(u)), // blob: urls don't survive reloads
} }, r));
const r = await bg({ type: 'hardReload' });
if (!r || !r.ok) location.reload();
return; // the fresh page harvests via runPostReloadHarvest
}
// Images: the grid updates live — wait in-page for a new asset, error, or timeout.
const outcome = await waitForResultOrError(before.size, timeoutMs);
if (outcome === 'ratelimit') { await handleRateLimit(task); return; } // long back-off, don't hammer
if (outcome === 'error') return await requeueWithReload(task, '"Something went wrong" persisted through Try-again');
@ -396,6 +438,56 @@ async function executeTask(task) {
}
}
// After the crucial post-gen refresh: diff the fresh grid against the persisted
// pre-submit snapshot, download what's new, report. Retries the refresh twice if
// the asset still hasn't surfaced; never re-submits (that would double-bill credits).
async function runPostReloadHarvest(ph) {
generationInProgress = true;
try {
if (Date.now() - (ph.t || 0) > 30 * 60 * 1000) { // stale (browser was closed?) — don't guess
chrome.storage.local.remove('pendingHarvest');
await reportCompletion(ph.id, 'failed', 'post-gen harvest state went stale (>30 min old)', []);
return;
}
await waitForAgentReady(30000);
const before = new Set(ph.before || []);
await waitForStable(() => [...snapshotAssets()].filter(u => /^https?:/.test(u)).length,
{ maxMs: 90000, min: before.size + 1 });
const fresh = [...snapshotAssets()].filter(u => /^https?:/.test(u) && !before.has(u));
if (!fresh.length) {
if ((ph.tries || 0) < 2) {
ph.tries = (ph.tries || 0) + 1;
await new Promise(r => chrome.storage.local.set({ pendingHarvest: ph }, r));
log('info', `Post-refresh grid shows nothing new yet — refresh ${ph.tries}/2 and re-check.`);
await sleep(10000);
const r = await bg({ type: 'hardReload' });
if (!r || !r.ok) location.reload();
return;
}
chrome.storage.local.remove('pendingHarvest');
await reportCompletion(ph.id, 'failed', 'video generated but never surfaced in the grid after 3 loads', []);
return;
}
const cat = (ph.category || 'misc').replace(/[^a-z0-9_\-]/gi, '_');
let saved = 0;
for (let i = 0; i < fresh.length; i++) {
const r = await bg({ type: 'download', url: fresh[i], filename: `flowrinse/${cat}/${ph.id}_${i}.${ph.kind === 'video' ? 'mp4' : 'png'}` });
if (r && r.ok) saved++;
}
log('success', `Post-refresh harvest: ${fresh.length} asset(s), downloaded ${saved} → Downloads/flowrinse/${cat}/.`);
chrome.storage.local.remove('pendingHarvest');
chrome.storage.local.remove('requeue_' + ph.id);
await reportCompletion(ph.id, 'success', null, fresh);
successCount++; consecutiveRateLimits = 0;
} catch (err) {
log('error', `Post-refresh harvest failed: ${err.message}`);
chrome.storage.local.remove('pendingHarvest');
await reportCompletion(ph.id, 'failed', `post-refresh harvest: ${err.message}`, []);
} finally {
generationInProgress = false;
}
}
async function reportCompletion(id, status, error, assets) {
try {
await fetch(`${settings.serverUrl}/task-completed`, {