From af2fb8ab353e97e5410df46fe1b4f362446345b1 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 5 Nov 2026 12:36:47 +0100 Subject: [PATCH] fix(server): a closed agent's options include the provider defaults, like a live one's The stored snapshot projected only the options the agent owns, so an agent that ran with a config.json default (for example Claude fast_mode: true) showed the option as missing, and its legacy Fast control as off, until it was resumed. The projection takes the same resolver the live agent uses (the provider's defaults under the agent's own options). --- .../server/src/server/agent/agent-manager.ts | 6 +-- .../server/agent/agent-projections.test.ts | 37 ++++++++++++++++--- .../src/server/agent/agent-projections.ts | 18 ++++++--- .../server/src/server/agent/directory.test.ts | 3 +- .../finish-subscriptions.test.ts | 1 + .../internal/observation-stream.test.ts | 2 + .../internal/observation.ts | 15 +++++++- .../src/server/agent/tools/paseo-tools.ts | 2 + .../src/server/hub/daemon-executions.ts | 6 ++- packages/server/src/server/session.ts | 4 +- .../src/server/session.workspaces.test.ts | 3 +- .../src/server/test-utils/session-stubs.ts | 1 + .../src/server/workspace-directory.test.ts | 3 +- 13 files changed, 79 insertions(+), 22 deletions(-) diff --git a/packages/server/src/server/agent/agent-manager.ts b/packages/server/src/server/agent/agent-manager.ts index 03f6de1be8..b43b3cc259 100644 --- a/packages/server/src/server/agent/agent-manager.ts +++ b/packages/server/src/server/agent/agent-manager.ts @@ -2427,10 +2427,8 @@ export class AgentManager { return input.client.effectiveOptions?.(input.own) ?? input.own ?? {}; } - private effectiveOptionsFor( - provider: AgentProvider, - own: ProviderOptions | undefined, - ): ProviderOptions { + /** What an agent of this provider runs with, given its own options: the defaults under them. */ + effectiveOptionsFor(provider: AgentProvider, own: ProviderOptions | undefined): ProviderOptions { const client = this.clients.get(provider); return client ? this.launchOptionsFor({ client, own }) : (own ?? {}); } diff --git a/packages/server/src/server/agent/agent-projections.test.ts b/packages/server/src/server/agent/agent-projections.test.ts index 38ba8bf3d1..7f5e62dc85 100644 --- a/packages/server/src/server/agent/agent-projections.test.ts +++ b/packages/server/src/server/agent/agent-projections.test.ts @@ -17,6 +17,9 @@ import type { AgentSessionConfig, } from "./agent-sdk-types.js"; +/** A provider with no defaults of its own: the effective options are the agent's own. */ +const ownOptionsOnly = (_provider: string, own: Record | undefined) => own ?? {}; + type ManagedAgentOverrides = Omit, "config" | "pendingPermissions"> & { config?: Partial; pendingPermissions?: Map; @@ -381,7 +384,7 @@ describe("toAgentPayload", () => { }), ); - const payload = buildStoredAgentPayload(record, ["codex"]); + const payload = buildStoredAgentPayload(record, ["codex"], ownOptionsOnly); expect(record.persistence?.metadata).toEqual({ conversationId: "conversation-stored", @@ -422,7 +425,7 @@ describe("toAgentPayload", () => { }), ); - const payload = buildStoredAgentPayload(record, ["codex"]); + const payload = buildStoredAgentPayload(record, ["codex"], ownOptionsOnly); expect(payload.status).toBe("closed"); expect(payload.controls).toEqual(controls); @@ -441,15 +444,37 @@ describe("toAgentPayload", () => { ]); }); + it("projects a closed agent's options with the provider defaults under its own, like a live one", () => { + const record = toStoredAgentRecord( + createManagedAgent({ + provider: "claude", + lifecycle: "closed", + controls: [{ type: "toggle", key: "fast_mode", label: "Fast" }], + config: { provider: "claude", providerOptions: {} }, + }), + ); + const withConfigDefaults = (_provider: string, own: Record | undefined) => ({ + fast_mode: true, + ...own, + }); + + const payload = buildStoredAgentPayload(record, ["claude"], withConfigDefaults); + + expect(payload.options).toEqual({ fast_mode: true }); + expect(payload.features).toEqual([ + { type: "toggle", id: "fast_mode", label: "Fast", value: true }, + ]); + }); + it("carries a stored lastError onto the payload", () => { const record = toStoredAgentRecord( createManagedAgent({ provider: "codex", config: { provider: "codex" }, lastError: "boom" }), ); - expect(buildStoredAgentPayload(record, ["codex"]).lastError).toBe("boom"); - expect(buildStoredAgentPayload({ ...record, lastError: null }, ["codex"])).not.toHaveProperty( - "lastError", - ); + expect(buildStoredAgentPayload(record, ["codex"], ownOptionsOnly).lastError).toBe("boom"); + expect( + buildStoredAgentPayload({ ...record, lastError: null }, ["codex"], ownOptionsOnly), + ).not.toHaveProperty("lastError"); }); it("omits lastUsage when not available", () => { diff --git a/packages/server/src/server/agent/agent-projections.ts b/packages/server/src/server/agent/agent-projections.ts index 135a9fedf0..1f64b9965c 100644 --- a/packages/server/src/server/agent/agent-projections.ts +++ b/packages/server/src/server/agent/agent-projections.ts @@ -225,17 +225,24 @@ function visibilityFlags(agent: { }; } +/** The options an agent runs with, given the options it owns; see `toAgentPayload`. */ +export type EffectiveOptionsResolver = ( + provider: AgentProvider, + own: ProviderOptions | undefined, +) => ProviderOptions; + function buildStoredOptionControls( record: StoredAgentRecord, + effectiveOptionsFor: EffectiveOptionsResolver, ): Pick { const controls = normalizeControls(record.controls); - const options = record.config?.providerOptions - ? sanitizeEffectiveOptions(record.config.providerOptions) - : undefined; + const options = sanitizeEffectiveOptions( + effectiveOptionsFor(record.provider, record.config?.providerOptions ?? undefined), + ); return { // COMPAT(featureValues): added in v0.15.0, remove after 2027-05-01. features: featuresFromControls({ controls, options }), - ...(options ? { options } : {}), + options, controls, }; } @@ -243,6 +250,7 @@ function buildStoredOptionControls( export function buildStoredAgentPayload( record: StoredAgentRecord, validProviders: Iterable, + effectiveOptionsFor: EffectiveOptionsResolver, ): AgentSnapshotPayload { const defaultCapabilities = { supportsStreaming: false, @@ -261,7 +269,7 @@ export function buildStoredAgentPayload( const lastUserMessageAt = record.lastUserMessageAt ? new Date(record.lastUserMessageAt) : null; const runtimeInfo = buildStoredRuntimeInfo(record); - const optionControls = buildStoredOptionControls(record); + const optionControls = buildStoredOptionControls(record, effectiveOptionsFor); const providerAvailable = isStoredAgentProviderAvailable(record, validProviders); const persistence = projectPersistenceHandleForWire( buildStoredPersistenceHandle(record, validProviders), diff --git a/packages/server/src/server/agent/directory.test.ts b/packages/server/src/server/agent/directory.test.ts index e5767de4f7..647db9aef9 100644 --- a/packages/server/src/server/agent/directory.test.ts +++ b/packages/server/src/server/agent/directory.test.ts @@ -55,7 +55,8 @@ beforeEach(async () => { projectLive: () => { throw new Error("No live agents in this suite"); }, - projectStored: (record) => buildStoredAgentPayload(record, ["codex"]), + projectStored: (record) => + buildStoredAgentPayload(record, ["codex"], (_provider, own) => own ?? {}), isProviderVisible: () => true, isStoredProviderAvailable: () => true, }); diff --git a/packages/server/src/server/agent/finish-subscriptions/finish-subscriptions.test.ts b/packages/server/src/server/agent/finish-subscriptions/finish-subscriptions.test.ts index 323e4be48f..9e18f87948 100644 --- a/packages/server/src/server/agent/finish-subscriptions/finish-subscriptions.test.ts +++ b/packages/server/src/server/agent/finish-subscriptions/finish-subscriptions.test.ts @@ -123,6 +123,7 @@ function outcome(status: "idle" | "running" | "closed" | "error" = "closed"): Wa lastStatus: status, }, ["codex"], + (_provider, own) => own ?? {}, ), lastMessage: "Partial work saved", error: null, diff --git a/packages/server/src/server/agent/finish-subscriptions/internal/observation-stream.test.ts b/packages/server/src/server/agent/finish-subscriptions/internal/observation-stream.test.ts index 1d15a890df..5bc0b3f7d7 100644 --- a/packages/server/src/server/agent/finish-subscriptions/internal/observation-stream.test.ts +++ b/packages/server/src/server/agent/finish-subscriptions/internal/observation-stream.test.ts @@ -43,6 +43,7 @@ class Host implements HostObservation { updatedAt: "2026-01-01", }, ["codex"], + (_provider, own) => own ?? {}, ), event, "Last response", @@ -133,6 +134,7 @@ test("late initial permission snapshot cannot resurrect an already resolved requ updatedAt: "2026-01-01", }, ["codex"], + (_provider, own) => own ?? {}, ); if (requestEvent.type !== "permission_requested") throw new Error("Expected permission"); final.pendingPermissions = [requestEvent.request]; diff --git a/packages/server/src/server/agent/finish-subscriptions/internal/observation.ts b/packages/server/src/server/agent/finish-subscriptions/internal/observation.ts index 7abe483cf4..bfe77db3f2 100644 --- a/packages/server/src/server/agent/finish-subscriptions/internal/observation.ts +++ b/packages/server/src/server/agent/finish-subscriptions/internal/observation.ts @@ -34,7 +34,11 @@ export function createFinishObservation(input: { const stored = await agentStorage.get(agentId); return { status: stored?.lastStatus === "error" ? "error" : "idle", - final: stored ? buildStoredAgentPayload(stored, [stored.provider]) : null, + final: stored + ? buildStoredAgentPayload(stored, [stored.provider], (provider, own) => + agentManager.effectiveOptionsFor(provider, own), + ) + : null, lastMessage: await agentManager.getLastAssistantMessage(agentId), error: stored?.lastError ?? null, }; @@ -93,7 +97,14 @@ export function createFinishObservation(input: { type: "turn_started", provider: current.provider, }); - else publish(stored ? buildStoredAgentPayload(stored, [stored.provider]) : null); + else + publish( + stored + ? buildStoredAgentPayload(stored, [stored.provider], (provider, own) => + agentManager.effectiveOptionsFor(provider, own), + ) + : null, + ); const stop = () => unsubscribe(); signal.addEventListener("abort", stop, { once: true }); return async () => { diff --git a/packages/server/src/server/agent/tools/paseo-tools.ts b/packages/server/src/server/agent/tools/paseo-tools.ts index 1ac0882d82..53c2e90d06 100644 --- a/packages/server/src/server/agent/tools/paseo-tools.ts +++ b/packages/server/src/server/agent/tools/paseo-tools.ts @@ -2133,6 +2133,7 @@ function createToolCatalog( const structuredSnapshot = buildStoredAgentPayload( record, new Set(providerSnapshotManager.listRegisteredProviderIds()), + (provider, own) => agentManager.effectiveOptionsFor(provider, own), ); return { content: [], @@ -2188,6 +2189,7 @@ function createToolCatalog( buildStoredAgentPayload( record, new Set(providerSnapshotManager.listRegisteredProviderIds()), + (provider, own) => agentManager.effectiveOptionsFor(provider, own), ), isProviderVisible: () => true, isStoredProviderAvailable: (record) => diff --git a/packages/server/src/server/hub/daemon-executions.ts b/packages/server/src/server/hub/daemon-executions.ts index c55b6e3258..029e2090c7 100644 --- a/packages/server/src/server/hub/daemon-executions.ts +++ b/packages/server/src/server/hub/daemon-executions.ts @@ -288,7 +288,11 @@ export class DaemonExecutions implements HubExecutionAgents { agent: live ? serializeAgentSnapshot(live) : { - ...buildStoredAgentPayload(record, this.agentManager.getRegisteredProviderIds()), + ...buildStoredAgentPayload( + record, + this.agentManager.getRegisteredProviderIds(), + (provider, own) => this.agentManager.effectiveOptionsFor(provider, own), + ), status: "closed", }, }; diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index b31a55210c..e1b5d61a23 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -2183,7 +2183,9 @@ export class Session { record: StoredAgentRecord, registeredProviderIds = new Set(this.providerSnapshotManager.listRegisteredProviderIds()), ): AgentSnapshotPayload { - return buildStoredAgentPayload(record, registeredProviderIds); + return buildStoredAgentPayload(record, registeredProviderIds, (provider, own) => + this.agentManager.effectiveOptionsFor(provider, own), + ); } private isProviderVisibleToClient(provider: string): boolean { diff --git a/packages/server/src/server/session.workspaces.test.ts b/packages/server/src/server/session.workspaces.test.ts index 6b1db34449..caff1b1b3d 100644 --- a/packages/server/src/server/session.workspaces.test.ts +++ b/packages/server/src/server/session.workspaces.test.ts @@ -2097,7 +2097,8 @@ test("workspace mark unread selects a finished cross-workspace child with scoped projectLive: async () => { throw new Error("No live agents"); }, - projectStored: (record) => buildStoredAgentPayload(record, ["codex"]), + projectStored: (record) => + buildStoredAgentPayload(record, ["codex"], (_provider, own) => own ?? {}), isProviderVisible: () => true, isStoredProviderAvailable: () => true, }); diff --git a/packages/server/src/server/test-utils/session-stubs.ts b/packages/server/src/server/test-utils/session-stubs.ts index a6ceb24ed8..ca702dc36d 100644 --- a/packages/server/src/server/test-utils/session-stubs.ts +++ b/packages/server/src/server/test-utils/session-stubs.ts @@ -37,6 +37,7 @@ export function asAgentManager(stub: { return createStub({ listProviderSubagentActivity: () => [], getRetiredInternalAgent: () => null, + effectiveOptionsFor: (_provider: string, own?: Record) => own ?? {}, ...stub, }); } diff --git a/packages/server/src/server/workspace-directory.test.ts b/packages/server/src/server/workspace-directory.test.ts index ab2ead15aa..e291007db1 100644 --- a/packages/server/src/server/workspace-directory.test.ts +++ b/packages/server/src/server/workspace-directory.test.ts @@ -921,7 +921,8 @@ test("scoped descriptor builds allocate independently of archived history", asyn projectLive: async () => { throw new Error("Unexpected live projection"); }, - projectStored: (record) => buildStoredAgentPayload(record, ["codex"]), + projectStored: (record) => + buildStoredAgentPayload(record, ["codex"], (_provider, own) => own ?? {}), isProviderVisible: () => true, isStoredProviderAvailable: () => true, });