geode-serve · git:20260825.9516eab · 2026-08-25 · sha256 5f014f2a73384b00

geode-serve git:20260825.9516eabA

Immutable. This exact content is served forever at /api/v1/blob/5f014f2a73384b00.

---
name: geode-serve
description: Slack Gateway operations guide. Socket Mode credentials, config.toml bindings, serve restart, receiver debugging, reaction behavior. Triggers on "serve", "gateway", "slack", "바인딩", "binding", "소켓", "socket", "폴러", "poller", "config.toml".
---

# geode serve — Slack Gateway Operations Guide

> **Source**: Distilled from Gateway debugging session (2026-03-26)
> **Primary failure modes**: missing binding, missing `xapp-` token, or bot not invited to a bound channel

## Architecture

```
geode serve
  → core/wiring/adapters.py: Merge ~/.geode/config.toml + project overlay
  → SlackPoller: SLACK_APP_TOKEN present → SlackSocketModeClient.run()
  → bounded-queue admit → ACK Events API envelope → filter exact bound channel → route_message()
  → _send_response(): SlackTransport.post_message (direct Web API, thread reply)

No `SLACK_APP_TOKEN` selects an explicit degraded polling fallback. It is a
migration path, not the operational target.
```

## Prerequisites

| Item | How to verify |
|------|---------------|
| `SLACK_BOT_TOKEN` | `echo $SLACK_BOT_TOKEN` — `xoxb-...` |
| `SLACK_APP_TOKEN` | `echo $SLACK_APP_TOKEN` — `xapp-...`, `connections:write` |
| App settings | Socket Mode on; bot events `app_mention`, `message.channels` |
| `.geode/config.toml` | `cat .geode/config.toml` — bindings.rules exist |
| Slack health | `geode doctor slack` is `OPERATIONAL`; every binding says `bot_member=True` |

## config.toml Setup

```bash
# Copy from template (on clean clone)
cp .geode/config.toml.example .geode/config.toml
```

```toml
[gateway.bindings]

[[gateway.bindings.rules]]
channel = "slack"
channel_id = "C0XXXXXXXXX"  # Slack channel → Click channel name → Channel ID at bottom
auto_respond = true
require_mention = true       # true: respond only on @mention
max_rounds = 5
```

- `config.toml` is in `.gitignore` — not deleted by git pull
- `config.toml.example` is committed — reference for clean clones
- **Adding channels**: Repeat `[[gateway.bindings.rules]]` blocks

## Start/Restart

```bash
# Kill existing process
kill $(pgrep -f "geode serve")

# Restart (background)
nohup uv run geode serve >/dev/null 2>&1 &

# Or foreground
uv run geode serve
```

## Debugging Checklist

### Symptom: Bot does not respond to messages

```bash
# 1. Check process
ps aux | grep "geode serve"

# 2. Verify binding load
grep "binding" ~/.geode/logs/serve.log
# Expected: "Loaded N gateway bindings from config"
# If 0 → config.toml missing or parse error

# 3. Verify config source and Socket Mode
grep -i "gateway config sources" ~/.geode/logs/serve.log
# Expected: "Gateway config sources: global:... [, project:...]"

grep -E "Slack inbound mode|Slack Socket Mode connected" ~/.geode/logs/serve.log
# Expected: Socket Mode (push), then connected

# 4. Verify credentials, scopes, and channel membership
geode doctor slack

# 5. Verify message reception after an @geode mention
grep "Slack message from" ~/.geode/logs/serve.log
```

### Symptoms and Causes

| Symptom | Cause | Resolution |
|---------|-------|------------|
| "Loaded 0 gateway bindings" | `.geode/config.toml` missing | `cp config.toml.example config.toml` + enter channel ID |
| `polling fallback` | `SLACK_APP_TOKEN` missing | Create an `xapp-` app token with `connections:write`, add it to `~/.geode/.env`, restart |
| `not_in_channel` / `bot_member=False` | Bot was not invited | Run `/invite @geode` in the linked channel |
| Repeated disconnects | App token invalid or Socket Mode disabled | Run `geode doctor slack`, then verify app-level token and Socket Mode settings |
| New top-level/unengaged message receives no response | `require_mention=true` but no @mention | Mention @botname once or set `require_mention=false` |
| Engaged thread stops after daemon restart | No resumable ACTIVE/PAUSED checkpoint, or receiver is still polling | Confirm Socket Mode logs; re-mention once if the prior machine is terminal |
| Bot re-responds to its own messages | bot_message filter bypassed | Check `bot_id` field — if normal, check Slack App settings |

## Reaction Behavior

`require_mention = true` + first `<@BOT_ID>` mention:

1. :eyes: reaction (acknowledge receipt)
2. Normalize the root `ts` as `thread_id` and remember the engaged thread
3. `ChannelManager.aroute_message()` → AgenticLoop execution
4. :white_check_mark: reaction (complete)
5. Send response in thread

Later human reply in that engaged thread (no repeated mention):

1. Match channel-scoped engaged state or the durable ACTIVE/PAUSED gateway checkpoint
2. Reuse the same session/lane/checkpoint key and restore checkpoint messages after restart
3. Run the same :eyes: → AgenticLoop → :white_check_mark: → thread-response lifecycle

Unengaged regular message (no mention, `require_mention = false`):

- Process without reaction + thread response

## Related Files

| File | Role |
|------|------|
| `core/messaging/slack_socket_mode.py` | Socket URL, WebSocket ACK/reconnect loop |
| `core/server/supervised/slack_poller.py` | Socket event normalization + compatibility fallback |
| `core/messaging/binding.py` | Binding management + message routing |
| `core/messaging/slack_transport.py` | Bot-token Web API outbound + channel diagnostics |
| `core/wiring/adapters.py` | Gateway config merge and receiver registration |
| `.geode/config.toml` | Channel bindings (local, untracked) |
| `.geode/config.toml.example` | Binding template (committed) |