feat(desktop,host-service): provision account profiles as complete environments with shared history

Merged
KI
3mo ago
+2103-193

Summary

Secondary provider accounts (the CLAUDE_CONFIG_DIR / CODEX_HOME profile dirs behind the Usage tab's account switching, #6585) were bare logins: an empty install with no skills, plugins, MCP servers, or settings — and a separate, fragmented conversation history. This PR provisions each profile from the default account so switching accounts keeps the whole environment:

  1. Config sharing (packages/agent-setup/src/provider-profiles.ts + profile-sharing.ts) — skills, plugins, agents, commands, output-styles, settings, MCP servers, CLAUDE.md; Codex prompts/, config.toml, AGENTS.md.
  2. Session-history sharing (packages/host-service/src/trpc/router/usage/profile-share.ts) — one projects/ transcript tree, --resume list, and prompt history across every Claude login.

Why token-swapping wasn't the design

Keeping one config dir and swapping credentials on switch was rejected: refresh tokens rotate on use, and a running session refreshes and rewrites the credential slot mid-session, so a swapped-in token gets clobbered and a parked copy goes stale (usage/claude.ts documents this as the "tokens are read-only" hard rule). Sharing state instead lets the provider CLIs own every login end to end.

How It Works

The rule: symlink where a symlink survives, copy/merge where it can't.

  • Symlinked — directories, and history.jsonl (append-only, written in place): capability dirs are linked at the default account's, so a skill or plugin installed later shows up in every account with no sync step; session dirs are linked so history is one tree.
  • Copied / key-merged — files the CLIs write with write-tmp-then-rename (settings.json, .claude.json, CLAUDE.md, config.toml): a rename would replace a symlink with a real file and silently fork the config. A per-profile ledger (.superset-profile.json) records exactly what provisioning wrote, so a value the user changes inside a profile is never clobbered by the next run.
  • Never shared: credentials/keychain, oauthAccount/userID identity, auth-related settings keys (apiKeyHelper, forceLoginMethod, …), credential env vars, Superset's lifecycle hooks (written per profile), and runtime dirs (daemon/, cache/, telemetry/).
  • Existing session state is merged, not clobbered: rename aside → symlink lands → files move into ~/.claude one by one. Renames preserve inodes, so a live session's open transcripts stay valid and its paths resolve through the link; conflicts wait in a .superset-merge dir and are retried.

Provisioning is idempotent and runs when an account is added (prepareAccount, generalized from prepareClaudeProfile to both providers), when one is selected, and at host boot for the selected accounts (usage/account-provisioning.ts). profile-seed.ts is absorbed into the .claude.json merge (forced hasCompletedOnboarding). The usage-history scanner realpath-dedupes its scan roots so a symlinked projects/ isn't walked and parsed once per profile. Docs: docs/agent-tooling.md gains a "Provider accounts (multi-login)" section.

Manual QA

Run live against a real secondary profile (~/.claude-work) while a session was running under it:

  • Live session survived the projects/sessions migration and kept writing (transcript readable through the link)
  • projects/ merged cleanly — 1,213 transcripts unified, zero conflicts; history.jsonl merged losslessly (26,757 + 39 = 26,796 lines)
  • claude --resume under the default account and under CLAUDE_CONFIG_DIR=~/.claude-work shows the identical session picker (recorded demo)
  • Full converged provisioning re-run on the provisioned profile is a clean no-op: skills/=unchanged plugins/=user-owned agents/=unchanged commands/=unchanged CLAUDE.md=unchanged settings.json=unchanged .claude.json=unchanged
  • User-owned surfaces respected (real plugins/ dir left alone; the bundled Superset plugin is then written into it directly)

Testing

  • bun test packages/host-service/src/trpc/router/usage/ — 17 pass (merge, conflict-leftover, interrupted-merge recovery, history append, guards; removal never deletes through shared links)
  • bun test packages/agent-setup/src/ — 143 pass (ledger semantics, link/sync/merge outcomes, settings/env filtering, onboarding force, Codex)
  • bun run typecheck in host-service, agent-setup, and desktop
  • biome clean on changed files

Design Decisions

  • Two mechanisms instead of one: symlinks alone silently fork on the first rename-write of a JSON file; copies alone go stale for capability dirs. Each surface gets the one that survives its CLI's write pattern.
  • Ledger over blind overwrite: per-profile divergence is legitimate (a profile-specific setting) — provisioning only overwrites what it last wrote.

Rollback

Revert the commit. Already-linked profiles keep working — the links are plain symlinks and copies the CLIs follow natively; no schema or persisted-state changes.

Summary by CodeRabbit

  • New Features

    • Claude and Codex profiles inherit skills, plugins, MCP servers, and settings from the default account.
    • Claude session history and state can be shared across profiles while preserving credentials and identity.
    • Profile setup runs automatically at startup and when switching accounts.
    • Shared configuration safely supports existing profiles and symlinked directories.
  • Bug Fixes

    • Duplicate or symlinked log locations are scanned only once.
    • Removing profiles no longer deletes linked shared data.
  • Documentation

    • Added Usage documentation and clarified shared versus profile-specific data.