Skip to content

fix(server): create the default session atomically (no duplicate "Session #1")#332

Merged
gwleclerc merged 2 commits into
mainfrom
fix/session-race
Jul 19, 2026
Merged

fix(server): create the default session atomically (no duplicate "Session #1")#332
gwleclerc merged 2 commits into
mainfrom
fix/session-race

Conversation

@gwleclerc

Copy link
Copy Markdown
Collaborator

Problem

The e2e job flaked on main (the merge of #315): the "rename a session" test failed with

strict mode violation: getByText('Session #1', { exact: true }) resolved to 2 elements

i.e. two sessions named Session #1 existed at once.

Root cause

GetLastSession auto-creates a default session when the list is empty, but it released the lock between the empty-check and the creation, and NewSession computed the Session #N name outside the lock:

s.mu.Lock()
if len(s.sessions) == 0 {
    s.mu.Unlock()      // ← gap
    s.NewSession("")   // re-locks internally
    s.mu.Lock()
}

When the UI fires concurrent requests against an empty session list — the History page's parallel GET /sessions + /history + /mocks (+ autorefresh) right after Reset Sessions — two of them each see len == 0 and each create a Session #1. Intermittent → flaky. Violates PRINCIPLES.md §3.7 (concurrency safety).

Fix

Move the append into an unlocked newSessionLocked helper, called within a single critical section by both NewSession and GetLastSession (name + carried-over locked mocks are all derived from s.sessions, so they must be computed under the same lock). No behaviour change otherwise.

Test

TestGetLastSessionConcurrent fires 50 concurrent GetLastSession calls on an empty service and asserts exactly one session is created. Verified it fails on the old code (created 2 sessions, want 1) and passes on the fix; the whole suite is green under -race.

🤖 Generated with Claude Code

gwleclerc and others added 2 commits July 19, 2026 20:42
…sion #1")

GetLastSession auto-created a session when the list was empty but released the
lock between the empty-check and the creation (and NewSession computed the
"Session #N" name outside the lock). Concurrent requests against an empty
session list — e.g. the History page's parallel GETs right after "Reset
Sessions" — could each create one, yielding two "Session #1" (the flaky
"rename a session" e2e test).

Move the append into an unlocked newSessionLocked helper used within a single
critical section by both NewSession and GetLastSession. Add a -race regression
test hammering GetLastSession on an empty service.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts:
#	server/services/mocks_test.go
@gwleclerc
gwleclerc merged commit c29583c into main Jul 19, 2026
6 checks passed
@gwleclerc
gwleclerc deleted the fix/session-race branch July 19, 2026 21:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant