Skip to content

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

Open

Description

@Artem-B

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 @ 3a8c250: 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 @ 3a8c250

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.

Activity

  1. added
    bugSomething is broken or behaving incorrectly.
    needs-triageIssue needs maintainer review and initial categorization.
    on Nov 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething is broken or behaving incorrectly.needs-triageIssue needs maintainer review and initial categorization.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions