Skip to content

fix(server): fork Claude threads in packaged desktop builds - #175

Merged
pandec merged 8 commits into
devfrom
worktree-claude-fork-fix
Aug 27, 2026
Merged

fix(server): fork Claude threads in packaged desktop builds#175
pandec merged 8 commits into
devfrom
worktree-claude-fork-fix

Conversation

@pandec

@pandec pandec commented Aug 27, 2026

Copy link
Copy Markdown
Owner

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-sdk by package name, and the packaged server is a single bundle with the SDK inlined, so there is no node_modules copy to resolve and the subprocess died before it started. It also spawned process.execPath, which in the packaged app is the Electron binary, without ELECTRON_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 forkSession in-process. The SDK reads CLAUDE_CONFIG_DIR from process.env at 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 same resolveClaudeConfigDirPath call session start uses, and drops its now-unused ChildProcessSpawner dependency. 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).

pandec added 8 commits August 27, 2026 13:16
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.
@pandec
pandec merged commit 7f12fa7 into dev Aug 27, 2026
2 checks passed
@pandec
pandec deleted the worktree-claude-fork-fix branch August 27, 2026 11:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant