Skip to content
Source
Sign inSign up

[Bug]: Codex fast-mode/model change kills running turn (shared session cwd ≠ thread worktree) #34119

Send to my agent
OpenArtem-B(GitHub profile, opens in a new tab) opened this issue 0 comments

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/server

Steps to reproduce

App (seen in real use, not scripted):

  1. Codex thread A opens shared Codex session; session cwd = A's dir.
  2. Codex thread B, same instance, different worktree (e.g. delegate_task child), turn running.
  3. Toggle Fast mode in B's picker → B's turn stops.

Unit repro: save as apps/server/src/orchestration-v2/ProviderSwitchService.sharedCwd.test.ts, run vp test run src/orchestration-v2/ProviderSwitchService.sharedCwd.test.ts from apps/server. Same change, only session cwd differs.

ProviderSwitchService.sharedCwd.test.ts
import { assert, it } from "@effect/vitest";
import {
  ProviderDriverKind,
  ProviderInstanceId,
  ProviderSessionId,
  ThreadId,
  type OrchestrationV2ThreadProjection,
} from "@t3tools/contracts";
import { turnScopedSelectionTransition } from "@t3tools/provider-core/server/selectionTransition";
import * as DateTime from "effect/DateTime";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";

import { CodexProviderCapabilitiesV2 } from "./Adapters/CodexAdapterV2.ts";
import * as ProviderAdapterRegistry from "./ProviderAdapterRegistry.ts";
import * as ProviderSwitch from "./ProviderSwitchService.ts";

const driver = ProviderDriverKind.make("codex");
const instanceId = ProviderInstanceId.make("codex");

const layer = ProviderSwitch.layer.pipe(
  Layer.provide(
    Layer.mock(ProviderAdapterRegistry.ProviderAdapterRegistryV2)({
      get: () =>
        Effect.succeed({
          instanceId,
          driver,
          getCapabilities: () => Effect.succeed(CodexProviderCapabilitiesV2),
          planSelectionTransition: () => Effect.succeed(turnScopedSelectionTransition()),
          openSession: () => Effect.die("not used"),
        }),
      list: () => Effect.succeed([instanceId]),
      getMetadata: () =>
        Effect.succeed({
          driver,
          continuationKey: "codex:instance:codex",
          enabled: true,
          capabilities: CodexProviderCapabilitiesV2,
        }),
    }),
  ),
);

const projectionWithSessionCwd = (sessionCwd: string) =>
  ({
    thread: {
      id: ThreadId.make("thread_b"),
      modelSelection: { instanceId, model: "gpt-6.1-sol" },
      runtimeMode: "full-access",
      interactionMode: "default",
      worktreePath: "/worktrees/thread-b",
    },
    providerSessions: [
      {
        id: ProviderSessionId.make("provider-session:provider-instance:codex:shared"),
        providerInstanceId: instanceId,
        status: "ready",
        cwd: sessionCwd,
        capabilities: CodexProviderCapabilitiesV2,
        updatedAt: DateTime.makeUnsafe("2026-11-11T00:00:00.000Z"),
      },
    ],
    providerThreads: [],
  }) as unknown as OrchestrationV2ThreadProjection;

const fastModeOn = {
  instanceId,
  model: "gpt-6.1-sol",
  options: [{ id: "fastMode", value: true }],
};

it.effect.each([
  ["the thread's own worktree", "/worktrees/thread-b"],
  ["another thread's directory", "/scratch/thread-a"],
])("turning on fast mode applies next turn when the shared session cwd is %s", ([, cwd]) =>
  Effect.gen(function* () {
    const service = yield* ProviderSwitch.ProviderSwitchServiceV2;
    const plan = yield* service.plan({
      projection: projectionWithSessionCwd(cwd!),
      targetModelSelection: fastModeOn,
    });
    assert.equal(plan.transition.type, "switch_model_in_session");
    assert.deepEqual(plan.releaseProviderSessionIds, []);
  }).pipe(Effect.provide(layer)),
);

main @ 3a8c250cb0: own-worktree passes, other-dir fails:

AssertionError: expected 'restart_and_resume' to equal 'switch_model_in_session'
Tests  1 failed | 1 passed (2)

Expected behavior

Change applies next turn (Codex adapter says apply_on_next_turn). Running turn continues.

Actual behavior

Turn interrupted at once; timeline says "Stopped by the provider" though T3 sent the interrupt.

Cause:

  • All Codex threads share one session (provider-session:provider-instance:codex:shared); its cwd = first opener's dir.
  • ProviderSwitchService.plan compares currentSession.cwd vs thread.worktreePath (L134, L149) → always differ for other threads.
  • decideProviderSessionTransition sees workspaceChanged → restart_and_resume (L55, L73).
  • Orchestrator detaches (L4556) → turn/interrupt + thread unload.

Related, different cause: #22748 (switch mid-turn kills turn; its fix would refuse this change, not defer it), #15300, #16000.

Impact

Major degradation or frequent failure

Version or commit

0.1.8 installed; repro fails on main @ 3a8c250cb0

Environment

Linux x64, service-managed server, web client, Codex gpt-6.1-sol

Logs or stack traces

# server trace (UTC), thread B
00:28:02.952 dispatchCommand thread.model-selection.set (fast mode on)
00:28:02.957 CodexAdapterV2.planSelectionTransition   # apply_on_next_turn
00:28:02.966 ProviderSessionManager.detach
00:28:02.969 CodexAdapterV2.interruptTurn
# codex app-server
00:28:02.971 -> turn/interrupt
00:28:02.986 <- turn/completed status=interrupted
# B's provider session row
cwd = <unrelated earlier thread's scratch dir>; thread.worktreePath = <B's worktree>

Screenshots, recordings, or supporting files

No response

Workaround

Change model/fast mode only while thread is idle.

  1. Artem-B(GitHub profile, opens in a new tab) added labelsneeds-triage

Sign in to join this conversation.