Skip to content

feat(chat): reconnect to running sessions from any tab/browser (T-52) - #11

Merged
jaesbit merged 1 commit into
mainfrom
feat/t52-session-reconnect
Jul 3, 2026
Merged

feat(chat): reconnect to running sessions from any tab/browser (T-52)#11
jaesbit merged 1 commit into
mainfrom
feat/t52-session-reconnect

Conversation

@jaesbit

@jaesbit jaesbit commented Jul 3, 2026

Copy link
Copy Markdown
Owner

Why

T-52: closing a session's tab and reopening it doesn't reconnect to the running agent. The agent should keep running in the background regardless, and opening the same session from a second browser should live-sync both views over the shared WebSocket without killing either.

Root cause

Server-side was already correct: InProcessAgentBridge supports multiple concurrent WS subscribers per session (Active.subscribers: Set<string>), fans out session_events to all of them, and only reaps a session after it's been idle and has zero subscribers for idleTimeoutMs (never mid-turn). The gap was entirely client-side: the active session id (activeId) lived only in in-memory zustand state with no URL or localStorage backing, so any full page load (tab close/reopen, reload, a second browser) always started at activeId: undefined and landed on the empty picker — even while the agent kept running server-side.

Change

  • New /c/:sessionId route + useSessionRoute() hook keeps the URL and the store's activeId in sync in both directions (URL to store on load/back-forward, store to URL on any other session change). Deep-linking to a session now works, and switching between two different sessions no longer gets clobbered by a stale localStorage default (caught this in manual verification — the store-to-URL mirror effect needed to wait for the URL-to-store adopt effect to run at least once, otherwise a localStorage-restored activeId could stomp a legitimate/different /c/:id link before it was ever adopted).
  • A bare / restores the last-active session via a new localStorage-backed default, so a brand-new tab (not just a restored one) reconnects too.
  • resumeIfKnown in the store: when a subscribe attempt comes back "session not active" (idle-reaped, or the server restarted) for a session that still exists on disk, transparently resumes it instead of leaving the tab stuck on a blank screen. Falls back to clearing activeId (back to the picker) only if the session genuinely doesn't exist.

No server or protocol changes — the multi-subscriber fan-out and idle-reap grace already existed; this closes the client-side reconnect gap only.

Verification

  • bun run --filter '@omp-deck/web' typecheck — clean.
  • cd apps/web && bun test — 139 pass.
  • cd apps/server && bun test — 212 pass (unaffected, no server changes).
  • bun run --filter '@omp-deck/web' build — clean production build.
  • Manual browser verification against an isolated scratch server instance (separate port/db/workspace, not the live deck.service):
    • Fresh deep link to /c/:id loads the session directly (no picker, no 404).
    • Switching between two different /c/:id deep links adopts the new session correctly.
    • Bare / redirects to the last-active session via localStorage.
    • An unknown/bogus session id falls back cleanly to the picker at /.
    • Full page reload on a session URL stays on the same session with history intact.
    • Two tabs opened on the same session URL both hydrate the same live transcript; a message sent from one tab appears in the other without reloading.
    • Disposing the in-memory session handle (simulating idle-reap or a server restart) then reopening the same URL transparently auto-resumes the session from disk with full history, on the same URL.

Out of scope

Idle-reap timeout policy itself is unchanged (still disposes a session after it's genuinely idle with zero subscribers) — that's expected server-side resource hygiene, not the reported bug; the fix is that reconnecting to it is now transparent.

Adds `/c/:sessionId` deep-link routing so the active chat session is
addressable by URL instead of living only in in-memory zustand state:

- `useSessionRoute` (new) keeps the URL and the store's `activeId` in
  sync in both directions. Reloading the tab, using "reopen closed
  tab", or pasting the URL into a second browser now reconnects to the
  same session and picks up its live WS event stream (session handles
  already supported multiple concurrent subscribers server-side; the
  gap was purely client-side session addressing).
- A bare `/` restores whatever session was last active via a
  `localStorage`-backed `activeId` default, so a brand-new tab (not
  just a restored one) reconnects too.
- `resumeIfKnown` in the store auto-resumes a session from disk when a
  `subscribe` attempt finds it not currently running in this server
  process (idle-reaped, or the server restarted) but it still exists
  on disk — covers "the agent should keep running in the background
  even after I close its tab" without silently leaving the tab stuck
  on a blank screen.

No server/protocol changes: multi-subscriber fan-out and idle-reap
grace already existed in the bridge; this closes the client-side
reconnect gap only.
@jaesbit
jaesbit merged commit 42cb865 into main Jul 3, 2026
@jaesbit
jaesbit deleted the feat/t52-session-reconnect branch July 3, 2026 12:52
jaesbit added a commit that referenced this pull request Jul 3, 2026
…anban workspace UX (T-17/18/19/27/38-52) (#12)

Fills in the [Unreleased] changelog gap left by twelve merged PRs (#1-#11 plus the unnumbered T-19/abort-shortcut/dispose-collision fixes) that shipped functionality without a doc pass:

- Session launch bundle: atomic model+Plan Mode at creation, Settings -> Workspaces per-cwd model default, native task priority, Send to agent.
- Goal Mode: /goal composer command, autonomous lifecycle, Plan Mode mutual exclusion.
- Kanban workspace color markers + header workspace filter.
- Multi-tab/multi-browser session continuity: /c/:sessionId deep links, transparent resume-on-reconnect, live sessions_changed broadcast.
- Chat polish: pinned todos panel, scroll-to-bottom button.
- Workspace picker: arbitrary path + folder browse dialog.
- Configurable stop-streaming keyboard shortcut.
- Fixed: orphaned session handle on resume/create collision.

Touches CHANGELOG.md (new Unreleased section), README.md (What you get), docs/tui-parity.md (deck-only list + multi-session row), docs/architecture.md (broadcast frames + DB tables), docs/slash-commands.md (/goal section).
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