Skip to content

Happy fleet control (mobile Claude/Codex sessions)

Status: Current | Last reviewed: 2026-08-07

One mobile/web app that shows the Claude Code / Codex sessions running in every dev environment (Vanguard + the eight cities), separated per environment, with live conversation view, replies, permission-prompt approval, and the ability to spawn new sessions remotely (directory, agent, model, permission mode).

Built on Happy (MIT, E2E-encrypted):

phone / browser ──HTTPS+WSS──▶ happy.trueserve.deloitte.com      (tooling EC2, Kind ingress)
                                └─ happy-relay pod :3005 (sync server + web app,
                                   stores only E2E-encrypted blobs; Postgres 16
                                   sidecar on a PVC — PGlite is broken upstream)
                                     ▲ outbound WSS only (socket.io at /v1/updates)
   vanguard devcontainer ──── happy daemon (Overmind `happy-daemon`)
   each city devcontainer ─── happy daemon   ← spawns `happy claude|codex` on demand
   VS Code Claude extension ── claudeProcessWrapper → happy wrapper → live in app

Because every daemon connects outbound, this works unchanged on the SSM-only city boxes — no inbound ports, no new nginx server blocks on the environments.

Pieces

Piece Where What
Relay (server + web app) tooling EC2, Kind ns tooling, happy.trueserve.deloitte.com tools/happy-relay/ — image, manifest, deploy script
Daemon every devcontainer, Overmind happy-daemon .devcontainer/scripts/start-happy-daemon.sh — pins the happy CLI under the unprivileged user's ~/.local, writes ~/.happy/settings.json serverUrl, waits for pairing, exec happy daemon start-sync
VS Code capture machine-scoped setting claudeCode.claudeProcessWrapper (seeded by devcontainer.json) .devcontainer/scripts/happy-vscode-wrapper.sh — routes fresh extension stream-json chat sessions through happy claude; utility probes and resume launches fall back to the bundled binary
History import run inside an env tools/happy-relay/import-claude-sessions.sh — re-homes Claude sessions (~/.claude/projects/**.jsonl, via happy claude --resume) AND Codex rollouts (~/.codex/sessions/**/rollout-*.jsonl, via happy codex --resume); refuses sessions modified <5 min ago (likely open in VS Code — a second attach would fight the first) unless --force

