A restart during an active print duplicated the running job in the
archive, and every further restart spawned another. on_print_start
re-attaches by subtask_id, falling back to a name match plus a 4-hour
staleness cutoff that cancelled + recreated any name-matched 'printing'
archive older than 4h - destroying the live archive of every long print.
Two fixes:
- start_print records the minted subtask_id (last_dispatch_subtask_id);
on_print_start falls back to it when the printer hasn't echoed one
yet, so queue/scheduled archives persist a restart-stable id.
- Replace the 4h cutoff with a progress-aware check: a name-matched
'printing' archive resumes whenever the printer reports real (or
unknown) progress; it is stale only when the printer shows a
freshly-started print (<1%) on an archive over 2h old.
Second wave of #972 — reproducer on a 37.5 MB BambuStudio print to an A1
showed three stacking root causes when Bambuddy restarts mid-print.
1. Archive start_time lost on container restart. The name-based dedup
cancelled any "printing" archive older than 4h and recreated it with
started_at=now(), so a 13h print that saw a restart 10h in ended up
showing ~1.5h duration. Persist MQTT subtask_id on every archive and
match on that first, regardless of age — same id means same print,
resume in place. Also revives Stale-cancelled rows for users
upgrading mid-print.
2. 3MF FTP search tried non-existent paths for ~48 min. Order was
/cache → /model → /data → /data/Metadata → / with 11×30s retries
each; BambuStudio actually pushes to / on A1, so the real path was
tested last. Reorder to / first, and raise a new FileNotOnPrinterError
sentinel from download_to_file on 550 so with_ftp_retry short-circuits
via non_retry_exceptions. 425 / SSL EOF / connection resets still
retry as before.
3. Cover endpoint and archive flow downloaded the same 36 MB twice and
competed for the printer's single FTP socket, producing 425 errors
that fed cause-2's retry storm. Add an in-memory _threemf_path_cache
keyed on (printer_id, normalized filename); whichever flow fetches
first populates it, the other reuses the file read-only. Eviction
runs on on_print_complete and deletes the temp file.
Backend: 14 new tests across test_bambu_ftp.py and a new
test_subtask_archive_resume.py. Existing suite: 2737 pass. ruff clean,
frontend build clean.