Skip to content

fix(desktop): first message of a new chat is silently aborted by the session-context drift guard - #63486

Closed
ccowan93 wants to merge 1 commit into
NousResearch:mainfrom
ccowan93:fix/first-message-new-chat-aborted
Closed

ccowan93 wants to merge 1 commit into
NousResearch:mainfrom
ccowan93:fix/first-message-new-chat-aborted

Conversation

@ccowan93

Copy link
Copy Markdown
Contributor

Summary

Starting a brand-new chat and pressing Enter on the first message creates the session and sets its title to the typed text, but never actually sends the message to the model. The user has to press Enter a second time to send. Every subsequent message in the chat works normally — only the very first one is dropped.

Root cause

Commit 7b5ba20 ("resync fallback editor after config reload") added a sessionContextDrifted() guard to the submit pipeline in apps/desktop/src/app/session/hooks/use-prompt-actions/submit.ts, meant to abort a submit if the user switches sessions mid-async-window (protects against #54527).

On a brand-new chat the flow is:

  1. submitPromptText captures startingActiveSessionId = null, startingStoredSessionId = null, startingRouteToken = <old>.
  2. Calls createBackendSessionForSend(visibleText) — which creates the session, sets its title, navigates the URL to the new session, and mutates activeSessionIdRef / selectedStoredSessionIdRef / the route token to point at the new session (lines 240–259 of use-session-actions/index.ts).
  3. Right after that returns, the guard runs:
    if (sessionContextDrifted()) {
      return abortForSessionSwitch(sessionId)
    }
    sessionContextDrifted() checks selectedStoredSessionIdRef.current !== startingStoredSessionId → "<new-id>" !== null → true. And getRouteToken() !== startingRouteToken → also true (URL changed).
  4. Guard trips → abortForSessionSwitch(sessionId) → drops the optimistic message, releases busy, returns false. The message never reaches prompt.submit.
  5. But the session is already created with the user's text as the title and the URL already navigated — so the user sees a new chat with their text as the title and reasonably assumes "it sent." The second Enter works because sessionId is now truthy, createBackendSessionForSend is skipped, and the guard passes.

The guard cannot distinguish "I created a session myself" from "the user switched to a different session mid-submit." On a fresh chat, the creator is the one who moved the refs.

Fix

Re-anchor the drift baseline to the freshly-created session right after createBackendSessionForSend returns, so the post-creation guard only catches a real concurrent session switch — not the session creation we just performed ourselves.

       if (!sessionId) {
         try {
           sessionId = await createBackendSessionForSend(visibleText)
         } catch (err) {
           dropOptimistic(null)
           releaseBusy()
           notifyError(err, copy.sessionUnavailable)
           return false
         }

+        // createBackendSessionForSend establishes a new session and updates
+        // activeSessionIdRef / selectedStoredSessionIdRef / the route token to
+        // point at it. Re-anchor the drift baseline to the freshly-created
+        // session so the post-creation guard only catches a *real* concurrent
+        // session switch, not the creation we just performed ourselves. Without
+        // this, the very first message of a new chat always trips the drift
+        // guard (the refs were null before creation, non-null after), aborts
+        // the submit, and leaves the user's message unsent — though the session
+        // is already created with their text as the title, so Enter "works" on
+        // the second press because creation is skipped on the existing session.
+        startingActiveSessionId = activeSessionIdRef.current
+        startingStoredSessionId = selectedStoredSessionIdRef.current
+        startingRouteToken = getRouteToken()
+
         if (sessionContextDrifted()) {
           return abortForSessionSwitch(sessionId)
         }

starting* become let instead of const. The three other sessionContextDrifted() call sites (after session.resume and after syncAttachmentsForSubmit) are unaffected — they still catch genuine concurrent session switches.

Repro

  1. Start a fresh chat in the desktop app.
  2. Type one message and press Enter.
  3. Expected: message sends, assistant responds.
  4. Actual (on main): a new session appears in the sidebar with your text as the title, the URL navigates to /chat/<new-id>, but no message is sent — the composer is empty and idle. Press Enter again (after re-typing) and it goes through.

Test plan

  • npx tsc -p . --noEmit passes (desktop renderer)
  • npm run build succeeds (vite + electron-main bundle)
  • Manual E2E on macOS arm64: first message of a new chat now sends on the first Enter; subsequent messages unaffected; concurrent session switches during a long submit still abort correctly (the other three drift-guard sites are untouched)
  • Existing desktop tests pass (scripts/test-desktop.mjs)

Notes

The sessionContextDrifted() guard added in 7b5ba20 ("resync fallback
editor after config reload") aborts a submit when the session context
changes mid-async-window. But on a brand-new chat, createBackendSessionForSend
itself mutates activeSessionIdRef / selectedStoredSessionIdRef / the route
token to point at the freshly-created session — so the guard always trips
on the very first message of a new chat, aborts the submit, and leaves the
user's text unsent. The session is already created with their text as the
title and the URL already navigated, so the second Enter appears to "work"
because session creation is skipped on the existing session.

Re-anchor the drift baseline to the freshly-created session right after
createBackendSessionForSend returns, so the guard only catches a *real*
concurrent session switch — not the session creation we just performed
ourselves.
@alt-glitch alt-glitch added type/bug Something isn't working comp/desktop Electron desktop app (apps/desktop/*) P2 Medium — degraded but workaround exists sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Jul 13, 2026
@alt-glitch

Copy link
Copy Markdown
Contributor

This was generated by AI during triage.

Related: this is one of several open PRs fixing the same regression of merged #54527 (new-chat first message silently aborted by the sessionContextDrifted() guard in use-prompt-actions/submit.ts). Same site, same root cause, competing implementations: #62482 (@DavidMetcalfe, earliest 2026-07-11 05:58Z — captureContext helper, looks canonical), #62571 (const->let re-pin), #62562 (pinned created-session identity), #63035 (binding-threading), #63310 (post-create re-baseline). This PR flips startingStoredSessionId const->let and re-baselines it after createBackendSessionForSend. Tracking issue #62481. Not a duplicate — independent competing fixes; a maintainer should pick the cleanest.

@OutThisLife

Copy link
Copy Markdown
Contributor

Closing as a duplicate of #63624, selected as the canonical fix for the #54527 / #62481 new-chat drift-abort regression (reproduced + confirmed fixed, ships a red-first regression test). Same root cause, same site — consolidating on one. Thanks for the fix; credit noted. See #63624.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/desktop Electron desktop app (apps/desktop/*) P2 Medium — degraded but workaround exists sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants