Skip to content

[#6042] fix(frontend): Stop losing a new session's first message on rapid create - #6048

Open
mmabrouk wants to merge 1 commit into
mainfrom
fix/first-message-dispatch
Open

[#6042] fix(frontend): Stop losing a new session's first message on rapid create#6048
mmabrouk wants to merge 1 commit into
mainfrom
fix/first-message-dispatch

Conversation

@mmabrouk

Copy link
Copy Markdown
Member

Context

Creating sessions quickly from the Home composer could silently lose the first message. The session either never came into existence (reproduced live on the 8180 dev stack: "Repro test 1" left no session row, no run, no error, while a second send worked), or it was created and named but its message never dispatched, so it read as busy forever on every reopen (Mahmoud's sessions 183cbbec… and 37cda04e… on bighetzner: named stream rows, zero turns, zero records, no runner log lines).

Three holes lined up, all silent:

  1. The create/seed handoff between Home and the playground rode two single global slots (pendingSessionOpenAtom, agentFirstRunSeedAtom). A second create before the first was consumed overwrote it, and a consumed seed died with the pane if you navigated away during the model/overlay wait.
  2. prepareSendMessagesRequest had no bounds: a workflow whose invocation URL had not loaded yet failed instantly, and a hung await inside the build (the auth-header fetch) parked the send in "submitted" forever with no error and no network request.
  3. Nothing ever reconciled the damage. The transcript is persisted locally, so the unanswered user message replayed on every reload with no hint that the run never started.

Changes

Both handoff carriers are now lists with add/remove writers. AgentChatPanel consumes every queued open for its scope (two rapid creates become two sessions), and a claimed seed leaves the list only once its conversation actually carries a message, so it survives an unmount mid-wait and retries on remount.

The request build now runs through buildRequestWithinDeadline (new assets/boundedRequest.ts): it retries while buildAgentRequest returns null (workflow still loading) and rejects on hang or deadline (15s), so a failed send surfaces through the existing red error bubble instead of an eternal spinner.

On reopen, useSessionHydration runs a one-shot stranded-send check: if the transcript tail is an unanswered user turn, nothing is running, and the durable record log is confirmed empty (records: []; a failed fetch returns null and never stamps), it appends the same run-error carrier the live error path uses: "This message never reached the agent — the run was not started. Send it again."

Tests

  • pendingHandoffs.test.ts: list semantics for both carriers (no overwrite across sessions, replace within a session, identity-based removal).
  • boundedRequest.test.ts: resolves immediately, retries null builds, not-ready error at deadline, timeout error on hang (fake timers).
  • hasStrandedTail cases added to useSessionHydration.test.ts.
  • Full AgentChatSlice suite: 38 files, 398 passed. tsc clean.

What to QA

  • Home composer: type a message, pick an agent, Send, and immediately click Home in the sidebar. Then send a second message. Both sessions exist under Sessions and both ran their first message.
  • Open one of the previously stuck sessions (unanswered first message). It now shows the red "never reached the agent" bubble instead of looking busy; sending again works.
  • Regression: normal single create from Home still auto-sends the first message once, not twice, and the empty-state Start flow still works for a freshly created agent.

Closes #6042

…avigate

Three holes in the first-message dispatch pipeline, all silent (#6042):
the create/seed handoff slots were single globals a second create
overwrote; the request build could fail instantly (invocation URL not
loaded) or park forever (hung auth-header fetch) with no error; and a
transcript stranded by any of these looked busy-forever on every reopen.

- Park create/open handoffs and first-run seeds in per-session LISTS;
  a seed leaves the list only once its conversation carries a message.
- Bound the run-request build: retry while the workflow is loading,
  reject on hang/deadline so the failure renders as an error bubble.
- On reopen, stamp a transcript whose unanswered user tail has a
  confirmed-EMPTY durable record log with a visible 'never reached the
  agent' error instead of an eternal spinner.
@dosubot dosubot Bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Aug 14, 2026
@vercel

vercel Bot commented Aug 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agenta-documentation Blocked Blocked Aug 14, 2026 5:13pm

Request Review

@coderabbitai

coderabbitai Bot commented Aug 14, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Multiple agent sessions can now be opened or started in succession without losing pending requests.
    • First-run prompts and staged files are preserved independently for multiple sessions.
  • Bug Fixes

    • Improved reliability when sending messages while an agent is still becoming ready, with automatic retries and clear timeout errors.
    • Restored sessions with interrupted sends now show an error state so messages can be resent.
    • Prevented duplicate session openings and preserved unrelated pending navigation actions.

Walkthrough

Agent session handoffs now support concurrent pending opens and first-run seeds. Agent request preparation retries within a deadline. Session hydration marks stranded sends with a retryable error.

Changes

Agent session reliability

Layer / File(s) Summary
Queue-based session handoffs
web/oss/src/components/AgentChatSlice/state/*, web/oss/src/components/AgentChatSlice/hooks/useOpenAgentSession.ts, web/oss/src/components/AgentChatSlice/hooks/useStartAgentSession.ts, web/oss/src/components/Sidebar/dynamic/registry.ts, web/oss/src/components/pages/agent-home/hooks/useCreateAgent.ts, web/oss/src/components/AgentChatSlice/state/pendingHandoffs.test.ts
Pending session opens and first-run seeds now use collections. Add and remove actions preserve unrelated entries during navigation and session creation.
Queued session and seed dispatch
web/oss/src/components/AgentChatSlice/AgentChatPanel.tsx, web/oss/src/components/AgentChatSlice/hooks/useFirstRunSeed.ts
The panel processes all scoped pending opens once and delays default seeding while they remain. Seeds remain claimed until message delivery.
Bounded agent request preparation
web/oss/src/components/AgentChatSlice/assets/boundedRequest.ts, web/oss/src/components/AgentChatSlice/assets/boundedRequest.test.ts, web/oss/src/components/AgentChatSlice/hooks/useAgentChatSession.ts
Request construction retries unavailable workflows, detects hung builders, and raises distinct timeout errors.
Stranded-send hydration recovery
web/oss/src/components/AgentChatSlice/hooks/useSessionHydration.ts, web/oss/src/components/AgentChatSlice/hooks/useSessionHydration.test.ts
Hydration detects unanswered user messages with no server records and appends an assistant error carrier. The check skips active or cancelled runs.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk: 🟡 Moderate · up to 4227a

The change improves rapid session creation and makes failed first sends visible, but a transient records lookup failure can still leave an unanswered message without the intended recovery notice until the session is revisited. Merge should wait for that bounded recovery issue to be fixed or explicitly accepted.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the frontend fix for losing the first message during rapid new-session creation.
Description check ✅ Passed The description directly explains the silent message-loss bug, the implemented fixes, and the supporting tests.
Linked Issues check ✅ Passed The changes address issue #6042 by preserving queued handoffs, bounding request creation, and surfacing stranded messages for retry.
Out of Scope Changes check ✅ Passed The implementation and tests remain focused on preventing lost first messages and recovering stranded new sessions.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 60.00%.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/first-message-dispatch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

Railway Preview Environment

Preview URL https://gateway-pr-6048.up.railway.app/w
Project agenta-oss-clone-spike
Image tag pr-6048-0990d30
Status Deployed
Railway logs Open logs
Workflow logs View workflow run
Updated at 2026-08-14T17:25:14.798Z

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: b9326cc2-c54b-4c76-ad89-eafb59764ccf

📥 Commits

Reviewing files that changed from the base of the PR and between 0af145e and 4227acf.

📒 Files selected for processing (14)
  • web/oss/src/components/AgentChatSlice/AgentChatPanel.tsx
  • web/oss/src/components/AgentChatSlice/assets/boundedRequest.test.ts
  • web/oss/src/components/AgentChatSlice/assets/boundedRequest.ts
  • web/oss/src/components/AgentChatSlice/hooks/useAgentChatSession.ts
  • web/oss/src/components/AgentChatSlice/hooks/useFirstRunSeed.ts
  • web/oss/src/components/AgentChatSlice/hooks/useOpenAgentSession.ts
  • web/oss/src/components/AgentChatSlice/hooks/useSessionHydration.test.ts
  • web/oss/src/components/AgentChatSlice/hooks/useSessionHydration.ts
  • web/oss/src/components/AgentChatSlice/hooks/useStartAgentSession.ts
  • web/oss/src/components/AgentChatSlice/state/firstRunSeed.ts
  • web/oss/src/components/AgentChatSlice/state/pendingHandoffs.test.ts
  • web/oss/src/components/AgentChatSlice/state/pendingSessionOpen.ts
  • web/oss/src/components/Sidebar/dynamic/registry.ts
  • web/oss/src/components/pages/agent-home/hooks/useCreateAgent.ts

timer = setTimeout(() => reject(new Error(PREPARE_HUNG_MESSAGE)), remaining)
}),
])
if (result) return result

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Return every non-null build result.

null is the only unavailable-result sentinel in the function type. A valid falsy T, such as 0 or "", retries until it reports PREPARE_NOT_READY_MESSAGE.

Proposed fix
-            if (result) return result
+            if (result !== null) return result
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (result) return result
if (result !== null) return result

} finally {
clearTimeout(timer)
}
await new Promise((resolve) => setTimeout(resolve, retryMs))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Limit the retry sleep to the remaining deadline.

If a null build resolves shortly before the deadline, this line waits another full retryMs. The function can then fail up to 300 ms after its configured 15-second deadline.

Proposed fix
-        await new Promise((resolve) => setTimeout(resolve, retryMs))
+        const delay = Math.min(retryMs, Math.max(0, deadline - Date.now()))
+        if (delay > 0) await new Promise((resolve) => setTimeout(resolve, delay))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await new Promise((resolve) => setTimeout(resolve, retryMs))
const delay = Math.min(retryMs, Math.max(0, deadline - Date.now()))
if (delay > 0) await new Promise((resolve) => setTimeout(resolve, delay))

Comment on lines +468 to +488
useEffect(() => {
if (strandedCheckedRef.current || isHydrating || busy) return
if (liveness.isLoading || liveness.nest.isRunning) return
if (!hasStrandedTail(messagesRef.current)) return
strandedCheckedRef.current = true
let cancelled = false
void getDefaultStore()
.set(fetchSessionRecordsAtom, sessionId)
.then(({records}) => {
if (cancelled || busyRef.current) return
if (!records || records.length > 0) return
const current = messagesRef.current
if (!hasStrandedTail(current)) return
const stamped = [...current, strandedRunErrorCarrier()]
setMessages(stamped)
persistMessages({id: sessionId, messages: stamped})
})
return () => {
cancelled = true
}
}, [isHydrating, busy, liveness, sessionId, messagesRef, busyRef, setMessages, persistMessages])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Retry an inconclusive record fetch.

Line 472 sets strandedCheckedRef.current before the fetch completes. If the fetch returns records: null, Line 478 exits without stamping an error, but every later effect run exits at Line 469. The stranded message then remains unresolved for the mounted session after a transient records failure.

Set the one-shot guard only after a conclusive non-empty or empty result. Schedule a bounded retry when records is unavailable.

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

Labels

bug frontend size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

(bug) First message of a new session can be lost silently; the session shows stuck forever

1 participant