diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts index 1e762543a13ae..dabf32b594fdb 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts @@ -4,6 +4,9 @@ import { type ProviderTextDeltaUpdate, } from "./ProviderTextDeltaCoalescer.ts"; import * as NodeServices from "@effect/platform-node/NodeServices"; +import * as HostProcess from "@t3tools/shared/HostProcess"; +// @effect-diagnostics-next-line nodeBuiltinImport:off -- Directory junctions keep these fixtures available on Windows. +import * as NodeFSP from "node:fs/promises"; import { CommandId, CheckpointId, @@ -915,6 +918,7 @@ describe("CodexAdapterV2 post-settle continuation", () => { Effect.void, onRequest: (method: string, params: unknown) => Effect.Effect = () => Effect.void, readChildMetadata?: Parameters[2], + runtimePolicy = CODEX_TEST_RUNTIME_POLICY, ) => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; @@ -943,6 +947,13 @@ describe("CodexAdapterV2 post-settle continuation", () => { (client) => ({ ...client, + raw: { + ...client.raw, + request: (method, params) => + onRequest(method, params).pipe( + Effect.andThen(client.raw.request(method, params)), + ), + }, request: (method, params) => onRequest(method, params).pipe( Effect.andThen(client.request(method, params)), @@ -975,12 +986,12 @@ describe("CodexAdapterV2 post-settle continuation", () => { threadId, providerSessionId: ProviderSessionId.make(`provider-session-${transcript.scenario}`), modelSelection: CODEX_TEST_MODEL_SELECTION, - runtimePolicy: CODEX_TEST_RUNTIME_POLICY, + runtimePolicy, }); const providerThread = yield* runtime.ensureThread({ threadId, modelSelection: CODEX_TEST_MODEL_SELECTION, - runtimePolicy: CODEX_TEST_RUNTIME_POLICY, + runtimePolicy, }); const events: Array = []; const firstTerminal = yield* Deferred.make(); @@ -1146,6 +1157,192 @@ describe("CodexAdapterV2 post-settle continuation", () => { ), ); + it.effect.each(["darwin", "win32"] as const)( + "resolves the Codex workspace per platform (%s) without changing the registered path", + (platform) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-codex-symlink-" }); + const target = `${root}/physical`; + const alias = `${root}/alias`; + yield* fs.makeDirectory(target); + yield* Effect.tryPromise(() => NodeFSP.symlink(target, alias, "junction")); + const expectedCwd = platform === "win32" ? alias : yield* fs.realPath(target); + const policy = { ...CODEX_TEST_RUNTIME_POLICY, runtimeMode: "auto" as const, cwd: alias }; + const requests: Array = []; + const preamble = codexReplayPreamble({ + nativeThreadId: "physical-workspace", + nativeTurnId: "physical-turn", + prompt: "Inspect the workspace.", + }); + const entries: Array = [ + ...preamble.slice(0, 5), + { + type: "expect_outbound", + label: "resume alias", + frame: { + id: 3, + method: "thread/resume", + params: { + threadId: "physical-workspace", + excludeTurns: true, + config: CodexAdapterV2.CODEX_THREAD_CONFIG, + }, + }, + }, + { + type: "emit_inbound", + label: "resume alias", + frame: { + id: 3, + result: { thread: { id: "physical-workspace", updatedAt: 1782622440 } }, + }, + }, + { + type: "expect_outbound", + label: "turn/start", + frame: { + id: 4, + method: "turn/start", + params: { + threadId: "physical-workspace", + input: [{ type: "text", text: "Inspect the workspace." }], + cwd: "/workspace", + model: "gpt-5.4", + approvalPolicy: "on-request", + approvalsReviewer: "auto_review", + sandboxPolicy: { type: "workspaceWrite" }, + summary: "detailed", + collaborationMode: { mode: "default", settings: { model: "gpt-5.4" } }, + }, + }, + }, + ...preamble + .slice(6) + .map((entry) => + entry.type !== "runtime_exit" && Predicate.hasProperty(entry.frame, "id") + ? withReplayRequestId(entry, 4) + : entry, + ), + ]; + const transcript = yield* decodeReplayTranscriptJson( + (yield* encodeReplayTranscriptJson( + makeCodexReplayTranscript({ + scenario: `physical-workspace-${platform}`, + entries, + }), + )).replaceAll( + yield* encodeStringJson("/workspace"), + yield* encodeStringJson(expectedCwd), + ), + ); + const harness = yield* makeCodexReplayHarness( + transcript, + () => Effect.void, + (method, params) => + Effect.sync(() => { + if (method === "thread/start" || method === "thread/resume") requests.push(params); + }), + undefined, + policy, + ); + yield* harness.runtime.resumeThread({ + providerThread: harness.providerThread, + runtimePolicy: policy, + modelSelection: CODEX_TEST_MODEL_SELECTION, + }); + assert.lengthOf(requests, 2); + for (const request of requests) assert.propertyVal(request, "cwd", expectedCwd); + yield* harness.runtime.startTurn({ + ...makeCodexTestTurnInput({ + threadId: harness.threadId, + providerThread: harness.providerThread, + now: yield* DateTime.now, + attemptId: RunAttemptId.make("physical-attempt"), + text: "Inspect the workspace.", + }), + runtimePolicy: policy, + }); + assert.equal(policy.cwd, alias); + assert.equal(harness.runtime.providerSession.cwd, alias); + }).pipe( + Effect.scoped, + Effect.provideService(HostProcess.Platform, platform), + Effect.provide( + Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer), + ), + ), + ); + + it.effect("follows a workspace alias retargeted between Codex turns", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-codex-retarget-" }); + const alias = `${root}/alias`; + const targets = [`${root}/first`, `${root}/second`]; + for (const target of targets) yield* fs.makeDirectory(target); + yield* Effect.tryPromise(() => NodeFSP.symlink(targets[0]!, alias, "junction")); + const entries: Array = []; + for (const [index, target] of targets.entries()) { + const preamble = codexReplayPreamble({ + nativeThreadId: "retargeted-workspace", + nativeTurnId: `retargeted-turn-${index}`, + prompt: `Inspect workspace ${index}.`, + }); + const transcript = yield* decodeReplayTranscriptJson( + (yield* encodeReplayTranscriptJson( + makeCodexReplayTranscript({ + scenario: "retargeted-workspace", + entries: + index === 0 + ? preamble + : preamble + .slice(5) + .map((entry) => + entry.type !== "runtime_exit" && Predicate.hasProperty(entry.frame, "id") + ? withReplayRequestId(entry, 4) + : entry, + ), + }), + )).replaceAll( + yield* encodeStringJson("/workspace"), + yield* encodeStringJson(yield* fs.realPath(target)), + ), + ); + entries.push(...transcript.entries); + } + const policy = { ...CODEX_TEST_RUNTIME_POLICY, cwd: alias }; + const harness = yield* makeCodexReplayHarness( + makeCodexReplayTranscript({ scenario: "retargeted-workspace", entries }), + undefined, + undefined, + undefined, + policy, + ); + for (const [index, target] of targets.entries()) { + yield* fs.remove(alias); + yield* Effect.tryPromise(() => NodeFSP.symlink(target, alias, "junction")); + yield* harness.runtime.startTurn({ + ...makeCodexTestTurnInput({ + threadId: harness.threadId, + providerThread: harness.providerThread, + now: yield* DateTime.now, + attemptId: RunAttemptId.make(`retargeted-attempt-${index}`), + text: `Inspect workspace ${index}.`, + }), + runtimePolicy: policy, + }); + } + assert.equal(policy.cwd, alias); + }).pipe( + Effect.scoped, + Effect.provideService(HostProcess.Platform, "darwin"), + Effect.provide( + Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer), + ), + ), + ); + it.effect("adds the session MCP server without replacing launch-argument servers", () => Effect.gen(function* () { const scenario = "launch-mcp-servers"; @@ -2552,7 +2749,9 @@ describe("CodexAdapterV2 post-settle continuation", () => { if (outcome === "recover") { const fs = yield* FileSystem.FileSystem; - const cwd = yield* fs.makeTempDirectoryScoped({ prefix: "t3-capacity-retry-" }); + const cwd = yield* fs.realPath( + yield* fs.makeTempDirectoryScoped({ prefix: "t3-capacity-retry-" }), + ); const localTranscript = yield* decodeReplayTranscriptJson( (yield* encodeReplayTranscriptJson( makeCodexReplayTranscript({ scenario, entries }), @@ -3476,7 +3675,9 @@ describe("CodexAdapterV2 post-settle continuation", () => { return Effect.scoped( Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; - const cwd = yield* fs.makeTempDirectoryScoped({ prefix: "t3-bg-stop-workspace-" }); + const cwd = yield* fs.realPath( + yield* fs.makeTempDirectoryScoped({ prefix: "t3-bg-stop-workspace-" }), + ); const localTranscript = yield* decodeReplayTranscriptJson( (yield* encodeReplayTranscriptJson(transcript)).replaceAll( yield* encodeStringJson("/workspace"), @@ -3603,7 +3804,9 @@ describe("CodexAdapterV2 post-settle continuation", () => { Effect.scoped( Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; - const cwd = yield* fs.makeTempDirectoryScoped({ prefix: "t3-bg-stale-workspace-" }); + const cwd = yield* fs.realPath( + yield* fs.makeTempDirectoryScoped({ prefix: "t3-bg-stale-workspace-" }), + ); const staleTranscript = makeCodexReplayTranscript({ scenario: "codex-bg-stop-untracked", entries: [ diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts index 74c4b46646e3e..32cca321d046e 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts @@ -1636,6 +1636,18 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( const { clientFactory, crypto, fileSystem, idAllocator, serverConfig } = adapterOptions; const continuationRequests = adapterOptions.continuationRequests; + const resolveCodexRuntimePolicy = Effect.fn("CodexAdapterV2.resolveCodexRuntimePolicy")( + function* (policy: ProviderAdapter.ProviderAdapterV2RuntimePolicy) { + const cwd = policy.cwd; + // Windows realpath can rewrite mapped drives to network paths; preserve that platform's cwd. + if (cwd === null || (yield* HostProcess.Platform) === "win32") return policy; + return { + ...policy, + cwd: yield* fileSystem.realPath(cwd).pipe(Effect.orElseSucceed(() => cwd)), + }; + }, + ); + return ProviderAdapter.ProviderAdapterV2.of({ instanceId: adapterOptions.instanceId, driver: CODEX_PROVIDER, @@ -6490,7 +6502,7 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( const turnStartParams = yield* buildCodexTurnStartParams({ nativeThreadId: threadId, codexInput, - runtimePolicy: turnInput.runtimePolicy, + runtimePolicy: yield* resolveCodexRuntimePolicy(turnInput.runtimePolicy), modelSelection: turnInput.modelSelection, hasT3Mcp: mcpSession !== undefined, browserToolsAvailable: mcpSession?.browserToolsAvailable ?? true, @@ -6774,8 +6786,9 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( } return false; }), - ensureThread: (threadInput) => - ensureInitialized.pipe( + ensureThread: Effect.fn("CodexAdapterV2.ensureThread")(function* (threadInput) { + const runtimePolicy = yield* resolveCodexRuntimePolicy(threadInput.runtimePolicy); + return yield* ensureInitialized.pipe( Effect.andThen(mcpSessions.read(threadInput.threadId)), Effect.flatMap((mcpSession) => client.request( @@ -6783,7 +6796,7 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( codexThreadRuntimeParams({ mcpSession, modelSelection: threadInput.modelSelection, - runtimePolicy: threadInput.runtimePolicy, + runtimePolicy, }), ), ), @@ -6805,7 +6818,8 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( cause: normalizeCodexCause(cause), }), ), - ), + ); + }), resumeThread: Effect.fn("CodexAdapterV2.resumeThread")( function* (threadInput) { const nativeThreadId = yield* getNativeThreadId(threadInput.providerThread); @@ -6822,7 +6836,9 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( : { modelSelection: threadInput.modelSelection }), ...(threadInput.runtimePolicy === undefined ? {} - : { runtimePolicy: threadInput.runtimePolicy }), + : { + runtimePolicy: yield* resolveCodexRuntimePolicy(threadInput.runtimePolicy), + }), }), }); const response = yield* ensureInitialized.pipe( @@ -7602,7 +7618,7 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( ...codexThreadRuntimeParams({ mcpSession: yield* readMcpSession(threadInput.providerThread.appThreadId), modelSelection: input.modelSelection, - runtimePolicy: input.runtimePolicy, + runtimePolicy: yield* resolveCodexRuntimePolicy(input.runtimePolicy), }), }); } @@ -7658,7 +7674,11 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( : { modelSelection: threadInput.modelSelection }), ...(threadInput.runtimePolicy === undefined ? {} - : { runtimePolicy: threadInput.runtimePolicy }), + : { + runtimePolicy: yield* resolveCodexRuntimePolicy( + threadInput.runtimePolicy, + ), + }), }), }), ),