Skip to content

Prevent broad integration local event watchers#98

Merged
kjgbot merged 2 commits into
mainfrom
fix/issue-82-track-a
Jun 5, 2026
Merged

Prevent broad integration local event watchers#98
kjgbot merged 2 commits into
mainfrom
fix/issue-82-track-a

Conversation

@kjgbot

@kjgbot kjgbot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

User description

Summary

  • Gate Pear's local integration filesystem event fallback so historical-download-off integrations produce no local watch roots.
  • Keep webhook/event delivery on the remote Relayfile subscription path.
  • Prevent recursive local fallback watching of broad provider history paths; only bounded .outbox command roots are eligible when historical download is explicitly enabled.

Issue

Track A for #82.

Tests

  • npm test

Follow-up notes for later tracks

  • Track B still needs the bounded per-project event queue/backpressure before broker injection. Live repro: a Claude session is currently receiving replayed historical Slack events as individual broker injections every 2-15 seconds; these should compact into a summary event during bursts/replays.
  • Track C still needs explicit no-backfill/reconnect semantics at the Relayfile subscription layer if the SDK supports them. Live repro: Slack messages up to 12 days old are being resynced as fresh relayfile.changed events.
  • Separate bug note for Prevent integration event and history sync from overwhelming local machines #82 follow-up: some replayed Slack events have malformed doubled paths like /slack/channels/C.../messages/<ts>/slack/channels/C.../messages/<ts>/meta.json.

CodeAnt-AI Description

Stop local fallback watchers from tracking broad history paths

What Changed

  • Local fallback watching is now turned off unless historical download is enabled and the integration includes a bounded command root like .outbox
  • Broad provider history paths such as channel or DM roots are no longer watched locally
  • When local fallback is allowed, only the bounded command root is watched, not parent history folders
  • Tests now cover the disabled case, the blocked broad-path case, and the allowed .outbox case

Impact

✅ Fewer runaway file watchers
✅ Less replay of old provider history
✅ Safer local sync for command-only folders

🔄 Retrigger CodeAnt AI Review

💡 Usage Guide

Checking Your Pull Request

Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.

Talking to CodeAnt AI

Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:

@codeant-ai ask: Your question here

This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.

Example

@codeant-ai ask: Can you suggest a safer alternative to storing this secret?

Preserve Org Learnings with CodeAnt

You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:

@codeant-ai: Your feedback here

This helps CodeAnt AI learn and adapt to your team's coding style and standards.

Example

@codeant-ai: Do not flag unused imports.

Retrigger review

Ask CodeAnt AI to review the PR again, by typing:

@codeant-ai: review

Check Your Repository Health

To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.

@codeant-ai

codeant-ai Bot commented Jun 5, 2026

Copy link
Copy Markdown

CodeAnt AI is reviewing your PR.

@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown

Review Change Stack

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 1dc29689-57ad-4f49-a63e-f94e65f23aec

📥 Commits

Reviewing files that changed from the base of the PR and between db86ed3 and 3fafd98.

📒 Files selected for processing (2)
  • src/main/__tests__/integration-event-bridge.test.ts
  • src/main/integration-event-bridge.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/tests/integration-event-bridge.test.ts

📝 Walkthrough

Walkthrough

Local mount watching and bridging in IntegrationEventBridge are restricted to integrations with downloadHistoricalData === true and canonical mount paths bounded to command roots (.outbox/outbox). Watch root resolution and tests were updated to reflect these tighter eligibility and filtering rules.

Changes

Local mount watch eligibility filtering

Layer / File(s) Summary
Eligibility detection and filtering helpers
src/main/integration-event-bridge.ts
New helpers detect bounded command-root segments (.outbox/outbox), determine integration eligibility (downloadHistoricalData === true + bounded mount path), and make localRootsForIntegration return empty when ineligible.
Watch root resolution from eligible integrations
src/main/integration-event-bridge.ts
localWatchRootsFor derives watchableIntegrations, returns [] if none qualify, iterates only watchable integrations to build the roots map, and filters remote-root candidates from watch globs to bounded command roots covered by at least one watchable integration.
Test coverage for eligibility filtering
src/main/__tests__/integration-event-bridge.test.ts
Tests assert empty roots when historical download is disabled, that broad provider history paths are not watched, that enabled Slack integrations are limited to /slack/.outbox with a bounded local suffix, and legacy mount-path expectations are simplified to include only /slack/.outbox.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

"I tunneled through the change with care,
eligibility petals in the air—
only bounded outboxes now take flight,
watching close, not every site.
Hop, review, and celebrate the light." 🐇✨


Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Pro by visiting https://app.coderabbit.ai/login.

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

@codeant-ai codeant-ai Bot added the size:M This PR changes 30-99 lines, ignoring generated files label Jun 5, 2026

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

