Skip to content

feat(transport): transport SSOT phase 1 — server-authoritative state#432

Closed
dimakis wants to merge 2 commits into
feat/transport-ssot-p0from
feat/transport-ssot-p1
Closed

feat(transport): transport SSOT phase 1 — server-authoritative state#432
dimakis wants to merge 2 commits into
feat/transport-ssot-p0from
feat/transport-ssot-p1

Conversation

@dimakis

@dimakis dimakis commented Jul 2, 2026

Copy link
Copy Markdown
Owner

Summary

  • Transport SSOT Phase 1: client reads running state exclusively from server session_state_changed events
  • Replaces SET_RUNNING action with SESSION_STATE_CHANGED — running derived from state === 'running'
  • Removes syncRunningState() REST polling (state events make it unnecessary)
  • Removes running field from reconnected and session_switched server messages
  • Migrates server-side is_active readers to use state column (handleReconnect, connection-registry, query-loop)
  • Net -360 lines — more dead code removed than new code added

Dead code removed

  • SET_RUNNING action type and reducer case
  • syncRunningState() function and runningSyncInFlight flag
  • running field in reconnected summaries and session_switched messages
  • markSessionInactive() call in query-loop (redundant with setSessionState)
  • SET_RUNNING dispatches from 8 protocol parser handlers

Context

Design doc: PR #430 (design/transport-ssot branch)
Depends on: PR #431 (P0 foundation)
Telos: 97361f814c5f54df

Test plan

  • 351 tests pass across modified test files (protocol-parser, store, ws-handler-v2, event-store, connection-registry)
  • TypeScript build passes (no new errors, only pre-existing worktree symlink issues)
  • Centaur review
  • Verify running state syncs correctly on phone + laptop (reconnect, session switch, foreground return)
  • Verify stop button works (handleStop dispatches SESSION_STATE_CHANGED)

🤖 Generated with Claude Code

dimakis and others added 2 commits July 2, 2026 10:21
…ecovery, SSE default

Transport SSOT Phase 0: additive foundation, no behavior change.

- Add ClientSessionState type and SessionStateEvent interface (types.ts)
- Emit session_state_changed event from setSessionState() with 7→3 state mapping
- Sync is_active from state on every transition (backwards-compatible)
- Add recoverStaleSessions() to mark ACTIVE/STARTING/DETACHED/SUSPENDED → ENDED on startup
- Call recoverStaleSessions() in server startup (index.ts)
- Add state field to /api/sessions/:id/meta response (app.ts)
- Flip SSE to default transport, WS opt-in via localStorage (client-store.ts)
- Add session_state_changed case to protocol parser (no-op, ready for P1)

Design doc: docs/design/transport-ssot.md (on design/transport-ssot branch)
Telos: 97361f814c5f54df

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…with session_state_changed

Transport SSOT Phase 1: client reads running state from server events only.

Server:
- handleReconnect uses getSessionState() instead of getSession().isActive
- Remove redundant markSessionInactive() in query-loop (setSessionState syncs is_active)
- Remove running field from reconnected and session_switched messages
- connection-registry isSessionActive uses state instead of is_active column

Client:
- Activate session_state_changed handler to dispatch SESSION_STATE_CHANGED action
- Replace SET_RUNNING action with SESSION_STATE_CHANGED (derives running from state)
- Remove syncRunningState() REST polling and foreground call site
- Remove running field handling from reconnected/session_switched/takeover/subscribed parsers
- handleStop uses SESSION_STATE_CHANGED instead of SET_RUNNING

Dead code removed: SET_RUNNING action type, syncRunningState(), runningSyncInFlight,
running field in reconnected summaries and session_switched messages.

Design doc: docs/design/transport-ssot.md (on design/transport-ssot branch)
Depends on: PR #431 (P0 foundation)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@dimakis

dimakis commented Jul 2, 2026

Copy link
Copy Markdown
Owner Author

Centaur Review

Found 7 issue(s) (1 critical) (4 warning).

server/ws-handler-v2.ts

