mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
fix(cli): clean up failed run workspaces
Agent creation errors left workspaces created by paseo run behind. Roll back only the workspace owned by that run while preserving the original failure.
This commit is contained in:
@@ -26,6 +26,7 @@ describe("existing run workspace resolution", () => {
|
||||
await expect(resolveExistingRunWorkspace({ fetchWorkspaces }, "workspace-2")).resolves.toEqual({
|
||||
id: "workspace-2",
|
||||
cwd: "/workspace/two",
|
||||
createdForRun: false,
|
||||
});
|
||||
expect(fetchWorkspaces).toHaveBeenCalledWith({
|
||||
filter: { query: "workspace-2" },
|
||||
|
||||
@@ -504,6 +504,32 @@ async function connectToDaemonOrThrow(
|
||||
interface RunWorkspace {
|
||||
id?: string;
|
||||
cwd: string;
|
||||
createdForRun: boolean;
|
||||
}
|
||||
|
||||
type CreateRunAgentInput = Parameters<ConnectedDaemonClient["createAgent"]>[0];
|
||||
|
||||
async function createAgentInRunWorkspace(
|
||||
client: ConnectedDaemonClient,
|
||||
workspace: RunWorkspace,
|
||||
input: CreateRunAgentInput,
|
||||
): Promise<AgentSnapshotPayload> {
|
||||
try {
|
||||
return await client.createAgent(input);
|
||||
} catch (error) {
|
||||
if (workspace.createdForRun && workspace.id) {
|
||||
try {
|
||||
const archived = await client.archiveWorkspace(workspace.id);
|
||||
if (archived.error) {
|
||||
console.error(`Warning: failed to clean up workspace ${workspace.id}: ${archived.error}`);
|
||||
}
|
||||
} catch (cleanupError) {
|
||||
const message = cleanupError instanceof Error ? cleanupError.message : String(cleanupError);
|
||||
console.error(`Warning: failed to clean up workspace ${workspace.id}: ${message}`);
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export interface RunWorkspaceLookupClient {
|
||||
@@ -523,7 +549,7 @@ export async function resolveExistingRunWorkspace(
|
||||
});
|
||||
const workspace = result.entries.find((entry) => entry.id === workspaceId);
|
||||
if (workspace) {
|
||||
return { id: workspace.id, cwd: workspace.workspaceDirectory };
|
||||
return { id: workspace.id, cwd: workspace.workspaceDirectory, createdForRun: false };
|
||||
}
|
||||
|
||||
throw {
|
||||
@@ -551,7 +577,7 @@ async function resolveRunWorkspace(
|
||||
}
|
||||
|
||||
if (!newWorkspace && resolveRunCallerAgentId()) {
|
||||
return { cwd };
|
||||
return { cwd, createdForRun: false };
|
||||
}
|
||||
|
||||
const ambientWorkspaceId = newWorkspace ? undefined : process.env.PASEO_WORKSPACE_ID?.trim();
|
||||
@@ -578,7 +604,11 @@ async function resolveRunWorkspace(
|
||||
console.error(
|
||||
"Tip: pass --workspace <id> (or set PASEO_WORKSPACE_ID) to run in an existing workspace.",
|
||||
);
|
||||
return { id: result.workspace.id, cwd: result.workspace.workspaceDirectory ?? cwd };
|
||||
return {
|
||||
id: result.workspace.id,
|
||||
cwd: result.workspace.workspaceDirectory ?? cwd,
|
||||
createdForRun: true,
|
||||
};
|
||||
}
|
||||
|
||||
export async function runRunCommand(
|
||||
@@ -627,7 +657,7 @@ export async function runRunCommand(
|
||||
|
||||
const callStructuredTurn = async (structuredPrompt: string): Promise<string> => {
|
||||
if (!structuredAgent) {
|
||||
structuredAgent = await client.createAgent({
|
||||
structuredAgent = await createAgentInRunWorkspace(client, workspace, {
|
||||
provider: resolvedProviderModel.provider,
|
||||
cwd: runCwd,
|
||||
workspaceId,
|
||||
@@ -698,7 +728,7 @@ export async function runRunCommand(
|
||||
}
|
||||
|
||||
// Create the agent
|
||||
const agent = await client.createAgent({
|
||||
const agent = await createAgentInRunWorkspace(client, workspace, {
|
||||
provider: resolvedProviderModel.provider,
|
||||
cwd: runCwd,
|
||||
workspaceId,
|
||||
|
||||
@@ -25,6 +25,7 @@ import { $ } from "zx";
|
||||
import { mkdtemp, rm, writeFile } from "fs/promises";
|
||||
import { tmpdir } from "os";
|
||||
import { join } from "path";
|
||||
import { createE2ETestContext } from "./helpers/test-daemon.ts";
|
||||
|
||||
$.verbose = false;
|
||||
|
||||
@@ -268,6 +269,56 @@ try {
|
||||
assert(output.includes("unknown option"), "should report unknown option for --isolation");
|
||||
console.log("✓ run --isolation is rejected\n");
|
||||
}
|
||||
|
||||
// Test 17: failed agent creation does not leave the run's workspace behind
|
||||
{
|
||||
console.log("Test 17: failed agent creation cleans up its workspace");
|
||||
const ctx = await createE2ETestContext({ timeout: 45000 });
|
||||
try {
|
||||
await $({ cwd: ctx.workDir })`git init -b main`;
|
||||
await $({ cwd: ctx.workDir })`git config user.email test@getpaseo.local`;
|
||||
await $({ cwd: ctx.workDir })`git config user.name "Paseo Test"`;
|
||||
await $({ cwd: ctx.workDir })`git commit --allow-empty -m initial`;
|
||||
|
||||
const before = await ctx.paseo(["workspace", "ls", "--json"]);
|
||||
assert.strictEqual(before.exitCode, 0, before.stderr);
|
||||
|
||||
const result = await ctx.paseo(
|
||||
[
|
||||
"run",
|
||||
"--provider",
|
||||
"claude",
|
||||
"--mode",
|
||||
"bypass",
|
||||
"--new-workspace",
|
||||
"worktree",
|
||||
"--worktree-mode",
|
||||
"branch-off",
|
||||
"--new-branch",
|
||||
"failed-run-workspace",
|
||||
"--base",
|
||||
"main",
|
||||
"--background",
|
||||
"do something",
|
||||
],
|
||||
{
|
||||
env: { PASEO_AGENT_ID: "", PASEO_WORKSPACE_ID: "" },
|
||||
},
|
||||
);
|
||||
assert.notStrictEqual(result.exitCode, 0, "invalid mode should reject agent creation");
|
||||
assert(
|
||||
`${result.stdout}\n${result.stderr}`.includes("Invalid mode 'bypass'"),
|
||||
`failure should reach provider mode validation\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`,
|
||||
);
|
||||
|
||||
const after = await ctx.paseo(["workspace", "ls", "--json"]);
|
||||
assert.strictEqual(after.exitCode, 0, after.stderr);
|
||||
assert.deepStrictEqual(JSON.parse(after.stdout), JSON.parse(before.stdout));
|
||||
} finally {
|
||||
await ctx.stop();
|
||||
}
|
||||
console.log("✓ failed agent creation cleans up its workspace\n");
|
||||
}
|
||||
} finally {
|
||||
// Clean up temp directory
|
||||
await rm(paseoHome, { recursive: true, force: true });
|
||||
|
||||
Reference in New Issue
Block a user