Companion & Gateway
The Companion is a local HTTP server that runs alongside Gee-Code. It lets the Gee platform reach into your machine — reading files, running commands, and delivering webhooks — even when no REPL session is open. The Communication Gateway builds on top of it, routing SMS, email, and voice between you and your Gees.
Why It Exists
Section titled “Why It Exists”When a Gee runs autonomously (via the daemon), it sometimes needs to:
- Receive inbound webhooks from long-running jobs (video generation, workflow results)
- Accept SMS or email replies from the user
- Let the Gee platform execute local file and shell operations
Without the companion, autonomous Gees would be isolated — they could send messages out, but nothing could reach them. The companion closes the loop.
Architecture
Section titled “Architecture” You (SMS/Email) │ ▼ Gee WebApp (to.gee.page) │ ▼ WebSocket tunnel (/companion namespace) │ ┌────┴────────────────────────────┐ │ Companion Server (port 7778) │ │ │ │ /health Health check │ │ /execute MCP tools │ │ /read_file File access │ │ /run_command_stream Shell │ │ /webhook/fal/* FAL results │ │ /webhook/agentic/* Workflows │ │ /webhook/gateway_reply Replies│ │ /webhook/gateway_sms SMS │ │ /ws/terminal Interactive │ └─────────────────────────────────┘The flow works in two directions:
- Outbound (Gee → You): A Gee calls
SendNotification→ the gateway client POSTs to gee-webapp → Twilio sends you an SMS or email - Inbound (You → Gee): You reply via SMS → Twilio webhook → gee-webapp → WebSocket tunnel → companion server → Gee’s inbox → daemon triggers the Gee
The Companion Server
Section titled “The Companion Server”The companion is an aiohttp web server that binds to 127.0.0.1:7778 by default. It implements the Desktop Companion API — the same protocol used by The Terminal’s GUI companion — so the backend treats both identically.
What It Exposes
Section titled “What It Exposes”| Endpoint | Purpose |
|---|---|
GET /health | Health check — returns service type and session token |
GET /tools | MCP tool discovery (list available local operations) |
POST /execute | Execute an MCP tool by name |
POST /read_file | Read a local file |
POST /write_file | Write to a local file |
POST /list_directory | List directory contents |
POST /run_command_stream | Execute a command with SSE streaming output |
POST / | MCP JSON-RPC endpoint (tools/list, tools/call) |
GET /ws/terminal | WebSocket for interactive PTY terminal sessions |
MCP Tools
Section titled “MCP Tools”The companion exposes five MCP tools to the backend:
| Tool | Description |
|---|---|
local_read_file | Read file contents (up to 10 MB) |
local_write_file | Write content to a file |
local_list_directory | List directory entries |
local_delete_file | Delete a file (creates timestamped backup) |
local_run_command | Execute a shell command (300s timeout, blocklist enforced) |
A blocklist prevents destructive commands (rm -rf /, mkfs, dd if=) from executing through the companion.
Port Selection
Section titled “Port Selection”The server tries port 7778, then 7779, then 7780. This avoids conflicts when The Terminal’s GUI companion is already running on 7777, and allows multiple gee-code instances to coexist when needed.
Multi-Instance Pooling
Section titled “Multi-Instance Pooling”Multiple gee-code sessions share a single companion server:
- The first session starts the server and writes a state file to
~/.gee-code/companion_state.json - Subsequent sessions detect the running companion via a health check
- All sessions share the tunnel URL registered with the backend
- When the owner session exits, it clears the state file — the next session takes over
The state file has a 5-minute TTL. If the owner process crashes without cleanup, other sessions detect the stale state and start fresh.
The WebSocket Tunnel
Section titled “The WebSocket Tunnel”The companion server only listens on 127.0.0.1 — it is not directly reachable from the internet. A WebSocket tunnel bridges this gap.
How It Works
Section titled “How It Works”- Gee-Code connects to
to.gee.pagevia Socket.IO on the/companionnamespace - The server assigns a proxy URL:
https://to.gee.page/companion/{companion_id}/proxy - When gee-webapp needs to reach the companion, it emits a
proxy_requestevent through the WebSocket - The tunnel forwards the request to
127.0.0.1:7778, gets the response, and emitsproxy_responseback - Gee-webapp returns the response to the original caller
This is a reverse proxy over WebSocket — no port forwarding, no ngrok, no firewall changes.
Resilience
Section titled “Resilience”The tunnel is built for reliability:
- Auto-reconnect — unlimited retries with exponential backoff (1s to 30s delay)
- Request deduplication — multi-worker webapp deployments can emit duplicate
proxy_requestevents; the tunnel deduplicates byrequest_idso local side effects run exactly once - Status tracking — the tunnel reports
connected,disconnected, orreconnectingstatus, visible in The Terminal’s Observability panel
Interactive Terminal Relay
Section titled “Interactive Terminal Relay”The tunnel also supports full PTY terminal sessions over Socket.IO:
| Event | Direction | Purpose |
|---|---|---|
terminal_start | Server → Client | Start a new terminal (cols, rows) |
terminal_started | Client → Server | Confirm terminal is running |
terminal_input | Server → Client | Keyboard input from the user |
terminal_output | Client → Server | Terminal output (stdout) |
terminal_resize | Server → Client | Window resize |
terminal_kill | Server → Client | Kill the terminal process |
terminal_exit | Client → Server | Process exited (with exit code) |
This powers The Terminal’s remote shell feature — a browser-based terminal that runs commands on your local machine through the tunnel.
Inbound Webhooks
Section titled “Inbound Webhooks”The companion accepts webhooks for long-running operations that complete asynchronously.
FAL Webhooks
Section titled “FAL Webhooks”When a Gee generates media through FAL (images, video, audio), it can register a webhook URL. When the job completes, FAL POSTs the result to:
POST /webhook/fal/{request_id}The companion resolves a pending future (if the Gee is waiting synchronously) or invokes a background callback (if the job was fire-and-forget).
Agentic Workflow Webhooks
Section titled “Agentic Workflow Webhooks”Visual workflows (Agenticss) that run on the Gee platform report completion to:
POST /webhook/agentic/{request_id}Same resolution pattern — pending future or background callback.
Generic Webhooks
Section titled “Generic Webhooks”Approval responses from the communication gateway arrive as:
POST /webhook/{request_id}When the payload includes type: "approval_response", the companion resolves the corresponding ApprovalRequest so the Gee can continue.
The Communication Gateway
Section titled “The Communication Gateway”The gateway is how Gees talk to humans and how humans talk back to Gees.
Outbound: Gee → Human
Section titled “Outbound: Gee → Human”A Gee sends messages using the SendNotification tool, which calls GatewayClient under the hood:
| Channel | Method | Delivery |
|---|---|---|
| SMS | send_sms() | Via Twilio through gee-webapp |
send_email() | Via gee-webapp email service | |
| Voice | initiate_call() | Via Twilio voice through gee-webapp |
Messages include the Gee’s name as a prefix (e.g., [analyst] Build failed on main) and a reply code for routing responses back. Identity resolves through a chain: explicit gee_name parameter → environment variables (for BYOP/CLI providers) → in-process mode context → "gee" fallback.
Timed Acknowledgement
Section titled “Timed Acknowledgement”When a user sends an SMS, the Gee might take 10–30 seconds to formulate a full response — it needs to read the message, load context, call tools, and compose a reply. During that silence, the user has no idea whether their message was received.
The timed ack solves this. When an inbound SMS arrives, a 5-second timer starts. If the Gee’s real reply hasn’t been sent by then, a short contextual acknowledgement is sent automatically:
You: "What's the deployment status?" ↓ 5 seconds, no reply yet[og] "Got your question about the deployment, looking into it now." ↓ 15 seconds later[og] "Staging deployed at 14:22, all health checks green. Production queued for 16:00."If the Gee replies within 5 seconds, the timer is cancelled and the user only sees the real answer — no redundant ack.
How it works:
- Inbound SMS arrives at the companion server
_start_delayed_ack()creates an async timer for that Gee- The timer sleeps for 5 seconds, then generates a contextual one-sentence ack using a fast model (Haiku)
- When the Gee’s real reply fires via
SendNotification→send_sms(),cancel_pending_ack()kills the timer - If the fast model is unavailable, a static fallback ack is sent instead
The ack is contextual — it references what the user asked about without answering it, so the user knows their message was received and is being worked on. Each Gee has its own independent timer, so multiple Gees processing messages simultaneously don’t interfere with each other.
Inbound: Human → Gee
Section titled “Inbound: Human → Gee”When you reply to a Gee’s message, the response routes back through two paths:
Direct Reply Routing
Section titled “Direct Reply Routing”POST /webhook/gateway_replyGee-webapp forwards your reply to the companion with a payload containing gee_name, message, and channel. The companion:
- Writes the message to the target Gee’s inter-mode inbox (
comms/inbox.jsonl) - Triggers the Gee via the daemon so it reads and responds immediately
SMS Routing Chain
Section titled “SMS Routing Chain”When an inbound SMS arrives, the gateway routes it through a four-step fallback cascade. Each step tries to identify the right Gee; if it fails, the message falls through to the next step.
Inbound SMS │ ▼[1] Approval response check │ (YES/NO to pending approval?) ▼[2] Reply-code prefix │ (TP, CR, etc.) ▼[3] Gee-name prefix │ (og, analyst, etc.) ▼[4] LLM intent classifier │ (reads recent conversation thread) ▼[5] Prime Gee fallback │ (designated default Gee) ▼[6] Default webapp GeeStep 1 — Approval responses. Before routing as a normal message, the system checks whether the SMS answers a pending approval. Patterns like TP-YES, YES ref:abc123, or a bare YES are matched against active approval requests. If found, the approval resolves and the Gee unblocks.
Step 2 — Reply-code prefix. Users can prefix a message with a reply code (e.g., TP Add a day in Rome). The code maps to a Gee name via Redis, and the message is forwarded with the code stripped.
Step 3 — Gee-name prefix. You can address a Gee directly by name:
og check the deployment statusanalyst run the weekly reportThe gateway tries an exact match against known Gee names, then falls back to fuzzy matching with a Levenshtein distance of 1. Only single unambiguous matches route — if two Gees fuzzy-match, the message falls through.
Step 4 — LLM intent classifier. For unaddressed messages, the system reads the recent conversation thread — a sliding window of the last 6 messages within a 10-minute window. If a Gee recently sent the user something, the classifier (a fast model call) determines whether the new message is a reply to that Gee. High and medium confidence matches route automatically; low confidence falls through.
Step 5 — Prime Gee. If no routing matched and a prime Gee is set, the message goes to that Gee as the designated default.
Step 6 — Default webapp. If nothing else matched (no daemon running, no prime Gee, no conversation context), the message routes to the webapp’s built-in core agent.
Voice Call Context
Section titled “Voice Call Context”When a Gee initiates a voice call, it sends more than just the message. The system enriches the call with the Gee’s current objectives so the voice AI can speak with full context about what the Gee is working on.
What gets forwarded:
- The original message (the reason for the call)
- The Gee’s name (the voice AI identifies as this Gee)
- Current objectives summary (so the AI knows what the Gee is tracking)
Post-call summary: When the call ends, gee-webapp sends a summary callback to the companion server. The companion writes the summary and full transcript to the originating Gee’s inbox as a voice_call_summary message. The daemon then triggers the Gee so it can process what was discussed — follow up on action items, update its state, or send a written recap.
This closes the loop: a Gee can call the user, discuss something with full context about its current work, and then act on the outcome without any manual bridging.
Approval Requests
Section titled “Approval Requests”Gees can send structured approval requests that require a YES/NO response. The SMS routing chain handles these as a priority — approval responses are checked before any other routing. See the SMS Routing Chain for how approval replies are matched by reply code, reference ID, or recency.
Trigger Flow
Section titled “Trigger Flow”When the companion receives an inbound message, it triggers the target Gee using this escalation:
- In-process daemon — if the daemon is running in the same process, trigger directly
- Cross-process daemon — if the daemon is in another process, write a trigger request file that the daemon picks up on its next check
- Direct fallback — if no daemon is running, activate the Gee directly
Prime Gee
Section titled “Prime Gee”The “prime Gee” is a designated default — the Gee that receives unaddressed messages when no other routing matches. Think of it as the Gee that answers the phone when you don’t specify who you want to talk to.
Setting a Prime Gee
Section titled “Setting a Prime Gee”Mark a mode as prime in its mode.json:
{ "name": "og", "is_prime": true, "autonomous": { "guiding_mission": "Executive assistant" }}Or set it interactively with the /gee browse command — press p to designate the selected Gee as prime.
Only one Gee should be prime per user. When the daemon starts, it scans all loaded modes and registers the prime Gee with the backend so the SMS routing chain can use it as a fallback.
Orchestrator Mode
Section titled “Orchestrator Mode”When a Gee is busy with a long activation — running tests, generating a report, building a skill — it holds the activation lock. Any inbound messages during this time would normally queue behind the lock and wait. Orchestrator mode changes this.
How It Works
Section titled “How It Works”With orchestrator mode enabled, inbound messages bypass the activation lock entirely. Instead of waiting, they trigger a lightweight triage call that runs outside the main lock:
Normal mode: Inbound trigger → waits for lock → heavy LLM call → response (minutes)
Orchestrator mode: Worker holding lock (long task running) │ Inbound trigger arrives │ Fast orchestrator triage (separate lock, ~5s) │ Immediate reply via SendNotification │ Worker continues uninterruptedThe orchestrator knows what the main worker is doing — it has access to the Gee’s background task list — so it can give informed responses like “I’m currently running the deployment pipeline, should be done in a few minutes” rather than silence.
Enabling Orchestrator Mode
Section titled “Enabling Orchestrator Mode”Opt in per mode via mode.json:
{ "name": "cfo", "autonomous": { "guiding_mission": "Manage financial operations", "orchestrator_mode": true }}What the Orchestrator Does
Section titled “What the Orchestrator Does”When an inbound message arrives and the main activation is locked:
- Acquires a separate orchestrator lock (serializes fast triage calls)
- Builds a concise system prompt with the Gee’s identity and current background tasks
- Reads the inbound message
- Calls a fast model to generate a reply
- Sends the reply via
SendNotificationon the same channel the message arrived on - Marks inbox messages as read to prevent duplicate processing when the main worker finishes
If the fast model is unavailable, it falls back to a canned acknowledgment.
Two-Lock Architecture
Section titled “Two-Lock Architecture”The runner maintains two independent locks:
| Lock | Purpose | Typical Hold Time |
|---|---|---|
_activation_lock | Main worker — full LLM activation with tools | Minutes |
_orchestrator_lock | Triage — fast read-and-reply calls | < 5 seconds |
The orchestrator lock serializes triage calls (one at a time) but never blocks behind the worker lock. This means a Gee can handle multiple rapid inbound messages without queuing, while a long activation runs uninterrupted.
Trigger Deduplication
Section titled “Trigger Deduplication”The same message can arrive via multiple paths — for example, a gateway reply event followed by a heartbeat that reads the inbox. The runner tracks recently handled trigger keys with a 120-second TTL to prevent duplicate processing. If a trigger key has already been handled, the second arrival is silently dropped.
Background Task Tracking
Section titled “Background Task Tracking”The orchestrator’s key advantage is situational awareness. It knows what the worker is doing because the runner maintains a background task registry:
register_background_task(task_id, description)— records a new task with a timestampcomplete_background_task(task_id, summary)— marks a task done with resultsget_active_background_tasks()— returns running tasks with elapsed time
When the orchestrator responds, it includes active task descriptions and timing in its system prompt. This lets it say “I’m 3 minutes into deploying the staging environment” instead of a generic “I’m busy.”
Busy Fallback
Section titled “Busy Fallback”If the LLM is unavailable (no executor configured, timeout, or error), the orchestrator falls back to a canned response that still references the current task:
I got your message. I’m currently working on: deploying staging environment. I’ll get back to you as soon as I’m done.
Task descriptions are truncated to 80 characters for clean formatting. For email fallbacks, the subject line includes Re: your message to {gee-name}.
When It Doesn’t Activate
Section titled “When It Doesn’t Activate”Orchestrator triage only fires when all three conditions are true:
orchestrator_modeis enabled in the mode config- The main activation lock is held (a worker is busy)
- The trigger is an inbound message (not a heartbeat or scheduled trigger)
If the main lock is free, the message goes through normal activation — no orchestrator needed.
Reply Guarantee
Section titled “Reply Guarantee”When a Gee is triggered by an inbound SMS or email, it always sends a reply. This is enforced at two levels:
-
Prompt-level mandate — The trigger prompt tells the model to respond using
SendNotificationon the same channel the message arrived on, and explicitly states “Do not finish this activation without sending a reply.” -
Fallback safety net — After the activation completes, the runner checks whether any tool actions were taken. If the model completed silently (zero actions), the system automatically sends a fallback reply:
I got your message: “check the deployment status”. I’m on it.
The incoming message is condensed to 140 characters and quoted back, so the user knows their message was received even if the Gee couldn’t formulate a full response.
This guarantee exists because autonomous Gees can occasionally fail to produce tool calls — model errors, context limits, or unexpected edge cases. Without the safety net, an SMS would disappear into a void with no acknowledgment. The fallback ensures the communication loop always closes.
For email replies, the fallback includes a Re: subject line with the Gee’s name (e.g., Re: your message to analyst).
Authentication
Section titled “Authentication”The companion requires authentication to establish the tunnel and register with gee-webapp:
gee auth loginThis opens a browser flow where you get a setup code, which gee-code exchanges for JWT tokens. Tokens are stored locally and auto-refresh.
Starting the Companion
Section titled “Starting the Companion”The companion starts automatically in two contexts:
- With the daemon —
gee-code daemon startlaunches both the daemon and companion - With a REPL session — any authenticated gee-code session starts a companion
To disable the companion when starting the daemon:
GEE_CODE_COMPANION=0 gee-code daemon startVerifying the Connection
Section titled “Verifying the Connection”Check companion status:
gee-code daemon statusThe status output shows whether the companion is running, the local port, and whether the tunnel is connected.
Next Steps
Section titled “Next Steps”- Modes & Daemon — the heartbeat system that activates Gees
- Missions & Objectives — what Gees work on
- Operating Gees — the full lifecycle guide