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
- Stop showing cryptic IDB DOMExceptions.
- Explain clearly: local storage is full / unavailable; freeing space alone is not enough — restart is required.
- Offer Manage storage (optional Phase 1+) and Quit app.
- 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.ts — LoroRepo.create({ storageAdapter: new IndexedDBStorageAdaptor({ dbName: 'lody-loro-repo-db-' + workspaceId }) })
Why it keeps toasting after disk is freed
- Adaptor caches
dbPromise; Chromium leaves the open IDBDatabase dying.
- Each new landing submit opens a new session room →
loadDoc → transaction() on the dead connection → same throw → new toast.
- No app-level circuit breaker for the repo IDB (cursor store already has a
degraded breaker for a different DB).
- 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-oss → loro-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:
QuotaExceededError → StorageQuotaExceeded (or stable code: 'quota')
InvalidStateError + /connection is closing/i → StorageUnavailable (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 onStorageFailure → storageCrisisAtom / 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
- Phase 1 (stop the bleeding): A (patch classify) + B (crisis breaker) + minimal modal (explain + Quit/Relaunch).
- Phase 2: FS Manage storage panel wired to existing worktree/log/cache cleanup.
- Phase 3: Upstream
loro-repo PR + drop/reduce the Lody patch; optional CLI ENOSPC UX (separate from this toast).
Acceptance criteria
References
- i18n:
locales/zh_CN.json → chat.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.
Summary
When the disk (or Chromium storage quota) is exhausted, creating a session shows a toast:
会话创建失败(chat.failed)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
deleteDatabase, archive/purge through repo, etc.).Root cause (source map)
Investigated against
loro-repo@0.20.0afterpnpm install(published packagedist/+ existingpatches/loro-repo.patch).Call flow
Toast construction:
packages/components/src/components/chat/chat-landing.tsx(~3318 and ~3165 dispatch path)Repo creation:
packages/components/src/providers/create-workspace-runtime.ts—LoroRepo.create({ storageAdapter: new IndexedDBStorageAdaptor({ dbName: 'lody-loro-repo-db-' + workspaceId }) })Why it keeps toasting after disk is freed
dbPromise; Chromium leaves the openIDBDatabasedying.loadDoc→transaction()on the dead connection → same throw → new toast.degradedbreaker for a different DB).Note: the first failure is often
QuotaExceededErroronput(may only hitconsole.errorvialogAsyncError/ meta flush). The sticky user-visible error is usually the aftermathInvalidStateError.Classification today
None. Raw
error.messageis pasted into toasts.loro-repocreateError()only wraps request/abort events; baredb.transaction()is unwrapped.Sibling storage (do not confuse)
lody-loro-repo-db-<workspaceId>viaIndexedDBStorageAdaptorlody-loro-stream-cursors-<workspaceId>~/.lody/~/.lody-oss→loro-repo/<ws>/repo.sqlite3Renderer IndexedDB lives in Chromium
userData, not undergetLodyDataDir(). 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.patchfor speed (upstreamloro-dev/loro-repois the long-term home; current automation may lack write access there).Wrap
db.transaction()inrunInTransaction/runInStoresTransaction:QuotaExceededError→StorageQuotaExceeded(or stablecode: 'quota')InvalidStateError+/connection is closing/i→StorageUnavailable(code: 'unavailable', sticky — do not retry-write)unavailable,db.close(), cleardbPromise, try one reopen; still fail → mark adaptor deadMust cover
loadDoc, not onlyMetaPersister— create fails on the awaited load path.B. App — fail-closed
StorageCrisiscircuit breakerMirror the idea of
ResilientRemoteCursorStore.degraded, but do not invent an in-memory Loro repo (data-loss hallucination).Suggested owner:
create-workspace-runtime.tsright afterLoroRepo.create(wrap adapter oronStorageFailure→storageCrisisAtom/ equivalent).Then short-circuit:
WorkspaceWriter.startSession/acquireSessionStore/ writers that would hit IDBtoast.error(..., err.message)paths for this class of failureDo 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: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 worktreesDo not use in crisis:
packages/components/src/lib/clear-local-cache.ts(indexedDB.deleteDatabase— blocks while runtime holds the repo open)purgeDocthrough the dying connectionAfter FS cleanup: copy that says space may be freed → Restart now / Quit.
Suggested implementation phasing
loro-repoPR + drop/reduce the Lody patch; optional CLIENOSPCUX (separate from this toast).Acceptance criteria
IDBDatabase/transactionstrings in toasts.deleteDatabasewhile crisis is active.References
locales/zh_CN.json→chat.failedpatches/loro-repo.patch(today only touchesLoroRepo.ready()/ meta monitor — not storage)packages/components/src/providers/resilient-remote-cursor-store.tsOut of scope / notes for implementers
loro-repo/<ws>/repo.sqlite3as a fix for this renderer toast (different store; data loss risk).toast.errorsites pasteerror.message— classification must sit under writer/runtime, not landing copy alone.