close
Skip to content

fix: keep cloud user message visible while log echo is pending#1890

Draft
VojtechBartos wants to merge 12 commits intomainfrom
posthog-code/cloud-queued-messages-optimistic
Draft

fix: keep cloud user message visible while log echo is pending#1890
VojtechBartos wants to merge 12 commits intomainfrom
posthog-code/cloud-queued-messages-optimistic

Conversation

@VojtechBartos
Copy link
Copy Markdown
Member

Summary

When you type a follow-up during a cloud run that's mid-turn, the message renders as a queued bubble — but as soon as the turn completes, the bubble disappears for several seconds before the cloud's session/prompt echo arrives in the log stream. The user sees their message vanish.

This mirrors the optimistic-bubble pattern that already exists on the local execution path:

  • sendCloudPrompt now appends an optimistic user_message before firing the API call, and clears it if the send fails.
  • sendQueuedCloudMessages converts each dequeued queued message into an optimistic bubble (first attempt only — retries don't stack), and clears them after retry exhaustion.
  • handleCloudTaskUpdate clears optimistic items when newly appended log entries contain a session/prompt echo. Mirrors replaceOptimisticWithEvent on the local path; cleared in bulk because cloud entries arrive batched.
  • Terminal-status cleanup also clears optimistic items so a dying run doesn't leave a ghost bubble.

Test plan

  • pnpm --filter code test — 945 passed (2 new tests for the cloud optimistic + failure paths)
  • pnpm --filter code typecheck
  • pnpm lint
  • Manual: type a follow-up during a cloud turn → bubble stays visible end-to-end (no gap when the queue drains)
  • Manual: send a direct cloud message during an in-progress run → bubble visible immediately, replaced cleanly by the echo
  • Manual: resume a finished cloud run with a queued message → bubble appears as soon as the run reaches in_progress
  • Manual: force a network failure mid-send → bubble disappears alongside the existing error toast (no ghost)

Created with PostHog Code

@VojtechBartos VojtechBartos self-assigned this Apr 26, 2026
When a follow-up was queued during the final turn of a cloud run, the
terminal-status branch in handleCloudTaskUpdate silently dropped it —
the auto-flush above had also raced ahead with sendCloudPrompt against
a now-finished run, so the user_message command never reached the cloud.

Skip the auto-flush when this update brings the run terminal, and have
the terminal-status branch dequeue any pending messages and replay them
via resumeCloudRun (which spins up a fresh task run carrying the prompt
as pending_user_message).

Generated-By: PostHog Code
Task-Id: 8aeaf6f9-8b18-426a-9453-c668ca17d227
After the queue auto-flush dequeues a message, sendCloudPrompt fires
the user_message command but the cloud log stream takes a moment to
echo back the session/prompt event. Without an optimistic placeholder,
the bubble vanishes during that window and the user thinks the message
was dropped.

Mirror the local optimistic flow: append a user_message item before the
mutate, drop it when the echo arrives, and clear on send failure or
retry exhaustion.

Generated-By: PostHog Code
Task-Id: 8aeaf6f9-8b18-426a-9453-c668ca17d227
@VojtechBartos VojtechBartos force-pushed the posthog-code/cloud-queued-messages-optimistic branch from 0b65c88 to 8ff7273 Compare April 27, 2026 10:24
…lush

Two race conditions could still drop the message after the previous fix:

1. The auto-flush mutate fires while the cloud is in_progress, but the
   run terminates before the cloud handles the user_message — the cloud
   rejects the command and sendQueuedCloudMessages eats the message in
   retry exhaustion. Catch the failure in sendCloudPrompt and, if the
   cloud has gone terminal in the meantime, replay through resumeCloudRun.

2. The mutate succeeds but the cloud terminates before echoing back the
   session/prompt request — the optimistic bubble hangs, the message is
   silently lost on the cloud side. When the terminal-status block runs
   with no queue but a pending optimistic user_message, reconstruct the
   prompt from the optimistic items and replay through resumeCloudRun.

Generated-By: PostHog Code
Task-Id: 8aeaf6f9-8b18-426a-9453-c668ca17d227
…ress queue

The cloud's /command/ endpoint accepts user_message commands at any time
once the run is in_progress and queues them server-side until the agent
turn ends — same shape as ACP's session/prompt queueing in the local
agent. The renderer was mirroring that queueing locally, which kept
introducing race conditions: messages got lost when the run terminated
between dequeue and mutate, when the mutate succeeded but the cloud
silently terminated before processing, or anywhere in between.

Send straight to cloud as soon as the user submits. Show the message in
chat right away as an optimistic bubble; if a prior turn is still in
flight, mark `isQueued: true` so the bubble renders with the same
"queued" affordance the user is used to. The real session/prompt event
arrives via the log stream and replaces the optimistic item.

The local messageQueue now exists only for the sandbox-not-ready window
(cloudStatus !== "in_progress"); handleCloudTaskUpdate flushes it
through the regular send path once the run goes in_progress, and the
terminal-status branch falls back to resumeCloudRun if anything is left
local when the run finishes.

Removes ~150 lines of auto-flush, retry, and replay machinery.

Generated-By: PostHog Code
Task-Id: 8aeaf6f9-8b18-426a-9453-c668ca17d227
The previous attempt rendered an optimistic bubble with an isQueued flag
to mimic the queued affordance. In practice the bubble was either
invisible or vanished too quickly for the user to perceive.

Drop the flag and lean on the existing messageQueue / QueuedMessageView
instead: when sendCloudPrompt sees a prior turn is in flight, push the
message to messageQueue (which renders the familiar queued bubble at
the bottom of the chat) AND fire the user_message command to the cloud
immediately. When the cloud's session/prompt echo for our message lands
in the log stream, handleCloudTaskUpdate pops the oldest queued message
— the agent has just started processing it, and the real event takes
over the visual.

For the idle case (no prior turn), keep the existing optimistic bubble
to fill the dequeue→echo gap.

Generated-By: PostHog Code
Task-Id: 8aeaf6f9-8b18-426a-9453-c668ca17d227
Match the local agent's behavior: when the user submits during a
running cloud turn, hold the message in the local messageQueue (queued
bubble shown via QueuedMessageView) and only POST the user_message
command after the prior turn's end_turn lands in the log stream.

handleCloudTaskUpdate's auto-flush picks up the queued message once
isPromptPending flips to false, drains the queue, and routes through
sendCloudPrompt (which now adds an optimistic bubble for the dispatch
gap until the cloud's session/prompt echo arrives).

The terminal-status branch still resumes through a fresh run if the
cloud goes terminal with anything pending; the catch in sendCloudPrompt
still falls back to resumeCloudRun if the user_message mutate fails
because the run terminated mid-flight. Both paths protect the user's
message from being silently dropped.

Generated-By: PostHog Code
Task-Id: 8aeaf6f9-8b18-426a-9453-c668ca17d227
The hold-locally-then-dispatch design dropped messages whenever the
cloud's lifecycle didn't line up with the renderer's expectations. The
immediate-dispatch design got the message through but the queued
affordance only flashed for ~1s.

Combine the two: enqueue the message locally (queued bubble visible)
AND fire the user_message command to the cloud right away. The cloud
queues server-side. handleCloudTaskUpdate now pops one entry from
messageQueue per completed turn (matched against currentPromptId so
nested calls don't cause spurious pops), so the bubble disappears
exactly when the cloud picks our message up — same UX as the local
agent.

Cleanup on failure paths: if the run terminates while the mutate is in
flight, fall back to resumeCloudRun (queue is dropped along with the
session). For non-terminal failures, remove the queued entry the user
saw so it doesn't outlive the failure toast.

Generated-By: PostHog Code
Task-Id: 8aeaf6f9-8b18-426a-9453-c668ca17d227
Sending user_message during an in-flight cloud turn preempts the
prior turn on the cloud and breaks its response. Hold the follow-up
locally as a queued bubble (matching the local agent UX) and only
fire the user_message mutate after the prior turn's end_turn lands.

handleCloudTaskUpdate's auto-flush re-enters sendCloudPrompt with
priorTurnInFlight=false once isPromptPending flips, at which point
the mutate dispatches into a now-idle agent. Skip the auto-flush if
the same update brings the run to a terminal status — the
terminal-status block below replays via resumeCloudRun instead.

Generated-By: PostHog Code
Task-Id: 8aeaf6f9-8b18-426a-9453-c668ca17d227
…ssage

The auto-flush was firing user_message mutates after the prior turn's
end_turn lands. The cloud accepts the command and queues it server-side
but the run is already winding down — by the time the agent would pick
the message up the run has terminated, and the message is silently
dropped.

Replace the auto-flush mutate path with resumeCloudRun, which is what
the cloud's resume API is actually designed for: a fresh task run
inheriting the prior conversation state, with our prompt carried in as
`pending_user_message`. The cloud then runs a normal turn against the
queued message and emits its session/prompt + assistant content like
any other run.

Direct user input (no prior turn) keeps using sendCloudPrompt's mutate
path; the existing catch-side fallback to resumeCloudRun handles the
case where that mutate fails because the run terminated mid-flight.

Generated-By: PostHog Code
Task-Id: 8aeaf6f9-8b18-426a-9453-c668ca17d227
The auto-flush was dispatching resumeCloudRun the moment end_turn
arrived in the log stream, but the cloud's resume API requires the
prior run to actually be in a terminal state. End_turn alone doesn't
guarantee that — the run may still be wrapping up server-side, in
which case runTaskInCloud rejects the resume, the catch toasts, and
the message is lost.

Drop the auto-flush. Let the queue sit until the cloud's status update
brings the run terminal, at which point the terminal-status block
dequeues + resumeCloudRun. By then the prior run is genuinely terminal
and the resume succeeds.

Generated-By: PostHog Code
Task-Id: 8aeaf6f9-8b18-426a-9453-c668ca17d227
Combine the auto-flush + resume strategies. After end_turn arrives,
auto-flush dequeues the queue and calls sendCloudPrompt, which fires
the user_message mutate. If the cloud accepts, the agent processes
the message normally. If the mutate fails for any reason — terminal
status, transitional wind-down, network blip — sendCloudPrompt now
unconditionally falls back to resumeCloudRun, which spins up a fresh
task run carrying the prompt as `pending_user_message` and inheriting
the prior conversation state.

This covers both single-shot clouds (mutate rejected, resume kicks in)
and multi-turn clouds (mutate accepted, agent processes immediately).
The terminal-status block stays as a final safety net for in-flight
optimistic items when the run goes terminal mid-mutate.

Adds an info log on auto-flush so we can confirm it's firing in
DevTools when reproducing.

Generated-By: PostHog Code
Task-Id: 8aeaf6f9-8b18-426a-9453-c668ca17d227
The user_message sendCommand path is the wrong tool for queued cloud
follow-ups. While a turn is in flight it preempts the prior response;
after end_turn the cloud either rejects it or silently drops it as the
run winds down. Both modes leave the user with a vanished bubble and
no agent reply.

Skip the mutate entirely. After end_turn, dequeue and call
resumeCloudRun, which uses the same /tasks/{id}/run/ endpoint the
initial task run uses, with `resume_from_run_id` and
`pending_user_message`. The cloud creates a new run that inherits the
prior conversation state and starts a normal turn against the queued
prompt — exactly how follow-ups are designed to work in this cloud's
single-turn-per-run model.

If the resume call fails the auto-flush's outer .catch logs and shows
a toast so the failure is visible to the user instead of silent.

Generated-By: PostHog Code
Task-Id: 8aeaf6f9-8b18-426a-9453-c668ca17d227
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