Status · 2026-08-16: the complete Liberty Labs Socket Mode round trip has been proven through the TypeScript agent-server, real agent loop, durable outbox sync, Delivery Dispatcher, and Slack Web API. The isolated canary used a deterministic test LLM; the ordinary long-running paws process still needs its real-provider production soak.
Authority: current code, executable tests, the agent-server EventLog, and Message Relay SQLite records. This page describes the architecture. A visible Slack reply alone is not sufficient evidence that the intended process handled it.
apps/slack is intentionally greenfield. It runs as a standalone Socket Mode process, owns its Bolt connection and SlackRelayRuntime directly, and does not inherit BaseBridgeAdapter, join the shared bridge loader, or fall back to the legacy /turns path on port 8788.
flowchart LR
U["Slack user"] --> S["SlackBridge / paws\nSocket Mode"]
S --> I["Message Relay intake\nSQLite"]
I --> A["TypeScript OpenHands\nagent-server :8790"]
A --> E["durable EventLog"]
E --> R["Outbound Relay\nsyncDeliveryOutbox()"]
R --> O["delivery outbox\nSQLite"]
O --> D["Delivery Dispatcher"]
D --> T["SlackDeliveryTarget"]
T --> U
| Fact | Owner |
|---|---|
| Conversation events and agent execution | TypeScript agent-server EventLog |
| External intake, ordering, retries, claims, and delivery outcomes | Message Relay SQLite store |
| Slack connection, formatting, thread coordinates, and network send | SlackBridge and SlackDeliveryTarget |
The agent-server remains upstream-shaped. External queue semantics, Slack retries, and uncertain network effects stay in SmolPaws-owned Relay infrastructure.
sequenceDiagram
participant U as Slack user
participant S as SlackBridge
participant M as Message Relay
participant A as TS agent-server
participant R as Outbound Relay
participant D as Delivery Dispatcher
participant API as Slack Web API
U->>S: DM or @paws mention
S->>S: policy, short-lived gate, thread context
S->>M: accept message
M->>M: persist intake row
M-->>S: durable acceptance complete
M->>A: deterministic user event + run=true
A->>A: agent loop writes durable events
R->>A: search events from cursor
R->>M: syncDeliveryOutbox()
M->>M: insert idempotent delivery row
D->>M: claim lane-head delivery
D->>M: mark send_attempted
D->>API: chat.postMessage
API-->>D: Slack message ts
D->>M: settle done + external_message_id
API-->>U: reply appears once
| Component | Responsibility |
|---|---|
apps/slack/src/adapter.ts | Standalone Bolt/Socket Mode lifecycle and Slack Web API wiring |
slackHandler.ts | Access policy, normalization, mention stripping, bounded thread context, and a short-lived duplicate gate |
relayRuntime.ts | Slack-hosted intake, reconciliation, outbox synchronization, and bounded delivery workers |
MessageRelay | Lane identity, durable intake, deterministic event append, and syncDeliveryOutbox() |
OutboundRelay | Catches EventLog changes up into the outbox and performs a bounded dispatch drain |
DeliveryDispatcher | Claim → validate → send fence → external call → fenced settlement |
SlackDeliveryTarget | Sends chunks to the durable Slack channel/thread coordinates |
syncDeliveryOutbox()?The Outbound Relay repeatedly catches a conversation up from its durable agent-event cursor. For the first Slack generation, the successful terminal finish observation becomes one durable delivery row. Repeating the sync is safe because the source identity combines the originating event and destination lane.
agent event id + destination lane
↓
unique delivery source_key
↓
replay becomes a no-op
The name is intentionally plainer than “projector”: the method synchronizes an outbox. It does not itself contact Slack.
The Dispatcher validates work before the external boundary. Immediately before calling Slack, it durably sets send_attempted = true.
| Situation | Outcome |
|---|---|
| Invalid payload or no registered target | Known failed work before any network send |
| Claim expires before a send attempt | Safe to make ready again |
| Exception after the send fence | delivery_unknown; never blindly retry |
| Slack confirms the message | done with Slack ts as external_message_id |
On 2026-08-16, an isolated, self-expiring canary at fork commit a69456fc6f818f23ecb6e2e064f3e03fceeafaf4 completed the real Slack transport path. The observed response was:
RELAY-LIVE-a69456fc6f81
The response travelled through real Socket Mode ingress, durable Relay intake, the real TypeScript agent-server and agent loop, a deterministic test LLM calling finish, syncDeliveryOutbox(), Delivery Dispatcher, Slack Delivery Target, and chat.postMessage. The canary used separate state, port, checkout, and SQLite storage, and stopped itself automatically.
Open the Liberty Labs canary thread.
The architectural round trip is proven. The remaining boundary is operational rather than structural:
paws against port 8790 with the configured real LLM profile;delivery_unknown reconciliation behavior before calling it the production Slack path.A response containing 🐾 Done — nothing to report back. identifies the old BaseBridgeAdapter//turns fallback. It is not proof of the Message Relay path.
| Layer | Evidence |
|---|---|
| Message Relay state machines | Real-SQLite tests for dedup, ordering, fencing, retries, replay, and ambiguous sends |
| Agent-server seam | Real in-process TypeScript agent-server tests with deterministic LLM output |
| Outbound path | Outbox sync, Dispatcher, Slack target, thread routing, send fence, and external-ID assertions |
| Workspace canary | Real Liberty Labs Socket Mode round trip with the unique Relay response token |
Operational commands and read-only inspection queries live in docs/slack/instructions.md.