fix(core): omit running shells from forks (#44001)

This commit is contained in:
Kit Langton
2026-08-21 19:27:13 -04:00
committed by GitHub
parent 3694149135
commit 2937f0e635
2 changed files with 45 additions and 0 deletions
+1
View File
@@ -183,6 +183,7 @@ const projectFork = Effect.fn("SessionProjector.projectFork")(function* (
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} != 'shell' or json_extract(${SessionMessageTable.data}, '$.status') != 'running'`,
sql`${SessionMessageTable.type} != 'compaction' or json_extract(${SessionMessageTable.data}, '$.status') != 'running'`,
),
)
+44
View File
@@ -4,6 +4,7 @@ import fs from "fs/promises"
import path from "path"
import { DateTime, Effect, Layer, Stream } from "effect"
import { Money } from "@opencode-ai/schema/money"
import { Shell } from "@opencode-ai/schema/shell"
import { Agent } from "@opencode-ai/core/agent"
import { asc, eq } from "drizzle-orm"
import { Database } from "@opencode-ai/core/database/database"
@@ -451,6 +452,49 @@ describe("Session.create", () => {
}),
)
it.effect("copies only settled shell messages into forks", () =>
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 a shell", resume: false })
yield* SessionInbox.promote(db, bus, parent.id, "steer")
const shell = Shell.Info.make({
id: Shell.ID.make("sh_fork_running"),
status: "running",
command: "sleep 10",
cwd: location.directory,
shell: "/bin/sh",
file: "/tmp/sh_fork_running.out",
metadata: {},
time: { started: 0 },
})
yield* bus.publish(SessionEvent.Shell.Started, { sessionID: parent.id, shell })
const running = yield* session.fork({ sessionID: parent.id, boundary: { type: "through" } })
expect(yield* session.context(parent.id)).toMatchObject([
{ type: "user", text: "Run a shell" },
{ type: "shell", command: "sleep 10", status: "running" },
])
expect(yield* session.context(running.id)).toMatchObject([{ type: "user", text: "Run a shell" }])
yield* bus.publish(SessionEvent.Shell.Ended, {
sessionID: parent.id,
shell: { ...shell, status: "exited", exit: 0, time: { started: 0, completed: 1 } },
output: { output: "complete", cursor: 8, size: 8, truncated: false },
})
const completed = yield* session.fork({ sessionID: parent.id, boundary: { type: "through" } })
expect(yield* session.context(running.id)).toMatchObject([{ type: "user", text: "Run a shell" }])
expect(yield* session.context(completed.id)).toMatchObject([
{ type: "user", text: "Run a shell" },
{ type: "shell", command: "sleep 10", status: "exited", output: { output: "complete" } },
])
}),
)
it.effect("rejects forking an empty session", () =>
Effect.gen(function* () {
const session = yield* Session.Service