Skip to content

Claude account profiles: dormant WSL guest setup (Step 3 of 4) - #24384

Merged
Jinwoo-H merged 98 commits into
mainfrom
brennanb2025/claude-profile-wsl
Oct 7, 2026
Merged

Jinwoo-H merged 98 commits into
mainfrom
brennanb2025/claude-profile-wsl

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 8 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​515 $\color{#cf222e}{\Huge{\mathbf{−}}}$​1 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​514
Prod 16 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​517 $\color{#cf222e}{\Huge{\mathbf{−}}}$​65 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​452

ELI5

#24351 points every Orca Claude launch at the selected account's own folder. This PR does the same inside WSL: each account gets its own Claude folder inside the Linux distro, and Claude terminals in WSL use the selected one. It stays switched off until #24434, so nothing changes for users yet.

What Changed

Step 3 of 4 for STA-3698. Builds on #24351.

  • ClaudeWslProfileRouter gives each WSL distro the same four actions as the main router:
    • write the which-account file inside the distro;
    • prepare a launch, which waits only for an account that has never been set up;
    • return the launch settings;
    • give WSL terminals their environment.
  • Account setup runs as Linux inside the distro (claude-profile-wsl-entry.ts). It runs feat(claude): prepare account profiles and shared history (Step 1 of 4) #24300's setup on Orca's pinned Node runtime, the same one OpenCode's WSL reader already downloads, using the same cache and the same folder in the distro.
    • History links stay inside the distro, so they resolve in WSL.
    • Nothing is ever linked across the Windows/WSL boundary.
  • Stopped distros are never booted. Background updates skip any distro that isn't running.
  • Dev and packaged builds use separate which-account files in the distro.
  • Unchanged: OpenCode, and how Orca runs commands in WSL. Folding OpenCode onto the shared runtime installer is a separate follow-up.

Why

Setting up from Windows over \\wsl.localhost would be wrong:

  • Windows Node writes link targets in Windows form, which Linux can't follow.
  • File moves over that network share don't get the guarantees the setup relies on.

A plain shell script can't merge settings.json without tools a distro may not have. Reusing the runtime Orca already ships for WSL is the smallest correct option.

Linked Issue

Part of STA-3698 (fixed by #24434). Refs #9582.

Visual Proof

N/A. Switched off until #24434.

Testing

  • Unit tests cover the WSL router: publish, waiting only for a first setup, refusals, skipping stopped distros. A test bundles the guest helper and runs it.

  • Shell-setup snapshots and OpenCode's files are unchanged from the base.

  • Real-distro testing on a Windows machine with WSL is planned before fix(claude): activate account profiles and remove credential replay (Step 4 of 4) #24434.

  • I manually tested these changes locally

  • Automated tests added/updated, or explained why not below

Agent skill upstream boundary

  • Not applicable, or this change follows docs/reference/agent-skill-sharing-upstream-boundary.md and copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

Review round 1 of the dormant profile setup found that the pieces could
be called without their safety checks, that one failed write or an
unreadable bookkeeping file could silently stop sharing for good, and
that Windows prompt history could bring back history the user cleared.

- One entry, provisionClaudeAccountProfile: the profile gate (namespace,
  no linked components, outside ~/.claude and ~/.config/claude, and an
  ownership marker beside the home naming the account and target) runs
  first and refuses before creating anything; then history sharing,
  config provisioning, and the hook install after the settings merge.
  Results come back per surface with closed warning codes instead of
  message text.
- The sharing ledger is keyed by surface name, records a value only
  after its write succeeded, and an unreadable ledger starts empty and
  is rewritten instead of blocking every surface.
- The profile state file goes through the same locked writer as folder
  trust (Claude's <file>.lock plus the in-process queue), generalized as
  updateClaudeGlobalConfig. Onboarding and trust are still applied when
  the personal state file is unreadable.
- WSL descriptors build guest POSIX paths; the state-file path style
  follows the injected platform.
- Orca's managed statusLine has one owner in a profile: the settings
  merge never shares it, a user's own statusLine is shared over it, and
  the profile installer follows the default home's slot so a default
  opt-out reaches every profile. remove() takes the same destination;
  the remote installer cannot accept one.
- Prompt history compares file identity (bigint dev+ino) on every
  platform, never drains the shared file into itself, drains retained
  copies in generation order, never reuses a stale cursor, and on Windows
  keeps a replaced default's old copy aside instead of replaying it.
  Directory merges keep going past a failed entry.
A user's own Claude hooks in ~/.claude (notifications, formatters) did
not run under a managed account, because the whole hooks key stayed
private. They are now shared like any other settings key: Orca's own
hook entries and its managed statusLine are stripped from both the
personal value and the profile's current value before the per-key
ledger comparison, so they never travel through the merge and never make
the key look user-owned. Orca entries already in the profile are kept on
write, and the profile hook installer adds them on top as before.

Prompt history: merged bytes that lack a final newline are terminated,
so Claude's next record no longer fuses onto the last merged line. When
a CLI rewrote the profile's history file (old records plus new), only
the lines past the part it shares with the default history are added,
instead of the whole file again.
Hooks and statusLine sharing:
- When ~/.claude holds only Orca's hook entries, the user's shared hooks
  now read as an empty value instead of a missing key. Removing the
  user's last own hook in ~/.claude therefore reaches profiles that
  never edited it, and deleting the only shared hook inside a profile
  stays deleted.
- A custom statusLine Orca shared, and the profile never edited, goes
  away when the default home drops it. When a shared custom line
  replaced Orca's line in a profile, the profile's statusline marker is
  dropped so Orca's line comes back once the default returns to it; a
  profile that opted out stays opted out. No other key gains deletion.
- install/remove/getStatus with a profile directory refuse when it is
  the default home, or its settings.json resolves to the default one,
  instead of editing System Default's hooks and opt-out state.
- The profile statusline rule reads the default settings under the
  userHome passed to the setup entry, not os.homedir().

Profile state and ownership:
- A malformed `projects` value skips only folder trust (new warning
  code trust-refused); onboarding and shared keys still apply.
- The ownership marker stores only host-local facts (account, runtime,
  distro). The execution host id is the caller's view of the host, so
  it stays in the in-memory descriptor and is not compared.

Prompt history interruption paths:
- With no cursor yet, a retained copy starts past the bytes it shares
  with the default history, so an interrupted share no longer replays
  the whole history.
- A retained name for the shared file itself is removed with its cursor
  instead of lingering until a later scrub makes it look new.
- The Windows link record is read three-state: unreadable stops the
  share instead of reading as "no link". If the record cannot be
  written after linking, the fresh link is undone.
- An unreadable retained copy is reported and no longer blocks linking.
hook-service.ts and hook-settings.ts are compiled into the packaged CLI
project, which lists every file explicitly. The statusline policy and
profile destination modules they now import were missing, so the CLI
typecheck failed with TS6307. The CLI still loads hook-service through
the existing managed-agent-hook-controls build entry, which bundles
both modules; neither imports electron.
- A profile whose hooks hold only Orca's entries and that sharing never
  recorded is no longer treated as a user edit, so the user's first own
  hook in ~/.claude reaches it (for example when the profile was set up
  before ~/.claude had any hooks).
- A retained prompt-history file is removed as a second name for the
  shared file only when the default history does not itself link to it;
  otherwise it holds the only copy and is kept.
- Default-home checks compare file identity: the profile hook
  destination check uses device and inode, and the profile/default
  separation check resolves on-disk case, so a case-only alias of
  ~/.claude is refused on case-insensitive filesystems.
- A test pins that an unreadable leftover session tree no longer blocks
  linking.
QA found that removing a setting from ~/.claude never reached a managed
account: deleting the whole `hooks` block left the user's hook running
there. Only statusLine followed the default away.

Every shared key now follows the same rule through the existing per-key
ledger: when a key disappears from ~/.claude/settings.json (or
mcpServers/theme from the personal state file), it is removed from the
profile if the profile still holds exactly what Orca last shared. A
value changed inside the account is kept. Keys Orca never shared,
including denylisted ones, are never touched. Deleting the whole hooks
block removes the user's shared hooks and keeps Orca's own entries. A
missing source counts as empty; an unreadable source removes nothing.
…e by capability

Restores the inline mobile allowlist so its source-scan guard sees every
accounts.* method again, and the generated params catalog to generator order.
…ed config dir

The function is defined only in a routed pane where claude is a real
executable (the codex function's guard), re-reads the pointer only while
CLAUDE_CONFIG_DIR is unset or still Orca's injected twin, accepts Git Bash
drive paths, and starts on its own line after the fish/PowerShell codex text.
…lifecycle triggers

Round-1 review fixes for the dormant profile routing:
- Panes get the selected profile's CLAUDE_CONFIG_DIR plus an Orca twin at
  spawn, so nested shells and scripts inherit the account; System Default
  injects nothing and its home is the inherited CLAUDE_CONFIG_DIR.
- An absent routing owner is System Default, never a throw; AI Vault and
  session-search scans receive profile roots from their parent, and the
  capability is advertised only where an owner is installed.
- Account listing never throws: per-account readiness, a stale pointer is
  republished in the background and reported on the snapshot.
- Profiles are set up at select and startup; a launch only sets up one that
  never was, and a worker fault on a prepared profile is a warning. The
  Claude version probe is cached per binary identity.
- Pre-trust goes through the existing deadline- and realpath-guarded writer
  against the launch env's profile config.
- Skill discovery keeps a caller's Claude root and a broken Claude selection
  no longer fails other providers.
- The durable record carries a provider-neutral launchAccountHome, read
  through one helper by the launch fallback and the model catalog.
…published

A pointer left naming the previous account would launch it silently; a
missing pointer makes the claude function refuse visibly. A newer selection
that raced the failed one keeps its pointer.
…r than 3.4

Shell tests skip system config and abort unless claude resolves to the fake.
…fig dir lookup

- An overtaken publish that fails leaves the newer selection's pointer.
- The runtime config dir falls back to the legacy home for an unresolvable
  account or a WSL target, so skill roots never fail for other providers.
- WSL guest reader roots merge verbatim, never realpathed on this thread.
- History readers include ~/.claude, where step-1 setup pools profile history.
- System Default ignores a config dir an outer Orca injected (twin-marked).
…eate resolver

A Claude agent-env CLAUDE_CONFIG_DIR the create path stored is now the home
the launch pins and the model probe accepts.
A worker thread's os.homedir() ignores its own env, so the hook and
statusline scripts now go under the home the job names. The worker test pins
the process HOME to a sentinel, refuses to run unless the worker sees it, and
asserts nothing lands there.
…d of setting null

On .NET 9+ (pwsh 7.5+) SetEnvironmentVariable with $null creates an empty
variable, so stripped auth vars reached claude as empty strings and the
restore left CLAUDE_CONFIG_DIR empty in the user's session.
…file publishes

A WSL pane now gets the same non-throwing, guest-free spawn env as a host
pane; only select, startup and Claude launches publish into the guest.
Overlapping publishes of one target share the newest publish while the
selection still names the same profile, instead of failing as superseded.
Publish issues name their WSL distro and drop out when the target is no
longer routed. A late inspect from an older selection no longer replaces
the newer one's verification, a failed guest request evicts the cached
guest, and readiness is derived per account from the guest's owned homes.
With profiles, a failed select or remove republishes just its own target
instead of running startup over every WSL distro, and a rollback failure is
logged instead of replacing the error that caused the rollback.
Vault and usage scans pass Claude profile roots through the same
running-distro filter as every other WSL root, so a stopped distro's UNC
paths are never walked.
The helper only ever runs inside WSL from the desktop, so it moves out of
the SSH relay artifacts (no upload, no relay version change) into
out/relay/wsl beside the other WSL-only guest bundles. The three WSL bundle
resolvers share one candidate list.
…wnload per caller

The pinned Node runtime needs glibc 2.28, so a distro below the floor is
refused before any download with a message naming both versions, as SSH
hosts are. The shared download again owns its own deadline and each caller
waits on its own signal, and the OpenCode reader keeps its architecture
error text.
…h the WSL runner

A cached guest no longer carries its 180 s preparation deadline into later
requests. The helper runs through runWslProcess (stdin payload, WSL_UTF8),
the distro is confirmed running once per preparation and once per request,
a failed `claude --version` probe continues with an unknown version like
native setup, the helper resolves from the WSL bundle dir, and the guest
entry decodes stdin once so split UTF-8 survives.
Jinwoo-H and others added 22 commits October 6, 2026 18:40
…ing' into claude-profile-wsl-lane

# Conflicts:
#	src/main/ipc/pty/ipc/spawn-preflight.ts
#	src/main/ipc/pty/runtime/spawn-preflight.ts
…es, link setup folders by path

- Prompt-history drain appends only lines the shared file lacks, so a purge never re-adds lines.
- A set-aside history copy whose saved offset reaches its end is deleted on the next run.
- Setup folders link to the default home's own entry, not its resolved target.
- The profile gate and folder creation run once, in provisionClaudeAccountProfile.
- installHooks receives only configDir; drop a duplicate test key that fails CI.
…nother account; zsh claude function; setup timeout

- With account routing, a chat resume checks its transcript is in the launch folder; a missing one
  with a stored leaf refuses with historyInOtherAccount instead of starting fresh.
- The launch folder of a selected account comes from prepareLaunch(); the resolver stays for System default.
- zsh panes get the claude function like bash, fish and PowerShell (empty while routing is off).
- The setup worker is terminated after 60 s so a later launch can retry.
- Document that the setup marker means setup started, not finished.
…eturns; one relay bundle candidate list

- prepareLaunch awaits writePointer, so a missing or stale guest pointer cannot run another account.
- Startup's WSL republish runs inside serializeMutation, like rollback.
- relayBundleCandidates takes 'wsl'; the hook relay, browser relay and Claude helper use it, and
  wsl-relay-bundle-dirs.ts is gone.
- One setup-marker path helper for host and WSL; the guest pointer path is home-relative and only
  the pane value carries '~/'; drop a no-op esbuild external.
… is found in another folder

A transcript found in no known folder keeps the old stored-leaf resume.
…on current at write time; bound the pointer read

A selection made while a launch waited on setup was overwritten by the launch's stale account.
A hung \\wsl.localhost read no longer stalls startup's serialized publish.
…link tests on Windows

After Orca installs its hooks into an account, record the account's hooks in
the settings ledger so a later run can still bring the user's own hooks in.
Tests that create real symlinks now skip on Windows.
…laude function

Co-Authored-By: Claude <noreply@anthropic.com>
…ute path on every platform

Co-Authored-By: Claude <noreply@anthropic.com>
…g-lane

# Conflicts:
#	src/main/claude/claude-structured-launch-resolution.ts
#	src/main/native-chat/agent-session-wire/structured-agent-session-adapter.ts
#	src/main/native-chat/agent-session-wire/structured-agent-session-failure-text.ts
#	src/main/native-chat/agent-session-wire/structured-agent-session-pre-spawn-first-answer.test.ts
#	src/renderer/src/components/native-chat/agent-session-failure-words-text.ts
#	src/renderer/src/i18n/en-runtime-required.json
#	src/renderer/src/i18n/locales/en.json
#	src/renderer/src/i18n/locales/es.json
#	src/renderer/src/i18n/locales/fr.json
#	src/renderer/src/i18n/locales/ja.json
#	src/renderer/src/i18n/locales/ko.json
#	src/renderer/src/i18n/locales/zh.json
#	src/shared/agent-session-failure-copy.ts
#	src/shared/agent-session-failure-words.ts
#	src/shared/agent-session-failure.ts
#	src/shared/agent-session-refusal-details.ts
#	src/shared/agent-session-refusal-reason-words.ts
#	src/shared/structured-agent-session-dispatch-rejection.ts
A WSL profile's data root is a POSIX path, so building one from a Windows
temp dir fails the absolute-path check there.
@Jinwoo-H
Jinwoo-H changed the base branch from brennanb2025/claude-profile-routing to main October 7, 2026 05:00
# Conflicts:
#	src/main/claude-accounts/claude-profile-installed-router.ts
#	src/main/claude-accounts/claude-profile-router.ts
#	src/main/claude-accounts/runtime-auth-service.ts
#	src/main/claude/claude-structured-launch-resolution.ts
#	src/shared/claude-shell-function.test.ts
#	src/shared/claude-shell-function.ts
@coderabbitai

coderabbitai Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

📝 Walkthrough

Walkthrough

The changes add Claude profile provisioning and selection for WSL distros, including a bundled setup helper and a pinned guest Node runtime. Runtime authentication now routes eligible WSL targets through the WSL profile router. WSL terminal launches forward profile-related environment values, and POSIX and Fish wrappers expand profile pointer paths that begin with ~/. WSL relay bundle candidates are resolved through shared logic.

Priority: ⬇️ Low

Merge Risk: 🔵 Low · up to 9eae9

Before enabling WSL profiles, make incomplete setup retryable and preserve the original error when selection rollback fails. These defects do not currently block account switching because profile routing is off.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 52.17% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 23 functions across 24 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the change: dormant WSL setup for Claude account profiles. The step number adds useful context.
Description check ✅ Passed The description explains the change, its purpose, the implementation, and testing. It includes the required visual-proof N/A note and identifies linked issues. The manual-test and full lint, typecheck…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 1cae4235-4353-4aef-8260-0ba36e5acaef
📥 Commits

Reviewing files that changed from the base of the PR and between 8464b15 and 9eae996.

📒 Files selected for processing (24)
  • config/scripts/build-relay.mjs
  • src/main/agent-hooks/wsl-hook-relay-launch.ts
  • src/main/browser/wsl-browser-network-relay-launch.ts
  • src/main/claude-accounts/claude-profile-installed-router.ts
  • src/main/claude-accounts/claude-profile-paths.ts
  • src/main/claude-accounts/claude-profile-router.ts
  • src/main/claude-accounts/claude-profile-wsl-entry.ts
  • src/main/claude-accounts/claude-profile-wsl-paths.ts
  • src/main/claude-accounts/claude-profile-wsl-router.test.ts
  • src/main/claude-accounts/claude-profile-wsl-router.ts
  • src/main/claude-accounts/runtime-auth-service-wsl-runtime.test.ts
  • src/main/claude-accounts/runtime-auth-service.ts
  • src/main/daemon/pty-subprocess-wsl-launch.test.ts
  • src/main/daemon/pty-subprocess/shell-launch-plan.ts
  • src/main/ipc/pty/runtime/spawn-preflight-requested-shell.test.ts
  • src/main/providers/local-pty-provider-windows-shell-launch.test.ts
  • src/main/providers/local-pty-windows-spawn-environment.ts
  • src/main/ssh/relay-bundle-paths.test.ts
  • src/main/ssh/relay-bundle-paths.ts
  • src/main/wsl/wsl-pinned-runtime.test.ts
  • src/main/wsl/wsl-pinned-runtime.ts
  • src/shared/claude-shell-function.test.ts
  • src/shared/claude-shell-function.ts
  • src/shared/relay-artifacts.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 5 remain after this review.

Comment on lines +101 to +131
/** Waits for setup only for a folder that was never set up; otherwise launches at once. */
async prepareLaunch(distro: string): Promise<ClaudeRuntimeAuthPreparation> {
const { home, profile } = await this.resolve(distro)
await this.assertPresent(distro, profile)
if (profile && !(await guestStat(distro, claudeProfileMarkerPath(profile)))?.isFile()) {
await this.setUp(distro, home, profile.accountId).catch((error: unknown) => {
console.warn('[claude-profile] WSL account setup failed:', error)
throw new Error(CLAUDE_PROFILE_SETUP_FAILED_MESSAGE)
})
}
// Why: a missing or stale guest pointer would run the pane's `claude` under another account.
// Re-read the selection: one made during setup has already published its own pointer.
if (this.accountIn(distro)) {
const selected = this.selectedProfile(home, distro)
await writePointer(distro, this.pointerIn(home), selected?.home ?? '')
}
return this.preparationFor(distro, home, profile)
}

async preparation(distro: string): Promise<ClaudeRuntimeAuthPreparation> {
const { home, profile } = await this.resolve(distro)
await this.assertPresent(distro, profile)
return this.preparationFor(distro, home, profile)
}

/** Falling back would run the wrong account. */
private async assertPresent(distro: string, profile: ClaudeProfileDescriptor | null) {
if (profile && !(await guestStat(distro, profile.home))?.isDirectory()) {
throw new Error(CLAUDE_PROFILE_MISSING_MESSAGE)
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

rg -n 'marker|prepareClaudeProfileDirectory|prepareLaunch|setUp\(' src/main/claude-accounts/claude-profile-{paths,wsl-router,wsl-entry}.ts
sed -n '100,145p' src/main/claude-accounts/claude-profile-wsl-router.ts

Repository: stablyai/orca

Length of output: 4248


🏁 Script executed:

#!/bin/bash
set -u
printf '%s\n' '--- PR changed files ---'
git diff --name-only 8464b151c75b37bff37902638e9b7dc1d1d39a21...9eae996c483e4b96b8ab15a1ceda4d1bbd51ec24 -- src/main/claude-accounts || exit $?
printf '%s\n' '--- relevant files ---'
rg --files src/main/claude-accounts | rg 'claude-profile|wsl'
printf '%s\n' '--- marker/setup bindings ---'
rg -n -F -- 'prepareClaudeProfileDirectory' src
rg -n 'claudeProfileMarkerPath|CLAUDE_PROFILE_MARKER|markerPath|setup failed|setUp\(' src/main/claude-accounts
printf '%s\n' '--- router implementation ---'
sed -n '1,230p' src/main/claude-accounts/claude-profile-wsl-router.ts
printf '%s\n' '--- guest setup-related files ---'

Repository: stablyai/orca

Length of output: 16383


🏁 Script executed:

#!/bin/bash
set -u
printf '%s\n' '--- helper filename binding ---'
rg -n -F -- 'WSL_CLAUDE_PROFILE_HELPER_FILENAME' .
printf '%s\n' '--- guest setup function and marker operations ---'
rg -n 'prepareClaudeProfileDirectory|claudeProfileMarkerPath|ownership marker|markerPath|setup.*profile|profile.*setup|catch \(.*error' src/main/claude-accounts src/shared scripts resources 2>/dev/null || test "$?" -eq 1
printf '%s\n' '--- setup worker and setup implementation ---'
sed -n '1,240p' src/main/claude-accounts/claude-profile-setup.ts
sed -n '1,240p' src/main/claude-accounts/claude-profile-paths.ts
printf '%s\n' '--- WSL path implementation ---'
sed -n '1,240p' src/main/claude-accounts/claude-profile-wsl-paths.ts
printf '%s\n' '--- WSL router tests around setup and retry ---'
rg -n -C 5 'prepareLaunch|marker|setup failed|retry|runSetup|incomplete' src/main/claude-accounts/claude-profile-wsl-router.test.ts
printf '%s\n' '--- setup tail and helper invocation ---'
sed -n '220,280p' src/main/claude-accounts/claude-profile-wsl-router.ts

Repository: stablyai/orca

Length of output: 37946


🏁 Script executed:

#!/bin/bash
set -u
printf '%s\n' '--- WSL entry ---'
nl -ba src/main/claude-accounts/claude-profile-wsl-entry.ts
printf '%s\n' '--- relay build entry binding ---'
nl -ba config/scripts/build-relay.mjs | sed -n '325,365p'
printf '%s\n' '--- provisioning implementation ---'
nl -ba src/main/claude-accounts/claude-profile-provisioning.ts | sed -n '1,260p'
printf '%s\n' '--- report failure semantics ---'
nl -ba src/main/claude-accounts/claude-profile-report.ts | sed -n '1,230p'
printf '%s\n' '--- setup tests around failed surfaces/outcomes ---'
rg -n -C 6 'failed|refused|prepared|marker|profile.json|throws|throw' src/main/claude-accounts/claude-profile-setup.test.ts src/main/claude-accounts/claude-profile-provisioning.test.ts
printf '%s\n' '--- setup helper artifact conventions ---'
nl -ba src/shared/relay-artifacts.ts | sed -n '42,58p'

Repository: stablyai/orca

Length of output: 30505


🏁 Script executed:

#!/bin/bash
set -u
printf '%s\n' '--- WSL prepareLaunch callers ---'
rg -n -C 4 'prepareLaunch\(' src/main/claude-accounts/runtime-auth-service.ts src/main/claude-accounts/runtime-auth-service-wsl-runtime.test.ts src/main
printf '%s\n' '--- concrete transient state-lock failure and retry test ---'
nl -ba src/main/claude-accounts/claude-profile-provisioning.test.ts | sed -n '295,335p'
printf '%s\n' '--- relevant state merge and onboarding behavior ---'
nl -ba src/main/claude-accounts/claude-profile-provisioning.ts | sed -n '108,155p'
printf '%s\n' '--- marker and prepareLaunch exact lines ---'
nl -ba src/main/claude-accounts/claude-profile-paths.ts | sed -n '98,145p'
nl -ba src/main/claude-accounts/claude-profile-wsl-router.ts | sed -n '96,132p'
nl -ba src/main/claude-accounts/claude-profile-wsl-router.ts | sed -n '218,238p'

Repository: stablyai/orca

Length of output: 22730


Use setup completion, not profile.json, to decide whether to retry.

profile.json is written when setup starts. If a .claude.json.lock blocks state provisioning, setup records a warning but still returns prepared; the WSL launch can return the profile, and later launches skip setup because the marker exists. The profile can then open Claude’s onboarding wizard, where an accidental Enter can start a login and rebind the profile. Keep the ownership marker, but track setup completion separately and retry incomplete provisioning.

Comment on lines +104 to +111
/** Startup and rollback republish only running distros: neither may boot a stopped one. */
private async publishRunningWslDistros(): Promise<void> {
const router = this.wslRouter
if (router) {
const distros = await router.runningDistros()
await Promise.all(distros.map((distro) => this.publishWsl(router, distro)))
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

sed -n '80,165p' src/main/claude-accounts/runtime-auth-service.ts
sed -n '75,165p' src/main/claude-accounts/claude-account-selection.ts

Repository: stablyai/orca

Length of output: 6729


🏁 Script executed:

printf '%s\n' '--- router ---'
sed -n '1,190p' src/main/claude-accounts/claude-profile-wsl-router.ts
printf '%s\n' '--- selection/runtime target helpers ---'
rg -n -F -- 'resolveWslDefaultTarget' src/main
rg -n -F -- 'getClaudeSelectionTargetForAccount' src/main
rg -n -F -- 'wslDistro' src/main/claude-accounts
printf '%s\n' '--- runtime auth sync/selection declarations ---'
sed -n '1,125p' src/main/claude-accounts/runtime-auth-service.ts
printf '%s\n' '--- changed diff for relevant files ---'
git diff 8464b151c75b37bff37902638e9b7dc1d1d39a21 9eae996c483e4b96b8ab15a1ceda4d1bbd51ec24 -- src/main/claude-accounts/runtime-auth-service.ts src/main/claude-accounts/claude-profile-wsl-router.ts src/main/claude-accounts/claude-account-selection.ts

Repository: stablyai/orca

Length of output: 42033


🏁 Script executed:

printf '%s\n' '--- rollback callers and complete selection path ---'
rg -n -F -- 'forceMaterializeCurrentSelectionForRollback' src
sed -n '70,125p' src/main/claude-accounts/claude-account-selection.ts
sed -n '165,195p' src/main/claude-accounts/claude-account-selection.ts
printf '%s\n' '--- runtime selection declarations ---'
sed -n '1,165p' src/main/claude-accounts/runtime-selection.ts
printf '%s\n' '--- WSL distro settings/account contract ---'
sed -n '1,75p' src/main/claude-accounts/claude-managed-auth-storage.ts
sed -n '195,228p' src/main/claude-accounts/claude-managed-auth-storage.ts
printf '%s\n' '--- rate-limit preparation binding ---'
rg -n -F -- 'prepareForRateLimitFetch' src/main

Repository: stablyai/orca

Length of output: 25018


🏁 Script executed:

printf '%s\n' '--- getWslHomeAsync binding ---'
rg -n -F -- 'getWslHomeAsync' src/main/wsl.ts src/main/wsl
printf '%s\n' '--- WSL list/home implementation ---'
sed -n '210,255p' src/main/wsl.ts
printf '%s\n' '--- WSL process launch binding ---'
rg -n -F -- 'export async function runWslProcess' src/main/wsl
rg -n -F -- 'function runWslProcess' src/main/wsl

Repository: stablyai/orca

Length of output: 2399


🏁 Script executed:

sed -n '285,315p' src/main/wsl.ts
sed -n '180,245p' src/main/wsl/wsl-runner.ts

Repository: stablyai/orca

Length of output: 4328


Preserve the selection error when rollback fails.

If rate-limit preparation fails after the WSL pointer changes and runningDistros() then rejects, the rollback await exits before throw error. The caller receives the discovery error instead of the selection failure. Catch and log rollback errors, then rethrow the original error.

Suggested fix
     } catch (error) {
       this.restoreSettings(previousSettings)
-      await this.runtimeAuth.forceMaterializeCurrentSelectionForRollback()
+      try {
+        await this.runtimeAuth.forceMaterializeCurrentSelectionForRollback()
+      } catch (rollbackError) {
+        console.warn(
+          '[claude-account-selection] Failed to materialize the previous Claude selection:',
+          rollbackError
+        )
+      }
       throw error
     }

@Jinwoo-H
Jinwoo-H merged commit 64bb937 into main Oct 7, 2026
59 of 63 checks passed
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.

2 participants