## Device run: all eleven phone steps observed, and step 12 in a browser
Walked on **raven** (Pixel 6 Pro, Android 17), installed build `tools.relay.staircase.debug` md5 `02246fee295a5f8d9d20a73a16f7952f`, one package at both ends, battery 80%, the app's networking and `chain3` restored and read back as disabled. Both budgets were turned back off at the end, so the rig is as it was found.
The walked tree is now tip `c7591861`, which is the run's tree plus a merge of `main` at `56631063` (below).
### Three defects found before the steps proper, all fixed in the branch
1. **The Storage row drew nowhere.** A row on the Settings page takes its glyph and tile from `settingsSections()`, and `settingsSectionFor` returning null makes the row `return` rather than draw. `Route.Storage` was in neither that list nor `Route.isSettingsPage`, so the page had no row on Settings and — under the Staircase look, where the sidebar *is* the way in — no way in at all on a desktop. `SettingsSectionsTest` exists to catch this and could not: it holds `settingsSections()` against a route table written out by hand inside the test, so a page in none of the three lists agrees with itself and the gate stays green. Found by the browser walk, now registered in all three. **The shape of that guard is worth someone's attention and I have deliberately not redesigned it here.**
2. **A chat's figure was one no budget could act on.** raven drew `Rig 2be0071 — 87 messages · 9.0 MB`. The nine megabytes were its kept MLS states, which retention never touches, so the row invited somebody to set a budget against a number nothing on the page can move, and sorted that chat above chats the budget could actually shrink. The figure beside a chat's message count is now what its *messages* weigh and the list sorts by that; the database total above still accounts for everything. The same chat now reads `87 messages · 20 KB`, and `nym-rig` took the top of the list — which is the chat a 64 KB budget then pruned.
3. **The backup offer was skipped for a stale backup.** It was shown only when no backup was recorded at all. raven's account had one from **Mon 19 Oct** — a fortnight of messages that backup does not hold, which is the case the warning matters most in. It is now shown before the first prune an account has ever run on the device, whatever the backup state, and the dialog names the last backup rather than deciding on a person's behalf that it was recent enough.
### What the steps showed
- **1.** Settings → Account & devices → **Storage**, reading `21.8 MB used`. The page: `21.8 MB on this device`, `6.7 MB of that is free space inside the file.`, the chats biggest first with counts and sizes, the attachments note, both budgets at `Everything`/`For ever`, and `Nothing is deleted while both of these are off.`
- **2.** `nym-rig` already carried 851 messages; 80 messages of ~250 characters through `fill-chat.py` took it to **931 messages · 73 KB**, past the smallest budget.
- **3.** 64 KB, then **Free up space now** → "Back up first?" with `Last backup Mon 19 Oct` under it → **Free up space anyway** → `44 messages removed from 1 chat. 7.0 MB given back to this device.`
- **4.** `nym-rig`'s chat list line unchanged (`22:47`, `You: Silo-fill …`); `chaps-rig` still 4, `surcingle-rig` still 1, the tab still `Chats, 5 unread`. Re-observed after the much larger sweep below: **883 messages removed from 34 chats** and every line and badge still unchanged.
- **5.** `nym-rig` fell to `887 messages · 63 KB · pruned`, inside its budget. The database file went **22,872,064 → 15,507,456 bytes** (`run-as … ls -l files/staircase/staircase-56cbd175.db`), a drop of exactly the 7.0 MB reported, and the page's free-space line disappeared — the freelist is empty after the `VACUUM`. That is the `VACUUM` observed, not asserted.
- **6.** Scrolled to the top of `nym-rig`: *"Older messages are no longer on this device."* above the first day separator, read off a screenshot and not a dump.
- **7.** See below — this one needed a second run to mean anything.
- **8.** Quiet chats untouched by the size budget (`wrangle-a` 250 messages, `wrangle-b` 267, `prova italiana` 4, all unchanged). Then **7 days** → `883 messages removed from 34 chats. 444 KB given back to this device.` `chaps-rig` (messages from today) untouched; chats whose whole history was older than a week went down to one message each and are marked `pruned` — the row the chat list draws, which is the clamp doing its job and the documented consequence of the age limit not being held back by the comfort floor.
- **9.** Sent `silo-last-message-before-the-sweep`, swept at once (`39 messages removed from 1 chat. 48 KB given back`): the message is still in the timeline and still drives the chat list line (`23:06`).
- **10.** Networking taken off the package, a send queued (`Waiting for a connection`, `1 message waiting`), then a sweep that **did bite** — `46 messages removed from 1 chat. 44 KB given back` — and the queued send was untouched. Networking restored: it went out within 20 s.
- **11.** Force-stop and reopen: `14.4 MB used · keeping 64 KB per chat`, with 64 KB and 7 days still selected. Same build installed over the top: same.
- **12.** `./gradlew :app-web:e2eStorage`, 19 checks, all passed in headless Chromium against the built bundle. The walk is in the branch, not in my scratch directory.
### Verify steps corrected, superseding the body
The body cannot be edited, so these supersede it:
- **Step 2** said 300 messages. 80 was enough, because the chat already held 851. The step's substance — a chat over 64 KB at the top of the list — is what was observed.
- **Step 3** gave "this account has no backup on this device" as the reason the dialog appears. That is no longer the rule and was not true here: the account had a backup from 19 Oct and the dialog appeared anyway. Read it as **"Because this account has never pruned on this device, 'Back up first?' appears, naming the last backup"**.
- **Step 7** is not worth walking as written. The first run of it reported `recovered 6` and proved nothing about the floor: the prune's cut had landed *below* the oldest kept snapshot's cursor, so the catch-up could not have offered a pruned row even with no floor at all. The step needs a chat pruned **above** its oldest kept snapshot. After the 7-day sweep, the second run gave what the step is for:
```
newlay.ingest: ec61ad6d-…: 94 message(s) below the prune floor (157) not stored
newlay.runtime: full catch-up for ec61ad6d: … state #7 (healthy, epoch 7, cursor 62) over 880 message(s) (63..942): stored 0
newlay.runtime: full catch-up for ec61ad6d: 880 message(s) from cursor 62, 2 kept state(s), recovered 0
```
94 messages decrypted and refused, `recovered 0` reported honestly, the chat unchanged at 786 messages, and a message sent afterwards arrived normally. Without the floor that one tap puts 94 pruned messages back.
- **Step 12** said "a row per chat". A fresh browser profile has no chats, and the page deliberately leaves the list and its note off rather than drawing an empty card under a heading. Read it as **"shows a size for the database, and the chat list only when there are chats"**.
### A reading, not a defect
`44 messages removed from 1 chat. 7.0 MB given back to this device.` — the 7.0 MB is what the *file* shrank by, which includes free space the `VACUUM` reclaimed that earlier deletes had left behind. It is not a claim that 44 messages weighed 7 MB, and it is deliberately the number a person wants: reporting only the pruned bytes (about 10 KB) would understate what the device actually got back.
### main moved under this branch
`main` is now `56631063` (manta: stickers). Merged in here and re-gated at `c7591861`. The only conflict was `CHANGELOG.md`, resolved by placement — main's bullet first, mine appended, both verbatim.
**No Verify step re-walks**, judged on the hunks rather than the file names:
- `ChatScreen.kt` — main's hunks are the `Composer`'s sticker arguments, `sendSticker`, and `MessageRow`'s sticker drawing. None is in the `items(entries)` tail where the pruned-history note is added, and the note is intact in the merged file.
- `GroupListPane.kt` — one hunk, in `TimelineRow.preview`: a message that is *nothing but a sticker* draws as "Sticker". Step 4 reads a text line and two numeric badges; a lone-sticker branch cannot reach either.
- `Staircase.kt` — `noteStickerSent` added, a separate hunk from the driver wiring.
- `AppPrefs.kt` and the ten catalogues — additive on both sides.
### Checks at `c7591861`
Full ten-module `jvmTest` line plus `:app-desktop:compileKotlin` and `:app-android:compileDebugKotlin` (BUILD SUCCESSFUL, 4m 56s); `:cordn-store:`, `:cordn-sync:`, `:app-core:` and `:ui:wasmJsBrowserTest`; `:app-web:e2e` (152 checks) and `:app-web:e2eStorage` (19 checks).