mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-02 22:52:58 +00:00
fix(server): opening snapshots keep the defaults the session launched with, and config-change pushes carry legacy profile values
An opening resolved defaults once for the session and again at registration; an edit in between showed options the session never received. Create, resume and reload now resolve once, before the provider opens, and registration starts from that. The daemon_config_changed push skipped the featureValues projection that config replies apply, so the unchanged app lost a profile's legacy values when any setting changed.
This commit is contained in:
@@ -12895,6 +12895,31 @@ describe("agent options", () => {
|
||||
expect(published.every((options) => options?.service_tier === "slow")).toBe(true);
|
||||
});
|
||||
|
||||
test("a default edited while the session is opening does not change the opening snapshot", async () => {
|
||||
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-options-"));
|
||||
const client = new OptionsClient();
|
||||
let tier = "default";
|
||||
client.effectiveOptions = (own) => ({ service_tier: tier, ...own });
|
||||
const open = client.createSession.bind(client);
|
||||
client.createSession = async (config) => {
|
||||
const session = await open(config);
|
||||
// The user edits the default in config.json after the provider has launched.
|
||||
tier = "fast";
|
||||
return session;
|
||||
};
|
||||
const manager = new AgentManager({
|
||||
clients: { codex: client },
|
||||
registry: new AgentStorage(join(workdir, "agents"), logger),
|
||||
logger,
|
||||
});
|
||||
|
||||
const agent = await manager.createAgent({ provider: "codex", cwd: workdir }, undefined, {
|
||||
workspaceId: undefined,
|
||||
});
|
||||
|
||||
expect(agent.effectiveOptions).toEqual({ service_tier: "default" });
|
||||
});
|
||||
|
||||
test("an option the session dropped when opening does not come back from a configured default", async () => {
|
||||
const { manager, agentId } = await createOptionsAgent(undefined, { service_tier: null });
|
||||
|
||||
|
||||
@@ -1411,9 +1411,11 @@ export class AgentManager {
|
||||
);
|
||||
const providerLaunchConfig = this.resolveProviderLaunchConfig(launchConfig, launchContext);
|
||||
const createOptions = this.buildCreateSessionOptions(options);
|
||||
const launchedOptions = this.launchOptionsFor(client, storedConfig.providerOptions);
|
||||
const session = await client.createSession(providerLaunchConfig, launchContext, createOptions);
|
||||
await this.requireExternalMcpSupport(session, providerLaunchConfig, launchContext);
|
||||
const agent = await this.registerSession(session, storedConfig, resolvedAgentId, {
|
||||
launchedOptions,
|
||||
labels: options.labels,
|
||||
initialTitle: options.initialTitle,
|
||||
workspaceId: options.workspaceId,
|
||||
@@ -1536,6 +1538,7 @@ export class AgentManager {
|
||||
},
|
||||
);
|
||||
const providerLaunchConfig = this.resolveProviderLaunchConfig(launchConfig, launchContext);
|
||||
const launchedOptions = this.launchOptionsFor(client, storedConfig.providerOptions);
|
||||
const session = await client.resumeSession(
|
||||
handle,
|
||||
providerLaunchConfig,
|
||||
@@ -1544,6 +1547,7 @@ export class AgentManager {
|
||||
);
|
||||
await this.requireExternalMcpSupport(session, providerLaunchConfig, launchContext);
|
||||
return this.registerSession(session, storedConfig, resolvedAgentId, {
|
||||
launchedOptions,
|
||||
...options,
|
||||
launchEnv,
|
||||
persistence: handle,
|
||||
@@ -1722,6 +1726,7 @@ export class AgentManager {
|
||||
this.assertAcceptingAgentRegistrations();
|
||||
|
||||
this.paseoToolPolicies.set(agentId, paseoToolPolicy);
|
||||
const launchedOptions = this.launchOptionsFor(client, storedConfig.providerOptions);
|
||||
session = handle
|
||||
? await client.resumeSession(handle, providerLaunchConfig, launchContext)
|
||||
: await client.createSession(providerLaunchConfig, launchContext);
|
||||
@@ -1740,6 +1745,7 @@ export class AgentManager {
|
||||
// Preserve existing labels and timeline during reload.
|
||||
handedToRegistration = true;
|
||||
return this.registerSession(session, storedConfig, agentId, {
|
||||
launchedOptions,
|
||||
labels: existing.labels,
|
||||
workspaceId: existing.workspaceId,
|
||||
owner: existing.owner,
|
||||
@@ -2338,11 +2344,35 @@ export class AgentManager {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Defaults merge into what the session was given; what it reports it ended up with comes
|
||||
* after, so an option it dropped does not come back from a default.
|
||||
*/
|
||||
private openedEffectiveOptions(input: {
|
||||
session: AgentSession;
|
||||
openedConfig: AgentSessionConfig;
|
||||
launchedOptions: ProviderOptions | undefined;
|
||||
}): ProviderOptions {
|
||||
const { session, openedConfig, launchedOptions } = input;
|
||||
return applyProviderOptionsPatch({
|
||||
options:
|
||||
launchedOptions ??
|
||||
this.effectiveOptionsFor(openedConfig.provider, openedConfig.providerOptions),
|
||||
patch: session.openOptionsPatch ?? {},
|
||||
});
|
||||
}
|
||||
|
||||
/** The options a client opens with: its own defaults merged under the agent's. */
|
||||
private launchOptionsFor(client: AgentClient, own: ProviderOptions | undefined): ProviderOptions {
|
||||
return client.effectiveOptions?.(own) ?? own ?? {};
|
||||
}
|
||||
|
||||
private effectiveOptionsFor(
|
||||
provider: AgentProvider,
|
||||
own: ProviderOptions | undefined,
|
||||
): ProviderOptions {
|
||||
return this.clients.get(provider)?.effectiveOptions?.(own) ?? own ?? {};
|
||||
const client = this.clients.get(provider);
|
||||
return client ? this.launchOptionsFor(client, own) : (own ?? {});
|
||||
}
|
||||
|
||||
async setTitle(agentId: string, title: string): Promise<void> {
|
||||
@@ -3877,6 +3907,12 @@ export class AgentManager {
|
||||
* attention come from what was already recorded, and installing the session is not
|
||||
* activity in it.
|
||||
*/
|
||||
/**
|
||||
* The effective options the opening resolved before the session started. Registration
|
||||
* starts from these rather than reading the defaults again, which may have been edited
|
||||
* while the provider was opening.
|
||||
*/
|
||||
launchedOptions?: ProviderOptions;
|
||||
restoring?: boolean;
|
||||
initialTitle?: string | null;
|
||||
publishWhenReady?: boolean;
|
||||
@@ -3910,11 +3946,10 @@ export class AgentManager {
|
||||
resolvedAgentId,
|
||||
session,
|
||||
config,
|
||||
// Defaults merge into what the session was given; what it reports it ended up with comes
|
||||
// after, so an option it dropped does not come back from a default.
|
||||
effectiveOptions: applyProviderOptionsPatch({
|
||||
options: this.effectiveOptionsFor(openedConfig.provider, openedConfig.providerOptions),
|
||||
patch: session.openOptionsPatch ?? {},
|
||||
effectiveOptions: this.openedEffectiveOptions({
|
||||
session,
|
||||
openedConfig,
|
||||
launchedOptions: options?.launchedOptions,
|
||||
}),
|
||||
now,
|
||||
durableTimelineHasRows,
|
||||
|
||||
@@ -985,6 +985,45 @@ test("config changes require their own event demand and cannot escape to an idle
|
||||
}
|
||||
});
|
||||
|
||||
test("a config change push carries profiles with the legacy featureValues the unchanged app reads", async () => {
|
||||
const daemon = await createTestPaseoDaemon({ mcpEnabled: false });
|
||||
const admin = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws`, appVersion: "0.8.0" });
|
||||
let observer: SubscriptionPeer | undefined;
|
||||
try {
|
||||
await admin.connect();
|
||||
await admin.patchDaemonConfig({
|
||||
agentProfiles: [
|
||||
{ id: "p", name: "P", provider: "codex", options: {}, featureValues: { plan_mode: true } },
|
||||
],
|
||||
});
|
||||
observer = await SubscriptionPeer.connect(daemon.port, "config-push-session");
|
||||
await observer.request({
|
||||
type: "session.events.set_subscription.request",
|
||||
requestId: "config-feed",
|
||||
events: ["status.daemon_config_changed"],
|
||||
});
|
||||
|
||||
await admin.patchDaemonConfig({ autoArchiveAfterMerge: true });
|
||||
|
||||
const peer = observer;
|
||||
await expect
|
||||
.poll(() =>
|
||||
peer.frames.flatMap((frame) =>
|
||||
frame.type === "session" &&
|
||||
frame.message.type === "status" &&
|
||||
frame.message.payload.status === "daemon_config_changed"
|
||||
? [frame.message.payload.config?.agentProfiles?.[0]?.featureValues]
|
||||
: [],
|
||||
),
|
||||
)
|
||||
.toEqual([{ plan_mode: true }]);
|
||||
} finally {
|
||||
observer?.close();
|
||||
await admin.close();
|
||||
await daemon.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("status-shaped operation replies use actual source request provenance", async () => {
|
||||
const daemon = await createTestPaseoDaemon({ mcpEnabled: false });
|
||||
const peers: SubscriptionPeer[] = [];
|
||||
|
||||
@@ -34,7 +34,11 @@ import type { ProjectRegistry, WorkspaceRegistry } from "./workspace-registry.js
|
||||
import type { ProjectUpdate } from "./workspace-reconciliation-service.js";
|
||||
import type { ScheduleService } from "./schedule/service.js";
|
||||
import type { CheckoutDiffManager, CheckoutDiffMetrics } from "./checkout-diff-manager.js";
|
||||
import type { DaemonConfigStore, MutableDaemonConfig } from "./daemon-config-store.js";
|
||||
import {
|
||||
toWireDaemonConfig,
|
||||
type DaemonConfigStore,
|
||||
type MutableDaemonConfig,
|
||||
} from "./daemon-config-store.js";
|
||||
import {
|
||||
type ServerInfoStatusPayload,
|
||||
type ScriptStatusUpdateMessage,
|
||||
@@ -2197,7 +2201,7 @@ export class VoiceAssistantWebSocketServer {
|
||||
type: "status",
|
||||
payload: {
|
||||
status: "daemon_config_changed",
|
||||
config,
|
||||
config: toWireDaemonConfig(config),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user