fix(server): an OMP replacement runtime that refuses the session's Fast publishes what it accepted

The approval-mode restart installed the new runtime's state without comparing its Fast result to the session's, so a refusal never reached options_changed, the remembered value, or storage. The replacement now goes through the same comparison as refreshState.
This commit is contained in:
Mohamed Boudra
2026-11-01 20:07:00 +01:00
parent 133481d75c
commit d1a1eba23d
3 changed files with 29 additions and 0 deletions
@@ -780,6 +780,18 @@ describe("OMP agent client and session", () => {
expect(omp.runtime().setFastModeRequests).toEqual([]);
});
test("a replacement runtime that refuses the Fast the session had publishes what it accepted", async () => {
const omp = new OmpHarness();
omp.acceptFastOnNextRuntime();
await omp.start({ modeId: "full", providerOptions: { fast_mode: true } });
expect(omp.requireSession().openOptionsPatch).toEqual({});
// The approval-mode restart starts a second runtime, which refuses Fast.
await omp.setMode("ask");
expect(omp.optionsChanges()).toEqual([{ fast_mode: false }]);
});
test("restores Fast from the initial OMP state on create and resume", async () => {
const created = new OmpHarness();
await created.start({ providerOptions: { fast_mode: true } });
@@ -1095,6 +1095,7 @@ export class OmpAgentSession implements AgentSession {
const old = this.runtimeSession;
const oldHostTools = this.hostTools;
const previousSessionId = this.state.sessionId;
const fastModeBefore = this.state.fastModeEnabled;
const restarted = await this.restartRuntime(
this.config.internal ? null : (this.state.sessionFile ?? null),
modeId,
@@ -1115,6 +1116,7 @@ export class OmpAgentSession implements AgentSession {
this.config.modeId = modeId;
this.runtimeDead = false;
this.attachRuntime(next);
this.publishFastModeChange(fastModeBefore);
this.subagentIndex.clear(old);
await oldHostTools?.close();
await old.close().catch(() => undefined);
@@ -2267,6 +2269,11 @@ export class OmpAgentSession implements AgentSession {
private async refreshState(): Promise<void> {
const before = this.state.fastModeEnabled;
this.state = await this.runtimeSession.getState();
this.publishFastModeChange(before);
}
// The runtime owns Fast: whatever it accepted is what the session now runs with.
private publishFastModeChange(before: boolean | undefined): void {
if (this.state.fastModeEnabled !== before && typeof this.state.fastModeEnabled === "boolean") {
this.rememberFastMode(this.state.fastModeEnabled);
this.emit({
@@ -467,6 +467,16 @@ export class OmpHarness {
return this.events.map((event) => event.type);
}
acceptFastOnNextRuntime(): void {
this.omp.queueSessionSetup((runtime) => {
runtime.fastModeResult = { enabled: true, active: true };
});
}
optionsChanges(): Record<string, unknown>[] {
return this.events.flatMap((event) => (event.type === "options_changed" ? [event.patch] : []));
}
async history(): Promise<AgentTimelineItem[]> {
const items: AgentTimelineItem[] = [];
for await (const event of this.requireSession().streamHistory()) {