Libra CodeHub

CedarCopilot/cedar-mail

Branch: staging

fix(slack): scope channel linking to user membership, not the whole workspace

closed#2596CedarCopilot

CedarCopilot wants to merge 2 commits into staging from fix/slack-channel-link-scope

Not deployedTimeline and evidence
  1. Opened
    Aug 26, 2026, 4:42 PM
  2. Aug 26, 2026, 5:30 PM
  3. Merged, live on staging, live on prod, observed
    Pending
  4. Not deployed

    Not deployed

    Pending

Libra has no production signal for this change yet because it has not deployed. Libra checks hourly for 3 days after each deploy.

Root cause

Channel linking selected candidates from listSlackChannels, which calls Slack's conversations.list , every channel in the workspace. Corgi names customer channels {company}-corgi, so an endsWith: "-corgi" pattern matched 445 workspace channels for a user who is a member of 16. Each candidate costs a Superglue members fetch plus DB work at SLACK_SYNC_CONCURRENCY_LIMIT = 1, exhausting both the Step Functions 10-minute state timeout and the HTTP gateway timeout , while linking nothing.

The codebase already documented the distinction, in userIsInSlackChannel's own docstring: users.conversations is what the caller is IN; conversations.list is everything in the workspace. The selection path never applied it.

A first hypothesis was disproved by the logs. I suspected a missing Effect.timeout around a hung linkSlackChannelOnlyDirect. Worker logs show every batch emitting initial_sync_step_complete and zero initial_sync_slack_channel_error rows. Nothing hung , there was simply far too much work.

Changes

  1. listUserSlackChannels (new, slack-api.ts) , users.conversations, paginated to the end, user token. Linking uses it. listSlackChannels keeps whole-workspace semantics because channel pickers legitimately need the full list.
  2. Container bootstrap (slack-sync.ts) , linkSlackChannelOnly returned success:false whenever no channel_containers row existed, and containers were only ever created by message ingest. That made link-only sync unable to onboard a new user at all: every channel is uningested by definition. Now bootstraps via the idempotent ensureContainerForMember.
  3. Loud config failure (link-matching-channels.ts) , SlackConfigSchema.autoSelectPatterns is z.array, but live connections stored a bare object. safeParse rejects the whole config, and the old path returned {linked:0, skipped:0, errors:0} behind a console.warn , indistinguishable from "nothing configured". Now a structured error counted as errors: 1.

Evidence

{"type":"initial_sync_step_complete","step":"slack_sync_batch_6","syncedCount":0,"channelCount":30}
[SLACK_LINK] User r0WQ…: linking 445 changed/new channel(s) (0 linked, 14 archived, 0 unchanged)

initial_sync_slack_channel_error → zero rows. Postgres: channel_container_users Liam 0/0 vs Rhocar 91/91.

Full report: docs/bugs/slack-channel-link-timeout-2026-08-26.md

Tests

9 new across 2 files (5 failing before the fix, pasted in the report). Full Slack suite: 94 passing / 12 files. pnpm --filter @zero/server run types clean for touched files (2 pre-existing errors in phase2c-seed-playbook.ts are unrelated and predate this branch).

Follow-up not in this PR

6 Slack connections across 5 other orgs still store autoSelectPatterns as an object and are silently no-oping their periodic channel linking. Corgi's two are repaired. The rest need a decision before touching live customer config.

šŸ¤– Generated with Claude Code

Greptile Summary

The PR scopes automatic Slack channel linking to channels joined by the connected user, bootstraps missing channel containers before evaluating links, and makes malformed sync configuration visible as an error.

  • Adds a paginated users.conversations integration using the connected user's token.
  • Creates missing Slack containers and user membership associations during link-only synchronization.
  • Adds regression coverage for membership scoping, pagination, token selection, API failures, and Slack configuration shape.
  • Updates account-setup guidance and records the Corgi incident and remediation.

Confidence Score: 4/5

The PR appears safe to merge after considering a non-blocking privacy improvement to avoid logging stored Slack channel configuration.

The membership-scoped listing and container bootstrap align with existing Slack and container contracts; t

Show production surfaces and changed-file mapping

Production surfaces

Libra has not measured any production surfaces for this change yet.

Changed files → surfaces

  • .claude/skills/account-setup-from-transcript/SKILL.mdno production surface mapped
  • apps/server/src/db/integration-schemas.tsno production surface mapped
  • apps/server/src/services/integrations/slack/__tests__/list-user-slack-channels.test.tsno production surface mapped
  • apps/server/src/services/integrations/slack/__tests__/slack-config-shape.test.tsno production surface mapped
  • apps/server/src/services/integrations/slack/link-matching-channels.tsno production surface mapped
  • apps/server/src/services/integrations/slack/slack-api.tsno production surface mapped
  • apps/server/src/services/integrations/slack/slack-sync.tsno production surface mapped
  • docs/account-setup/corgi-liam-2026-08-26.mdno production surface mapped
  • docs/account-setup/corgi-liam-proposal.mdno production surface mapped
  • docs/account-setup/mastra-jake-2026-08-25.mdno production surface mapped
  • docs/bugs/slack-channel-link-timeout-2026-08-26.mdno production surface mapped