Skip to content

Race condition: project creation navigates to session route before workspace cache contains the new session #370

Description

@i-trytoohard

Bug

Creating a new project successfully spawns an orchestrator session, but the UI navigates to /projects/$projectId/sessions/$sessionId before useWorkspaceQuery's cache contains the new session. The route renders an empty/"Session not found" view until a later refetch catches up (15s interval or manual click).

Source: Discord (#bug-triaging) | Reported by: Cruzer | Analyzed against: 2155c3c
Confidence: High — traced the exact code path and React Query v5.101.0 source

Reproduction

  1. Add a new project via the "New project" button in the sidebar.
  2. The createProject callback in _shell.tsx runs: creates project → optimistic update → spawns orchestrator → invalidateQueriesnavigate to the session route.
  3. SessionView mounts, looks up the session from useWorkspaceQuery().data — it's not there.
  4. UI shows "Session not found. It may have been cleaned up — pick another from the sidebar." (or a blank terminal area depending on timing).
  5. The session appears only after the 15s refetchInterval fires, or after the user clicks the orchestrator again (which re-spawns + invalidates).

Root Cause

The createProject callback in frontend/src/renderer/routes/_shell.tsx (lines 58–109) has a timing flaw:

// Line 94 — optimistic update: workspace added with sessions: []
updateWorkspaces((current) => [workspace, ...current.filter((item) => item.id !== workspace.id)]);

// Lines 96–101
const sessionId = await spawnOrchestrator(workspace.id);
await queryClient.invalidateQueries({ queryKey: workspaceQueryKey });
void navigate({ to: "/projects/$projectId/sessions/$sessionId", params: { ... } });

Three factors combine:

  1. setQueryData resets dataUpdatedAt — The optimistic updateWorkspaces() (line 94) calls queryClient.setQueryData(), which sets dataUpdatedAt = Date.now() and marks the query as fresh.

  2. invalidateQueries swallows refetch errors — In React Query v5.101.0, refetchQueries does promise.catch(noop) (queryClient.js:174). So await invalidateQueries(...) always resolves, even if the refetch fails. If the daemon is briefly busy during the heavy spawn workload (git worktree creation, workspace provisioning, Zellij runtime launch), the GET /api/v1/sessions refetch can fail — and invalidateQueries resolves anyway, leaving the cache with the optimistic sessions: [] data.

  3. staleTime: 10_000 suppresses mount-triggered refetch — The global QueryClient config (frontend/src/renderer/lib/query-client.ts:6) sets staleTime: 10_000. When SessionView mounts and its useWorkspaceQuery() observer registers, it does NOT trigger a refetch because Date.now() - dataUpdatedAt < 10_000 (the setQueryData at step 1 just reset the timestamp). The session stays missing until the 15s refetchInterval in workspaceQueryOptions fires.

The backend is not the problem — Manager.Spawn (backend/internal/session_manager/manager.go:191) persists the session to the SQLite store synchronously before returning, so GET /api/v1/sessions would return it on a successful refetch. The issue is the React Query cache not being reliably refreshed before navigation.

Note: The existing-orchestrator respawn path in Sidebar.tsx (lines 412–415) has the same invalidateQueriesnavigate pattern but is less likely to hit this because it doesn't do the setQueryData optimistic update that resets dataUpdatedAt.

Fix

The most robust approach: optimistically inject the spawned session into the workspace's sessions array after spawnOrchestrator returns the sessionId, so SessionView can find it immediately regardless of refetch success:

const sessionId = await spawnOrchestrator(workspace.id);
updateWorkspaces((current) =>
    current.map((w) =>
        w.id === workspace.id
            ? { ...w, sessions: [
                { id: sessionId, workspaceId: w.id, workspaceName: w.name, title: "orchestrator",
                  kind: "orchestrator", branch: `session/${sessionId}`,
                  status: "working", createdAt: new Date().toISOString(), prs: [] },
                ...w.sessions,
              ] }
            : w,
    ),
);
await queryClient.refetchQueries({ queryKey: workspaceQueryKey }); // forces fresh data ASAP
void navigate({ ... });

Secondary improvement: soften the SessionView "Session not found" guard (SessionView.tsx:140) to show a brief loading/retry state for recently navigated sessions instead of immediately declaring the session missing.

Impact

  • Severity: Medium — every new project creation hits a broken-looking empty view. Workaround exists (wait 15s or re-click orchestrator) but the UX is confusing.
  • Affected: All users on the latest main (2155c3c), Electron app.

Related

  • frontend/src/renderer/routes/_shell.tsx:58–109createProject callback
  • frontend/src/renderer/hooks/useWorkspaceQuery.ts:67–76workspaceQueryOptions (15s refetch interval)
  • frontend/src/renderer/lib/query-client.ts:3–9 — global staleTime: 10_000
  • frontend/src/renderer/components/SessionView.tsx:140–146 — "Session not found" guard
  • React Query v5.101.0 queryClient.js:174refetchQueries swallows fetch errors

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingfrontendElectron frontend laneneeds-triageMaintainer needs to evaluate this issuepriority: mediumFix when convenient

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions