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:
parent
c66cc79e90
commit
7fbde35e47
96
content.js
96
content.js
@ -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`, {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user