The migration from SET_RUNNING to SESSION_STATE_CHANGED is well-structured but has a critical gap: handleSwitchSession removes the running field without adding event replay, leaving clients unable to determine session state for 0–5 seconds after switching sessions.

  • 🔴 regressions (L407): handleSwitchSession no longer sends running state, but also does not replay events. The client depends on session_state_changed events for running state, yet switch_session only sends metadata — no event replay occurs. The periodic sync (5s interval) is the only fallback, leaving a 0–5s window where the client has no idea if the target session is running. Reconnect correctly replays events; switch should too, or retain running in the response until event replay is added. [fixable]

frontend/src/pages/ChatView.tsx

The migration from SET_RUNNING to SESSION_STATE_CHANGED is well-structured but has a critical gap: handleSwitchSession removes the running field without adding event replay, leaving clients unable to determine session state for 0–5 seconds after switching sessions.

  • 🟡 regressions (L188): handleStop dispatches { type: 'SESSION_STATE_CHANGED', state: 'idle' } client-side — an optimistic update that contradicts the PR's stated goal of server-authoritative state. If the server doesn't honor the stop (e.g., query already completing), the client is wrong until the next server event. The old SET_RUNNING was equally optimistic, so this isn't a new bug, but it should be documented or replaced with server-only state flow in P2. [fixable]

packages/client/src/store.ts

The migration from SET_RUNNING to SESSION_STATE_CHANGED is well-structured but has a critical gap: handleSwitchSession removes the running field without adding event replay, leaving clients unable to determine session state for 0–5 seconds after switching sessions.

  • 🟡 regressions (L704): Foreground recovery (_foreground handler) removed syncRunningState() without a replacement. If the app is backgrounded while a session ends, the session_state_changed event may be lost (WS was disconnected). The client returns to foreground with stale running: true and no polling to correct it. The periodic sync may eventually deliver the event, but there's a meaningful gap. [fixable]
  • 🟡 regressions (L361): Pending send timer no longer sets running: true before sending a queued message. When session_end fires with pending messages, the UI briefly shows idle (running=false from session_end) then flips to running once the server echoes session_state_changed. This creates a visible flash/stutter between turns. The old optimistic SET_RUNNING avoided this. [fixable]

packages/client/__tests__/protocol-parser.test.ts

The migration from SET_RUNNING to SESSION_STATE_CHANGED is well-structured but has a critical gap: handleSwitchSession removes the running field without adding event replay, leaving clients unable to determine session state for 0–5 seconds after switching sessions.

  • 🟡 missing_tests (L268): No test verifies the new session_state_changedSESSION_STATE_CHANGED dispatch in the protocol parser. The test for the session_state_changed case was presumably in the P0 PR, but this PR changes its behavior from no-op to dispatching an action — that new behavior should have a dedicated test here. [fixable]
  • 🔵 style (L624): The reconnected test section was reduced from 6 tests to 1, removing validation for edge cases (no currentSessionId, undefined sessions, invalid shapes). While reconnected no longer dispatches SET_RUNNING, the handler still calls onReconnected() — the defensive validation tests for malformed sessions payloads had standalone value and didn't depend on SET_RUNNING. [fixable]

server/query-loop.ts

The migration from SET_RUNNING to SESSION_STATE_CHANGED is well-structured but has a critical gap: handleSwitchSession removes the running field without adding event replay, leaving clients unable to determine session state for 0–5 seconds after switching sessions.

  • 🔵 bugs (L1400): Removed markSessionInactive() call, relying on setSessionState('ENDED', ...) which internally syncs is_active = 0. This is correct — the markSessionInactive method on EventStore is now dead code in production (only called from tests). Consider removing the method to avoid confusion, or leave for P2 cleanup. [fixable]

@dimakis dimakis force-pushed the feat/transport-ssot-p0 branch from e830856 to 9e2fcbc Compare July 2, 2026 22:07
@dimakis dimakis deleted the branch feat/transport-ssot-p0 July 2, 2026 22:07
@dimakis dimakis closed this Jul 2, 2026
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