fix(server): Claude applies only the keys a patch sets over its current options

A Fast patch kept a copy of the options taken before the runtime answered, and
assigning it afterwards dropped a patch that landed meanwhile.
This commit is contained in:
Mohamed Boudra
2026-11-01 15:34:06 +01:00
parent c0c181425f
commit 0fef7414fc
2 changed files with 36 additions and 2 deletions
@@ -1530,6 +1530,38 @@ describe("ClaudeAgentSession features", () => {
await session.close();
});
test("a patch that lands while Fast is being applied is not overwritten by it", async () => {
const { queryFactory, queryMock } = createQueryMock();
const client = new ClaudeAgentClient({
logger,
queryFactory,
resolveBinary: async () => "/test/claude/bin",
});
const session = await client.createSession({
provider: "claude",
cwd: process.cwd(),
model: "claude-opus-4-8",
});
await (session as unknown as { ensureQuery(): Promise<unknown> }).ensureQuery();
let finishFast!: () => void;
queryMock.applyFlagSettings.mockImplementationOnce(
() =>
new Promise<void>((resolve) => {
finishFast = resolve;
}),
);
const fast = session.applyOptions?.({ fast_mode: true });
await session.applyOptions?.({ allowedTools: ["Read"] });
finishFast();
await fast;
expect(
(session as unknown as { config: { providerOptions: unknown } }).config.providerOptions,
).toEqual({ fast_mode: true, allowedTools: ["Read"] });
await session.close();
});
test("a patch before the first prompt keeps the restrictions the session opened with", async () => {
const { queryFactory } = createQueryMock();
const client = new ClaudeAgentClient({
@@ -2826,8 +2826,10 @@ 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.
this.config.providerOptions = providerOptions;
// 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(this.config.providerOptions, options);
}
getPendingPermissions(): AgentPermissionRequest[] {