SuperClaw Slack Bridge — Build and Operations
This is a living document. It began as a private ops draft on 2026-09-09 and is updated in place whenever the bridge changes: dated annotations mark what newer sections superseded, and new activity appends at the bottom. Last updated: September 15, 2026.
Scope: Creation, updates, and debugging of the SuperClaw Slack bridge — initial setup, the /new command (+ recent title addition), and crash handling.
TL;DR
A Socket-Mode Slack bot that pipes Slack DMs/mentions into the opencode CLI (SuperClaw) one session per channel. Born 2026-08-31, survived two identical socket-layer crashes, got a watchdog on 2026-09-02, and landed /new <title> on 2026-09-03. Net-new code is small (~340 lines of ESM + an 89-line watchdog); the hard part was a third-party crash the watchdog now absorbs.
How it's wired (for the record)
- SuperClaw connection: no socket/daemon; the bridge shells out per message —
opencode run --dir /workspace --format json --dangerously-skip-permissions [--session <id>] [--title <t>] <text>— and streams the JSONL response back (split at 4000 chars, Slack's hard limit). - State:
sessions.jsonmaps Slack channel → opencode session id(+ title). One session per channel = conversations keep context. - UX: per-channel queue serializes turns; an 8-second typing ping keeps the bubble alive; long turns are monitored (5-min sweep, SIGKILL after 20 min — never actually fired).
Initial setup (2026-08-31)
First boot, first command ran within ~90 seconds — the whole bridge-to-reply path was live at 05:40, first real turn at 05:42, first full reply streamed at 05:45 (all times UTC):
2026-08-31T05:40:57Z Connected as @<BOT-USER>
2026-08-31T05:40:57Z SuperClaw is live in Slack...
2026-08-31T05:42:14Z MENTION event: "...draft an article..."
2026-08-31T05:42:14Z SPAWNED opencode pid N
2026-08-31T05:45:49Z opencode exit event: 0
2026-08-31T05:45:49Z REPLY to <SLACK-CHANNEL> (756 chars, streamed)
That's the whole first article (the Sun Project activity article, 2026-08-31) authored through the bridge — good stress test of streaming.
The /new command
- 12 bare
/newuses 08-31 → 09-03, all clean resets. - 09-03 update:
/new [title]— trim, collapse whitespace, cap 80 chars, stored in the session map, forwarded toopencodeas the session title (--title). Log line changed to "…/new [title] starts a fresh conversation." - Not yet exercised as of 09-03: no titled
/newhas been sent since deploy (11:31 UTC). First live use will confirm the title actually reaches the opencode session it should.
Crashes & debugging (the meat)
Crash signature — an upstream state machine bug
Two crashes, identical root cause, raw stack, Node process died:
node_modules/finity/lib/core/StateMachine.js:76
throw new Error('Unhandled event 'server explicit disconnect'
in state 'connecting'.');
at SocketModeClient.onWebSocketMessage ...
Slack reconnects → socket-mode's finity state machine gets server explicit disconnect in state connecting → unhandled → process death.
- Crash #2: 2026-09-02 ~22:47; manual restart at 22:58. Both killed a live conversation mid-use.
Watchdog (2026-09-02, 23:05)
Decided the class of failure is "process dies silently," so absorb it procedurally:
- Restart =
kill+pkill -TERM -f node /workspace/slack-bridge/index.js(sweeps strays from previous supervisors) + nohup relaunch + pidfile. - Backoff: 3 rapid failures → 120s cooldown (protects against crash-on-boot loops like a revoked token).
Whole watchdog history to date — 3 restarts, all "bridge down," none wedged, no backoff:
2026-09-02T23:05:34Z watchdog up (pid …)
2026-09-02T23:05:36Z started bridge pid …
2026-09-02T23:06:06Z bridge down — restarting (fail #1)
2026-09-03T11:31:00Z bridge down — restarting (fail #1) ← old code boot
2026-09-03T11:31:45Z bridge down — restarting (fail #1) ← /new [title] deploy boot
Ha — one of those "fail #1" events is actually the code deploy on 09-03: old banner at 11:31:04, new banner at 11:31:49. And the very first takeover boot died ~30s in (silent, no stack — collateral of the pkill sweep).
Defense-in-depth that paid off
- Turn monitor: never fired, never stale — scans stayed active=0/1 only while genuinely running. Cheap insurance, zero false positives.
- Grace: channel stayed usable across crashes because state lives on disk, not in the process. After crash #2 the user could ask "any way to pick up where we left off here?" and the answer was yes — rejoin, session map intact.
Key numbers (as of 2026-09-09)
Historical snapshot — the Update — 2026-09-15 section at the bottom carries everything since.
| Thing | Value |
|---|---|
| Bridge age | 9 days and counting (born 2026-08-31) |
| Crashes | 2 socket-mode (both pre-watchdog, same upstream bug) — 0 evidenced since; Sep 4–6 unverifiable (bridge.log truncated 09-07) |
| New failure modes since 09-03 | ReferenceError: result is not defined ×3 (Sep 7, non-fatal, user-visible) + 1 silent bridge death (~19:44–19:46Z Sep 7) absorbed by the watchdog |
| Host/container restarts | 3 evidenced (Sep 4 ~23:21Z, Sep 7 ~18:40–18:59Z, Sep 9 ~00:13–04:40Z) — pid resets + log gaps |
| Watchdog restarts | 5 "bridge down" restarts total (3 through 09-03 + 2 on 09-07); +5 supervisor ("watchdog up") boots since 09-03; still 0 wedged, 0 backoffs |
| Non-zero opencode exits | 3 total (08-31 ×1, recovered; 09-07 19:03Z ×2 — "Session not found" → fresh-session retry failed on LM Studio "No models loaded") |
/new uses | lifetime 12 bare + 8 titled; since 09-03: 0 bare + 8 titled (7 of 8 titles verified verbatim in sessions.json) |
| Kill monitor strikes | 0 (still never fired) |
| Total log size | bridge.log ~60 KB (truncated 09-07 ~16:53Z — pre-09-07 story lost); + watchdog.log ~1.1 KB, restart-clean.log ~2.1 KB |
| Bridge status | alive — pid 412 (started 2026-09-09T04:43:17Z per watchdog.log); last bridge.log line 2026-09-09T14:29:05Z |
What I'd keep / what I'd change (draft opinions)
2026-09-15 status: the watchdog opinions below were mooted on 09-10 — the watchdog itself was retired in favor of the catherder supervisor (see the update section). The keep-list still stands.
Keep:
- watchdog before waiting for a third crash.
- session-on-disk design.
Change / next:
- Watchdog output on failed boots should print stderr tail (crash-on-boot is currently invisible unless you read bridge.log).
- Titled
/newis tested the first time someone actually uses it — TODO: self-test once and snapshot the--titlein the log. — DONE 2026-09-09: exercised live ×8 (first: 2026-09-07T17:01:05Z, "/new SuperClaw local model configuration"); titles stored verbatim per session. Caveat: bridge.log never echoes the--titleflag itself, so the snapshot lives in sessions.json, not the log. - Consider DRY of env config; single
WORKSPACE_DIRhardcode is fine today.
Day-by-day log
- Sep 1 — crash #1 (finity); manual restart; smooth socket reconnect observed once (no crash).
- Sep 2 — crash #2 = last straw; watchdog built, live 23:05; "pick up where we left off" proven.
- Sep 3 —
/new [title]landed via watchdog-restarted deploy; B6 roadmap item; B6 code branch; this draft. - Sep 4–7 (to 16:53Z) — no notable events evidenced: bridge.log was truncated 09-07 ~16:53Z by the
npm startrelaunch (earliest surviving entry is the 09-07T16:53:38Z banner), so Sep 4–6 detail is gone; watchdog.log shows one supervisor boot 2026-09-04T23:21:38Z (pid 249 → bridge 274, no "bridge down" — host-restart signature) and zero restarts for the next ~42 h. - Sep 7 — the busy day.
.env/index.jsedited (file mtimes 15:05/15:45Z); 16:53:38Znpm startrelaunch truncated bridge.log and ran alongside the watchdog bridge until restart-clean.sh (17:27:29Z) killed the duplicates and brought up the/resumebuild (banner: "…/new [title] archives a conversation; /resume [n] brings one back."). First titled/new17:01:05Z. Host restart ~18:40–18:59Z (log gap 18:39:25→18:59:12; pids reset). 19:03:06Z opencode exit 1 "Session not found" → fresh-session retry → exit 1 "No models loaded" (http://<LAN-IP>:1234had no model loaded). Clean restart #2 (19:06:40Z) killed hardcoded pid 249 instead of the live watchdog → duplicate watchdogs (388+856) and duplicate bridges (868+879).ReferenceError: result is not defined(index.js:353) ×3 (19:31/19:40/19:43Z), caught and sent to the user as "Sorry, something went wrong…"; bridge then died silently (~19:44–19:46Z) — watchdog restarted it (fail #1) 19:46:02Z; no ReferenceErrors after. Titled/new×4 on the day. - Sep 8 — steady use, zero watchdog events: titled
/new×3 ("widget rework" 01:37:56Z, "labor day article" 02:10:30Z, "SuperClaw status checks" 13:23:49Z); Labor Day article drafted, approved ("i approve. publish it." 04:26:12Z) and published through the bridge; quiet after ~16:12Z. - Sep 8: no bridge-ops events logged (quiet day)
- Sep 9 — host down ~00:13–04:40Z (log gap; watchdog up 04:40:48Z → bridge 298; second watchdog up 04:43:15Z → bridge 412, still current). 07:27–07:41Z socket-mode pong timeouts ×2 + DNS failures to slack.com (
EAI_AGAIN/ENOTFOUND) — no crash, no restart; absorbed./new retrospective article13:59:14Z (current session); "update it then post it to private page" 14:21:11Z — this draft update, in flight. 2026-09-15 annotation: the duplicate-watchdog outcome above is now structurally impossible — the catherder supervisor is flock-guarded and driven by persisted orders, so two supervisors (or two bridges) can no longer coexist.
Open follow-ups
- [x] First real
/new <title>— verify + screenshot the session title. - Verified: first titled
/newat 2026-09-07T17:01:05.047Z —<@<SLACK-USER>> /new SuperClaw local model configuration; title persisted verbatim in sessions.json (history entry "SuperClaw local model configuration", archivedAt 2026-09-07T17:59:59Z — the moment the next/newfired). 8 titled/newsince deploy; 7 of 8 titles verified verbatim in sessions.json — the "printer status" (2026-09-07T17:59:59Z) session never appears in history (Sep 7 duplicate-instance window). Closed 2026-09-15: the missing entry is a casualty of that duplicate-instance window (two bridges writing sessions.json); the entry itself is unrecoverable, and the class of bug is closed — catherder now enforces single-instance, so two writers can't race the file again. - [x] Decide: is "1 opencode per message" worth keeping vs. a persistent daemon (M1/M2 material)?
- Resolved 2026-09-15: keep per-message spawn. Turn ownership per process is exactly what makes the turn monitor and the new
/stop+/addcommands clean PID kills; per-channel queues already prevent head-of-line blocking across sessions. The media-delegation design (settled 09-10, Phase 2 machinery 09-13) runs on the append-only JSONL registry plus in-runtime subagents — it has no dependency on a persistent opencode server, so the "(M1/M2 material)" hunch (written 09-09, the day before that design settled) is stale. The one capability only a daemon would add — injecting a message into a running generation — is covered practically by/add <msg>(stop + requeue with full session context; the stopped turn's partial output stays in the session). Revisit only if true mid-turn injection or shared UI/bridge live state ever becomes a real need. - [x] Any lark/crash since the last update — add here.
- Added: Sep 7 restart/crash cluster; Sep 9 host bounce + Slack pong/DNS blip — see day-by-day log above. Sep 12–15 activity: see the 2026-09-15 update section below.
Update — 2026-09-15 (everything since the Sep 9 freeze)
Supervision: watchdog → catherder
- Catherder additions that matter here: orders mechanism (
status/start/stop/disable/enable/decommission→cats_state.json, re-read every cycle — operator and supervisor can't race) and per-cat non-blocking escalating backoff (09-12: v1 slept inside the loop and starved healthy cats for 120s per backoff event). - Bridge deploys now go through
catherder stop slack-bridge/start slack-bridge, driven by a detached waiter that waits for the in-flight turn'sopencode runto exit first.
Sep 14 outage (self-inflicted, 17:16–17:50Z)
Session store: reconciliation + hygiene (Sep 14)
- Sessions are one row each in a single global project (
worktree /); child sessions carryparent_id; UI-visible list =parent_id IS NULL.
New bridge commands (Sep 15)
/resumenow reconciles to the store on every invocation: prunes dead IDs from the channel binding + archive (7 stale entries pruned on first run — sessions deleted from the store long ago), lists store sessions newest-first, and/resume <n>adopts any store session even if this channel never archived it. Falls back to archive-only if the store is unreadable./stopaborts the in-flight turn for the channel: per-channel turn map (activeByChannel), SIGTERM + 2s SIGKILL backstop, a third close-path cause (killedByUser) beside the monitor's, and the generic "Sorry, something went wrong" suppressed for intentional stops. Partial output stays in the session; queued messages still run next./add <message>=/stop+ queue in one Slack message: kills the in-flight turn (if any) and falls through to the normal turn path, so the follow-up runs next with full session context. Degrades to a plain queue when nothing is in flight.
Restart/downtime notifications → SMS (Sep 15)
- The Windows box's reboot-notify v2 (tasks for Event 1074 restarting / 6008 dirty boot / boot "back up") switched from its Slack incoming webhook to the same SMS path, with a matching uninstall script — the webhook token was removed in the process.
- Rationale for the draft's §10 "status relay" vocabulary: Slack is now chat-only for this bot; out-of-band "the box is down/back" alerts live on SMS.
Evidence for this section: catherder/herder.log, slack-bridge/restart-status.log, slack-bridge/index.js (v: /status cats, /resume reconcile, /stop, /add), catherder.py (notify_sms), reboot-notify/reboot-notify-{setup,uninstall}.ps1, the store queries themselves.