fix(desktop): allow V1 and V2 to run concurrently#12480

merged6
t3code/codex-turn-mapping
fix/v2-concurrent-desktop-profile
openedSep 18, 2026byJJulius Marminge
View on GitHubTwo-way sync enabledPR last imported 22d ago

Opening V2 while V1/nightly is running can leave the app with no environments because both desktop processes open the same Chromium profile. In a macOS two-process reproduction, the second process fails to open t3code:connection-runtime with an IndexedDB backing-store lock error.

Packaged V2 now uses its own t3code-v2 profile and never falls back to the V1 profile. On Windows, the new profile imports only Local State once to preserve the encryption keys used by saved connection credentials. All profile selection and Windows initialization now live in DesktopUserData.resolveUserDataPath, shared by startup and the Clerk bridge. Development profile handling stays unchanged. This profile is Electron storage under the OS application-data directory; the server T3 home and its databases stay where they are.

Targets #2829.

Validation:

  • 20 focused desktop tests pass. New coverage is limited to profile isolation and Windows credential preservation/error context.
  • Targeted lint and desktop typecheck pass.
  • Isolated macOS Electron reproduction: shared profile fails; separate V2 profile opens IndexedDB and commits a write while V1 stays running. This checks the storage failure directly, rather than launching the complete T3 app.

Before, shared profile:

Shared Electron profile fails to open IndexedDB

After, separate V2 profile:

Separate V2 profile opens IndexedDB and commits a write

Browser cookies and caches do not migrate, so websites opened inside V2 may require signing in again.

Prepared with Codex.

