Skip to content

Page back to a bounded read's window instead of filtering one page (comms-rldp) - #221

Draft
GraemeF wants to merge 3 commits into
mainfrom
boundedread-comms-rldp
Draft

Page back to a bounded read's window instead of filtering one page (comms-rldp)#221
GraemeF wants to merge 3 commits into
mainfrom
boundedread-comms-rldp

Conversation

@GraemeF

@GraemeF GraemeF commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Zulip's GET /messages selects a page by anchor and count and has no timestamp predicate, so since and until never reached the realm. They filtered whatever a newest-anchored page happened to return. A window lying below that page came back empty, and an empty result looks the same as a channel with nothing in it. comms-rldp carries the measured runs.

readWindow in the new packages/zulip/history-paging.ts walks back one anchored page at a time until the window's lower bound is crossed, the cap is met, or history runs out. history.readChannel, history.readThread and inbox.replay all go through it.

Three properties the walk has to hold, each already established somewhere in the tree:

  • A Zulip anchor is a range hint, not an exact match, so a page can come back around a neighbour of the id asked for. The walk dedupes by id and re-derives each anchor from the rows that actually returned.
  • An anchor naming no live message answers success with an empty list, so an exhausted or deleted anchor ends the walk quietly instead of raising.
  • renderedContentForBatch re-issues the query it is handed, so mentions have to be resolved per page against that page's own query. Rendering once for the whole walk would index one page and report no mentions on every other.

What limit means now

It is still a cap and it still cuts from the old end, keeping the newest. What changed is that the walk keeps going while the cap is unmet and the bound is unreached, which is the case that returned the false zero.

So a bounded read that hits limit is still sheared at the old end of its window, and looks identical to a complete one. That is the commoner failure of the two, since anything sought near a window's start sits exactly there. Both tool descriptions and the limit field now say so, and name the check that separates the cases: compare the oldest message returned against since. docs/agent-experience.md gains it as a divergence, since a human scrolling back sees where they stopped.

Calls worth a look

  • Page size. An unbounded read is answered by the newest page, so its page is exactly limit and its budget is one — the request shape is unchanged. A bounded read pages at max(limit, 100), because pages it walks past cost a request each and a limit of 3 would otherwise crawl.
  • Page budget. HISTORY_MAX_PAGES is 20. A window far enough back to outrun it returns a short read rather than walking the realm. limit stops the common case long before it.
  • A page shorter than asked for does not end the walk. It almost certainly means history is exhausted, but the walk asks once more and stops on a page that adds nothing new. Terminating on a guess about Zulip's paging would truncate silently; the price of not guessing is one extra request at the end of an exhausting walk.
  • Concurrency. The walk is sequential by construction and does not fan out. The directory read is forked so it overlaps the first page, as it did before. It is now joined unconditionally, because a walk that finds nothing maps no page — a failed /users read was otherwise dropped with the interrupted fiber and answered [].
  • One existing test's fixture changed. The replay mention test scripted exactly two /messages responses. The walk asks a third time, so it now has a sticky empty page behind the script, the way a realm would. Its assertion is untouched.
  • inbox.replay. REPLAY_NUM_BEFORE is now the page size rather than the ceiling, so the roughly two days of realm history it covered is no longer a limit on catch-up.

Closes comms-rldp.

Zulip's GET /messages selects a page by anchor and count and has no
timestamp predicate, so since and until never reached the realm: they
filtered whatever a newest-anchored page happened to return. A window
lying below that page came back empty while looking authoritative.

readWindow walks back one anchored page at a time until the window's
lower bound is crossed, the cap is met, or history runs out. It is
sequential by construction, dedupes by id because a Zulip anchor is a
range hint that returns a neighbour rather than an exact match, and
maps each page against the query that fetched it, which is what the
rendered-content read has to re-issue.

readChannel, readThread and the boot catch-up path all go through it.
An unbounded read still asks for one page and gets the newest limit.
read_channel and read_thread promised a result "bounded by optional
since/until/limit". The since/until half is true now that a window is
read out of history. The limit half is not what it sounds like: it cuts
from the old end, so a result holding exactly limit messages may be
missing older ones inside the window it was asked for, and looks
identical to a complete read.

Both tool descriptions and the limit field now say that, and name the
check that separates the two cases: compare the oldest message returned
against since.

agent-experience.md gains the divergence rather than losing one. A human
scrolling back sees where they stopped; an agent whose read is capped
does not.
The directory read is forked so it overlaps the walk rather than sitting
in front of it, and the join lives where the mapping needs it. A window
that turns out empty maps no page, so nothing joined and a /users failure
was dropped with the interrupted fiber. The read then answered [] — the
same shape as a channel with nothing in the window.

Both call sites now join unconditionally before returning.
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