feat(requeue): --force, for when Flow succeeds at the wrong picture

cal79_throbbing was recorded 'success' with 4 assets — but they were Kenner
Alien toy ads, not the Throbbing Gristle clifftop it asked for. Flow refused
the Kenner brief, drew a Voyager plate instead, and emitted the toy variants
late, so the harvester (which attributes whatever NEW tiles appear to the
CURRENT task) filed them under the next task in the queue.

Successes stay protected by default — re-running one spends credits for an
asset you already have. --force is the deliberate override, and it says so
before it acts. --force with no ids matches nothing.

Known limitation, not fixed here: asset->task attribution is a grid diff, so
a late or refused generation can land under its successor. Check the pictures,
not just the status.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-08 20:13:50 +10:00
parent 84fa103125
commit 9974b93379

View File

@ -10,6 +10,10 @@ and we don't re-bill by accident. This is the deliberate, explicit way to retry.
python3 requeue.py cal79_ # do it (writes a .bak first) python3 requeue.py cal79_ # do it (writes a .bak first)
python3 requeue.py --failed # every failed task python3 requeue.py --failed # every failed task
python3 requeue.py cal79_disco cal79_tmi # exact ids python3 requeue.py cal79_disco cal79_tmi # exact ids
python3 requeue.py --force cal79_throbbing # re-run a SUCCESS (it drew the wrong thing)
Successes are protected: re-running one spends credits for an asset you already have.
--force is the deliberate override, for when Flow succeeded at the wrong picture.
Stop the queue server first, or it may rewrite the file under you. Stop the queue server first, or it may rewrite the file under you.
""" """
@ -34,6 +38,7 @@ def main():
print(__doc__) print(__doc__)
return 1 return 1
dry = '--dry' in args dry = '--dry' in args
force = '--force' in args
only_failed = '--failed' in args only_failed = '--failed' in args
pats = [a for a in args if not a.startswith('--')] pats = [a for a in args if not a.startswith('--')]
rs = rows() rs = rows()
@ -45,16 +50,18 @@ def main():
return 0 return 0
def hit(r): def hit(r):
if r['status'] == 'success': if r['status'] == 'success' and not force:
return False # never re-run a success; that's just burning credits return False # never re-run a success; that's just burning credits
if only_failed and r['status'] == 'failed': if only_failed:
return True return r['status'] == 'failed'
return any(r['id'] == p or r['id'].startswith(p) for p in pats) return any(r['id'] == p or r['id'].startswith(p) for p in pats)
doomed = [r for r in rs if hit(r)] doomed = [r for r in rs if hit(r)]
if not doomed: if not doomed:
print('nothing matched (successes are never requeued)') print('nothing matched (successes are never requeued — pass --force if Flow drew the wrong thing)')
return 1 return 1
if force and any(r['status'] == 'success' for r in doomed):
print('--force: re-running SUCCESSES. This spends credits for assets you already have.')
for r in doomed: for r in doomed:
print(f" requeue {r['id']:<24} {r['status']:<10} {str(r.get('error',''))[:46]}") print(f" requeue {r['id']:<24} {r['status']:<10} {str(r.get('error',''))[:46]}")
print(f"\n{len(doomed)} task(s){' [DRY RUN]' if dry else ''}") print(f"\n{len(doomed)} task(s){' [DRY RUN]' if dry else ''}")