Skip to content

fix(events): stop serving stale cached polls to per-thread clients - #831

Merged
lihongmeng merged 2 commits into
developfrom
fix/per-channel-poll-cache
Oct 24, 2026
Merged

lihongmeng merged 2 commits into
developfrom
fix/per-channel-poll-cache

Conversation

@lihongmeng

Copy link
Copy Markdown
Collaborator

Problem

GET /v1/events caches each answer for 1.5s by its exact parameters, and caches a shared empty page when the caller's cursor equals the newest event seen for that filter. When an event is posted, _invalidate_poll_cache only clears those entries for a fixed list of workspace-wide filters. Polls scoped with channel=, target= or conversation= were never cleared, and neither was the exact-parameter entry.

Clients that re-poll a thread on each SSE signal (the mobile app) were hit hardest:

  1. A non-message event lands in the thread. The client polls channel=<thread>&type=workspace.message&after=<cursor>, gets an empty page, and that page is cached.
  2. The agent's reply lands inside the 1.5s TTL.
  3. The poll triggered by the reply gets the cached empty page. The reply only shows up once some later event causes another poll.

Fix

Both cached answers (the exact-parameter entry and the at-head empty page) now include the workspace's event-write generation. poll_queries already bumps this generation after every committed event write and uses it to key the stream-head cache. So a write now retires every cached poll for its workspace, whatever the filters, and identical polls between writes still share one database query. A poll that read the old generation before the commit can only write under the old key, so it cannot bring back a stale page.

poll_generation() is factored out of stream_head() so both caches read the generation the same way. The existing enumerated invalidation is left as is.

Tests

  • New TestPollCacheFreshness (channel and target scopes): post, catch up (cached empty page), post a reply within the TTL, and the same poll must return the reply. Both cases fail on develop and pass with this change.
  • Full backend suite: the only difference from develop is the two new tests going from failing to passing. The other failures also happen on develop and need a local Postgres (push fanout and similar).

GET /v1/events caches each answer for 1.5s by its exact parameters, and
caches a shared empty page when the caller's cursor is the newest event
seen. Posting an event only cleared those entries for a fixed set of
workspace-wide filters, never for channel=, target= or conversation=
polls, and never for the exact-parameter entry at all.

Clients that re-poll a thread on each SSE signal were hit hardest: a
non-message event in the thread made them cache an empty page at the
current cursor, the agent's reply landed inside the TTL, and the poll
triggered by the reply got the cached empty page. The reply only showed
up once some later event caused another poll.

Both cached answers now include the workspace's event-write generation,
which poll_queries already bumps after every committed event write (it
keys the stream-head cache the same way). A write therefore retires
every cached poll for that workspace, whatever its filters, while
identical polls between writes still share one database query.
@vercel

vercel Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated
openagents-workspace Ready Ready Preview Oct 24, 2026 7:02pm UTC

Request Review

…ll-cache

# Conflicts:
#	workspace/backend/app/routers/events.py
@lihongmeng
lihongmeng merged commit 3a2117a into develop Oct 24, 2026
4 checks passed

This branch was successfully deployed

1 active deployment
Preview — c9061c99 Deployed Oct 24, 2026 by vercel[bot]
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