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).
This commit is contained in:
Mohamed Boudra
2026-11-05 15:30:11 +01:00
parent cdc64ecfa4
commit af2fb8ab35
13 changed files with 79 additions and 22 deletions
@@ -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 ?? {});
}
@@ -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<string, unknown> | undefined) => own ?? {};
type ManagedAgentOverrides = Omit<Partial<ManagedAgent>, "config" | "pendingPermissions"> & {
config?: Partial<AgentSessionConfig>;
pendingPermissions?: Map<string, AgentPermissionRequest>;
@@ -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<string, unknown> | 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", () => {
@@ -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<AgentSnapshotPayload, "features" | "options" | "controls"> {
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<AgentProvider>,
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),
@@ -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,
});
@@ -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,
@@ -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];
@@ -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 () => {
@@ -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) =>
@@ -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",
},
};
+3 -1
View File
@@ -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 {
@@ -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,
});
@@ -37,6 +37,7 @@ export function asAgentManager(stub: {
return createStub<SessionOptions["agentManager"]>({
listProviderSubagentActivity: () => [],
getRetiredInternalAgent: () => null,
effectiveOptionsFor: (_provider: string, own?: Record<string, unknown>) => own ?? {},
...stub,
});
}
@@ -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,
});