feat(sessions): stream in-flight partial when joining a mid-turn session

Seed the in-flight partial assistant message (text, thinking, and
in-progress tool calls) when opening or reconnecting to a session that is
mid-stream, then continue streaming live deltas on top of it. Replaces the
blocking "Catching up..." placeholder and the end-of-turn transcript reload.

Server stamps every per-session UI event with a monotonic seq at the
SessionEventHub publish choke point and exposes
GET /sessions/:sessionId/stream-snapshot returning { seq, partial }. The
client fetches the snapshot on join, seeds the normalized partial into the
in-memory transcript (never the history cache), and applies buffered/live
events using the seq watermark for exactly-once delivery.

The snapshot is a progressive enhancement: a 404 from an older remote
pi-web or a not-yet-restarted session daemon falls back to an empty seed
(seq 0, drops nothing), so sessions still open and stream normally. The
stream-snapshot route is registered in the federation allowlist for
remote-machine proxying.
This commit is contained in:
Federico Jaramillo Martinez
2026-07-17 14:49:56 +02:00
parent ae9eaf3082
commit 2b17145291
31 changed files with 681 additions and 108 deletions
+24 -2
View File
@@ -689,12 +689,34 @@ export interface MessagePage {
total: number;
}
/**
* Join-time snapshot of a session's in-flight assistant stream. `seq` is the
* `SessionEventHub` watermark captured together with `partial` in a single tick,
* so a joining client can seed `partial` and then apply only buffered live events
* with `seq > snapshot.seq` (exactly-once). `partial` is a browser-projected
* in-flight `AssistantMessage` (thinking signatures stripped), or `null` when the
* session is not mid assistant-message stream.
*/
export interface SessionStreamSnapshot {
seq: number;
/** Browser-projected in-flight `AssistantMessage`, or `null` when idle. */
partial: unknown;
}
export type CommandResult =
| { type: "done"; message?: string; session?: SessionInfo; promptDraft?: string }
| { type: "select"; requestId: string; title: string; options: CommandOption[] }
| { type: "unsupported"; message: string };
export type SessionUiEvent =
/**
* Transport-level per-session sequence stamp. `SessionEventHub.publish` assigns a
* monotonic `seq` to every per-session event as it is serialized to the socket.
* Clients use it as a watermark against the join-time stream snapshot so buffered
* live events are applied exactly once. Existing consumers may ignore it.
*/
export type SessionUiEvent = SessionUiEventBody & { seq?: number };
type SessionUiEventBody =
| { type: "message.append"; message: unknown }
| { type: "assistant.delta"; text: string }
| { type: "assistant.thinking.delta"; text: string }
@@ -715,5 +737,5 @@ export type SessionUiEvent =
| { type: "session.created"; session: SessionInfo }
| { type: "pi.event"; eventType: string };
export type GlobalSessionEvent = Extract<SessionUiEvent, { type: "status.update" | "activity.update" | "session.name" | "session.created" }>;
export type GlobalSessionEvent = Extract<SessionUiEventBody, { type: "status.update" | "activity.update" | "session.name" | "session.created" }>;
export type RealtimeEvent = GlobalSessionEvent | TerminalUiEvent | WorkspaceActivityUiEvent;