Repository navigation
[Feature] Reclaim the stale post-migration message blob on a per-key timer, with a manual clear #962
Description
The migrated blob goes stale and is never reclaimed
After #920, a message-history key that migrates to rows keeps its blob forever. Verified: every blob write sits in the fallback branch behind an early return in the migrated branch (channel_message_store.dart:175,235, message_store.dart:111,152). So the blob is a frozen snapshot taken at the migration moment, not a mirror. Nothing updates it, and nothing ever deletes it.
Clearing history is not affected: clearChannelMessages removes both copies (deleteEverywhere plus deleteMessageRows), so there is no stale-history-after-delete hole. Checked specifically, because that would have been worse than wasted space.
Measured
On the owner's live 70.9 MiB database:
| reclaimable now (40 migrated keys) | 16.2 MB |
| still authoritative (98 unmigrated keys) | 23.8 MB |
| contacts / discovered / path history (blob IS the storage) | 2.0 MB |
DELETE of the 40 blobs, no VACUUM |
+0 bytes |
DELETE + VACUUM |
54.9 MiB, -18.5 MiB, 0.6 s, integrity_check ok, all 28,386 rows intact |
Two things that shape the design:
DELETEfrees nothing on disk. SQLite marks pages free for reuse.VACUUMis what shrinks the file.- Migration is lazy and per key. 40 of his keys had migrated and 98 had not, because those radios had not been reconnected. An install-wide clock would be wrong.
Who it affects
Schema 2 has not shipped (v1.6.0, 2026-10-05, predates 4d4d44a6), so no user has this today. Once it ships, all ~175 active Android installs (173 on 2026-10-17) plus desktop hold their history twice. Severity scales with history size, and the users with the largest histories are by definition the ones who hit the original ANRs, since the blob re-encode cost was proportional to history.
The design, owner decision 2026-10-22
- A per-key time marker written at migration.
- X time after a key migrates, its stale blob is cleared automatically.
- Plus a manual "Clear Stale History Now" action, so a user need not wait.
Owner's rationale, recorded because it sets the acceptable-loss bar: "Considering how many messages have gotten merged, lost, or messed up because of the lack of radio separation, loss of everything isn't a big loss. MC stock doesn't have this kind of message retention anyway, people are used to not having a consistent, persistent store of messages going back indefinitely in time."
What the snapshot does and does not protect, so this is not mistaken for removing a backup:
- It protects the pre-migration history against a migration or read-path bug found later.
- It protects nothing that arrived since. That traffic is single-copy from the instant it lands, blob or no blob.
- Its coverage decays as new messages accumulate.
One safety property that makes deletion tolerable: if the migrated marker were ever lost after the blobs are gone, the empty-blob path at blob_store.dart:340 re-marks the key and reads rows, so history survives marker loss.
Related
- [Plan] Storage: remove O(history) work from the message save path (A then B) #917, [Epic A] (#917) Move the blob read-modify-write off the UI isolate #919, [Epic B] (#917) Normalise messages into rows #920 the storage rewrite this follows.
- Schema-2 migration is one-way: a pre-#917 build cannot open a migrated database, and the plan says it can #943 downgrades are unsupported, which is what permits deleting the blob at all.
- Disclose the one-way storage upgrade in the release texts for the first version carrying schema 2 #953 release-text disclosure for the first version carrying schema 2.
- Retention limit (how many days to keep at all) is deliberately a separate, later feature.
Plan of record to follow; this is not approved for build yet.
Agent Mail: TealGlen
Activity
docs(#962): pin that VACUUM touches nothing the sweep kept
docs(#962): record the owner's four decisions and resolve the placeho…
diffwireauto commentedon Oct 23, 2026
Owner decisions, 2026-10-22
All four as recommended, now recorded in the plan's section 9 with the reason for each:
__msgrows_migrated_atblob, leaving the hot marker path untouchedEvery
Xplaceholder in the plan is now the literal 30 days, and the acceptance bar tests the boundary at 29 days rather than only the middle of each range.Also added as diagnosis row 11, from the owner's question about whether
VACUUMwould wipe out non-migrated blobs: it cannot. Measured on a copy, 40 migrated blobs deleted and 291 keys kept including all 98 unmigrated history blobs, each hashed before and after: 0 missing, 0 byte-changed, 0 unexpected, rows 28,389 unchanged,integrity_check ok. The consequence is the useful part and is now stated in the plan: all the risk is in the DELETE selection and none of it is inVACUUM, which is why the safety budget sits on the per-key re-verify and the key-pattern restriction.Plan PR: #964.
epic_chainis filled in after it merges and before the grant is minted.Agent Mail: TealGlen
docs(#962): plan of record for reclaiming the stale post-migration blob
docs(#962): pin that VACUUM touches nothing the sweep kept
docs(#962): record the owner's four decisions and resolve the placeho…
docs(#962): fill epic_chain with the three Epic issues
feat(#966): mock-ups, a confirmation that states the scope, and the e…
feat(#965): per-key migration timestamps and the stale-blob sweep
feat(#966): Clear Stale History Now, with refusals reported
feat(#966): mock-ups, a confirmation that states the scope, and the e…
diffwireauto commentedon Oct 24, 2026
Feature complete
All three Epics merged to dev via #973, head
5b061924. Owner-validated on Windows 2026-10-23.What shipped
__msgrows_migrated_atblob, leaving the hot marker path untouched.VACUUM, only when something was deleted, sinceDELETEalone frees nothing.Measured on the owner's install
The 98 untouched blobs are the line that matters: those radios have not been reconnected since the storage upgrade, so their history is still single-copy and the sweep correctly left it alone.
Two things found while building, not before
eligibleByAgerule, with the automatic sweep still refusing and an explicit press not.missingFromRowstrusted the converter's own output as the measure of the converter. A callback that silently dropped a record would have had everything it did produce found present and the blob holding the only copy of the dropped one deleted. Now cross-checked against an independent count, as the migration gate always was.Process change
The plan gained B0: mock-ups first, owner-approved before any of it is built, after this Feature's UI was built and installed on both his machines without him ever seeing the design.
epic-b-storage-mockups.htmlis committed beside the plan in the format the firmware initiative uses.Carried forward
#963, the retention limit. Its fourth open question is the one to design first: trimming rows makes the stale blob a superset of the rows, which is this sweep's refusal condition, so the two must agree on authority or a trim permanently blocks reclamation the way #952 did.
Agent Mail: TealGlen