Skip to content

fix: stop re-downloading and re-parsing the full transcript on every watcher tick - #1

Open
sigalor wants to merge 2 commits into
mainfrom
fix/messages-full-history-refetch
Open

fix: stop re-downloading and re-parsing the full transcript on every watcher tick#1
sigalor wants to merge 2 commits into
mainfrom
fix/messages-full-history-refetch

Conversation

@sigalor

@sigalor sigalor commented Aug 30, 2026

Copy link
Copy Markdown

Summary

  • refreshFromServer (client) requested the entire unbounded message history on every filesystem-watcher-triggered refresh; now requests a bounded tail window and merges only new messages by id.
  • getSessionMessages/parseAgentTools (server) re-read and re-parsed the whole JSONL transcript (and every subagent file) from scratch on every call, regardless of the caller's limit; now cache parsed lines per file (keyed by size+mtime) and read only newly-appended bytes.

Together these turned an actively-worked, long-running session into a continuous multi-MB full-transcript refetch loop (observed: ~30MB / ~45s per /messages call, repeating every couple of seconds) — this fork exists to carry this fix on top of the pinned v1.36.2 release used internally.

Test plan

  • npm run typecheck
  • npm run lint
  • npm run build
  • npx tsx --tsconfig server/tsconfig.json --test server/**/*.test.ts ... — 116/116 passing (115 pre-existing + 1 new test covering the cache's incremental-append, idempotent-repeat, and shrink-fallback behavior)

sigalor and others added 2 commits August 30, 2026 12:26
…watcher tick

An actively-working session's transcript grows continuously, and the
filesystem watcher fires a `session_upserted` event on every change while
it's the currently-viewed session. The client reacted to that by calling
refreshFromServer(), which fetched the entire, unbounded message history on
every single tick — and the server's getSessionMessages() re-read and
re-JSON.parsed the whole JSONL file (plus every referenced subagent
agent-*.jsonl file) from scratch each time, regardless of the caller's
requested limit. Together this meant network transfer and CPU cost per tick
scaled with total session size instead of with how much actually changed,
turning a long-running session into a continuous multi-MB refetch loop.

- refreshFromServer now requests a bounded tail window (TAIL_REFRESH_LIMIT)
  and merges only genuinely-new messages by id, falling back to a full fetch
  only if more messages appeared in one tick than the window covers.
- getSessionMessages/parseAgentTools now cache parsed JSONL lines per file
  (keyed by size+mtime) and read only newly-appended bytes on repeat calls,
  instead of re-reading and re-parsing from byte 0 every time.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
mergeTailMessages (introduced in 47828c8) appended not-yet-seen tail
messages to the END of the already-loaded array, assuming they were always
newer. That's false whenever the loaded page is smaller than the tail
window: refreshFromServer's tail request commonly reaches further back than
fetchFromServer's initial page (e.g. any session small enough that its
whole history fits inside TAIL_REFRESH_LIMIT), so the "new" messages it
surfaces are actually OLDER than what's already showing, not newer — they
were landing after the newest messages instead of before them.

Symptoms in production: messages rendering out of order and reshuffling on
every watcher tick ("jumping around"), assistant replies whose content the
model itself referenced not appearing where expected, and the chat only
looking right immediately after sending a message (the one path that still
does a full, correctly-ordered reload).

mergeTailMessages now merges by id (tail's copy wins on overlap, so content
that updates after the fact — e.g. a tool_result's subagentTools gaining
entries — isn't stuck stale) and re-sorts the result chronologically
instead of trusting concatenation order.

The pure ordering/merge logic (readMessageTime, compareMessagesChronologically,
mergeTailMessages) is split into sessionMessageOrdering.ts specifically so it
can be covered by a real test — this project has no frontend test runner, and
useSessionStore.ts itself can't be imported outside Vite (import.meta.env).
The new test reproduces the exact bug shape (a 2-message loaded page vs. a
5-message tail window) and is confirmed to fail against the old
concatenation-only logic before this fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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