run: replay session history on interactive resume (#26880)

This commit is contained in:
Simon Klee
2026-05-18 13:06:27 +02:00
committed by GitHub
parent 116a4e33ba
commit 5970c12d90
10 changed files with 1120 additions and 76 deletions
@@ -67,6 +67,22 @@ function idle(sessionID = "session-1") {
} satisfies SdkEvent
}
function retry(sessionID: string, attempt: number, message: string) {
return {
id: `evt-${sessionID}-retry-${attempt}`,
type: "session.status",
properties: {
sessionID,
status: {
type: "retry",
attempt,
message,
next: 1,
},
},
} satisfies SdkEvent
}
function assistant(id: string) {
return {
id: `evt-${id}`,
@@ -290,12 +306,12 @@ function toolUpdated(part: SessionToolPart): SdkEvent {
}
}
function textDelta(messageID: string, partID: string, delta: string): SdkEvent {
function textDelta(messageID: string, partID: string, delta: string, sessionID = "session-1"): SdkEvent {
return {
id: `evt-${partID}-delta`,
type: "message.part.delta",
properties: {
sessionID: "session-1",
sessionID,
messageID,
partID,
field: "text",
@@ -331,6 +347,7 @@ function footer(fn?: (commit: StreamCommit) => void) {
const commits: StreamCommit[] = []
const events: FooterEvent[] = []
let closed = false
let idleCalls = 0
const api: FooterApi = {
get isClosed() {
@@ -346,6 +363,7 @@ function footer(fn?: (commit: StreamCommit) => void) {
fn?.(next)
},
idle() {
idleCalls += 1
return Promise.resolve()
},
close() {
@@ -356,7 +374,7 @@ function footer(fn?: (commit: StreamCommit) => void) {
},
}
return { api, commits, events }
return { api, commits, events, get idleCalls() { return idleCalls } }
}
function sdk(
@@ -398,6 +416,355 @@ function sdk(
}
describe("run stream transport", () => {
test("does not replay persisted main-session history during bootstrap by default", async () => {
const src = eventFeed()
const ui = footer()
const transport = await createSessionTransport({
sdk: sdk({
stream: src.stream,
messages: async ({ sessionID }) =>
sessionID === "session-1"
? ok([
assistantMessage({
sessionID: "session-1",
id: "msg-1",
parts: [
{
...textPart("text-1", "msg-1", "Hello."),
time: {
start: 1,
end: 2,
},
},
],
}),
])
: ok([]),
}),
sessionID: "session-1",
thinking: true,
limits: () => ({}),
footer: ui.api,
})
try {
expect(ui.commits).toEqual([])
expect(ui.idleCalls).toBe(0)
} finally {
src.close()
await transport.close()
}
})
test("replays persisted main-session history during bootstrap when enabled", async () => {
const src = eventFeed()
const ui = footer()
const transport = await createSessionTransport({
sdk: sdk({
stream: src.stream,
messages: async ({ sessionID }) =>
sessionID === "session-1"
? ok([
assistantMessage({
sessionID: "session-1",
id: "msg-1",
parts: [
{
...textPart("text-1", "msg-1", "Hello."),
time: {
start: 1,
end: 2,
},
},
],
}),
])
: ok([]),
}),
sessionID: "session-1",
thinking: true,
replay: true,
limits: () => ({}),
footer: ui.api,
})
try {
await waitFor(() => ui.commits.find((item) => item.kind === "assistant" && item.text === "Hello."))
expect(ui.idleCalls).toBeGreaterThan(0)
} finally {
src.close()
await transport.close()
}
})
test("caps replayed bootstrap history to the configured number of messages", async () => {
const src = eventFeed()
const ui = footer()
const transport = await createSessionTransport({
sdk: sdk({
stream: src.stream,
messages: async ({ sessionID }) =>
ok(
sessionID === "session-1"
? [
assistantMessage({
sessionID: "session-1",
id: "msg-1",
parts: [
{
...textPart("text-1", "msg-1", "Hello."),
time: {
start: 1,
end: 2,
},
},
],
}),
assistantMessage({
sessionID: "session-1",
id: "msg-2",
parts: [
{
...textPart("text-2", "msg-2", "World."),
time: {
start: 3,
end: 4,
},
},
],
}),
]
: [],
),
}),
sessionID: "session-1",
thinking: true,
replay: true,
replayLimit: 1,
limits: () => ({}),
footer: ui.api,
})
try {
await waitFor(() => (ui.commits.length > 0 ? ui.commits : undefined))
expect(ui.commits.filter((item) => item.kind === "assistant")).toEqual([
expect.objectContaining({
text: "World.",
}),
])
} finally {
src.close()
await transport.close()
}
})
test("skips buffered pre-bootstrap deltas already covered by replay history", async () => {
const src = eventFeed()
const ui = footer()
const gate = defer<void>()
let transport: Awaited<ReturnType<typeof createSessionTransport>> | undefined
const task = createSessionTransport({
sdk: sdk({
stream: src.stream,
messages: async ({ sessionID }) => {
if (sessionID !== "session-1") {
return ok([])
}
await gate.promise
return ok([
assistantMessage({
sessionID: "session-1",
id: "msg-1",
parts: [textPart("text-1", "msg-1", "Hello")],
}),
])
},
}),
sessionID: "session-1",
thinking: true,
replay: true,
limits: () => ({}),
footer: ui.api,
})
try {
await Promise.resolve()
src.push(textDelta("msg-1", "text-1", "lo"))
gate.resolve()
transport = await task
await waitFor(() => (ui.commits.length > 0 ? ui.commits : undefined))
await Bun.sleep(20)
expect(ui.commits.filter((item) => item.kind === "assistant")).toEqual([
expect.objectContaining({
text: "Hello",
}),
])
} finally {
src.close()
await transport?.close()
}
})
test("applies buffered pre-bootstrap deltas not yet persisted", async () => {
const src = eventFeed()
const ui = footer()
const gate = defer<void>()
let transport: Awaited<ReturnType<typeof createSessionTransport>> | undefined
const task = createSessionTransport({
sdk: sdk({
stream: src.stream,
messages: async ({ sessionID }) => {
if (sessionID !== "session-1") {
return ok([])
}
await gate.promise
return ok([
assistantMessage({
sessionID: "session-1",
id: "msg-1",
parts: [textPart("text-1", "msg-1", "")],
}),
])
},
}),
sessionID: "session-1",
thinking: true,
replay: true,
limits: () => ({}),
footer: ui.api,
})
try {
await Promise.resolve()
src.push(textDelta("msg-1", "text-1", "Hello"))
gate.resolve()
transport = await task
await waitFor(() => (ui.commits.length > 0 ? ui.commits : undefined))
await Bun.sleep(20)
expect(ui.commits.filter((item) => item.kind === "assistant")).toEqual([
expect.objectContaining({
text: "Hello",
}),
])
} finally {
src.close()
await transport?.close()
}
})
test("preserves running footer state for resumed active sessions", async () => {
const src = eventFeed()
const ui = footer()
const transport = await createSessionTransport({
sdk: sdk({
stream: src.stream,
messages: async ({ sessionID }) =>
sessionID === "session-1"
? ok([
assistantMessage({
sessionID: "session-1",
id: "msg-1",
parts: [
runningTool({
sessionID: "session-1",
messageID: "msg-1",
id: "bash-1",
callID: "call-1",
tool: "bash",
body: {
command: "pwd",
},
}),
],
}),
])
: ok([]),
}),
sessionID: "session-1",
thinking: true,
replay: true,
limits: () => ({}),
footer: ui.api,
})
try {
const patch = await waitFor(() => {
const item = ui.events.findLast((event) => event.type === "stream.patch")
return item?.type === "stream.patch" ? item.patch : undefined
})
expect(patch).toEqual(
expect.objectContaining({
phase: "running",
status: "running bash",
}),
)
} finally {
src.close()
await transport.close()
}
})
test("drops completed historical subagent tabs during bootstrap", async () => {
const src = eventFeed()
const ui = footer()
const transport = await createSessionTransport({
sdk: sdk({
stream: src.stream,
messages: async ({ sessionID }) => {
if (sessionID !== "session-1") {
return ok([])
}
return ok([
assistantMessage({
sessionID: "session-1",
id: "msg-1",
parts: [
completedTool({
sessionID: "session-1",
messageID: "msg-1",
id: "task-1",
callID: "call-1",
tool: "task",
body: {
description: "Explore run folder",
subagent_type: "explore",
},
metadata: {
sessionId: "child-1",
},
}),
],
}),
])
},
children: async () => ok([child("child-1")]),
}),
sessionID: "session-1",
thinking: true,
limits: () => ({}),
footer: ui.api,
})
try {
const state = await waitFor(() => {
const item = ui.events.findLast((event) => event.type === "stream.subagent")
return item?.type === "stream.subagent" ? item.state : undefined
})
expect(state.tabs).toEqual([])
expect(state.details).toEqual({})
} finally {
src.close()
await transport.close()
}
})
test("bootstraps child tabs and resumed blocker input", async () => {
const src = eventFeed()
const ui = footer()
@@ -487,7 +854,7 @@ describe("run stream transport", () => {
expect.objectContaining({
sessionID: "child-1",
label: "Explore",
description: "Explore run folder",
description: "Pending permission",
status: "running",
}),
])
@@ -565,16 +932,16 @@ describe("run stream transport", () => {
messages: async ({ sessionID }) => {
if (sessionID === "session-1") {
return ok([
assistantMessage({
sessionID: "session-1",
id: "msg-1",
parts: [
completedTool({
sessionID: "session-1",
messageID: "msg-1",
id: "task-1",
callID: "call-1",
tool: "task",
assistantMessage({
sessionID: "session-1",
id: "msg-1",
parts: [
runningTool({
sessionID: "session-1",
messageID: "msg-1",
id: "task-1",
callID: "call-1",
tool: "task",
body: {
description: "Explore run.ts",
subagent_type: "explore",
@@ -582,10 +949,10 @@ describe("run stream transport", () => {
metadata: {
sessionId: "child-1",
},
}),
],
}),
])
}),
],
}),
])
}
return sessionID === "child-1"
@@ -711,6 +1078,109 @@ describe("run stream transport", () => {
}
})
test("replays child events buffered during bootstrap once the tab is known", async () => {
const global = globalFeed()
const ui = footer()
const gate = defer<void>()
let transport: Awaited<ReturnType<typeof createSessionTransport>> | undefined
const task = createSessionTransport({
sdk: sdk({
globalStream: global.stream,
messages: async ({ sessionID }) => {
if (sessionID !== "session-1") {
return ok([])
}
await gate.promise
return ok([])
},
children: async () => ok([]),
}),
sessionID: "session-1",
thinking: true,
limits: () => ({}),
footer: ui.api,
})
try {
await Promise.resolve()
global.push(globalEvent(retry("child-1", 1, "retry child")))
global.push(
globalEvent({
id: "evt-child-message",
type: "message.updated",
properties: {
sessionID: "child-1",
info: assistantMessage({
sessionID: "child-1",
id: "msg-child-1",
parts: [],
}).info,
},
}),
)
global.push(globalEvent(textUpdated(textPart("txt-child-1", "msg-child-1", "", "child-1"))))
global.push(globalEvent(textDelta("msg-child-1", "txt-child-1", "Hello", "child-1")))
global.push(
globalEvent(
toolUpdated(
runningTool({
sessionID: "session-1",
messageID: "msg-1",
id: "task-1",
callID: "call-1",
tool: "task",
body: {
description: "Explore run.ts",
subagent_type: "explore",
},
metadata: {
sessionId: "child-1",
},
}),
),
),
)
gate.resolve()
transport = await task
await waitFor(() => {
const item = ui.events.findLast((event) => event.type === "stream.subagent")
return item?.type === "stream.subagent" && item.state.tabs.some((tab) => tab.sessionID === "child-1")
? item
: undefined
})
transport.selectSubagent("child-1")
const detail = await waitFor(() => {
const item = ui.events.findLast((event) => event.type === "stream.subagent")
const next = item?.type === "stream.subagent" ? item.state.details["child-1"] : undefined
return next?.commits.some((commit) => commit.kind === "error" && commit.text === "retry child") &&
next.commits.some((commit) => commit.kind === "assistant" && commit.text === "Hello")
? next
: undefined
})
expect(detail).toEqual({
sessionID: "child-1",
commits: expect.arrayContaining([
expect.objectContaining({
kind: "error",
text: "retry child",
}),
expect.objectContaining({
kind: "assistant",
text: "Hello",
}),
]),
})
} finally {
global.close()
await transport?.close()
}
})
test("streams selected subagent output from global events while it is running", async () => {
const global = globalFeed()
const ui = footer()