Skip to content

Fix Slack integration event context reads#154

Merged
khaliqgant merged 2 commits into
mainfrom
fix/slack-integration-event-context
Jun 7, 2026
Merged

Fix Slack integration event context reads#154
khaliqgant merged 2 commits into
mainfrom
fix/slack-integration-event-context

Conversation

@khaliqgant
Copy link
Copy Markdown
Member

@khaliqgant khaliqgant commented Jun 7, 2026

Summary

  • make Slack DM integration-event subscriptions opt-in instead of defaulting selected channel mounts into unreadable DM scopes
  • retry Slack targeted context reads through same-channel mounted alias paths so raw C123 events can resolve C123__name content
  • mount selected Slack /threads roots as narrow mirror context roots so thread replies can reconcile locally without broad channel-history sync

Cross-repo alignment

  • Relayfile worker confirmed provider ingestion canonicalizes Slack provider-relative paths, resolves raw channel IDs to existing same-channel aliases before store writes/events, and passes go test ./....
  • Cloud worker confirmed gateway files.read and path-scope checks apply the same same-channel raw/alias equivalence, deny DMs by default, and pass focused gateway regression tests.
  • Pear keeps the same contract: alias fallback only for the same canonical Slack channel ID, no default DM subscription, and no broad channel-history mount.

Validation

  • node --experimental-strip-types --no-warnings --test src/main/__tests__/integration-event-bridge.test.ts (61/61)
  • npx vitest run src/main/integrations.test.ts src/main/integration-mounts.test.ts (42/42)
  • npm test -- src/main/__tests__/integration-event-bridge.test.ts (92/92)

@codeant-ai
Copy link
Copy Markdown

codeant-ai Bot commented Jun 7, 2026

Your free trial PR review limit of 300 PRs has been reached. Please upgrade your plan to continue using CodeAnt AI.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 7, 2026

Review Change Stack

Warning

Review limit reached

@khaliqgant, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 44 minutes and 59 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ebc09bdd-8312-4f80-a417-bf3954f7d4ad

📥 Commits

Reviewing files that changed from the base of the PR and between 00128f5 and 8b7b26b.

📒 Files selected for processing (5)
  • src/main/__tests__/integration-event-bridge.test.ts
  • src/main/integration-event-bridge.ts
  • src/main/integrations.test.ts
  • src/renderer/src/components/settings/AccountSettings.tsx
  • src/renderer/src/components/settings/ProjectSettings.tsx
📝 Walkthrough

Walkthrough

This PR improves Slack integration event handling by introducing targeted context path selection based on subscription-matched mount paths, making DM listening opt-in rather than default, and adding infrastructure to mount and surface live thread context roots to agents.

Changes

Slack Integration Enhancements

Layer / File(s) Summary
Foundation: Deduplication, DM Opt-In, & Mount Path Scoping
src/main/integration-event-bridge.ts, src/main/__tests__/integration-event-bridge.test.ts
Introduces dedupeStringsInOrder utility for deterministic duplicate removal with insertion-order preservation. Changes slackListenDms default from enabled to disabled. Updates integration mount path scoping test expectations to narrow subscription watches to selected Slack mount-path aliases, removing broader coverage and validating that non-selected channel events produce no delivered messages. Updates related test scopes to explicitly enable listenDms where needed.
Slack Event Context Path Selection & Formatting
src/main/integration-event-bridge.ts, src/main/__tests__/integration-event-bridge.test.ts
Implements slackContextReadCandidatePaths to compute ordered, deduplicated candidate context read paths by analyzing which subscription specs match the event and canonicalizing Slack channel IDs. Updates readEventContextPreview to accept matchedSpecs and implements targeted readFile attempts across candidate paths with fallback behavior. Updates message formatting to derive the rendered scope label from resolved contextPreview.path rather than always using the event's original path. Validates with test replacements that demonstrate raw-id context reads failing over to mounted slug-alias paths and updates targeted-context read expectations to include both mounted and canonical path attempts.
Live Thread Context Mount Paths
src/main/integrations.ts, src/main/__tests__/integration-mounts.test.ts, src/main/integrations.test.ts
Adds slackThreadContextMountPathsForIntegration helper to derive thread context roots from Slack collection-level mount paths, integrating them into localSyncMountPathsForIntegration when historical downloads are disabled. Introduces test case validating that thread context roots are mounted with localLayout: 'exact' and syncMode: 'mirror'. Updates mount path expectations to include live thread context entries under channels and DMs.
Agent Guidance for Live Thread Contexts
src/main/integrations.ts, src/main/integrations.test.ts
Extends system message generation to compute liveContextPaths from thread mount infrastructure (empty when historical download is enabled; otherwise derived from Slack thread context mounts) and includes them in agent-facing guidance. Updates test expectations to validate that messages document the .integrations/slack/.../threads path locations for live thread context roots.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • AgentWorkforce/pear#148: Both PRs modify src/main/integration-event-bridge.ts's Slack event injection behavior; this PR adds targeted context path selection via matchedSpecs, while the retrieved PR layers injected-delivery confirmation and provisional/committed dedupe state tracking into the same event injection core.

Poem

🐰 A rabbit hops through Slack's deep threads,
Now smarter paths lead where context spreads,
With dedup ordered, mounts precise and bright,
Live thread contexts guide agents right! 🧵✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Fix Slack integration event context reads' accurately summarizes the main change: improving how Slack integration event context is read, matching the core objective of fixing context targeting and deduplication.
Description check ✅ Passed The description is directly related to the changeset, detailing three key improvements (DM opt-in, context read retry, thread root mounts), cross-repo validation, and comprehensive test results.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/slack-integration-event-context

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates the Slack integration event bridge to resolve event context through mounted slug aliases when raw-id targeted reads fail, and introduces mounting for Slack thread context roots in mirror mode when historical downloads are disabled. Additionally, Slack direct message event scoping is now opt-in by default. Feedback suggests optimizing the retry loop for event context preview reads by skipping further retries on candidate paths that encounter permanent authorization errors, and generalizing the Slack provider check to support custom or enterprise Slack provider variants.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines 2331 to 2340
for (const [index, delayMs] of readDelays.entries()) {
if (delayMs > 0) await delay(delayMs)
try {
return eventContextPreviewFromFile(await client.readFile(handle.workspaceId, path))
} catch (error) {
readFileError = error
if (index === readDelays.length - 1) break
for (const candidatePath of candidatePaths) {
try {
return eventContextPreviewFromFile(await client.readFile(handle.workspaceId, candidatePath))
} catch (error) {
readFileError = error
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If a candidate path fails with a permanent error like 403 Forbidden or 401 Unauthorized (which can happen if the raw channel ID path is outside the allowed pathScope), retrying it in subsequent delay iterations is wasteful and increases latency. We should filter out candidate paths that encounter unauthorized/forbidden errors so they aren't retried in the next rounds.

        let activeCandidates = [...candidatePaths]
        for (const [index, delayMs] of readDelays.entries()) {
          if (activeCandidates.length === 0) break
          if (delayMs > 0) await delay(delayMs)
          const nextCandidates: string[] = []
          for (const candidatePath of activeCandidates) {
            try {
              return eventContextPreviewFromFile(await client.readFile(handle.workspaceId, candidatePath))
            } catch (error) {
              readFileError = error
              if (!isUnauthorizedError(error)) {
                nextCandidates.push(candidatePath)
              }
            }
          }
          activeCandidates = nextCandidates
        }

Comment thread src/main/integrations.ts
Comment on lines +398 to +410
function slackThreadContextMountPathsForIntegration(integration: ConnectedIntegration): string[] {
if (toRelayfileProvider(integration.provider) !== 'slack') return []
return canonicalMountPathsForConnectedIntegration(integration)
.filter(isNarrowHistoricalMountPath)
.flatMap((mountPath) => {
const segments = mountPath.split('/').filter(Boolean)
if (segments[0] !== 'slack') return []
const collection = segments[1]
if (!['channels', 'dms', 'users'].includes(collection || '')) return []
if (segments.length === 3) return [`${mountPath}/threads`]
return []
})
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current implementation hardcodes the provider check to exactly 'slack'. If the repository uses enterprise or custom Slack provider variants (e.g., 'slack-enterprise'), this check and the subsequent segments[0] !== 'slack' check will fail, preventing thread context paths from being mounted. We should generalize this to support any provider starting with 'slack-'.

Suggested change
function slackThreadContextMountPathsForIntegration(integration: ConnectedIntegration): string[] {
if (toRelayfileProvider(integration.provider) !== 'slack') return []
return canonicalMountPathsForConnectedIntegration(integration)
.filter(isNarrowHistoricalMountPath)
.flatMap((mountPath) => {
const segments = mountPath.split('/').filter(Boolean)
if (segments[0] !== 'slack') return []
const collection = segments[1]
if (!['channels', 'dms', 'users'].includes(collection || '')) return []
if (segments.length === 3) return [`${mountPath}/threads`]
return []
})
}
function slackThreadContextMountPathsForIntegration(integration: ConnectedIntegration): string[] {
const provider = toRelayfileProvider(integration.provider)
if (provider !== 'slack' && !provider.startsWith('slack-')) return []
return canonicalMountPathsForConnectedIntegration(integration)
.filter(isNarrowHistoricalMountPath)
.flatMap((mountPath) => {
const segments = mountPath.split('/').filter(Boolean)
if (segments[0] !== provider) return []
const collection = segments[1]
if (!['channels', 'dms', 'users'].includes(collection || '')) return []
if (segments.length === 3) return [mountPath + '/threads']
return []
})
}

@agent-relay-code
Copy link
Copy Markdown
Contributor

ℹ️ pr-reviewer: review only — no file changes were applied to the PR (nothing to commit after review). The notes below are advisory and were not pushed.

Reviewed PR #154 against .workforce/pr.diff, changed-files.txt, and the current checkout. I did not find a reproducible current breakage or stale bot-review issue to fix, so I left the PR code unchanged.

Validation run:

  • npm install
  • npx vitest run src/main/integrations.test.ts src/main/integration-mounts.test.ts
  • node --experimental-strip-types --no-warnings --test src/main/__tests__/integration-event-bridge.test.ts
  • npm test
  • npx vitest run

All local tests passed. I’m not printing READY because I cannot verify GitHub CI completion, pending checks, or mergeability from this sandbox.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/main/integration-event-bridge.ts`:
- Around line 413-416: The bridge function slackListenDms uses
scopeBooleanDefault(..., false) to make DM listening opt-in, but the UI snippets
still default listenDms to true; update the UI/snippet/default payloads so the
listenDms/listenDirectMessages/directMessages scope defaults to false (matching
slackListenDms and scopeBooleanDefault) — ensure the UI uses the same default
value or the same helper logic when constructing the integration scope so DM
listening is opt-in across both codepaths.

In `@src/main/integration-mounts.test.ts`:
- Around line 279-295: Add a regression test that ensures a single Slack channel
spec expands into both messages and threads mounts when downloadHistoricalData
is false: instantiate IntegrationMountManager, call ensureMounted with a spec
like { provider: 'slack', mountPaths: ['/slack/channels/C123'],
downloadHistoricalData: false } and assert that mock.mountInputs contains two
entries matching remotePath '/slack/channels/C123/messages' and
'/slack/channels/C123/threads' with distinct localDir values and both having
localLayout 'exact' and syncMode 'mirror' (use IntegrationMountManager,
ensureMounted, and mock.mountInputs to locate and verify the mounts).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d9b8f737-bfeb-489b-bb5e-ee94f6757110

📥 Commits

Reviewing files that changed from the base of the PR and between 6721107 and 00128f5.

📒 Files selected for processing (5)
  • src/main/__tests__/integration-event-bridge.test.ts
  • src/main/integration-event-bridge.ts
  • src/main/integration-mounts.test.ts
  • src/main/integrations.test.ts
  • src/main/integrations.ts

Comment on lines 413 to 416
function slackListenDms(integration: ConnectedIntegration): boolean {
if (!isSlackProvider(integration.provider)) return false
return scopeBooleanDefault(integration.scope, ['listenDms', 'listenDirectMessages', 'directMessages'], true)
return scopeBooleanDefault(integration.scope, ['listenDms', 'listenDirectMessages', 'directMessages'], false)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Align DM defaults across UI and bridge to keep DM listening truly opt-in.

Line 415 now defaults unset DM scope to false, but the provided UI snippets still default listenDms to true when unset. That mismatch can silently keep DM subscriptions enabled and weakens the PR’s intended privacy posture.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/integration-event-bridge.ts` around lines 413 - 416, The bridge
function slackListenDms uses scopeBooleanDefault(..., false) to make DM
listening opt-in, but the UI snippets still default listenDms to true; update
the UI/snippet/default payloads so the
listenDms/listenDirectMessages/directMessages scope defaults to false (matching
slackListenDms and scopeBooleanDefault) — ensure the UI uses the same default
value or the same helper logic when constructing the integration scope so DM
listening is opt-in across both codepaths.

Comment on lines +279 to +295
it('mounts Slack thread context roots in mirror mode', async () => {
const manager = new IntegrationMountManager()

await manager.ensureMounted([
{
provider: 'slack',
mountPaths: ['/slack/channels/C123/threads']
}
])

expect(mock.mountInputs[0]).toMatchObject({
localDir: '/tmp/pear-home/.agentworkforce/pear/relayfile/workspaces/account-workspace-id/slack/channels/C123/threads',
remotePath: '/slack/channels/C123/threads',
localLayout: 'exact',
syncMode: 'mirror'
})
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add a test case for mounting both messages and threads for the same channel.

The coding guideline requires regression tests when touching integration notifications, not just happy-path cases. Based on integrations.test.ts line 490-507, when downloadHistoricalData is false, a single channel spec expands to both /slack/channels/C123/messages and /slack/channels/C123/threads. However, there's no test verifying that mounting both together works correctly as separate mounts without conflicts.

Suggested test case
+  it('mounts both messages and threads for the same channel as separate mounts', async () => {
+    const manager = new IntegrationMountManager()
+
+    await manager.ensureMounted([
+      {
+        provider: 'slack',
+        mountPaths: ['/slack/channels/C123/messages', '/slack/channels/C123/threads']
+      }
+    ])
+
+    expect(mock.mountInputs).toHaveLength(2)
+    expect(mock.mountInputs[0]).toMatchObject({
+      localDir: '/tmp/pear-home/.agentworkforce/pear/relayfile/workspaces/account-workspace-id/slack/channels/C123/messages',
+      remotePath: '/slack/channels/C123/messages',
+      localLayout: 'exact',
+      syncMode: 'write-only'
+    })
+    expect(mock.mountInputs[1]).toMatchObject({
+      localDir: '/tmp/pear-home/.agentworkforce/pear/relayfile/workspaces/account-workspace-id/slack/channels/C123/threads',
+      remotePath: '/slack/channels/C123/threads',
+      localLayout: 'exact',
+      syncMode: 'mirror'
+    })
+  })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/integration-mounts.test.ts` around lines 279 - 295, Add a regression
test that ensures a single Slack channel spec expands into both messages and
threads mounts when downloadHistoricalData is false: instantiate
IntegrationMountManager, call ensureMounted with a spec like { provider:
'slack', mountPaths: ['/slack/channels/C123'], downloadHistoricalData: false }
and assert that mock.mountInputs contains two entries matching remotePath
'/slack/channels/C123/messages' and '/slack/channels/C123/threads' with distinct
localDir values and both having localLayout 'exact' and syncMode 'mirror' (use
IntegrationMountManager, ensureMounted, and mock.mountInputs to locate and
verify the mounts).

Source: Coding guidelines

Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

View 4 additional findings in Devin Review.

Open in Devin Review

Comment on lines 412 to 416

function slackListenDms(integration: ConnectedIntegration): boolean {
if (!isSlackProvider(integration.provider)) return false
return scopeBooleanDefault(integration.scope, ['listenDms', 'listenDirectMessages', 'directMessages'], true)
return scopeBooleanDefault(integration.scope, ['listenDms', 'listenDirectMessages', 'directMessages'], false)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 Slack DM listening default change is a breaking behavioral change

The slackListenDms function at src/main/integration-event-bridge.ts:403 changes its default from true to false. Any existing integration configuration that omits an explicit listenDms/listenDirectMessages/directMessages scope key will silently stop receiving DM events. This is clearly intentional (test renamed from 'can be disabled' to 'is opt-in'), but downstream consumers or existing project configurations that relied on the implicit default will be affected without any migration path or warning log.

(Refers to lines 403-416)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 5 files

Re-trigger cubic

@codeant-ai
Copy link
Copy Markdown

codeant-ai Bot commented Jun 7, 2026

Your free trial PR review limit of 300 PRs has been reached. Please upgrade your plan to continue using CodeAnt AI.

@khaliqgant khaliqgant merged commit d2449ff into main Jun 7, 2026
4 checks passed
@khaliqgant khaliqgant deleted the fix/slack-integration-event-context branch June 7, 2026 21:42
khaliqgant added a commit that referenced this pull request Jun 8, 2026
…ssion (#155)

* fix(slack): local-disk fallback for event preview + suffixed path emission

readEventContextPreview() was SDK-read-only (client.readFile), which missed the suffixed mount candidate under remote-consistency lag even when the file was already on local disk. Events therefore emitted bare-path + "content unavailable" on a current build — #154 fixed candidate generation, not the read source.

- Add local-disk fallback scoped to matched concrete mount roots only (no broad .integrations scan; preserves DM "watched-but-not-mounted" state)
- Emit the resolved suffixed path for visible Path:, data.path, and data.resource.path (closes the structured-metadata-stays-bare gap)
- Regression for bare event path + suffixed local mount + SDK read miss

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore: apply pr-reviewer fixes for #155

* chore: apply pr-reviewer fixes for #155

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: agent-relay-code[bot] <agent-relay-code[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant