Skip to content

feat(history): replay a window of a task's history, with older messages on demand - #19

Open
Antisophy wants to merge 2 commits into
CyberShadow:masterfrom
Antisophy:feat/history-window
Open

feat(history): replay a window of a task's history, with older messages on demand#19
Antisophy wants to merge 2 commits into
CyberShadow:masterfrom
Antisophy:feat/history-window

Conversation

@Antisophy

@Antisophy Antisophy commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Opening a task replays its entire history, one WebSocket frame per event. That is fine for a young task and increasingly not fine for an old one: a task here reached ~26,000 events, which takes tens of seconds to stream, builds a DOM of roughly 21,000 nodes, and on a phone can stall the client long enough to drop its WebSocket (which disables the composer, since sending needs a live connection).

Two commits, separable and reviewable on their own:

1. Replay only a window (feat(history): replay only a window of a task's history)

  • New optional history_window with desktop and mobile keys, in messages. Absent or zero replays everything, so current behaviour is the default and nothing changes for anyone who does not opt in.
  • The client sends only its device class; the server picks the number. The server is the only side that always knows it: tasks_list is sent before server_status and is what triggers the first history request, so a client deciding for itself asks for everything whenever those two messages arrive in separate ticks. That failure is invisible on a fast local connection and reliable on a slow one.
  • Each requested message carries a budget of four events, because page weight tracks events rather than bubbles and one turn can drag dozens of tool calls along. Measured on the task above: a 100-message window spans 1,196 events without the budget and ~400 with it.
  • The window always cuts at a non-pending user message, so the client's reducers never see a split turn, and sequence numbers stay true history indices, so anything anchored to them is unaffected.
  • content-visibility on messages now applies only to unwindowed replays. It reserves a guessed 100px per offscreen message, so heights change as they scroll into view and the scroll region grows under a dragged scrollbar handle; that trade only pays when the whole transcript is in the DOM.

2. Load older history on demand (feat(web): load older history on demand)

  • New request_history_before: the client names the seq it currently starts at, and the server replays only the messages before it, framed by task_history_prepend_start/_end.
  • The client reduces that batch into a state of its own and merges it in front of what it holds, so nothing on screen is rebuilt. A separate request, rather than a larger window on the existing one, is what keeps the server from resending everything the client already has.
  • The list is column-reverse, so scrollTop measures distance from the bottom, which is exactly the quantity held constant across the batch: the reader stays put and older messages arrive above them.

Measured end to end on that task with a 30-message window: 127 of 26,039 events replayed, ~1,000 DOM nodes, and a load-more click costing 131 events instead of the whole history.

One of five independent changes for using cydo on a phone: #15 (composer spacing), #16 (the page dragging sideways), #17 (hiding the composer while reading history), #18 (attaching images with a file picker). They touch different code and can merge in any order.

Opening a task replays its entire history, one WebSocket frame per event. A
long-lived task runs to tens of thousands of events, which takes tens of
seconds to stream, builds a DOM to match, and on a phone can stall the client
badly enough to drop its WebSocket.

Add an optional `history_window` (desktop/mobile, in messages). Absent or
zero replays everything, so current behaviour is the default.

The client sends only its device class and the server picks the number,
because the server is the only side that always knows it: `tasks_list` is
sent before `server_status` and is what triggers the first history request,
so a client deciding for itself would ask for everything whenever the two
messages arrive in separate ticks.

Each requested message carries a budget of four events, since page weight
tracks events rather than bubbles and a single turn can drag dozens of tool
calls along. Whichever budget runs out first ends the window, always cutting
at a non-pending user message so the client's reducers never see a split
turn. Sequence numbers stay true history indices, so anything anchored to
them is unaffected.

`content-visibility` on messages now applies only to unwindowed replays: it
reserves a guessed 100px per offscreen message, so heights change as they
scroll into view and the scroll region grows under a dragged scrollbar
handle. That trade only pays when the whole transcript is in the DOM.
A windowed replay needs a way back into the rest of the task. The existing
request takes a window size, but a larger window resends everything the
client already holds and rebuilds its list; a separate request for the range
before the current start leaves what is on screen untouched.

Add request_history_before: the client names the seq it currently starts at,
the server replays only the messages before it, framed by
task_history_prepend_start/end. The client reduces that batch into a state of
its own and merges it in front of what it holds.

Staged message ids start well above the live counter, since ids come from a
per-state counter and would otherwise collide, handing preact two different
messages under one key.

The list is column-reverse, so scrollTop measures distance from the bottom,
which is exactly the quantity to hold across the batch: the reader stays
where they were and the older messages arrive above them. Scroll anchoring is
switched off for the duration, since it latches onto the load-more row and
drags the viewport to the new top.
@Antisophy
Antisophy force-pushed the feat/history-window branch from df8d812 to bafaa73 Compare August 21, 2026 22:51
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