Compare commits

...
Author SHA1 Message Date
paseo-bot[bot]andcoolwind abf9a6ff77 fix(agent): carry launch environment preservation onto current main
Include the merged Windows receipt persistence and integration synchronization fixes so the restart fix can be verified on current CI.

Co-authored-by: coolwind <45729206+coolwindX@users.noreply.github.com>
2026-10-26 17:11:07 +00:00
coolwind ae153721c8 fix(agent): preserve launch env across daemon restarts 2026-10-25 12:48:59 +00:00
5 changed files with 143 additions and 3 deletions
@@ -135,6 +135,7 @@ export async function ensureAgentLoaded(
labels: record.labels,
workspaceId: record.workspaceId,
owner: record.owner,
env: record.launchEnv,
});
deps.logger.info({ agentId, provider: record.provider }, "Agent created from stored config");
}
@@ -2869,6 +2869,134 @@ test("createAgent passes daemon launch env through the provider launch context",
});
});
test.each([
{ provider: "codex", hasHandle: true },
{ provider: "opencode", hasHandle: true },
{ provider: "codex", hasHandle: false },
{ provider: "opencode", hasHandle: false },
])(
"$provider preserves explicit launch env across reloads and storage-backed resumes (handle: $hasHandle)",
async ({ provider, hasHandle }) => {
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-launch-env-"));
const storagePath = join(workdir, "agents");
class LaunchEnvClient extends TestAgentClient {
lastLaunchContext: AgentLaunchContext | undefined;
override async createSession(
config: AgentSessionConfig,
context?: AgentLaunchContext,
): Promise<AgentSession> {
this.lastLaunchContext = context;
const session = await super.createSession(config);
session.describePersistence = () => ({ provider, sessionId: session.id });
return session;
}
override async resumeSession(
handle: AgentPersistenceHandle,
config?: Partial<AgentSessionConfig>,
context?: AgentLaunchContext,
): Promise<AgentSession> {
this.lastLaunchContext = context;
const session = await super.resumeSession(handle, config, context);
session.describePersistence = () => ({ provider, sessionId: handle.sessionId });
return session;
}
}
const client = new LaunchEnvClient(provider);
let storage = new AgentStorage(storagePath, logger);
let manager = new AgentManager({ clients: { [provider]: client }, registry: storage, logger });
const env = { XDG_DATA_HOME: join(workdir, "agent-data"), CUSTOM_EMPTY: "" };
const expectedEnv = { ...env };
const agent = await manager.createAgent({ provider, cwd: workdir }, undefined, {
workspaceId: undefined,
env,
});
onTestFinished(async () => {
await manager.closeAgent(agent.id);
await manager.flush();
rmSync(workdir, { recursive: true, force: true });
});
env.XDG_DATA_HOME = "caller-mutated";
await manager.closeAgent(agent.id);
await manager.flush();
if (!hasHandle) {
const record = await storage.get(agent.id);
if (!record) throw new Error("Created agent was not persisted");
await storage.upsert({ ...record, persistence: null });
}
// Fresh storage and manager instances must read the environment from disk.
for (let restart = 0; restart < 2; restart += 1) {
storage = new AgentStorage(storagePath, logger);
manager = new AgentManager({ clients: { [provider]: client }, registry: storage, logger });
const resumed = await ensureAgentLoaded(agent.id, {
agentManager: manager,
agentStorage: storage,
logger,
});
expect(client.lastLaunchContext).toEqual({
agentId: agent.id,
env: { ...expectedEnv, PASEO_AGENT_ID: agent.id, PASEO_AGENT_CWD: workdir },
});
await manager.reloadAgentSession(agent.id);
expect(client.lastLaunchContext).toEqual({
agentId: agent.id,
env: { ...expectedEnv, PASEO_AGENT_ID: agent.id, PASEO_AGENT_CWD: workdir },
});
expect(toAgentPayload(resumed)).not.toHaveProperty("launchEnv");
expect(toAgentPayload(resumed).persistence?.metadata).not.toHaveProperty("launchEnv");
await manager.closeAgent(agent.id);
await manager.flush();
}
},
);
test("session-open hooks override persisted launch env without persisting per-opening values", async () => {
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-launch-env-hook-"));
const storage = new AgentStorage(join(workdir, "agents"), logger);
const observedEnv: Array<Record<string, string> | undefined> = [];
const manager = new AgentManager({
clients: { codex: new TestAgentClient() },
registry: storage,
logger,
pluginLifecycle: {
emit() {},
async before(name, request) {
if (name === "agent.create" && "env" in request) {
request.env = { ...request.env, FROM_CREATE_HOOK: "saved" };
}
if (name === "agent.session_open" && "env" in request) {
observedEnv.push({ ...request.env });
request.env = { ...request.env, XDG_DATA_HOME: "current-hook-data" };
}
return request;
},
},
});
const agent = await manager.createAgent({ provider: "codex", cwd: workdir }, undefined, {
workspaceId: undefined,
env: { XDG_DATA_HOME: "original-data" },
});
onTestFinished(async () => {
await manager.closeAgent(agent.id);
await manager.flush();
rmSync(workdir, { recursive: true, force: true });
});
await manager.reloadAgentSession(agent.id);
expect(observedEnv).toEqual([
{ XDG_DATA_HOME: "original-data", FROM_CREATE_HOOK: "saved" },
{ XDG_DATA_HOME: "original-data", FROM_CREATE_HOOK: "saved" },
]);
expect((await storage.get(agent.id))?.launchEnv).toEqual({
XDG_DATA_HOME: "original-data",
FROM_CREATE_HOOK: "saved",
});
});
test("createAgent passes persistSession to provider create options", async () => {
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-"));
const storagePath = join(workdir, "agents");
@@ -414,6 +414,8 @@ interface ManagedAgentBase {
owner?: AgentOwner;
capabilities: AgentCapabilityFlags;
config: AgentSessionConfig;
/** Explicit launch overrides, before per-opening hooks and daemon-owned variables. */
launchEnv?: Record<string, string>;
runtimeInfo?: AgentRuntimeInfo;
createdAt: Date;
updatedAt: Date;
@@ -1323,6 +1325,7 @@ export class AgentManager {
initialTitle: options.initialTitle,
workspaceId: options.workspaceId,
owner: options.owner,
launchEnv: options.env ? { ...options.env } : undefined,
historyPrimed: true,
});
if (!agent.internal) {
@@ -1414,7 +1417,7 @@ export class AgentManager {
const { storedConfig, launchConfig, paseoToolPolicy } = await this.prepareSessionConfig(
mergedConfig,
resolvedAgentId,
{ purpose },
{ purpose, env: record?.launchEnv },
);
const client = this.requireClient(handle.provider);
const available = await client.isAvailable();
@@ -1429,7 +1432,7 @@ export class AgentManager {
client,
storedConfig.cwd,
paseoToolPolicy,
undefined,
record?.launchEnv,
{
reason: "resume",
purpose,
@@ -1446,6 +1449,7 @@ export class AgentManager {
await this.requireExternalMcpSupport(session, storedConfig);
return this.registerSession(session, storedConfig, resolvedAgentId, {
...options,
launchEnv: record?.launchEnv,
persistence: handle,
restoring: true,
});
@@ -1577,6 +1581,7 @@ export class AgentManager {
const { storedConfig, launchConfig, paseoToolPolicy } = await this.prepareSessionConfig(
refreshConfig,
agentId,
{ env: existing.launchEnv },
);
const hadPreviousPaseoToolPolicy = this.paseoToolPolicies.has(agentId);
const previousPaseoToolPolicy = this.paseoToolPolicies.get(agentId);
@@ -1585,7 +1590,7 @@ export class AgentManager {
client,
storedConfig.cwd,
paseoToolPolicy,
undefined,
existing.launchEnv,
{ reason: "refresh", purpose: "interactive", workspaceId: existing.workspaceId },
);
const providerLaunchConfig = this.resolveProviderLaunchConfig(launchConfig, launchContext);
@@ -1630,6 +1635,7 @@ export class AgentManager {
labels: existing.labels,
workspaceId: existing.workspaceId,
owner: existing.owner,
launchEnv: existing.launchEnv,
createdAt: existing.createdAt,
updatedAt: existing.updatedAt,
lastUserMessageAt: existing.lastUserMessageAt,
@@ -3668,6 +3674,7 @@ export class AgentManager {
publishWhenReady?: boolean;
workspaceId?: string;
owner?: AgentOwner;
launchEnv?: Record<string, string>;
},
): Promise<ManagedAgent> {
let registered = false;
@@ -3846,6 +3853,7 @@ export class AgentManager {
persistence?: AgentPersistenceHandle;
workspaceId?: string;
owner?: AgentOwner;
launchEnv?: Record<string, string>;
}
| undefined;
}): ActiveManagedAgent {
@@ -3856,6 +3864,7 @@ export class AgentManager {
cwd: config.cwd,
workspaceId: options?.workspaceId,
owner: options?.owner,
launchEnv: options?.launchEnv,
session,
capabilities: session.capabilities,
config,
@@ -95,6 +95,7 @@ export function toStoredAgentRecord(
lastStatus: agent.lifecycle,
lastModeId: agent.currentModeId ?? config?.modeId ?? null,
config: config ?? null,
launchEnv: agent.launchEnv ? { ...agent.launchEnv } : undefined,
runtimeInfo,
features: normalizeFeatures(agent.features),
persistence,
@@ -56,6 +56,7 @@ const STORED_AGENT_SCHEMA = z.object({
lastStatus: AgentStatusSchema.default("closed"),
lastModeId: z.string().nullable().optional(),
config: SERIALIZABLE_CONFIG_SCHEMA,
launchEnv: z.record(z.string(), z.string()).optional(),
runtimeInfo: z
.object({
provider: z.string(),