Repository navigation
Description
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):
- Codex thread A opens shared Codex session; session
cwd= A's dir. - Codex thread B, same instance, different worktree (e.g.
delegate_taskchild), turn running. - 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); itscwd= first opener's dir. ProviderSwitchService.plancomparescurrentSession.cwdvsthread.worktreePath(L134, L149) → always differ for other threads.decideProviderSessionTransitionseesworkspaceChanged→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