Thanks to visit codestin.com
Credit goes to github.com

Skip to content

fix(email): wire EmailSendRequest.provider fallback + exact pre-scan budget (#1603 review)#1616

Merged
itomek merged 5 commits into
mainfrom
feat/1603-review-followups
Jun 12, 2026
Merged

fix(email): wire EmailSendRequest.provider fallback + exact pre-scan budget (#1603 review)#1616
itomek merged 5 commits into
mainfrom
feat/1603-review-followups

Conversation

@itomek

@itomek itomek commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator

Follow-up to #1614 (merged) — applies the claude.yml review suggestions plus the root-cause enumeration fix that #1614 only worked around.

Why this matters

#1614 shipped connector-derived mailbox selection, but two gaps remained:

  1. store.list_connections() was hardcoded to ("google",) — so every generic connection consumer stayed Microsoft-blind: api.list_connections(), api.get_connection("microsoft") (returned None even when Microsoft was connected), the /v1/connections REST endpoint, the UI connectors list, and tripwire_check() (never swept the Microsoft connection at startup). feat(email): connector-derived mailbox selection — multi-inbox triage + send (#1603, #1594) #1614's connected_mailbox_providers() patched only the email agent's path. This makes the primitive registry-driven (enumerate the registered oauth_pkce providers via a function-local import — no layering regression), so the whole system is consistent.
  2. EmailSendRequest.provider was a public REST field that did nothing — with two mailboxes connected and an unbound confirmation token, a caller supplying provider to disambiguate still got a 400. Now wired as the unbound-token fallback (the token's own binding still wins when present).

Plus a latent-correctness fix to the pre-scan budget guard (counts actual returned messages, not the per-backend cap).

Test plan

  • GAIA_MEMORY_DISABLED=1 python -m pytest tests/unit/connectors/ tests/unit/agents/email/ -x — green; util/lint.py --all clean

  • New tests: store.list_connections() finds microsoft-only / both; api.list_connections() + get_connection("microsoft") see Microsoft (no secret leak); TestSendRequestProviderFallback (unbound→routes there, bound-token wins, unbound+none+both→400); pre-scan budget counts actual returned

  • Real-world (Strix Halo, same persisted real Gmail + Outlook connections), before/after:

    Surface BEFORE (f768c2e, merged feat(email): connector-derived mailbox selection — multi-inbox triage + send (#1603, #1594) #1614) AFTER (e694f00, this PR)
    store.list_connections() ['google'] ['google','microsoft']
    api.list_connections() ['google'] ['google','microsoft']
    api.get_connection('microsoft') None full metadata (no secrets)
    GET /v1/connections ['google'] ['google','microsoft']

    Microsoft was connected the whole time — only the enumeration primitive changed.

Deferred from the review (own follow-up): surfacing the chosen from: mailbox in the send_now confirmation prompt — needs a base-agent confirmation hook touching all agents.

itomek added 2 commits June 11, 2026 19:22
…ack (#1603)

The send handler resolved the backend purely from the token's bound provider,
so the declared request.provider field was dead in every path. It now acts as
the fallback when the token carries no binding — the token's binding still
wins, and an unbound token with no provider and two mailboxes connected is
still rejected 400 ambiguous.
…#1603)

_pre_scan_all_backends advanced its budget counter by the per-backend cap
rather than the messages a backend actually returned, so the budget guard
could trip a hair early when an inbox underfilled its cap. Count the real
section totals plus informational instead; behavior is otherwise identical.
@github-actions github-actions Bot added the tests Test changes label Jun 11, 2026
@itomek itomek self-assigned this Jun 11, 2026
…crosoft (#1603)

store.list_connections() hardcoded the provider set to ('google',), so every
generic consumer was Microsoft-blind: api.list_connections() and
get_connection('microsoft') returned nothing even with Microsoft connected,
the /v1/connections endpoint and UI connectors list omitted it, and
tripwire_check() never swept the Microsoft connection at startup. Enumerate the
registered oauth_pkce providers instead (function-local registry import, same
pattern as connected_mailbox_providers), so a newly added provider surfaces
without editing this function.
@itomek itomek marked this pull request as ready for review June 11, 2026 23:42
@itomek itomek requested a review from kovtcharov-amd as a code owner June 11, 2026 23:42
@github-actions

Copy link
Copy Markdown
Contributor

Code Review — #1616 fix(email): wire EmailSendRequest.provider fallback + exact pre-scan budget

Summary

Solid, tightly-scoped follow-up — Approve with suggestions. All three production changes are correct and the root-cause store.list_connections() fix is the right call: making enumeration registry-driven (filter spec.type == "oauth_pkce") is consistent with the existing pattern at api.py:569 and fixes every generic consumer at once rather than patching one caller. Fail-loud behavior is preserved throughout — an invalid request.provider still 400s/503s, no silent fallback. The one thing worth your attention before merge: the new pre-scan budget regression test passes identically with and without the fix, so it doesn't actually guard the regression it names.

Issues

🟡 Budget-guard regression test doesn't exercise the fix (tests/unit/agents/email/test_multi_inbox_triage.py:244)

test_pre_scan_budget_counts_actual_returned_not_per_backend_cap uses exactly two backends and asks for 4 (per_backend = 4 // 2 = 2). The early-exit if scanned >= max_messages: break sits at the top of the loop, so it can only skip a backend that comes after the one that crossed the budget. With two backends, the only skippable one is the second/last — and to skip it the old code would need scanned >= 4 after google. The old scanned += per_backend adds just 2 there (2 < 4), so microsoft runs regardless. Result: the assertion mailboxes == {"google","microsoft"} is green on both the pre-fix (+= per_backend) and post-fix (+= actual) code — it doesn't fail-first.

The overcount can only skip a backend with ≥3 backends (or any setup where the cumulative over-count crosses max_messages before a later backend is reached). Suggest a three-backend scenario where the first two are under-filled, e.g. backends a=["a1"], b=["b1"], c=["c1","c2","c3"] with pre_scan_inbox(6) (per_backend = 2): under the old cap, a+b push scanned to 4 and a third would be at the cutoff sooner — pick counts so the old count reaches max_messages before c but the actual count does not, then assert c's mailbox is present. The production fix itself is correct; only the test needs strengthening so a future regression here is caught.

🟢 WHY comment runs a touch long (hub/agents/python/email/gaia_agent_email/agent.py:521) — the four-line block explains the rationale well, but CLAUDE.md leans toward one-line WHY comments. Optional trim; the content is accurate so not worth blocking on.

Strengths

  • Root-cause over workaround. Driving enumeration off REGISTRY.all() rather than extending a hardcoded tuple means the next oauth_pkce provider is picked up with zero edits here, and the function-local import (mirroring api._require_mcp_server_for_activation) keeps the module free of a registry-level dependency. The test_list_connections_finds_both_providers assertion that mcp-git is excluded nicely pins the oauth_pkce-only contract.
  • Provider-precedence tests are thorough. The three TestSendRequestProviderFallback cases cover the real matrix — unbound→request.provider routes, bound-token wins over request.provider, and unbound+none+both-connected still 400s — and the spy asserts no send leaks through the ambiguity guard (calls == []).
  • Secret-leak assertion included. test_microsoft_connection_visible_to_generic_api checks refresh_token not in conn, so the Microsoft-visibility fix can't silently start leaking token material through get_connection.
  • Updated EmailSendRequest.provider description and the send_email precedence comment now match the wired behavior exactly — docstring and code agree.

Verdict

Approve with suggestions. No correctness or security blockers — the production fixes are sound and well-tested where it counts. Strengthening the one budget-guard test to a ≥3-backend scenario (so it fails on the pre-fix code) is the only thing I'd ask for, and it's a test-only change.

…1603)

The budget regression test passed on both the buggy and fixed counters because
with max_messages >= n_backends the per-backend over-count can never trip the
early-exit guard. Rewrite it to the only scenario that diverges — max_messages
< n_backends: an empty google inbox scanned first, ask for 1, microsoft must
still be reached. RED on 'scanned += per_backend', GREEN on the actual-count
fix. Also trims the 4-line WHY comment to one line per the comment guidance.
@itomek

itomek commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed in dfe1b1d6 (test-only).

You're right the prior test was green on both the old (scanned += per_backend) and new (scanned += actual) code, so it didn't fail-first. But the suggested 3-backend variant can't expose it either: while max_messages ≥ n_backends, the over-count can never trip the early-exit, because (N-1)·⌊max/N⌋ < max always holds — so no later backend is ever skipped regardless of the counter.

The behavior only diverges when max_messages < n_backends: then per_backend floors to 1 and an under-filled earlier backend lets the old over-count cross the budget early. The new test hits exactly that, with GAIA's real two backends:

  • google inbox empty (0 messages), microsoft has 1, pre_scan(max_messages=1)per_backend = max(1, 1//2) = 1.
  • OLD += per_backend: empty google bumps scanned to 1 → guard trips → microsoft skipped.
  • NEW += actual: google returns 0 → scanned stays 0 → microsoft scanned.
  • Asserts microsoft is present. Verified RED on the reverted pre-fix code (assert 'microsoft' in set() → 1 failed), GREEN on the fix.

Renamed to test_pre_scan_under_budget_backend_not_skipped_when_earlier_backend_underfills. Also trimmed the 4-line WHY comment at agent.py to one line per the 🟢 note.

@itomek itomek enabled auto-merge June 11, 2026 23:54
@itomek itomek added this pull request to the merge queue Jun 12, 2026
Merged via the queue into main with commit b14e09d Jun 12, 2026
50 of 51 checks passed
@itomek itomek deleted the feat/1603-review-followups branch June 12, 2026 00:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

tests Test changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants