fix(chat): serialize runtime settings commits

This commit is contained in:
murashit 2026-07-17 20:57:22 +09:00
parent 1eb074b361
commit 9ad8ade044
3 changed files with 317 additions and 93 deletions

View file

@ -0,0 +1,192 @@
import type { RuntimeSettingsPatch } from "../../../../domain/runtime/thread-settings";
export type RuntimeSettingsCommitTarget = { readonly kind: "settle" } | { readonly kind: "fields"; readonly update: RuntimeSettingsPatch };
export interface RuntimeSettingsCommitCoordinatorHost {
activeThreadId: () => string | null;
pendingPatch: () => RuntimeSettingsPatch;
updateThreadSettings: (threadId: string, update: RuntimeSettingsPatch) => Promise<boolean>;
commitPatch: (update: RuntimeSettingsPatch) => void;
reportError: (error: unknown) => void;
}
export interface RuntimeSettingsCommitCoordinator {
commit: (threadId: string, target: RuntimeSettingsCommitTarget) => Promise<boolean>;
}
interface PendingCommit {
readonly kind: RuntimeSettingsCommitTarget["kind"];
remaining: RuntimeSettingsPatch;
resolve: (ok: boolean) => void;
}
interface ThreadCommitDrain {
revision: number;
readonly pending: PendingCommit[];
}
export function createRuntimeSettingsCommitCoordinator(host: RuntimeSettingsCommitCoordinatorHost): RuntimeSettingsCommitCoordinator {
const drains = new Map<string, ThreadCommitDrain>();
return {
commit: (threadId, target) => {
let drain = drains.get(threadId);
if (!drain) {
drain = { revision: 0, pending: [] };
drains.set(threadId, drain);
}
drain.revision += 1;
const result = new Promise<boolean>((resolve) => {
drain.pending.push({
kind: target.kind,
remaining: target.kind === "fields" ? { ...target.update } : {},
resolve,
});
});
if (drain.pending.length === 1) void runThreadCommitDrain(host, threadId, drain, drains);
return result;
},
};
}
async function runThreadCommitDrain(
host: RuntimeSettingsCommitCoordinatorHost,
threadId: string,
drain: ThreadCommitDrain,
drains: Map<string, ThreadCommitDrain>,
): Promise<void> {
try {
while (drain.pending.length > 0) {
if (host.activeThreadId() !== threadId) {
resolveAll(drain, false);
return;
}
const update = host.pendingPatch();
if (patchEmpty(update)) {
resolveSettledCommits(drain);
return;
}
const sentRevision = drain.revision;
let updated: boolean;
try {
updated = await host.updateThreadSettings(threadId, update);
} catch (error) {
if (host.activeThreadId() === threadId) host.reportError(error);
resolveAll(drain, false);
return;
}
if (host.activeThreadId() !== threadId) {
resolveAll(drain, false);
return;
}
if (!updated) {
resolveAll(drain, false);
return;
}
const committed = matchingPendingPatch(host.pendingPatch(), update);
if (!patchEmpty(committed)) host.commitPatch(committed);
resolveFieldCommits(drain, committed, host.pendingPatch());
if (drain.pending.length === 0) return;
if (patchEmpty(host.pendingPatch())) {
resolveSettledCommits(drain);
return;
}
if (drain.revision !== sentRevision || drain.pending.some((pending) => pending.kind === "settle")) {
continue;
}
resolveAll(drain, false);
return;
}
} finally {
if (drains.get(threadId) === drain) drains.delete(threadId);
}
}
function matchingPendingPatch(current: RuntimeSettingsPatch, sent: RuntimeSettingsPatch): RuntimeSettingsPatch {
const matching: RuntimeSettingsPatch = {};
for (const key of patchKeys(sent)) {
if (key in current && threadSettingsValueEqual(current[key], sent[key])) {
Object.assign(matching, { [key]: sent[key] });
}
}
return matching;
}
function resolveFieldCommits(drain: ThreadCommitDrain, committed: RuntimeSettingsPatch, current: RuntimeSettingsPatch): void {
for (const pending of [...drain.pending]) {
if (pending.kind === "settle") continue;
for (const key of patchKeys(pending.remaining)) {
if (key in committed && threadSettingsValueEqual(pending.remaining[key], committed[key])) {
pending.remaining = patchWithoutField(pending.remaining, key);
}
}
if (patchEmpty(pending.remaining)) {
resolveCommit(drain, pending, true);
continue;
}
if (patchKeys(pending.remaining).some((key) => !(key in current) || !threadSettingsValueEqual(pending.remaining[key], current[key]))) {
resolveCommit(drain, pending, false);
}
}
}
function resolveSettledCommits(drain: ThreadCommitDrain): void {
for (const pending of [...drain.pending]) {
resolveCommit(drain, pending, pending.kind === "settle" || patchEmpty(pending.remaining));
}
}
function resolveAll(drain: ThreadCommitDrain, ok: boolean): void {
for (const pending of [...drain.pending]) resolveCommit(drain, pending, ok);
}
function resolveCommit(drain: ThreadCommitDrain, pending: PendingCommit, ok: boolean): void {
const index = drain.pending.indexOf(pending);
if (index >= 0) drain.pending.splice(index, 1);
pending.resolve(ok);
}
function patchKeys(patch: RuntimeSettingsPatch): (keyof RuntimeSettingsPatch)[] {
return Object.keys(patch) as (keyof RuntimeSettingsPatch)[];
}
function patchEmpty(patch: RuntimeSettingsPatch): boolean {
return patchKeys(patch).length === 0;
}
function patchWithoutField(patch: RuntimeSettingsPatch, omittedKey: keyof RuntimeSettingsPatch): RuntimeSettingsPatch {
const remaining: RuntimeSettingsPatch = {};
for (const key of patchKeys(patch)) {
if (key !== omittedKey) Object.assign(remaining, { [key]: patch[key] });
}
return remaining;
}
function threadSettingsValueEqual(left: unknown, right: unknown): boolean {
if (Object.is(left, right)) return true;
if (Array.isArray(left) || Array.isArray(right)) {
return (
Array.isArray(left) &&
Array.isArray(right) &&
left.length === right.length &&
left.every((value, index) => threadSettingsValueEqual(value, right[index]))
);
}
if (!isPlainRecord(left) || !isPlainRecord(right)) return false;
const leftKeys = Object.keys(left);
const rightKeys = Object.keys(right);
return (
leftKeys.length === rightKeys.length &&
leftKeys.every((key) => Object.hasOwn(right, key) && threadSettingsValueEqual(left[key], right[key]))
);
}
function isPlainRecord(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === "object";
}

