From 292caaec03d106cd9ab649328714cf85e46bf650 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Tue, 27 Oct 2026 20:52:21 -0700 Subject: [PATCH 1/3] fix(server): Codex shadow homes share thread writer locks with the main home Codex 0.165 allows one writer per thread across processes through lock files in /thread-writer-locks. Shadow homes did not link that directory, so shadow-home and main-home processes could lock in different places and write one rollout at once. Link it as shared, and replace a local copy that a shadow home already created. Keep auth.lock (Codex 0.166+) private, since it guards the shadow home's private auth.json. Before, it caused a shadow-home entry conflict. Co-Authored-By: Claude Opus 5.5 --- .../provider/Drivers/CodexHomeLayout.test.ts | 55 +++++++++++++++++++ .../src/provider/Drivers/CodexHomeLayout.ts | 10 +++- 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/apps/server/src/provider/Drivers/CodexHomeLayout.test.ts b/apps/server/src/provider/Drivers/CodexHomeLayout.test.ts index 093777e67db5d..87fed3555d7ba 100644 --- a/apps/server/src/provider/Drivers/CodexHomeLayout.test.ts +++ b/apps/server/src/provider/Drivers/CodexHomeLayout.test.ts @@ -221,6 +221,61 @@ it.layer(NodeServices.layer)("CodexHomeLayout", (it) => { }), ); + it.effect.skipIf(!symlinksSupported)( + "replaces Codex-created local thread writer locks with the shared lock directory", + () => + Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const sharedHome = yield* makeTempDir("t3code-codex-shared-"); + const shadowRoot = yield* makeTempDir("t3code-codex-shadow-root-"); + const shadowHome = path.join(shadowRoot, "shadow"); + const shadowLocks = path.join(shadowHome, "thread-writer-locks"); + + yield* writeTextFile(path.join(shadowLocks, ".coordination.lock"), ""); + + const layout = yield* resolveCodexHomeLayout( + decodeCodexSettings({ + homePath: sharedHome, + shadowHomePath: shadowHome, + }), + ); + + yield* materializeCodexShadowHome(layout); + + expect(yield* fileSystem.readLink(shadowLocks)).toBe( + path.join(sharedHome, "thread-writer-locks"), + ); + }), + ); + + it.effect.skipIf(!symlinksSupported)("keeps the shadow auth lock private", () => + Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const sharedHome = yield* makeTempDir("t3code-codex-shared-"); + const shadowRoot = yield* makeTempDir("t3code-codex-shadow-root-"); + const shadowHome = path.join(shadowRoot, "shadow"); + const shadowLock = path.join(shadowHome, "auth.lock"); + + yield* writeTextFile(path.join(sharedHome, "auth.lock"), ""); + yield* writeTextFile(shadowLock, ""); + + const layout = yield* resolveCodexHomeLayout( + decodeCodexSettings({ + homePath: sharedHome, + shadowHomePath: shadowHome, + }), + ); + + yield* materializeCodexShadowHome(layout); + + const lockLinkResult = yield* fileSystem.readLink(shadowLock).pipe(Effect.result); + expect(lockLinkResult._tag).toBe("Failure"); + expect(yield* fileSystem.exists(shadowLock)).toBe(true); + }), + ); + it.effect.skipIf(!symlinksSupported)( "accepts Codex-created shadow-local runtime directories", () => diff --git a/apps/server/src/provider/Drivers/CodexHomeLayout.ts b/apps/server/src/provider/Drivers/CodexHomeLayout.ts index f175a0fdd00a9..2bbb11746fd24 100644 --- a/apps/server/src/provider/Drivers/CodexHomeLayout.ts +++ b/apps/server/src/provider/Drivers/CodexHomeLayout.ts @@ -26,13 +26,19 @@ const KNOWN_SHARED_DIRECTORIES = [ "cache", "logs", "mcp-oauth-locks", + "thread-writer-locks", ] as const; -const PRIVATE_ENTRY_NAMES = new Set(["auth.json", "models_cache.json"]); +// auth.lock guards the shadow home's private auth.json. +const PRIVATE_ENTRY_NAMES = new Set(["auth.json", "auth.lock", "models_cache.json"]); // Finder maintains metadata separately for each directory, including shadow homes. const SHADOW_LOCAL_ENTRY_NAMES = new Set(["log", "memories", "tmp", ".DS_Store"]); // Shadow homes may create local runtime locks first; replace them with shared links because they guard shared state. -const REPLACEABLE_SHARED_RUNTIME_ENTRIES = new Set(["mcp-oauth-locks", ".sqlite-maintenance.lock"]); +const REPLACEABLE_SHARED_RUNTIME_ENTRIES = new Set([ + "mcp-oauth-locks", + "thread-writer-locks", + ".sqlite-maintenance.lock", +]); function resolveHomePath(path: Path.Path, home: string, value: string | undefined): string { const expanded = From f47abc4dedd6346e46eb9614c1152bbf49ac44d8 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Tue, 27 Oct 2026 21:42:08 -0700 Subject: [PATCH 2/3] fix(server): keep a shadow thread lock directory while it holds writer locks Codex deletes each thread lock on release, so a lock file that is still there may belong to a live writer, such as an app-server left by a killed server. Replacing the directory then would let a second writer lock the same thread in the shared home, and the old writer's cleanup would delete the new lock through the symlink. Keep the local directory until a later setup finds no thread locks in it. Co-Authored-By: Claude Opus 5.5 --- .../provider/Drivers/CodexHomeLayout.test.ts | 40 ++++++------------- .../src/provider/Drivers/CodexHomeLayout.ts | 24 +++++++++++ 2 files changed, 36 insertions(+), 28 deletions(-) diff --git a/apps/server/src/provider/Drivers/CodexHomeLayout.test.ts b/apps/server/src/provider/Drivers/CodexHomeLayout.test.ts index 87fed3555d7ba..425107b36ec98 100644 --- a/apps/server/src/provider/Drivers/CodexHomeLayout.test.ts +++ b/apps/server/src/provider/Drivers/CodexHomeLayout.test.ts @@ -121,8 +121,10 @@ it.layer(NodeServices.layer)("CodexHomeLayout", (it) => { '{"models":["shared"]}\n', ); yield* writeTextFile(path.join(sharedHome, "auth.json"), '{"shared":true}\n'); + yield* writeTextFile(path.join(sharedHome, "auth.lock"), ""); yield* fileSystem.makeDirectory(shadowHome, { recursive: true }); yield* writeTextFile(path.join(shadowHome, "auth.json"), '{"shadow":true}\n'); + yield* writeTextFile(path.join(shadowHome, "auth.lock"), ""); yield* fileSystem.symlink( path.join(sharedHome, "models_cache.json"), path.join(shadowHome, "models_cache.json"), @@ -149,6 +151,9 @@ it.layer(NodeServices.layer)("CodexHomeLayout", (it) => { .readLink(path.join(shadowHome, "auth.json")) .pipe(Effect.result); const authContents = yield* fileSystem.readFileString(path.join(shadowHome, "auth.json")); + const authLockLinkResult = yield* fileSystem + .readLink(path.join(shadowHome, "auth.lock")) + .pipe(Effect.result); expect(sessionsTarget).toBe(path.join(sharedHome, "sessions")); expect(configTarget).toBe(path.join(sharedHome, "config.toml")); @@ -156,6 +161,7 @@ it.layer(NodeServices.layer)("CodexHomeLayout", (it) => { expect(modelsCacheExists).toBe(false); expect(authLinkResult._tag).toBe("Failure"); expect(authContents).toContain("shadow"); + expect(authLockLinkResult._tag).toBe("Failure"); }), ); @@ -222,7 +228,7 @@ it.layer(NodeServices.layer)("CodexHomeLayout", (it) => { ); it.effect.skipIf(!symlinksSupported)( - "replaces Codex-created local thread writer locks with the shared lock directory", + "shares local thread writer locks once no writer still holds one", () => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; @@ -231,8 +237,10 @@ it.layer(NodeServices.layer)("CodexHomeLayout", (it) => { const shadowRoot = yield* makeTempDir("t3code-codex-shadow-root-"); const shadowHome = path.join(shadowRoot, "shadow"); const shadowLocks = path.join(shadowHome, "thread-writer-locks"); + const threadLock = path.join(shadowLocks, "019a0000-0000-7000-8000-000000000000.lock"); yield* writeTextFile(path.join(shadowLocks, ".coordination.lock"), ""); + yield* writeTextFile(threadLock, ""); const layout = yield* resolveCodexHomeLayout( decodeCodexSettings({ @@ -242,40 +250,16 @@ it.layer(NodeServices.layer)("CodexHomeLayout", (it) => { ); yield* materializeCodexShadowHome(layout); + expect(yield* fileSystem.exists(threadLock)).toBe(true); + yield* fileSystem.remove(threadLock); + yield* materializeCodexShadowHome(layout); expect(yield* fileSystem.readLink(shadowLocks)).toBe( path.join(sharedHome, "thread-writer-locks"), ); }), ); - it.effect.skipIf(!symlinksSupported)("keeps the shadow auth lock private", () => - Effect.gen(function* () { - const fileSystem = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const sharedHome = yield* makeTempDir("t3code-codex-shared-"); - const shadowRoot = yield* makeTempDir("t3code-codex-shadow-root-"); - const shadowHome = path.join(shadowRoot, "shadow"); - const shadowLock = path.join(shadowHome, "auth.lock"); - - yield* writeTextFile(path.join(sharedHome, "auth.lock"), ""); - yield* writeTextFile(shadowLock, ""); - - const layout = yield* resolveCodexHomeLayout( - decodeCodexSettings({ - homePath: sharedHome, - shadowHomePath: shadowHome, - }), - ); - - yield* materializeCodexShadowHome(layout); - - const lockLinkResult = yield* fileSystem.readLink(shadowLock).pipe(Effect.result); - expect(lockLinkResult._tag).toBe("Failure"); - expect(yield* fileSystem.exists(shadowLock)).toBe(true); - }), - ); - it.effect.skipIf(!symlinksSupported)( "accepts Codex-created shadow-local runtime directories", () => diff --git a/apps/server/src/provider/Drivers/CodexHomeLayout.ts b/apps/server/src/provider/Drivers/CodexHomeLayout.ts index 2bbb11746fd24..6557a9be9862c 100644 --- a/apps/server/src/provider/Drivers/CodexHomeLayout.ts +++ b/apps/server/src/provider/Drivers/CodexHomeLayout.ts @@ -259,6 +259,30 @@ const ensureSymlink = Effect.fn("CodexHomeLayout.ensureSymlink")(function* (inpu }); } + if (input.entryName === "thread-writer-locks") { + const lockNames = yield* input.fileSystem.readDirectory(link).pipe( + Effect.catchTags({ + PlatformError: (cause) => + new CodexShadowHomeFileSystemError({ + sharedHomePath: input.sharedHomePath, + effectiveHomePath: input.effectiveHomePath, + operation: "readDirectory", + path: link, + entryName: input.entryName, + cause, + }), + }), + ); + // Codex deletes a thread lock on release, so one that is left may still guard a live + // writer. Keep the local directory until a later setup finds it empty. + if (lockNames.some((name) => name !== ".coordination.lock")) { + return yield* Effect.logWarning( + "Codex shadow home still has thread writer locks; it will share them on a later setup.", + { path: link }, + ); + } + } + yield* input.fileSystem.remove(link, { recursive: true }).pipe( Effect.catchTags({ PlatformError: (cause) => From 209cac13760639ec231a06e41fac52530193eed4 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Tue, 27 Oct 2026 21:47:41 -0700 Subject: [PATCH 3/3] fix(server): only thread lock files hold back the shadow lock repair Codex removes stale `.lock` files only, so any other file, such as Finder's .DS_Store, would keep the local directory forever. Co-Authored-By: Claude Opus 5.5 --- apps/server/src/provider/Drivers/CodexHomeLayout.test.ts | 1 + apps/server/src/provider/Drivers/CodexHomeLayout.ts | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/server/src/provider/Drivers/CodexHomeLayout.test.ts b/apps/server/src/provider/Drivers/CodexHomeLayout.test.ts index 425107b36ec98..2bbcea20faa91 100644 --- a/apps/server/src/provider/Drivers/CodexHomeLayout.test.ts +++ b/apps/server/src/provider/Drivers/CodexHomeLayout.test.ts @@ -240,6 +240,7 @@ it.layer(NodeServices.layer)("CodexHomeLayout", (it) => { const threadLock = path.join(shadowLocks, "019a0000-0000-7000-8000-000000000000.lock"); yield* writeTextFile(path.join(shadowLocks, ".coordination.lock"), ""); + yield* writeTextFile(path.join(shadowLocks, ".DS_Store"), ""); yield* writeTextFile(threadLock, ""); const layout = yield* resolveCodexHomeLayout( diff --git a/apps/server/src/provider/Drivers/CodexHomeLayout.ts b/apps/server/src/provider/Drivers/CodexHomeLayout.ts index 6557a9be9862c..f6d2ba9d44cf5 100644 --- a/apps/server/src/provider/Drivers/CodexHomeLayout.ts +++ b/apps/server/src/provider/Drivers/CodexHomeLayout.ts @@ -274,8 +274,8 @@ const ensureSymlink = Effect.fn("CodexHomeLayout.ensureSymlink")(function* (inpu }), ); // Codex deletes a thread lock on release, so one that is left may still guard a live - // writer. Keep the local directory until a later setup finds it empty. - if (lockNames.some((name) => name !== ".coordination.lock")) { + // writer. Keep the local directory until a later setup finds none. + if (lockNames.some((name) => name.endsWith(".lock") && name !== ".coordination.lock")) { return yield* Effect.logWarning( "Codex shadow home still has thread writer locks; it will share them on a later setup.", { path: link },