Skip to content

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)

  1. Telegram message arrives, voice → whisper if needed.
  2. Auth check against TELEGRAM_AUTHORIZED_USER_ID=7110798439.
  3. react(👀) + typing() ack the receive instantly (no "Got it" bubble).
  4. parse_command(text) returns (action, args).
  5. Default action spawn: send "Session Spawning: opus…" → call forge_spawn_session.sh opus <auto-name> <prompt> (timeout 120s) → reply with Session Spawned: ... <URL> (or Failed: spawn opus, ...). React 👌 on success, 🤔 on failure.
  6. 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

  • /sessions matches 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 via tmux send-keys + diff ~/.claude/sessions/<bridgeSessionId>.json transcript.

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]