TunnelVision

Shared/Retention

What the storage caps mean, in the two halves that decision has: RetentionPlanner decides who goes given a directory listing, the caps and now, and CaptureRetention executes that plan — deletes the files it names and prunes the history to its cutoff.

Both were the app’s until M11. The caps only ever applied with the Settings screen in front of the user (on appear and on change), so a tunnel capturing all night could sit over its cap until the next visit — and while nobody is looking is exactly when the directory grows. The extension now applies them too, on rotation: the only moment it is already touching the directory, and the only one that exists with the app closed. That is what moved these two here. A cap cannot mean one thing in front of the user and another behind their back.

What did not move is each side’s plumbing, and that is deliberate. Resolving the App Group container and opening the history are the steps that can fail, and they fail differently for each caller: the app has a screen to explain it on and opens the store lazily per operation, the extension resolved both when the session started. So CaptureRetention.execute takes the listing its caller already had and a way to reach the history, holds no state, and never throws — a half-done cleanup is a result (RetentionOutcome), not a fault, because what was freed still has to be counted.

The app keeps StorageManager (../../TunnelVision/Services) on top: how much is used, and clearing everything. Those are gestures of the user’s, and the extension has nobody to report them to — it counts its own sweeps into PipelineStats instead, which is how a failure with nobody watching avoids being swallowed.

There is a second pair here, for the decrypted content that TLS inspection produces (PlaintextRetentionPlanner / PlaintextRetention, ADR 0007). It is deliberately not a case of the first: the capture caps are the user’s and can be removed, while decrypted content always expires and its ceiling is fixed and unraisable, so sharing a method would have tied the sweep to the isUnlimited shortcut that must never apply to it. Its age is measured over index rows, not files — one file mixes conversations hours apart — so the sequence is prune, then ask which files are still referenced (an orphan is what the prune just left without rows), then delete. Full rationale in ../../docs/spec/plaintext.md § The sweep.

Spec: ../../docs/spec/app-services.md, ../../docs/spec/plaintext.md · Milestones: M9, M11