← Home

SmolPaws Slack Entry Point live

How the Slack ingress receives messages, fetches thread context, and dispatches to the agent server.
Source: apps/slack/ · PRs: #106#112

Overview

The Slack ingress is a long-running local Node.js process. It connects to Slack via Socket Mode WebSocket (@slack/bolt), receives events, fetches thread context, and dispatches to the shared SmolPaws agent server over HTTP.

flowchart LR
    A["Slack\nSocket Mode"] <-->|WebSocket| B["apps/slack\n(@slack/bolt)"]
    B -->|HTTP POST| C["agent-server\n127.0.0.1:8788"]
    C -->|runs agent| D["OpenHands\nruntime"]
    D -->|turn result +\noutbound msgs| C
    C -->|poll + claim| B
    B -->|chat.postMessage\nwith thread_ts| A
    

Message Flow

sequenceDiagram
    participant U as Slack User
    participant S as Slack API
    participant I as apps/slack
    participant A as agent-server
    participant R as Agent Runtime

    U->>S: DM or @smolpaws mention
    S->>I: event via Socket Mode
    I->>I: dedup + checkAccess()
    I->>S: reactions.add(eyes)
    I->>I: stripBotMention()
    alt threaded message
        I->>S: conversations.replies()
        S-->>I: thread history
        I->>I: formatThreadContext()
    end
    I->>I: buildConversationId()
    I->>A: submitConversationMessage()
    A->>R: create/resume conversation
    loop poll until terminal
        I->>A: getTurnStatus()
        I->>A: claimTurnOutboundMessages()
        I-->>S: deliver outbound messages
    end
    I->>A: getTurnResult()
    I->>S: chat.postMessage (thread_ts)
    S->>U: sees reply
    

Entry Decision Logic

The bot decides whether to respond through layered checks:

Event Filtering (index.ts)

1. Ignore bot messages and self-mentions
2. app_mention events → always process
3. message.im events → always process (DMs)
4. Channel thread replies → only if thread was previously mentioned
5. Everything else → ignore

checkAccess()

1. Non-DMs: check team and channel allowlists (if configured)
2. If user is in ALLOWED_USER_IDS → allowed
3. If no user allowlist configured → allowed (open)
4. Otherwise → guest (rate-limited to 5 conversations)

Thread Context

When the bot is mentioned in a thread, it fetches prior messages via conversations.replies and prepends them as context to the prompt. This lets the agent understand the conversation that preceded the mention.

[Thread context]
<@U1>: What is OpenHands?
<@U2>: An AI agent platform for software development
smolpaws: It's built on top of...

[Current message]
Can you explain the runtime architecture?

Conversation ID Generation

Stable IDs derived from Slack's own identifiers:

ContextPatternExample
DMslack-im-{team}-{channel}slack-im-T06P-D08X
Threadslack-thread-{team}-{channel}-{root_ts}slack-thread-T06P-C123-1717200000.000100

Thread Follow-ups

Once smolpaws has been mentioned in a thread, subsequent replies in that thread are processed without requiring another @mention. The MentionedThreadTracker keeps an in-memory set of tracked threads (capped at 1000, LRU-evicted).

flowchart TB
    A["@smolpaws help me"] -->|app_mention| B["handleSlackEvent"]
    B --> C["mentionedThreads.track(thread_ts)"]
    C --> D["dispatch to agent"]
    E["follow-up reply\n(no @mention)"] -->|message event| F["mentionedThreads.isTracked?"]
    F -->|yes| B
    F -->|no| G["ignore"]
    

Turn API Dispatch

The Slack app uses the shared turnClient.ts — the same client used by Discord and the GitHub Cloudflare Worker.

flowchart TB
    subgraph "apps/slack"
        A["slackHandler.ts"] --> B["agentServerClient.ts"]
    end
    subgraph "src/shared/turnClient.ts"
        C1["submitConversationMessage()"]
        C2["monitorTurn()"]
        C3["getTurnStatus()"]
        C4["claimTurnOutboundMessages()"]
        C5["getTurnResult()"]
        C2 --> C3 & C4 & C5
    end
    B --> C1
    B --> C2
    

