The #3018 carry-forward reassigns msgs but it was declared const, throwing a
TypeError that surfaced as 'Failed to load conversation messages' on every mobile
message (v0.51.161-166). Change to let.
Adds a static JS runtime-error lint guard (eslint.runtime-guard.config.mjs +
tests/test_static_js_runtime_lint.py) using no-const-assign/no-import-assign — the
exact class node --check and source-presence tests miss. Dev-only dependency; app
stays pure Python + vanilla JS.
The gateway SSE handler (`_handle_gateway_sse_stream`) and session-events
SSE handler (`_handle_session_events_stream`) both set
`Connection: close` on the response. On long-lived SSE streams, browsers
(Chrome, Firefox, Safari) interpret that header as 'this is a one-shot
response, the EventSource lifecycle is over the moment the body ends'
and trigger an immediate reconnect when the server-side worker rotates.
The result, with EventSource's auto-reconnect on top, is a tight loop of
connect -> sessions_changed snapshot -> reconnect every ~1s that pegs
the worker, thrashes the session list, and prevents the in-app session
list from ever settling.
Removing the header lets the python BaseHTTPServer close the socket
naturally after the stream ends, which is what EventSource expects.
Regression introduced in 598fd4ff.
When tc.snippet === tc.preview (common for no-progress tools where
both are set to the same result_snippet), the detail block would show
identical content as the header. Skip the detail block in this case.
This also handles the reload-path where derived entries get snippet
populated but no preview, so displaySnippet falls back to the snippet
content for the header — same deduplication applies.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
#3020: Sync viewed-count in the polling path for actively-viewed sessions
so navigating away doesn't show a stale unread dot. Defensive clear of
completion-unread marker in _setSessionViewedCount.
#2973: Clear elapsed-timer attributes and interval when a live compression
card transitions from running to done, preventing the orphan timer from
overwriting the completed card state. Guarded by active-session check.
Tool card duplication: Route tool_complete result to tc.snippet (detail)
instead of tc.preview (header) to prevent identical content appearing in
both the card header and expanded detail section.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
`_turnUsage` (and `_turnDuration`, `_turnTps`, `_gatewayRouting`,
`_statusCard`) are computed client-side in `_finishDone()` and attached
to the last assistant message in `S.messages`. Three code paths replace
`S.messages` wholesale with fresh API data that lacks these fields:
1. `_restoreSettledSession()` after a late `stream_end` or SSE error.
2. The late-restore branch after `done` (messages.js ~L2247).
3. `loadSession()` for active-session external refresh / focus-change.
When any of these fire after `done`, the badge/duration/gateway-pill
flashes for ~1s and disappears, never returning until the next message
or page reload.
Add `_carryForwardEphemeralTurnFields(prev, next)` that matches messages
by `(role, timestamp, content prefix)` — the same identity the renderer
already uses — and copies forward the ephemeral fields when the server
payload is missing them. Wire it into all three replace sites. The fix
is conservative: it only fills slots that are `null` on the new message,
so an authoritative server-side value (if/when the API ever surfaces
per-turn usage) wins automatically.
Picked Option A from the bug report (preserve on the client side) over
Option B (synthesize from `S.lastUsage`) because `lastUsage` is a
session-level aggregate; reconstructing per-turn breakdowns from it is
lossy. Option C (set `_streamFinalized` earlier) would suppress legit
late-arriving server data on transient errors.