Skip to content

fix(web): persist session id in URL so reload restores instead of resets#17

Merged
That1Drifter merged 1 commit intomasterfrom
fix-session-persistence
Apr 10, 2026
Merged

fix(web): persist session id in URL so reload restores instead of resets#17
That1Drifter merged 1 commit intomasterfrom
fix-session-persistence

Conversation

@That1Drifter
Copy link
Copy Markdown
Owner

Summary

`PlayClient.tsx` called `startSession()` unconditionally on mount with no URL/cookie restore, so any F5 mid-scenario silently destroyed 30+ minutes of work. The session-store already rehydrates from `data/sessions.json` on disk, so the fix is purely client-side.

The third critical bug from the fresh-eyes Sonnet playthrough.

What changed

  • New route `apps/web/app/api/session/[id]/route.ts` — `GET` returns the existing session in the same shape as `/api/session/start` so the client can use one apply handler for both paths.
  • `PlayClient.tsx`:
    • Reads `?session=` from the URL on mount. If present, tries restore first; falls back to fresh start on 404.
    • Adds an effect that keeps the URL in sync with the active `sessionId` via `history.replaceState` — applies on first start, on reset, and on restore.
    • Refactors the duplicated session-data wiring into a single `applySessionData` helper used by both `startSession` and `restoreSession`.

Test plan

  • `pnpm typecheck` — clean
  • `pnpm lint` — clean
  • Backend smoke via curl — start/turn/restore returns identical state, unknown id returns 404 cleanly
  • Reload preserves session. Local Playwright run: navigate to `/play/support-triage` → session id appears in URL → reload → same session id, same state.
  • Bogus session id falls back. Local Playwright run: navigate to `/play/support-triage?session=00000000-0000-0000-0000-000000000000` → 404 from restore → falls back to a fresh session id without surfacing the error to the user.

This is the third of three critical bugs from the playthrough TODO Now section. Contract guard fix shipped in #16, this is bug #3, only the demo GIF remains.

PlayClient called startSession() unconditionally on mount with no
URL/cookie restore, so any F5 mid-scenario silently destroyed 30+
minutes of work. The session-store already rehydrates from
data/sessions.json on disk, so the fix is purely client-side: read
?session=<id> from the URL on mount, fetch the existing session, fall
back to a fresh start only if missing or expired.

Adds GET /api/session/[id] returning the same shape as
/api/session/start so the client can use one apply handler for both
paths. Adds a small effect to keep the URL in sync with the active
sessionId via history.replaceState — applies on first start, on
reset, and on restore.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@That1Drifter That1Drifter merged commit b162d85 into master Apr 10, 2026
1 check passed
@That1Drifter That1Drifter deleted the fix-session-persistence branch April 10, 2026 23:38
That1Drifter added a commit that referenced this pull request Apr 10, 2026
Re-ran the fresh-eyes Sonnet playthrough on the patched build via
Playwright (against a local dev server, since the staging basic-auth
header trick blocks fetch in headless browsers). Both critical bugs
from the original report are fixed:

- 0 contract errors out of 10 turns (down from ~50% in the original).
  One turn showed the silent retry, otherwise clean.
- Mid-scenario reload preserved session: turn counter, cost, trust
  scores, objective state, and inbox all restored exactly. Same
  ?session=<id> URL.

The agent completed 10 turns (vs 3 in the original where contract
errors blocked progress at turn 4), saw both surprises fire, and
the debrief rendered cleanly.

Two new findings worth fixing:
- Last-turn work area narrative is empty after reload because
  lastEffects lives in component state, not the session store.
  Visible in frame-06-turn3-post-reload.png. Small follow-on to the
  session URL persistence work in #17.
- The `retried` badge added by the contract guard fix (#16) shows up
  in the turn metadata line with no explanation, which is mildly
  confusing for users since the retry is meant to be silent. Either
  drop it from the UI or add a tooltip.

Updates the demo GIF entry to point at the v2 frames captured by
the playthrough, which include both surprises firing and the reload
restore in action.

Also updates scripts/stitch-demo-gif.py with auto-discovery: if the
hardcoded curated frame list matches fewer than 5 frames in the
target dir, fall back to globbing all frame-*.png files in lexical
order with uniform timing. Makes the script work with any new
playthrough run without requiring a code edit.

Co-Authored-By: Claude Opus 4.6 (1M context) <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.

1 participant