From 6a15fc1a46f39386ca104a82d9f75b23755bd3ae Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Tue, 3 Nov 2026 23:09:38 +0000 Subject: [PATCH] fix(server): recognize persisted parents during child creation Creation only needs the caller's identity and workspace. Read the existing stored record when the parent is unloaded after restart, rather than requiring its provider session to be opened first. --- .../persisted-creation-caller.e2e.test.ts | 64 +++++++++++++++++++ packages/server/src/server/session.ts | 32 +++++----- 2 files changed, 79 insertions(+), 17 deletions(-) create mode 100644 packages/server/src/server/daemon-e2e/persisted-creation-caller.e2e.test.ts 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 }