fix(session): auto-resume persisted sessions on /session/:id deep links #3

Merged
lucas merged 1 commit from feat/t-58 into main 2026-10-14 17:34:14 +02:00
Collaborator

After a server restart, opening /session/<id> directly showed "Session not found" because WS subscribe only attached to live sessions. subscribe now resumes a persisted, non-live session via a new ensureSessionLive on the bridge. Resume is single-flight per id. Archived non-live sessions are refused rather than reopened, and error frames carry a code (session_not_found / resume_failed / session_archived). SessionView uses it to show a loading state, "Session not found", "Failed to resume" with Retry, or "This session is archived" with "Unarchive & open". Sidebar and SessionPicker persisted rows now only navigate, and the resumeFromPath create-session path is removed from protocol, route and store.

Decisions

  • Retry reuses selectSession(id) (which now clears the stored error) instead of adding a separate retrySubscribe; the error handler already drops the id from subscribed, so selectSession re-sends the subscribe.
  • unarchiveSession now rethrows on failure instead of logging and returning. "Unarchive & open" shows the failure inline, and the Sidebar's existing unarchive alert now actually fires.
  • A second subscribe for the same id from one connection while a resume is in flight is ignored; the first one answers. If the client unsubscribes or the socket closes mid-resume, the subscribe is dropped, but the resume itself still completes and the session stays live.
  • The error frame's code is only set on subscribe failures; the other getSession-gated handlers keep replying session not active with no code.
  • docs/architecture.md doesn't describe the subscribe contract or resume, so it is unchanged.

Verification

  • bun run typecheck (protocol, server, web): pass.
  • bun test apps/server/src/bridge/ensure-live.test.ts apps/web/src/lib/subscribe-state.test.ts: 11 pass, 0 fail.
  • Protocol-level smoke against an isolated server instance (temp HOME/DB, port 8799, seeded copies of a real session JSONL), driven over raw WebSocket after a server restart:
    • Two connections subscribing to the same non-live session at once: both got subscribed with the transcript, and the server logged exactly one resumed session ….
    • Bogus id → error with code: session_not_found.
    • Archived session → session_archived; after POST /unarchive, re-subscribing → subscribed.
    • Subscribe then immediate unsubscribe while resuming → only unsubscribed, no subscribed.
    • Duplicate subscribe on one connection while resuming → exactly one subscribed.
    • Socket closed while resuming → no errors logged.
  • resume_failed: the SDK falls back to the launch cwd for a missing or non-directory cwd, so no natural input made resume fail. This path is covered by the unit test only.
  • Not run: browser UI checks (no usable browser in this environment), and restoring plan mode or a pending approval after an auto-resume.
After a server restart, opening `/session/<id>` directly showed "Session not found" because WS `subscribe` only attached to live sessions. `subscribe` now resumes a persisted, non-live session via a new `ensureSessionLive` on the bridge. Resume is single-flight per id. Archived non-live sessions are refused rather than reopened, and error frames carry a `code` (`session_not_found` / `resume_failed` / `session_archived`). `SessionView` uses it to show a loading state, "Session not found", "Failed to resume" with Retry, or "This session is archived" with "Unarchive & open". Sidebar and SessionPicker persisted rows now only navigate, and the `resumeFromPath` create-session path is removed from protocol, route and store. ## Decisions - Retry reuses `selectSession(id)` (which now clears the stored error) instead of adding a separate `retrySubscribe`; the `error` handler already drops the id from `subscribed`, so `selectSession` re-sends the subscribe. - `unarchiveSession` now rethrows on failure instead of logging and returning. "Unarchive & open" shows the failure inline, and the Sidebar's existing unarchive `alert` now actually fires. - A second `subscribe` for the same id from one connection while a resume is in flight is ignored; the first one answers. If the client unsubscribes or the socket closes mid-resume, the subscribe is dropped, but the resume itself still completes and the session stays live. - The `error` frame's `code` is only set on `subscribe` failures; the other `getSession`-gated handlers keep replying `session not active` with no code. - `docs/architecture.md` doesn't describe the subscribe contract or resume, so it is unchanged. ## Verification - `bun run typecheck` (protocol, server, web): pass. - `bun test apps/server/src/bridge/ensure-live.test.ts apps/web/src/lib/subscribe-state.test.ts`: 11 pass, 0 fail. - Protocol-level smoke against an isolated server instance (temp HOME/DB, port 8799, seeded copies of a real session JSONL), driven over raw WebSocket after a server restart: - Two connections subscribing to the same non-live session at once: both got `subscribed` with the transcript, and the server logged exactly one `resumed session …`. - Bogus id → `error` with `code: session_not_found`. - Archived session → `session_archived`; after `POST /unarchive`, re-subscribing → `subscribed`. - Subscribe then immediate unsubscribe while resuming → only `unsubscribed`, no `subscribed`. - Duplicate subscribe on one connection while resuming → exactly one `subscribed`. - Socket closed while resuming → no errors logged. - `resume_failed`: the SDK falls back to the launch cwd for a missing or non-directory cwd, so no natural input made resume fail. This path is covered by the unit test only. - Not run: browser UI checks (no usable browser in this environment), and restoring plan mode or a pending approval after an auto-resume.
CHANGELOG.md Outdated
@ -57,6 +57,7 @@ All notable changes to omp-deck. The format is loosely based on
- Processed inbox items open correctly via direct URL even when "Show processed" is off.
- Direct-linking to an inbox item or task that doesn't exist shows an inline "not found" message instead of a blank or silently empty pane.
- Opening a task in chat now navigates to the newly created session instead of the root chat page.
- Opening `/session/<id>` directly (bookmark, shared link, reload) after a server restart now resumes the persisted session instead of showing "Session not found". The page shows a loading state while it resumes, "Failed to resume" with a Retry button when resuming errors, and for archived sessions an "Unarchive & open" button — archived sessions are never reopened implicitly. Opening the same link in several tabs at once resumes it only once. Clicking a persisted session in the sidebar or session picker now simply navigates to its URL.
Owner

This is quite verbose. Are you sure it adheres to the conventions/ KB?

This is quite verbose. Are you sure it adheres to the `conventions/` KB?
Author
Collaborator

You're right, it didn't: conventions/changelog.md asks for entries proportional to the change, with a fix stated in one concise line, and it also listed the sidebar/picker navigation change, which isn't user-relevant. Shortened in ef6356f to:

  • Fixed session links (reload, bookmark, shared URL) showing "Session not found" after a server restart; archived sessions offer "Unarchive & open" instead.
You're right, it didn't: `conventions/changelog.md` asks for entries proportional to the change, with a fix stated in one concise line, and it also listed the sidebar/picker navigation change, which isn't user-relevant. Shortened in `ef6356f` to: > - Fixed session links (reload, bookmark, shared URL) showing "Session not found" after a server restart; archived sessions offer "Unarchive & open" instead.
lucas approved these changes 2026-10-14 17:34:08 +02:00
lucas merged commit 7dfacf5230 into main 2026-10-14 17:34:14 +02:00
lucas deleted branch feat/t-58 2026-10-14 17:34:14 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
lucas/omp-deck!3
No description provided.