Skip to content

Stop a sign-out from wrapping Chrome's file-system usage to about 4 GB - #492

Merged
cdeutsch merged 5 commits into
mainfrom
claude/priceless-franklin-cf4e8e
Nov 12, 2026
Merged

cdeutsch merged 5 commits into
mainfrom
claude/priceless-franklin-cf4e8e

Conversation

@cdeutsch

Copy link
Copy Markdown
Contributor

Chrome never saves bytes written through a FileSystemSyncAccessHandle to the origin's usage counter, so after a restart it has forgotten them. The library and its search index are written entirely that way (wa-sqlite's OPFSCoopSyncVFS), and the wipe on sign-out, account deletion or a hand-over removed them with a plain removeEntry, which subtracts their real size from a counter that never held it. The counter went negative and Chrome reported it as about 4 GB, as seen on two Macs. This is Chromium issue 436552046, which gives the same workaround.

It matters beyond Settings: where a browser caps a site's quota below the wrapped figure (Brave, at 2 GiB), the origin reads as over quota, and Keep offline and files added offline, which ask estimate() for room before writing, refuse.

What changed

  • wipeStoreFiles hands the database files to a new small worker (store/shrink-worker.ts, logic in store/shrink.ts), which empties each file through a sync access handle (truncate(0), flush, close) and then removes it, so the removal subtracts nothing.
  • Anything the worker does not report removed is removed on the page as before: no workers, no sync handles, a file another tab holds at that moment, a worker that errors or runs past two seconds. A failed worker is waited on once per wipe, not once per retry.
  • The files directories are untouched: their bytes are written with createWritable, which Chrome counts properly.
  • The close broadcast, CLOSE_WAIT_MS, the retries and the per-account rules are unchanged. The worker chunk is in the service worker's precache, so an offline sign-out still uses it.
  • contract.json records the wipe's new local ShrinkState as fitting: the channel message between builds is unchanged.

Verified

Headless Chrome stable, a throwaway profile, the real app on a worktree dev server with the demo seed, sign-out from the account menu, Chrome quit gracefully between sessions and the saved counter (File System/<n>/t/.usage) read from disk.

Case Before After
Library written in a session that started from a valid saved counter, then a restart and a sign-out saved -1,760,736; next launch reports 4,293 MB saved 4,640; next launch reports 0.00 MB
Library written before Chrome had saved any counter, so it measured the disk at restart saved 3,476 (exact) saved 1,768,850 (about 1.8 MB high)

The second row is the trade: when Chrome has measured the disk, the emptied bytes stay counted. Nothing on the page can tell the two cases apart, so the counter ends high by at most the library's size at the last measurement, instead of below zero. shrink.ts records this.

Unit tests cover the worker path and each fallback (no worker, worker errors, worker silent, no sync handles, a held file retried). pnpm -r typecheck and the web suite (2,771 tests) pass.

Not covered: the fallbacks in a real browser, Safari and Firefox, the two-second bound on a phone, and account deletion or a hand-over in Chrome (they share the wipe with sign-out, which was run). This does not fix Chrome's undercount after a restart; it only stops the wraparound.

🤖 Generated with Claude Code

cdeutsch and others added 5 commits November 12, 2026 12:44
Chrome never saves the bytes written through a sync access handle to the origin's usage counter, so after a restart it has forgotten them. The library and search index are written entirely that way, and the wipe on sign-out, account deletion or a hand-over then removed them with a plain `removeEntry`, which subtracts their real size from a counter that never held it. The counter went negative and Chrome reported it as about 4 GB, as seen on two Macs. This is Chromium issue 436552046 (https://issues.chromium.org/issues/436552046), which gives the same workaround.

It is more than a wrong number in Settings: where a browser caps a site's quota below the wrapped figure (Brave, at 2 GiB), the origin reads as over quota, and Keep offline and files added offline, which ask `estimate()` for room before writing, refuse.

`wipeStoreFiles` now hands the database files to a small dedicated worker (`shrink-worker.ts`), which empties each one through a sync access handle before removing it, so the removal subtracts nothing. Anything the worker cannot do (no workers, no sync handles, a file another tab holds, a worker that errors or takes over two seconds) is removed on the page as before, and the close, retry and per-account rules are unchanged.

Checked in a throwaway Chrome profile, writing the library in a session that started from a valid saved counter, then restarting and signing out: the saved counter went to -1,760,736 bytes (reported as 4,293 MB) before this change and stays at 4,640 after it. When Chrome has instead measured the disk at launch, the emptied bytes stay counted and the counter ends about the library's size high (1.8 MB for the demo library) where a plain removal was exact; `shrink.ts` records that trade.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A wipe tries the store files up to four times while other tabs let go of them, and each try started a new worker and could wait the full two seconds for it. A worker that never answered could therefore hold a sign-out for about eight seconds beyond the bound the comment in `wipeLocalStores` promised.

Each wipe now carries a `ShrinkState`: once its worker fails to start, errors or runs past `SHRINK_WAIT_MS`, the later tries go straight to the page's plain removal. The state is per wipe, so the next sign-out tries the worker again. The comment now counts that one wait in the bound.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e's exclusivity.

The comment on `shrinkAndRemove` read as if any file another tab had open would refuse the sync access handle and be skipped. wa-sqlite's cooperative VFS lets a file go between transactions, so an idle library elsewhere is usually emptied and removed, just as the plain `removeEntry` removed it before. The comment now says so, points at the close the wipe broadcasts first, and names the brief window after `close` in which another engine could take the emptied file back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The `flush()` after `truncate(0)` reads as if durability mattered, which it does not: the file is removed straight after. It stays because truncate, flush, close is the sequence the saved counter was measured with in Chrome, and a sequence without the flush has not been measured.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The contract test asks about any new declaration in `wipe.ts`, since the wipe's message on the library channel is shared between builds. `ShrinkState` is not part of that message: it lives inside one tab's wipe and only decides whether that wipe starts its worker again. A tab on the previous build and one on this build still exchange the same close message, so no page needs to reload.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added the web label Nov 12, 2026
@github-actions

Copy link
Copy Markdown

Preview: https://unbent-pr-492.of2668.workers.dev

Sign in with Continue on the dev instance: a preview cannot register
its own OAuth callback (its host changes every PR), so it borrows a Google
session from https://dev.unbent.app and comes back signed in. A password with an
allowlisted address still works too.

Runs against the shared unbent-dev database, bucket and note stores (the same
notes as the dev instance), so what you change here changes there too. Production
is untouched. The Worker is deleted when this PR closes.

Worker unbent-pr-492 · deployed 2915bae

@cdeutsch
cdeutsch merged commit fd40d47 into main Nov 12, 2026
12 checks passed
@cdeutsch
cdeutsch deleted the claude/priceless-franklin-cf4e8e branch November 12, 2026 20:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant