mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-06 16:34:36 +00:00
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:
committed by
paseo-bot[bot]
co-authored by
Claude Opus 5.5
parent
005ffbffd5
commit
d1450ad1b7
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user