A windowed conversation view

Proposal for Durable: the view holds a window of the newest entries with a stable start; older entries are fetched in pages; a reconnect resumes after the last entry it saw.

type ConversationView = {
  conversation: ConversationRecord;
  head?: EntryRecord;               // the current compaction summary, if any
  entries: readonly EntryRecord[];  // the window: active entries with id >= from
  earlier: boolean;                 // older active entries exist before it
  docs: { "pi.live", "pi.agent", "pi.inbox", "pi.provider", "pi.usage" };
};
conversation.watch(context, { from?: EntryId, tail?: number });          // default: the last `tail`
conversation.entries({ before, order: "descending" }, limit, cursor, ctx); // exists today

Watch it work

Shown with tail = 4 (the proposal: about 50). The window grows by appends until it holds twice tail; then the oldest half leaves in one splice.

store: every active entry
client: view window (blue), kept or fetched (purple)
window 0 entries ops sent 0 last snapshot -

What each step costs

stepwire
a new entryone append splice: p entries <len> 0 [entry]
window reaches 2 × tailone splice removing the oldest half: p entries 0 <tail> [], once every tail entries
streaming into the newest messageappends on docs["pi.live"].generation.message, unchanged
hold, re-hold, overflowa snapshot of at most 2 × tail entries plus the live documents, never the whole transcript
scroll upa page from entries(), outside the view: the client keeps it, the view never grows
reconnect with from: lastSeen + 1only the entries it missed plus the documents (issue #11327)

Why a stable start instead of “always the last 50”: a sliding window would remove one entry at the front for every entry appended at the back, two ops per entry, and every index in the window would shift. With a stable start, entries only append, and the front moves in one splice now and then.