Skip to content

Storage Crisis Mode: disk-full / IndexedDB failure UX (会话创建失败 toast) #417

Description

@zxch3n

Summary

When the disk (or Chromium storage quota) is exhausted, creating a session shows a toast:

  • Title: 会话创建失败 (chat.failed)
  • Body: raw Chromium error, e.g. Failed to execute 'transaction' on 'IDBDatabase': The database connection is closing.

Even after the user frees disk space, the toast keeps firing until the app is fully restarted. IndexedDB is left in a dead connection state; any further IDB writes from the renderer keep failing.

We need a WeChat-style Storage Crisis Mode: classify the error, fail closed, show a blocking recovery UI, allow filesystem-only cleanup, then require quit/relaunch. Never touch IndexedDB while in crisis.

User-visible goal

  1. Stop showing cryptic IDB DOMExceptions.
  2. Explain clearly: local storage is full / unavailable; freeing space alone is not enough — restart is required.
  3. Offer Manage storage (optional Phase 1+) and Quit app.
  4. Manage storage must not call IndexedDB (deleteDatabase, archive/purge through repo, etc.).

Root cause (source map)

Investigated against loro-repo@0.20.0 after pnpm install (published package dist/ + existing patches/loro-repo.patch).

Call flow

ChatLanding submit
  startFailureReason = 'session_create_failed'
  await startSession(...)
    DirectWorkspaceWriter.startSession
      acquireSessionStore → createSessionStore
        repo.openPersistedDoc(roomId)
          DocManager.loadOrCreateDoc
            IndexedDBStorageAdaptor.loadDoc   // awaited; uses readwrite txn even for a new empty room
              db.transaction([...], 'readwrite')  // bare — no try/catch
                throws InvalidStateError ("connection is closing")
  catch → toast.error(t('chat.failed'), { description: err.message })

Toast construction:

  • packages/components/src/components/chat/chat-landing.tsx (~3318 and ~3165 dispatch path)

Repo creation:

  • packages/components/src/providers/create-workspace-runtime.tsLoroRepo.create({ storageAdapter: new IndexedDBStorageAdaptor({ dbName: 'lody-loro-repo-db-' + workspaceId }) })

Why it keeps toasting after disk is freed

  1. Adaptor caches dbPromise; Chromium leaves the open IDBDatabase dying.
  2. Each new landing submit opens a new session roomloadDoctransaction() on the dead connection → same throw → new toast.
  3. No app-level circuit breaker for the repo IDB (cursor store already has a degraded breaker for a different DB).
  4. Freeing disk does not reopen that connection — process quit / relaunch (or a full new adaptor after reload that actually rebuilds the connection) is required.

Note: the first failure is often QuotaExceededError on put (may only hit console.error via logAsyncError / meta flush). The sticky user-visible error is usually the aftermath InvalidStateError.

Classification today

None. Raw error.message is pasted into toasts. loro-repo createError() only wraps request/abort events; bare db.transaction() is unwrapped.

Sibling storage (do not confuse)

Store Path Notes
Renderer CRDT repo IDB lody-loro-repo-db-<workspaceId> via IndexedDBStorageAdaptor This toast
Stream cursors IDB lody-loro-stream-cursors-<workspaceId> Already has circuit breaker → in-memory fallback
CLI replica ~/.lody / ~/.lody-ossloro-repo/<ws>/repo.sqlite3 Different process; FS cleanup does not revive renderer IDB
Other IDB caches eager-sync high-water, github PR cache, etc. No quota taxonomy

Renderer IndexedDB lives in Chromium userData, not under getLodyDataDir(). Deleting worktrees can free the volume for a later restart; it cannot repair a live dying connection.

Proposed design

A. Lib — classify storage errors (loro-repo)

Prefer patches/loro-repo.patch for speed (upstream loro-dev/loro-repo is the long-term home; current automation may lack write access there).

Wrap db.transaction() in runInTransaction / runInStoresTransaction:

  • QuotaExceededErrorStorageQuotaExceeded (or stable code: 'quota')
  • InvalidStateError + /connection is closing/iStorageUnavailable (code: 'unavailable', sticky — do not retry-write)
  • Optional: on unavailable, db.close(), clear dbPromise, try one reopen; still fail → mark adaptor dead

Must cover loadDoc, not only MetaPersister — create fails on the awaited load path.

B. App — fail-closed StorageCrisis circuit breaker

Mirror the idea of ResilientRemoteCursorStore.degraded, but do not invent an in-memory Loro repo (data-loss hallucination).

Suggested owner: create-workspace-runtime.ts right after LoroRepo.create (wrap adapter or onStorageFailurestorageCrisisAtom / equivalent).

Then short-circuit:

  • WorkspaceWriter.startSession / acquireSessionStore / writers that would hit IDB
  • All toast.error(..., err.message) paths for this class of failure

Do not put the breaker only in chat-landing.tsx — archive, send, catalog flock writes will keep hitting the dead DB.

C. UI — blocking modal (WeChat-style)

On first classified quota | unavailable:

  • One non-dismissible (or quit-only) modal; stop toast spam
  • Copy: local storage full/unavailable; after cleanup, restart is required
  • Actions:
    • Manage storage → FS-only panel (Phase 1 can defer this)
    • Quit app → real process quit (preferred over reload; Chromium may keep the dying IDB in-process)

Manage storage (FS only)

IPC → main/CLI under getLodyDataDir() only, e.g.:

  • repos/<repoId>/worktrees/<sessionId> (WorktreeManager / existing cleanup)
  • session-files/, logs/ (cleanupExpiredLogs), npm-cache/, chat workdirs, speculative worktrees

Do not use in crisis:

  • packages/components/src/lib/clear-local-cache.ts (indexedDB.deleteDatabase — blocks while runtime holds the repo open)
  • Archive / delete / purgeDoc through the dying connection
  • Settings “Clear cache” as the primary in-crisis action (OK as post-restart recovery)

After FS cleanup: copy that says space may be freed → Restart now / Quit.

Suggested implementation phasing

  1. Phase 1 (stop the bleeding): A (patch classify) + B (crisis breaker) + minimal modal (explain + Quit/Relaunch).
  2. Phase 2: FS Manage storage panel wired to existing worktree/log/cache cleanup.
  3. Phase 3: Upstream loro-repo PR + drop/reduce the Lody patch; optional CLI ENOSPC UX (separate from this toast).

Acceptance criteria

  • Disk-full / dying IDB no longer surfaces raw IDBDatabase / transaction strings in toasts.
  • First failure enters crisis mode once; subsequent session-create clicks do not stack toasts.
  • Modal copy makes restart requirement explicit.
  • Manage storage (when present) never opens IndexedDB / Streams persist / deleteDatabase while crisis is active.
  • Quit/relaunch recovers after space is freed (manual or FS cleanup).
  • Cursor/stream degraded path remains independent; repo stays fail-closed.

References

  • i18n: locales/zh_CN.jsonchat.failed
  • Patch surface already exists: patches/loro-repo.patch (today only touches LoroRepo.ready() / meta monitor — not storage)
  • Existing good pattern to mirror (different DB): packages/components/src/providers/resilient-remote-cursor-store.ts

Out of scope / notes for implementers

  • Do not treat deleting CLI loro-repo/<ws>/repo.sqlite3 as a fix for this renderer toast (different store; data loss risk).
  • Dozens of toast.error sites paste error.message — classification must sit under writer/runtime, not landing copy alone.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    status:needs-issue-bodyIssue does not meet the Bug or Feature form requirements

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions