diff --git a/packages/server/src/server/daemon-e2e/persisted-creation-caller.e2e.test.ts b/packages/server/src/server/daemon-e2e/persisted-creation-caller.e2e.test.ts new file mode 100644 index 0000000000..db9341f794 --- /dev/null +++ b/packages/server/src/server/daemon-e2e/persisted-creation-caller.e2e.test.ts @@ -0,0 +1,64 @@ +import { mkdir, mkdtemp, rm } from "node:fs/promises"; +import path from "node:path"; +import { expect, test } from "vitest"; +import { PARENT_AGENT_ID_LABEL } from "@getpaseo/protocol/agent-labels"; +import { createDaemonTestContext, type DaemonTestContext } from "../test-utils/index.js"; + +test.each(["agent", "workspace"] as const)( + "%s creation recognizes an unopened persisted caller after restart", + async (kind) => { + const artifacts = path.resolve(".artifacts"); + await mkdir(artifacts, { recursive: true }); + const root = await mkdtemp(path.join(artifacts, "persisted-caller-")); + const cwd = path.join(root, "project"); + await mkdir(cwd); + let ctx: DaemonTestContext | undefined; + try { + ctx = await createDaemonTestContext({ + paseoHomeRoot: root, + staticDir: cwd, + cleanup: false, + mcpEnabled: false, + }); + const parent = await ctx.client.createAgent({ provider: "claude", cwd, background: true }); + await ctx.client.sendMessage(parent.id, "Reply DONE."); + expect((await ctx.client.waitForFinish(parent.id, 5_000)).status).toBe("idle"); + await ctx.cleanup(); + ctx = undefined; + ctx = await createDaemonTestContext({ + paseoHomeRoot: root, + staticDir: cwd, + cleanup: false, + mcpEnabled: false, + }); + expect(ctx.daemon.daemon.agentManager.getAgent(parent.id)).toBeNull(); + expect( + (await ctx.client.fetchAgents({ filter: { includeBackground: true } })).entries, + ).toContainEqual( + expect.objectContaining({ agent: expect.objectContaining({ id: parent.id }) }), + ); + + if (kind === "agent") { + const child = await ctx.client.createAgent({ + provider: "claude", + cwd, + callerAgentId: parent.id, + }); + expect(child.workspaceId).toBe(parent.workspaceId); + expect(child.labels[PARENT_AGENT_ID_LABEL]).toBe(parent.id); + } else { + const result = await ctx.client.createWorkspace({ + source: { kind: "directory", path: cwd }, + callerAgentId: parent.id, + }); + expect(result.error).toBeNull(); + expect(result.workspace?.background).toBe(true); + } + expect(ctx.daemon.daemon.agentManager.getAgent(parent.id)).toBeNull(); + } finally { + await ctx?.cleanup(); + await rm(root, { recursive: true, force: true }); + } + }, + 60_000, +); diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index 8911f9acf1..9553b2db51 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -119,7 +119,11 @@ import type { ManagedAgent, } from "./agent/agent-manager.js"; import { createAgentCommand } from "./agent/create-agent/create.js"; -import { resolveCreateAgentIntent, type CreateAgentIntent } from "./agent/create-agent/intent.js"; +import { + resolveCreateAgentIntent, + type CreateAgentIntent, + type CreateAgentCaller, +} from "./agent/create-agent/intent.js"; import { archiveAgentCommand, cancelAgentRunCommand, @@ -4586,7 +4590,7 @@ export class Session { target: worktree, firstAgentContext, background: msg.background, - callerWorkspaceId: this.getCallerWorkspaceId(msg.callerAgentId), + callerWorkspaceId: (await this.resolveCreationCaller(msg.callerAgentId))?.workspaceId, }); createdWorktreeForCleanup = createdWorktree; const resolvedIntent = await this.resolveSessionCreateAgentIntent({ @@ -4671,20 +4675,13 @@ export class Session { workspacePromptTitle: string | null; }): Promise { const { request, createdWorktree } = input; - const callerAgent = request.callerAgentId - ? this.agentManager.getAgent(request.callerAgentId) - : null; - if (request.callerAgentId && !callerAgent) { - throw new Error(`Caller agent ${request.callerAgentId} not found`); - } + const callerAgent = await this.resolveCreationCaller(request.callerAgentId); let config = request.config; const intent = await resolveCreateAgentIntent({ explicitWorkspaceId: createdWorktree?.workspace.workspaceId ?? request.workspaceId, - caller: callerAgent - ? { id: callerAgent.id, cwd: callerAgent.cwd, workspaceId: callerAgent.workspaceId } - : null, + caller: callerAgent, labels: request.labels, resolveWorkspace: async (workspaceId) => { if (createdWorktree?.workspace.workspaceId === workspaceId) { @@ -6964,11 +6961,12 @@ export class Session { } } - private getCallerWorkspaceId(callerAgentId?: string): string | undefined { - if (!callerAgentId) return undefined; - const caller = this.agentManager.getAgent(callerAgentId); + private async resolveCreationCaller(callerAgentId?: string): Promise { + if (!callerAgentId) return null; + const caller = + this.agentManager.getAgent(callerAgentId) ?? (await this.agentStorage.get(callerAgentId)); if (!caller) throw new Error(`Caller agent ${callerAgentId} not found`); - return caller.workspaceId; + return { id: caller.id, cwd: caller.cwd, workspaceId: caller.workspaceId }; } private async createRequestedWorkspace( @@ -7018,7 +7016,7 @@ export class Session { expectsInitialAgent: Boolean(request.firstAgentContext), workspaceId, background: request.background, - callerWorkspaceId: this.getCallerWorkspaceId(request.callerAgentId), + callerWorkspaceId: (await this.resolveCreationCaller(request.callerAgentId))?.workspaceId, }, ); await this.syncWorkspaceGitObserverForWorkspace(workspace); @@ -7082,7 +7080,7 @@ export class Session { firstAgentContext: request.firstAgentContext, title: request.title, background: request.background, - callerWorkspaceId: this.getCallerWorkspaceId(request.callerAgentId), + callerWorkspaceId: (await this.resolveCreationCaller(request.callerAgentId))?.workspaceId, }, source.baseBranch ? { resolveDefaultBranch: async () => source.baseBranch as string }