Skip to content

Plan: chat.justin session sections mirror claude.ai active/archived status

URL: https://mkdocs.justinsforge.com/memory/plans/chat-justin-mirror-claude-web-archive-status-2026-07-05/

Date: 2026-07-05 Approved spec: chat.justin's session sections currently key off local tmux liveness. Rebucket them to mirror the claude.ai/code web UI instead: Active = claude.ai status == "active", Unlinked/archived drawer = status == "archived", plus a residual local-only bucket for workers/pool that never bridged. The launcher backend fetches GET https://api.anthropic.com/v1/code/sessions (paginated via next_cursor) using the existing claudeAiOauth token from ~/.claude/.credentials.json (scope user:sessions:claude_code), self-refreshing the hourly-expiring access token via refreshToken and atomically writing it back to .credentials.json so the CLI and launcher share one always-fresh token. A {bridge-suffix / title -> status} map is TTL-cached (~45s). Join to local tmux sessions by bridge-id suffix (local session_XXX == API cse_XXX) or by title == tmux name. Any API/token failure falls back silently to today's tmux-only sections so a hiccup never blanks the list. Archiving on the web only moves the card; the tmux process keeps running and Kill stays a manual button. Out of scope: - Auto-killing or purging archived sessions' tmux processes (display-only rebucket). - Changing the spawn, bridge, rebridge, or kill flows. - Touching Remote Control / bridge internals or the worker/pool lifecycle. - Any UI beyond the three section buckets (no new archive toggles, no web-UI actions from chat.justin).

Verification prerequisite

The launcher runs as a service (forge-session-launcher / bound 0.0.0.0:7365). Confirm the unit name before verifying:

systemctl --user list-units 'forge*launcher*' 2>/dev/null; systemctl list-units 'forge*launcher*' 2>/dev/null
Use whichever manager owns it in the systemctl ... restart / status steps below (tasks assume --user; swap to system scope if that is where it lives).

Task list

Task 1: OAuth token provider with self-refresh + atomic write-back

  • Files: /home/justinwieb/forge/scripts/forge_claude_oauth_token.py (new)
  • What: Standalone module that returns a valid claudeAiOauth access token. Reads ~/.claude/.credentials.json; if expiresAt is within a 5-minute skew, POST the OAuth refresh (grant_type=refresh_token, refresh_token, client_id) to the Anthropic OAuth token endpoint, then atomically rewrite .credentials.json (write temp in same dir + os.replace, preserve 0600) with the new accessToken/refreshToken/expiresAt, preserving all other keys (mcpOAuth, scopes, subscriptionType, rateLimitTier). Fail loud: raise on refresh HTTP error (caller catches for fallback). Expose get_access_token() -> str and get_access_token(force_refresh=False). Client id + token URL sourced from the CLI's own OAuth config (grep ~/.local/bin/claude bundle / existing debug logs for the token endpoint and client_id); if not discoverable, module reads them from FORGE_CLAUDE_OAUTH_CLIENT_ID / FORGE_CLAUDE_OAUTH_TOKEN_URL env with the discovered defaults baked in as constants.
  • Verification: python3 /home/justinwieb/forge/scripts/forge_claude_oauth_token.py --check prints token length + minutes-to-expiry and exits 0; run twice and confirm .credentials.json still valid JSON with mode 600 (stat -c '%a' ~/.claude/.credentials.json).
  • Commit: feat(launcher): claude.ai OAuth token provider with self-refresh + atomic write-back

Task 2: claude.ai code-sessions client (paginated fetch)

  • Files: /home/justinwieb/forge/scripts/forge_claude_sessions_client.py (new)
  • What: fetch_sessions() -> list[dict] that calls GET https://api.anthropic.com/v1/code/sessions, follows next_cursor until exhausted (cap ~10 pages / 200 rows to bound cost), using the token from forge_claude_oauth_token.get_access_token(). Headers: Authorization: Bearer, anthropic-version: 2023-06-01, anthropic-beta: claude-code-20250219. Returns each row trimmed to {id, title, status, status_bucket, worker_status, last_event_at}. On any HTTP/network error, raise (caller handles fallback). One retry after a forced token refresh on 401.
  • Verification: python3 /home/justinwieb/forge/scripts/forge_claude_sessions_client.py --json | python3 -c "import json,sys; d=json.load(sys.stdin); print(len(d),'sessions'); from collections import Counter; print(Counter(x['status'] for x in d))" prints a count and an active/archived breakdown.
  • Commit: feat(launcher): paginated claude.ai code-sessions client

