fix(core): omit active assistants from forks (#43994)

This commit is contained in:
Kit Langton
2026-08-21 17:52:36 -04:00
committed by GitHub
parent 656c4cc7fa
commit 59f0e3a378
2 changed files with 57 additions and 0 deletions
+2
View File
@@ -181,6 +181,8 @@ const projectFork = Effect.fn("SessionProjector.projectFork")(function* (
eq(SessionMessageTable.session_id, event.data.parentID),
gt(SessionMessageTable.seq, cursor),
lt(SessionMessageTable.seq, copiedSeq + 1),
// Terminal events for active projections stay on the parent, so forks copy only settled history.
sql`${SessionMessageTable.type} != 'assistant' or json_extract(${SessionMessageTable.data}, '$.time.completed') is not null`,
sql`${SessionMessageTable.type} != 'compaction' or json_extract(${SessionMessageTable.data}, '$.status') != 'running'`,
),
)
+55
View File
@@ -401,6 +401,56 @@ describe("Session.create", () => {
}),
)
it.effect("does not copy a running assistant into a fork", () =>
Effect.gen(function* () {
const session = yield* Session.Service
const bus = yield* Bus.Service
const { db } = yield* Database.Service
const parent = yield* session.create({ location })
yield* session.prompt({ sessionID: parent.id, text: "Run both tools", resume: false })
yield* SessionInbox.promote(db, bus, parent.id, "steer")
const assistantMessageID = SessionMessage.ID.create()
const model = Model.Ref.make({ id: Model.ID.make("model"), providerID: Provider.ID.make("provider") })
yield* bus.publish(SessionEvent.Step.Started, {
sessionID: parent.id,
assistantMessageID,
agent: Agent.ID.make("build"),
model,
})
yield* bus.publish(SessionEvent.Tool.Input.Started, {
sessionID: parent.id,
assistantMessageID,
id: "call_running",
name: "shell",
})
yield* bus.publish(SessionEvent.Tool.Input.Ended, {
sessionID: parent.id,
assistantMessageID,
id: "call_running",
text: '{"command":"sleep 10"}',
})
yield* bus.publish(SessionEvent.Tool.Called, {
sessionID: parent.id,
assistantMessageID,
id: "call_running",
input: { command: "sleep 10" },
executed: true,
})
const forked = yield* session.fork({ sessionID: parent.id, boundary: { type: "through" } })
expect(yield* session.context(parent.id)).toMatchObject([
{ type: "user", text: "Run both tools" },
{
type: "assistant",
content: [{ type: "tool", id: "call_running", state: { status: "running" } }],
},
])
expect(yield* session.context(forked.id)).toMatchObject([{ type: "user", text: "Run both tools" }])
}),
)
it.effect("rejects forking an empty session", () =>
Effect.gen(function* () {
const session = yield* Session.Service
@@ -470,6 +520,11 @@ describe("Session.create", () => {
expect(forked).toMatchObject({ cost: 0, tokens: { input: 0, output: 0, reasoning: 0 } })
expect(yield* session.context(beforeFirst.id)).toEqual([])
expect(beforeFirst).toMatchObject({ cost: 0, tokens: { input: 0, output: 0, reasoning: 0 } })
expect(yield* session.context(complete.id)).toMatchObject([
{ type: "user", text: "First" },
{ type: "user", text: "Second" },
{ type: "assistant", finish: "stop" },
])
expect(complete).toMatchObject({
cost: 0,
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },