Disable forking active sessions

This commit is contained in:
Federico Jaramillo Martinez
2026-05-12 15:55:19 +02:00
parent 0481b5287c
commit a0ca1de51e
2 changed files with 41 additions and 0 deletions
@@ -114,4 +114,34 @@ describe("SessionCommandService", () => {
expect(active.runtime.fork).toHaveBeenCalledWith("newest"); expect(active.runtime.fork).toHaveBeenCalledWith("newest");
await expect(service.respond("s1", result.requestId, "newest")).resolves.toEqual({ type: "unsupported", message: "Command request expired" }); 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<string, unknown>)["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();
});
}); });
@@ -52,6 +52,7 @@ export class SessionCommandService {
this.pendingSelects.delete(requestId); this.pendingSelects.delete(requestId);
const active = await this.getActive(sessionId); const active = await this.getActive(sessionId);
if (sessionHasActiveWork(active.runtime.session)) return forkActiveUnsupported("fork");
const result = await active.runtime.fork(value); const result = await active.runtime.fork(value);
if (result.cancelled) return { type: "done", message: "Fork cancelled" }; if (result.cancelled) return { type: "done", message: "Fork cancelled" };
return { type: "done", message: "Session forked", session: clientSessionFromRuntime(active.runtime) }; return { type: "done", message: "Session forked", session: clientSessionFromRuntime(active.runtime) };
@@ -84,6 +85,7 @@ export class SessionCommandService {
} }
private async clone(active: ActiveSession): Promise<ClientCommandResult> { private async clone(active: ActiveSession): Promise<ClientCommandResult> {
if (sessionHasActiveWork(active.runtime.session)) return forkActiveUnsupported("clone");
const leafId = active.runtime.session.sessionManager.getLeafId(); const leafId = active.runtime.session.sessionManager.getLeafId();
if (leafId === null || leafId === "") return { type: "unsupported", message: "Cannot clone: no current session entry" }; if (leafId === null || leafId === "") return { type: "unsupported", message: "Cannot clone: no current session entry" };
const result = await active.runtime.fork(leafId, { position: "at" }); const result = await active.runtime.fork(leafId, { position: "at" });
@@ -92,6 +94,7 @@ export class SessionCommandService {
} }
private fork(active: ActiveSession): ClientCommandResult { private fork(active: ActiveSession): ClientCommandResult {
if (sessionHasActiveWork(active.runtime.session)) return forkActiveUnsupported("fork");
const messages = active.runtime.session.getUserMessagesForForking(); const messages = active.runtime.session.getUserMessagesForForking();
if (!messages.length) return { type: "unsupported", message: "No user messages to fork from" }; if (!messages.length) return { type: "unsupported", message: "No user messages to fork from" };
const requestId = crypto.randomUUID(); 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 { function formatSessionStats(session: AgentSession): string {
const stats = session.getSessionStats(); const stats = session.getSessionStats();
return [ return [