Task 3: Status-map builder with TTL cache + join keys

  • Files: /home/justinwieb/forge/scripts/forge_session_launcher_api.py (edit)
  • What: Add an async helper _claude_status_map() returning {key -> status} where keys are BOTH the bridge-id suffix (strip cse_/session_ prefix) and the lowercased title, cached in a module-level dict with a ~45s TTL and an asyncio.Lock (mirror the existing _bridge_map_lock pattern). On builder exception, log once and return None (signal = "no data, fall back"), never raise into the request path. Import from Task 1/2 modules.
  • Verification: curl -fsS -H "Authorization: Bearer $SESSION_LAUNCHER_TOKEN" http://127.0.0.1:7365/api/sessions | jq '.sessions[0]' still returns 200 after restart (no regression); temporary debug log line confirms the map built with N entries (remove before commit or gate behind a debug flag).
  • Commit: feat(launcher): TTL-cached claude.ai status map with suffix+title join keys

Task 4: Annotate /api/sessions rows with claude.ai status

  • Files: /home/justinwieb/forge/scripts/forge_session_launcher_api.py (edit, in list_sessions() ~line 451)
  • What: After the existing per-session enrichment, look up each session in _claude_status_map() by bridge-id suffix first, then by title/name. Add "claude_status": "active"|"archived"|None to each row. When the map is None (fallback), set claude_status=None on every row so the UI reverts to tmux-only logic. Do not alter alive/url semantics.
  • Verification: curl -fsS -H "Authorization: Bearer $SESSION_LAUNCHER_TOKEN" http://127.0.0.1:7365/api/sessions | jq '.sessions[] | {name, alive, claude_status}' shows claude_status populated (active/archived) for bridged sessions and null for pure workers.
  • Commit: feat(launcher): expose claude_status on /api/sessions rows

Task 5: Rebucket the launcher UI by claude_status

  • Files: /home/justinwieb/forge/sites/justinsforge.com/chat/launcher.js (edit, session partition ~lines 694-707)
  • What: Change the partition: primary = claude_status === 'active' (fall back to alive && url when claude_status == null); others (Unlinked & ended drawer) = claude_status === 'archived' OR (fallback) !(alive && url); keep pool logic unchanged; add residual local-only handling so a bridged-but-null session still shows in primary if alive. Update the drawer label to "Unlinked, archived & ended". Include s.claude_status in the render signature (line ~123) so the list re-renders when status flips.
  • Verification: bash /home/justinwieb/forge/scripts/forge_chat_ext_sync.sh (if it syncs the site) then load https://chat.justinsforge.com/ui/; archive a test session in the claude.ai web UI, wait ~1 min, confirm its card moves from Active into the archived drawer without killing the process (row still shows a live dot / Open).
  • Commit: feat(launcher-ui): rebucket sections by claude.ai active/archived status

Task 6: End-to-end verification against real archive flow

  • Files: none (verification only; capture result in daily log)
  • What: Full-path test: pick one currently-archived session and one active session, hit /api/sessions, confirm they land in the right buckets in the live UI; then kill the launcher's network path (temporarily point token URL to an unreachable host via env, or revoke by moving .credentials.json aside for one poll) and confirm the UI falls back to tmux-only sections instead of blanking. Restore.
  • Verification: Observed: archived session in archived drawer, active in Active; with API forced-unreachable the list still renders via tmux fallback (no empty state, no error toast).
  • Commit: test(launcher): verify claude.ai status rebucket + graceful fallback (empty/allow-empty commit or note-only if nothing to stage)

Task 7: Register and document

  • Files: /home/justinwieb/.claude/projects/-home-justinwieb-forge/memory/MEMORY.md (edit), /home/justinwieb/forge/memory/general/reference_forge_chat_extension.md (edit — add the status-mirror behavior + token-refresh note) or new /home/justinwieb/forge/memory/general/reference_launcher_claude_status_sync.md
  • What: Document the new token provider + sessions client + status-sync behavior, the api.anthropic.com/v1/code/sessions endpoint and status field, the join-key rule (suffix + title), the TTL cache, and the fallback contract. Add a one-line MEMORY.md index entry under the launcher/surfaces section. Register the two new scripts per feedback_register_tools.md.
  • Verification: bash /home/justinwieb/forge/scripts/forge_eval_run.sh passes; grep -c launcher_claude_status ~/.claude/projects/-home-justinwieb-forge/memory/MEMORY.md returns >=1.
  • Commit: docs(launcher): register claude.ai status-sync tools + MEMORY index