Skip to content

fix(email): mail search stops broadening on wrong results, not just on no results #4156

Description

@itomek

Problem

Ask the flagship for a message whose wording you don't remember and it can still hand you a confident wrong answer. PR #4148 fixed the case where the search comes back empty — the tool now retries progressively broader queries. But it only does that when the result count is zero. A query that returns two or three wrong messages looks like a success, so the ladder never runs, the model answers from those messages, and the user is told about mail they never asked about.

Live on a real Gmail mailbox (1137 messages), "sign off on a contract schedule before the end of the week" failed both runs. The message is there — Re: Fieldstone MSA — counter-signature needed before Friday — and searching counter-signature or Fieldstone returns it instantly, so this is the agent layer, not the mailbox and not the connector.

This is the non-empty-but-wrong half of #4134, which recorded only the zero-result half. #4148 genuinely fixes that half (two of three recollection questions now pass on first try) and was not scoped to this one. Consequence for the gate: #4074's "Find" acceptance criterion is still not satisfied after #4148 merges.

Part of #3814. Related: #4134, #4148, #4074.
Spec: none: searched docs/spec/ and docs/plans/ for a flagship mail-retrieval contract — none governs search behaviour; the acceptance contract is the "Find" criterion in #4074.

Outcome

A recollection question whose wording shares no term with the target message either finds the message or says plainly that it didn't — never a confident answer built from unrelated mail.

Acceptance criteria

  • Given "asking me to sign off on a contract schedule before the end of the week" on a mailbox containing Re: Fieldstone MSA — counter-signature needed before Friday, the flagship's answer names that thread — today it answers from 2–3 unrelated messages ~7 months older
  • search_email no longer treats any non-empty result as final: for a query whose first rung returns a small, low-relevance set, the returned attempts array has more than one entry (today: exactly one, because the loop breaks on the first non-empty rung)
  • A non-empty rung that is still judged unusable is reported as such rather than silently accepted — the payload carries a note the model can act on, in the same shape as today's zero-result note
  • Results are deduped per thread: search_email("counter-signature", limit=5) returns 5 distinct threads, not 5 messages from one thread (today: one thread consumes every slot)
  • Unit tests assert the above two payload shapes directly (multi-rung attempts for a weak first rung; one hit per thread for a multi-message thread), with the mail backend faked
  • Live re-run of the three fix(email): the flagship cannot find mail you describe in your own words #4134 recollection questions on a real mailbox: 3 of 3 return the correct thread (today: 2 of 3)

Scope & expectations

  • Where: src/gaia/agents/tools/email_tools.py — search_email (the for candidate in _broadening_ladder(...) loop) and _broadening_ladder
  • Out of scope: the Gmail/Outlook connectors and the mail backends — both return the target thread correctly on a short query; the per-turn read budget (fix(email): bound mail reads per turn, not just per message #4111)
  • Constraints: a longer ladder alone cannot fix this. Term-dropping has no path from contract schedule to counter-signature — there is no shared term to drop down to. Closing this needs a different mechanism (semantic/synonym expansion, or letting the model reformulate from what it already knows about the thread), not more rungs.

How to verify

CLI/agent surface on a machine with a connected Gmail account and the flagship granted. Run the three #4134 recollection questions and show, for each, the query the agent sent, the attempts array, and the thread it named. All three must name the right thread.

🔍 Technical details

Claim class: reproduced (executed) — relayed. The run below was executed on real hardware by another session; this issue relays its evidence and does not re-derive it. The code citations here were opened and checked against the tree at the tested SHA.

Tested at 572e8b79171ce836a712ee4088c3adb862bb42b0 (head of PR #4148, branch issue-4134), flagship run from source, Acer Swift (Ryzen AI 9 465, Radeon 880M, Ubuntu 24.04), live Gmail, 1137 inbox / 447 unread. Full evidence: #4148 (comment)

Question Result
"argument over cameras police departments use to watch streets" broadened to cameras police, 1 hit, correct thread, first try — PASS
"a newsletter about Flock cameras" Flock cameras newsletter, 1 hit, correct thread, first try — PASS
"sign off on a contract schedule before the end of the week" FAIL, 2 of 2 runs — target thread never returned

Root cause — src/gaia/agents/tools/email_tools.py:441-447:

for candidate in _broadening_ladder(query) or [query]:
    messages = mixin._email_call("search", candidate, limit=_clamp(limit))
    attempts.append({"query": candidate, "count": len(messages)})
    if messages:
        break

The first rung is the query as sent. Any non-empty result ends the loop, so the ladder is unreachable for every query that matches the wrong mail. Only count == 0 reaches rung two.

Compounding half — in the one run where broadening did fire (0 hits on the full query → 11 hits on contract schedule), it stopped on a non-empty but wrong rung, and the payload's broadened note told the model the match was approximate. The model answered from it anyway.

Third contributor — search_email returns per-message hits with no thread grouping (grep -n thread src/gaia/agents/tools/email_tools.py finds only a docstring mention). Searching counter-signature spent all 5 result slots on one conversation, which makes every broadened rung less useful than its limit suggests.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingp0high priority

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions