feat(agent): resume parked runs from inbound Slack events

GI
2mo ago
+5214-205

Opened automatically when feat/builder-creates-channel was pushed.

The title is written from the diff and rewritten as you push, because this is squashed onto main and the title becomes the commit subject and the changelog line. Retitle it yourself and it is yours — the automation stops touching it.


Summary by cubic

Inbound Slack events now resume the parked run that owns a channel instead of stopping at an outbound-only integration. Agents can open and invite people in run-owned channels, while duplicate, late, or unowned events settle without starting another run.

New Features

  • POST /webhooks/slack/events verifies signatures, answers URL verification, and stores supported messages, mentions, and member joins in a deduplicated inbox.
  • Leased dispatch drains events in order, retries undelivered resumes, isolates per-event failures, and returns 200 for invalid or unsupported payloads Slack should not retry.
  • slack.channel.open claims or reuses normalized channels, joins the bot before use, records the channel on the run, and refuses a reused channel the agent can no longer reach; slack.channel.invite supports workspace members and Slack Connect guests.
  • Resumed runs keep the channel they own, keep a paused agent's message for replay, and stop sending invitations when the run ends.
  • run-channel destinations resolve to the channel opened by the run; chosen destinations remain pinned to inspected Slack IDs.
  • The builder can create missing channels with approval and asks one question before routing a named channel outside the inspected inventory.

Migration

  • Set SLACK_SIGNING_SECRET, configure Slack Event Subscriptions with API_URL + /webhooks/slack/events, and keep the tunnel hostname stable with bun run tunnel:slack; the tunnel forwards to PORT unless SLACK_TUNNEL_PORT overrides it.
  • Re-approve the Slack app to grant app_mentions:read, then apply the four Prisma migrations.
  • Existing Slack destinations must include resolution: "chosen" or "run-channel"; destinations without it fail validation.