From a0ca1de51e21aad39acdeec2271571db644073d1 Mon Sep 17 00:00:00 2001 From: Federico Jaramillo Martinez Date: Tue, 12 May 2026 15:55:19 +0200 Subject: [PATCH] Disable forking active sessions --- .../sessions/sessionCommandService.test.ts | 30 +++++++++++++++++++ src/server/sessions/sessionCommandService.ts | 11 +++++++ 2 files changed, 41 insertions(+) diff --git a/src/server/sessions/sessionCommandService.test.ts b/src/server/sessions/sessionCommandService.test.ts index 53a19ef..1bff313 100644 --- a/src/server/sessions/sessionCommandService.test.ts +++ b/src/server/sessions/sessionCommandService.test.ts @@ -114,4 +114,34 @@ describe("SessionCommandService", () => { expect(active.runtime.fork).toHaveBeenCalledWith("newest"); await expect(service.respond("s1", result.requestId, "newest")).resolves.toEqual({ type: "unsupported", message: "Command request expired" }); }); + + it("rejects fork and clone while the session has active work", async () => { + const active = activeSession({ isStreaming: true }); + const service = new SessionCommandService(() => getActive(active), vi.fn(), { publish: vi.fn() } as never); + + await expect(service.run("s1", "/fork")).resolves.toEqual({ + type: "unsupported", + message: "Cannot fork while the session is active. Stop current activity before forking.", + }); + await expect(service.run("s1", "/clone")).resolves.toEqual({ + type: "unsupported", + message: "Cannot clone while the session is active. Stop current activity before cloning.", + }); + expect(active.runtime.fork).not.toHaveBeenCalled(); + }); + + it("rejects fork responses if the session becomes active after choosing fork", async () => { + const active = activeSession(); + const service = new SessionCommandService(() => getActive(active), vi.fn(), { publish: vi.fn() } as never); + + const result = await service.run("s1", "/fork"); + if (result.type !== "select") throw new Error("Expected select result"); + (active.runtime.session as Record)["isStreaming"] = true; + + await expect(service.respond("s1", result.requestId, "m1")).resolves.toEqual({ + type: "unsupported", + message: "Cannot fork while the session is active. Stop current activity before forking.", + }); + expect(active.runtime.fork).not.toHaveBeenCalled(); + }); }); diff --git a/src/server/sessions/sessionCommandService.ts b/src/server/sessions/sessionCommandService.ts index 5ca1d9c..73e85a7 100644 --- a/src/server/sessions/sessionCommandService.ts +++ b/src/server/sessions/sessionCommandService.ts @@ -52,6 +52,7 @@ export class SessionCommandService { this.pendingSelects.delete(requestId); const active = await this.getActive(sessionId); + if (sessionHasActiveWork(active.runtime.session)) return forkActiveUnsupported("fork"); const result = await active.runtime.fork(value); if (result.cancelled) return { type: "done", message: "Fork cancelled" }; return { type: "done", message: "Session forked", session: clientSessionFromRuntime(active.runtime) }; @@ -84,6 +85,7 @@ export class SessionCommandService { } private async clone(active: ActiveSession): Promise { + if (sessionHasActiveWork(active.runtime.session)) return forkActiveUnsupported("clone"); const leafId = active.runtime.session.sessionManager.getLeafId(); if (leafId === null || leafId === "") return { type: "unsupported", message: "Cannot clone: no current session entry" }; const result = await active.runtime.fork(leafId, { position: "at" }); @@ -92,6 +94,7 @@ export class SessionCommandService { } private fork(active: ActiveSession): ClientCommandResult { + if (sessionHasActiveWork(active.runtime.session)) return forkActiveUnsupported("fork"); const messages = active.runtime.session.getUserMessagesForForking(); if (!messages.length) return { type: "unsupported", message: "No user messages to fork from" }; const requestId = crypto.randomUUID(); @@ -127,6 +130,14 @@ function clientSessionFromRuntime(runtime: AgentSessionRuntime): ClientSession { }; } +function sessionHasActiveWork(session: AgentSession): boolean { + return session.isStreaming || session.isBashRunning || session.isCompacting || session.pendingMessageCount > 0; +} + +function forkActiveUnsupported(command: "fork" | "clone"): ClientCommandResult { + return { type: "unsupported", message: `Cannot ${command} while the session is active. Stop current activity before ${command === "fork" ? "forking" : "cloning"}.` }; +} + function formatSessionStats(session: AgentSession): string { const stats = session.getSessionStats(); return [