mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-25 10:42:45 +00:00
fix(opencode): restore complete session environments on connection (#5526)
* fix(opencode): restore complete session environments on connection * fix(opencode): use the shared provider environment policy --------- Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
This commit is contained in:
co-authored by
Mohamed Boudra
parent
7f5d32cdd5
commit
da48803a43
@@ -3,7 +3,11 @@ import {
|
||||
executableExists,
|
||||
findExecutable,
|
||||
} from "../../executable-resolution/executable-resolution.js";
|
||||
import { createExternalProcessEnv, type ProcessEnvRecord } from "../paseo-env.js";
|
||||
import {
|
||||
createExternalProcessEnv,
|
||||
type ProcessEnvRecord,
|
||||
type ExternalProcessEnv,
|
||||
} from "../paseo-env.js";
|
||||
export {
|
||||
AgentProviderRuntimeSettingsMapSchema,
|
||||
ProviderCommandSchema,
|
||||
@@ -242,7 +246,7 @@ export function createProviderEnvSpec(options: ProviderEnvOptions = {}): Provide
|
||||
};
|
||||
}
|
||||
|
||||
export function createProviderEnv(options: ProviderEnvOptions = {}): NodeJS.ProcessEnv {
|
||||
export function createProviderEnv(options: ProviderEnvOptions = {}): ExternalProcessEnv {
|
||||
const spec = createProviderEnvSpec(options);
|
||||
return createExternalProcessEnv(spec.baseEnv ?? process.env, spec.envOverlay);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,83 @@ import { OpenCodeV2AgentClient } from "./agent.js";
|
||||
import { V2Harness } from "../test-utils/v2-harness.js";
|
||||
|
||||
describe("OpenCode v2 session lifecycle", () => {
|
||||
test("restores each same-directory agent environment on reconnect and resume", async () => {
|
||||
const first = new V2Harness();
|
||||
const second = new V2Harness();
|
||||
first.info.id = "session-first";
|
||||
second.info.id = "session-second";
|
||||
const environments = new Map<string, Record<string, string>>();
|
||||
const bind = async (input: { sessionID: string; variables: Record<string, string> }) => {
|
||||
environments.set(input.sessionID, { ...input.variables });
|
||||
};
|
||||
first.api.session.environment = bind;
|
||||
second.api.session.environment = bind;
|
||||
const clients = [first, second].map(
|
||||
(harness) =>
|
||||
new OpenCodeV2AgentClient({
|
||||
logger: createTestLogger(),
|
||||
runtime: harness.runtime,
|
||||
settings: {
|
||||
env: {
|
||||
PASEO_ENV_TEST: "configured",
|
||||
PASEO_AGENT_ID: "configured",
|
||||
ELECTRON_RUN_AS_NODE: "1",
|
||||
PASEO_SUPERVISED: "1",
|
||||
CLAUDECODE: "1",
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
const config = { provider: "opencode" as const, cwd: "/tmp/project" };
|
||||
const launches = ["first", "second"].map((id) => ({
|
||||
env: { PASEO_AGENT_ID: id, PASEO_AGENT_CWD: config.cwd },
|
||||
}));
|
||||
const assertEnvironment = (index: number) => {
|
||||
const env = environments.get([first, second][index].info.id);
|
||||
expect(env?.PATH).toBe(process.env.PATH);
|
||||
expect(env).toMatchObject({ ...launches[index].env, PASEO_ENV_TEST: "configured" });
|
||||
expect(env?.USER).toBe(process.env.USER);
|
||||
for (const key of [
|
||||
"ELECTRON_RUN_AS_NODE",
|
||||
"PASEO_SUPERVISED",
|
||||
"CLAUDECODE",
|
||||
"ESBUILD_BINARY_PATH",
|
||||
"PASEO_NODE_ENV",
|
||||
]) {
|
||||
expect(env).not.toHaveProperty(key);
|
||||
}
|
||||
};
|
||||
const sessions = await Promise.all(
|
||||
clients.map((client, i) => client.createSession(config, launches[i])),
|
||||
);
|
||||
try {
|
||||
assertEnvironment(0);
|
||||
assertEnvironment(1);
|
||||
environments.clear();
|
||||
first.push({ id: "reconnected-first", created: 2, type: "server.connected", data: {} });
|
||||
second.push({ id: "reconnected-second", created: 2, type: "server.connected", data: {} });
|
||||
await expect.poll(() => environments.size).toBe(2);
|
||||
assertEnvironment(0);
|
||||
assertEnvironment(1);
|
||||
} finally {
|
||||
await Promise.all(sessions.map((session) => session.close()));
|
||||
}
|
||||
environments.clear();
|
||||
const resumed = await Promise.all(
|
||||
clients.map((client, i) =>
|
||||
client.resumeSession(sessions[i].describePersistence(), undefined, launches[i]),
|
||||
),
|
||||
);
|
||||
try {
|
||||
assertEnvironment(0);
|
||||
assertEnvironment(1);
|
||||
expect(first.prompts).toEqual([]);
|
||||
expect(second.prompts).toEqual([]);
|
||||
} finally {
|
||||
await Promise.all(resumed.map((session) => session.close()));
|
||||
}
|
||||
});
|
||||
|
||||
test("reconnects after a helper exits and restores session configuration on the next turn", async () => {
|
||||
const first = new V2Harness();
|
||||
const second = new V2Harness();
|
||||
@@ -54,7 +131,10 @@ describe("OpenCode v2 session lifecycle", () => {
|
||||
expect((await session.run("after exit")).finalText).toBe("recovered");
|
||||
expect(second.prompts).toEqual(["after exit"]);
|
||||
expect(second.mcpAdds).toEqual(["tools"]);
|
||||
expect(second.environments).toEqual([{ sessionID: "session", variables: { TOKEN: "test" } }]);
|
||||
expect(second.environments).toMatchObject([
|
||||
{ sessionID: "session", variables: { TOKEN: "test", PATH: process.env.PATH } },
|
||||
]);
|
||||
expect(second.environments).toEqual(first.environments);
|
||||
expect(first.releases).toBe(1);
|
||||
} finally {
|
||||
await session.close();
|
||||
@@ -384,7 +464,7 @@ describe("OpenCode v2 session lifecycle", () => {
|
||||
);
|
||||
try {
|
||||
expect(acquires).toEqual([{}]);
|
||||
expect(harness.environments).toEqual([
|
||||
expect(harness.environments).toMatchObject([
|
||||
{
|
||||
sessionID: "session",
|
||||
variables: { PASEO_AGENT_ID: "agent", PASEO_AGENT_CWD: "/tmp/project" },
|
||||
|
||||
@@ -29,7 +29,10 @@ import type {
|
||||
ListImportableSessionsOptions,
|
||||
ProviderRefreshContext,
|
||||
} from "../../../agent-sdk-types.js";
|
||||
import type { ProviderRuntimeSettings } from "../../../provider-launch-config.js";
|
||||
import {
|
||||
createProviderEnv,
|
||||
type ProviderRuntimeSettings,
|
||||
} from "../../../provider-launch-config.js";
|
||||
import type { ManagedProcessRegistry } from "../../../../managed-processes/managed-processes.js";
|
||||
|
||||
import { importSessionFromPersistence } from "../../../provider-session-import.js";
|
||||
@@ -205,7 +208,11 @@ export class OpenCodeV2AgentClient implements AgentClient {
|
||||
const location = { directory: config.cwd };
|
||||
await awaitPaseoPlugin({ client: connection.client, location });
|
||||
}
|
||||
await session.initialize(launch);
|
||||
await session.initialize(
|
||||
launch?.env
|
||||
? createProviderEnv({ runtimeSettings: this.options.settings, overlays: [launch.env] })
|
||||
: undefined,
|
||||
);
|
||||
return session;
|
||||
} catch (error) {
|
||||
await session.close();
|
||||
|
||||
@@ -8,7 +8,6 @@ import type { SessionInfo, SessionMessageInfo } from "@opencode/client";
|
||||
import { setTimeout as delay } from "node:timers/promises";
|
||||
import type { Logger } from "pino";
|
||||
import type {
|
||||
AgentLaunchContext,
|
||||
AgentMode,
|
||||
AgentPermissionResponse,
|
||||
AgentPersistenceHandle,
|
||||
@@ -36,6 +35,7 @@ import { features } from "./configuration.js";
|
||||
import { commands } from "./commands.js";
|
||||
import { messages } from "./history.js";
|
||||
import { SessionPermissions } from "./permissions.js";
|
||||
|
||||
export class OpenCodeV2Session implements AgentSession {
|
||||
readonly provider = "opencode";
|
||||
readonly capabilities = V2_CAPABILITIES;
|
||||
@@ -106,8 +106,9 @@ export class OpenCodeV2Session implements AgentSession {
|
||||
private get client() {
|
||||
return this.connection.client;
|
||||
}
|
||||
async initialize(launch?: AgentLaunchContext) {
|
||||
this.launchEnv = launch?.env;
|
||||
async initialize(environment?: Record<string, string>) {
|
||||
// OpenCode replaces the whole local shell environment, rather than overlaying it.
|
||||
this.launchEnv = environment;
|
||||
this.watchExit(this.connection);
|
||||
await this.configureConnection();
|
||||
const location = { directory: this.config.cwd };
|
||||
@@ -130,8 +131,6 @@ export class OpenCodeV2Session implements AgentSession {
|
||||
await waitForLocationReady({ client: this.client, location, signal: this.abort.signal });
|
||||
if (this.requiresPaseoPlugin)
|
||||
await awaitPaseoPlugin({ client: this.client, location, signal: this.abort.signal });
|
||||
if (this.launchEnv)
|
||||
await this.client.session.environment({ sessionID: this.id, variables: this.launchEnv });
|
||||
for (const [server, config] of Object.entries(this.config.mcpServers ?? {})) {
|
||||
await this.client.mcp.add({
|
||||
server,
|
||||
@@ -389,6 +388,10 @@ export class OpenCodeV2Session implements AgentSession {
|
||||
}
|
||||
}
|
||||
private async reconcileConnection() {
|
||||
// Session environments are process-local. Reapply this agent's snapshot before
|
||||
// reconciling a new connection, including event-stream reconnections.
|
||||
if (this.launchEnv)
|
||||
await this.client.session.environment({ sessionID: this.id, variables: this.launchEnv });
|
||||
await this.reconcile();
|
||||
await this.children.reconcile(this.id);
|
||||
const active = await this.client.session.active();
|
||||
|
||||
Reference in New Issue
Block a user