What happened
#1006 asked for a stream-ceiling refusal to read as recoverable rather than as a generic failure. It was closed by PR #1065 (4820773ad), which delivered the engine and packages/client halves only. The vault browser's consumer was never written, so the behaviour #1006 describes still does not exist for a user browsing a folder.
This is not a regression. It is a residual: the plumbing landed, and the vault browser is the consumer that still does not read it.
Verified state on main (3d087d673)
The engine classifies the refusal and the client carries the classification to the UI boundary:
crates/engine/src/facade.rs:1368-1398 — StreamSlot::acquire enforces MAX_OPEN_STREAMS and refuses with a distinct, recoverable error rather than a generic one.
apps/web/src/engine/snapshotStore.ts:12-17 — code?: string on the failure type, documented verbatim as "carrying the engine's stable code so the UI can classify it."
apps/web/src/engine/snapshotStore.ts:154 — EngineRequestError is unwrapped into { message, code }.
The upload surface now reads the code (landed with #1071):
apps/web/src/hooks/useDropUpload.ts:237 captures error.code onto the upload row.
apps/web/src/components/file-browser/UploadListItem.tsx:32 branches on it — upload.code === 'overBudget' renders as transient rather than terminal red.
The vault browser still does not — this is the whole of what remains:
apps/web/src/components/file-browser/FileBrowser.tsx:20-24 renders every failure identically, a flat <p role="alert">{error.message}</p>. It never looks at error.code.
FileBrowser.tsx:14 gates the listing on const settled = !isLoading && error === null, so any error hides the file list (:33, :42) and the upload drop target (:32). A recoverable refusal blanks the vault browser exactly as a fatal one does.
The net effect is unchanged: a user who opens one file too many sees their entire file listing disappear, replaced by a raw error string with no indication that retrying will work.
Scope
- Classify on
code, not on presence of an error. Split the failure surface into recoverable and fatal. FileBrowser.tsx should branch on the code that snapshotStore already provides — the field exists, is documented for this purpose, and UploadListItem already sets the precedent for consuming it.
- Keep the last-known-good listing on a recoverable failure. Decouple
settled from error === null so a recoverable refusal renders as a notice over the existing rows rather than instead of them. A fatal error may keep the current blanking behaviour.
- Make the notice actionable. A recoverable refusal is retryable by construction — the ceiling clears when a stream closes — so it needs a retry affordance, not just a message.
- Do not invent a code vocabulary in the web layer. The codes are the engine's (
crates/wasm/src/host.rs maps each EngineError variant to its stable string); consume those and fail closed — treat an unrecognised one as fatal — so a new engine code cannot silently render as recoverable.
Gate
- A stream-ceiling refusal renders as a recoverable notice with the file rows still on screen — asserted on the rows, not only on the notice, so a blanked listing cannot pass.
- A fatal failure still blanks the listing and renders as an error. The two paths are distinct and separately asserted.
- An unrecognised code is treated as fatal, proving the classification is closed rather than open.
- Retrying from the recoverable notice re-drives the pull.
web unit. tests/web-e2e/ now exists (#809, #1078), so the classification can additionally be asserted at the e2e tier once its write-path blocker clears (#1096).
Notes
Corrected 2026-08-06 against main @ 3d087d673. Stale before this edit: the claim that rg '\.code\b' apps/web/src returns no hits (it now returns useDropUpload.ts:237 and UploadListItem.tsx:32, landed with #1071, so the upload half of the classification is delivered); every FileBrowser.tsx line reference (the file was rewritten by #1071/#1081); the facade.rs line range for StreamSlot; and the note claiming apps/web has no e2e suite (tests/web-e2e/ landed with #809/#1078). The core defect — FileBrowser.tsx blanking the listing on any error — is unchanged and still real.
What happened
#1006 asked for a stream-ceiling refusal to read as recoverable rather than as a generic failure. It was closed by PR #1065 (
4820773ad), which delivered the engine andpackages/clienthalves only. The vault browser's consumer was never written, so the behaviour #1006 describes still does not exist for a user browsing a folder.This is not a regression. It is a residual: the plumbing landed, and the vault browser is the consumer that still does not read it.
Verified state on
main(3d087d673)The engine classifies the refusal and the client carries the classification to the UI boundary:
crates/engine/src/facade.rs:1368-1398—StreamSlot::acquireenforcesMAX_OPEN_STREAMSand refuses with a distinct, recoverable error rather than a generic one.apps/web/src/engine/snapshotStore.ts:12-17—code?: stringon the failure type, documented verbatim as "carrying the engine's stable code so the UI can classify it."apps/web/src/engine/snapshotStore.ts:154—EngineRequestErroris unwrapped into{ message, code }.The upload surface now reads the code (landed with #1071):
apps/web/src/hooks/useDropUpload.ts:237captureserror.codeonto the upload row.apps/web/src/components/file-browser/UploadListItem.tsx:32branches on it —upload.code === 'overBudget'renders as transient rather than terminal red.The vault browser still does not — this is the whole of what remains:
apps/web/src/components/file-browser/FileBrowser.tsx:20-24renders every failure identically, a flat<p role="alert">{error.message}</p>. It never looks aterror.code.FileBrowser.tsx:14gates the listing onconst settled = !isLoading && error === null, so any error hides the file list (:33,:42) and the upload drop target (:32). A recoverable refusal blanks the vault browser exactly as a fatal one does.The net effect is unchanged: a user who opens one file too many sees their entire file listing disappear, replaced by a raw error string with no indication that retrying will work.
Scope
code, not on presence of an error. Split the failure surface into recoverable and fatal.FileBrowser.tsxshould branch on the code thatsnapshotStorealready provides — the field exists, is documented for this purpose, andUploadListItemalready sets the precedent for consuming it.settledfromerror === nullso a recoverable refusal renders as a notice over the existing rows rather than instead of them. A fatal error may keep the current blanking behaviour.crates/wasm/src/host.rsmaps eachEngineErrorvariant to its stable string); consume those and fail closed — treat an unrecognised one as fatal — so a new engine code cannot silently render as recoverable.Gate
web unit.tests/web-e2e/now exists (#809, #1078), so the classification can additionally be asserted at the e2e tier once its write-path blocker clears (#1096).Notes
main. Read web: the media player has no surface for a recoverable stream refusal #1064's scope with that correction in mind; the two are adjacent but distinct (this one is the vault browser, web: the media player has no surface for a recoverable stream refusal #1064 is the media player).Corrected 2026-08-06 against
main@3d087d673. Stale before this edit: the claim thatrg '\.code\b' apps/web/srcreturns no hits (it now returnsuseDropUpload.ts:237andUploadListItem.tsx:32, landed with #1071, so the upload half of the classification is delivered); everyFileBrowser.tsxline reference (the file was rewritten by #1071/#1081); thefacade.rsline range forStreamSlot; and the note claimingapps/webhas no e2e suite (tests/web-e2e/landed with #809/#1078). The core defect —FileBrowser.tsxblanking the listing on any error — is unchanged and still real.