v1.46.4: detached plans were being collected as garbage — data loss

Deployed v1.46.3 to my own instance, restarted, and the startup sweep deleted
both floor plans: config/houseplan/plans/ went from f1.svg + f2.png to empty.
The backup is a SecureTar, so they are gone.

The rule was wrong, not the code. v1.46.0 introduced collection that treats
'nothing references this right now' as abandoned and gives it an hour. But
detaching a plan — switching a space to 'draw' — is a normal, reversible action,
and the editor's own comment says the file stays on disk. Those two plans had
been detached for weeks; every pass since v1.46.0 was entitled to remove them,
and the one that finally ran did.

New rule, one for every path:
  * superseded by a commit (was in the old revision, is not in the new) — goes
    immediately; that is the one thing a commit knows for certain;
  * belongs to a space or marker that still exists — never collected, at any
    age, because unreferenced is not abandoned;
  * a per-dialog staging folder (up_*) — one hour, unchanged: by construction it
    only ever holds an upload from a dialog that was never saved;
  * anything else — thirty days.

The  flag I added an hour ago is gone with it: two rules for the same
question is how this happened. Tests updated to the new grace, plus two that pin
the distinction directly.

I am sorry about the files.
This commit is contained in:
Matysh
2026-07-28 19:55:38 +03:00
parent 254354bf56
commit ef270d11b7
16 changed files with 173 additions and 36 deletions
+9 -2
View File
@@ -207,8 +207,15 @@ happen when somebody saves, and a file uploaded into a dialog that was then
cancelled would wait for a write that may never come. A new icon has no id yet, so its
files go to a per-dialog staging folder and move to the real id once the config
write is accepted — the same copy → save → cleanup order as a rebind.
`config/set` collects what its commit superseded, and anything unreferenced and
older than `PLAN_ORPHAN_TTL_S`.
`config/set` collects what its commit superseded and only that, because
supersession is the one thing a commit knows for certain. *Unreferenced* is a
much weaker signal: detaching a plan is reversible and the editor promises the
file stays. So a file belonging to a space or marker that still exists is never
collected, and anything else waits `SCHEDULED_GRACE_S` (30 days). The exception
is a per-dialog staging folder, which by construction only holds an upload from
a dialog that was never saved — `PLAN_ORPHAN_TTL_S` (1 hour) is right there.
This is not theoretical caution: the one-hour rule applied to unreferenced files
destroyed two detached plans on 2026-07-28.
**Config writes are serialized** (HP-1454-03). `_writeConfig()` chains onto a
single promise: one `config/set` in flight, each carrying the revision the