Skip to content

fix: prevent feedback loss from stale cached widget sessions (#46)#47

Open
yepzdk wants to merge 1 commit into
mainfrom
feature/issue-46-fix-stale-session-cache
Open

fix: prevent feedback loss from stale cached widget sessions (#46)#47
yepzdk wants to merge 1 commit into
mainfrom
feature/issue-46-fix-stale-session-cache

Conversation

@yepzdk
Copy link
Copy Markdown
Collaborator

@yepzdk yepzdk commented May 11, 2026

Summary

  • The widget bundle had the session ID baked into its WebSocket URL at serve time, so a cached bundle reconnected under the old session after an MCP restart and feedback was lost in an orphan in-memory bucket.
  • Fixed across four layers so the failure mode is structurally impossible: runtime session resolution + no-store, server-side rebind/rejection of unknown sessions, on-disk queues, and orphan-bucket surfacing/auto-rescue.

Closes #46.

Changes

  • Layer 1 — eliminate cache vs. session coupling
    • src/server.js: drop session from the __WEBSOCKET_URL__ injection; serve /widget.js with Cache-Control: no-store.
    • src/widget.js: read the session at boot from document.currentScript.src (or the tagged script element).
    • src/utils.js: new parseSessionFromScriptSrc helper.
  • Layer 2 — self-healing WS routing
    • src/server.js WS connection handler: if ?session= isn't in sessionRegistry, rebind to the sole live session (with a rebound field in connected), or send {type:'session_invalid', knownSessions} + close.
    • src/widget.js: adopt rebound IDs, handle session_invalid by re-resolving from the script src and otherwise showing a visible reload banner.
  • Layer 3 — durable queues
    • New src/storage.js (debounced, atomic, per-session JSON files under os.tmpdir()/claude-browser-feedback/).
    • src/server.js: write-through accessors, rehydrate on owner boot, flush pending writes on shutdown.
  • Layer 4 — visibility & recovery
    • get_pending_feedback, get_connection_status, /status, and /feedback expose orphan buckets.
    • When exactly one MCP session is registered, orphan buckets are auto-rescued into the live session.

Test plan

  • npm test — 67 tests pass (3 new test files / suites: storage round-trip + debounce, parseSessionFromScriptSrc, WS rebind + session_invalid + orphan reporting).
  • Manual smoke test: server boots, Cache-Control: no-store is set, __WEBSOCKET_URL__ placeholder is gone, only the base WS URL is injected, rehydrate-on-boot reads persisted sessions, WS connection with a stale UUID is rebound with a rebound:{from,to} payload.
  • Manual browser E2E (requires a real tab): restart MCP server with an unreloaded tab, submit feedback, confirm it lands in the current session via get_pending_feedback.
  • Manual E2E: kill server mid-pending feedback, restart, confirm items are restored from disk.
  • Manual E2E: hack ?session=DEADBEEF into the script src and reload — confirm the visible reload banner appears.

🤖 Generated with Claude Code

The widget bundle had the session ID baked into its WebSocket URL at
serve time, so a cached bundle would keep reconnecting under the old
session after the MCP server restarted. The server filed feedback under
the orphan bucket and `get_pending_feedback` returned "No pending
feedback." Because the queues lived only in RAM, the data was lost.

Fixed across four layers so the failure mode is impossible going
forward:

- The widget reads the session from `document.currentScript.src` at
  runtime and `/widget.js` is served with `Cache-Control: no-store`,
  so a cached bundle can never carry a stale session.
- WebSocket connections with an unknown session ID are rebound to the
  live session when exactly one MCP session is registered, or
  rejected with a `session_invalid` message (and a visible reload
  banner in the widget) when the server can't safely guess.
- Pending and ready queues are persisted to disk under
  `os.tmpdir()/claude-browser-feedback/<sessionId>.json` and
  rehydrated on boot, so feedback survives crashes and restarts.
- `get_pending_feedback`, `get_connection_status`, and `/status`
  surface orphan buckets and auto-rescue them into the live session
  when it is the only one registered.

Closes #46

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Browser feedback lost when widget.js is cached with a previous MCP session ID

1 participant