Skip to content

Launcher claude.ai status sync (chat.justin active/archived mirror)

URL: https://mkdocs.justinsforge.com/memory/general/reference_launcher_claude_status_sync/

chat.justin's session sections mirror the claude.ai/code web UI: the primary list = sessions the web UI shows as active; the "Unlinked, archived & ended" drawer = sessions you archived on claude.ai. Archive a session in the web UI and it drops into the drawer on the next launcher refresh (~1 min), so the old manual purge workflow is gone. Built 2026-07-05, plan chat-justin-mirror-claude-web-archive-status-2026-07-05.

Data source

GET https://api.anthropic.com/v1/code/sessions returns every Claude Code session with a status of "active" or "archived" (mirrors the web UI), plus title (== the tmux session name), id (cse_<suffix>), status_bucket, worker_status, last_event_at. Paginates via next_cursor. Auth = Authorization: Bearer <claudeAiOauth accessToken> + anthropic-version + anthropic-beta: claude-code-20250219. Requires OAuth scope user:sessions:claude_code (already on the Claude Code login token).

The three scripts

  • scripts/forge_claude_oauth_token.py , get_access_token(force_refresh=False). Reads ~/.claude/.credentials.json; when the token is within 5 min of expiry it refreshes via POST https://platform.claude.com/v1/oauth/token (grant_type=refresh_token, client_id 9d1c250a-..., both env-overridable), then atomically writes the rotated token back to .credentials.json (0600 preserved). The whole read-refresh-write runs under an flock on ~/.claude/.credentials.json.forge.lock so the launcher and the CLI cooperate instead of clobbering each other's refresh token. --check prints token len + minutes-to-expiry. Why self-refresh: the launcher runs 24/7 but the token expires hourly and the CLI only refreshes while a claude process runs.
  • scripts/forge_claude_sessions_client.py , fetch_sessions(max_pages=10). Pages the endpoint, trims rows, force-refreshes the token once on a 401. --json dumps rows; bare run prints the active/archived count.
  • scripts/forge_session_launcher_api.py , _claude_status_map() builds {key -> active|archived} keyed by both the bridge-id suffix (exact) and the lowercased title (fallback), TTL-cached ~45s (CLAUDE_STATUS_TTL_S, CLAUDE_STATUS_MAX_PAGES=6). list_sessions() tags each row with claude_status; the UI (sites/justinsforge.com/chat/launcher.js, isActive()) buckets by it.

Join key

Local bridge id session_<suffix> and API id cse_<suffix> share <suffix>; _bridge_suffix() strips the prefix. Primary match is by suffix (from the row's bridge URL), fallback is tmux name == title.

Fallback contract (load-bearing)

If claude.ai is unreachable or the token can't refresh, _claude_status_map() returns a stale map or None; list_sessions() then sets claude_status=None on every row and the UI reverts to the old tmux-only rule (alive && url). A claude.ai hiccup must never blank the launcher's session list. Pure workers / warm-pool spares that never bridged also get claude_status=None and stay in their existing buckets.

Behavior notes

  • Archiving on the web moves the card only; the tmux process keeps running (Kill stays a manual button). Freeing the process is still a manual Kill.
  • Service: forge-session-launcher.service (user unit), ExecStart runs the repo working-tree file directly, so edits go live on systemctl --user restart. chat.justinsforge.com is served by this service (thin-shell extension iframes the live app), not a CF Pages static deploy.