Skip to content

Bugfix refresh loses unanswered message - #574

Open
QuanCheng-QC wants to merge 4 commits into
developfrom
bugfix/refresh-loses-unanswered-message
Open

Bugfix refresh loses unanswered message#574
QuanCheng-QC wants to merge 4 commits into
developfrom
bugfix/refresh-loses-unanswered-message

Conversation

@QuanCheng-QC

Copy link
Copy Markdown
Collaborator

Problem

Sending a complex task (e.g. 22 screenshots + instructions) and refreshing the page while the agent was still thinking made the just-sent question disappear. Two independent defects combined:

  1. History window exhausted by step events. History hydration fetches the newest 50 workspace.message events, but agent intermediate output (thinking/status/todos) shares that event type — a busy agent pushes the user's message out of the first page. The message was persisted; it just never loaded back.
  2. nginx 1MB default body limit. client_max_body_size was unset, so larger screenshot uploads were 413'd before reaching the backend (which allows 50MB). The frontend swallowed the failure silently and cleared the composer, losing the entire message.

Changes

Backend

  • GET /v1/events supports exclude_message_types (filters payload.message_type; events without one are always kept)
  • Shared _poll_filter_hash between poll-key construction and cache invalidation — the param joins the hash only when set, so legacy adapter-poll hashes stay invalidatable

Frontend

  • History hydration (channel + DM + infinite scroll) excludes step events so the 50-message window only counts real messages
  • One unfiltered catch-up poll after hydration backfills step events (SSE does not replay pre-connection events); skipped when no cursor was established, to avoid paging the whole event log
  • All merge paths (hydration / catch-up / SSE / load-older) go through a sorted, deduped mergeMessages with a monotonic newest-cursor — concurrent arrival order can no longer reorder messages or strand the UI on "agent working"
  • Send failures show a toast and restore the typed text as the thread draft; client-side 50MB per-file validation
  • New vitest setup with unit tests for the merge/ordering logic

nginx

  • Exact-match locations: = /v1/files at 52m (multipart overhead headroom) and = /v1/files/base64 at 70m (~4/3 base64 overhead); other routes keep the default limit

Testing

  • Backend: 5 new tests in tests/test_events.py (exclusion filtering, limit-window regression, cache-key consistency) pass; remaining local failures are pre-existing env issues (no Postgres), identical on clean develop
  • Frontend: npm test (6 vitest cases), tsc --noEmit, next build all pass
  • Needs a deploy-env check: upload of a file near the 50MB boundary through nginx

Refreshing the page while an agent was still working could make the
just-sent question disappear. Two causes were fixed.

History loading counted agent thinking/status/todos events against the
50-event window, so a busy agent pushed the user's own message out of
the first page. GET /v1/events now supports exclude_message_types
(wired into both poll cache keys) and the frontend uses it for channel
history, DM history and infinite scroll. The forward poll still fetches
intermediate events, so the current-steps display is unaffected.

nginx had no client_max_body_size, so its 1MB default rejected larger
image uploads with 413 before the backend's 50MB limit ever applied.
Sending many screenshots failed silently and the whole message was lost.

The send path no longer swallows failures. It shows a toast, restores
the typed text as the thread draft, and validates file size client-side
before uploading.
Run one unfiltered catch-up poll after history hydration. SSE only
carries events published after the connection opens and history now
excludes step events, so without the catch-up poll the steps emitted
before a refresh never loaded and a working agent looked idle.

Share one filter-hash builder between poll_events and the poll-cache
invalidation. The exclude_message_types param joins the hash only when
set, so the enumerated adapter patterns keep their legacy hashes and
head/at-head keys stay invalidatable. Covered by regression tests.

Scope the nginx body limit to the /v1/files upload route instead of the
whole server, and raise it to 52m so multipart overhead cannot 413 a
file of exactly 50MB. The backend still enforces the precise per-file
limit.
Merge incoming messages sorted by timestamp with the id as tiebreaker
instead of appending. The catch-up poll and the SSE stream run
concurrently, so an older status batch could land after a newer
SSE-delivered reply, become the trailing message, and make the UI treat
a finished agent as still working. The newest-message cursor now only
advances chronologically, and the poll loop paginates on a local cursor
so a concurrent SSE jump cannot skip the batches it is backfilling.

Run the catch-up poll only when history hydration succeeded. After a
failed history request the cursor is null and the poll loop would page
through the channel's entire event log from the first message.

Use exact-match nginx locations for the two upload endpoints so
GET /v1/files/{id} and other file routes keep the default body limit.
POST /v1/files/base64 needs its own location with ~4/3 headroom for the
base64 encoding overhead, which the review's prefix-match concern did
not cover.

Revert the incidental next-env.d.ts change from the previous commit —
it was a next build artifact, not an intended edit.
Merge history hydration into live state instead of replacing it. The
SSE stream opens alongside the history request, so a reply delivered
during hydration was dropped by the replace and the cursor rolled back;
a failed catch-up would then never recover it since SSE does not
replay. The empty-history branch likewise no longer clears state the
SSE stream already delivered, and the newest cursor only moves through
bumpNewest so it stays monotonic.

Skip the catch-up poll when no cursor exists after hydration. A
successful history response over a channel containing only excluded
step events left the cursor null, and the poll loop would page through
the channel's entire event log from the beginning.

Extract the merge helpers into lib/message-merge.ts and add vitest with
unit tests covering the out-of-order status-after-reply scenario, the
hydration/SSE interleave, dedup, and the null-timestamp comparator.
The frontend previously had no test setup at all; hook-level timing
tests around the polling loop would additionally need jsdom and API
mocks and are left for follow-up.
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