Skip to content

fix(composio): keep chat integrations cache in sync on Windows#776

Merged
graycyrus merged 1 commit into
tinyhumansai:mainfrom
CodeGhost21:fix/issue-749-windows-tool-sync
Apr 22, 2026
Merged

fix(composio): keep chat integrations cache in sync on Windows#776
graycyrus merged 1 commit into
tinyhumansai:mainfrom
CodeGhost21:fix/issue-749-windows-tool-sync

Conversation

@CodeGhost21
Copy link
Copy Markdown
Contributor

@CodeGhost21 CodeGhost21 commented Apr 22, 2026

Closes #749.

Summary

On Windows, integrations show as Connected in Settings but the chat agent keeps reporting them as "not connected" / unavailable. The root cause is a cache-invalidation miss that's much more likely on Windows than on macOS/Linux:

  • composio_authorize publishes ComposioConnectionCreated as soon as the backend returns a connectUrl.
  • ComposioConnectionCreatedSubscriber then polls wait_for_connection_active for up to 60 s, and only calls invalidate_connected_integrations_cache() if it observes ACTIVE/CONNECTED within that window.
  • On Windows the OAuth round-trip routinely exceeds 60 s (Defender SmartScreen delay, slower browser launch, extra consent dialogs), so the invalidate call never fires.
  • Meanwhile the Settings UI polls listConnections() every 5 s (app/src/lib/composio/hooks.ts) and shows the badge correctly from the fresh backend response — but that path never touches the Rust INTEGRATIONS_CACHE, so the chat runtime stays frozen on the pre-connect snapshot indefinitely.

This PR layers two cross-platform defenses on top of the existing event-bus path, without changing the happy-path behavior:

  1. UI-poll-driven sync: composio_list_connections now diffs the backend's active-toolkit set against every entry in INTEGRATIONS_CACHE and invalidates diverged entries. Because the UI polls every 5 s, the chat cache converges to truth within one poll interval regardless of how the OAuth completed or which OS the user is on.
  2. Defensive TTL: CACHE_TTL = 60 s caps the worst-case staleness even when nothing polls (CLI-only flows, backgrounded app).

Also adds grep-friendly [composio][integrations] logs at every decision point — cache hit / miss / TTL expiry / divergence diff with added+removed toolkit sets — so the next Windows regression can be traced from a user-supplied debug dump without needing a live repro.

Changes

  • src/openhuman/composio/ops.rs:
    • New CachedIntegrations { entries, cached_at } struct; INTEGRATIONS_CACHE now keys by config path → timestamped entry.
    • New sync_cache_with_connections() called from composio_list_connections — O(n) set diff, no lock held across the decision.
    • TTL-aware read path in fetch_connected_integrations.
    • Six new unit tests for the regression: activate, disconnect, steady-state no-op, non-active rows ignored, ACTIVE≡CONNECTED alias, TTL expiry.
    • Shared CACHE_TEST_GUARD mutex across all cache-touching tests so the new tests don't race the existing mock-backend tests over the process-wide map.

Test plan

  • cargo test --lib -p openhuman composio:: — all 254 tests pass (31 in composio::ops::tests including the 6 new regression tests).
  • cargo fmt, cargo clippy --lib -p openhuman --no-deps — no new warnings from this PR.
  • Windows manual smoke: connect Gmail via Settings, wait past the 60 s event-bus timeout, confirm chat sees delegate_gmail on the next turn without restart.
  • Windows restart smoke: relaunch the app, confirm the connected integration is still available in chat (unchanged behavior — cache starts empty on boot).
  • macOS/Linux smoke: confirm normal OAuth flow still invalidates within the 60 s window (happy path unchanged).