View file

@ -12,6 +12,11 @@ import {
import { type ActivePanelOperation, activePanelOperationDecision } from "../panel-operation-policy";
import { activeThreadId, type ChatAction, type ChatState } from "../state/root-reducer";
import type { ChatStateStore } from "../state/store";
import {
createRuntimeSettingsCommitCoordinator,
type RuntimeSettingsCommitCoordinator,
type RuntimeSettingsCommitTarget,
} from "./runtime-settings-commit-coordinator";
import type { RuntimeSettingsTransport } from "./settings-transport";
interface RuntimeSettingsCommitResult {
@ -30,6 +35,10 @@ export interface RuntimeSettingsActionsHost {
addSystemMessage: (text: string) => void;
}
interface RuntimeSettingsActionsContext extends RuntimeSettingsActionsHost {
runtimeSettingsCommits: RuntimeSettingsCommitCoordinator;
}
export interface ChatRuntimeSettingsActions {
applyPendingThreadSettings: () => Promise<boolean>;
requestModel: (model: string) => Promise<boolean>;
@ -53,36 +62,54 @@ export interface ChatRuntimeSettingsActions {
}
export function createChatRuntimeSettingsActions(host: RuntimeSettingsActionsHost): ChatRuntimeSettingsActions {
return {
applyPendingThreadSettings: () => applyPendingThreadSettings(host),
requestModel: (model) => requestModel(host, model),
resetModelToConfig: () => resetModelToConfig(host),
requestModelFromUi: (model) => requestModelFromUi(host, model),
requestReasoningEffort: (effort) => requestReasoningEffort(host, effort),
resetReasoningEffortToConfig: () => resetReasoningEffortToConfig(host),
requestReasoningEffortFromUi: (effort) => requestReasoningEffortFromUi(host, effort),
resetReasoningEffortToConfigFromUi: () => resetReasoningEffortToConfigFromUi(host),
requestPermissionProfile: (permissionProfile) => requestPermissionProfile(host, permissionProfile),
resetPermissionProfileToConfig: () => resetPermissionProfileToConfig(host),
enableFastMode: () => setFastMode(host, "enabled"),
disableFastMode: () => setFastMode(host, "disabled"),
toggleFastMode: () => toggleFastMode(host),
toggleCollaborationMode: () => toggleCollaborationMode(host),
setCollaborationMode: (collaborationMode) => setCollaborationMode(host, collaborationMode),
requestDefaultCollaborationModeForNextTurn: () => {
requestDefaultCollaborationModeForNextTurn(host);
const runtimeSettingsCommits = createRuntimeSettingsCommitCoordinator({
activeThreadId: () => activeThreadId(state(host)),
pendingPatch: () => currentPendingRuntimeSettingsPatch(host),
updateThreadSettings: (threadId, update) => host.runtimeTransport.updateThreadSettings(threadId, update),
commitPatch: (update) => {
dispatch(host, { type: "runtime/pending-thread-settings-committed", update });
},
enableAutoReview: () => setAutoReview(host, "enabled"),
disableAutoReview: () => setAutoReview(host, "disabled"),
toggleAutoReview: () => toggleAutoReview(host),
reportError: (error) => {
host.addSystemMessage(error instanceof Error ? error.message : String(error));
},
});
const context: RuntimeSettingsActionsContext = { ...host, runtimeSettingsCommits };
return {
applyPendingThreadSettings: () => applyPendingThreadSettings(context),
requestModel: (model) => requestModel(context, model),
resetModelToConfig: () => resetModelToConfig(context),
requestModelFromUi: (model) => requestModelFromUi(context, model),
requestReasoningEffort: (effort) => requestReasoningEffort(context, effort),
resetReasoningEffortToConfig: () => resetReasoningEffortToConfig(context),
requestReasoningEffortFromUi: (effort) => requestReasoningEffortFromUi(context, effort),
resetReasoningEffortToConfigFromUi: () => resetReasoningEffortToConfigFromUi(context),
requestPermissionProfile: (permissionProfile) => requestPermissionProfile(context, permissionProfile),
resetPermissionProfileToConfig: () => resetPermissionProfileToConfig(context),
enableFastMode: () => setFastMode(context, "enabled"),
disableFastMode: () => setFastMode(context, "disabled"),
toggleFastMode: () => toggleFastMode(context),
toggleCollaborationMode: () => toggleCollaborationMode(context),
setCollaborationMode: (collaborationMode) => setCollaborationMode(context, collaborationMode),
requestDefaultCollaborationModeForNextTurn: () => {
requestDefaultCollaborationModeForNextTurn(context);
},
enableAutoReview: () => setAutoReview(context, "enabled"),
disableAutoReview: () => setAutoReview(context, "disabled"),
toggleAutoReview: () => toggleAutoReview(context),
};
}
async function applyPendingThreadSettings(host: RuntimeSettingsActionsHost): Promise<boolean> {
return (await commitPendingThreadSettings(host)).ok;
async function applyPendingThreadSettings(
host: RuntimeSettingsActionsContext,
fields?: readonly (keyof RuntimeSettingsPatch)[],
): Promise<boolean> {
return (await commitPendingThreadSettings(host, fields)).ok;
}
async function commitPendingThreadSettings(host: RuntimeSettingsActionsHost): Promise<RuntimeSettingsCommitResult> {
async function commitPendingThreadSettings(
host: RuntimeSettingsActionsContext,
fields?: readonly (keyof RuntimeSettingsPatch)[],
): Promise<RuntimeSettingsCommitResult> {
const threadId = activeThreadId(state(host));
if (!threadId) return { ok: true, collaborationModeApplied: true };
@ -91,70 +118,58 @@ async function commitPendingThreadSettings(host: RuntimeSettingsActionsHost): Pr
const collaborationModeApplied = !collaborationModeWarning && "collaborationMode" in update;
if (Object.keys(update).length === 0) return { ok: true, collaborationModeApplied };
try {
if (activePanelOperationBlocked(host, "thread-settings")) return { ok: false, collaborationModeApplied: false };
if (!(await host.runtimeTransport.updateThreadSettings(threadId, update))) {
return { ok: false, collaborationModeApplied: false };
}
if (activeThreadId(state(host)) !== threadId) return { ok: false, collaborationModeApplied: false };
if (!runtimeSettingsPatchStillPending(currentPendingRuntimeSettingsPatch(host), update)) {
return { ok: false, collaborationModeApplied: false };
}
dispatch(host, { type: "runtime/pending-thread-settings-committed", update });
return { ok: true, collaborationModeApplied };
} catch (error) {
if (activeThreadId(state(host)) !== threadId) return { ok: false, collaborationModeApplied: false };
host.addSystemMessage(error instanceof Error ? error.message : String(error));
return { ok: false, collaborationModeApplied: false };
}
if (activePanelOperationBlocked(host, "thread-settings")) return { ok: false, collaborationModeApplied: false };
const target = runtimeSettingsCommitTarget(update, fields);
const ok = await host.runtimeSettingsCommits.commit(threadId, target);
return { ok, collaborationModeApplied: ok && collaborationModeApplied };
}
async function requestModel(host: RuntimeSettingsActionsHost, model: string): Promise<boolean> {
async function requestModel(host: RuntimeSettingsActionsContext, model: string): Promise<boolean> {
if (activePanelOperationBlocked(host, "thread-settings")) return false;
dispatch(host, { type: "runtime/model-requested", model });
return applyPendingThreadSettings(host);
return applyPendingThreadSettings(host, ["model"]);
}
async function resetModelToConfig(host: RuntimeSettingsActionsHost): Promise<boolean> {
async function resetModelToConfig(host: RuntimeSettingsActionsContext): Promise<boolean> {
if (activePanelOperationBlocked(host, "thread-settings")) return false;
dispatch(host, { type: "runtime/model-reset-to-config" });
return applyPendingThreadSettings(host);
return applyPendingThreadSettings(host, ["model"]);
}
async function requestModelFromUi(host: RuntimeSettingsActionsHost, model: string): Promise<void> {
async function requestModelFromUi(host: RuntimeSettingsActionsContext, model: string): Promise<void> {
await runRuntimeUiCommand(host, () => requestModel(host, model), modelOverrideMessage(model));
}
async function requestReasoningEffort(host: RuntimeSettingsActionsHost, effort: ReasoningEffort): Promise<boolean> {
async function requestReasoningEffort(host: RuntimeSettingsActionsContext, effort: ReasoningEffort): Promise<boolean> {
if (activePanelOperationBlocked(host, "thread-settings")) return false;
dispatch(host, { type: "runtime/reasoning-effort-requested", effort });
return applyPendingThreadSettings(host);
return applyPendingThreadSettings(host, ["effort"]);
}
async function resetReasoningEffortToConfig(host: RuntimeSettingsActionsHost): Promise<boolean> {
async function resetReasoningEffortToConfig(host: RuntimeSettingsActionsContext): Promise<boolean> {
if (activePanelOperationBlocked(host, "thread-settings")) return false;
dispatch(host, { type: "runtime/reasoning-effort-reset-to-config" });
return applyPendingThreadSettings(host);
return applyPendingThreadSettings(host, ["effort"]);
}
async function requestReasoningEffortFromUi(host: RuntimeSettingsActionsHost, effort: ReasoningEffort): Promise<void> {
async function requestReasoningEffortFromUi(host: RuntimeSettingsActionsContext, effort: ReasoningEffort): Promise<void> {
await runRuntimeUiCommand(host, () => requestReasoningEffort(host, effort), reasoningEffortOverrideMessage(effort));
}
async function resetReasoningEffortToConfigFromUi(host: RuntimeSettingsActionsHost): Promise<void> {
async function resetReasoningEffortToConfigFromUi(host: RuntimeSettingsActionsContext): Promise<void> {
await runRuntimeUiCommand(host, () => resetReasoningEffortToConfig(host), reasoningEffortOverrideMessage(null));
}
async function requestPermissionProfile(host: RuntimeSettingsActionsHost, permissionProfile: string): Promise<boolean> {
async function requestPermissionProfile(host: RuntimeSettingsActionsContext, permissionProfile: string): Promise<boolean> {
if (activePanelOperationBlocked(host, "permission-settings")) return false;
dispatch(host, { type: "runtime/permission-profile-requested", permissionProfile });
return applyPendingThreadSettings(host);
return applyPendingThreadSettings(host, ["permissions"]);
}
async function resetPermissionProfileToConfig(host: RuntimeSettingsActionsHost): Promise<boolean> {
async function resetPermissionProfileToConfig(host: RuntimeSettingsActionsContext): Promise<boolean> {
if (activePanelOperationBlocked(host, "permission-settings")) return false;
dispatch(host, { type: "runtime/permission-profile-reset-to-config" });
return applyPendingThreadSettings(host);
return applyPendingThreadSettings(host, ["permissions"]);
}
function activePanelOperationBlocked(host: RuntimeSettingsActionsHost, operation: ActivePanelOperation): boolean {
@ -164,34 +179,34 @@ function activePanelOperationBlocked(host: RuntimeSettingsActionsHost, operation
return true;
}
async function toggleFastMode(host: RuntimeSettingsActionsHost): Promise<void> {
async function toggleFastMode(host: RuntimeSettingsActionsContext): Promise<void> {
const { snapshot, config } = runtimeProjection(host);
await setFastMode(host, resolveRuntimeControls(snapshot, config).fastMode.active ? "disabled" : "enabled");
}
async function setFastMode(host: RuntimeSettingsActionsHost, mode: FastModeState): Promise<void> {
async function setFastMode(host: RuntimeSettingsActionsContext, mode: FastModeState): Promise<void> {
if (activePanelOperationBlocked(host, "thread-settings")) return;
const fastMode: RequestedFastMode = mode;
await runRuntimeUiCommand(
host,
async () => {
dispatch(host, { type: "runtime/fast-mode-requested", fastMode });
return applyPendingThreadSettings(host);
return applyPendingThreadSettings(host, ["serviceTier"]);
},
mode === "enabled" ? "Fast mode on for subsequent turns." : "Fast mode off for subsequent turns.",
);
}
async function toggleCollaborationMode(host: RuntimeSettingsActionsHost): Promise<void> {
async function toggleCollaborationMode(host: RuntimeSettingsActionsContext): Promise<void> {
const { snapshot, config } = runtimeProjection(host);
const next = nextCollaborationMode(resolveRuntimeControls(snapshot, config).collaborationMode.effective);
await setCollaborationMode(host, next);
}
async function setCollaborationMode(host: RuntimeSettingsActionsHost, collaborationMode: CollaborationModeSelection): Promise<boolean> {
async function setCollaborationMode(host: RuntimeSettingsActionsContext, collaborationMode: CollaborationModeSelection): Promise<boolean> {
if (activePanelOperationBlocked(host, "thread-settings")) return false;
dispatch(host, { type: "runtime/requested-collaboration-mode-set", collaborationMode });
const result = await commitPendingThreadSettings(host);
const result = await commitPendingThreadSettings(host, ["collaborationMode"]);
if (result.ok) closeRuntimePanel(host);
if (result.ok && result.collaborationModeApplied) {
host.addSystemMessage(collaborationMode === "plan" ? "Plan mode on for subsequent turns." : "Plan mode off for subsequent turns.");
@ -204,19 +219,19 @@ function requestDefaultCollaborationModeForNextTurn(host: RuntimeSettingsActions
dispatch(host, { type: "runtime/requested-collaboration-mode-set", collaborationMode: "default" });
}
async function toggleAutoReview(host: RuntimeSettingsActionsHost): Promise<void> {
async function toggleAutoReview(host: RuntimeSettingsActionsContext): Promise<void> {
const { snapshot, config } = runtimeProjection(host);
const nextState = resolveRuntimeControls(snapshot, config).autoReview.active ? "disabled" : "enabled";
await setAutoReview(host, nextState);
}
async function setAutoReview(host: RuntimeSettingsActionsHost, mode: AutoReviewState): Promise<void> {
async function setAutoReview(host: RuntimeSettingsActionsContext, mode: AutoReviewState): Promise<void> {
if (activePanelOperationBlocked(host, "thread-settings")) return;
await runRuntimeUiCommand(
host,
async () => {
dispatch(host, { type: "runtime/approvals-reviewer-requested", approvalsReviewer: mode === "enabled" ? "auto_review" : "user" });
return applyPendingThreadSettings(host);
return applyPendingThreadSettings(host, ["approvalsReviewer"]);
},
mode === "enabled" ? "Auto-review on for subsequent turns." : "Auto-review off for subsequent turns.",
);
@ -256,33 +271,16 @@ function currentPendingRuntimeSettingsPatch(host: RuntimeSettingsActionsHost): R
return buildPendingRuntimeSettingsPatch(snapshot, config).update;
}
function runtimeSettingsPatchStillPending(current: RuntimeSettingsPatch, applied: RuntimeSettingsPatch): boolean {
return (Object.keys(applied) as (keyof RuntimeSettingsPatch)[]).every((key) => {
return key in current && threadSettingsValueEqual(current[key], applied[key]);
});
}
function threadSettingsValueEqual(left: unknown, right: unknown): boolean {
if (Object.is(left, right)) return true;
if (Array.isArray(left) || Array.isArray(right)) {
return (
Array.isArray(left) &&
Array.isArray(right) &&
left.length === right.length &&
left.every((value, index) => threadSettingsValueEqual(value, right[index]))
);
function runtimeSettingsCommitTarget(
update: RuntimeSettingsPatch,
fields: readonly (keyof RuntimeSettingsPatch)[] | undefined,
): RuntimeSettingsCommitTarget {
if (!fields) return { kind: "settle" };
const targetedUpdate: RuntimeSettingsPatch = {};
for (const key of fields) {
if (key in update) Object.assign(targetedUpdate, { [key]: update[key] });
}
if (!isPlainRecord(left) || !isPlainRecord(right)) return false;
const leftKeys = Object.keys(left);
const rightKeys = Object.keys(right);
return (
leftKeys.length === rightKeys.length &&
leftKeys.every((key) => Object.hasOwn(right, key) && threadSettingsValueEqual(left[key], right[key]))
);
}
function isPlainRecord(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === "object";
return Object.keys(targetedUpdate).length === 0 ? { kind: "settle" } : { kind: "fields", update: targetedUpdate };
}
function runtimeProjection(host: RuntimeSettingsActionsHost): {

View file

@ -458,17 +458,51 @@ describe("createChatRuntimeSettingsActions", () => {
expect(transport.updateThreadSettings).toHaveBeenNthCalledWith(1, "thread", { model: "gpt-old" });
const secondRequest = actions.requestModel("gpt-new");
expect(transport.updateThreadSettings).toHaveBeenCalledTimes(1);
firstUpdate.resolve();
await expect(firstRequest).resolves.toBe(false);
expect(transport.updateThreadSettings).toHaveBeenNthCalledWith(2, "thread", { model: "gpt-new" });
secondUpdate.resolve();
await expect(secondRequest).resolves.toBe(true);
expect(store.getState().runtime.active.model).toBe("gpt-new");
expect(store.getState().runtime.pending.model).toEqual({ kind: "unchanged" });
expect(messages).toEqual([]);
});
firstUpdate.resolve();
await expect(firstRequest).resolves.toBe(false);
expect(store.getState().runtime.active.model).toBe("gpt-new");
it("coalesces a different-field intent behind the active settings update", async () => {
let state = chatStateFixture();
state = chatStateWith(state, { activeThread: { id: "thread" } });
const store = createChatStateStore(state);
const modelUpdate = deferred(true);
const effortUpdate = deferred(true);
const transport = settingsTransportFixture({
updateThreadSettings: vi
.fn()
.mockImplementationOnce(() => modelUpdate.promise)
.mockImplementationOnce(() => effortUpdate.promise),
});
const messages: string[] = [];
const actions = runtimeActionsFixture(store, transport, messages);
const modelRequest = actions.requestModel("gpt-5.5");
expect(transport.updateThreadSettings).toHaveBeenNthCalledWith(1, "thread", { model: "gpt-5.5" });
const effortRequest = actions.requestReasoningEffort("high");
expect(transport.updateThreadSettings).toHaveBeenCalledTimes(1);
modelUpdate.resolve();
await expect(modelRequest).resolves.toBe(true);
expect(transport.updateThreadSettings).toHaveBeenNthCalledWith(2, "thread", { effort: "high" });
expect(store.getState().runtime.active.model).toBe("gpt-5.5");
expect(store.getState().runtime.pending.model).toEqual({ kind: "unchanged" });
expect(store.getState().runtime.pending.reasoningEffort).toEqual({ kind: "set", value: "high" });
effortUpdate.resolve();
await expect(effortRequest).resolves.toBe(true);
expect(store.getState().runtime.active.reasoningEffort).toBe("high");
expect(store.getState().runtime.pending.reasoningEffort).toEqual({ kind: "unchanged" });
expect(messages).toEqual([]);
});