apps/desktop/src/app/initializeV2DesktopProfile.tsline 42Outdated · 2bef6d4Open thread
……@@ -0,0 +1,44 @@
39+ ),
40+ );
41+ }).pipe(
42+ Effect.mapError((cause) => new V2DesktopProfileInitializationError({ destinationPath, cause })),

This wraps every filesystem failure in the pipeline with only the destination directory, so failures while probing or reading the source lose the resource and stage where they occurred. The error convention requires structured, bounded context at the known translation boundary.

Suggested fix: map each fallible filesystem operation where it occurs, recording the relevant source/destination path, stage, and normalized filesystem category while preserving the immediate error as cause.

Posted via Macroscope — Effect Service Conventions

Fixed in ace0d4421df. Each filesystem operation now records its operation and relevant resource path, normalizes the category from the filesystem reason, and preserves the immediate error as cause. Added a regression test for a failed source read; focused tests, lint, and desktop typecheck pass.

Synced from GitHub

Effect Service Conventions found one blocking issue in apps/desktop/src/app/initializeV2DesktopProfile.ts: filesystem failures are wrapped at the whole-pipeline boundary without preserving the operation stage or relevant source resource. An inline review comment provides the expected fix.

Posted via Macroscope — Effect Service Conventions

Synced from GitHub

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ℹ️ No successful main baseline artifact is available yet. This run establishes the initial measurement.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire—4.9 KiB—6.8 KiB✅
CodexThread snapshot wire—3.7 KiB—4.9 KiB✅
CodexLive turn WebSocket wire—1.2 KiB—2.0 KiB✅
CodexLive turn WebSocket decoded—20.4 KiB—29.3 KiB✅
CodexLive turn messages—2—8✅
ClaudeTotal thread wire—4.9 KiB—6.8 KiB✅
ClaudeThread snapshot wire—3.7 KiB—4.9 KiB✅
ClaudeLive turn WebSocket wire—1.2 KiB—2.0 KiB✅
ClaudeLive turn WebSocket decoded—20.8 KiB—29.3 KiB✅
ClaudeLive turn messages—2—8✅

Baseline: unavailable · PR result: 7f1069e · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 106.1 KiB
  • Claude decoded thread snapshot: 106.4 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

Synced from GitHub

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This PR introduces startup-time profile migration and changes the production Electron user-data default to t3code-v2, affecting concurrent operation, browser state, and Windows credential metadata. A reported fresh-install startup failure remains unresolved, so the cross-platform initialization behavior requires human validation.

Adjust the Minimum Blocking Severity for this repo — including turning it Off — in Settings. You can add or adjust custom eligibility rules. Learn more.

Added 4 commits

  • fix(desktop): allow V1 and V2 to run concurrentlyc19a1cb
  • test(desktop): focus profile regression coverage49ae8c3
  • refactor(desktop): centralize Electron profile selectiondafb1a0
  • fix(desktop): preserve profile initialization error contextf003b04
apps/desktop/src/app/DesktopAppIdentity.tslines 87–89Outdated · dafb1a0Open thread
……@@ -112,10 +84,9 @@ export const make = Effect.gen(function* () {
118- ),
87+ const userDataPath = DesktopUserData.resolveUserDataPath(environment).pipe(
88+ Effect.provideService(FileSystem.FileSystem, fileSystem),
89+ Effect.provideService(Path.Path, environment.path),
MmacroscopeappBoton lines 87–89Sep 18, 2026

This hides Path.Path by extracting a service instance from DesktopEnvironment and providing it manually. Production Effect dependencies should remain in the operation’s environment so make/layer expose them for composition. Let resolveUserDataPath acquire Path.Path from context directly, and remove the now-unused Path import.

Posted via Macroscope — Effect Service Conventions

Fixed in 7aad2a497bd. Identity now captures the FileSystem and Path dependencies from the Effect context during make, so both requirements are exposed in its make/layer types. The deferred resolution operation uses that context, following DesktopAssets.make, and no longer extracts Path from DesktopEnvironment. All 20 focused tests, targeted lint, and desktop typecheck pass.

apps/desktop/src/app/DesktopClerk.tslines 93–95Outdated · dafb1a0Open thread
……@@ -88,13 +89,10 @@ export const make = Effect.gen(function* () {
92+ // The SDK bridge acquires Electron's profile-scoped single-instance lock.
93+ const userDataPath = yield* DesktopUserData.resolveUserDataPath(environment).pipe(
94+ Effect.provideService(Path.Path, environment.path),
95+ );
MmacroscopeappBoton lines 93–95Sep 18, 2026

This manually injects the Path.Path service stored inside another service, hiding the dependency from DesktopClerk.make and its layer. Let resolveUserDataPath acquire Path.Path from the Effect environment and remove the now-unused Path import.

Posted via Macroscope — Effect Service Conventions

Fixed in 7aad2a497bd. Clerk now calls the shared helper without supplying a Path service; its make/layer expose Path.Path for composition through NodeServices. The existing tests now supply NodePath.layerPosix explicitly. All 20 focused tests, targeted lint, and desktop typecheck pass.

Synced from GitHub

Effect Service Conventions found two blocking instances where Path.Path is manually extracted from DesktopEnvironment and provided to DesktopUserData.resolveUserDataPath, hiding the dependency from layer composition. Inline review comments identify both consumers and the expected fix.

Posted via Macroscope — Effect Service Conventions

Synced from GitHub

Effect Service Conventions found two blocking issues in DesktopAppIdentity.ts and DesktopClerk.ts: both manually inject Path.Path from DesktopEnvironment, hiding that dependency from Effect layer composition. Inline review comments provide the expected fix.

Posted via Macroscope — Effect Service Conventions

Synced from GitHub

Effect Service Conventions found two blocking issues. See the inline review comments on DesktopAppIdentity.ts and DesktopClerk.ts for the required dependency-composition fixes.

Posted via Macroscope — Effect Service Conventions

Added 1 commit

  • refactor(desktop): expose profile path dependencies7aad2a4
apps/desktop/src/app/DesktopUserData.tslines 63–67Outdated · 7aad2a4Open thread
……@@ -0,0 +1,98 @@
64+ ? legacyPath
65+ : path.join(input.appDataDirectory, "t3code");
66+ const sourceState = path.join(sourcePath, "Local State");
67+ if (!(yield* inspect(sourceState))) return destinationPath;
MmacroscopeappBoton lines 63–67Sep 18, 2026

🟠 High app/DesktopUserData.ts:63

The upgrade skips importing Windows safeStorage keys when T3 Code (Alpha) exists without Local State, even if the V1/nightly t3code profile contains it, leaving saved connection credentials inaccessible. The sourcePath selection checks only whether the Alpha directory exists; select the source based on each candidate's Local State instead, falling back to t3code when Alpha lacks it.

diff
1- const sourcePath = (yield* inspect(legacyPath))
2- ? legacyPath
3- : path.join(input.appDataDirectory, "t3code");
4- const sourceState = path.join(sourcePath, "Local State");
5+ const legacyState = path.join(legacyPath, "Local State");
6+ const sourceState = (yield* inspect(legacyState))
7+ ? legacyState
8+ : path.join(input.appDataDirectory, "t3code", "Local State");
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/desktop/src/app/DesktopUserData.ts around lines 63-67:
2
3The upgrade skips importing Windows safeStorage keys when `T3 Code (Alpha)` exists without `Local State`, even if the V1/nightly `t3code` profile contains it, leaving saved connection credentials inaccessible. The `sourcePath` selection checks only whether the Alpha directory exists; select the source based on each candidate's `Local State` instead, falling back to `t3code` when Alpha lacks it.

Fixed in 7f1069e9e58. Source selection now checks the legacy Local State file and falls back to the nightly file when the Alpha directory is empty. The existing nightly credential-preservation test now creates that empty Alpha directory; it fails before the fix and passes after it. All 20 focused tests, targeted lint, and desktop typecheck pass.

Added 1 commit

  • fix(desktop): find Windows keys in the populated profile7f1069e
……@@ -0,0 +1,98 @@
57+ return (yield* inspect(legacyPath)) ? legacyPath : destinationPath;
58+ }
59+ // Chromium databases require their own profile for each running version.
60+ if (input.platform !== "win32") return destinationPath;

🟠 High app/DesktopUserData.ts:60

resolveUserDataPath returns a nonexistent t3code-v2 directory on fresh non-Windows launches and on Windows when no legacy Local State exists, so the subsequent Electron.app.setPath("userData", ...) throws and aborts startup. Create destinationPath before each of these returns.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/desktop/src/app/DesktopUserData.ts around line 60:
2
3`resolveUserDataPath` returns a nonexistent `t3code-v2` directory on fresh non-Windows launches and on Windows when no legacy `Local State` exists, so the subsequent `Electron.app.setPath("userData", ...)` throws and aborts startup. Create `destinationPath` before each of these returns.

Verified as a false positive with an isolated native Electron 44.4.2 process, the version pinned on this branch. Before ready, app.setPath(userData, target) succeeded for a nonexistent nested directory, and app.getPath(userData) returned that target. Result: {electron:44.4.2,existsBefore:false,setPathSucceeded:true,pathMatches:true,existsAfter:false}. The pinned implementation uses OverrideAndCreateIfNeeded with is_absolute=true and create=false: https://github.com/electron/electron/blob/v44.4.2/shell/browser/api/electron_api_app.cc#L893-L904. The SDK subsequently acquires the profile lock and creates its directory. This does not abort startup, so no extra filesystem initialization is needed.