Read-back: live sizing-data.json?v=4 byte-equal to the commit; the presets keep their verdicts, Immich alone on 8 GB still tight; only the set without Immich changed. Spike B2 on 9202: POST /api/assets/jobs regenerate-thumbnail rebuilt exactly the 10 broken assets (1.55 s, 5.3 s CPU), 0 of the other 20 photos' 40 files rewritten (control: all rewrote 40 of 40); an empty file maps to its asset by the first 36 characters of its name. Immich removed through the product, no leftovers. Register 134 -> 133 (opened 0, closed 1). STATUS D9/D10 ruled. Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
Power-cut fixes 2026-10-14 — spike (Part C) and live proof (Part E)
Times UTC. Box: demo-hp (Tier 0). No key, token or password in this folder.
C1 — what the night's cut left (from audits/night-power-2026-10-13/R3/)
| Item | Value |
|---|---|
| left on disk | /var/lib/vz/dump/vzdump-lxc-9201-2026_10_14-04_42_18.tar.dat (file, root:root 0644, 12.4 GB at the cut, 12.2 GB after the boot — the unwritten tail was lost) and vzdump-lxc-9201-2026_10_14-04_42_18.tmp/ (directory, root:root 0755). No .log (vzdump writes it at the end) |
| name pattern | vzdump-lxc-<vmid>-YYYY_MM_DD-HH_MM_SS + .tar.dat / .tmp; the timestamp is the host's local time |
| the task | UPID:demo-hp:000A8C2E:000DFB52:6ACEEC0A:vzdump:9201:felhom-agent@pve!agent: — started by the agent's API token |
| after the boot | 02:52:56 the agent: „a vzdump backup is genuinely in-flight — leaving the lock" — so the dead task WAS in tasks?source=active 1 min after the boot. How long it stays: not recorded → C3 |
| who may delete | the files are root's in a root-owned directory; the agent runs as felhom-agent → it cannot delete them itself |
C2 — predictions (written before the run)
| Question | Prediction |
|---|---|
left on disk after kill -9 of the vzdump worker |
the same pair (.tar.dat + .tmp); the tar/zstd children may keep writing for a moment, then die on the broken pipe |
StorageContent (backup) lists it |
no — Proxmox lists vzdump-*.tar.zst/.vma.zst etc.; .tar.dat is a temporary name |
| the active-task list keeps the task | no — the PVE task daemon sees the worker die and ends the task with an error (unlike a power cut, where nobody ends it) |
lock: backup on the guest |
stays (vzdump's cleanup did not run) |
| the next backup | the space preflight sizes from the last COMPLETE archive (5.6 GB) and passes while space is free; the partial still occupies the disk |
C2 — a partial without a power cut (C2/; 9201, because 9202 has no agent link — its „back up now" cannot run)
Backup pressed through the product 08:07:30 (POST /api/guest-backup/trigger, 200); at 1.5 GB written the vzdump worker
(task UPID:…:vzdump:9201:…) was killed -9 at 08:09:44.
| Question | Prediction | Measured |
|---|---|---|
| left on disk | .tar.dat + .tmp |
yes — vzdump-lxc-9201-2026_10_14-10_08_05.tar.dat + .tmp/, root 0644 / 0755, no .log |
| the writers | die on a broken pipe | no — tar and zstd (children of a bash -c, none named „vzdump") kept writing for 4 min 40 s, to 5.6 GB (a whole archive's size), then exited. The file was never renamed. A „no vzdump process" check alone would call a file dead while it is being written |
StorageContent lists it |
no | no — only the complete …09_28_50.tar.zst is listed |
| active-task list keeps the task | no | no — gone within seconds (the PVE daemon ended it: „backup job failed" 08:09:46) |
| lock | backup stays |
the agent's 10-min janitor cleared it at 08:10:18 while the orphan tar still read the snapshot; its snapshot delete failed („contains a filesystem in use") → lock: snapshot-delete; the next janitor pass (08:20:18) cleared that lock and removed the snapshot |
| the next backup | passes the preflight | none came: a failed manual press is not retried (the night loop retries; this one did not within 30 min) |
| clean-up | — | the 5.6 GB partial removed by hand at 08:41 (host root 78 % → 63 %) |
C3 — one real power cut mid-write (9201's backup, pressed through the product)
Predictions (written before the cut):
| Question | Prediction |
|---|---|
the dead vzdump task in tasks?source=active after the boot |
yes, and it stays there (nobody ends a task whose node died) — for the whole 15 min |
BackupRunning (the agent's startup stale-lock line) |
true → „genuinely in-flight — leaving the lock"; Proxmox's start-all removes the lock anyway |
the guest restart request (POST /api/server/reboot, the dashboard's) |
refused „backup running" while the dead task is listed |
| the household power-off request | refused — „too soon" first (host up < 10 min), and on demo-hp „other guests" anyway (9202 and Tester 1 run); it cannot reach the backup check here |
| files | .tar.dat + .tmp stay, nothing removes them |
Measured (C3/): backup pressed 08:42:25; at 1.5 GB the cut: 08:44:08.089 off → 08:44:18.165 on (plug countdown).
Boot btime 1791967481; the dead task's start 1791967345 (136 s before the boot).
| Question | Prediction | Measured |
|---|---|---|
| dead task in the active list | stays all 15 min | listed RUNNING at 08:44:57; gone at 08:45:30 — Proxmox's own start-up clears it within ~30–60 s of the boot (at 10:45:00 local pvesh/pve-guests: „removed left over backup lock from '9201'!") |
BackupRunning |
true | true at the agent's start (08:44:59: „genuinely in-flight — leaving the lock") — the agent starts inside that short window |
guest restart request (POST /api/server/reboot, 08:47:09) |
refused „backup running" | 202 — it restarted 9201. The restart route never asks BackupRunning; the guestpower.go caller is the guest-power WATCHDOG (it starts stopped guests), not the restart |
| power-off request (08:48:41) | refused | 404 — demo-hp's 9201 runs controller 0.309.0, which has no power-off route; the guard can be exercised only after the floor raise (Part E) |
| files | stay | stay all 15 min: …10_42_25.tar.dat 1.16 GB (1.5 GB at the cut — the unwritten tail lost) + .tmp/ |
| lock | Proxmox removes it | removed by Proxmox at boot |
What this changes (STOP-rule check): the night's mechanism is the same — the partial is the damage, Proxmox does not list it as a backup. R-1008's window is short (≈ 1 min after a boot): the startup stale-lock pass, the first guest-power watchdog sweep and the controller supervisor in that minute see a dead task as running. D1 stays (one helper, cheap, correct); its live effect is small. D2 is the fix that matters.
C4 — a backup that runs out of space (prediction before the run)
On scratch guest 9202, vzdump 9202 --storage local --mode snapshot --compress zstd run as root on the host (9202 has no
agent link; the question is vzdump's own behaviour), with the host root filled by a test file to ~600 MB free.
Prediction: vzdump fails with „No space left on device" and removes its own .tar.dat/.tmp (its error path runs,
unlike a power cut); the lock clears; nothing is left.
Measured (C4/, host root filled to 600 MB free by C4-fill-test.bin, removed after):
| Run | Result |
|---|---|
vzdump 9202 (dir storage → vzdump's rsync path, suspend mode) |
„No space left on device" in the first sync; vzdump removed its .tmp; only vzdump-lxc-9202-…11_01_36.log (2 KB) left; space back |
vzdump 9201 (LVM-thin → the snapshot + tar … | zstd > .tar.dat path, the night's path) |
zstd: error 70 … No space left on device; vzdump removed its .tar.dat; only the .log left. But the full disk broke LVM: „Failed to write metadata … Backup of volume group pve metadata failed"; the vzdump snapshot was half removed (disk-0's gone, disk-1's left) → lock: snapshot-delete |
| the agent's janitor after it | 11:14:59 local: cleared the lock, then its snapshot delete failed („Failed to find logical volume pve/snap_vm-9201-disk-0_vzdump") — it would fail the same way every 10 min, the guest's snapshot list stuck. Fixed by hand 09:19 UTC: pct unlock 9201; pct delsnapshot 9201 vzdump --force (removed disk-1's snapshot LV, cleared the entry) |
D3 decided by C4: vzdump cleans its own partial on ENOSPC, so the preflight's sizing (R-1006 part 2) costs only wasted
time, never a leftover; spaceFits stays as it is. The real danger C4 shows is different: a host root that fills up
breaks LVM's own metadata writes — the night's 12 GB partial took demo-hp to 92 %. Removing a dead partial (D2) is what
keeps the root disk from filling. New finding: the janitor's snapshot delete has no --force path for a half-removed
snapshot (row).
F — can Immich repair the power cut's damage by itself? (R-1007, spike only)
Prediction (before the run): Immich's thumbnail job with „missing" does NOT redo a zero-byte thumbnail (the database says it exists); the „all" (force) run does; nothing in Immich restores a deleted original, but a storage/integrity report lists it.
Measured (F/immich-repair.txt, F/immich-delete-then-missing.txt; Immich v3.2.4 on 9202, 30 photos, deployed and
removed through the product):
| Act | Result |
|---|---|
| 10 assets' thumbnail + preview truncated to 0 bytes; 1 original deleted (the night's damage, R2) | the API serves 0-byte thumbnails/previews; the deleted original: 404 |
integrity jobs integrity-missing-files, -untracked-files, -checksum-mismatch (POST /api/jobs) |
the deleted original is found: GET /api/admin/integrity/report?type=missing_file lists it with its asset id. The zero-byte thumbnails are not reported (the checks cover originals) |
thumbnails „missing" (PUT /api/jobs/thumbnailGeneration {force:false}) |
does not fix a zero-byte thumbnail — the database says it exists |
| zero-byte files deleted first, then „missing" | still not fixed (Immich decides from its database, not the disk) |
thumbnails „all" ({force:true}) |
fixes all 10 — every thumbnail regenerated (30 photos: 16 s; a family library of 50,000: hours of CPU) |
| the deleted original | nothing in Immich restores it — only a backup |
Prediction: right on „missing" and „all"; right that nothing restores an original; the integrity report exists in this
version (/api/admin/integrity/…) and finds the missing original.
Proposal for R-1007 (an operator decision): after an unclean boot, run Immich's „integrity-missing-files" job and show its report (operator page, later the household), and run „thumbnails: all" in the night window. Cost: one catalog-level hook and CPU in proportion to the library; a missing original then needs a restore from the night backup. If nothing is done: broken previews stay until someone forces a regeneration by hand; a lost original stays unnoticed.
B2 — can Immich rebuild single photos? (R-1007, after the operator ruling „fix only the broken photos", spike only)
Immich v3.2.4 on scratch guest 9202, deployed and removed through the product; 30 bench photos (B2/).
What the running server answers (asked, not remembered): POST /api/assets/jobs with a bad body → 400 listing the
names refresh-faces, refresh-metadata, regenerate-thumbnail, transcode-video; body {"assetIds": [<uuid>…], "name": "regenerate-thumbnail"}, permission AssetUpdate (an admin or the owner's key). In the server code
(dist/services/asset.service.js run()): one AssetGenerateThumbnails job per id given — nothing else is queued.
Path layout: <upload>/thumbs/<user id>/<a[0:2]>/<a[2:4]>/<asset id>_thumbnail.webp and …_preview.jpeg (+ one
.immich marker file). Mapping an empty file to its asset: the first 36 characters of the file name ARE the asset
id. A box finds the broken photos from the disk alone: find <upload>/thumbs -type f -size 0, cut the names to 36
characters, unique.
| Step | Result |
|---|---|
| damage | assets 1–5: thumbnail → 0 B; assets 6–10: preview → 0 B; the API served the 0-byte files |
| found from the disk | 10 zero-byte files → 10 asset ids (the mapping above) |
POST /api/assets/jobs regenerate-thumbnail for those 10 |
204; no zero-byte file left after 1.55 s; queue idle after 16.9 s (5 s polling); CPU 5.3 s (all Immich containers, cgroup usage_usec) |
| the 10 assets | all 20 files rewritten (the undamaged sibling too), none empty; the API serves real thumbnails and previews |
| the other 20 assets | 0 of 40 files rewritten (mtimes before/after) |
positive control: thumbnails „all" (force=true) |
rewrote 40 of 40 of those same files (the 41st counted file is the .immich marker); CPU 16.5 s for 30 photos |
| teardown | Immich removed through the product; no folder, container, volume or image left; 9202's standing apps untouched |
Answer: yes — v3.2.4 rebuilds single photos. Cost ≈ 0.5 s CPU per photo here (a family library with 80 broken photos,
like the night's R2: well under a minute) against „all" in proportion to the whole library. A lost original is still
found only by integrity-missing-files and restored only from a backup.
E — the live proof on demo-hp (agent 0.156.1 + bundle, delivered by signed jobs)
Vouched by the operator 2026-10-14 (read back from /configuration): agent 0.156.1 83c7da27…, golden 0.310.0 2110d5e4…,
min_agent 0.155.0, global floor 0.310.0. demo-hp: agent_update 0.156.1 (11:00 UTC) → agent_config_update 0.156.1-step1
(11:15, „written=1", probe 70/71 — the helper not yet there) → agent_config_update 0.156.1 (11:30, „BUNDLE WROTE
/usr/local/sbin/felhom-vzdump-partial-clean (new)", probe 71/71). The new agent's first start (11:00) already FOUND C3's
partial and called it dead — and left it, „the root helper refused: sudo: a password is required" (the helper came with the
bundle). Real sudo after the bundle: E/sudo-live-demo-hp.txt (the two shapes allowed, six attacks refused).
Prediction (before the cut): after the boot the agent's start-up sweep removes BOTH partials (C3's from 08:44 and this one), logged „removed a dead half-written backup … written before this boot"; the dead vzdump task is logged once as „worker process is gone" and the start-up stale-lock pass clears the lock itself (or finds Proxmox already did); the next backup runs and succeeds; host root returns below its use before this cut.
Measured so far (11:42 UTC):
| Step | Result |
|---|---|
| floor 0.310.0 | not delivered: each demo customer (demo-felhom, demo-hp, Tester 1) has its OWN floor 0.309.0 (set 2026-10-12 16:07), which wins over the global one (hub log + floor_raise_skipped mail). demo-hp's guest stays on 0.309.0 |
| backup pressed (11:42:33 UTC) | the pre-backup cleanup worked live: removed a dead half-written backup … vzdump-lxc-9201-2026_10_14-10_42_25.tar.dat (C3's 1.16 GB) and its .tmp, through the root helper (sudo … felhom-vzdump-partial-clean local 9201 …); each removal took ~7 s (the helper's /proc scan); then „space preflight passed" (E/agent-cleanup-before-backup.txt) |
| the backup itself | failed in 1 s: lvcreate snapshot 'pve/snap_vm-9201-disk-1_vzdump' error: Failed to suspend thin snapshot origin; kernel: „device-mapper: thin: Creation of new snapshot 4 of device 2 failed" (E/failed-backup.log) |
| why | the thin pool holds an orphan thin device 4 — a 19 GiB snapshot of 9201's data disk (device 2) created at pool time 138 — which LVM does not know (LVM: devices 1, 2; transaction 559 on both sides). It is what C4's full root disk left: the snapshot cleanup could not write LVM's metadata. Read with a metadata snapshot (thin_ls --metadata-snap, reserved and released; E/thin-ls.txt) |
| consequence | no whole-guest backup of 9201 on demo-hp can run until device 4 is deleted (every snapshot create picks id 4). Caused by this session's C4 test |
Repair (operator „B2 - A", 12:02 UTC): dmsetup message pve-data-tpool 0 "delete 4" → thin_ls shows devices 1 and 2
only, pool transaction 559 unchanged, LVM agrees (E/repair.txt). Identity, not size: device 4 was the one id LVM does not
know, a snapshot (CREATE_TIME 138) of device 2.
The cut (E/): fences clear 12:04:48; backup pressed through the product 12:04:48 (POST /api/guest-backup/trigger,
200); the apps were stopped for the snapshot and started again at 12:05:30 (snapshot create worked — the repair held);
at 1.53 GB written, plugcut.py --plug hp --off-seconds 10: 12:06:52.325 off → 12:07:02.409 on (plug countdown;
watchdog not needed). Root use at the cut 26,249,101,312 B; before the press 24,720,375,808 B. SSH back 12:07:45.
| Prediction | Measured |
|---|---|
| dead task logged once as „worker process is gone" | yes — 12:07:44 „backup-in-flight: a vzdump task is listed as active but its worker process is gone — it is dead, not running" (the agent started 12:07:42, inside the 30–60 s window in which Proxmox still lists the dead task — the R-1008 case, now judged right) |
| the start-up stale-lock pass clears the lock, or finds Proxmox did | both, in the same second: the agent decided „clearing a stale backup lock" and ran pct unlock 9201; Proxmox's start-all had just removed it („removed left over backup lock from '9201'!", 12:07:44) → the agent logged ERROR „pct unlock failed … no lock found". Harmless (the guest started 12:07:47), but an ERROR for an outcome that is the goal — R-1013 |
| the start-up sweep removes the partial | yes — 12:07:48 „removed a dead half-written backup … vzdump-lxc-9201-2026_10_14-14_05_21.tar.dat 964,239,360 B (written before this boot)" and 12:07:52 its .tmp — through the root helper. 1.53 GB were written before the cut, 0.96 GB survived (the unwritten tail lost, as in C1) |
| guards do not refuse because of the dead task | the start-up sweep IS a BackupRunning caller: it went ahead only because the dead task was judged not running. The household power-off request at 12:09:53 (uptime 147 s) was refused too_soon_after_boot only (E/requests.txt). The guest restart is not guarded by BackupRunning at all (C3) |
| the next backup succeeds | yes — pressed 12:10:06, „found old vzdump snapshot (force removal)" (vzdump removed the cut's LVM snapshot itself), archive vzdump-lxc-9201-2026_10_14-14_10_50.tar.zst 5,650,197,172 B at 12:17:08; the older archive pruned (keep-last 1) (E/next-backup.txt) |
| host root back below its use before this cut | yes — 24,689,217,536 B after the boot sweep; 24,691,134,464 B after the new backup (before the press: 24,720,375,808 B) |
| no orphan thin device this time | yes — pool transaction 567, LVM lists only vm-9201-disk-0/1 |
E/demo-hp-journal-boot.txt is the whole boot journal (scanned: no key, token or password). R-1006 and R-1008 are proven
live on demo-hp.
Delivery to the other two boxes (E/delivery-tester1-n100.txt): signed jobs agent_update 0.156.1 (12:21:24 UTC) →
agent_config_update 0.156.1-step1 (12:35:37) → agent_config_update 0.156.1 (12:50:14). Tester 1: agent 0.156.1 committed
12:24, bundle done 12:53 (probe 71/71); N100: agent 12:34 (it reports every 15 min), bundle done 13:04 (probe 71/71). On
both, real sudo -l -U felhom-agent allows the cleanup shape and refuses a finished archive (.tar.zst). The operator
cleared the per-customer floors (demo-hp and demo-felhom 12:00, Tester 1 12:44 UTC): every guest runs controller 0.310.0.
Both plugs ON: the HP relay back on by its countdown (12:07:02, E/plugcut.log), the N100's never cut; both hosts answer.