Copy link
Copy Markdown

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 restricts local fallback watchers to bounded command roots (specifically paths containing '.outbox' or 'outbox') and ensures they are only active when historical download is enabled. It introduces helper functions to validate watchable integrations and filters candidates accordingly, accompanied by updated integration tests. No review comments were provided, so there is no additional feedback.

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 +494 to 501
function hasWatchableLocalIntegrationFor(
integrations: ConnectedIntegration[],
remoteRoot: string
): boolean {
return integrations.some((integration) =>
canonicalMountPaths(integration).some((mountPath) => pathIsInsideMount(remoteRoot, mountPath))
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: The mount-containment check is reversed in hasWatchableLocalIntegrationFor: it checks whether the candidate root is inside the mount path, which incorrectly rejects parent candidates generated for dynamic children (for example, candidate /.../.outbox vs mount /.../.outbox/commands). This prevents expected parent roots from being watched and can drop local fallback events for newly created dynamic subpaths. Compare in the opposite direction so mount paths are checked as being inside the candidate root. [incorrect condition logic]

Severity Level: Major ⚠️
- ⚠️ Local fallback watchers skip parent .outbox roots for commands.
- ⚠️ Dynamic command subpaths may miss local filesystem events.
- ⚠️ IntegrationEventBridge local subscriptions become less reliable offline.
Steps of Reproduction ✅
1. In `src/main/integration-event-bridge.ts:520-524`, `localWatchRootsFor()` computes
local watch roots from a workspace ID, a list of `ConnectedIntegration`s, and `globs`
coming from subscription watches.

2. `IntegrationEventBridge.reconcile()` at `src/main/integration-event-bridge.ts:860-939`
calls `watchLocalMounts()`, which in turn calls `localWatchRootsFor()` (see
`watchLocalMounts()` at lines 577-585) with `globs` derived from `watchRegistrationsFor()`
/ `subscriptionSpecsFor()` using `canonicalMountPaths()`.

3. Consider a realistic configuration where a `ConnectedIntegration` has a bounded command
mount path like `/slack/channels/C123ABC__proj-cloud/.outbox/commands` (allowed by
`canonicalMountPaths()` at lines 20-35) and `downloadHistoricalData: true`.
`watchRegistrationsFor()` then produces a watch glob
`/slack/channels/C123ABC__proj-cloud/.outbox/commands/**`, which is passed into
`localWatchRootsFor()`.

4. Inside `localWatchRootsFor()` (lines 520-121), for each glob the helper
`parentRemoteRootForDynamicChildren()` (lines 81-85) derives a parent candidate
`/slack/channels/C123ABC__proj-cloud/.outbox`. This candidate passes
`isBoundedLocalCommandRoot()` (lines 55-58), but `hasWatchableLocalIntegrationFor()`
(lines 65-71) calls `pathIsInsideMount(remoteRoot, mountPath)` with `remoteRoot` as the
candidate and `mountPath` as the (deeper) canonical mount path. Given
`pathIsInsideMount()` at lines 117-121 returns true only when the first argument is equal
to or below the second, the parent `/.../.outbox` is not considered "inside" the child
`/.../.outbox/commands`, so `hasWatchableLocalIntegrationFor()` returns false and
`localWatchRootsFor()` drops the parent candidate entirely. As a result, expected parent
`.outbox` roots produced for dynamic children are never watched, and local fallback events
under those parent roots are missed.

Fix in Cursor | Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/main/integration-event-bridge.ts
**Line:** 494:501
**Comment:**
	*Incorrect Condition Logic: The mount-containment check is reversed in `hasWatchableLocalIntegrationFor`: it checks whether the candidate root is inside the mount path, which incorrectly rejects parent candidates generated for dynamic children (for example, candidate `/.../.outbox` vs mount `/.../.outbox/commands`). This prevents expected parent roots from being watched and can drop local fallback events for newly created dynamic subpaths. Compare in the opposite direction so mount paths are checked as being inside the candidate root.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

@agent-relay-code

Copy link
Copy Markdown
Contributor

Fixed one verified PR issue: local fallback watchability now checks canonical mount paths, so legacy /integrations/<provider>/.outbox command mounts are not incorrectly rejected.

Added a regression test covering that legacy command mount path.

Local validation passed:
node --experimental-strip-types --no-warnings --test src/main/__tests__/integration-event-bridge.test.ts
npm test
npm run build
npm run verify:mcp-resources-drift

@agent-relay-code

Copy link
Copy Markdown
Contributor

pr-reviewer applied fixes — committed and pushed 3fafd98 to this PR. The notes below describe what changed.

Fixed one verified PR issue: local fallback watchability now checks canonical mount paths, so legacy /integrations/<provider>/.outbox command mounts are not incorrectly rejected.

Added a regression test covering that legacy command mount path.

Local validation passed:
node --experimental-strip-types --no-warnings --test src/main/__tests__/integration-event-bridge.test.ts
npm test
npm run build
npm run verify:mcp-resources-drift

Comment on lines +525 to +527
const watchableIntegrations = watchableLocalIntegrations(integrations)
if (watchableIntegrations.length === 0) 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.

🟠 Architect Review — HIGH

Local fallback eligibility now depends on downloadHistoricalData (via allowsLocalMountWatching), but IntegrationEventBridge.reconcile's subscription signature does not include this flag; when a user toggles historical download, syncEventSubscriptions still calls reconcile, yet the unchanged signature causes an early return so local mount fallback watchers are not created or torn down to match the new setting.

Suggestion: Extend the reconcile signature to include local-fallback–relevant state (at least downloadHistoricalData or derived localWatchRootsFor output), or explicitly force close-and-resubscribe when that state changes so toggling historical download reliably updates local watchers.

Fix in Cursor | Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is an **Architect / Logical Review** comment left during a code review. These reviews are first-class, important findings — not optional suggestions. Do NOT dismiss this as a 'big architectural change' just because the title says architect review; most of these can be resolved with a small, localized fix once the intent is understood.

**Path:** src/main/integration-event-bridge.ts
**Line:** 525:527
**Comment:**
	*HIGH: Local fallback eligibility now depends on downloadHistoricalData (via allowsLocalMountWatching), but IntegrationEventBridge.reconcile's subscription signature does not include this flag; when a user toggles historical download, syncEventSubscriptions still calls reconcile, yet the unchanged signature causes an early return so local mount fallback watchers are not created or torn down to match the new setting.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
If a suggested approach is provided above, use it as the authoritative instruction. If no explicit code suggestion is given, you MUST still draft and apply your own minimal, localized fix — do not punt back with 'no suggestion provided, review manually'. Keep the change as small as possible: add a guard clause, gate on a loading state, reorder an await, wrap in a conditional, etc. Do not refactor surrounding code or expand scope beyond the finding.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix

@codeant-ai

codeant-ai Bot commented Jun 5, 2026

Copy link
Copy Markdown

CodeAnt AI finished reviewing your PR.

@kjgbot kjgbot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Review: Track A (#82) — scope-conformance + acceptance verification

Scope conformance: ✅ Both commits map cleanly to Track A ("Remove broad local recursive watchers"). No scope creep. Commit 3fafd98's mountPathscanonicalMountPaths in the gate correctly handles the legacy /integrations/<provider>/... form.

Acceptance criteria verified:

  1. Historical download OFF → zero recursive watchers on integration data trees: ✅
    allowsLocalMountWatching (src/main/integration-event-bridge.ts:478-482) requires downloadHistoricalData === true AND a bounded command root; localWatchRootsFor short-circuits to [] (:525-526), so watchLocalMounts returns null and watch(localRoot, { recursive: true }) (:624) is never reached. That :624 call is the only fs.watch site in non-test main-process code (verified by grep), now reachable only for bounded .outbox roots with history on. Glob-derived candidates are double-gated (:542-543).

  2. Webhook events still notify subscribed agents: ✅ The remote Relayfile subscription path (:865-888) is untouched and unconditional; existing delivery tests pass.

  3. Writeback still works: ✅ Writeback upload goes through the poll-mode integration mounts (integration-mounts.ts), not the bridge — the gated watchers were inbound-event fallback only. Draft/command files were already notification-filtered (:760).

Tests: ran locally at PR head — bridge suite 12/12, full npm test 38/38 pass. CI green.

Unbounded-dataflow check: change strictly reduces watcher surface. Remaining Promise.all recipient fanout (:1002) and per-event listAgents are pre-existing Track B/E work, correctly deferred.

Findings (non-blocking)

  1. isBoundedLocalCommandRoot accepts bare outbox segments (src/main/integration-event-bridge.ts:487). A provider history resource literally named outbox (plausible for mail providers, e.g. /google-mail/outbox) combined with history-on would get a recursive watcher over a history tree — violating the invariant stated in the comment at :479. Issue #82 only ever names dotted .outbox as the command surface. Suggest dropping the bare 'outbox' arm or anchoring to a shared command-root constant. Latent today (no catalog path produces outbox), so non-blocking.

  2. Test gap: the "disabled when historical download is off" test leaves downloadHistoricalData undefined and uses non-outbox mounts, so it conflates the two gates. Add a case with downloadHistoricalData: false + mountPaths: ['/slack/.outbox'] asserting [] to pin the history off-switch in isolation.

  3. Boundary observation (for Track G, not this PR): integration-mounts.ts:280 still starts mode: 'poll' mounts for live/writeback provider paths even with history off. Track A's watcher criterion is met, but poll-mount traffic on provider paths remains — belongs to Track G (codex-5).

Verdict

APPROVE — Track A acceptance criteria met; findings 1–2 are good fast-follows (fine to fold into the Track B PR).

@agent-relay-code

Copy link
Copy Markdown
Contributor

Fixed one validated issue in PR #98: outbox is now only treated as a bounded command root at the provider command position, not anywhere inside a history path like /github/repos/outbox.

Changes:

Verification run:

  • npm test passed: 39/39
  • npm run build passed after installing dependencies
  • npm run verify:mcp-resources-drift passed
  • Removed local install/build artifacts afterward

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M This PR changes 30-99 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant