Skip to content

fix(integrations): distinguish mid-OAuth / expired / failed states in spawn gate (#2365)#2373

Merged
senamakel merged 6 commits into
tinyhumansai:mainfrom
CodeGhost21:fix/2365-integration-gate-states
May 23, 2026
Merged

fix(integrations): distinguish mid-OAuth / expired / failed states in spawn gate (#2365)#2373
senamakel merged 6 commits into
tinyhumansai:mainfrom
CodeGhost21:fix/2365-integration-gate-states

Conversation

@CodeGhost21
Copy link
Copy Markdown
Contributor

@CodeGhost21 CodeGhost21 commented May 20, 2026

Summary

  • Distinguish mid-OAuth (INITIATED / INITIALIZING / PENDING), expired (EXPIRED), failed (FAILED / ERROR), and truly-never-connected states in the integrations_agent spawn-gate so the agent's reply to "send me an email" matches the actual Composio connection status.
  • Carry the upstream connection status forward in ConnectedIntegration.non_active_status, populated from raw list_connections rows in fetch_connected_integrations_uncached.
  • New describe_unconnected_state(toolkit, status) helper emits one of 5 distinct messages instead of the legacy single "available but not authorized yet" copy that conflated all causes.

Problem

Repro: Settings → Connections shows Gmail as connected. User asks the agent to send an email. Orchestrator delegates to integrations_agent(toolkit="gmail", …). spawn_subagent's gate refreshes from fetch_connected_integrations_status and finds gmail in the allowlist with connected: false (because is_active() accepts only ACTIVE / CONNECTED). Gate returns the legacy generic "not authorized yet" copy. Agent paraphrases as "Gmail isn't connected" — which contradicts what Settings shows.

The actual cause could be any of: OAuth still pending (INITIATED), token expired (EXPIRED), handshake failed (FAILED) — each with a different remediation. The legacy message conflated all of them.

Solution

ConnectedIntegration carries the non-active status

  • New field non_active_status: Option<String> on prompts::types::ConnectedIntegration.
  • fetch_connected_integrations_uncached (src/openhuman/composio/ops.rs) builds a per-slug map from the raw connections vec, filtered to non-active rows that don't have a competing ACTIVE row for the same slug. Status prioritised EXPIRED > FAILED/ERROR > INITIATED/INITIALIZING/PENDING > other so the most-actionable label wins.
  • All 14 ConnectedIntegration { … } literal sites updated to carry the new field (test fixtures use None).

Gate emits a specific message

spawn_subagent's integrations_agent branch routes through describe_unconnected_state(toolkit, status):

Status User-facing intent
INITIATED / INITIALIZING / PENDING OAuth flow in progress; finish the browser handshake
EXPIRED Token expired; reconnect at Settings → Connections → '<toolkit>'
FAILED / ERROR Previous OAuth attempt failed; reconnect
any other non-empty Status quoted verbatim so triage can act on it without a code change
None (no row at all) Legacy "never authorized" copy preserved

Submission Checklist

If a section does not apply to this change, mark the item as N/A with a one-line reason.

  • Tests added or updated (happy path + at least one failure / edge case) — 7 new tests + 6 pre-existing; cargo test --lib describe_unconnected_state non_active_status integrations_agent → 13 passed.
  • Diff coverage ≥ 80% — every new branch in describe_unconnected_state has at least one focused test; the new non_active_status_by_slug build path is exercised through the existing composio::ops integration tests.
  • Coverage matrix updated — N/A: behaviour-only change to existing integrations_agent gate; no new feature row added/removed/renamed.
  • All affected feature IDs from the matrix are listed in ## Related — N/A: no matrix row touched.
  • No new external network dependencies introduced — pure-Rust prompts::types extension and status mapping.
  • Manual smoke checklist updated if this touches release-cut surfaces — N/A: gate-message wording only; happy path (active connection → action succeeds) unchanged.
  • Linked issue closed via Closes #NNN in the ## Related section — see below.

Impact

  • Runtime/platform: backend integrations_agent spawn-gate.
  • User-visible: Discord/Gmail/etc. cards whose OAuth is mid-flight, expired, or failed now produce specific guidance instead of a generic "not authorized yet" message that conflicts with Settings UI.
  • Performance / security: zero runtime cost — non_active_status is computed once per fetch_connected_integrations_uncached cycle (cached). No new IO; no new error surfaces.
  • Migration / compatibility: ConnectedIntegration gained one optional field; all in-repo constructors updated.

Tests

cargo test --lib describe_unconnected_state non_active_status integrations_agent13 passed, 0 failed (7 new). cargo test --lib composio::ops57 passed (sanity-check the new build path).

New test Covers
describe_unconnected_state_initiated_says_oauth_in_progress INITIATED routes to in-progress copy and explicitly does NOT borrow the legacy "has not authorized it yet" wording
describe_unconnected_state_pending_and_initializing_are_aliased PENDING / INITIALIZING share the in-progress branch with INITIATED
describe_unconnected_state_expired_says_reconnect EXPIRED → "OAuth token has expired" + reconnect
describe_unconnected_state_failed_and_error_route_to_reconnect FAILED / ERROR → "FAILED state" + reconnect
describe_unconnected_state_quotes_unknown_status_verbatim Unknown wire status (e.g. DEAUTH_REQUIRED) is quoted so triage can read it from logs
describe_unconnected_state_none_is_truly_disconnected None preserves the legacy never-connected copy
describe_unconnected_state_status_match_is_case_insensitive initiated / Expired route the same way as the canonical uppercase forms

CI flakes

The "Rust Tauri Coverage" job is currently red on core_process::tests::ensure_running_* — those tests live in app/src-tauri/src/core_process_tests.rs, which this PR does NOT modify. The failure pattern ("core process did not become ready" cascading into "env lock poisoned") is a known port-binding flake in CI. A no-op re-run of just that job should pass; the PR's own changed-line tests are all green.

Related


AI Authored PR Metadata (required for Codex/Linear PRs)

Linear Issue

Commit & Branch

  • Branch: fix/2365-integration-gate-states (branched from origin/main after fresh fetch)
  • Commit SHA: see PR head

Validation Run

  • pnpm --filter openhuman-app format:check — N/A: no frontend changes.
  • pnpm typecheck — N/A: no TypeScript changes.
  • Focused tests: cargo test --lib describe_unconnected_state non_active_status integrations_agent → 13 passed, 0 failed.
  • Rust fmt/check (if changed): cargo fmt --manifest-path Cargo.toml applied; cargo check clean.
  • Tauri fmt/check (if changed): N/A — no app/src-tauri/ changes. The Tauri Coverage CI red is on an unrelated core_process port-binding flake.

Validation Blocked

  • command: N/A
  • error: N/A
  • impact: N/A

Behavior Changes

  • Intended behavior change: integrations_agent gate emits one of 5 distinct messages keyed on upstream connection status, instead of the legacy single "not authorized yet" message.
  • User-visible effect: a Discord/Gmail/etc. card whose OAuth is mid-flight, expired, or failed now produces specific guidance instead of a confusing "not connected" message that contradicts Settings.

Parity Contract

  • Legacy behavior preserved: ACTIVE-connection path is unchanged. None (no row) keeps the legacy "never authorized" copy. The Settings → Connections → '<toolkit>' remediation literal is preserved verbatim so existing UI-navigation tests pass.
  • Guard/fallback/dispatch parity checks: status case-insensitivity (initiated, Expired, etc.) locked in by _status_match_is_case_insensitive.

Duplicate / Superseded PR Handling

  • Duplicate PR(s): none for this slice.
  • Canonical PR: this PR.
  • Resolution: N/A.

Summary by CodeRabbit

  • New Features

    • Track and surface a toolkit's non-active connection status to improve delegation messaging and UX.
    • Provide tailored explanations when a toolkit is allowlisted but not connected.
  • Bug Fixes

    • Integration messaging now distinguishes expired, failed/error, pending, and other disconnected states.
    • Spawn-time refreshed integration records preserve prior non-active status where applicable.
  • Tests

    • Updated fixtures and added tests across integration and orchestration flows to validate status tracking and messaging.

Review Change Stack

… spawn gate (tinyhumansai#2365)

The `integrations_agent` spawn-gate used to emit the same
"available but the user has not authorized it yet" copy regardless
of WHY a Composio connection wasn't usable — whether the OAuth was
still in flight (`INITIATED`), the token had rolled over (`EXPIRED`),
or the handshake had failed outright (`FAILED`). Users who saw
Gmail showing as connected in Settings would then see the agent
say "Gmail isn't connected", concluded the app was broken, and
opened the issue.

Settings UI reflects the FE's optimistic post-OAuth view; the
spawn-gate reads the backend's authoritative `list_connections`
status. When those diverge — most commonly because OAuth never
reached `ACTIVE` — the user-facing message has to be precise enough
that the user can act on the actual situation, not retry the same
flow.

Wire path
- `ConnectedIntegration` gains a `non_active_status: Option<String>`
  field carrying the most-informative upstream status for toolkits
  in the backend allowlist that lack an ACTIVE connection row.
  `EXPIRED > FAILED/ERROR > INITIATED/INITIALIZING/PENDING > other`.
- `fetch_connected_integrations_uncached` builds a per-slug map
  from the raw `connections` vec (filtered to non-active rows that
  don't have a competing ACTIVE row for the same slug) and feeds
  the prioritised status into every emitted `ConnectedIntegration`.
- `spawn_subagent`'s integrations_agent gate routes through a new
  `describe_unconnected_state(toolkit, status)` helper instead of
  the inline literal. Five distinct messages:
    INITIATED/INITIALIZING/PENDING → "finish the browser OAuth flow"
    EXPIRED                        → "reconnect — token expired"
    FAILED/ERROR                   → "reconnect — previous attempt failed"
    other non-empty status         → quoted verbatim for triage
    None (no row at all)           → legacy "never authorized" copy
- All 14 `ConnectedIntegration { ... }` literal sites updated to
  carry the new field. Test fixtures all use `None`.

Tests (in `spawn_subagent::tests`, 7 new, 13 passing in the targeted
filter; 57 passing in `composio::ops`)
- INITIATED / PENDING / INITIALIZING all route to the OAuth-in-progress
  branch and explicitly do NOT borrow the legacy "has not authorized
  it yet" wording — that was the actual user-perception bug from
  tinyhumansai#2365 (Settings showed Gmail connected, agent said "not authorized").
- EXPIRED → reconnect copy with `OAuth token has expired`.
- FAILED / ERROR → reconnect copy with `FAILED state`.
- Unknown status (e.g. `DEAUTH_REQUIRED`) is quoted verbatim so
  triage can act on it without needing a code change.
- None → preserves the legacy never-connected copy.
- Case-insensitive matching (`initiated`, `Expired`) routes the
  same way as the canonical uppercase form, since Composio's wire
  shape isn't case-stable.

Acceptance criteria touched
- [x] No false disconnected response: connections in the
      mid-OAuth / expired / failed states are now described with
      their actual state.
- [x] Scope issue surfaced: scope-mismatch errors continue to flow
      through the existing `composio::error_mapping`
      `InsufficientScope` path; this PR doesn't regress that.
- [x] Connection state consistent: the gate now reads the same
      backend status the FE Settings UI reads.
- [x] Regression safety: 7 new tests + 6 pre-existing
      integrations_agent gate tests still pass.

Out of scope (separate PRs / not needed)
- No FE change: the spawn-gate output bubbles up to the
  orchestrator, which paraphrases for the user.
- No `is_active` change: pre-existing semantics (only ACTIVE /
  CONNECTED count as usable) are preserved; the new field only
  describes the *non-active* case.
- No new RPC: the new field rides along the existing
  `ConnectedIntegration` payload used by the agent harness.
@CodeGhost21 CodeGhost21 requested a review from a team May 20, 2026 20:45
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 20, 2026

Caution

Review failed

Failed to post review comments

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds ConnectedIntegration.non_active_status, computes and attaches it in composio integration overviews, preserves it during spawn refreshes, and uses it to produce status-specific user messages; test fixtures and unit tests updated accordingly.

Changes

Integration Status Tracking and User Messaging

Layer / File(s) Summary
ConnectedIntegration schema extension
src/openhuman/agent/prompts/types.rs
Added non_active_status: Option<String> with documentation clarifying interpretation for non-ACTIVE OAuth states.
Backend non-active status computation
src/openhuman/composio/ops.rs, src/openhuman/composio/ops_test.rs
Compute non_active_status_by_slug from backend connections (priority: EXPIRED > FAILED/ERROR > INITIATED/INITIALIZING/PENDING > raw) and set ConnectedIntegration.non_active_status when building overview entries; test helper updated.
Integration toolkit spawn preserves status
src/openhuman/agent/harness/subagent_runner/ops.rs
When constructing refreshed ConnectedIntegration for an integrations_agent spawn, clone non_active_status from the cached integration to maintain consistency.
Status-specific user messages
src/openhuman/tools/impl/agent/spawn_subagent.rs
Added describe_unconnected_state(toolkit, status) to produce tailored explanations for pending/init, EXPIRED, FAILED/ERROR, unknown statuses (quoted), and legacy None. Includes unit tests covering branches, aliasing, case-insensitivity, and trimming/quoting behavior.
Test fixture updates for new field
src/openhuman/agent/agents/integrations_agent/prompt.rs, src/openhuman/agent/agents/orchestrator/prompt.rs, src/openhuman/agent/agents/welcome/prompt.rs, src/openhuman/agent/harness/test_support_test.rs, src/openhuman/tools/orchestrator_tools.rs, src/openhuman/composio/ops_test.rs, src/openhuman/agent/harness/session/tests.rs
Updated ConnectedIntegration struct initialization across prompt builders, harness tests, and tools tests to include non_active_status: None, ensuring fixtures compile without altering assertions.

Sequence Diagram

sequenceDiagram
  participant Composio
  participant IntegrationOverviewBuilder
  participant CachedIntegrationStore
  participant SpawnSubagent
  participant UserClient

  Composio->>IntegrationOverviewBuilder: provide backend connections list
  IntegrationOverviewBuilder->>IntegrationOverviewBuilder: compute non_active_status_by_slug (priority)
  IntegrationOverviewBuilder->>CachedIntegrationStore: build ConnectedIntegration entries with non_active_status
  UserClient->>SpawnSubagent: request integrations_agent spawn for toolkit
  SpawnSubagent->>CachedIntegrationStore: read cached ConnectedIntegration
  SpawnSubagent->>SpawnSubagent: copy non_active_status into refreshed record
  SpawnSubagent->>SpawnSubagent: call describe_unconnected_state(toolkit, non_active_status)
  SpawnSubagent->>UserClient: return ToolResult message explaining unconnected state
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Suggested reviewers

  • graycyrus
  • senamakel
  • M3gA-Mind

Poem

🐰 I sniffed the statuses, one by one,
EXPIRED, PENDING — the sorting's done.
Now whispers tell why tools won't run,
No more guessing — straight and clear,
Hop, hop, hooray — the answer's here!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main change: distinguishing non-ACTIVE connection states in the spawn-gate logic for integrations_agent.
Linked Issues check ✅ Passed The PR successfully addresses all coding requirements from issue #2365: adds non_active_status field, implements per-slug priority mapping, updates 14 construction sites, and routes unconnected cases through describe_unconnected_state with proper message handling.
Out of Scope Changes check ✅ Passed All changes are directly related to issue #2365 objectives: ConnectedIntegration struct extension, composio::ops mapping logic, spawn_subagent messaging routing, and test fixture updates remain within scope.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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


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

@coderabbitai coderabbitai Bot added agent Built-in agents, prompts, orchestration, and agent runtime in src/openhuman/agent/. bug labels May 20, 2026
Copy link
Copy Markdown
Contributor

@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: 1

🧹 Nitpick comments (2)
src/openhuman/tools/impl/agent/spawn_subagent.rs (1)

355-369: ⚡ Quick win

Add a debug log for non-active status classification before early return.

This branch is a state-transition gate with user-visible behavior changes, but it currently returns without emitting status context. A single structured tracing::debug! here would make regressions much easier to triage.

As per coding guidelines: "Use log / tracing at debug or trace level on ... state transitions, and any branch that is hard to infer from tests alone."

🤖 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/openhuman/tools/impl/agent/spawn_subagent.rs` around lines 355 - 369,
Before the early return that builds and returns the user-visible message, add a
tracing::debug! call to record the non-active status classification and relevant
context (e.g., include ci.non_active_status.as_deref(), ci.toolkit
identifier/state if available) so state transitions are observable; place this
debug log immediately before the call to describe_unconnected_state(...) / the
return of ToolResult::success(message) in the same block where
describe_unconnected_state and ci.non_active_status are referenced.
src/openhuman/composio/ops.rs (1)

1671-1714: ⚡ Quick win

Add trace/debug context for chosen non-active status per toolkit.

This new priority-selection path is a state-interpretation branch, but logs currently don’t expose the selected non_active_status. Including it in the integration overview/debug path would make spawn-gate regressions much faster to triage.

Proposed patch
@@
     for ci in &integrations {
         tracing::debug!(
             toolkit = %ci.toolkit,
             connected = ci.connected,
+            non_active_status = ?ci.non_active_status,
             tool_count = ci.tools.len(),
             "[composio] integration overview"
         );
     }

As per coding guidelines: "Use log / tracing at debug or trace level on ... state transitions, and any branch that is hard to infer from tests alone."

Also applies to: 1812-1816

🤖 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/openhuman/composio/ops.rs` around lines 1671 - 1714, The
non_active_status_by_slug construction doesn't emit any debug/trace info about
which non-active status was chosen per toolkit slug; add a debug-level log
(using tracing::debug! or log::debug!) at the points where you insert or update
the map entry (inside the map.entry(...).and_modify(...) / .or_insert_with(...)
flow) to record the slug, the candidate status (conn.status) and the chosen
status after comparison, and also emit a final trace/debug of the resulting
non_active_status_by_slug map after collection so the integration overview can
show the selected non-active status per toolkit; reference variables/functions:
non_active_status_by_slug, connections, connected_slugs,
conn.normalized_toolkit(), conn.status, and the local priority(...) helper.
🤖 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/openhuman/tools/impl/agent/spawn_subagent.rs`:
- Around line 663-689: The function describe_unconnected_state uppercases the
incoming status for matching then echoes that uppercased value back in the
Some(other) branch, breaking the requirement to preserve the original verbatim
status; fix by first capturing the trimmed original status (e.g., let orig =
status.map(|s| s.trim().to_string())), compute an uppercase version for matching
(e.g., let up = orig.as_deref().map(|s| s.to_ascii_uppercase())), then perform
the match on up.as_deref() but when formatting the unknown-status message use
the original trimmed string (orig.as_deref().unwrap_or("<empty>")) instead of
the uppercased value so describe_unconnected_state returns the upstream status
verbatim in the Some(other) arm.

---

Nitpick comments:
In `@src/openhuman/composio/ops.rs`:
- Around line 1671-1714: The non_active_status_by_slug construction doesn't emit
any debug/trace info about which non-active status was chosen per toolkit slug;
add a debug-level log (using tracing::debug! or log::debug!) at the points where
you insert or update the map entry (inside the map.entry(...).and_modify(...) /
.or_insert_with(...) flow) to record the slug, the candidate status
(conn.status) and the chosen status after comparison, and also emit a final
trace/debug of the resulting non_active_status_by_slug map after collection so
the integration overview can show the selected non-active status per toolkit;
reference variables/functions: non_active_status_by_slug, connections,
connected_slugs, conn.normalized_toolkit(), conn.status, and the local
priority(...) helper.

In `@src/openhuman/tools/impl/agent/spawn_subagent.rs`:
- Around line 355-369: Before the early return that builds and returns the
user-visible message, add a tracing::debug! call to record the non-active status
classification and relevant context (e.g., include
ci.non_active_status.as_deref(), ci.toolkit identifier/state if available) so
state transitions are observable; place this debug log immediately before the
call to describe_unconnected_state(...) / the return of
ToolResult::success(message) in the same block where describe_unconnected_state
and ci.non_active_status are referenced.
🪄 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

Run ID: a907bcd8-ec55-4b3f-9bb0-1c8c4e627498

📥 Commits

Reviewing files that changed from the base of the PR and between fa8d75f and d6bf21f.

📒 Files selected for processing (10)
  • src/openhuman/agent/agents/integrations_agent/prompt.rs
  • src/openhuman/agent/agents/orchestrator/prompt.rs
  • src/openhuman/agent/agents/welcome/prompt.rs
  • src/openhuman/agent/harness/subagent_runner/ops.rs
  • src/openhuman/agent/harness/test_support_test.rs
  • src/openhuman/agent/prompts/types.rs
  • src/openhuman/composio/ops.rs
  • src/openhuman/composio/ops_test.rs
  • src/openhuman/tools/impl/agent/spawn_subagent.rs
  • src/openhuman/tools/orchestrator_tools.rs

Comment thread src/openhuman/tools/impl/agent/spawn_subagent.rs Outdated
… verbatim status in unknown-status branch

CodeRabbit major finding on src/openhuman/tools/impl/agent/spawn_subagent.rs.
The previous `describe_unconnected_state` uppercased the status
once and then used the uppercased value in BOTH the match arms AND
the unknown-status format string, so a mixed-case wire value like
`DeauthRequired` was echoed back as `DEAUTH_REQUIRED` — breaking
the "quote unknown statuses verbatim" contract.

Fix
- Capture the trimmed original separately:
    let trimmed = status.map(str::trim).filter(|s| !s.is_empty());
    let upper   = trimmed.map(|s| s.to_ascii_uppercase());
- Match on `upper.as_deref()` for the known branches.
- In the unknown-status branch, format with `trimmed.unwrap_or("")`
  so the upstream casing is preserved verbatim.
- `filter(|s| !s.is_empty())` collapses whitespace-only inputs to
  the truly-disconnected `_` branch (instead of formatting "" into
  the unknown-status template).

Tests
- Expanded `describe_unconnected_state_quotes_unknown_status_verbatim`
  to pin three shapes (uppercase / snake_case / PascalCase) so a
  silent drift back to echoing the uppercased value fails CI.
- New `describe_unconnected_state_quotes_unknown_status_after_trimming_whitespace`
  pins:
    * blank/whitespace input collapses to the legacy `None` branch
    * padded status is trimmed but its casing is preserved
- All other tests unchanged and still pass.

`cargo test --lib describe_unconnected_state` → 8 passed, 0 failed
(7 pre-existing + 1 new).
coderabbitai[bot]
coderabbitai Bot previously approved these changes May 20, 2026
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.

Review — graycyrus

Solid PR. The spawn-gate now correctly distinguishes mid-OAuth / expired / failed / truly-disconnected states instead of conflating them into a single misleading message. The priority map in fetch_connected_integrations_uncached is well-designed (EXPIRED > FAILED/ERROR > INITIATED/PENDING > other), and the CodeRabbit verbatim-quoting fix in 6e74932 looks correct.

CI: all green. Coverage gate: passing. Tests: 7 new, 13 total — good coverage of all branches including case-insensitivity and whitespace trimming.

Issue #2365 alignment

The core repro (Settings shows Gmail connected → agent says "not authorized") is resolved for the cases this PR targets (INITIATED, EXPIRED, FAILED, ERROR). One acceptance criterion — "If Gmail is connected but lacks send scopes, the UI/agent clearly says scopes are missing" — isn't addressed here, but that would be a different code path (action-execution, not spawn-gate). Fine to track separately.

Change summary

File What changed
prompts/types.rs New non_active_status: Option<String> on ConnectedIntegration
composio/ops.rs Builds non_active_status_by_slug map with priority logic
spawn_subagent.rs New describe_unconnected_state() replaces legacy single message
7 other files Test fixtures + field propagation

1 minor finding below — no blockers.

to reconnect '{toolkit}' at Settings → Connections → '{toolkit}' before \
retrying the original request."
),
Some(_) => {
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.

[minor] When status is ERROR, this message says "a previous OAuth attempt in a FAILED state" — which misquotes the upstream status. For the known FAILED/ERROR pair this is cosmetic, but it could confuse triage if they're cross-referencing backend logs that show ERROR.

Consider interpolating the actual matched category or using a neutral phrasing like "in a failed/error state":

Some("FAILED") | Some("ERROR") => format!(
    "Integration '{toolkit}' has a previous OAuth attempt that did not succeed. \
     Do NOT retry this spawn. ..."
)

Non-blocking — the user-facing model paraphrases this anyway.

@graycyrus
Copy link
Copy Markdown
Contributor

Review note — overlap with merged #2414

Issue #2365 now has two PRs linked. #2414 (by @aqilaziz) was already merged and closed the issue — it fixed the specific case where Gmail is connected but lacks send scopes, surfacing "missing permissions" instead of the generic "not connected" message.

This PR (#2373) is still valuable — it covers a different slice of the same problem that #2414 doesn't handle:

Scenario #2414 (merged) #2373 (this PR)
Connected + missing scopes ✅ Fixed
OAuth mid-flight (INITIATED/PENDING) ❌ Still generic "not authorized" ✅ "OAuth flow in progress"
Token expired (EXPIRED) ❌ Still generic "not authorized" ✅ "Token expired — reconnect"
Auth failed (FAILED/ERROR) ❌ Still generic "not authorized" ✅ "Previous attempt failed — reconnect"
Unknown status ❌ Still generic "not authorized" ✅ Quotes status verbatim

Without this PR, users whose Gmail shows as INITIATED/EXPIRED/FAILED in Settings will still get the confusing "not authorized yet" message from the agent.

Since #2414 already merged and touched some of the same files (spawn_subagent, prompts, composio ops), this branch likely needs a rebase. Pushing a conflict resolution now.

@graycyrus
Copy link
Copy Markdown
Contributor

@CodeGhost21 — heads up, 3 CI checks are failing after the main merge, but none are caused by your changes:

  1. Frontend Unit Tests — i18n coverage test fails on missing settings.mcpServer.* locale keys that came in from main. Your PR is Rust-only.
  2. i18n Coverage — Same root cause (322 unused English keys + missing MCP server translations from another merged PR).
  3. Windows Rust Tests — Empty logs, likely the known core_process port-binding flake you already noted in the PR description.

All Rust checks pass (fmt, clippy, core tests, core coverage, Tauri shell tests, Tauri coverage) — your actual changes are green.

That said, can you take a look and confirm these are indeed pre-existing on main? A quick check would be to see if the same Frontend Unit Tests / i18n Coverage jobs are also failing on other recent PRs or on main itself. If they are, this PR is clear to review as-is.

…LED/ERROR + state-transition tracing

graycyrus review: the FAILED/ERROR branch hard-coded "in a FAILED state",
which misquoted upstream `ERROR` rows and wasted triage time when cross-
referencing backend logs. Now quotes the actual trimmed upstream label
(`failed`, `Error`, etc.) in backticks, matching the unknown-status
branch's verbatim-quoting contract. Test updated; new test pins
mixed-case round-trip.

CodeRabbit nitpicks: two state-transition gates were silent. Added
tracing::debug! calls so regressions are easy to trace end-to-end
(matches CLAUDE.md "Debug logging" rule):

- `spawn_subagent.rs` integrations_agent gate: log toolkit + non_active_status
  immediately before emitting the status-specific message.
- `composio/ops.rs` `non_active_status_by_slug` build: log each
  insert / upgrade / kept candidate, and the final map size. Also
  include `non_active_status` in the existing per-toolkit integration
  overview debug line.

Tests: cargo test --lib describe_unconnected_state → 9 passed (8 prior + 1 new).
       cargo test --lib composio::ops → 57 passed.
       cargo test --lib integrations_agent → 7 passed.
coderabbitai[bot]
coderabbitai Bot previously approved these changes May 22, 2026
…ession test

After merging upstream main (b2ae12a — perf(agent): prewarm session
integrations) into this branch, the new test
`set_connected_integrations_marks_session_initialized_and_updates_hash`
constructs a `ConnectedIntegration` literal without the
`non_active_status: Option<String>` field that this PR added to the
struct. Auto-merge with main therefore failed CI compile with E0063
at session/tests.rs:254 even though both sides compiled in isolation —
a classic silent semantic conflict.

Add `non_active_status: None` to the merged-in literal so the merged
tree compiles. No behavior change: the test asserts initialization /
hash bookkeeping and does not touch the new field.

Tests:
- cargo test --lib describe_unconnected_state -> 9 passed (PR coverage)
- cargo test --lib set_connected_integrations_marks_session -> 1 passed
  (the merged-in regression test)
- cargo check --tests -p openhuman -> clean
@senamakel senamakel merged commit 49528de into tinyhumansai:main May 23, 2026
29 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent Built-in agents, prompts, orchestration, and agent runtime in src/openhuman/agent/. bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Agent says Gmail is disconnected when sending email

3 participants