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, labels: record.labels,
workspaceId: record.workspaceId, workspaceId: record.workspaceId,
owner: record.owner, owner: record.owner,
env: record.launchEnv,
}); });
deps.logger.info({ agentId, provider: record.provider }, "Agent created from stored config"); 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 () => { test("createAgent passes persistSession to provider create options", async () => {
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-")); const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-"));
const storagePath = join(workdir, "agents"); const storagePath = join(workdir, "agents");
@@ -414,6 +414,8 @@ interface ManagedAgentBase {
owner?: AgentOwner; owner?: AgentOwner;
capabilities: AgentCapabilityFlags; capabilities: AgentCapabilityFlags;
config: AgentSessionConfig; config: AgentSessionConfig;
/** Explicit launch overrides, before per-opening hooks and daemon-owned variables. */
launchEnv?: Record<string, string>;
runtimeInfo?: AgentRuntimeInfo; runtimeInfo?: AgentRuntimeInfo;
createdAt: Date; createdAt: Date;
updatedAt: Date; updatedAt: Date;
@@ -1323,6 +1325,7 @@ export class AgentManager {
initialTitle: options.initialTitle, initialTitle: options.initialTitle,
workspaceId: options.workspaceId, workspaceId: options.workspaceId,
owner: options.owner, owner: options.owner,
launchEnv: options.env ? { ...options.env } : undefined,
historyPrimed: true, historyPrimed: true,
}); });
if (!agent.internal) { if (!agent.internal) {
@@ -1414,7 +1417,7 @@ export class AgentManager {
const { storedConfig, launchConfig, paseoToolPolicy } = await this.prepareSessionConfig( const { storedConfig, launchConfig, paseoToolPolicy } = await this.prepareSessionConfig(
mergedConfig, mergedConfig,
resolvedAgentId, resolvedAgentId,
{ purpose }, { purpose, env: record?.launchEnv },
); );
const client = this.requireClient(handle.provider); const client = this.requireClient(handle.provider);
const available = await client.isAvailable(); const available = await client.isAvailable();
@@ -1429,7 +1432,7 @@ export class AgentManager {
client, client,
storedConfig.cwd, storedConfig.cwd,
paseoToolPolicy, paseoToolPolicy,
undefined, record?.launchEnv,
{ {
reason: "resume", reason: "resume",
purpose, purpose,
@@ -1446,6 +1449,7 @@ export class AgentManager {
await this.requireExternalMcpSupport(session, storedConfig); await this.requireExternalMcpSupport(session, storedConfig);
return this.registerSession(session, storedConfig, resolvedAgentId, { return this.registerSession(session, storedConfig, resolvedAgentId, {
...options, ...options,
launchEnv: record?.launchEnv,
persistence: handle, persistence: handle,
restoring: true, restoring: true,
}); });
@@ -1577,6 +1581,7 @@ export class AgentManager {
const { storedConfig, launchConfig, paseoToolPolicy } = await this.prepareSessionConfig( const { storedConfig, launchConfig, paseoToolPolicy } = await this.prepareSessionConfig(
refreshConfig, refreshConfig,
agentId, agentId,
{ env: existing.launchEnv },
); );
const hadPreviousPaseoToolPolicy = this.paseoToolPolicies.has(agentId); const hadPreviousPaseoToolPolicy = this.paseoToolPolicies.has(agentId);
const previousPaseoToolPolicy = this.paseoToolPolicies.get(agentId); const previousPaseoToolPolicy = this.paseoToolPolicies.get(agentId);
@@ -1585,7 +1590,7 @@ export class AgentManager {
client, client,
storedConfig.cwd, storedConfig.cwd,
paseoToolPolicy, paseoToolPolicy,
undefined, existing.launchEnv,
{ reason: "refresh", purpose: "interactive", workspaceId: existing.workspaceId }, { reason: "refresh", purpose: "interactive", workspaceId: existing.workspaceId },
); );
const providerLaunchConfig = this.resolveProviderLaunchConfig(launchConfig, launchContext); const providerLaunchConfig = this.resolveProviderLaunchConfig(launchConfig, launchContext);
@@ -1630,6 +1635,7 @@ export class AgentManager {
labels: existing.labels, labels: existing.labels,
workspaceId: existing.workspaceId, workspaceId: existing.workspaceId,
owner: existing.owner, owner: existing.owner,
launchEnv: existing.launchEnv,
createdAt: existing.createdAt, createdAt: existing.createdAt,
updatedAt: existing.updatedAt, updatedAt: existing.updatedAt,
lastUserMessageAt: existing.lastUserMessageAt, lastUserMessageAt: existing.lastUserMessageAt,
@@ -3668,6 +3674,7 @@ export class AgentManager {
publishWhenReady?: boolean; publishWhenReady?: boolean;
workspaceId?: string; workspaceId?: string;
owner?: AgentOwner; owner?: AgentOwner;
launchEnv?: Record<string, string>;
}, },
): Promise<ManagedAgent> { ): Promise<ManagedAgent> {
let registered = false; let registered = false;
@@ -3846,6 +3853,7 @@ export class AgentManager {
persistence?: AgentPersistenceHandle; persistence?: AgentPersistenceHandle;
workspaceId?: string; workspaceId?: string;
owner?: AgentOwner; owner?: AgentOwner;
launchEnv?: Record<string, string>;
} }
| undefined; | undefined;
}): ActiveManagedAgent { }): ActiveManagedAgent {
@@ -3856,6 +3864,7 @@ export class AgentManager {
cwd: config.cwd, cwd: config.cwd,
workspaceId: options?.workspaceId, workspaceId: options?.workspaceId,
owner: options?.owner, owner: options?.owner,
launchEnv: options?.launchEnv,
session, session,
capabilities: session.capabilities, capabilities: session.capabilities,
config, config,
@@ -95,6 +95,7 @@ export function toStoredAgentRecord(
lastStatus: agent.lifecycle, lastStatus: agent.lifecycle,
lastModeId: agent.currentModeId ?? config?.modeId ?? null, lastModeId: agent.currentModeId ?? config?.modeId ?? null,
config: config ?? null, config: config ?? null,
launchEnv: agent.launchEnv ? { ...agent.launchEnv } : undefined,
runtimeInfo, runtimeInfo,
features: normalizeFeatures(agent.features), features: normalizeFeatures(agent.features),
persistence, persistence,
@@ -56,6 +56,7 @@ const STORED_AGENT_SCHEMA = z.object({
lastStatus: AgentStatusSchema.default("closed"), lastStatus: AgentStatusSchema.default("closed"),
lastModeId: z.string().nullable().optional(), lastModeId: z.string().nullable().optional(),
config: SERIALIZABLE_CONFIG_SCHEMA, config: SERIALIZABLE_CONFIG_SCHEMA,
launchEnv: z.record(z.string(), z.string()).optional(),
runtimeInfo: z runtimeInfo: z
.object({ .object({
provider: z.string(), provider: z.string(),