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
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
claudeAiOauthaccess token. Reads~/.claude/.credentials.json; ifexpiresAtis 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, preserve0600) with the newaccessToken/refreshToken/expiresAt, preserving all other keys (mcpOAuth,scopes,subscriptionType,rateLimitTier). Fail loud: raise on refresh HTTP error (caller catches for fallback). Exposeget_access_token() -> strandget_access_token(force_refresh=False). Client id + token URL sourced from the CLI's own OAuth config (grep~/.local/bin/claudebundle / existing debug logs for the token endpoint andclient_id); if not discoverable, module reads them fromFORGE_CLAUDE_OAUTH_CLIENT_ID/FORGE_CLAUDE_OAUTH_TOKEN_URLenv with the discovered defaults baked in as constants. - Verification:
python3 /home/justinwieb/forge/scripts/forge_claude_oauth_token.py --checkprints token length + minutes-to-expiry and exits 0; run twice and confirm.credentials.jsonstill valid JSON with mode600(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 callsGET https://api.anthropic.com/v1/code/sessions, followsnext_cursoruntil exhausted (cap ~10 pages / 200 rows to bound cost), using the token fromforge_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 anactive/archivedbreakdown. - 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 (stripcse_/session_prefix) and the lowercasedtitle, cached in a module-level dict with a ~45s TTL and anasyncio.Lock(mirror the existing_bridge_map_lockpattern). On builder exception, log once and returnNone(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, inlist_sessions()~line 451) - What: After the existing per-session enrichment, look up each session in
_claude_status_map()by bridge-id suffix first, then bytitle/name. Add"claude_status": "active"|"archived"|Noneto each row. When the map isNone(fallback), setclaude_status=Noneon every row so the UI reverts to tmux-only logic. Do not alteralive/urlsemantics. - Verification:
curl -fsS -H "Authorization: Bearer $SESSION_LAUNCHER_TOKEN" http://127.0.0.1:7365/api/sessions | jq '.sessions[] | {name, alive, claude_status}'showsclaude_statuspopulated (active/archived) for bridged sessions andnullfor 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 toalive && urlwhenclaude_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". Includes.claude_statusin 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 loadhttps://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.jsonaside 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/sessionsendpoint andstatusfield, 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 perfeedback_register_tools.md. - Verification:
bash /home/justinwieb/forge/scripts/forge_eval_run.shpasses;grep -c launcher_claude_status ~/.claude/projects/-home-justinwieb-forge/memory/MEMORY.mdreturns >=1. - Commit:
docs(launcher): register claude.ai status-sync tools + MEMORY index