Skip to content

[fix] Repair the attachment-only send and the ghost session row - #7028

Open
mmabrouk wants to merge 4 commits into
release/v0.119.1from
fix/unqueued-send-failure-edge-1191
Open

mmabrouk wants to merge 4 commits into
release/v0.119.1from
fix/unqueued-send-failure-edge-1191

Conversation

@mmabrouk

@mmabrouk mmabrouk commented Sep 21, 2026

Copy link
Copy Markdown
Member

Five defects from the v0.119.1 review, all in the chat send path or next to it. Risk map entries 5, 16, 20 and review findings R9, R10.

1. An attachment-only send carries an empty text part

Attach a file, leave the composer empty, press Enter. The submit guard allows it (AgentConversation.tsx:826 and useAgentConversation.ts:1301 both require only text or files), and the message goes out with {type: "text", text: ""} in front of the attachment. The SDK keeps any text that is not None (sdks/python/agenta/sdk/agents/adapters/vercel/messages.py:110), so the model receives an empty text content block, and Anthropic-family models refuse those. On a first turn that is the whole run.

v0.119.0 did not do this. It used the AI SDK shorthand, item.fileParts?.length ? (item.text ? {text, files} : {files}) : {text}, which omitted the text entirely for a file-only send. Two files replaced that shorthand with an explicit message object in this release and dropped the guard with it. The evidence that it was an oversight rather than a decision is that the durable path kept it (useServerSessionInputs.ts:331).

The guard is back, and it now lives in one place. outboundUserParts in @agenta/chat/assets builds the parts for all three send paths:

before (non-durable and desktop):  [{type: "text", text: ""}, file]
after  (all three paths):          [file]

It guards on the text that is actually sent, executionText ?? text, rather than on text alone. That is a hair stricter than the durable path was: a file-only send on a session with a pending display edit used to drop the edit context, and now carries it. Neither path can emit an empty text part any more, and the text-then-files order from this release is kept.

2. A failed first send leaves a ghost session row on /m

