Files
paseo/docs/timeline-sync.md
Mohamed Boudra b613bea9f6 Default client RPC waits to 60 seconds (#1789)
* fix(client): wait longer for session responses

* fix(client): default RPC waits to sixty seconds

* fix(app): preserve detached stream scroll on delayed history

Code drift: longer client RPC waits let delayed timeline responses arrive after a user scroll-away, so web stream anchoring must not reattach on transient scroll-top resets.

Restore the 15s connect deadline and leave app initialization slack above the default 60s session RPC wait.

* fix(client): keep helper waits within caller deadlines

Review fix: the 60s default session RPC wait leaked into wait previews and waitForAgentUpsert helper fetches. Bound those helper RPCs to the caller deadline or a short best-effort preview timeout, and allow small scroll ranges to reattach at bottom.

* fix(client): respect caller timeout budgets

* fix(cli): keep diagnostic probes responsive

* Refactor daemon client request options

* Preserve daemon client legacy overloads
2026-06-29 17:23:18 +02:00

2.5 KiB

Timeline sync

Agent chat delivery has two paths:

  1. Live streamagent_stream WebSocket messages for immediacy. These may be delta-shaped lifecycle updates.
  2. Authoritative historyfetch_agent_timeline_request for correctness. This always returns full projected timeline items, never lifecycle deltas.

The invariant is:

If the daemon has committed timeline rows for an agent, any connected client that opens or resumes that agent eventually displays every row through the daemon's current tail.

Presence is not delivery

Client heartbeat reports presence:

  • device type
  • app visibility
  • focused agent
  • last activity time

Heartbeat is used for notification routing. It must not be used as a correctness gate for agent_stream delivery. A stale mobile focus heartbeat may affect whether the user gets notified; it must not make timeline rows disappear from the live stream.

Catch-up is paged but complete

Large unbounded timeline responses can exceed relay frame limits, so catch-up uses bounded pages. Bounded does not mean partial.

Page limits are projected-item targets. A tool call lifecycle is one projected item even if it spans many source sequence numbers, and assistant/reasoning chunks are merged before counting. The response carries seqStart, seqEnd, sourceSeqRanges, and collapsed so clients can advance sequence cursors without rendering delta rows.

When the app fetches direction: "after" and the daemon responds with hasNewer: true, the app must immediately fetch the next page from endCursor. The catch-up is complete only when hasNewer: false.

Initialization timeouts guard lack of catch-up progress, not the full multi-page sync. A successful page that queues the next after page refreshes the watchdog.

The first load of an agent without a local cursor is different: it fetches a bounded latest tail page. Older history remains user-driven by scrolling upward.

Resume behavior

When a client resumes with a known cursor, it catches up after that cursor to completion. It does not replace the view with a latest tail page, because tail pagination can skip the middle of a long background run.

When a client resumes without a cursor, it fetches the latest tail page.

Relevant code

  • Server live stream forwarding: packages/server/src/server/session.ts
  • App sync planning: packages/app/src/timeline/timeline-sync-plan.ts
  • App stream/timeline reducer: packages/app/src/timeline/session-stream-reducers.ts
  • Session wiring: packages/app/src/contexts/session-context.tsx