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;
|
let consecutiveRateLimits = 0;
|
||||||
|
|
||||||
function init() {
|
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 };
|
settings = { ...settings, ...data };
|
||||||
if (settings.autoStart && !settings.isRunning) {
|
if (settings.autoStart && !settings.isRunning) {
|
||||||
settings.isRunning = true;
|
settings.isRunning = true;
|
||||||
chrome.storage.local.set({ isRunning: true }); // so the popup reflects it
|
chrome.storage.local.set({ isRunning: true }); // so the popup reflects it
|
||||||
}
|
}
|
||||||
log('system', `Content script connected (${settings.isRunning ? 'ACTIVE' : 'idle'}).`);
|
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();
|
if (settings.isRunning) startPolling();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -216,6 +221,15 @@ function persistentError() {
|
|||||||
(e.textContent || '').length < 180);
|
(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.
|
// 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.
|
// Clicking it re-runs the SAME prompt (no re-injection needed) — the manual fix.
|
||||||
function findTryAgain() {
|
function findTryAgain() {
|
||||||
@ -354,8 +368,36 @@ async function executeTask(task) {
|
|||||||
|
|
||||||
log('info', 'Submitted (stop square seen). Waiting for generation…');
|
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;
|
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);
|
const outcome = await waitForResultOrError(before.size, timeoutMs);
|
||||||
if (outcome === 'ratelimit') { await handleRateLimit(task); return; } // long back-off, don't hammer
|
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');
|
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) {
|
async function reportCompletion(id, status, error, assets) {
|
||||||
try {
|
try {
|
||||||
await fetch(`${settings.serverUrl}/task-completed`, {
|
await fetch(`${settings.serverUrl}/task-completed`, {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user