From 22fadbb217b3e70ae790c35e35c8d6d032d2b8ef Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sun, 8 Nov 2026 12:41:49 +0100 Subject: [PATCH] Run option, model, thinking and mode changes in the agent's lifecycle lane The agent manager owns all sequencing for one agent: applyOptions, setModel, setThinkingOption and setMode now run in the same lane as open, close, archive, labels and metadata, so a provider session never sees two of them at once. Provider-side serialization (the applying chains in the ACP, plugin and OpenCode sessions, and Claude's apply-over-current-options handling) is deleted. --- .../src/server/agent/agent-manager.test.ts | 16 +- .../server/src/server/agent/agent-manager.ts | 197 ++++++++++-------- .../src/server/agent/plugin-provider.test.ts | 46 +--- .../src/server/agent/plugin-provider.ts | 11 +- .../server/agent/providers/acp-agent.test.ts | 13 -- .../src/server/agent/providers/acp-agent.ts | 11 +- .../server/agent/providers/claude/agent.ts | 9 +- .../agent/providers/opencode/v2/agent.test.ts | 41 ---- .../agent/providers/opencode/v2/session.ts | 10 +- .../providers/test-utils/fake-acp-agent.mjs | 11 - 10 files changed, 126 insertions(+), 239 deletions(-) diff --git a/packages/server/src/server/agent/agent-manager.test.ts b/packages/server/src/server/agent/agent-manager.test.ts index 8508602ba0..78506ed9bd 100644 --- a/packages/server/src/server/agent/agent-manager.test.ts +++ b/packages/server/src/server/agent/agent-manager.test.ts @@ -13449,6 +13449,7 @@ describe("agent options", () => { class OptionsSession extends TestAgentSession { readonly applied: ProviderOptions[] = []; rejectWith: Error | null = null; + /** Holds the next apply only; the one after it is answered at once. */ applyGate: Promise | null = null; /** What the session says it ended up with after the next apply. */ ended: Record = {}; @@ -13456,7 +13457,9 @@ describe("agent options", () => { async applyOptions(options: ProviderOptions): Promise> { if (this.rejectWith) throw this.rejectWith; - await this.applyGate; + const gate = this.applyGate; + this.applyGate = null; + await gate; this.applied.push(options); return this.ended; } @@ -13681,7 +13684,7 @@ describe("agent options", () => { }); }); - test("concurrent patches both land: each is applied to the options as they are when its provider call resolves", async () => { + test("overlapping patches reach the provider one after the other, and both land", async () => { const { manager, client, agentId } = await createOptionsAgent(); let release!: () => void; client.session!.applyGate = new Promise((resolve) => { @@ -13689,11 +13692,14 @@ describe("agent options", () => { }); const slow = manager.setAgentOptions(agentId, { sandbox_mode: "read-only" }); - client.session!.applyGate = null; - await manager.setAgentOptions(agentId, { web_search: true }); + const next = manager.setAgentOptions(agentId, { web_search: true }); + await new Promise((resolve) => setTimeout(resolve, 20)); + // The second patch waits for the first to be answered: the provider never sees two at once. + expect(client.session?.applied).toEqual([]); release(); - await slow; + await Promise.all([slow, next]); + expect(client.session?.applied).toEqual([{ sandbox_mode: "read-only" }, { web_search: true }]); expect(manager.getAgent(agentId)?.config.providerOptions).toEqual({ sandbox_mode: "read-only", web_search: true, diff --git a/packages/server/src/server/agent/agent-manager.ts b/packages/server/src/server/agent/agent-manager.ts index 1359f1ae8b..db52096664 100644 --- a/packages/server/src/server/agent/agent-manager.ts +++ b/packages/server/src/server/agent/agent-manager.ts @@ -2363,111 +2363,123 @@ export class AgentManager { }); } + // Mode, model, thinking and options change inside the agent's lifecycle lane, like every other + // mutation of one agent. A provider session therefore never has two of `setMode`, `setModel`, + // `setThinkingOption` and `applyOptions` in flight at once, and none of them overlaps an open, + // reload or close; providers do no sequencing of their own. + async setAgentMode(agentId: string, modeId: string): Promise { - const agent = await this.ensureInteractiveSession(agentId); - const notice = (await agent.session.setMode(modeId)) ?? null; - await this.drainSessionEvents(agentId); - const currentMode = (await agent.session.getCurrentMode()) ?? modeId; - agent.config.modeId = currentMode ?? undefined; - agent.currentModeId = currentMode; - // Update runtimeInfo to reflect the new mode - if (agent.runtimeInfo) { - agent.runtimeInfo = { ...agent.runtimeInfo, modeId: currentMode }; - } - this.touchUpdatedAt(agent); - this.emitState(agent); - return notice; + return this.runLifecycleMutation(agentId, async () => { + const agent = await this.ensureInteractiveSessionUnlocked(agentId); + const notice = (await agent.session.setMode(modeId)) ?? null; + await this.drainSessionEvents(agentId); + const currentMode = (await agent.session.getCurrentMode()) ?? modeId; + agent.config.modeId = currentMode ?? undefined; + agent.currentModeId = currentMode; + // Update runtimeInfo to reflect the new mode + if (agent.runtimeInfo) { + agent.runtimeInfo = { ...agent.runtimeInfo, modeId: currentMode }; + } + this.touchUpdatedAt(agent); + this.emitState(agent); + return notice; + }); } async setAgentModel(agentId: string, modelId: string | null): Promise { - const agent = await this.ensureInteractiveSession(agentId); - const normalizedModelId = - typeof modelId === "string" && modelId.trim().length > 0 ? modelId : null; + return this.runLifecycleMutation(agentId, async () => { + const agent = await this.ensureInteractiveSessionUnlocked(agentId); + const normalizedModelId = + typeof modelId === "string" && modelId.trim().length > 0 ? modelId : null; - const selection = await agent.session.setModel?.(normalizedModelId); - await this.drainSessionEvents(agentId); + const selection = await agent.session.setModel?.(normalizedModelId); + await this.drainSessionEvents(agentId); - // A reset clears the preference even when the provider retains its active model. - let effectiveModelId = normalizedModelId; - if (normalizedModelId !== null && selection) effectiveModelId = selection.model; + // A reset clears the preference even when the provider retains its active model. + let effectiveModelId = normalizedModelId; + if (normalizedModelId !== null && selection) effectiveModelId = selection.model; - agent.config.model = effectiveModelId ?? undefined; - if (agent.runtimeInfo) { - agent.runtimeInfo = { ...agent.runtimeInfo, model: effectiveModelId }; - } - this.refreshSessionPersistence(agent); - this.touchUpdatedAt(agent); - this.emitState(agent); + agent.config.model = effectiveModelId ?? undefined; + if (agent.runtimeInfo) { + agent.runtimeInfo = { ...agent.runtimeInfo, model: effectiveModelId }; + } + this.refreshSessionPersistence(agent); + this.touchUpdatedAt(agent); + this.emitState(agent); + }); } async setAgentThinkingOption( agentId: string, thinkingOptionId: string | null, ): Promise { - const agent = await this.ensureInteractiveSession(agentId); - const normalizedThinkingOptionId = - typeof thinkingOptionId === "string" && thinkingOptionId.trim().length > 0 - ? thinkingOptionId - : null; + return this.runLifecycleMutation(agentId, async () => { + const agent = await this.ensureInteractiveSessionUnlocked(agentId); + const normalizedThinkingOptionId = + typeof thinkingOptionId === "string" && thinkingOptionId.trim().length > 0 + ? thinkingOptionId + : null; - let notice: AgentProviderNotice | null = null; - if (agent.session.setThinkingOption) { - notice = (await agent.session.setThinkingOption(normalizedThinkingOptionId)) ?? null; - } - await this.drainSessionEvents(agentId); + let notice: AgentProviderNotice | null = null; + if (agent.session.setThinkingOption) { + notice = (await agent.session.setThinkingOption(normalizedThinkingOptionId)) ?? null; + } + await this.drainSessionEvents(agentId); - let effectiveThinkingOptionId = normalizedThinkingOptionId; - const runtimeInfo = await agent.session.getRuntimeInfo(); - if (runtimeInfo.thinkingOptionId !== undefined) { - effectiveThinkingOptionId = runtimeInfo.thinkingOptionId; - } + let effectiveThinkingOptionId = normalizedThinkingOptionId; + const runtimeInfo = await agent.session.getRuntimeInfo(); + if (runtimeInfo.thinkingOptionId !== undefined) { + effectiveThinkingOptionId = runtimeInfo.thinkingOptionId; + } - agent.config.thinkingOptionId = effectiveThinkingOptionId ?? undefined; - if (agent.runtimeInfo) { - agent.runtimeInfo = { - ...agent.runtimeInfo, - thinkingOptionId: effectiveThinkingOptionId, - }; - } - this.touchUpdatedAt(agent); - this.emitState(agent); - return notice; + agent.config.thinkingOptionId = effectiveThinkingOptionId ?? undefined; + if (agent.runtimeInfo) { + agent.runtimeInfo = { + ...agent.runtimeInfo, + thinkingOptionId: effectiveThinkingOptionId, + }; + } + this.touchUpdatedAt(agent); + this.emitState(agent); + return notice; + }); } /** * Applies an options patch to a running agent (see `AgentSession.applyOptions`). The provider * gets each key the patch touches, resolved against the `config.json` defaults, with `null` * where nothing is set. The patch and whatever the provider says it ended up with are stored in - * one step once the provider accepts, over the options as they are at that moment, so concurrent - * writes to one agent resolve by arrival order. + * one step once the provider accepts. Patches to one agent run one at a time, in arrival order. */ async setAgentOptions(agentId: string, requested: ProviderOptionsPatch): Promise { - const agent = await this.ensureInteractiveSession(agentId); - const patch = this.convertLegacyOptions(agent.provider, requested); - let ended: ProviderOptionsPatch = {}; - let sent: ProviderOptionsPatch = {}; - if (Object.keys(patch).length > 0) { - // A provider that cannot apply options to a running agent must not store them either. - if (!agent.session.applyOptions) { - throw new ProviderOptionsValidationError( - agent.provider, - Object.keys(patch).map((key) => ({ - path: [key], - message: "cannot change on a running session; it applies when the agent is opened", - })), - ); + return this.runLifecycleMutation(agentId, async () => { + const agent = await this.ensureInteractiveSessionUnlocked(agentId); + const patch = this.convertLegacyOptions(agent.provider, requested); + let ended: ProviderOptionsPatch = {}; + let sent: ProviderOptionsPatch = {}; + if (Object.keys(patch).length > 0) { + // A provider that cannot apply options to a running agent must not store them either. + if (!agent.session.applyOptions) { + throw new ProviderOptionsValidationError( + agent.provider, + Object.keys(patch).map((key) => ({ + path: [key], + message: "cannot change on a running session; it applies when the agent is opened", + })), + ); + } + const set = Object.fromEntries(Object.entries(patch).filter(([, value]) => value != null)); + const resolved = this.effectiveOptionsFor(agent.provider, set); + sent = Object.fromEntries(Object.keys(patch).map((key) => [key, resolved[key] ?? null])); + ended = (await agent.session.applyOptions(sent)) ?? {}; + // Changes the provider made on its own while applying are older than its final answer. + await this.drainSessionEvents(agentId); } - const set = Object.fromEntries(Object.entries(patch).filter(([, value]) => value != null)); - const resolved = this.effectiveOptionsFor(agent.provider, set); - sent = Object.fromEntries(Object.keys(patch).map((key) => [key, resolved[key] ?? null])); - ended = (await agent.session.applyOptions(sent)) ?? {}; - // Changes the provider made on its own while applying are older than its final answer. - await this.drainSessionEvents(agentId); - } - this.storeOptionsPatch({ agent, patch: { ...patch, ...ended } }); - this.updateEffectiveOptions({ agent, sent, reported: ended }); - this.touchUpdatedAt(agent); - this.emitState(agent); + this.storeOptionsPatch({ agent, patch: { ...patch, ...ended } }); + this.updateEffectiveOptions({ agent, sent, reported: ended }); + this.touchUpdatedAt(agent); + this.emitState(agent); + }); } /** The config as the opened session left it: its own difference from the options it was given. */ @@ -3061,22 +3073,25 @@ export class AgentManager { private async ensureInteractiveSession(agentId: string): Promise { const agent = this.requireSessionAgent(agentId); if (this.historySessions.has(agent.session)) { - await this.trackAgentRegistrationOperation( - this.runLifecycleMutation(agentId, async () => { - const record = await this.registry?.get(agentId); - if (record?.archivedAt) throw new Error(`Agent is archived: ${agentId}`); - const current = this.requireSessionAgent(agentId); - if (this.historySessions.has(current.session)) { - // Recheck inside the lane: another prompt may already have upgraded it. - // Reload validates the directory before releasing readable history. - await this.reloadAgentSessionInternal(agentId); - } - }), + await this.runLifecycleMutation(agentId, () => + this.ensureInteractiveSessionUnlocked(agentId), ); } return this.requireSessionAgent(agentId); } + /** Inside the agent's lane: a history session is upgraded before anything is applied to it. */ + private async ensureInteractiveSessionUnlocked(agentId: string): Promise { + const agent = this.requireSessionAgent(agentId); + if (this.historySessions.has(agent.session)) { + const record = await this.registry?.get(agentId); + if (record?.archivedAt) throw new Error(`Agent is archived: ${agentId}`); + // Reload validates the directory before releasing readable history. + await this.trackAgentRegistrationOperation(this.reloadAgentSessionInternal(agentId)); + } + return this.requireSessionAgent(agentId); + } + private async *streamAfterHistoryLoad( agentId: string, prompt: AgentPromptInput, diff --git a/packages/server/src/server/agent/plugin-provider.test.ts b/packages/server/src/server/agent/plugin-provider.test.ts index 610e9545aa..9820acf2e0 100644 --- a/packages/server/src/server/agent/plugin-provider.test.ts +++ b/packages/server/src/server/agent/plugin-provider.test.ts @@ -58,11 +58,6 @@ interface ProviderHarnessOptions { handleInput?: (input: ProviderInput, emit: (event: ProviderEvent) => void) => Promise; } -/** Long enough for a request that is free to run to have run. */ -function pause(): Promise { - return new Promise((resolve) => setTimeout(resolve, 20)); -} - function controlKey(control: { key: string }): string { return control.key; } @@ -1157,10 +1152,7 @@ describe("PluginAgentClientRegistry", () => { // A plugin that keeps `voice` on "direct" whatever it is asked, and takes `loud` as given. async function openStubbornVoiceSession( given: Record, - { - refuseAfterChanging = false, - holdLoudChange, - }: { refuseAfterChanging?: boolean; holdLoudChange?: Promise } = {}, + { refuseAfterChanging = false }: { refuseAfterChanging?: boolean } = {}, ) { const state: { voice: string; loud: boolean; tokenBudget?: number } = { voice: "direct", @@ -1259,16 +1251,6 @@ describe("PluginAgentClientRegistry", () => { // Going loud returns an unrequested token budget to its default, by leaving it out. if (loud === true && tokenBudget === undefined) state.tokenBudget = undefined; emitConfig(emit, input.sessionId); - // A change that has taken effect but whose request is still pending. - if (typeof loud === "boolean" && holdLoudChange) { - await holdLoudChange; - emit({ - type: "request.failed", - requestId: input.requestId, - error: { message: "no" }, - }); - return true; - } emit( refuseAfterChanging ? { type: "request.failed", requestId: input.requestId, error: { message: "no" } } @@ -1358,32 +1340,6 @@ describe("PluginAgentClientRegistry", () => { await session.close(); }); - test("overlapping applies resolve one at a time: a request that fails after its change took still reports it", async () => { - let release: () => void = () => undefined; - const holdLoudChange = new Promise((resolve) => { - release = resolve; - }); - const { harness, session, events } = await openStubbornVoiceSession({}, { holdLoudChange }); - - const first = session.applyOptions?.({ loud: true }); - const firstFailed = expect(first).rejects.toThrow("no"); - await pause(); - expect(lastConfigure(harness.inputs)).toMatchObject({ - changes: { providerOptions: { loud: true } }, - }); - const second = session.applyOptions?.({ voice: "other" }); - // The second request does not start until the first has ended, so it cannot settle first. - await pause(); - release(); - - await firstFailed; - await expect(second).resolves.toEqual({ voice: "direct" }); - expect(eventsOfType(events, "options_changed")).toEqual([ - { type: "options_changed", provider: "plugin-direct", patch: { loud: true } }, - ]); - await session.close(); - }); - test("a setting the plugin opened differently is the session's open patch, not an event", async () => { const { session, events } = await openStubbornVoiceSession({ voice: "other", loud: false }); diff --git a/packages/server/src/server/agent/plugin-provider.ts b/packages/server/src/server/agent/plugin-provider.ts index 766d892147..32657b19a5 100644 --- a/packages/server/src/server/agent/plugin-provider.ts +++ b/packages/server/src/server/agent/plugin-provider.ts @@ -1211,8 +1211,6 @@ class PluginAgentSession implements AgentSession { private readonly committedOptions: Record; /** While `applyOptions` waits on the plugin, its `session.config` answers that call. */ private applyingOptions = false; - /** One apply at a time: each reconciles against the settings the one before it left. */ - private applying: Promise = Promise.resolve(); private currentTurnId: string | null = null; private closed = false; private replay: { @@ -1422,13 +1420,8 @@ class PluginAgentSession implements AgentSession { await this.bridge.configure({ thinkingOption: thinkingOptionId }); } - applyOptions(options: ProviderOptions): Promise> { - const run = this.applying.then(() => this.applyOptionsNow(options)); - this.applying = run.catch(() => undefined); - return run; - } - - private async applyOptionsNow(options: ProviderOptions): Promise> { + /** The agent manager sends one apply at a time per session. */ + async applyOptions(options: ProviderOptions): Promise> { this.assertReplayComplete(); const before = this.currentOptions(); const { legacySettings } = readPluginOptionState(this.bridge.config); diff --git a/packages/server/src/server/agent/providers/acp-agent.test.ts b/packages/server/src/server/agent/providers/acp-agent.test.ts index f207835725..4e6a9313c9 100644 --- a/packages/server/src/server/agent/providers/acp-agent.test.ts +++ b/packages/server/src/server/agent/providers/acp-agent.test.ts @@ -1965,19 +1965,6 @@ describe("ACPAgentSession options against an ACP agent", () => { ]); }); - test("overlapping applies that both fail still report what the agent moved", async () => { - const { session, optionEvents } = await openSession({ scenario: { onWrite: "slow-reject" } }); - - const first = session.applyOptions({ agent: "Probe Agent" }); - const second = session.applyOptions({ agent: null }); - - await expect(first).rejects.toThrow(/write refused/); - await expect(second).rejects.toThrow(/write refused/); - expect(optionEvents).toEqual([ - { type: "options_changed", provider: "copilot", patch: { agent: "Probe Agent" } }, - ]); - }); - test("a removal writes the select's empty choice and returns it, since the options hold what the session runs", async () => { const { session, optionEvents } = await openSession({ agent: "Probe Agent" }); diff --git a/packages/server/src/server/agent/providers/acp-agent.ts b/packages/server/src/server/agent/providers/acp-agent.ts index abd3400261..384c6a8f24 100644 --- a/packages/server/src/server/agent/providers/acp-agent.ts +++ b/packages/server/src/server/agent/providers/acp-agent.ts @@ -1776,7 +1776,6 @@ export class ACPAgentSession implements AgentSession, ACPClient { openOptionsPatch: Record = {}; /** Selects the provider moves while Paseo is opening or applying options belong to that result, not to events. */ private optionsTransactionDepth = 0; - private applying: Promise = Promise.resolve(); private cachedCommands: AgentSlashCommand[] = []; /** When the last command batch arrived; null while nothing has been published. */ private commandsUpdatedAt: number | null = null; @@ -2449,15 +2448,9 @@ export class ACPAgentSession implements AgentSession, ACPClient { * Applies only the options the patch sets; returns the selects that ended up different. * Provider notifications that arrive meanwhile are part of that result. When a write is * rejected, what the provider moved is reported as an event, since nothing is stored. + * The agent manager sends one apply at a time per session. */ - /** One apply at a time: each reconciles against the state the one before it left. */ - applyOptions(options: ProviderOptions): Promise> { - const run = this.applying.then(() => this.applyOptionsNow(options)); - this.applying = run.catch(() => undefined); - return run; - } - - private async applyOptionsNow(options: ProviderOptions): Promise> { + async applyOptions(options: ProviderOptions): Promise> { assertChangesAreLive({ provider: this.config.provider, patch: options, diff --git a/packages/server/src/server/agent/providers/claude/agent.ts b/packages/server/src/server/agent/providers/claude/agent.ts index 39df65edee..cdcf80408a 100644 --- a/packages/server/src/server/agent/providers/claude/agent.ts +++ b/packages/server/src/server/agent/providers/claude/agent.ts @@ -2888,13 +2888,8 @@ class ClaudeAgentSession implements AgentSession { await this.query?.applyFlagSettings({ fastMode: providerOptions.fast_mode === true }); this.cachedRuntimeInfo = null; } - // Held until the runtime accepts, so a rejected change is retried instead of skipped. Only - // this call's keys are applied, over the options as they are now: another patch may have - // landed while the runtime was answering. - this.config.providerOptions = applyProviderOptionsPatch({ - options: this.config.providerOptions, - patch: options, - }); + // Held until the runtime accepts, so a rejected change is retried instead of skipped. + this.config.providerOptions = providerOptions; } getPendingPermissions(): AgentPermissionRequest[] { diff --git a/packages/server/src/server/agent/providers/opencode/v2/agent.test.ts b/packages/server/src/server/agent/providers/opencode/v2/agent.test.ts index 64427f05ca..58e9ffaefb 100644 --- a/packages/server/src/server/agent/providers/opencode/v2/agent.test.ts +++ b/packages/server/src/server/agent/providers/opencode/v2/agent.test.ts @@ -88,47 +88,6 @@ describe("OpenCode v2 session lifecycle", () => { } }); - test("overlapping auto_accept applies do not undo each other when one fails", async () => { - const harness = new V2Harness(); - const permissions = [ - { id: "approval", sessionID: "session", action: "shell", resources: ["pwd"] }, - ]; - harness.api.permission.list = async () => permissions; - let failFirst!: () => void; - let calls = 0; - harness.api.permission.reply = async () => { - calls += 1; - if (calls > 1) return; - await new Promise((_, reject) => { - failFirst = () => reject(new Error("upstream refused")); - }); - }; - const client = new OpenCodeV2AgentClient({ - logger: createTestLogger(), - runtime: harness.runtime, - }); - const session = await client.createSession({ provider: "opencode", cwd: "/tmp/project" }); - try { - const first = session.applyOptions?.({ auto_accept: true }); - const second = session.applyOptions?.({ auto_accept: true }); - await expect.poll(() => calls).toBe(1); - failFirst(); - - await expect(first).rejects.toThrow("upstream refused"); - await expect(second).resolves.toBeUndefined(); - - // The accepted second apply is in force: the next permission is approved, not asked. - const next = { id: "next", sessionID: "session", action: "shell", resources: ["ls"] }; - permissions.push(next); - harness.push({ id: "asked", created: 2, type: "permission.asked", data: next }); - await expect.poll(() => calls).toBeGreaterThan(2); - const pendingIds = () => session.getPendingPermissions().map(({ id }) => id); - expect(pendingIds()).not.toContain("next"); - } finally { - await session.close(); - } - }); - test("rejects a live permission change the running session cannot apply", async () => { const harness = new V2Harness(); const client = new OpenCodeV2AgentClient({ diff --git a/packages/server/src/server/agent/providers/opencode/v2/session.ts b/packages/server/src/server/agent/providers/opencode/v2/session.ts index 455a45a5c0..1724bacbe7 100644 --- a/packages/server/src/server/agent/providers/opencode/v2/session.ts +++ b/packages/server/src/server/agent/providers/opencode/v2/session.ts @@ -386,14 +386,8 @@ export class OpenCodeV2Session implements AgentSession { this.info = await this.client.session.get({ sessionID: this.id }); this.emit({ type: "thinking_option_changed", provider: "opencode", thinkingOptionId: variant }); } - /** Overlapping applies run one after another, so a failed one restores exactly what it changed. */ - private applying: Promise = Promise.resolve(); - applyOptions(options: ProviderOptions): Promise { - const run = this.applying.then(() => this.applyOptionsNow(options)); - this.applying = run.catch(() => undefined); - return run; - } - private async applyOptionsNow(options: ProviderOptions): Promise { + /** The agent manager sends one apply at a time, so a failed one restores exactly what it changed. */ + async applyOptions(options: ProviderOptions): Promise { assertChangesAreLive({ provider: "opencode", patch: options, diff --git a/packages/server/src/server/agent/providers/test-utils/fake-acp-agent.mjs b/packages/server/src/server/agent/providers/test-utils/fake-acp-agent.mjs index cb6bf65e67..e284a326da 100644 --- a/packages/server/src/server/agent/providers/test-utils/fake-acp-agent.mjs +++ b/packages/server/src/server/agent/providers/test-utils/fake-acp-agent.mjs @@ -3,8 +3,6 @@ // answers: { [configId]: value } the agent ends up with instead of the requested value // onWrite: "notify-first" sends config_option_update before answering the write; // "notify-then-reject" does the same and then fails the write -// "slow-reject" moves the select and announces it for a non-empty value, then refuses -// after a delay; an empty value is refused without moving anything // moves: { [configId]: { [id]: value } } writes that also move another select, announced // by a config_option_update after the write is answered // adds: { [configId]: option[] } writes that make more selects appear, announced the same way @@ -30,21 +28,12 @@ export const connection = new AgentSideConnection( return { sessionId: "session-1", modes: null, models: null, configOptions: options }; }, async setSessionConfigOption({ sessionId, configId, value }) { - if (scenario.onWrite === "slow-reject" && !value) { - await new Promise((resolve) => setTimeout(resolve, 150)); - throw new Error("write refused"); - } set(configId, scenario.answers?.[configId] ?? value); const announce = () => agentConnection.sessionUpdate({ sessionId, update: { sessionUpdate: "config_option_update", configOptions: options }, }); - if (scenario.onWrite === "slow-reject") { - await announce(); - await new Promise((resolve) => setTimeout(resolve, 100)); - throw new Error("write refused"); - } if (scenario.onWrite === "notify-first" || scenario.onWrite === "notify-then-reject") { await announce(); if (scenario.onWrite === "notify-then-reject") throw new Error("write refused");