fix: guard localStorage quota writes

This commit is contained in:
Michael Lam 2026-05-16 07:31:03 -07:00
parent e3035b3e40
commit 3bb8c7b276
6 changed files with 44 additions and 6 deletions

View file

@ -2,6 +2,10 @@
## [Unreleased]
### Fixed
- **PR #2387** by @Michaelyklam (closes #2386) — Active-session and workspace-panel `localStorage` writes now degrade gracefully when browser storage quota is exhausted. The missed session/panel persistence writes now match the existing best-effort storage pattern so clicking sessions, creating chats, completing streams, and toggling the workspace panel no longer throw uncaught `QuotaExceededError` exceptions.
## [v0.51.74] — 2026-05-16 — Release AX (stage-367 — 4-PR safe-lane batch — #2362 table-cell spacing + #2363 run-state-consistency RFC + #2365 custom_providers list-format + #2367 settings sidebar i18n)
### Added

View file

@ -101,7 +101,7 @@ function _setWorkspacePanelMode(mode){
// Persist open/closed across refreshes (browse/preview → open; closed → closed)
// Do NOT overwrite the user's "keep open" preference — only track runtime state
// so that toggleWorkspacePanel(false) from the toolbar doesn't clear the setting.
localStorage.setItem('hermes-webui-workspace-panel', open ? 'open' : 'closed');
try{localStorage.setItem('hermes-webui-workspace-panel', open ? 'open' : 'closed');}catch(_){}
layout.classList.toggle('workspace-panel-collapsed',!open);
if(_isCompactWorkspaceViewport()){
panel.classList.toggle('mobile-open',open);

View file

@ -424,7 +424,7 @@ async function _applyManualCompressionResult(data, focusTopic, visibleCount, com
S.messages=data.session.messages||[];
S.toolCalls=data.session.tool_calls||[];
clearLiveToolCards();
localStorage.setItem('hermes-webui-session',S.session.session_id);
try{localStorage.setItem('hermes-webui-session',S.session.session_id);}catch(_){}
if(typeof _setActiveSessionUrl==='function') _setActiveSessionUrl(S.session.session_id);
syncTopbar();
renderMessages();

View file

@ -1454,7 +1454,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
const _prevCost=(S.session&&S.session.estimated_cost)||0;
S.session=d.session;S.messages=d.session.messages||[];if(typeof _messagesTruncated!=='undefined')_messagesTruncated=!!d.session._messages_truncated;
if(S.session&&S.session.session_id){
localStorage.setItem('hermes-webui-session',S.session.session_id);
try{localStorage.setItem('hermes-webui-session',S.session.session_id);}catch(_){}
if(typeof _setActiveSessionUrl==='function') _setActiveSessionUrl(S.session.session_id);
}
const _markerOnlyAssistantError=_replaceMarkerOnlyAssistantWithStreamError(S.messages);
@ -1824,7 +1824,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
clearLiveToolCards();if(!assistantText)removeThinking();
S.session=session;S.messages=(session.messages||[]).filter(m=>m&&m.role);
if(S.session&&S.session.session_id){
localStorage.setItem('hermes-webui-session',S.session.session_id);
try{localStorage.setItem('hermes-webui-session',S.session.session_id);}catch(_){}
if(typeof _setActiveSessionUrl==='function') _setActiveSessionUrl(S.session.session_id);
}
const _markerOnlyAssistantError=_replaceMarkerOnlyAssistantWithStreamError(S.messages);

View file

@ -416,7 +416,7 @@ async function newSession(flash, options={}){
S.session=data.session;S.messages=data.session.messages||[];
S.lastUsage={...(data.session.last_usage||{})};
if(flash)S.session._flash=true;
localStorage.setItem('hermes-webui-session',S.session.session_id);
try{localStorage.setItem('hermes-webui-session',S.session.session_id);}catch(_){}
_setActiveSessionUrl(S.session.session_id);
_setSessionViewedCount(S.session.session_id, S.session.message_count || 0);
// Sync chat-header dropdown to the session's model so the UI reflects
@ -526,7 +526,7 @@ async function loadSession(sid){
if(typeof syncTopbar==='function') syncTopbar();
_setSessionViewedCount(S.session.session_id, Number(data.session.message_count || 0));
_clearSessionCompletionUnread(S.session.session_id);
localStorage.setItem('hermes-webui-session',S.session.session_id);
try{localStorage.setItem('hermes-webui-session',S.session.session_id);}catch(_){}
_setActiveSessionUrl(S.session.session_id);
const activeStreamId=S.session.active_stream_id||null;

View file

@ -0,0 +1,34 @@
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
def _script(path):
return (ROOT / path).read_text()
def _assert_storage_setitem_guarded(src, needle):
matches = [line.strip() for line in src.splitlines() if needle in line]
assert matches, f"expected at least one {needle} write"
for line in matches:
assert line.startswith("try{localStorage.setItem("), (
f"localStorage quota errors must not escape from {needle} writes: {line}"
)
assert "catch(_)" in line or "catch(e)" in line or "catch{}" in line
def test_active_session_localstorage_writes_ignore_quota_errors():
"""Session persistence writes are best-effort when the browser quota is full (#2386)."""
for path in ["static/sessions.js", "static/commands.js", "static/messages.js"]:
_assert_storage_setitem_guarded(
_script(path),
"localStorage.setItem('hermes-webui-session'",
)
def test_workspace_panel_localstorage_write_ignores_quota_errors():
"""Workspace panel state should not break UI toggles if localStorage throws (#2386)."""
_assert_storage_setitem_guarded(
_script("static/boot.js"),
"localStorage.setItem('hermes-webui-workspace-panel'",
)