fix(server): keep the workspace when resuming an agent by handle

resume_agent_request matched the stored agent but dropped its workspace id, so
the resumed agent had no PASEO_WORKSPACE_ID and no workspace. Pass the matched
record's workspace id into the resume.

resumeAgentFromPersistence now resolves the workspace once and gives it to both
the launch env and the registered agent, so a caller that omits it no longer
erases the stored value on the next snapshot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Darrell
2026-11-06 13:59:23 +00:00
committed by paseo-bot[bot]
co-authored by Claude Opus 5.5
parent 005ffbffd5
commit d1450ad1b7
4 changed files with 68 additions and 18 deletions
@@ -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 () => {
@@ -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<StoredAgentRecord, "workspaceId"> | 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,
@@ -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<Partial<AgentSessionConfig> | undefined> = [];
readonly resumeLaunchContexts: Array<AgentLaunchContext | undefined> = [];
private readonly inner = createTestAgentClients({ supportsMcpServers: true }).codex;
constructor() {
@@ -341,8 +343,10 @@ class RecordingMcpResumeClient implements AgentClient {
resumeSession(
handle: AgentPersistenceHandle,
overrides?: Partial<AgentSessionConfig>,
launchContext?: AgentLaunchContext,
): Promise<AgentSession> {
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();
+6 -1
View File
@@ -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);