name: Remote Bridge Bot (Phase 4.2.5 + spawn-by-default 2026-04-29) description: 4th Telegram bot, @forge_remote_bridge_bot. SPAWN-BY-DEFAULT: every text/voice message becomes a fresh opus tmux Remote Control session. Slash commands for /sessions, /kill, /ask escape hatch. type: reference originSessionId: c45564c1-64ed-485b-8a40-14907a996d2e
Remote Bridge Bot, Phase 4.2.5 + spawn-by-default flip¶
Originally built 2026-04-29 as "claude -p shell on a Telegram bot." Re-identified later that day as a session spawner + relay: every plain message creates a fresh opus tmux Remote Control session that pops in Justin's claude.ai/code web UI. He can then drive the heavy work in the real Claude Code session, with the bot acting as the on-ramp from his phone.
Identity¶
| Layer | Behavior |
|---|---|
| Plain text or voice (no slash) | Routes through Sonnet brain (see below). Default outcome is still spawn opus, but brain can pick kill, propose_kill, or reply if the message is a meta-request about the bot's own state. |
/opus, /sonnet, /haiku <prompt> |
Deterministic spawn with that model, no brain call |
/spawn [model] <prompt> |
Explicit deterministic spawn |
/NewBot <prompt> |
Opus 4.7 + /effort medium + --dangerously-skip-permissions (default for spawn script). Anything after /NewBot becomes the initial prompt. Added 2026-04-29 evening. |
/sessions |
Lists active tmux sessions + their bridge URLs (matched newest-first) |
/kill <name> |
tmux kill-session -t <name> |
/ask [model] <prompt> |
One-shot claude -p answer in chat (escape hatch when you don't want a tmux session for a quick question) |
/help |
Lists commands |
Effort levels honored by the CLI flag: low | medium | high | xhigh | max. The spawn script reads EFFORT env var; if set, it appends --effort <level> to the claude invocation, so the entire session uses that thinking budget from the very first response (no in-session /effort slash-command keystrokes needed). /NewBot hard-codes EFFORT=medium. Other callers can pass effort=... to spawn_session() if they want a different level. The in-session /effort auto slash command still works manually inside Claude Code, but the CLI flag does not accept auto.
Auto-name: first 2 alphanumeric tokens of the prompt, lowercased and hyphenated, capped at 24 chars, collision-suffixed with -2, -3. Empty prompt becomes phone-<int>.
Routing brain (added 2026-04-29 after spawn-eats-everything bug)¶
Plain text routes through _call_brain_sonnet(), a single-shot claude -p --model sonnet call. The brain has 4 tools and returns strict JSON: spawn, kill, propose_kill, reply.
Why it exists: pre-brain, every text message hit forge_spawn_session.sh directly. Conversational follow-ups about the bot's own state ("disable the archived ones, confirm before doing") got eaten as new spawn requests. Brain sees the live tmux session list + last 8 conversation turns, so it can:
- Spawn (default for "do work" requests).
- Identify candidates for cleanup and propose-then-confirm without acting.
- Look up the previous proposal in history when Justin says "go" / "yes" / "confirm" and execute the kill.
- Just reply for "how many sessions" type questions.
Tools and rules at BRIDGE_BRAIN_RULES constant in forge_telegram_remote_bridge.py. History at forge/data/bridge-brain-context.jsonl (last 8 turns, JSONL).
Cost: ~3.7k-token system prompt + small user message + small JSON response per plain-text message. Sonnet 4.6, prompt-cached after first call. Slash commands skip the brain entirely (zero LLM cost, fast path).
Layout¶
| Path | Role |
|---|---|
forge/scripts/forge_telegram_remote_bridge.py |
Long-poller, transcribe-on-voice, slash dispatch, spawn + ask handlers |
forge/scripts/forge_spawn_session.sh |
Canonical spawn script, also used by /spawn skill and coordinator's tool_spawn_remote_session |
/etc/systemd/system/forge-remote-bridge.service |
Unit, no PrivateTmp; ReadWritePaths includes ~/.claude (needed for spawn script to find session JSONs) |
~/.forge-secrets/telegram-remote-bridge.env |
Token + authorized user (chmod 600) |
forge/logs/telegram-remote-bridge.log |
Service log |
Why spawn-by-default¶
The pre-flip bridge ran claude -p for every message, defaulting to sonnet. Two problems:
1. Most of Justin's actual asks ("create an opus worker to debug X", "fire up an opus to do Y") wanted a separate tmux session, not an in-process answer. The bridge had no spawn_remote_session tool because it bypasses the brain layer; it would attempt the work itself and either hit its bash sandbox or burn tokens with no ongoing session to take over from his phone.
2. claude -p runs inside a sandbox with read-only /tmp/claude-1000/, so most diagnostic commands fail.
Spawn-by-default flips the bridge into being the on-ramp from phone to Real Brain. The bridge's own Python (running under systemd, not a sandbox) shells out directly: spawn via forge_spawn_session.sh, list/kill via tmux. No LLM call needed for the spawn path; deterministic and ~30s.
/ask preserves the legacy claude -p behavior for the rare "just answer me, no session" case.
Per-message flow (spawn path)¶
- Telegram message arrives, voice → whisper if needed.
- Auth check against
TELEGRAM_AUTHORIZED_USER_ID=7110798439. react(👀)+typing()ack the receive instantly (no "Got it" bubble).parse_command(text)returns(action, args).- Default action
spawn: send "Session Spawning: opus…" → callforge_spawn_session.sh opus <auto-name> <prompt>(timeout 120s) → reply withSession Spawned: ... <URL>(orFailed: spawn opus, ...). React 👌 on success, 🤔 on failure. quota.record(invoker="forge_telegram_remote_bridge", mode="spawn", ...).
Reactions UX (added 2026-04-29)¶
Bridge matches the rest of the fleet. react() and typing() helpers in forge_telegram_remote_bridge.py. Receive triggers 👀 + typing once; final outcome flips to 👌 or 🤔. Action-labeled prefixes on outbound bubbles (Session Spawning:, Session Spawned:, Sessions Killed:, Proposed Kill:, Failed:). Conversational reply action stays prefix-free. See feedback_telegram_bot_ux_reactions.
Per-message flow (/ask path, legacy)¶
Same as the original bridge: spawn claude -p with stream-json, edit a status message as tools fire, reply with final_text. Hard cap 25 min. Sandboxed bash limitations apply.
Known limitations¶
/sessionsmatches tmux sessions to bridge URLs by recency only (Claude session JSONs don't carry tmux session names). Newest tmux session pairs with newest unmatched JSON. Usually correct but not guaranteed; worst case Justin opens both URLs and figures it out.- Old/dormant tmux sessions (started without
/remote-control) show no URL. That's correct. - No read-back yet from
/say <session> <message>(skipped for v1 per Justin 2026-04-29). When wanted: implement viatmux send-keys+ diff~/.claude/sessions/<bridgeSessionId>.jsontranscript.
Operations¶
sudo systemctl status forge-remote-bridge.service
sudo systemctl restart forge-remote-bridge.service
tail -f /home/justinwieb/forge/logs/telegram-remote-bridge.log
Quota¶
Invoker name in forge/data/claude-quota/<YYYY-MM>.jsonl: forge_telegram_remote_bridge. extra.mode is spawn or ask. Spawn entries record latency_ms=0 (deterministic, not LLM-bound).
[Claude Code, Phase 4.2.5 spawn-by-default flip 2026-04-29]