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
@@ -6,6 +6,7 @@ import type { PiAgentSession, PiSessionManager, PiSessionRuntime, PiSessionServi
export class CapturingSessionEventHub extends SessionEventHub {
readonly sessionEvents: { sessionId: string; event: SessionUiEvent }[] = [];
readonly globalEvents: GlobalSessionEvent[] = [];
private readonly seqBySessionOverride = new Map<string, number>();
override publish(sessionId: string, event: SessionUiEvent): void {
this.sessionEvents.push({ sessionId, event });
@@ -14,6 +15,15 @@ export class CapturingSessionEventHub extends SessionEventHub {
override publishGlobal(event: GlobalSessionEvent): void {
this.globalEvents.push(event);
}
/** Test seam: set the per-session watermark returned by {@link currentSeq}. */
setSeq(sessionId: string, value: number): void {
this.seqBySessionOverride.set(sessionId, value);
}
override currentSeq(sessionId: string): number {
return this.seqBySessionOverride.get(sessionId) ?? 0;
}
}
export type SessionGateway = NonNullable<PiSessionServiceDependencies["sessionManager"]>;
@@ -68,6 +78,7 @@ export function fakeRuntime(sessionId = "session-1", patch: Partial<TestSession>
sessionId,
sessionFile: `/tmp/${sessionId}.jsonl`,
messages: [],
state: {},
sessionName: undefined,
model: undefined,
thinkingLevel: "off",