Per-environment setup (one-time)

  1. Make sure the Procfile entry is present (rides the normal vanguard → city update flow) and overmind status shows happy-daemon. Until paired it logs "not authenticated — run 'happy auth login'" every 30 s.
  2. In a devcontainer terminal: happy auth login → scan the QR with the Happy app (or approve from the web app at https://happy.trueserve.deloitte.com). This writes ~/.happy/access.key.
  3. overmind restart happy-daemon. The machine appears in the app.
  4. In the app, open the machine and rename it (hostnames are ip-10-120-…) to the environment name — that's the per-environment separation.

Mobile apps (iOS/Android): install Happy, then in Settings → Server set https://happy.trueserve.deloitte.com before logging in, so the app and the daemons share one account/relay. The web app needs no configuration (the relay injects its own URL).

Container rebuild: ~/.happy lives in the container home like ~/.claude — a devcontainer rebuild wipes the pairing; re-run step 2.

What you can do from the app

  • See each environment's machine + its active sessions; live transcript, tool calls, permission prompts (approve/deny from the phone), push notifications when an agent needs input.
  • Spawn a new session on any online machine: pick directory, agent (claude / codex / gemini), model mode, and permission mode.
  • Take over / hand back: a session started in a terminal or the VS Code extension can be controlled from the phone; pressing a key in the original terminal takes control back.

VS Code extension capture (verified working 2026-07-08)

The extension launches Claude through claudeCode.claudeProcessWrapper (machine scope). The wrapper receives the bundled claude binary as its first argument and exports it as HAPPY_CLAUDE_PATH. Fresh extension chat streams (--output-format stream-json plus --input-format stream-json) go through happy claude, so they appear in the app like terminal ones. Utility probes (auth status --json, --version, mcp, doctor, etc.) and VS Code resume launches go directly to the bundled binary so the extension sees clean Claude stdout.

Resume capture is intentionally disabled for now: Happy 1.1.10 plus the stable-resume-tag dist patch can reload an existing relay row with null metadata and crash before Claude starts with Cannot read properties of null (reading 'path'). Direct Claude resumes keep the editor usable; fresh VS Code sessions still show in Happy.

Session titles: Happy injects a system-prompt instruction so Claude calls the mcp__happy__change_title MCP tool at the start of each new chat — new sessions name themselves in the app automatically. A resumed/imported session keeps a generic name until its next turn (or ask it to set a title).

Safety valves:

  • Not paired yet / happy missing → wrapper execs the real binary (extension behaves exactly as stock).
  • Utility commands / resume launches → wrapper execs the real binary to keep VS Code's auth/status JSON and SDK stream protocols clean.
  • Upstream jank warning: slopus/happy#379 (permission prompts + local↔remote switching). If it misbehaves, clear the claudeCode.claudeProcessWrapper setting (Remote → Machine settings) and reload the window; sessions revert to invisible-but-stock.
  • Codex has no equivalent wrapper setting (the ChatGPT extension's chatgpt.cliExecutable swaps the whole CLI binary and talks a proprietary app-server protocol — not wrappable). Live Codex-in-VSCode threads stay uncaptured; to see a Codex conversation in Happy afterwards, import its rollout with the import script (happy codex --resume <thread-id>), or run Codex work via daemon-spawn / terminal happy codex when you know you'll want mobile access.

Two-way live sync (TrueServe happy-cli fork)

Stock happy is one-directional for live rendering: VS Code turns stream to the app, but a turn you send from the phone/web flips the session to remote mode — happy drives Claude via the SDK and streams only to the relay, so the VS Code webview goes silent until you take control back (type in VS Code) or reopen the conversation (upstream #379). The conversation itself never desyncs (one shared transcript); only the in-editor rendering of remote turns is missing.

The fork closes that gap: in remote mode it also tees each agent turn back onto happy's own stdout in the wrapping extension's wire protocol, so phone/web turns render live in VS Code too. It's a ~90-line additive change (src/utils/extensionStdoutEcho.ts + a one-line hook in the Claude remote launcher), exported as tools/happy-relay/patches/0001-extension-stdout-echo.patch and applied by tools/happy-relay/build-happy-fork.sh (clone pinned happy → git apply → build → swap the global happy dist, keeping dist.orig-<version> for revert). Gated: echoes only when launched by an extension (--output-format stream-json for Claude; app-server for Codex) on a non-TTY stdout; HAPPY_DISABLE_EXTENSION_ECHO=1 opts out; a broken pipe is swallowed (relay stays source of truth).

Codex: the echo module is agent-agnostic and the gate already recognizes the ChatGPT extension's codex app-server launch, but Codex needs its turn events translated into app-server JSON-RPC notifications (item/completed, codex/event) before the echo — that translation is the Codex passthrough work tracked separately. Once that lands, it calls the same echoStreamJsonLine(...) seam. The module deliberately does NOT assume Claude's stream-json shape.

Upgrading

Bump the pinned versions together (client/server wire compat): happy-server-self-host in tools/happy-relay/Dockerfile and HAPPY_VERSION_PIN in start-happy-daemon.sh. Redeploy the relay (see its README), then overmind restart happy-daemon per environment. Re-run build-happy-fork.sh after any CLI version bump — a plain npm i -g happy@<new> overwrites the forked dist and reverts to one-way sync.

Troubleshooting

Symptom Check
Machine missing in app overmind connect happy-daemon — unpaired loop? run happy auth login.
Machine online, spawn fails Daemon spawns need claude/codex on PATH in the container; check happy doctor.
Web app loads but login loops Relay pod restarted with a fresh HANDY_MASTER_SECRET (auth state reset) — check the happy-relay k8s Secret wasn't recreated; re-pair if it was.
Phone: "Not a valid Happy server" The app probes GET / for the literal Welcome to Happy Server! banner — our image embeds it as an HTML comment in the webapp index.html (curl -s https://happy…/ \| grep -c Welcome should print 1). If 0, the Dockerfile marker step was lost in a rebuild.
Phone linked but sees nothing; relay logs spam P2025 "No record was found" That client holds credentials for an account that no longer exists (server DB was reset). Clear the app's storage / log out fully, re-set the server URL, then link via web app → Settings → Link new device.
Message sent from phone seems to vanish (1) The reply may just be slow — open the session and wait for streaming. (2) If the session was IMPORTED while also open in VS Code, two processes share one transcript and the idle import eats the message — kill its tmux window (tmux kill-window -t happy-imports:<name>); see the import guard note.
Sessions stall / disconnect kubectl -n tooling logs deploy/happy-relay -c happy-relay on the tooling box; websocket path is /v1/updates behind ingress-nginx with 3600 s read/send timeouts. Note the relay pod drops all websockets during a redeploy; clients auto-reconnect.
Extension session not in app Wrapper only engages when paired; confirm ~/.happy/access.key exists and the machine setting points at the wrapper script, then reload the VS Code window.
VS Code logs claude auth status parse failed: Unexpected token 'D', "[DEV] Sessi"... The extension routed auth status --json through Happy and tried to parse Happy's [DEV] Session... banner as JSON. Use the current wrapper, which bypasses Happy for utility probes.
VS Code launch exits with Cannot read properties of null (reading 'path') Happy hit the stable-resume-tag/null-metadata bug on a --resume launch. Use the current wrapper, which bypasses Happy for VS Code resumes; clear the wrapper setting only if you need an immediate stock fallback.
Duplicate sessions (same conversation appears many times) Stock happy generates a random session tag per launch; our dist-patch uses the --resume ID as the tag so reloads reattach. Verify with grep 'args\[i\] === "--resume"' "$HAPPY_LIB/dist/index-BuEUifH5.cjs" (should find 1 match). If unpatched, run bash tools/happy-relay/apply-dist-patches.sh and restart the daemon. Prune stale entries: python3 -c 'import json,os; p=os.path.expanduser("~/.happy/sessions.json"); d=json.load(open(p)); d["sessions"]={k:v for k,v in d["sessions"].items() if os.path.exists(f"/proc/{v[\"metadata\"][\"hostPid\"]}")}; json.dump(d,open(p,"w"),indent=2)'.
Session titles not updating Title is set per Happy relay session; if duplicates were created, the titled session is a stale zombie and the current one is untitled. Fix the duplicate problem (above) and reload — the mcp__happy__change_title call fires on the first turn of each new chat.

See also