mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-01 22:23:07 +00:00
fix(server): Codex keeps unsupplied running options, snapshots show the live setting after a removal
- Codex applyOptions merges the supplied options into the running ones, so an unrelated patch or a removal no longer drops the sandbox and approval settings that were merged in when the session opened. - A control the running session reports for a key the agent no longer holds shows its live value in the effective options, instead of the config.json default, so the snapshot and unattended-parent checks agree with the provider.
This commit is contained in:
@@ -12831,6 +12831,55 @@ describe("agent options", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test("after a removal the snapshot shows the live setting, not the configured default", async () => {
|
||||
class LiveSession extends OptionsSession {
|
||||
private tier = "priority";
|
||||
override async applyOptions(options: ProviderOptions): Promise<void> {
|
||||
await super.applyOptions(options);
|
||||
if (typeof options.service_tier === "string") this.tier = options.service_tier;
|
||||
}
|
||||
get controls(): AgentOptionControl[] {
|
||||
return [
|
||||
{
|
||||
type: "select",
|
||||
key: "service_tier",
|
||||
label: "Speed",
|
||||
defaultValue: this.tier,
|
||||
choices: [
|
||||
{ value: "default", label: "Normal" },
|
||||
{ value: "priority", label: "Fast" },
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
class LiveClient extends OptionsClient {
|
||||
override async createSession(config: AgentSessionConfig): Promise<AgentSession> {
|
||||
return new LiveSession(config);
|
||||
}
|
||||
}
|
||||
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-options-"));
|
||||
const manager = new AgentManager({
|
||||
clients: { codex: new LiveClient() },
|
||||
registry: new AgentStorage(join(workdir, "agents"), logger),
|
||||
logger,
|
||||
});
|
||||
const created = await manager.createAgent(
|
||||
{ provider: "codex", cwd: workdir, providerOptions: { service_tier: "priority" } },
|
||||
undefined,
|
||||
{ workspaceId: undefined },
|
||||
);
|
||||
|
||||
await manager.setAgentOptions(created.id, { service_tier: null });
|
||||
|
||||
const agent = manager.getAgent(created.id)!;
|
||||
expect(agent.config.providerOptions).toBeUndefined();
|
||||
expect(toAgentPayload(agent).options).toEqual({ service_tier: "priority" });
|
||||
expect(toAgentPayload(agent).features).toEqual([
|
||||
expect.objectContaining({ id: "service_tier", value: "priority" }),
|
||||
]);
|
||||
});
|
||||
|
||||
test("an option without a control is settable through the patch and readable in the snapshot", async () => {
|
||||
const { manager, client, storage, agentId } = await createOptionsAgent();
|
||||
|
||||
|
||||
@@ -5274,7 +5274,17 @@ export class AgentManager {
|
||||
if ("session" in agent && agent.session?.controls) {
|
||||
agent.controls = agent.session.controls;
|
||||
}
|
||||
agent.effectiveOptions = this.effectiveOptionsFor(agent.provider, agent.config.providerOptions);
|
||||
const own = agent.config.providerOptions;
|
||||
const effective = { ...this.effectiveOptionsFor(agent.provider, own) };
|
||||
// After a removal the running provider keeps its setting, so a control it reports for a key
|
||||
// the agent no longer holds shows that live value rather than the `config.json` default.
|
||||
for (const control of agent.controls ?? []) {
|
||||
const live = control.defaultValue;
|
||||
if (own?.[control.key] === undefined && control.key in effective && live !== undefined) {
|
||||
effective[control.key] = live;
|
||||
}
|
||||
}
|
||||
agent.effectiveOptions = effective;
|
||||
}
|
||||
|
||||
private checkAndSetAttention(agent: ManagedAgent): void {
|
||||
|
||||
@@ -533,6 +533,32 @@ describe("Codex app-server provider controls", () => {
|
||||
await expect(session.applyOptions?.({ sandbox_mode: "read-only" })).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
test("applying unrelated options keeps the running sandbox", async () => {
|
||||
const { session, appServer } = await createConnectedSession({
|
||||
providerOptions: { sandbox_mode: "read-only" },
|
||||
});
|
||||
|
||||
await session.applyOptions?.({ plan_mode: true });
|
||||
await session.startTurn("hello");
|
||||
|
||||
await expect(appServer.waitForTurnStart()).resolves.toMatchObject({
|
||||
sandboxPolicy: { type: "readOnly" },
|
||||
});
|
||||
});
|
||||
|
||||
test("removing the sandbox option keeps the running sandbox", async () => {
|
||||
const { session, appServer } = await createConnectedSession({
|
||||
providerOptions: { sandbox_mode: "read-only" },
|
||||
});
|
||||
|
||||
await session.applyOptions?.({});
|
||||
await session.startTurn("hello");
|
||||
|
||||
await expect(appServer.waitForTurnStart()).resolves.toMatchObject({
|
||||
sandboxPolicy: { type: "readOnly" },
|
||||
});
|
||||
});
|
||||
|
||||
test("startTurn includes serviceTier when fast mode is enabled", async () => {
|
||||
const { session, appServer } = await createConnectedSession();
|
||||
|
||||
|
||||
@@ -4671,7 +4671,8 @@ export class CodexAppServerAgentSession implements AgentSession {
|
||||
`Codex speed '${String(requestedTier)}' is not available for model '${this.config.model ?? "default"}'`,
|
||||
);
|
||||
}
|
||||
this.providerOptions = next;
|
||||
// Supplied keys update the running options; absent keys keep their running value.
|
||||
this.providerOptions = { ...this.providerOptions, ...next };
|
||||
this.requestedServiceTier = requestedTier;
|
||||
this.setPlanMode(next.plan_mode === undefined ? this.planModeEnabled : next.plan_mode === true);
|
||||
this.cachedRuntimeInfo = null;
|
||||
|
||||
Reference in New Issue
Block a user