fix(server): fork Claude threads in packaged desktop builds - #175
Merged
Conversation
The fork driver spawned a node subprocess that resolved @anthropic-ai/claude-agent-sdk by name. The packaged desktop server is a single bundle with the SDK inlined, so the resolve threw 'Cannot find module' and every Claude fork failed with 'Conversation fork failed'. Call the statically imported forkSession in-process instead. The SDK reads CLAUDE_CONFIG_DIR from process.env at call time, so the driver swaps the variable to the instance's resolved config dir for the duration of the fork, serialized through a queue so concurrent forks against different config dirs cannot interleave.
Serialize forks with an Effect semaphore instead of a promise queue so a canceled waiter never runs its fork, and skip the env swap entirely when CLAUDE_CONFIG_DIR already matches. Snapshot makeClaudeEnvironment's base env instead of returning process.env by reference, so a session starting mid-fork cannot observe the temporary override. Unify the injected and production fork seams on one input shape so adapter tests assert the resolved config dir production uses, and validate the SDK result before trusting its session id.
Drop the matching-env fast path (assigning an identical string is unobservable on a single thread), decode the SDK fork result with a schema instead of a predicate, share one ClaudeSessionForkInput type between the driver and the injectable seam, and centralize makeClaudeEnvironment's snapshot so every branch returns a copy.
Prefer the live source session's pinned configDirPath so a relative CLAUDE_CONFIG_DIR/HOME is not re-resolved against a cwd that moved into a worktree; stopped sessions keep the restart-equivalent resolution. Make mergeProviderInstanceEnvironment always return a copy so no driver retains live process.env across the fork driver's temporary override. Make the fork concurrency test actually prove serialization by passing dir (the SDK awaits realpath before reading CLAUDE_CONFIG_DIR) and assert env restoration.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Forking a Claude thread failed in every packaged desktop build with "Conversation fork failed: Failed to fork Claude session ...". The fork driver spawned a node subprocess that resolved
@anthropic-ai/claude-agent-sdkby package name, and the packaged server is a single bundle with the SDK inlined, so there is nonode_modulescopy to resolve and the subprocess died before it started. It also spawnedprocess.execPath, which in the packaged app is the Electron binary, withoutELECTRON_RUN_AS_NODE, so the subprocess route was broken twice over. Dev builds resolved the SDK from disk, which is why this never showed up locally.The driver now calls the statically imported
forkSessionin-process. The SDK readsCLAUDE_CONFIG_DIRfromprocess.envat call time (memoized, keyed on the value), so the driver sets it to the instance's resolved config dir for the duration of the fork and restores it after, with a queue serializing forks so two instances with different config dirs cannot interleave. The adapter pins that config dir with the sameresolveClaudeConfigDirPathcall session start uses, and drops its now-unusedChildProcessSpawnerdependency. The rewritten driver tests exercise the exact code path the packaged app runs: a real SDK fork into a temp config dir, error mapping plus env restoration for unknown sessions, and two concurrent forks landing in their own config dirs.Fable 5 via Claude Code (T3 Code).