Skip to content

fix(email-agent): search_messages returns no answer at all for a long-bodied sender — context overflow #2763

Description

@itomek

Problem

Ask the email agent about a sender whose messages have long bodies and it returns no answer at all — not a wrong answer, nothing. The search succeeds, the results come back, and the model then blows the context window and emits a canned apology.

Verbatim, every time:

I had to trim the conversation to fit my memory but I'm still not making progress. Could you re-ask in a fresh chat with just the essentials?

Reproduced independently on two machines and two operating systems, so this is not platform-specific. Both GPU-only and Gmail-only, both Gemma-4-E4B-it-GGUF at ctx 65536 (runtime-resolved, verified — not the configured value):

box probe true runs result
macOS / Metal from:Every newer_than:14d, both phrasings 15 2/2 tool succeeded, then the fallback. No answer.
Linux / vulkan from:Every, 3 REST + 3 TUI 15 6/6 stated 0, enumerated 0. No answer.

The tool call itself is fine in every run — tool_end success=true, ~1.8s, results returned. Determinism is total: 8 of 8 across both boxes, zero variance.

Root cause path (src/gaia/agents/base/agent.py:3758 streaming, :3907 non-streaming): the LLM call raises, is_ctx_overflow is true, the loop calls _shrink_messages_for_overflow(messages) and retries exactly once (_retried_after_trim_stream), overflows again, and emits that string as final_answer. The fixed single retry is what makes the failure perfectly deterministic — a payload-size cliff alone would show variance near the boundary.

The mechanism underneath is NOT yet measured, and that is the first task here. search_messages sizes its payload via _format_messages_within_budget against envelope_budget_tokens(ctx_size=active_profile_ctx_size()) — 65536 minus the fixed prompt and response reserve — and can shrink bodies to a 200-char floor per message (THREAD_MIN_PER_MESSAGE_CHARS, hub/agents/email/python/gaia_agent_email/tools/read_tools.py:93). Fifteen messages at that floor is ~3K chars, which fits trivially. So either the shrinker is not firing on this path, or the envelope budget ignores what the rest of the conversation already occupies. Do not assume which — measure it.

Two contributing factors worth carrying into the fix:

  1. search_messages ships full message bodiesDEFAULT_BODY_LIMIT_CHARS = 4000 per message (read_tools.py:79) — for questions like "how many emails from X" that need zero body bytes. The bodies are the entire payload cost.
  2. The model widens its own request. In the macOS runs it chose max_results of 100 and 50, not the 25 default, because the phrasing implied breadth ("all messages", "how many"). It asks for more than it can fit.

Part of #2511 (surfaced through T5). Distinct from #2756 / #2760, which fix the wrong count case — a precomputed count cannot help a turn that never reaches the answering stage. #2760 states this boundary explicitly rather than claiming T5 fixed.

Outcome

A counting or listing question against a long-bodied sender returns a real answer instead of the context-overflow apology, on the same GPU/Gmail conditions where it currently fails 8 of 8.

Acceptance criteria

  • Root cause measured and stated before any fix. For the failing probe, record the serialized envelope size in chars and estimated tokens, whether _format_messages_within_budget fired, the per-message body limit it settled on, and the budget it computed. Expected artifact: a comment on this issue naming which of the two hypotheses holds — shrinker did not fire or budget ignores existing conversation occupancy — with the numbers behind it.
  • The failing probe answers. Given from:Every newer_than:14d (true 15, Gmail-only, GPU) → the agent states 15, on 3 consecutive runs, with no occurrence of the string "I had to trim the conversation". Today: 0 of 8 runs answered.
  • A metadata-only path exists for counting questions — a counting/listing ask must not pull 4000-char bodies. Expected: for the same probe, the tool payload contains no message body content, and the envelope size drops by at least an order of magnitude versus today's measured value from criterion 1.
  • The no-answer path is never silent about why. If a turn still cannot fit, the user-facing text names the actual constraint and a next step, not a generic "re-ask with just the essentials". Expected: the message states the result set was too large and suggests narrowing the query (e.g. a shorter date range).
  • Unit tests cover the payload-size guard at the registered-tool layer: a seeded backend returning N long-bodied messages produces an envelope under the computed budget. Assert the envelope size, not just that the call returned.
  • Live-inference check on GPU + Gmail, since the defect is model-context behaviour that unit tests cannot reach. Record the runtime-resolved model id and device, the model's chosen max_results, and the verbatim answer for each of the 3 runs.

Scope & expectations

How to verify

CLI/agent surface, 3 runs of the currently-failing probe:

gaia tui chat --agent email --query 'how many emails from Every in the last two weeks?'

Attach per run: the verbatim answer, the tool_args the model chose (including max_results), the measured envelope size, and the runtime-resolved model id + device. Plus the ground-truth count from POST /v1/email/search for the same query string.

Metadata

Metadata

Assignees

No one assigned

    Labels

    agent::emailEmail agent changesbugSomething isn't workingp0high priority

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions