From 21d055be19cd858fada71ddf96a9f16ccd4b10d0 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 8 May 2026 20:25:53 -0400 Subject: [PATCH] fix(workspace): claim detached sessions to source project (#26413) --- .../opencode/src/control-plane/workspace.ts | 3 +- .../test/control-plane/workspace.test.ts | 35 +++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/control-plane/workspace.ts b/packages/opencode/src/control-plane/workspace.ts index 03640576d6..b30536ec02 100644 --- a/packages/opencode/src/control-plane/workspace.ts +++ b/packages/opencode/src/control-plane/workspace.ts @@ -5,7 +5,6 @@ import { asc } from "drizzle-orm" import { eq } from "drizzle-orm" import { inArray } from "drizzle-orm" import { Project } from "@/project/project" -import { Instance } from "@/project/instance" import { BusEvent } from "@/bus/bus-event" import { GlobalBus } from "@/bus/global" import { Auth } from "@/auth" @@ -646,7 +645,7 @@ export const layer = Layer.effect( // "claim" this session so any future events coming from // the old workspace are ignored - SyncEvent.claim(input.sessionID, input.workspaceID ?? Instance.project.id) + SyncEvent.claim(input.sessionID, input.workspaceID ?? previous.projectID) } } diff --git a/packages/opencode/test/control-plane/workspace.test.ts b/packages/opencode/test/control-plane/workspace.test.ts index e3de9cae71..8333d9573f 100644 --- a/packages/opencode/test/control-plane/workspace.test.ts +++ b/packages/opencode/test/control-plane/workspace.test.ts @@ -848,6 +848,41 @@ describe("workspace CRUD", () => { }) }) + test("sessionWarp detaches to the source project when invoked from a workspace instance", async () => { + await withInstance(async () => { + const projectID = Instance.project.id + await using workspaceTmp = await tmpdir({ git: true }) + const previousType = unique("warp-detach-workspace-instance") + const previous = workspaceInfo(projectID, previousType) + insertWorkspace(previous) + registerAdapter(projectID, previousType, localAdapter(workspaceTmp.path, { createDir: false }).adapter) + const session = await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({}))) + attachSessionToWorkspace(session.id, previous.id) + + const workspaceProjectID = await WithInstance.provide({ + directory: workspaceTmp.path, + fn: async () => { + const id = Instance.project.id + expect(id).not.toBe(projectID) + await warpWorkspaceSession({ workspaceID: null, sessionID: session.id }) + return id + }, + }) + + expect( + Database.use((db) => + db + .select({ workspaceID: SessionTable.workspace_id }) + .from(SessionTable) + .where(eq(SessionTable.id, session.id)) + .get(), + )?.workspaceID, + ).toBeNull() + expect(sessionSequenceOwner(session.id)).toBe(projectID) + expect(sessionSequenceOwner(session.id)).not.toBe(workspaceProjectID) + }) + }) + it.live("sessionWarp syncs previous remote history, replays it, steals, and claims the sequence", () => { const calls: FetchCall[] = [] let historySessionID: SessionID | undefined