Key details:

Safety Features

FeatureImplementation
DeduplicationIn-memory MessageDeduplicator with 60s TTL, auto-prunes at 200 entries
Eyes reaction👀 added to the triggering message immediately on acceptance (before dispatch)
Guest rate limitingNon-allowlisted users get 5 conversations total, persisted to disk
Bot loop preventionIgnores bot messages, self-mentions, and message subtypes
Message splittingLong replies split at 5900 chars, preferring newlines then spaces
Graceful errorsDispatch failures post a friendly error message, don't crash the process

Hermes Comparison

The Hermes agent uses the same Socket Mode approach but at a very different scale:

AspectHermes (SlackAdapter)SmolPaws (apps/slack)
LanguagePython (slack-bolt)TypeScript (@slack/bolt)
Scale~3000-line adapter, 20+ platforms in one gateway~600 lines source, standalone ingress app
RoutingSingle GatewayRunner multiplexes all platformsHTTP dispatch to shared agent-server
Thread contextSession-presence auto-reply, thread memoryconversations.replies fetch + context formatting
Thread follow-upsBot-mentioned-thread trackingMentionedThreadTracker with LRU eviction
DedupMessageDeduplicatorMessageDeduplicator with TTL + auto-prune
Slash commandsAll gateway commands as native slashesNot yet
Rich contentBlock Kit, files, audio, approval buttonsPlain text

SmolPaws' design is intentionally simpler. The complexity lives in the agent server, not the ingress adapter. Each ingress is a thin adapter — receive platform events, build a conversation ID, dispatch, deliver results back.

Slack Permissions

Bot token scopes

ScopePurpose
app_mentions:readReceive @mention events in channels
chat:writePost replies
im:historyRead DM messages
reactions:writeAdd 👀 acknowledgment reactions

Event subscriptions

EventPurpose
app_mentionChannel/thread mentions
message.imDM messages

Configuration

VariableRequiredDefaultPurpose
SLACK_BOT_TOKENyesBot token (xoxb-...)
SLACK_APP_TOKENyesApp-level token for Socket Mode (xapp-...)
SMOLPAWS_RUNNER_URLnohttp://127.0.0.1:8788Agent server base URL
SMOLPAWS_RUNNER_TOKENnoAuth token for agent server
SLACK_ALLOWED_TEAM_IDSnoComma-separated workspace IDs
SLACK_ALLOWED_CHANNEL_IDSnoComma-separated channel IDs
SLACK_ALLOWED_USER_IDSnoComma-separated user IDs

File Layout

apps/slack/
├── package.json
├── tsconfig.json
├── AGENTS.md
└── src/
    ├── index.ts              # Bolt app, Socket Mode, event handlers
    ├── config.ts             # env parsing, allowlists
    ├── slackContext.ts        # conversation IDs, access, thread context, dedup
    ├── slackHandler.ts        # handleSlackEvent() — testable core logic
    ├── agentServerClient.ts   # turn API wrapper
    └── __tests__/
        ├── agentServerClient.test.ts
        ├── slackContext.test.ts
        └── slackHandler.test.ts

Process Model

Shared Infrastructure

Slack, Discord, and GitHub all share the same turn client:

flowchart TB
    subgraph "Ingress Apps"
        D["Discord\napps/discord"]
        G["GitHub\napps/github"]
        S["Slack\napps/slack"]
    end
    subgraph "Shared"
        T["turnClient.ts\nsrc/shared/"]
    end
    subgraph "Backend"
        A["agent-server\napps/agent-server"]
    end
    D --> T
    G --> T
    S --> T
    T --> A
    

Each ingress is a thin adapter: receive platform events → build conversation ID → dispatch via turnClient → deliver results back. Agent logic, conversation state, and turn management all live in the agent server.

← Home