Acceptance criteria (from #749)

  • Connection status shown in UI is reflected in chat skill availability (within 5 s via UI poll, or 60 s via TTL).
  • Chat no longer reports "not connected" for genuinely connected tools.
  • Restart/relaunch preserves correct tool availability (unchanged — startup path unaffected).
  • Tests cover connection → sync → cache-visibility path.
  • Logs for connection updates, skill sync events, and runtime tool resolution at every decision point.

Summary by CodeRabbit

  • Improvements

    • Integrations cache now automatically expires based on time, ensuring fresher data retrieval
    • Backend connections are continuously synchronized with cached integrations for up-to-date accuracy
    • Enhanced cache coherence by intelligently removing only outdated entries
  • Tests

    • Expanded test coverage for cache synchronization and time-based expiration validation

Closes #749

…umansai#749)

On Windows the `ComposioConnectionCreated` → `wait_for_connection_active`
invalidation path often misses: the 60 s readiness poll can expire before
the user finishes the hosted OAuth flow (Defender SmartScreen, slower
browser launch, extra consent dialogs), so `invalidate_connected_integrations_cache`
never fires and the chat runtime stays frozen on the pre-connect snapshot
while Settings correctly shows "Connected" via its 5 s `listConnections`
poll.

Layer two cross-platform defenses on top of the existing event-bus path:

1. `composio_list_connections` (the RPC the UI polls every 5 s) now
   diffs the backend's active-toolkit set against every cached entry
   and invalidates on divergence, so chat converges to truth within one
   poll interval — regardless of how the OAuth round-trip completed.
2. Add a 60 s TTL on the `INTEGRATIONS_CACHE` so stale entries can't
   live forever even if nothing polls.
3. Grep-friendly `[composio][integrations]` logs at every decision
   point (cache hit / miss / expiry / divergence diff) for quick
   diagnosis from user-supplied debug dumps.

Also covers the regression with six new unit tests (activate, disconnect,
steady state, non-active rows, CONNECTED vs ACTIVE alias, TTL expiry)
serialized via a shared test mutex so they don't race the existing
mock-backend tests over the process-wide cache.
@CodeGhost21 CodeGhost21 requested a review from a team April 22, 2026 09:41
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 22, 2026

📝 Walkthrough

Walkthrough

The composio_list_connections function now synchronizes an integrations cache after fetching connections from the backend. The cache structure was updated to include a timestamp, with TTL-based freshness validation and selective invalidation logic that removes only divergent cache entries rather than clearing the entire cache.

Changes

Cohort / File(s) Summary
Cache Coherence & TTL Implementation
src/openhuman/composio/ops.rs
Updated composio_list_connections to sync cache with backend responses. Cache structure changed to CachedIntegrations { entries, cached_at } with CACHE_TTL freshness checks. Implemented selective cache invalidation that removes only divergent toolkit entries rather than clearing the entire map. Added logging for cache invalidation count and extended tests with process-wide mutex to prevent concurrent cache interference. New unit tests cover TTL staleness behavior and non-active status handling.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 A rabbit hops through the cache with glee,
TTL timestamps keeping things fresh as can be,
Selective removal, no brutal sweeps—
Just clearing what's stale from the data heaps,
Connected integrations, now synced and sound! 🥕✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title references Windows cache sync issues which align with the PR's core objective to fix a Windows-specific race condition affecting cache synchronization.
Linked Issues check ✅ Passed The PR implements cache synchronization via timestamped entries, TTL checks, and diff-based invalidation to ensure UI connection state matches chat runtime state, addressing issue #749's core requirement that connected integrations show availability in chat.
Out of Scope Changes check ✅ Passed All changes in ops.rs directly target the cache synchronization issue: timestamped cache structure, TTL freshness checks, sync_cache_with_connections diff logic, and related tests align with #749 objectives.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@graycyrus
Copy link
Copy Markdown
Contributor

Review: ✅ Looks good

PR #776 — fix(composio): keep chat integrations cache in sync on Windows
Linked issue: #749

Verification

  • Code matches PR description — The two defenses described (UI-poll-driven sync via sync_cache_with_connections and defensive TTL via CACHE_TTL) are both implemented in src/openhuman/composio/ops.rs exactly as described.
  • Code satisfies issue acceptance criteria:
    • "If integration is shown as connected, related skills are available in chat"sync_cache_with_connections is called from composio_list_connections (polled every 5s by UI), diffing live active toolkits against cached state and invalidating on divergence. TTL caps worst-case at 60s.
    • "Chat no longer reports 'not connected' for genuinely connected tools" — Cache invalidation on divergence forces fetch_connected_integrations to re-fetch from backend on next call.
    • "Windows restart/relaunch preserves correct tool availability" — Cache starts empty on boot (unchanged behavior, process-wide LazyLock), so startup path is unaffected.
    • "Tests cover connection → sync → chat invocation path" — 6 new regression tests cover: activate, disconnect, steady-state no-op, non-active rows ignored, ACTIVE≡CONNECTED alias, TTL expiry.
    • "Add logging for connection updates, skill sync events, runtime tool resolution" — grep-friendly [composio][integrations] logs at cache hit/miss/TTL expiry/divergence diff with added+removed toolkit sets.
  • No undocumented changes — Version bump in Cargo.lock (0.52.27 → 0.52.28) is expected. All code changes are in ops.rs as described.

What was checked

  • Root cause analysis in PR description aligns with the code: the wait_for_connection_active 60s timeout can be overrun on Windows, leaving INTEGRATIONS_CACHE stale.
  • sync_cache_with_connections does an O(n) set diff without holding the lock across the decision (read lock → collect divergent keys → release → write lock to remove). Clean lock discipline.
  • CachedIntegrations struct wraps entries + Instant timestamp cleanly. TTL check in fetch_connected_integrations falls through to backend fetch on expiry rather than serving stale data.
  • CACHE_TEST_GUARD mutex prevents test races over the process-wide map — good practice given cargo test parallelism.
  • Tests use unique cache keys and clear_cache_key (per-key removal) instead of invalidate_connected_integrations_cache (full wipe), preventing cross-test interference.
  • TTL expiry test ages the entry by rewriting cached_at rather than sleeping — deterministic.

Minor notes

  • The cache_entries_expire_after_ttl test proves the entry is stale but can't trigger the actual re-fetch (no mock backend in that test). This is fine — the read-path TTL check is straightforward and the mock-backend tests above cover the full fetch flow. Just noting for completeness.

Copy link
Copy Markdown
Contributor

@graycyrus graycyrus 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: fix(composio): keep chat integrations cache in sync on Windows

Thorough line-by-line review of the diff. Clean, well-motivated fix with good defensive layering. Approving with a few non-blocking suggestions in inline comments.

What works well

  • Root cause analysis is excellent. The PR description traces the exact failure mode (Windows OAuth exceeding the 60s wait_for_connection_active window) and explains why the Settings UI shows "Connected" while the chat cache is stale.
  • Two-layer defense is sound architecture. UI-poll-driven sync (converges within one 5s poll) and defensive TTL (caps CLI/background staleness at 60s) are complementary.
  • Lock discipline is correct. sync_cache_with_connections reads under a read lock, collects divergent keys, drops the lock, then takes a write lock to remove. No lock held across network calls.
  • Tests are deterministic. TTL test ages the entry by rewriting cached_at rather than sleeping. CACHE_TEST_GUARD prevents races over the process-wide map.
  • Debug logging is grep-friendly with consistent [composio][integrations] prefixes, structured fields, and no PII.
  • Test helper conn() using serde_json::from_value is a smart choice — decouples tests from struct field additions.

Summary of inline comments

  1. Suggestion — File size (~1560 lines, 3x guideline). Consider extracting cache layer into cache.rs as follow-up.
  2. Suggestioninvalidate_connected_integrations_cache log level bumped from debug to info — intentional for the happy-path bus subscriber call?
  3. Nitpicklive_active.clone() inside filter_map — minor allocation, harmless in practice.
  4. Nitpick — Static docstring on INTEGRATIONS_CACHE not updated to mention TTL.
  5. Nitpick — Subtle cross-test sync side-effect protected by CACHE_TEST_GUARD — worth a comment.

All non-blocking. LGTM.


use crate::openhuman::context::prompt::{ConnectedIntegration, ConnectedIntegrationTool};
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Suggestion — file size: With this PR, ops.rs grows to ~1560 lines (~3x the project's preferred ≤500-line guideline from CLAUDE.md). The file already held RPC handlers, cache logic, prompt integration discovery, and a substantial test module before this PR.

Not blocking for this fix — the new code logically belongs here. But as a follow-up, consider extracting the cache/sync layer (CachedIntegrations, INTEGRATIONS_CACHE, sync_cache_with_connections, fetch_connected_integrations, connected_toolkit_set, invalidate_*) into a sibling cache.rs file within the composio/ module. The test module for the cache tests could move with it. This would bring both files comfortably under ~800 lines and make the cache behavior easier to audit independently.

/// Clears the entire map because the bus subscriber doesn't carry a
/// config reference.
/// new OAuth connection completes, by [`composio_list_connections`]
/// when it observes a divergence between the backend response and the
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Nitpick: The docstring on the INTEGRATIONS_CACHE static (line 445-448 in the pre-image) still says "until explicitly invalidated" — should also mention the TTL and the UI-poll sync path for completeness. The old text didn't get updated in this PR.

(The extensive docstring on CACHE_TTL covers the full story, so this is just a consistency polish.)

/// cached snapshot, and from tests. Clears the entire map because the
/// callers don't carry a config reference.
pub fn invalidate_connected_integrations_cache() {
if let Ok(mut guard) = INTEGRATIONS_CACHE.write() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💬 Question — log level: The existing invalidate_connected_integrations_cache log was debug and is now info. This is called from the event-bus subscriber on every successful OAuth (the happy path on macOS/Linux). Was the bump to info intentional?

info is fine for the new sync_cache_with_connections divergence path (that's a notable state change worth surfacing at runtime). But for the bus subscriber's normal invalidation, debug might be more appropriate to avoid noise in production logs. Consider keeping the full-wipe path at debug and the divergence-detected path at info.

let Ok(guard) = INTEGRATIONS_CACHE.read() else {
return;
};
guard
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Nitpick — allocation: live_active.clone() is called inside the filter_map closure, so it clones the full HashSet once per divergent cache key. In practice the cache usually has one key (single user context), so this is harmless. But if you want to tighten it, you could move the live_active reference comparison outside the closure and collect divergent keys as just Vec<String>, then compute the diff sets only in the write-lock block where you log. Minor — the current approach is perfectly readable.

}

// ── Windows-observed sync regression coverage (issue #749) ────
//
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Nitpick — test isolation: The six new regression tests seed the cache with literal string keys ("windows-regression-1" through "windows-regression-6") and use clear_cache_key for surgical cleanup — good. But sync_cache_with_connections compares live_active against every entry in the cache map, not just the test's key. If a parallel mock-backend test (e.g. fetch_connected_integrations_via_mock_aggregates_tools) happens to have populated a cache entry with a different connected-toolkit set, the sync function could invalidate that entry too.

The CACHE_TEST_GUARD mutex prevents this from being a real problem today, but the invariant is subtle. A brief comment in the test module docstring noting that the guard also protects against cross-test sync side effects would help future maintainers.

@graycyrus graycyrus merged commit b3b451f into tinyhumansai:main Apr 22, 2026
7 of 8 checks passed
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.

Fix Windows tool sync mismatch: connected in UI but unavailable in chat skills

2 participants