Archived
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:
@@ -18,7 +18,7 @@ describe("SessionEventHub", () => {
|
||||
|
||||
hub.publish("s1", { type: "assistant.delta", text: "hello" });
|
||||
|
||||
expect(sessionSocket.send).toHaveBeenCalledWith(JSON.stringify({ type: "assistant.delta", text: "hello" }));
|
||||
expect(sessionSocket.send).toHaveBeenCalledWith(JSON.stringify({ type: "assistant.delta", text: "hello", seq: 1 }));
|
||||
expect(otherSocket.send).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -34,6 +34,7 @@ describe("SessionEventHub", () => {
|
||||
expect(socket.send).toHaveBeenCalledWith(JSON.stringify({
|
||||
type: "message.end",
|
||||
message: { role: "assistant", content: [{ type: "thinking", thinking: "private chain", redacted: true }, { type: "text", text: "visible answer" }] },
|
||||
seq: 1,
|
||||
}));
|
||||
expect(thinkingBlock.thinkingSignature).toBe("opaque-provider-payload");
|
||||
});
|
||||
@@ -76,4 +77,65 @@ describe("SessionEventHub", () => {
|
||||
expect(globalSocket.send).toHaveBeenCalledWith(JSON.stringify({ type: "status.update", status }));
|
||||
expect(sessionSocket.send).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("stamps a monotonically increasing per-session seq on published events", () => {
|
||||
const hub = new SessionEventHub();
|
||||
const socket = new FakeSocket();
|
||||
hub.add("s1", socket);
|
||||
|
||||
hub.publish("s1", { type: "assistant.delta", text: "a" });
|
||||
hub.publish("s1", { type: "assistant.delta", text: "b" });
|
||||
hub.publish("s1", { type: "assistant.delta", text: "c" });
|
||||
|
||||
expect(socket.send).toHaveBeenNthCalledWith(1, JSON.stringify({ type: "assistant.delta", text: "a", seq: 1 }));
|
||||
expect(socket.send).toHaveBeenNthCalledWith(2, JSON.stringify({ type: "assistant.delta", text: "b", seq: 2 }));
|
||||
expect(socket.send).toHaveBeenNthCalledWith(3, JSON.stringify({ type: "assistant.delta", text: "c", seq: 3 }));
|
||||
});
|
||||
|
||||
it("advances seq even when no sockets are attached so the watermark stays accurate", () => {
|
||||
const hub = new SessionEventHub();
|
||||
|
||||
hub.publish("s1", { type: "assistant.delta", text: "a" });
|
||||
hub.publish("s1", { type: "assistant.delta", text: "b" });
|
||||
|
||||
expect(hub.currentSeq("s1")).toBe(2);
|
||||
|
||||
const socket = new FakeSocket();
|
||||
hub.add("s1", socket);
|
||||
hub.publish("s1", { type: "assistant.delta", text: "c" });
|
||||
|
||||
expect(socket.send).toHaveBeenCalledWith(JSON.stringify({ type: "assistant.delta", text: "c", seq: 3 }));
|
||||
});
|
||||
|
||||
it("tracks seq independently per session", () => {
|
||||
const hub = new SessionEventHub();
|
||||
const s1 = new FakeSocket();
|
||||
const s2 = new FakeSocket();
|
||||
hub.add("s1", s1);
|
||||
hub.add("s2", s2);
|
||||
|
||||
hub.publish("s1", { type: "assistant.delta", text: "a" });
|
||||
hub.publish("s1", { type: "assistant.delta", text: "b" });
|
||||
hub.publish("s2", { type: "assistant.delta", text: "x" });
|
||||
|
||||
expect(hub.currentSeq("s1")).toBe(2);
|
||||
expect(hub.currentSeq("s2")).toBe(1);
|
||||
expect(s1.send).toHaveBeenLastCalledWith(JSON.stringify({ type: "assistant.delta", text: "b", seq: 2 }));
|
||||
expect(s2.send).toHaveBeenCalledWith(JSON.stringify({ type: "assistant.delta", text: "x", seq: 1 }));
|
||||
});
|
||||
|
||||
it("reports zero seq for a session that has never published", () => {
|
||||
const hub = new SessionEventHub();
|
||||
expect(hub.currentSeq("never")).toBe(0);
|
||||
});
|
||||
|
||||
it("does not stamp seq on global events", () => {
|
||||
const hub = new SessionEventHub();
|
||||
const globalSocket = new FakeSocket();
|
||||
hub.addGlobal(globalSocket);
|
||||
|
||||
hub.publishGlobal({ type: "session.name", sessionId: "s1", name: "Renamed" });
|
||||
|
||||
expect(globalSocket.send).toHaveBeenCalledWith(JSON.stringify({ type: "session.name", sessionId: "s1", name: "Renamed" }));
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user