diff --git a/packages/server/src/server/agent/agent-manager.test.ts b/packages/server/src/server/agent/agent-manager.test.ts index 2721cd67f9..02e1798789 100644 --- a/packages/server/src/server/agent/agent-manager.test.ts +++ b/packages/server/src/server/agent/agent-manager.test.ts @@ -3333,22 +3333,29 @@ test("createAgent and resume give the agent its workspace id", async () => { PASEO_WORKSPACE_ID: "ws-agent-home", }); - // A daemon restart resumes from the stored record, without a workspace id in the call. - await manager.closeAgent(snapshot.id); - await manager.resumeAgentFromPersistence( - { - provider: "codex", - sessionId: "workspace-env-session", - metadata: { provider: "codex", cwd: workdir }, - }, - { cwd: workdir }, - snapshot.id, - ); - expect(client.resumeLaunchContext?.env).toEqual({ + // The stored record supplies the workspace when the caller doesn't. + const handle: AgentPersistenceHandle = { + provider: "codex", + sessionId: "workspace-env-session", + metadata: { provider: "codex", cwd: workdir }, + }; + const expectedEnv = { PASEO_AGENT_ID: snapshot.id, PASEO_AGENT_CWD: workdir, PASEO_WORKSPACE_ID: "ws-agent-home", - }); + }; + + await manager.closeAgent(snapshot.id); + const resumed = await manager.resumeAgentFromPersistence(handle, { cwd: workdir }, snapshot.id); + expect(client.resumeLaunchContext?.env).toEqual(expectedEnv); + expect(resumed.workspaceId).toBe("ws-agent-home"); + expect((await storage.get(snapshot.id))?.workspaceId).toBe("ws-agent-home"); + + // Each restart reads what the previous resume stored. + client.resumeLaunchContext = undefined; + await manager.closeAgent(snapshot.id); + await manager.resumeAgentFromPersistence(handle, { cwd: workdir }, snapshot.id); + expect(client.resumeLaunchContext?.env).toEqual(expectedEnv); }); test("createAgent passes persistSession to provider create options", async () => { diff --git a/packages/server/src/server/agent/agent-manager.ts b/packages/server/src/server/agent/agent-manager.ts index c008999944..b47e12e558 100644 --- a/packages/server/src/server/agent/agent-manager.ts +++ b/packages/server/src/server/agent/agent-manager.ts @@ -683,15 +683,15 @@ function agentIdentityEnv({ }; } -// A restart resumes from the stored record, with no workspace id in the call. +// Launch env and the registered agent must agree, or the next snapshot erases the stored workspace. function resolveResumeWorkspaceId({ options, record, }: { options: { workspaceId?: string } | undefined; record: Pick | null; -}): string | null { - return options?.workspaceId ?? record?.workspaceId ?? null; +}): string | undefined { + return options?.workspaceId ?? record?.workspaceId; } function validateAgentId(agentId: string, source: string): string { @@ -1601,6 +1601,7 @@ export class AgentManager { const currentResumeOptions = { purpose } satisfies AgentResumeSessionOptions; const launchEnv = record?.launchEnv; + const workspaceId = resolveResumeWorkspaceId({ options, record }); const { storedConfig, launchConfig, paseoToolPolicy } = await this.prepareSessionConfig( mergedConfig, resolvedAgentId, @@ -1623,7 +1624,7 @@ export class AgentManager { { reason: "resume", purpose, - workspaceId: resolveResumeWorkspaceId({ options, record }), + workspaceId, title: record?.title ?? mergedConfig.title ?? null, labels: record?.labels ?? options?.labels ?? {}, }, @@ -1639,6 +1640,7 @@ export class AgentManager { await this.requireExternalMcpSupport(session, providerLaunchConfig, launchContext); return this.registerSession(session, storedConfig, resolvedAgentId, { ...options, + workspaceId, launchEnv, persistence: handle, restoring: true, diff --git a/packages/server/src/server/daemon-client.e2e.test.ts b/packages/server/src/server/daemon-client.e2e.test.ts index a442dfe6a9..701a7f6108 100644 --- a/packages/server/src/server/daemon-client.e2e.test.ts +++ b/packages/server/src/server/daemon-client.e2e.test.ts @@ -18,6 +18,7 @@ import { getFullAccessConfig, getAskModeConfig } from "./daemon-e2e/agent-config import { parsePcm16MonoWav, wordSimilarity } from "./test-utils/dictation-e2e.js"; import type { AgentClient, + AgentLaunchContext, AgentPersistenceHandle, AgentRunResult, AgentSession, @@ -324,6 +325,7 @@ class RecordingMcpResumeClient implements AgentClient { readonly provider = "codex" as const; readonly capabilities; readonly resumeOverrides: Array | undefined> = []; + readonly resumeLaunchContexts: Array = []; private readonly inner = createTestAgentClients({ supportsMcpServers: true }).codex; constructor() { @@ -341,8 +343,10 @@ class RecordingMcpResumeClient implements AgentClient { resumeSession( handle: AgentPersistenceHandle, overrides?: Partial, + launchContext?: AgentLaunchContext, ): Promise { this.resumeOverrides.push(overrides); + this.resumeLaunchContexts.push(launchContext); return this.inner.resumeSession(handle, overrides); } @@ -1034,6 +1038,38 @@ test("resume_agent rehydrates the newest private MCP config from a redacted pers } }); +test("resume_agent keeps the workspace of the agent it matches", async () => { + const cwd = tmpCwd(); + const provider = new RecordingMcpResumeClient(); + const localCtx = await createDaemonTestContext({ + agentClients: { codex: provider }, + }); + + try { + const opened = await localCtx.client.openProject(cwd); + if (!opened.workspace) { + throw new Error(opened.error ?? "Workspace did not open"); + } + const workspaceId = opened.workspace.id; + const created = await localCtx.client.createAgent({ + config: { provider: "codex", cwd }, + workspaceId, + }); + if (!created.persistence) { + throw new Error("Expected a persistence handle"); + } + + await localCtx.client.archiveAgent(created.id); + const resumed = await localCtx.client.resumeAgent(created.persistence); + + expect(provider.resumeLaunchContexts[0]?.env?.PASEO_WORKSPACE_ID).toBe(workspaceId); + expect(resumed.workspaceId).toBe(workspaceId); + } finally { + await localCtx.cleanup(); + rmSync(cwd, { recursive: true, force: true }); + } +}); + test("resume_agent restores archive state when an MCP-capable provider returns an unsupported session", async () => { const cwd = tmpCwd(); const provider = new RejectingMcpResumeClient(); diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index 83f030d971..55d6c58401 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -4856,7 +4856,12 @@ export class Session { : overrides; let snapshot: ManagedAgent; try { - snapshot = await this.agentManager.resumeAgentFromPersistence(handle, effectiveOverrides); + snapshot = await this.agentManager.resumeAgentFromPersistence( + handle, + effectiveOverrides, + undefined, + { workspaceId: matched?.record.workspaceId }, + ); } catch (error) { if (matched?.didUnarchive && matched.originalArchivedAt) { await this.agentManager.archiveSnapshot(matched.record.id, matched.originalArchivedAt);