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>
84 lines
3.1 KiB
Python
84 lines
3.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Make finished tasks pending again by removing their rows from results.jsonl.
|
|
|
|
The queue server treats any id in results.jsonl as done (except 'ratelimited'), so a task
|
|
that failed is never retried automatically — a timeout may have burned credits invisibly
|
|
and we don't re-bill by accident. This is the deliberate, explicit way to retry.
|
|
|
|
python3 requeue.py --list # what's failed, grouped
|
|
python3 requeue.py --dry cal79_ # what WOULD be requeued (prefix match)
|
|
python3 requeue.py cal79_ # do it (writes a .bak first)
|
|
python3 requeue.py --failed # every failed task
|
|
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.
|
|
"""
|
|
import collections
|
|
import json
|
|
import os
|
|
import shutil
|
|
import sys
|
|
|
|
HERE = os.path.expanduser('~/Library/Application Support/flowrinse')
|
|
RESULTS = os.path.join(HERE, 'results.jsonl')
|
|
|
|
|
|
def rows():
|
|
with open(RESULTS) as f:
|
|
return [json.loads(l) for l in f if l.strip()]
|
|
|
|
|
|
def main():
|
|
args = [a for a in sys.argv[1:]]
|
|
if not args:
|
|
print(__doc__)
|
|
return 1
|
|
dry = '--dry' in args
|
|
force = '--force' in args
|
|
only_failed = '--failed' in args
|
|
pats = [a for a in args if not a.startswith('--')]
|
|
rs = rows()
|
|
|
|
if '--list' in args:
|
|
c = collections.Counter((r['status'], r['id'].rsplit('_', 1)[0]) for r in rs)
|
|
for (st, grp), n in sorted(c.items()):
|
|
print(f'{st:<12} {grp:<24} {n}')
|
|
return 0
|
|
|
|
def hit(r):
|
|
if r['status'] == 'success' and not force:
|
|
return False # never re-run a success; that's just burning credits
|
|
if only_failed:
|
|
return r['status'] == 'failed'
|
|
return any(r['id'] == p or r['id'].startswith(p) for p in pats)
|
|
|
|
doomed = [r for r in rs if hit(r)]
|
|
if not doomed:
|
|
print('nothing matched (successes are never requeued — pass --force if Flow drew the wrong thing)')
|
|
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:
|
|
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 ''}")
|
|
if dry:
|
|
return 0
|
|
|
|
shutil.copy2(RESULTS, RESULTS + '.bak')
|
|
keep = [r for r in rs if r not in doomed]
|
|
with open(RESULTS + '.tmp', 'w') as f:
|
|
for r in keep:
|
|
f.write(json.dumps(r) + '\n')
|
|
os.replace(RESULTS + '.tmp', RESULTS) # a kill mid-write can't truncate the real file
|
|
print(f'wrote {len(keep)} rows (backup: results.jsonl.bak). Restart the queue server; '
|
|
'they are pending again.')
|
|
return 0
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main())
|