Skip to content

perf(control-plane): batch Slack external-session membership checks #466

Description

@philz3906

Rewritten 2026-08-15. The original issue bundled nine acceptance criteria.
Five of them landed as side effects of other work, and its cost model was wrong:
the dominant cost of a cold /sessions read was the SQL visibility predicate,
not Slack round trips (#751). What follows is only what is still undone.

Already landed — do not redo

What is left

1. Resolve viewer membership as one snapshot per credential

checkMembership() still paginates conversations.members once per external scope.
A viewer with 200 private-channel scopes spends ~200 member walks on a cold read, and
the per-principal lease is short (allow 120s, deny 30s), so continued browsing re-spends
them. Nothing warms this half: the warmer observes resource facts, and membership is
per-principal.

Direction, unchanged from the original proposal:

  1. Group candidate scopes by credentialId + credentialRevision + realmKey, and load the bot and its secret once per group.

  2. For groups holding restricted scopes, paginate with the existing bot token:

    users.conversations(
      user=<viewer_user_id>,
      types=public_channel,private_channel,mpim
    )
    

    then intersect the returned conversation ids with the candidate external scopes.

  3. Preserve the current policy exactly: ordinary full workspace members read public-channel sessions without channel membership; private channels and MPIMs require membership; guests and external / Slack Connect identities require membership even for public channels; DMs keep the private owner-identity path and stay out of the snapshot.

Slack documents that a bot token plus the user parameter returns the conversations
shared by the bot and that user:
https://docs.slack.dev/reference/methods/users.conversations/

2. Honor Retry-After on HTTP 429

slackCall() turns any non-2xx into http_<status>, so a 429 becomes a short-lived
unknown and the next read retries into the same limit. Handle Retry-After while
preserving the fail-closed and degraded behavior.

Identity and OAuth constraints

Unchanged, and they still govern the snapshot design. Do not introduce a Feishu-style
identity bridge or reuse a console Slack OAuth user token — Slack user identity is
already the workspace-qualified (team_id, user_id), directly comparable with the
linked console identity. Sign in with Slack OIDC scopes cannot be mixed with Web API
scopes in the same OAuth flow:
https://docs.slack.dev/authentication/sign-in-with-slack/

The existing bot template already grants channels:read, groups:read, mpim:read,
and users:read, so this requires no new scope and no user reauthorization.

Acceptance criteria

  • Membership work is performed once per bot credential/viewer snapshot, not once per external scope.
  • A 200-restricted-scope regression test proves one paginated users.conversations snapshot is reused across all scopes, with provider-call-count assertions on both the cold and warm paths.
  • Public-channel behavior is unchanged for full members, guests, and external users; private channels and MPIMs stay fail-closed.
  • Revoked credentials, deleted conversations, missing scopes, provider outages, and rate limits preserve the current definitive-denial vs. degraded-unknown distinction.
  • HTTP 429 honors Retry-After.
  • Scope 201+ remains eligible; no provider optimization reintroduces a visibility ceiling.
  • No console Slack Web API user token, second OAuth flow, new identity bridge, or additional bot scope is introduced.
  • Slack Connect behavior is verified with a bot token and a foreign-workspace user before rollout.

Priority

Lower than when this was filed. Re-measure the cold-visit budget after #793#799 before
starting; that measurement, not this issue's original estimate, should decide whether the
snapshot is worth building.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions