Session Launcher API¶
URL: https://mkdocs.justinsforge.com/memory/general/reference_session_launcher/
Self-serve "+" button for spawning Claude Code Remote Control sessions without going through home-base. Built 2026-06-12; full report at self-serve-session-launcher-findings-2026-06-12.
| Piece | Where |
|---|---|
| Production | https://chat.justinsforge.com (Cloudflare Access "Justin Secure Only", app id 1383d9f5-9859-484a-be89-64a53122a104, 730h session; media-server tunnel ingress -> http://192.168.86.50:7365; live 2026-06-12, end-to-end verified via temp Access service token, since revoked) |
| Service | scripts/forge_session_launcher_api.py, FastAPI, port 7365, bind 0.0.0.0 (LAN + tailnet) |
| Unit | systemctl --user status forge-session-launcher (user unit, enabled) |
| Venv | infra/session-launcher/.venv |
| Auth | On chat.justinsforge.com: CF Access only; the API validates the edge-injected Cf-Access-Jwt-Assertion (PyJWT vs CF_ACCESS_TEAM_DOMAIN/CF_ACCESS_AUD in forge/.env), no token paste. Bearer SESSION_LAUNCHER_TOKEN remains the fallback for direct LAN/tailnet hits |
| UI widget | sites/justinsforge.com/chat/ (served by the API at / -> /ui/); originally lived in landing-v2/launcher/ but the hub promote wiped that dir, moved out of the redesign tree |
| Hub tile | "Sessions" pinned tile on the landing SERVICES registry -> chat.justinsforge.com |
| Hub embed (2026-06-12) | justinsforge.com chatbar: "+ New Chat" and "Active Chats (n)" expand an inline panel above the pinned row that pushes the page down (animated grid-template-rows 0fr->1fr; Justin rejected modal and slide drawer first). New Chat auto-clicks the widget's [data-act="new"] sheet; Active Chats clicks [data-act="cancel"] for the list; same button toggles closed. Live-session count badge via GET /chat-api/sessions. nginx on the apex (justinsforge-games.conf) proxies /chat-api/ -> 127.0.0.1:7365/api/ and aliases /chat-assets/ -> the chat UI dir, so the widget runs same-origin (no CORS, no token paste). CF_ACCESS_AUD in forge/.env is now comma-separated (chat app + Forge Hub apex app); the API validates the JWT against the list. Widget JS/CSS stay single-source in sites/justinsforge.com/chat/ |
| Preview | http://100.97.43.104:7365/ui/ (tailnet, still works) |
| Log | forge/logs/session-launcher.log |
Endpoints (all /api/* require the bearer token)¶
GET /healthz: pool counts + jobs in flight, no auth.GET /api/sessions: workers from/tmp/tmux-1000/spawn-*sockets, sorted bysession_activitydesc (most recently active first); liveness =pane_current_commandin {claude,node}; URL from/tmp/spawn-rc-<name>.logbridge regex; pool spares flaggedrole=pool-spare; includespool_by_modelcounts.POST /api/sessions{model, name?, prompt?}->{job}. Opus/Fable/Sonnet + no name + no prompt = warmforge_spawn_pool.sh claim <model>(~2s); anything custom or Haiku = coldforge_spawn_session.shwith argv prompt (30-60s). Never types a prompt into a live TUI (spawn-audit race).- Session-weight fields (2026-07-06):
vanilla,no_mcp,readonly_tools,effort,context_1m. See reference_session_prompt_toggles Session weight section. All force a cold spawn; Console-only. GET /api/dev-slotsrows includeurl(tailnet IP + port); the UI shows a "🔗 Open" button next to Teardown when the slot's dev server is listening.target: "vector"(2026-07-01): queues the job for the Vector pull daemon instead of spawning locally; UI checkbox "Start on Vector". Rejects resume/advanced. Full doc: reference_vector_session_launcher.POST /api/sessions/{name}/bridge: re-activate Remote Control on a live session with an unknown/expired bridge and return the fresh URL (same mechanics as pool activate_remote; needs the session's--debug-filelog, so pre-2026-06-11 sessions 409).- Advanced Prompt (
"advanced": true+ a prompt): a headless Sonnet scout (~/.local/bin/claude -p, AVX2-safe binary) researches forge context (MEMORY.md, memory/general/, system-map/, handoffs) and rewrites the raw ask into a curated worker briefing, then the session cold-boots with it via argv (curation makes warm handoff moot; prompt delivery is only safe at boot). Job exposesphase: curating|booting+curated_prompt; curator failure falls back to the raw prompt with a note. Curations logged tologs/session-launcher-curated.log. Tested 2026-06-12: 35s curation, real memory recall in the briefing. - UI served with
Cache-Control: no-cache+ versioned asset URLs (stale iOS-cached launcher.js once hid a whole round of fixes). - Last-chat time =
max(tmux session_activity, rc-debug-log mtime): tmux never sees Remote Control traffic, the debug log updates every exchange. GET /api/history: all transcripts viascripts/forge_session_list.py;POST /api/sessions {resume: <uuid>}bootsclaude --resume(additiveSPAWN_RESUMEenv inforge_spawn_session.sh) so the new session carries the full old conversation. UI: "All sessions" month-grouped dropdown with Resume buttons; "Re-link" (in each row's ▾ menu, confirm modal) mints a fresh claude.ai/code link when the old one is archived/dead.POST /api/history/summarize {ids}: lazy one-sentence chat summaries, one Haiku batch call per 40 transcripts, cached forever atdata/launcher_history_summaries.json; triggered by expanding a month in the UI.GET /api/jobs/{id}:booting | ready (with url) | ready-no-bridge | error.DELETE /api/sessions/{name}:tmux -L spawn-<name> kill-server+ reaps rc log + pool markers. home-base is 403-protected.GET /api/pool: raw pool status passthrough.
Rules baked in¶
- Zero spawn logic in the service; it only calls the canonical scripts and parses their stdout (
session:/url:lines). - Fully async subprocesses, no threads (feedback_no_threads_with_subprocess_run).
- Exposed ONLY behind Cloudflare Access at chat.justinsforge.com (Security Rule #4; justinsforge.com is never public). The Access app was created BEFORE the DNS/ingress so the hostname was never up unauthenticated.
- Prompt delivery (2026-07-07): the first prompt is delivered via a FILE, not argv. The API writes it to
data/session_prompts/<ts>_<name>.txtand passesSPAWN_PROMPT_FILE=<path>toforge_spawn_session.sh, which buildsclaude ... -- "$(cat <file>)"so the pane reads it at exec time. Reason: the spawn script types the whole boot command into the pane's bash viatmux send-keys, and tmux REJECTS a command line beyond ~16k chars ("command too long"), so a large inline prompt silently crashed the boot. Direct callers of the script (e.g./spawn) still pass the prompt as argv$3and keep the inline path. Prompt cap is 50k chars (_prompt_ok). Prompt files auto-prune after 1 day. - Attachments (2026-07-07): up to 6 images per launch. Client sends
images: [{data, name}](data = base64 data-URL; legacy singleimage/image_namestill merged in first). Saved todata/session_uploads/, each appended to the prompt as a Read-tool path reference AFTER curation. Console targets only (paths won't resolve on Vector). - First-prompt textarea auto-grows to fit content (JS
autogrow, capped 420px then scrolls);#fl-prompthasresize:none. - Robustness (2026-07-07): the client
api()helper now THROWS the server's reason on any non-2xx (was silently resolving 422/400/500 as a phantom success, which left the launcher spinning forever on a job id that never existed).launch()also guards for a missingjobid. Client-side length guard mirrors the 50k cap for instant feedback. - Quirk: tmux
kill-serverleaves the stale socket file; harmless,forge_tmux_socket_sweep.shreaps it. - Quirk: "REMOTE CONTROL FAILED, banner never appeared" under heavy concurrent-session load is bridge-registration lag, NOT a boot/prompt failure; the session is usually alive and usable, the API just missed the bridge URL in time.
- Quirk: the API raises at boot if the UI dir is missing (fail loud); a parallel site promote once wiped the old UI location and the silent
is_dir()skip masked it for a service restart.
[session-launcher_Fable5]