fix(core): preserve active session continuation when moving

This commit is contained in:
Kit Langton 2026-08-31 12:14:45 -04:00 committed by GitHub
parent 1ced747051
commit 9517ff1054
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 66 additions and 1 deletions

View file

@ -468,7 +468,8 @@ const layer = Layer.effect(
Effect.gen(function* () {
const latest = yield* result.get(input.sessionID)
const source = yield* fs.stat(latest.location.directory).pipe(Effect.orElseSucceed(() => undefined))
if (!source || source.type !== "Directory") {
// Active runners must hand off at a step boundary to retain their continuation.
if ((!source || source.type !== "Directory") && !(yield* execution.isActive(input.sessionID))) {
const cancellations = (yield* SessionInbox.moveIDs(db, input.sessionID)).map(
(item) => [SessionEvent.InboxCancelled, { sessionID: input.sessionID, inboxID: item.id }] as const,
)

View file

@ -15,6 +15,7 @@ import { Session } from "@opencode-ai/core/session"
import { SessionEvent } from "@opencode-ai/core/session/event"
import { SessionExecution } from "@opencode-ai/core/session/execution"
import { SessionProjector } from "@opencode-ai/core/session/projector"
import { SessionRunner } from "@opencode-ai/core/session/runner/index"
import { SessionStore } from "@opencode-ai/core/session/store"
import { LayerNode } from "@opencode-ai/util/effect/layer-node"
import { tmpdirScoped } from "./fixture/tmpdir"
@ -30,6 +31,34 @@ const it = testEffect(
],
),
)
const itWithActiveExecution = testEffect(
AppNodeBuilder.build(
LayerNode.group([
Database.node,
Bus.node,
SessionProjector.node,
SessionStore.node,
SessionExecution.node,
Session.node,
]),
[
[Project.node, globalProjectNode],
[
LocationServiceMap.node,
Layer.effect(
LocationServiceMap.Service,
LayerMap.make(
(ref: Location.Ref) =>
Layer.merge(
LayerNode.compile(Location.boundNode(ref), [[Project.node, globalProjectNode]]),
Layer.succeed(SessionRunner.Service, { drain: () => Effect.never }),
) as unknown as Layer.Layer<LocationServices>,
),
),
],
],
),
)
const unavailableLocations = Layer.effect(
LocationServiceMap.Service,
LayerMap.make(
@ -104,6 +133,41 @@ describe("Session.move", () => {
),
)
itWithActiveExecution.live("defers an active move when the source directory no longer exists", () =>
tmpdirScoped().pipe(
Effect.flatMap((tmp) =>
Effect.gen(function* () {
const session = yield* Session.Service
const execution = yield* SessionExecution.Service
const source = AbsolutePath.make(path.join(tmp.path, "source"))
const destination = AbsolutePath.make(tmp.path)
yield* Effect.promise(() => mkdir(source))
const created = yield* session.create({ location: Location.Ref.make({ directory: source }) })
// Hold real execution open so the move cannot be consumed before admission is checked.
yield* execution.wake(created.id)
expect(yield* execution.isActive(created.id)).toBe(true)
yield* Effect.promise(() => rm(source, { recursive: true }))
yield* session.move({ sessionID: created.id, directory: destination })
expect((yield* session.get(created.id)).location.directory).toBe(source)
expect(yield* session.inbox(created.id)).toMatchObject([
{
type: "move",
delivery: "steer",
payload: { location: { directory: destination } },
},
])
expect(yield* execution.isActive(created.id)).toBe(true)
yield* execution.interrupt(created.id)
yield* execution.awaitIdle(created.id)
}),
),
),
)
it.effect("keeps a moved session out of its former directory's new identity", () =>
tmpdirScoped().pipe(
Effect.flatMap((tmp) =>