On /m a brand new session appears in the rail the moment you hit send, before the server knows it exists (#6776). The row follows that send: admitted keeps it until the server lists the session, failed takes it away.

The non-durable path never reported a failure. dispatchUnqueued hands the message to the AI SDK and immediately reports it admitted, which is the fix from dbc3efebf1 for a row stuck at submitting. Nothing took that back when the run request failed, so the row stayed in the rail for the rest of the page session, pointing at a session the server will never list, and tapping it opened an empty chat.

The review comment on #6783 read the cause as a swallowed promise rejection, because sendQueued ends in .catch(ignoreStreamRejection). That is not it. Chat.makeRequest in the AI SDK catches a refusal, a 5xx and a dropped connection alike and lands them as status: "error" without rethrowing, so sendMessage resolves and that .catch never sees anything. Handing the promise back would have carried nothing.

useAgentChatQueue still reports the send as admitted and now treats that admission as provisional. lastSentRef already holds the message until a turn is named, so a new effect reports onSendFailed when status turns "error" while it still does.

  • A stream that fails after the runner named the turn has already dropped the ref, so it stays a failed turn the transcript owns, not a failed send.
  • A user stop lands on "ready", so it never reaches the effect.
  • An accepted run whose invoke stream then dropped (acceptedRunPending) is a disconnect, not a failed send: the server sent data-session-accepted and the shared run owns a turn the records will carry, before any message-metadata frame names it. canReleaseNow already reads the flag the same way.
  • The effect reports without consuming the ref, because the desktop's session-busy recovery claims the same message through takeLastSent from an effect that runs later.

markLocalSessionAccepted and dropUnacceptedLocalSession now carry the send's id, so a retraction retires the row while a later message's failure still leaves it alone, which is the guard dbc3efebf1 relied on.

How far defects 1 and 2 reach

Both sit behind the same gate, and it is worth stating plainly because it is easy to get wrong.

sendQueued has exactly one caller, dispatchUnqueued, which has exactly two: the local-send branch of submit and the queue-release effect. When the capability probe reports queue: true, submit returns inside the durable branch before either the recoverable branch or the local-send branch runs, so nothing is ever pushed into the local queued array and the release effect returns early on queued.length === 0. dispatchUnqueued therefore never runs, on desktop or on /m: both hosts pass the same server: serverInputs adapter (AgentConversation.tsx:506, useAgentConversation.ts:835).

So the non-durable path runs only when the probe reports queue: false, which means AGENTA_SESSIONS_QUEUE set to a non-truthy value, or a backend whose /sessions/streams/ response carries no capabilities object at all, since sessionStreamResponseSchema defaults it to all-false. AGENTA_SESSIONS_QUEUE defaults to true (api/oss/src/utils/env.py:724) and is set nowhere in this repo.

Both fixes are therefore for self-hosted and mid-upgrade deployments. The risk map's entry 5 says the empty-text send "is reachable" on the strength of the submit guard alone; that guard is necessary but not sufficient, because the message still has to travel the non-durable path to pick up the empty part. The durable path kept its guard all along.

3. A rate-limited trace list tells you to check your connection

TracesList.tsx passed only a title to the shared LoadError, which supplies a hard-coded "The request did not come back. Check your connection and try again." A 429 did come back. It now passes description={null}, which the component already handles.

4 and 5. Two review nits

  • agentModelCandidates.ts picked the reported HTTP status with ??, which skips a null error although sourceOutcome counts null as an error. It now takes the first source whose error is not undefined, which is what the field's own documentation says it reports.
  • The MCP acceptance skip reason said the mock upstream "did not answer". The probe also treats a 5xx as unreachable, and a 5xx is an answer. It now says "is unavailable".

Tests

  • @agenta/chat, @agenta/entities and @agenta/mobile unit suites, plus tsc --noEmit for those three and @agenta/oss, and turbo lint --fix.
  • The empty text part is covered on all three send paths: the helper in displayContent.test.ts, the non-durable path end to end in useAgentConversation.test.ts (real useChat, mocked transport, asserting the outbound message the request builder receives), and the durable path in useServerSessionInputs.test.ts. All three fail if the guard is removed.
  • The retraction has five cases in useAgentChatQueue.test.ts, filed under the existing "every way that send can die" block, which until now tested only the durable path. Three of them fail on release/v0.119.1.
  • agent-creation-failure-event.test.ts gains a case for a source that rejected with null.

Risk map entry 21, the dead template tap on the mobile first-run screen, is not fixed here and the suggested fix would be a regression. CONNECT_STEP_MODE is on unless NEXT_PUBLIC_AGENT_CONNECT_STEP=false, which nothing sets, so pickTemplate opens the setup step and returns before it ever reaches the !entityId bail. Adding disabled={... || !entityId} would disable a strip that works today. The tap is dead only for a template whose step.open declines because it has nothing to connect, and the honest fix there is to queue the tap until entityId arrives, the way the ?template= arrival already does. That is new state and an effect, which is not a change to make on the last day of a release.

No demo yet. This branch has no deployed stack, and defect 2 needs a backend reporting queue: false plus a blocked run request, which no component harness can show honestly. A recording goes on this PR once QA runs the steps below.

What to QA

Defects 1 and 2 both need a stack whose API reports queue: false, for the reason above. Set AGENTA_SESSIONS_QUEUE=false on the API container and restart it, then confirm GET /sessions/streams/?session_id=<any-uuid>&project_id=<pid> answers "queue": false. On a default stack neither defect reproduces, and that is the first thing to check.

Defect 1, desktop and /m. Open an agent chat, attach a file, leave the composer empty, press Enter. On an Anthropic model the turn now runs instead of failing its first request. Regression: send text plus a file and confirm the text still arrives, still ahead of the file. Regression on a default queue: true stack: attachment-only sends keep working, since they already did.

Defect 2, same queue: false stack.

  • On /m, open an agent and start a fresh session with +. Block the run request (DevTools request blocking on */services/*/invoke*, or stop the services container) and send a message. The transcript shows the run error, and the session rail does not keep a row for that session. Before the fix, the row stays and opens an empty chat until you reload.
  • Regression, same stack: a send that succeeds still puts a row in the rail immediately and keeps it until the server lists the session. Then send a second message and block that one; the row survives, because that failure belongs to a later message.
  • Regression, a normal queue: true stack: a fresh session's first send still puts a row in the rail, and a refused send still takes it away.

Defect 3. Trigger a 429 on the mobile traces list. The error card shows the rate-limit message and no "check your connection" line, with Try again still there.

The non-durable send path reports admission the moment the message reaches the
transport (dbc3efe), because that is the only admission this path has.
Nothing took it back when the run request then failed, so on /m a fresh
session's rail row stayed marked accepted for a session the server will never
list, and tapping it opened an empty chat.

The AI SDK never rejects a send. `Chat.makeRequest` catches a refusal, a 5xx
and a dropped connection alike and lands them as `status: "error"`, so the
`.catch(ignoreStreamRejection)` on `sendQueued` was never going to see one.
That status is this path's failure event, so the queue hook reports
`onSendFailed` when it arrives while `lastSentRef` still holds the message,
which means no turn was ever named. A stream that fails after the turn is named
has already dropped the ref and stays a failed turn; a user stop lands on
"ready" and never reaches the effect.

The local session registry now carries the id of the send that admitted a row,
so a retraction retires it while a later message's failure still leaves it
alone, which is the guard dbc3efe relied on.

Two review nits from the same release ride along: the model-source report picks
its HTTP status from the first source whose error is not undefined, matching
what the field documents, and the MCP acceptance skip reason says "is
unavailable" rather than "did not answer", since the probe counts a 5xx as
unreachable too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mmabrouk

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

Understand this PR’s impact

Explore downstream dependencies and potential security impact with Blast Radius.

View blast radius →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

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

Review profile: CHILL

Plan: Advanced

Run ID: 69caa23c-e1e8-4a73-8aa8-ad0bf2aa5e96

📥 Commits

Reviewing files that changed from the base of the PR and between 7fae987 and abe0eae.

📒 Files selected for processing (1)
  • web/packages/agenta-chat/tests/unit/hooks/useAgentChatQueue.test.ts

Included review availability: Your plan provides up to 8 included reviews per hour; 6 remain after this review.


📝 Summary

Summary by CodeRabbit

  • Bug Fixes
    • Improved chat handling when sends fail before acceptance, while preserving established conversations.
    • Local sessions are removed only when the send that created them fails.
    • Chat callbacks now identify the related message for more reliable tracking.
    • Improved agent creation status reporting when provider checks return empty errors.
    • Expanded MCP connection diagnostics to include unreachable, slow, or server-error responses.
    • Attachment-only messages no longer include an empty text part.
    • Rate-limited trace errors now show focused retry guidance.

Walkthrough

The change adds send-aware provisional admission handling, centralizes outbound message-part construction, corrects source-status selection for null rejections, and updates upstream and rate-limit diagnostics.

Changes

Chat and local-session lifecycle

Layer / File(s) Summary
Provisional send retraction
web/packages/agenta-chat/src/hooks/useAgentChatQueue.ts, web/packages/agenta-chat/src/hooks/useAgentConversation.ts, web/packages/agenta-chat/tests/unit/hooks/useAgentChatQueue.test.ts
Callbacks now include message ids. A non-durable send reports failure once when an error occurs before a turn is named.
Send-aware local-session admission
web/packages/agenta-entities/src/session/core/localSessions.ts, web/mobile/src/features/chat/LiveConversation.tsx, web/packages/agenta-entities/tests/unit/session-local-sessions.test.ts
Local sessions record the admitting send. A failure removes the session only when its send id matches the admitting send.

Outbound message parts

Layer / File(s) Summary
Shared outbound parts construction
web/packages/agenta-chat/src/assets/displayContent.ts, web/packages/agenta-chat/src/hooks/useAgentConversation.ts, web/packages/agenta-chat/src/hooks/useServerSessionInputs.ts, web/oss/src/components/AgentChatSlice/AgentConversation.tsx
Outbound message construction uses outboundUserParts. The helper selects execution text, omits empty text parts, and appends file parts.
Outbound parts validation
web/packages/agenta-chat/tests/unit/assets/displayContent.test.ts, web/packages/agenta-chat/tests/unit/hooks/useAgentConversation.test.ts, web/packages/agenta-chat/tests/unit/hooks/useServerSessionInputs.test.ts
Tests cover text messages, execution text, attachment ordering, and attachment-only messages.

Source status reporting

Layer / File(s) Summary
First-defined source status
web/packages/agenta-entities/src/workflow/state/agentModelCandidates.ts, web/packages/agenta-entities/tests/unit/agent-creation-failure-event.test.ts
Status selection now uses the first non-undefined source error, including null rejection outcomes.

Diagnostic messages

Layer / File(s) Summary
Expanded failure diagnostics
web/oss/tests/playwright/acceptance/utils/mcpConnections.ts, web/mobile/src/features/observability/TracesList.tsx
The MCP diagnostic includes unreachable, slow, and 5xx upstream conditions. Rate-limit errors no longer show the default connection description.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ChatQueue
  participant Conversation
  participant LocalSession
  participant Transcript
  ChatQueue->>Conversation: onSendAccepted(message with id)
  Conversation->>LocalSession: markLocalSessionAccepted(sessionId, sendId)
  Transcript-->>ChatQueue: error without named turn
  ChatQueue->>Conversation: onSendFailed(message with id)
  Conversation->>LocalSession: dropUnacceptedLocalSession(sessionId, sendId)
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 60.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 16 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies two primary fixes: attachment-only sends and ghost session rows. It is concise and directly related to the main changes.
Description check ✅ Passed The description is detailed and directly explains the five fixes, affected send paths, tests, and QA steps.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

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

github-actions Bot commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

Website preview

Preview URL: https://pr-7028-agenta-website-preview.mahmoud-637.workers.dev

Built from abe0eaef3443afa6b314f744b517592b4a8596d2. This comment updates in place on every push.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

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

Review profile: CHILL

Plan: Advanced

Run ID: fdb39e74-b412-4cfc-b0d9-d58aed085536

📥 Commits

Reviewing files that changed from the base of the PR and between b0e817c and 67918dc.

📒 Files selected for processing (9)
  • web/mobile/src/features/chat/LiveConversation.tsx
  • web/oss/tests/playwright/acceptance/utils/mcpConnections.ts
  • web/packages/agenta-chat/src/hooks/useAgentChatQueue.ts
  • web/packages/agenta-chat/src/hooks/useAgentConversation.ts
  • web/packages/agenta-chat/tests/unit/hooks/useAgentChatQueue.test.ts
  • web/packages/agenta-entities/src/session/core/localSessions.ts
  • web/packages/agenta-entities/src/workflow/state/agentModelCandidates.ts
  • web/packages/agenta-entities/tests/unit/agent-creation-failure-event.test.ts
  • web/packages/agenta-entities/tests/unit/session-local-sessions.test.ts

Included review availability: Your plan provides up to 8 included reviews per hour; 3 remain after this review.

Comment thread web/packages/agenta-chat/src/hooks/useAgentChatQueue.ts Outdated
@coderabbitai

coderabbitai Bot commented Sep 21, 2026

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Already reviewed the last commit. Use @coderabbitai full review to rerun a review of the entire changeset.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Attaching a file and pressing Enter with an empty composer sends a message whose
first part is `{type: "text", text: ""}`. The SDK keeps any text that is not None,
so the model receives an empty text content block, and Anthropic-family models
refuse those. On a first turn that is the whole run.

v0.119.0 used the AI SDK shorthand, which omitted the text entirely for a
file-only send. Two files replaced that shorthand with an explicit message object
in this release and dropped the guard with it; the durable path kept it, which is
the evidence it was an oversight.

Reach is narrower than it looks: `sendQueued` runs only on the non-durable
path, which a deployment takes only when the session capability probe reports
`queue: false`. `AGENTA_SESSIONS_QUEUE` defaults to true, so a default cloud or
self-host send goes durable and was never affected. Both hosts are gated the
same way; the desktop passes the same server adapter as /m.

All three send paths now build their parts through one `outboundUserParts`
helper, so the guard cannot go missing on one of them again. It guards on the
text actually sent, `executionText ?? text`, so a file-only send with a pending
display edit keeps that edit rather than dropping it. The text-then-files order
from this release is unchanged.

Also fixes the mobile traces list telling a rate-limited user to check their
connection: the shared `LoadError` supplies that line by default, and a 429 did
come back, so the list now passes `description={null}`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mmabrouk mmabrouk changed the title [fix] Drop a mobile session row when its first send never becomes a turn [fix] Repair the attachment-only send and the ghost session row Sep 21, 2026
@mmabrouk

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Sep 21, 2026

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Already reviewed the last commit. Use @coderabbitai full review to rerun a review of the entire changeset.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@github-actions

github-actions Bot commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

Railway Preview Environment

Preview URL https://gateway-pr-7028.up.railway.app/w
Project agenta-oss-clone-spike
Image tag pr-7028-ef787bbc
Status Deployed
Railway logs Open logs
Workflow logs View workflow run
Updated at 2026-09-21T21:56:18.722Z

mmabrouk and others added 2 commits September 21, 2026 21:41
…stream

`acceptedRunPending` comes from the `data-session-accepted` frame, which carries
the execution id and arrives independently of the `message-metadata` frame that
names the turn. So the server can have accepted the run while `latestTurnId` is
still null and `lastSentRef` still holds the message. If the invoke stream then
dropped, the retraction effect read that as a send that never left and retired a
row for a turn the records will carry.

The effect now skips a pending accepted run, which is how `canReleaseNow`
already reads the same flag.

Found by CodeRabbit on #7028.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`lastSentRef` is written only by the two `dispatchUnqueued` call sites, which
`admit` returns before when the server advertises a durable queue, so the
retraction effect cannot reach a durable send. Nothing pinned that, and a
durable send already reports its own failure, so a retraction on top would show
a healthy message as failed.

Covers both shapes QA asked about: a durable send followed by `status: "error"`,
and a new durable send made while the chat is ALREADY in error from an earlier
turn. Setting `lastSentRef` in the durable branch fails it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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